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: support cursed inscriptions #85

Merged
merged 8 commits into from
Jun 8, 2023
Merged
Show file tree
Hide file tree
Changes from 6 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
8 changes: 4 additions & 4 deletions .github/workflows/vercel.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ jobs:
runs-on: ubuntu-latest

env:
VERCEL_ENV: ${{ contains(github.ref_name, "refs/tags") && 'production' || 'preview' }}
VERCEL_PROD: ${{ contains(github.ref_name, "refs/tags") && '--prod' || '' }}
VERCEL_ENV: ${{ contains(github.ref_name, 'refs/tags') && 'production' || 'preview' }}
VERCEL_PROD: ${{ contains(github.ref_name, 'refs/tags') && '--prod' || '' }}

environment:
name: ${{ contains(github.ref_name, "refs/tags") && 'Production' || 'Preview' }}
url: ${{ contains(github.ref_name, "refs/tags") && 'https://ordinals-api.vercel.app/' || 'https://ordinals-api-pbcblockstack-blockstack.vercel.app/' }}
name: ${{ contains(github.ref_name, 'refs/tags') && 'Production' || 'Preview' }}
url: ${{ contains(github.ref_name, 'refs/tags') && 'https://ordinals-api.vercel.app/' || 'https://ordinals-api-pbcblockstack-blockstack.vercel.app/' }}

steps:
- uses: actions/checkout@v2
Expand Down
16 changes: 16 additions & 0 deletions migrations/1685378650151_prev-location.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/* 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 {
pgm.addColumns('locations', {
prev_output: {
type: 'text',
},
prev_offset: {
type: 'numeric',
},
});
pgm.createIndex('locations', ['prev_output']);
}
48 changes: 47 additions & 1 deletion src/api/routes/sats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,17 @@ import { TypeBoxTypeProvider } from '@fastify/type-provider-typebox';
import { Type } from '@sinclair/typebox';
import { FastifyPluginCallback } from 'fastify';
import { Server } from 'http';
import { NotFoundResponse, OrdinalParam, SatoshiResponse } from '../schemas';
import {
InscriptionResponse,
LimitParam,
NotFoundResponse,
OffsetParam,
OrdinalParam,
PaginatedResponse,
SatoshiResponse,
} from '../schemas';
import { OrdinalSatoshi } from '../util/ordinal-satoshi';
import { DEFAULT_API_LIMIT, parseDbInscriptions } from '../util/helpers';

export const SatRoutes: FastifyPluginCallback<Record<never, never>, Server, TypeBoxTypeProvider> = (
fastify,
Expand Down Expand Up @@ -49,5 +58,42 @@ export const SatRoutes: FastifyPluginCallback<Record<never, never>, Server, Type
}
);

fastify.get(
'/sats/:ordinal/inscriptions',
{
schema: {
operationId: 'getSatoshiInscriptions',
summary: 'Satoshi Inscriptions',
description: 'Retrieves all inscriptions associated with a single satoshi',
tags: ['Satoshis'],
params: Type.Object({
ordinal: OrdinalParam,
}),
querystring: Type.Object({
// Pagination
offset: Type.Optional(OffsetParam),
limit: Type.Optional(LimitParam),
}),
response: {
200: PaginatedResponse(InscriptionResponse, 'Paginated Satoshi Inscriptions Response'),
},
},
},
async (request, reply) => {
const limit = request.query.limit ?? DEFAULT_API_LIMIT;
const offset = request.query.offset ?? 0;
const inscriptions = await fastify.db.getInscriptions(
{ limit, offset },
{ sat_ordinal: BigInt(request.params.ordinal) }
);
await reply.send({
limit,
offset,
total: inscriptions.total,
results: parseDbInscriptions(inscriptions.results),
});
}
);

done();
};
2 changes: 2 additions & 0 deletions src/api/routes/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,13 @@ export const StatusRoutes: FastifyPluginCallback<
const result = await fastify.db.sqlTransaction(async sql => {
const block_height = await fastify.db.getChainTipBlockHeight();
const max_inscription_number = await fastify.db.getMaxInscriptionNumber();
const max_cursed_inscription_number = await fastify.db.getMaxCursedInscriptionNumber();
return {
server_version: `ordinals-api ${SERVER_VERSION.tag} (${SERVER_VERSION.branch}:${SERVER_VERSION.commit})`,
status: 'ready',
block_height,
max_inscription_number,
max_cursed_inscription_number,
};
});
await reply.send(result);
Expand Down
2 changes: 1 addition & 1 deletion src/api/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,6 @@ export const InscriptionIdsParam = Type.Array(InscriptionIdParam, {
});

export const InscriptionNumberParam = Type.Integer({
minimum: 0,
title: 'Inscription Number',
description: 'Inscription number',
examples: ['10500'],
Expand Down Expand Up @@ -282,6 +281,7 @@ export const ApiStatusResponse = Type.Object(
status: Type.String(),
block_height: Type.Optional(Type.Integer()),
max_inscription_number: Type.Optional(Type.Integer()),
max_cursed_inscription_number: Type.Optional(Type.Integer()),
rafaelcr marked this conversation as resolved.
Show resolved Hide resolved
},
{ title: 'Api Status Response' }
);
Expand Down
80 changes: 57 additions & 23 deletions src/pg/pg-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ export class PgStore extends BasePgStore {
address: reveal.inscriber_address,
output: `${satpoint.tx_id}:${satpoint.vout}`,
offset: satpoint.offset ?? null,
prev_output: null,
prev_offset: null,
value: reveal.inscription_output_value.toString(),
timestamp: event.timestamp,
sat_ordinal: reveal.ordinal_number.toString(),
Expand All @@ -125,6 +127,7 @@ export class PgStore extends BasePgStore {
if (operation.inscription_transferred) {
const transfer = operation.inscription_transferred;
const satpoint = parseSatPoint(transfer.satpoint_post_transfer);
const prevSatpoint = parseSatPoint(transfer.satpoint_pre_transfer);
const satoshi = new OrdinalSatoshi(transfer.ordinal_number);
const id = await this.insertInscriptionTransfer({
location: {
Expand All @@ -135,6 +138,8 @@ export class PgStore extends BasePgStore {
address: transfer.updated_address,
output: `${satpoint.tx_id}:${satpoint.vout}`,
offset: satpoint.offset ?? null,
prev_output: `${prevSatpoint.tx_id}:${prevSatpoint.vout}`,
prev_offset: prevSatpoint.offset ?? null,
value: transfer.post_transfer_output_value
? transfer.post_transfer_output_value.toString()
: null,
Expand Down Expand Up @@ -193,30 +198,28 @@ export class PgStore extends BasePgStore {
}

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

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

async getInscriptionTransfersETag(): Promise<string> {
const result = await this.sql<{ max: number }[]>`SELECT MAX(id) FROM locations`;
return result[0].max.toString();
}

async getInscriptionCurrentLocation(args: { output: string }): Promise<DbLocation | undefined> {
const result = await this.sql<DbLocation[]>`
SELECT ${this.sql(LOCATIONS_COLUMNS)}
FROM locations
WHERE output = ${args.output}
AND current = TRUE
`;
if (result.count === 0) {
return undefined;
}
return result[0];
}

async getInscriptionContent(
args: InscriptionIdentifier
): Promise<DbInscriptionContent | undefined> {
Expand Down Expand Up @@ -505,15 +508,42 @@ export class PgStore extends BasePgStore {
);
} else {
// Is this a sequential genesis insert?
const maxNumber = await this.getMaxInscriptionNumber();
if (maxNumber !== undefined && maxNumber + 1 !== args.inscription.number) {
logger.error(
{
block_height: args.location.block_height,
genesis_id: args.inscription.genesis_id,
},
`PgStore inserting out-of-order inscription genesis #${args.inscription.number}, current max is #${maxNumber}`
);
if (args.inscription.number < 0) {
// Is it a cursed inscription?
const maxCursed = await this.getMaxCursedInscriptionNumber();
if (maxCursed !== undefined && maxCursed - 1 !== args.inscription.number) {
logger.warn(
{
block_height: args.location.block_height,
genesis_id: args.inscription.genesis_id,
},
`PgStore inserting out-of-order cursed inscription genesis #${args.inscription.number}, current max is #${maxCursed}`
);
}
} else {
const maxNumber = await this.getMaxInscriptionNumber();
if (maxNumber !== undefined && maxNumber + 1 !== args.inscription.number) {
logger.warn(
{
block_height: args.location.block_height,
genesis_id: args.inscription.genesis_id,
},
`PgStore inserting out-of-order inscription genesis #${args.inscription.number}, current max is #${maxNumber}`
);
}
// Is this a blessed inscription in a duplicate sat?
const dup = await sql<{ id: number }[]>`
SELECT id FROM locations WHERE sat_ordinal = ${args.location.sat_ordinal}
`;
if (dup.count > 0) {
logger.error(
{
block_height: args.location.block_height,
genesis_id: args.inscription.genesis_id,
},
`PgStore inserting duplicate blessed inscription in satoshi ${args.location.sat_ordinal}`
);
}
}
}

Expand All @@ -537,6 +567,8 @@ export class PgStore extends BasePgStore {
address: args.location.address,
output: args.location.output,
offset: args.location.offset,
prev_output: args.location.prev_output,
prev_offset: args.location.prev_offset,
value: args.location.value,
sat_ordinal: args.location.sat_ordinal,
sat_rarity: args.location.sat_rarity,
Expand Down Expand Up @@ -601,6 +633,8 @@ export class PgStore extends BasePgStore {
address: args.location.address,
output: args.location.output,
offset: args.location.offset,
prev_output: args.location.prev_output,
prev_offset: args.location.prev_offset,
value: args.location.value,
sat_ordinal: args.location.sat_ordinal,
sat_rarity: args.location.sat_rarity,
Expand Down
4 changes: 4 additions & 0 deletions src/pg/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ export type DbLocationInsert = {
address: string | null;
output: string;
offset: PgNumeric | null;
prev_output: string | null;
prev_offset: PgNumeric | null;
value: PgNumeric | null;
sat_ordinal: PgNumeric;
sat_rarity: string;
Expand All @@ -55,6 +57,8 @@ export type DbLocation = {
address: string | null;
output: string;
offset: string | null;
prev_output: string | null;
prev_offset: string | null;
value: string | null;
sat_ordinal: string;
sat_rarity: string;
Expand Down
Loading