Skip to content

Commit

Permalink
feat: support cursed inscriptions (#85)
Browse files Browse the repository at this point in the history
* chore: keep previous output in locations table

* feat: support cursed inscriptions

* feat: add inscriptions per sat endpoint

* chore: log when blessed inscriptions appear in duplicate sats

* ci: fix quotes in vercel ci

* fix: support new cursed event
  • Loading branch information
rafaelcr committed Jun 8, 2023
1 parent 5eabad9 commit fb93474
Show file tree
Hide file tree
Showing 13 changed files with 568 additions and 27 deletions.
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']);
}
12 changes: 12 additions & 0 deletions migrations/1686170300438_curse-type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/* 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('inscriptions', {
curse_type: {
type: 'text',
},
});
}
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
3 changes: 2 additions & 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 @@ -250,6 +249,7 @@ export const InscriptionResponse = Type.Object(
content_type: Type.String({ examples: ['text/plain;charset=utf-8'] }),
content_length: Type.Integer({ examples: [59] }),
timestamp: Type.Integer({ examples: [1677733170000] }),
curse_type: Nullable(Type.String({ examples: ['p2wsh'] })),
},
{ title: 'Inscription Response' }
);
Expand Down Expand Up @@ -282,6 +282,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()),
},
{ title: 'Api Status Response' }
);
Expand Down
1 change: 1 addition & 0 deletions src/api/util/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export function parseDbInscriptions(
content_type: i.content_type,
content_length: parseInt(i.content_length),
timestamp: i.timestamp.valueOf(),
curse_type: i.curse_type,
}));
}
export function parseDbInscription(item: DbFullyLocatedInscriptionResult): InscriptionResponseType {
Expand Down
18 changes: 18 additions & 0 deletions src/chainhook/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,23 @@ const InscriptionRevealedSchema = Type.Object({
});
export type InscriptionRevealed = Static<typeof InscriptionRevealedSchema>;

const CursedInscriptionRevealedSchema = Type.Object({
content_bytes: Type.String(),
content_type: Type.String(),
content_length: Type.Integer(),
inscription_number: Type.Integer(),
inscription_fee: Type.Integer(),
inscription_id: Type.String(),
inscription_output_value: Type.Integer(),
inscriber_address: Type.String(),
ordinal_number: Type.Integer(),
ordinal_block_height: Type.Integer(),
ordinal_offset: Type.Integer(),
satpoint_post_inscription: Type.String(),
curse_type: Type.String(),
});
export type CursedInscriptionRevealed = Static<typeof CursedInscriptionRevealedSchema>;

const InscriptionTransferredSchema = Type.Object({
inscription_number: Type.Integer(),
inscription_id: Type.String(),
Expand All @@ -36,6 +53,7 @@ const InscriptionTransferredSchema = Type.Object({
export type InscriptionTransferred = Static<typeof InscriptionTransferredSchema>;

const OrdinalOperation = Type.Object({
cursed_inscription_revealed: Type.Optional(CursedInscriptionRevealedSchema),
inscription_revealed: Type.Optional(InscriptionRevealedSchema),
inscription_transferred: Type.Optional(InscriptionTransferredSchema),
});
Expand Down
131 changes: 107 additions & 24 deletions src/pg/pg-store.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { Order, OrderBy } from '../api/schemas';
import { normalizedHexString, parseSatPoint } from '../api/util/helpers';
import { OrdinalSatoshi, SatoshiRarity } from '../api/util/ordinal-satoshi';
import { ChainhookPayload, InscriptionEvent } from '../chainhook/schemas';
import {
ChainhookPayload,
CursedInscriptionRevealed,
InscriptionRevealed,
} from '../chainhook/schemas';
import { ENV } from '../env';
import { logger } from '../logger';
import { getIndexResultCountType, inscriptionContentToJson } from './helpers';
Expand Down Expand Up @@ -68,6 +72,12 @@ export class PgStore extends BasePgStore {
await this.rollBackInscriptionGenesis({ genesis_id });
logger.info(`PgStore rollback reveal #${number} (${genesis_id})`);
}
if (operation.cursed_inscription_revealed) {
const number = operation.cursed_inscription_revealed.inscription_number;
const genesis_id = operation.cursed_inscription_revealed.inscription_id;
await this.rollBackInscriptionGenesis({ genesis_id });
logger.info(`PgStore rollback cursed reveal #${number} (${genesis_id})`);
}
if (operation.inscription_transferred) {
const number = operation.inscription_transferred.inscription_number;
const genesis_id = operation.inscription_transferred.inscription_id;
Expand Down Expand Up @@ -101,6 +111,7 @@ export class PgStore extends BasePgStore {
number: reveal.inscription_number,
content: reveal.content_bytes,
fee: reveal.inscription_fee.toString(),
curse_type: null,
},
location: {
block_hash,
Expand All @@ -110,6 +121,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 @@ -122,9 +135,47 @@ export class PgStore extends BasePgStore {
`PgStore reveal #${reveal.inscription_number} (${reveal.inscription_id}) at block ${block_height}`
);
}
if (operation.cursed_inscription_revealed) {
const reveal = operation.cursed_inscription_revealed;
const satoshi = new OrdinalSatoshi(reveal.ordinal_number);
const satpoint = parseSatPoint(reveal.satpoint_post_inscription);
const id = await this.insertInscriptionGenesis({
inscription: {
genesis_id: reveal.inscription_id,
mime_type: reveal.content_type.split(';')[0],
content_type: reveal.content_type,
content_length: reveal.content_length,
number: reveal.inscription_number,
content: reveal.content_bytes,
fee: reveal.inscription_fee.toString(),
curse_type: reveal.curse_type,
},
location: {
block_hash,
block_height,
tx_id,
genesis_id: reveal.inscription_id,
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(),
sat_rarity: satoshi.rarity,
sat_coinbase_height: satoshi.blockHeight,
},
});
if (id) updatedInscriptionIds.add(id);
logger.info(
`PgStore cursed reveal #${reveal.inscription_number} (${reveal.inscription_id}) at block ${block_height}`
);
}
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 +186,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 +246,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 @@ -280,6 +331,7 @@ export class PgStore extends BasePgStore {
i.content_type,
i.content_length,
i.fee AS genesis_fee,
i.curse_type,
gen.block_height AS genesis_block_height,
gen.block_hash AS genesis_block_hash,
gen.tx_id AS genesis_tx_id,
Expand Down Expand Up @@ -505,15 +557,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 +616,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 +682,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
Loading

0 comments on commit fb93474

Please sign in to comment.