Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add endpoint to retrieve all inscription transfers per block #63

Merged
merged 4 commits into from
May 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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