Skip to content

Commit

Permalink
feat: detect and tag recursive inscriptions (#167)
Browse files Browse the repository at this point in the history
* feat: initial impl

* fix: tests

* feat: filter by recursive

* fix: tests

* fix: make migration progressive
  • Loading branch information
rafaelcr committed Jul 25, 2023
1 parent 50ddff6 commit fb36285
Show file tree
Hide file tree
Showing 9 changed files with 358 additions and 13 deletions.
46 changes: 46 additions & 0 deletions migrations/1690229956705_inscription-recursions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/* 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.createTable('inscription_recursions', {
id: {
type: 'bigserial',
primaryKey: true,
},
inscription_id: {
type: 'bigint',
notNull: true,
},
ref_inscription_id: {
type: 'bigint',
notNull: true,
},
});
pgm.createConstraint(
'inscription_recursions',
'locations_inscription_id_fk',
'FOREIGN KEY(inscription_id) REFERENCES inscriptions(id) ON DELETE CASCADE'
);
pgm.createConstraint(
'inscription_recursions',
'locations_ref_inscription_id_fk',
'FOREIGN KEY(ref_inscription_id) REFERENCES inscriptions(id) ON DELETE CASCADE'
);
pgm.createConstraint(
'inscription_recursions',
'inscriptions_inscription_id_ref_inscription_id_unique',
'UNIQUE(inscription_id, ref_inscription_id)'
);
pgm.createIndex('inscription_recursions', ['ref_inscription_id']);

// Add columns to `inscriptions` table.
pgm.addColumn('inscriptions', {
recursive: {
type: 'boolean',
default: false,
},
});
pgm.createIndex('inscriptions', ['recursive'], { where: 'recursive = TRUE' });
}
3 changes: 3 additions & 0 deletions src/api/routes/inscriptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
OrdinalParam,
OutputParam,
PaginatedResponse,
RecursiveParam,
SatoshiRaritiesParam,
TimestampParam,
} from '../schemas';
Expand Down Expand Up @@ -84,6 +85,7 @@ const IndexRoutes: FastifyPluginCallback<Record<never, never>, Server, TypeBoxTy
address: Type.Optional(AddressesParam),
mime_type: Type.Optional(MimeTypesParam),
rarity: Type.Optional(SatoshiRaritiesParam),
recursive: Type.Optional(RecursiveParam),
// Pagination
offset: Type.Optional(OffsetParam),
limit: Type.Optional(LimitParam),
Expand Down Expand Up @@ -120,6 +122,7 @@ const IndexRoutes: FastifyPluginCallback<Record<never, never>, Server, TypeBoxTy
address: request.query.address,
mime_type: request.query.mime_type,
sat_rarity: request.query.rarity,
recursive: request.query.recursive,
},
{
order_by: request.query.order_by ?? OrderBy.genesis_block_height,
Expand Down
17 changes: 17 additions & 0 deletions src/api/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,12 @@ export const OutputParam = Type.RegEx(/^[a-fA-F0-9]{64}:[0-9]+$/, {
examples: ['8f46f0d4ef685e650727e6faf7e30f23b851a7709714ec774f7909b3fb5e604c:0'],
});

export const RecursiveParam = Type.Boolean({
title: 'Recursive',
description: 'Whether or not the inscription is recursive',
examples: [false],
});

export const OffsetParam = Type.Integer({
minimum: 0,
title: 'Offset',
Expand Down Expand Up @@ -257,6 +263,17 @@ export const InscriptionResponse = Type.Object(
content_length: Type.Integer({ examples: [59] }),
timestamp: Type.Integer({ examples: [1677733170000] }),
curse_type: Nullable(Type.String({ examples: ['p2wsh'] })),
recursive: Type.Boolean({ examples: [true] }),
recursion_refs: Nullable(
Type.Array(
Type.String({
examples: [
'1463d48e9248159084929294f64bda04487503d30ce7ab58365df1dc6fd58218i0',
'541076e29e1b63460412d3087b37130c9a14abd0beeb4e9b2b805d2072c84dedi0',
],
})
)
),
},
{ title: 'Inscription Response' }
);
Expand Down
2 changes: 2 additions & 0 deletions src/api/util/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ export function parseDbInscriptions(
content_length: parseInt(i.content_length),
timestamp: i.timestamp.valueOf(),
curse_type: i.curse_type,
recursive: i.recursive,
recursion_refs: i.recursion_refs?.split(',') ?? null,
}));
}
export function parseDbInscription(item: DbFullyLocatedInscriptionResult): InscriptionResponseType {
Expand Down
17 changes: 17 additions & 0 deletions src/pg/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { PgBytea } from '@hirosystems/api-toolkit';
import { DbInscriptionIndexFilters, DbInscriptionIndexResultCountType } from './types';
import { hexToBuffer } from '../api/util/helpers';

/**
* Returns which inscription count is required based on filters sent to the index endpoint.
Expand Down Expand Up @@ -30,3 +32,18 @@ export function getIndexResultCountType(
return DbInscriptionIndexResultCountType.intractable;
}
}

/**
* Returns a list of referenced inscription ids from inscription content.
* @param content - Inscription content
* @returns List of IDs
*/
export function getInscriptionRecursion(content: PgBytea): string[] {
const buf = typeof content === 'string' ? hexToBuffer(content) : content;
const strContent = buf.toString('utf-8');
const result: string[] = [];
for (const match of strContent.matchAll(/\/content\/([a-fA-F0-9]{64}i\d+)/g)) {
result.push(match[1]);
}
return result;
}
51 changes: 38 additions & 13 deletions src/pg/pg-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Order, OrderBy } from '../api/schemas';
import { isProdEnv, isTestEnv, normalizedHexString, parseSatPoint } from '../api/util/helpers';
import { OrdinalSatoshi, SatoshiRarity } from '../api/util/ordinal-satoshi';
import { ENV } from '../env';
import { getIndexResultCountType } from './helpers';
import { getIndexResultCountType, getInscriptionRecursion } from './helpers';
import {
DbFullyLocatedInscriptionResult,
DbInscriptionContent,
Expand Down Expand Up @@ -360,6 +360,13 @@ export class PgStore extends BasePgStore {
i.sat_ordinal,
i.sat_rarity,
i.sat_coinbase_height,
i.recursive,
(
SELECT STRING_AGG(ii.genesis_id, ',')
FROM inscription_recursions AS ir
INNER JOIN inscriptions AS ii ON ii.id = ir.ref_inscription_id
WHERE ir.inscription_id = i.id
) AS recursion_refs,
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 @@ -443,6 +450,7 @@ export class PgStore extends BasePgStore {
: sql``
}
${filters?.sat_ordinal ? sql`AND i.sat_ordinal = ${filters.sat_ordinal}` : sql``}
${filters?.recursive !== undefined ? sql`AND i.recursive = ${filters.recursive}` : sql``}
ORDER BY ${orderBy} ${order}
LIMIT ${page.limit}
OFFSET ${page.offset}
Expand Down Expand Up @@ -583,8 +591,13 @@ export class PgStore extends BasePgStore {
const upsert = await sql<{ id: number }[]>`
SELECT id FROM inscriptions WHERE number = ${args.inscription.number}
`;
const recursion = getInscriptionRecursion(args.inscription.content);
const data = {
...args.inscription,
recursive: recursion.length > 0,
};
const inscription = await sql<{ id: number }[]>`
INSERT INTO inscriptions ${sql(args.inscription)}
INSERT INTO inscriptions ${sql(data)}
ON CONFLICT ON CONSTRAINT inscriptions_number_unique DO UPDATE SET
genesis_id = EXCLUDED.genesis_id,
mime_type = EXCLUDED.mime_type,
Expand All @@ -600,18 +613,8 @@ export class PgStore extends BasePgStore {
`;
inscription_id = inscription[0].id;
const location = {
...args.location,
inscription_id,
genesis_id: args.location.genesis_id,
block_height: args.location.block_height,
block_hash: args.location.block_hash,
tx_id: args.location.tx_id,
tx_index: args.location.tx_index,
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,
timestamp: sql`to_timestamp(${args.location.timestamp})`,
};
const locationRes = await sql<{ id: number }[]>`
Expand All @@ -636,6 +639,7 @@ export class PgStore extends BasePgStore {
tx_index: args.location.tx_index,
address: args.location.address,
});
await this.updateInscriptionRecursion({ inscription_id, ref_genesis_ids: recursion });
logger.info(
`PgStore${upsert.count > 0 ? ' upsert ' : ' '}reveal #${args.inscription.number} (${
args.location.genesis_id
Expand Down Expand Up @@ -836,4 +840,25 @@ export class PgStore extends BasePgStore {
`;
});
}

private async updateInscriptionRecursion(args: {
inscription_id: number;
ref_genesis_ids: string[];
}): Promise<void> {
await this.sqlWriteTransaction(async sql => {
const validated = await sql<{ id: string }[]>`
SELECT id FROM inscriptions WHERE genesis_id IN ${this.sql(args.ref_genesis_ids)}
`;
if (validated.count > 0) {
const values = validated.map(i => ({
inscription_id: args.inscription_id,
ref_inscription_id: i.id,
}));
await this.sql`
INSERT INTO inscription_recursions ${sql(values)}
ON CONFLICT ON CONSTRAINT inscriptions_inscription_id_ref_inscription_id_unique DO NOTHING
`;
}
});
}
}
3 changes: 3 additions & 0 deletions src/pg/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ export type DbFullyLocatedInscriptionResult = {
content_length: string;
timestamp: Date;
curse_type: string | null;
recursive: boolean;
recursion_refs: string | null;
};

export type DbLocationInsert = {
Expand Down Expand Up @@ -188,6 +190,7 @@ export type DbInscriptionIndexFilters = {
sat_ordinal?: bigint;
from_sat_ordinal?: bigint;
to_sat_ordinal?: bigint;
recursive?: boolean;
};

export type DbInscriptionIndexOrder = {
Expand Down
Loading

0 comments on commit fb36285

Please sign in to comment.