Skip to content

Commit

Permalink
feat: add BRC-20 event history/activity ingestion and endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
janniks committed Aug 25, 2023
1 parent eb6e7fd commit ed3d63a
Show file tree
Hide file tree
Showing 9 changed files with 737 additions and 136 deletions.
29 changes: 29 additions & 0 deletions migrations/1692891772000_brc20-events-types.ts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/* eslint-disable @typescript-eslint/naming-convention */
import { MigrationBuilder, ColumnDefinitions } from 'node-pg-migrate';

export const shorthands: ColumnDefinitions | undefined = undefined;

export function up(pgm: MigrationBuilder): void {
// Attention: This migration assumes that the `brc20_events` table is empty, otherwise events will be lost!

pgm.addColumns('brc20_events', {
genesis_location_id: {
type: 'bigint',
references: '"locations"',
onDelete: 'CASCADE',
notNull: true,
unique: true, // only one event exists per location
},
operation: {
type: 'text', // enum-style: ['deploy', 'mint', 'prepare_transfer', 'transfer']
notNull: true,
},
});

pgm.createIndex('brc20_events', ['genesis_location_id']);
pgm.createIndex('brc20_events', ['operation']);

pgm.createIndex('brc20_events', ['brc20_deploy_id']);
pgm.createIndex('brc20_events', ['transfer_id']);
pgm.createIndex('brc20_events', ['mint_id']);
}
2 changes: 1 addition & 1 deletion src/api/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export const Api: FastifyPluginAsync<
export async function buildApiServer(args: { db: PgStore }) {
const fastify = Fastify({
trustProxy: true,
logger: PINO_LOGGER_CONFIG,
logger: { level: 'debug' },
}).withTypeProvider<TypeBoxTypeProvider>();

fastify.decorate('db', args.db);
Expand Down
47 changes: 45 additions & 2 deletions src/api/routes/brc20.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { TypeBoxTypeProvider } from '@fastify/type-provider-typebox';
import { Type } from '@sinclair/typebox';
import { Value } from '@sinclair/typebox/value';
import { FastifyPluginCallback } from 'fastify';
import { Server } from 'http';
import {
AddressParam,
BlockHeightParam,
Brc20ActivityResponseSchema,
Brc20BalanceResponseSchema,
Brc20HolderResponseSchema,
Brc20OperationsParam,
Brc20TickerParam,
Brc20TickersParam,
Brc20TokenDetailsSchema,
Expand All @@ -16,15 +19,15 @@ import {
OffsetParam,
PaginatedResponse,
} from '../schemas';
import { handleInscriptionTransfersCache } from '../util/cache';
import {
DEFAULT_API_LIMIT,
parseBrc20Activities,
parseBrc20Balances,
parseBrc20Holders,
parseBrc20Supply,
parseBrc20Tokens,
} from '../util/helpers';
import { Value } from '@sinclair/typebox/value';
import { handleInscriptionTransfersCache } from '../util/cache';

export const Brc20Routes: FastifyPluginCallback<
Record<never, never>,
Expand Down Expand Up @@ -195,5 +198,45 @@ export const Brc20Routes: FastifyPluginCallback<
}
);

fastify.get(
'/brc-20/activity',
{
schema: {
operationId: 'getBrc20Activity',
summary: 'BRC-20 Activity',
description: 'Retrieves BRC-20 activity',
tags: ['BRC-20'],
querystring: Type.Object({
ticker: Type.Optional(Brc20TickersParam),
block_height: Type.Optional(BlockHeightParam),
operation: Type.Optional(Brc20OperationsParam),
// Pagination
offset: Type.Optional(OffsetParam),
limit: Type.Optional(LimitParam),
}),
response: {
200: PaginatedResponse(Brc20ActivityResponseSchema, 'Paginated BRC-20 Activity Response'),
},
},
},
async (request, reply) => {
const limit = request.query.limit ?? DEFAULT_API_LIMIT;
const offset = request.query.offset ?? 0;
const balances = await fastify.db.brc20.getActivity({
limit,
offset,
ticker: request.query.ticker,
block_height: request.query.block_height ? parseInt(request.query.block_height) : undefined,
operation: request.query.operation,
});
await reply.send({
limit,
offset,
total: balances.total,
results: parseBrc20Activities(balances.results),
});
}
);

done();
};
70 changes: 68 additions & 2 deletions src/api/schemas.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { SwaggerOptions } from '@fastify/swagger';
import { SERVER_VERSION } from '@hirosystems/api-toolkit';
import { Static, TSchema, Type } from '@sinclair/typebox';
import { TypeCompiler } from '@sinclair/typebox/compiler';
import { SatoshiRarity, SAT_SUPPLY } from './util/ordinal-satoshi';
import { SERVER_VERSION } from '@hirosystems/api-toolkit';
import { SAT_SUPPLY, SatoshiRarity } from './util/ordinal-satoshi';

export const OpenApiSchemaOptions: SwaggerOptions = {
openapi: {
Expand Down Expand Up @@ -198,6 +198,22 @@ export const LimitParam = Type.Integer({
description: 'Results per page',
});

export const Brc20OperationParam = Type.Union(
[
Type.Literal('deploy'),
Type.Literal('mint'),
Type.Literal('prepare_transfer'),
Type.Literal('transfer'),
],
{
title: 'Operation',
description: 'BRC-20 token operation',
examples: ['deploy', 'mint', 'prepare_transfer', 'transfer'],
}
);

export const Brc20OperationsParam = Type.Array(Brc20OperationParam);

export enum OrderBy {
number = 'number',
genesis_block_height = 'genesis_block_height',
Expand Down Expand Up @@ -371,6 +387,56 @@ export const Brc20BalanceResponseSchema = Type.Object({
});
export type Brc20BalanceResponse = Static<typeof Brc20BalanceResponseSchema>;

export const Brc20ActivityResponseSchema = Type.Object({
operation: Type.Union([
Type.Literal('deploy'),
Type.Literal('mint'),
Type.Literal('prepare_transfer'),
Type.Literal('transfer'),
]),
ticker: Type.String({ examples: ['PEPE'] }),
inscription_id: Type.String({
examples: ['1463d48e9248159084929294f64bda04487503d30ce7ab58365df1dc6fd58218i0'],
}),
block_height: Type.Integer({ examples: [778921] }),
block_hash: Type.String({
examples: ['0000000000000000000452773967cdd62297137cdaf79950c5e8bb0c62075133'],
}),
tx_id: Type.String({
examples: ['1463d48e9248159084929294f64bda04487503d30ce7ab58365df1dc6fd58218'],
}),
address: Type.String({
examples: ['bc1pvwh2dl6h388x65rqq47qjzdmsqgkatpt4hye6daf7yxvl0z3xjgq247aq8'],
}),
timestamp: Type.Integer({ examples: [1677733170000] }),
mint: Type.Optional(
Type.Object({
amount: Nullable(Type.String({ examples: ['1000000'] })),
})
),
deploy: Type.Optional(
Type.Object({
max_supply: Type.String({ examples: ['21000000'] }),
mint_limit: Nullable(Type.String({ examples: ['100000'] })),
decimals: Type.Integer({ examples: [18] }),
})
),
transfer: Type.Optional(
Type.Object({
amount: Type.String({ examples: ['1000000'] }),
from_address: Type.String({
examples: ['bc1pvwh2dl6h388x65rqq47qjzdmsqgkatpt4hye6daf7yxvl0z3xjgq247aq8'],
}),
to_address: Type.Optional(
Type.String({
examples: ['bc1pvwh2dl6h388x65rqq47qjzdmsqgkatpt4hye6daf7yxvl0z3xjgq247aq8'],
})
),
})
),
});
export type Brc20ActivityResponse = Static<typeof Brc20ActivityResponseSchema>;

export const Brc20TokenResponseSchema = Type.Object(
{
id: Type.String({
Expand Down
50 changes: 49 additions & 1 deletion src/api/util/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { DbBrc20Token, DbBrc20Supply, DbBrc20Balance, DbBrc20Holder } from '../../pg/brc20/types';
import {
DbBrc20Activity,
DbBrc20Balance,
DbBrc20Holder,
DbBrc20Supply,
DbBrc20Token,
} from '../../pg/brc20/types';
import {
DbFullyLocatedInscriptionResult,
DbInscriptionLocationChange,
Expand All @@ -8,6 +14,7 @@ import {
BlockHashParamCType,
BlockHeightParamCType,
BlockInscriptionTransfer,
Brc20ActivityResponse,
Brc20BalanceResponse,
Brc20HolderResponse,
Brc20Supply,
Expand Down Expand Up @@ -138,6 +145,47 @@ export function parseBrc20Balances(items: DbBrc20Balance[]): Brc20BalanceRespons
}));
}

export function parseBrc20Activities(items: DbBrc20Activity[]): Brc20ActivityResponse[] {
return items.map(i => {
const a: Partial<Brc20ActivityResponse> = {
operation: i.operation,
address: i.address,
tx_id: i.tx_id,
timestamp: i.timestamp,
block_height: parseInt(i.block_height),
block_hash: i.block_hash,
ticker: i.ticker,
inscription_id: i.inscription_id,
};

// Typescript needs checking both `i` and `a` (even though they're the same)
if (i.operation === 'deploy' && a.operation === 'deploy') {
a.deploy = {
max_supply: i.deploy_max,
mint_limit: i.deploy_limit,
decimals: i.deploy_decimals,
};
} else if (i.operation === 'mint' && a.operation === 'mint') {
a.mint = {
amount: i.mint_amount,
};
} else if (i.operation === 'transfer' && a.operation === 'transfer') {
a.transfer = {
amount: i.transfer_amount,
from_address: i.transfer_from,
to_address: i.transfer_to,
};
} else if (i.operation === 'prepare_transfer' && a.operation === 'prepare_transfer') {
a.transfer = {
amount: i.transfer_amount,
from_address: i.transfer_from,
};
}

return a as Brc20ActivityResponse;
});
}

export function parseBrc20Holders(items: DbBrc20Holder[]): Brc20HolderResponse[] {
return items.map(i => ({
address: i.address,
Expand Down
Loading

0 comments on commit ed3d63a

Please sign in to comment.