Skip to content
Open
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
30c1be6
feat: add protocol support for register, transfer and accept
jeronimoalbi Nov 12, 2025
ad49c3b
feat: add API endpoints for register, transfer and accept
jeronimoalbi Nov 12, 2025
cd6162d
feat: change handle register to remove previous handle
jeronimoalbi Nov 13, 2025
32bada1
fix: correct issue in reader that led to always start from 1st block
jeronimoalbi Nov 13, 2025
58aeae0
chore: remove one API URL from reader Docker compose file
jeronimoalbi Nov 13, 2025
d964956
feat: remove prepared statements and change code for consistency
jeronimoalbi Nov 13, 2025
754e0f5
fix: remove previous handle transfer if it exists
jeronimoalbi Nov 13, 2025
9cdf188
feat: support search posts by user handle
jeronimoalbi Nov 17, 2025
9a468eb
feat: add user handle to feed, follow and post endpoints
jeronimoalbi Nov 17, 2025
6b50ff7
feat: add user handle to posts endpoint
jeronimoalbi Nov 17, 2025
62bcb9b
feat: add user handle display support protocol
jeronimoalbi Nov 17, 2025
50c3dd4
feat: add display field to API endpoints
jeronimoalbi Nov 17, 2025
39c2632
feat: add notification for registration of a user handle already taken
jeronimoalbi Nov 18, 2025
e356f0a
feat: add API endpoint to get user handle by name or address
jeronimoalbi Nov 18, 2025
8464e85
refactor: change protocol names to be more explicit
jeronimoalbi Nov 19, 2025
f9cf2f7
chore: use uppercase for global consts
jeronimoalbi Nov 19, 2025
9447752
refactor: change HandleTable name to AccountTable
jeronimoalbi Nov 19, 2025
bf956b9
refactor: update user handle protocol features for account table
jeronimoalbi Nov 20, 2025
26ef719
feat: add notification when a handle is transfered to a user
jeronimoalbi Nov 20, 2025
8e4e07c
Merge branch 'main' into feat/username-handle-support
jeronimoalbi Nov 24, 2025
f576d6b
fix: change Tiltfile to reload on code changes
jeronimoalbi Nov 24, 2025
ff9eb48
chore: change reader docker compose to use testnet API by default
jeronimoalbi Nov 24, 2025
eb2fef3
chore: update dockerignore
jeronimoalbi Nov 25, 2025
25e0350
chore: allow requests to `https://*.allinbits.services`
jeronimoalbi Nov 25, 2025
1a9cc2f
feat: change UI to render user handle when available
jeronimoalbi Nov 25, 2025
4ea2887
feat: add tooltip to user address or handle
jeronimoalbi Nov 25, 2025
a94360d
refactor: change handle API endpoint to account
jeronimoalbi Nov 25, 2025
278c79f
fix: correct issue with account API endpoint
jeronimoalbi Nov 25, 2025
29025e3
chore: reduce handle length to 25 characters
jeronimoalbi Nov 26, 2025
58fb139
feat: add `useAcount()` composable
jeronimoalbi Nov 26, 2025
5686a5b
feat: add user handle to profile view and wallet connect button
jeronimoalbi Nov 26, 2025
df24fcf
chore: add missing import
jeronimoalbi Nov 26, 2025
ce50231
fix: correct following API endpoint issue with user handles
jeronimoalbi Nov 26, 2025
60ff6c2
feat: add views and dialogs to register a new account handle
jeronimoalbi Nov 27, 2025
8017c27
fix: correct TS issues
jeronimoalbi Nov 27, 2025
2d589da
feat: change frontend to validate account handle registration
jeronimoalbi Nov 27, 2025
1462ac5
chore: add handle name to the registration error notification
jeronimoalbi Nov 29, 2025
6eedb03
chore: remove error when `useAccount()` fails to find an account
jeronimoalbi Nov 29, 2025
090030d
feat: change handle to appear below account addresses
jeronimoalbi Nov 29, 2025
4b2016f
fix: correct TS issues
jeronimoalbi Nov 29, 2025
f04a21b
feat: allow configuring the required fee to register a handle
jeronimoalbi Nov 29, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions packages/api-main/drizzle/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ import pg from 'pg';

dotenv.config();

export type DbClient = ReturnType<typeof drizzle>;

const { Pool } = pg;

let db: ReturnType<typeof drizzle>;
let db: DbClient;

export function getDatabase() {
export function getDatabase(): DbClient {
if (!db) {
const client = new Pool({
connectionString: process.env.PG_URI!,
Expand Down
40 changes: 39 additions & 1 deletion packages/api-main/drizzle/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,35 @@ import { bigint, boolean, index, integer, pgEnum, pgTable, primaryKey, serial, t

const MEMO_LENGTH = 512;

export const HandleTable = pgTable(
'handle',
{
name: varchar({ length: 32 }).primaryKey(),
address: varchar({ length: 44 }).notNull(),
display: varchar({ length: 128 }),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll also need to store the profile picture (and possibly other new fields in the future, see #491), so I was thinking we could create an account/profile table instead of one specifically for the handle. Wdyt?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@luciorubeens handle table was renamed to account in 9447752

Protocol related code was refactored to work with the new table in bf956b9
Changes added more complexity to the code but I guess it will make it easier for the new user related features to be implemented.

hash: varchar({ length: 64 }).notNull(),
timestamp: timestamp({ withTimezone: true }).notNull(),
},
t => [
index('handle_address_idx').on(t.address),
index('handle_display_idx').on(t.display),
],
);

export const HandleTransferTable = pgTable(
'handle_transfer',
{
hash: varchar({ length: 64 }).notNull(),
name: varchar({ length: 32 }).notNull(),
from_address: varchar({ length: 44 }).notNull(),
to_address: varchar({ length: 44 }).notNull(),
timestamp: timestamp({ withTimezone: true }).notNull(),
},
t => [
index('handle_transfer_to_idx').on(t.name, t.to_address),
],
);

export const FeedTable = pgTable(
'feed',
{
Expand Down Expand Up @@ -129,7 +158,14 @@ export const ModeratorTable = pgTable('moderators', {
deleted_at: timestamp({ withTimezone: true }),
});

export const notificationTypeEnum = pgEnum('notification_type', ['like', 'dislike', 'flag', 'follow', 'reply']);
export const notificationTypeEnum = pgEnum('notification_type', [
'like',
'dislike',
'flag',
'follow',
'reply',
'registerHandle',
]);

export const NotificationTable = pgTable(
'notifications',
Expand Down Expand Up @@ -166,4 +202,6 @@ export const tables = [
'state',
'authrequests',
'ratelimits',
'handle',
'handle_transfer',
];
11 changes: 8 additions & 3 deletions packages/api-main/src/gets/feed.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import type { Gets } from '@atomone/dither-api-types';

import { and, count, desc, gte, isNull, sql } from 'drizzle-orm';
import { and, count, desc, eq, getTableColumns, gte, isNull, sql } from 'drizzle-orm';

import { getDatabase } from '../../drizzle/db';
import { FeedTable } from '../../drizzle/schema';
import { FeedTable, HandleTable } from '../../drizzle/schema';

const statement = getDatabase()
.select()
.select({
...getTableColumns(FeedTable),
handle: HandleTable.name,
display: HandleTable.display,
})
.from(FeedTable)
.leftJoin(HandleTable, eq(FeedTable.author, HandleTable.address))
.limit(sql.placeholder('limit'))
.offset(sql.placeholder('offset'))
.where(
Expand Down
10 changes: 8 additions & 2 deletions packages/api-main/src/gets/followers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,17 @@ import type { Gets } from '@atomone/dither-api-types';
import { and, desc, eq, isNull, sql } from 'drizzle-orm';

import { getDatabase } from '../../drizzle/db';
import { FollowsTable } from '../../drizzle/schema';
import { FollowsTable, HandleTable } from '../../drizzle/schema';

const statementGetFollowers = getDatabase()
.select({ address: FollowsTable.follower, hash: FollowsTable.hash })
.select({
address: FollowsTable.follower,
handle: HandleTable.name,
display: HandleTable.display,
hash: FollowsTable.hash,
})
.from(FollowsTable)
.leftJoin(HandleTable, eq(HandleTable.address, FollowsTable.follower))
.where(and(eq(FollowsTable.following, sql.placeholder('following')), isNull(FollowsTable.removed_at)))
.limit(sql.placeholder('limit'))
.offset(sql.placeholder('offset'))
Expand Down
10 changes: 8 additions & 2 deletions packages/api-main/src/gets/following.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,17 @@ import type { Gets } from '@atomone/dither-api-types';
import { and, desc, eq, isNull, sql } from 'drizzle-orm';

import { getDatabase } from '../../drizzle/db';
import { FollowsTable } from '../../drizzle/schema';
import { FollowsTable, HandleTable } from '../../drizzle/schema';

const statementGetFollowing = getDatabase()
.select({ address: FollowsTable.following, hash: FollowsTable.hash })
.select({
address: FollowsTable.following,
handle: HandleTable.name,
display: HandleTable.display,
hash: FollowsTable.hash,
})
.from(FollowsTable)
.leftJoin(HandleTable, eq(HandleTable.address, FollowsTable.follower))
.where(and(eq(FollowsTable.follower, sql.placeholder('follower')), isNull(FollowsTable.removed_at)))
.limit(sql.placeholder('limit'))
.offset(sql.placeholder('offset'))
Expand Down
42 changes: 42 additions & 0 deletions packages/api-main/src/gets/handle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import type { Gets } from '@atomone/dither-api-types';

import { eq, or, sql } from 'drizzle-orm';

import { getDatabase } from '../../drizzle/db';
import { HandleTable } from '../../drizzle/schema';

const statement = getDatabase()
.select({
name: HandleTable.name,
address: HandleTable.address,
display: HandleTable.display,
})
.from(HandleTable)
.where(
or(
eq(HandleTable.name, sql.placeholder('name')),
eq(HandleTable.address, sql.placeholder('address')),
),
)
.orderBy(HandleTable.name)
.limit(1)
.prepare('stmnt_get_handle');

export async function Handle(query: Gets.HandleQuery) {
const { address, name } = query;
if (!address && !name) {
return { status: 400, error: 'handle name or address is required' };
}

try {
const [handle] = await statement.execute({ address, name });
if (!handle) {
return { status: 404, rows: [] };
}

return { status: 200, rows: [handle] };
} catch (error) {
console.error(error);
return { error: 'failed to read data from database' };
}
}
1 change: 1 addition & 0 deletions packages/api-main/src/gets/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export * from './feed';
export * from './flags';
export * from './followers';
export * from './following';
export * from './handle';
export * from './health';
export * from './isFollowing';
export * from './lastBlock';
Expand Down
17 changes: 13 additions & 4 deletions packages/api-main/src/gets/post.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,28 @@
import type { Gets } from '@atomone/dither-api-types';

import { and, eq, isNull, sql } from 'drizzle-orm';
import { and, eq, getTableColumns, isNull, sql } from 'drizzle-orm';

import { getDatabase } from '../../drizzle/db';
import { FeedTable } from '../../drizzle/schema';
import { FeedTable, HandleTable } from '../../drizzle/schema';

const statementGetPost = getDatabase()
.select()
.select({
...getTableColumns(FeedTable),
handle: HandleTable.name,
display: HandleTable.display,
})
.from(FeedTable)
.leftJoin(HandleTable, eq(FeedTable.author, HandleTable.address))
.where(and(isNull(FeedTable.removed_at), eq(FeedTable.hash, sql.placeholder('hash'))))
.prepare('stmnt_get_post');

const statementGetReply = getDatabase()
.select()
.select({
...getTableColumns(FeedTable),
handle: HandleTable.name,
})
.from(FeedTable)
.leftJoin(HandleTable, eq(FeedTable.author, HandleTable.address))
.where(and(isNull(FeedTable.removed_at), eq(FeedTable.hash, sql.placeholder('hash')), eq(FeedTable.post_hash, sql.placeholder('post_hash'))))
.prepare('stmnt_get_reply');

Expand Down
11 changes: 8 additions & 3 deletions packages/api-main/src/gets/posts.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import type { Gets } from '@atomone/dither-api-types';

import { and, desc, eq, gte, inArray, isNull, sql } from 'drizzle-orm';
import { and, desc, eq, getTableColumns, gte, inArray, isNull, sql } from 'drizzle-orm';

import { getDatabase } from '../../drizzle/db';
import { FeedTable, FollowsTable } from '../../drizzle/schema';
import { FeedTable, FollowsTable, HandleTable } from '../../drizzle/schema';

const statement = getDatabase()
.select()
.select({
...getTableColumns(FeedTable),
handle: HandleTable.name,
display: HandleTable.display,
})
.from(FeedTable)
.leftJoin(HandleTable, eq(FeedTable.author, HandleTable.address))
.where(
and(
eq(FeedTable.author, sql.placeholder('author')),
Expand Down
26 changes: 20 additions & 6 deletions packages/api-main/src/gets/search.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { Gets } from '@atomone/dither-api-types';

import { and, desc, gte, ilike, inArray, isNull, or, sql } from 'drizzle-orm';
import { and, desc, eq, getTableColumns, gte, ilike, inArray, isNull, or, sql } from 'drizzle-orm';

import { getDatabase } from '../../drizzle/db';
import { FeedTable } from '../../drizzle/schema';
import { FeedTable, HandleTable } from '../../drizzle/schema';

export async function Search(query: Gets.SearchQuery) {
try {
Expand All @@ -22,12 +22,26 @@ export async function Search(query: Gets.SearchQuery) {
const matchedAuthors = await getDatabase()
.selectDistinct({ author: FeedTable.author })
.from(FeedTable)
.where(and(ilike(FeedTable.author, `%${query.text}%`), isNull(FeedTable.removed_at)));
.leftJoin(HandleTable, eq(FeedTable.author, HandleTable.address))
.where(
and(
or(
eq(FeedTable.author, query.text.toLowerCase()), // Exact address
ilike(HandleTable.name, `%${query.text}%`), // Registered handle (partial match)
),
isNull(FeedTable.removed_at),
),
);
const matchedAuthorAddresses = matchedAuthors.map(a => a.author);

const matchedPosts = await getDatabase()
.select()
.select({
...getTableColumns(FeedTable),
handle: HandleTable.name,
display: HandleTable.display,
})
.from(FeedTable)
.leftJoin(HandleTable, eq(FeedTable.author, HandleTable.address))
.where(
and(
or(
Expand All @@ -40,8 +54,8 @@ export async function Search(query: Gets.SearchQuery) {
)
.limit(100)
.offset(0)
.orderBy(desc(FeedTable.timestamp))
.execute();
.orderBy(desc(FeedTable.timestamp));

return { status: 200, rows: [...matchedPosts], users: matchedAuthorAddresses };
} catch (error) {
console.error(error);
Expand Down
36 changes: 36 additions & 0 deletions packages/api-main/src/posts/acceptHandle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type { Posts } from '@atomone/dither-api-types';

import type { DbClient } from '../../drizzle/db';

import { and, eq } from 'drizzle-orm';

import { getDatabase } from '../../drizzle/db';
import { HandleTable, HandleTransferTable } from '../../drizzle/schema';
import { lower } from '../utility';

export async function AcceptHandle(body: Posts.AcceptHandleBody) {
const db = getDatabase();
try {
if (!await doesTransferToAddressExists(db, body.handle, body.to_address)) {
return { status: 400, error: 'handle not found or not transferred to address' };
}

await db
.update(HandleTable)
.set({ address: body.to_address.toLowerCase() })
.where(eq(lower(HandleTable.name), body.handle.toLowerCase()));

return { status: 200 };
} catch (err) {
console.error(err);
return { status: 400, error: 'failed to transfer handle' };
}
}

async function doesTransferToAddressExists(db: DbClient, handle: string, address: string): Promise<boolean> {
const count = await db.$count(HandleTransferTable, and(
eq(lower(HandleTransferTable.name), handle.toLowerCase()),
eq(HandleTransferTable.to_address, address.toLowerCase()),
));
return count !== 0;
}
44 changes: 44 additions & 0 deletions packages/api-main/src/posts/displayHandle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import type { Posts } from '@atomone/dither-api-types';

import { count, eq, sql } from 'drizzle-orm';

import { getDatabase } from '../../drizzle/db';
import { HandleTable } from '../../drizzle/schema';
import { lower } from '../utility';
import { MAX_DISPLAY_LENGTH } from './registerHandle';

const handleAddressExistsStmt = getDatabase()
.select({ count: count() })
.from(HandleTable)
.where(eq(lower(HandleTable.address), sql.placeholder('address')))
.prepare('stmt_handle_address_exists');

export async function DisplayHandle(body: Posts.DisplayHandleBody) {
const display = (body.display || '').trim();
if (display.length > MAX_DISPLAY_LENGTH) {
return { status: 400, error: `maximum display length is ${MAX_DISPLAY_LENGTH} characters long` };
}

const db = getDatabase();
try {
const address = body.from.toLowerCase();
if (!await hasHandle(address)) {
return { status: 400, error: 'account requires a handle to set a display text' };
}

await db
.update(HandleTable)
.set({ display })
.where(eq(HandleTable.address, address));

return { status: 200 };
} catch (err) {
console.error(err);
return { status: 400, error: 'failed to update display handle' };
}
}

async function hasHandle(address: string): Promise<boolean> {
const [result] = await handleAddressExistsStmt.execute({ address });
return (result?.count ?? 0) !== 0;
}
4 changes: 4 additions & 0 deletions packages/api-main/src/posts/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
export * from './acceptHandle';
export * from './auth';
export * from './authCreate';
export * from './dislike';
export * from './displayHandle';
export * from './flag';
export * from './follow';
export * from './like';
export * from './logout';
export * from './mod';
export * from './post';
export * from './postRemove';
export * from './registerHandle';
export * from './reply';
export * from './transferHandle';
export * from './unfollow';
export * from './updateState';
Loading