Skip to content

Commit

Permalink
feat: rune holders
Browse files Browse the repository at this point in the history
  • Loading branch information
rafaelcr committed Jun 26, 2024
1 parent 7aed218 commit 867724e
Show file tree
Hide file tree
Showing 6 changed files with 109 additions and 15 deletions.
40 changes: 39 additions & 1 deletion api/src/api/routes/etchings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Value } from '@sinclair/typebox/value';
import { FastifyPluginCallback } from 'fastify';
import { Server } from 'http';
import {
BalanceResponseSchema,
EtchingActivityResponseSchema,
EtchingParamSchema,
EtchingResponseSchema,
Expand All @@ -12,7 +13,11 @@ import {
OffsetParamSchema,
PaginatedResponse,
} from '../schemas';
import { parseEtchingActivityResponse, parseEtchingResponse } from '../util/helpers';
import {
parseBalanceResponse,
parseEtchingActivityResponse,
parseEtchingResponse,
} from '../util/helpers';

export const EtchingRoutes: FastifyPluginCallback<
Record<never, never>,
Expand Down Expand Up @@ -111,5 +116,38 @@ export const EtchingRoutes: FastifyPluginCallback<
}
);

fastify.get(
'/etchings/:etching/holders',
{
schema: {
operationId: 'getRuneHolders',
summary: 'Rune holders',
description: 'Retrieves a paginated list of holders for a Rune',
tags: ['Runes'],
params: Type.Object({
etching: EtchingParamSchema,
}),
querystring: Type.Object({
offset: Type.Optional(OffsetParamSchema),
limit: Type.Optional(LimitParamSchema),
}),
response: {
200: PaginatedResponse(BalanceResponseSchema, 'Paginated holders response'),
},
},
},
async (request, reply) => {
const offset = request.query.offset ?? 0;
const limit = request.query.limit ?? 20;
const results = await fastify.db.getRuneHolders(request.params.etching, offset, limit);
await reply.send({
limit,
offset,
total: results.total,
results: results.results.map(r => parseBalanceResponse(r)),
});
}
);

done();
};
19 changes: 14 additions & 5 deletions api/src/api/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,12 +105,14 @@ export const EtchingResponseSchema = Type.Object({
});
export type EtchingResponse = Static<typeof EtchingResponseSchema>;

const RuneDetailResponseSchema = Type.Object({
id: Type.String({ examples: ['840000:1'] }),
name: Type.String({ examples: ['ZZZZZFEHUZZZZZ'] }),
spaced_name: Type.String({ examples: ['Z•Z•Z•Z•Z•FEHU•Z•Z•Z•Z•Z'] }),
});

export const EtchingActivityResponseSchema = Type.Object({
rune: Type.Object({
id: Type.String({ examples: ['840000:1'] }),
name: Type.String({ examples: ['ZZZZZFEHUZZZZZ'] }),
spaced_name: Type.String({ examples: ['Z•Z•Z•Z•Z•FEHU•Z•Z•Z•Z•Z'] }),
}),
rune: RuneDetailResponseSchema,
block_height: Type.Integer({ examples: [840000] }),
tx_index: Type.Integer({ examples: [1] }),
tx_id: Type.String({
Expand All @@ -135,6 +137,13 @@ export const EtchingActivityResponseSchema = Type.Object({
});
export type EtchingActivityResponse = Static<typeof EtchingActivityResponseSchema>;

export const BalanceResponseSchema = Type.Object({
rune: RuneDetailResponseSchema,
address: Type.Optional(Type.String({ examples: ['bc1q7jd477wc5s88hsvenr0ddtatsw282hfjzg59wz'] })),
balance: Type.String({ examples: ['11000000000'] }),
});
export type BalanceResponse = Static<typeof BalanceResponseSchema>;

export const NotFoundResponse = Type.Object(
{
error: Type.Literal('Not found'),
Expand Down
18 changes: 15 additions & 3 deletions api/src/api/util/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import BigNumber from 'bignumber.js';
import { DbRune, DbLedgerEntryWithRune } from '../../pg/types';
import { EtchingResponse, EtchingActivityResponse } from '../schemas';
import { DbBalance, DbItemWithRune, DbLedgerEntry, DbRune } from '../../pg/types';
import { EtchingResponse, EtchingActivityResponse, BalanceResponse } from '../schemas';

function divisibility(num: string, decimals: number): string {
return new BigNumber(num).shiftedBy(-1 * decimals).toFixed(decimals);
Expand Down Expand Up @@ -36,7 +36,7 @@ export function parseEtchingResponse(rune: DbRune): EtchingResponse {
}

export function parseEtchingActivityResponse(
entry: DbLedgerEntryWithRune
entry: DbItemWithRune<DbLedgerEntry>
): EtchingActivityResponse {
return {
rune: {
Expand All @@ -56,3 +56,15 @@ export function parseEtchingActivityResponse(
amount: divisibility(entry.amount, entry.divisibility),
};
}

export function parseBalanceResponse(item: DbItemWithRune<DbBalance>): BalanceResponse {
return {
rune: {
id: item.rune_id,
name: item.name,
spaced_name: item.spaced_name,
},
address: item.address,
balance: divisibility(item.balance, item.divisibility),
};
}
34 changes: 30 additions & 4 deletions api/src/pg/pg-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,14 @@ import {
connectPostgres,
} from '@hirosystems/api-toolkit';
import { ENV } from '../env';
import { DbCountedQueryResult, DbLedgerEntryWithRune, DbPaginatedResult, DbRune } from './types';
import {
DbBalance,
DbCountedQueryResult,
DbItemWithRune,
DbLedgerEntry,
DbPaginatedResult,
DbRune,
} from './types';
import {
EtchingParam,
LimitParam,
Expand Down Expand Up @@ -78,9 +85,9 @@ export class PgStore extends BasePgStore {
id: EtchingParam,
offset: OffsetParam,
limit: LimitParam
): Promise<DbPaginatedResult<DbLedgerEntryWithRune>> {
const results = await this.sql<DbCountedQueryResult<DbLedgerEntryWithRune>[]>`
SELECT l.*, r.name, r.spaced_name, r.divisibility, COUNT(*) OVER() AS total
): Promise<DbPaginatedResult<DbItemWithRune<DbLedgerEntry>>> {
const results = await this.sql<DbCountedQueryResult<DbItemWithRune<DbLedgerEntry>>[]>`
SELECT l.*, r.name, r.spaced_name, r.divisibility, r.total_operations AS total
FROM ledger AS l
INNER JOIN runes AS r ON r.id = l.rune_id
WHERE ${getEtchingIdWhereCondition(this.sql, id, 'r')}
Expand All @@ -92,4 +99,23 @@ export class PgStore extends BasePgStore {
results,
};
}

async getRuneHolders(
id: EtchingParam,
offset: OffsetParam,
limit: LimitParam
): Promise<DbPaginatedResult<DbItemWithRune<DbBalance>>> {
const results = await this.sql<DbCountedQueryResult<DbItemWithRune<DbBalance>>[]>`
SELECT b.*, r.name, r.spaced_name, r.divisibility, COUNT(*) OVER() AS total
FROM balances AS b
INNER JOIN runes AS r ON r.id = b.rune_id
WHERE ${getEtchingIdWhereCondition(this.sql, id, 'r')}
ORDER BY b.balance DESC
OFFSET ${offset} LIMIT ${limit}
`;
return {
total: results[0]?.total ?? 0,
results,
};
}
}
11 changes: 10 additions & 1 deletion api/src/pg/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export type DbRune = {
total_mints: number;
burned: string;
total_burns: number;
total_operations: number;
timestamp: number;
};

Expand All @@ -45,8 +46,16 @@ export type DbLedgerEntry = {
timestamp: number;
};

export type DbLedgerEntryWithRune = DbLedgerEntry & {
export type DbItemWithRune<T> = T & {
name: string;
spaced_name: string;
divisibility: number;
total_operations: number;
};

export type DbBalance = {
rune_id: string;
address: string;
balance: string;
total_operations: number;
};
2 changes: 1 addition & 1 deletion migrations/V2__ledger.sql
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ CREATE TABLE IF NOT EXISTS ledger (
rune_id TEXT NOT NULL,
block_height NUMERIC NOT NULL,
tx_index BIGINT NOT NULL,
tx_id TEXT NOT NULL,
event_index BIGINT NOT NULL,
tx_id TEXT NOT NULL,
output BIGINT,
address TEXT,
receiver_address TEXT,
Expand Down

0 comments on commit 867724e

Please sign in to comment.