Skip to content

Commit

Permalink
feat: add endpoint to retrieve all inscription transfers per block (#63)
Browse files Browse the repository at this point in the history
  • Loading branch information
rafaelcr committed May 10, 2023
1 parent 23e48f1 commit e1afa7d
Show file tree
Hide file tree
Showing 6 changed files with 406 additions and 9 deletions.
47 changes: 45 additions & 2 deletions src/api/routes/inscriptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,14 @@ import {
AddressesParam,
InscriptionIdsParam,
InscriptionNumbersParam,
InscriptionLocationResponse,
InscriptionLocationResponseSchema,
BlockInscriptionTransferSchema,
} from '../schemas';
import { handleInscriptionCache, handleInscriptionTransfersCache } from '../util/cache';
import {
DEFAULT_API_LIMIT,
hexToBuffer,
parseBlockTransfers,
parseDbInscription,
parseDbInscriptions,
parseInscriptionLocations,
Expand Down Expand Up @@ -144,6 +146,47 @@ const IndexRoutes: FastifyPluginCallback<Record<never, never>, Server, TypeBoxTy
}
);

fastify.get(
'/inscriptions/transfers',
{
schema: {
operationId: 'getTransfersPerBlock',
summary: 'Transfers per block',
description:
'Retrieves a list of inscription transfers that ocurred at a specific Bitcoin block',
tags: ['Inscriptions'],
querystring: Type.Object({
block: BlockParam,
// Pagination
offset: Type.Optional(OffsetParam),
limit: Type.Optional(LimitParam),
}),
response: {
200: PaginatedResponse(
BlockInscriptionTransferSchema,
'Paginated Block Transfers Response'
),
404: NotFoundResponse,
},
},
},
async (request, reply) => {
const limit = request.query.limit ?? DEFAULT_API_LIMIT;
const offset = request.query.offset ?? 0;
const transfers = await fastify.db.getTransfersPerBlock({
limit,
offset,
...blockParam(request.query.block, 'block'),
});
await reply.send({
limit,
offset,
total: transfers.total,
results: parseBlockTransfers(transfers.results),
});
}
);

done();
};

Expand Down Expand Up @@ -235,7 +278,7 @@ const ShowRoutes: FastifyPluginCallback<Record<never, never>, Server, TypeBoxTyp
}),
response: {
200: PaginatedResponse(
InscriptionLocationResponse,
InscriptionLocationResponseSchema,
'Paginated Inscription Locations Response'
),
404: NotFoundResponse,
Expand Down
14 changes: 12 additions & 2 deletions src/api/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ export const ApiStatusResponse = Type.Object(
{ title: 'Api Status Response' }
);

export const InscriptionLocationResponse = Type.Object(
export const InscriptionLocationResponseSchema = Type.Object(
{
block_height: Type.Integer({ examples: [778921] }),
block_hash: Type.String({
Expand All @@ -311,7 +311,17 @@ export const InscriptionLocationResponse = Type.Object(
},
{ title: 'Inscription Location Response' }
);
export type InscriptionLocationResponse = Static<typeof InscriptionLocationResponse>;
export type InscriptionLocationResponse = Static<typeof InscriptionLocationResponseSchema>;

export const BlockInscriptionTransferSchema = Type.Object({
id: Type.String({
examples: ['1463d48e9248159084929294f64bda04487503d30ce7ab58365df1dc6fd58218i0'],
}),
number: Type.Integer({ examples: [248751] }),
from: InscriptionLocationResponseSchema,
to: InscriptionLocationResponseSchema,
});
export type BlockInscriptionTransfer = Static<typeof BlockInscriptionTransferSchema>;

export const NotFoundResponse = Type.Object(
{
Expand Down
43 changes: 41 additions & 2 deletions src/api/util/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import { DbFullyLocatedInscriptionResult, DbLocation } from '../../pg/types';
import { InscriptionLocationResponse, InscriptionResponseType } from '../schemas';
import {
DbFullyLocatedInscriptionResult,
DbInscriptionLocationChange,
DbLocation,
} from '../../pg/types';
import {
BlockInscriptionTransfer,
InscriptionLocationResponse,
InscriptionResponseType,
} from '../schemas';

export const DEFAULT_API_LIMIT = 20;

Expand Down Expand Up @@ -48,6 +56,37 @@ export function parseInscriptionLocations(items: DbLocation[]): InscriptionLocat
}));
}

export function parseBlockTransfers(
items: DbInscriptionLocationChange[]
): BlockInscriptionTransfer[] {
return items.map(i => ({
id: i.genesis_id,
number: parseInt(i.number),
from: {
block_height: parseInt(i.from_block_height),
block_hash: i.from_block_hash,
address: i.from_address,
tx_id: i.from_tx_id,
location: `${i.from_output}:${i.from_offset}`,
output: i.from_output,
value: i.from_value,
offset: i.from_offset,
timestamp: i.from_timestamp.valueOf(),
},
to: {
block_height: parseInt(i.to_block_height),
block_hash: i.to_block_hash,
address: i.to_address,
tx_id: i.to_tx_id,
location: `${i.to_output}:${i.to_offset}`,
output: i.to_output,
value: i.to_value,
offset: i.to_offset,
timestamp: i.to_timestamp.valueOf(),
},
}));
}

/**
* Decodes a `0x` prefixed hex string to a buffer.
* @param hex - A hex string with a `0x` prefix.
Expand Down
55 changes: 52 additions & 3 deletions src/pg/pg-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
DbInscriptionIndexPaging,
DbInscriptionIndexResultCountType,
DbInscriptionInsert,
DbInscriptionLocationChange,
DbJsonContent,
DbLocation,
DbLocationInsert,
Expand Down Expand Up @@ -188,9 +189,9 @@ export class PgStore extends BasePgStore {
}

async getMaxInscriptionNumber(): Promise<number | undefined> {
const result = await this.sql<{ max: number }[]>`SELECT MAX(number) FROM inscriptions`;
const result = await this.sql<{ max: string }[]>`SELECT MAX(number) FROM inscriptions`;
if (result[0].max) {
return result[0].max;
return parseInt(result[0].max);
}
}

Expand Down Expand Up @@ -407,6 +408,54 @@ export class PgStore extends BasePgStore {
};
}

async getTransfersPerBlock(
args: { block_height?: number; block_hash?: string } & DbInscriptionIndexPaging
): Promise<DbPaginatedResult<DbInscriptionLocationChange>> {
const results = await this.sql<({ total: number } & DbInscriptionLocationChange)[]>`
WITH transfers AS (
SELECT
i.id AS inscription_id,
i.genesis_id,
i.number,
l.id AS to_id,
(
SELECT id
FROM locations AS ll
WHERE
ll.inscription_id = i.id
AND ll.block_height < l.block_height
ORDER BY ll.block_height DESC
LIMIT 1
) AS from_id,
COUNT(*) OVER() as total
FROM locations AS l
INNER JOIN inscriptions AS i ON l.inscription_id = i.id
WHERE
${
'block_height' in args
? this.sql`l.block_height = ${args.block_height}`
: this.sql`l.block_hash = ${args.block_hash}`
}
AND l.genesis = FALSE
LIMIT ${args.limit}
OFFSET ${args.offset}
)
SELECT
t.genesis_id,
t.number,
t.total,
${this.sql.unsafe(LOCATIONS_COLUMNS.map(c => `lf.${c} AS from_${c}`).join(','))},
${this.sql.unsafe(LOCATIONS_COLUMNS.map(c => `lt.${c} AS to_${c}`).join(','))}
FROM transfers AS t
INNER JOIN locations AS lf ON t.from_id = lf.id
INNER JOIN locations AS lt ON t.to_id = lt.id
`;
return {
total: results[0]?.total ?? 0,
results: results ?? [],
};
}

async getJsonContent(args: InscriptionIdentifier): Promise<DbJsonContent | undefined> {
const results = await this.sql<DbJsonContent[]>`
SELECT ${this.sql(JSON_CONTENTS_COLUMNS.map(c => `j.${c}`))}
Expand Down Expand Up @@ -453,7 +502,7 @@ export class PgStore extends BasePgStore {
} else {
// Is this a sequential genesis insert?
const maxNumber = await this.getMaxInscriptionNumber();
if (maxNumber && maxNumber + 1 !== args.inscription.number) {
if (maxNumber !== undefined && maxNumber + 1 !== args.inscription.number) {
logger.error(
{
block_height: args.location.block_height,
Expand Down
35 changes: 35 additions & 0 deletions src/pg/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,41 @@ export type DbLocation = {
current: boolean;
};

export type DbInscriptionLocationChange = {
genesis_id: string;
number: string;
from_id: string;
from_inscription_id: string;
from_block_height: string;
from_block_hash: string;
from_tx_id: string;
from_address: string | null;
from_output: string;
from_offset: string | null;
from_value: string | null;
from_sat_ordinal: string;
from_sat_rarity: string;
from_sat_coinbase_height: string;
from_timestamp: Date;
from_genesis: boolean;
from_current: boolean;
to_id: string;
to_inscription_id: string;
to_block_height: string;
to_block_hash: string;
to_tx_id: string;
to_address: string | null;
to_output: string;
to_offset: string | null;
to_value: string | null;
to_sat_ordinal: string;
to_sat_rarity: string;
to_sat_coinbase_height: string;
to_timestamp: Date;
to_genesis: boolean;
to_current: boolean;
};

export const LOCATIONS_COLUMNS = [
'id',
'inscription_id',
Expand Down
Loading

0 comments on commit e1afa7d

Please sign in to comment.