diff --git a/CHANGELOG.md b/CHANGELOG.md index fec752f4..cd90ab7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,52 @@ +## [3.1.0-beta.1](https://github.com/hirosystems/ordinals-api/compare/v3.0.2-beta.6...v3.1.0-beta.1) (2024-03-27) + + +### Features + +* support self minted 5-byte wide BRC-20 tokens ([#338](https://github.com/hirosystems/ordinals-api/issues/338)) ([60f46d3](https://github.com/hirosystems/ordinals-api/commit/60f46d3533e837843e8aa6094b4673a3bc84c124)) + +## [3.0.2-beta.6](https://github.com/hirosystems/ordinals-api/compare/v3.0.2-beta.5...v3.0.2-beta.6) (2024-03-09) + + +### Bug Fixes + +* remove gap detection ([#330](https://github.com/hirosystems/ordinals-api/issues/330)) ([040ee04](https://github.com/hirosystems/ordinals-api/commit/040ee04b0906106fdece3f00d34fb4817d7f318e)) + +## [3.0.2-beta.5](https://github.com/hirosystems/ordinals-api/compare/v3.0.2-beta.4...v3.0.2-beta.5) (2024-03-09) + + +### Bug Fixes + +* ignore spent as fee on gap check ([#328](https://github.com/hirosystems/ordinals-api/issues/328)) ([a1277cf](https://github.com/hirosystems/ordinals-api/commit/a1277cf39eb61e548f55bd8e524054db6a11c843)) + +## [3.0.2-beta.4](https://github.com/hirosystems/ordinals-api/compare/v3.0.2-beta.3...v3.0.2-beta.4) (2024-03-08) + + +### Bug Fixes + +* calculate transfer type correctly upon reveal ([#326](https://github.com/hirosystems/ordinals-api/issues/326)) ([6c4c54b](https://github.com/hirosystems/ordinals-api/commit/6c4c54b45a74744c4e61a2437632f390080a9624)) + +## [3.0.2-beta.3](https://github.com/hirosystems/ordinals-api/compare/v3.0.2-beta.2...v3.0.2-beta.3) (2024-03-07) + + +### Bug Fixes + +* check only the first blessed inscription in next block on gap detection ([#325](https://github.com/hirosystems/ordinals-api/issues/325)) ([9cad6c1](https://github.com/hirosystems/ordinals-api/commit/9cad6c16d34fdd11c1d9f473b2f3802a8da464d8)) + +## [3.0.2-beta.2](https://github.com/hirosystems/ordinals-api/compare/v3.0.2-beta.1...v3.0.2-beta.2) (2024-03-07) + + +### Bug Fixes + +* filter correct content types for brc20 ([#323](https://github.com/hirosystems/ordinals-api/issues/323)) ([4d52b48](https://github.com/hirosystems/ordinals-api/commit/4d52b4820e5f1a36264977dc4a6c4ca324864108)) + +## [3.0.2-beta.1](https://github.com/hirosystems/ordinals-api/compare/v3.0.1...v3.0.2-beta.1) (2024-03-07) + + +### Bug Fixes + +* use brc20_total_balances table when inserting new transfer ([#321](https://github.com/hirosystems/ordinals-api/issues/321)) ([925fb0e](https://github.com/hirosystems/ordinals-api/commit/925fb0e05a982eeec802bee6f53a957bc5ea3acf)) + ## [3.0.1](https://github.com/hirosystems/ordinals-api/compare/v3.0.0...v3.0.1) (2024-03-04) diff --git a/migrations/1711465842961_brc20-deploy-self-mint.ts b/migrations/1711465842961_brc20-deploy-self-mint.ts new file mode 100644 index 00000000..8cacf691 --- /dev/null +++ b/migrations/1711465842961_brc20-deploy-self-mint.ts @@ -0,0 +1,19 @@ +/* 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.addColumn('brc20_deploys', { + self_mint: { + type: 'boolean', + default: 'false', + }, + }); + pgm.sql(`UPDATE brc20_deploys SET self_mint = false`); + pgm.alterColumn('brc20_deploys', 'self_mint', { notNull: true }); +} + +export function down(pgm: MigrationBuilder): void { + pgm.dropColumn('brc20_deploys', ['self_mint']); +} diff --git a/src/api/schemas.ts b/src/api/schemas.ts index 973ba9f6..053f38d3 100644 --- a/src/api/schemas.ts +++ b/src/api/schemas.ts @@ -488,6 +488,7 @@ export const Brc20TokenResponseSchema = Type.Object( deploy_timestamp: Type.Integer({ examples: [1677733170000] }), minted_supply: Type.String({ examples: ['1000000'] }), tx_count: Type.Integer({ examples: [300000] }), + self_mint: Type.Boolean(), }, { title: 'BRC-20 Token Response' } ); diff --git a/src/api/util/helpers.ts b/src/api/util/helpers.ts index 173e7672..9b7815eb 100644 --- a/src/api/util/helpers.ts +++ b/src/api/util/helpers.ts @@ -120,6 +120,7 @@ export function parseBrc20Tokens(items: DbBrc20Token[]): Brc20TokenResponse[] { deploy_timestamp: i.timestamp.valueOf(), minted_supply: decimals(i.minted_supply, i.decimals), tx_count: parseInt(i.tx_count), + self_mint: i.self_mint, })); } diff --git a/src/env.ts b/src/env.ts index 21aeef65..956d42f3 100644 --- a/src/env.ts +++ b/src/env.ts @@ -67,8 +67,6 @@ const schema = Type.Object({ /** Enables BRC-20 processing in write mode APIs */ BRC20_BLOCK_SCAN_ENABLED: Type.Boolean({ default: true }), - /** Enables inscription gap detection to prevent ingesting unordered blocks */ - INSCRIPTION_GAP_DETECTION_ENABLED: Type.Boolean({ default: true }), }); type Env = Static; diff --git a/src/pg/brc20/brc20-pg-store.ts b/src/pg/brc20/brc20-pg-store.ts index 357795a4..8641a620 100644 --- a/src/pg/brc20/brc20-pg-store.ts +++ b/src/pg/brc20/brc20-pg-store.ts @@ -1,6 +1,5 @@ import { BasePgStoreModule, logger } from '@hirosystems/api-toolkit'; import * as postgres from 'postgres'; -import { hexToBuffer } from '../../api/util/helpers'; import { DbInscriptionIndexPaging, InscriptionData, @@ -27,7 +26,7 @@ import { DbBrc20TokenWithSupply, DbBrc20TransferEvent, } from './types'; -import { Brc20Deploy, Brc20Mint, Brc20Transfer, brc20FromInscriptionContent } from './helpers'; +import { Brc20Deploy, Brc20Mint, Brc20Transfer, UINT64_MAX, brc20FromInscription } from './helpers'; import { Brc20TokenOrderBy } from '../../api/schemas'; import { objRemoveUndefinedValues } from '../helpers'; @@ -47,15 +46,7 @@ export class Brc20PgStore extends BasePgStoreModule { const pointer = args.pointers[i]; if (parseInt(pointer.block_height) < BRC20_GENESIS_BLOCK) continue; if ('inscription' in reveal) { - if ( - reveal.inscription.classic_number < 0 || - reveal.inscription.number < 0 || - reveal.location.transfer_type != DbLocationTransferType.transferred - ) - continue; - const brc20 = brc20FromInscriptionContent( - hexToBuffer(reveal.inscription.content as string).toString('utf-8') - ); + const brc20 = brc20FromInscription(reveal); if (brc20) { switch (brc20.op) { case 'deploy': @@ -208,7 +199,7 @@ export class Brc20PgStore extends BasePgStoreModule { private async insertDeploy(deploy: { brc20: Brc20Deploy; - reveal: InscriptionEventData; + reveal: InscriptionRevealData; pointer: DbLocationPointerInsert; }): Promise { if (deploy.reveal.location.transfer_type != DbLocationTransferType.transferred) return; @@ -218,10 +209,11 @@ export class Brc20PgStore extends BasePgStoreModule { tx_id: deploy.reveal.location.tx_id, address: deploy.pointer.address as string, ticker: deploy.brc20.tick, - max: deploy.brc20.max, + max: deploy.brc20.max === '0' ? UINT64_MAX.toString() : deploy.brc20.max, limit: deploy.brc20.lim ?? null, decimals: deploy.brc20.dec ?? '18', tx_count: 1, + self_mint: deploy.brc20.self_mint === 'true', }; const deployRes = await this.sql` WITH deploy_insert AS ( @@ -258,19 +250,21 @@ export class Brc20PgStore extends BasePgStoreModule { private async insertMint(mint: { brc20: Brc20Mint; - reveal: InscriptionEventData; + reveal: InscriptionRevealData; pointer: DbLocationPointerInsert; }): Promise { if (mint.reveal.location.transfer_type != DbLocationTransferType.transferred) return; // Check the following conditions: // * Is the mint amount within the allowed token limits? + // * Is this a self_mint with the correct parent inscription? // * Is the number of decimals correct? // * Does the mint amount exceed remaining supply? const mintRes = await this.sql` WITH mint_data AS ( - SELECT id, decimals, "limit", max, minted_supply - FROM brc20_deploys - WHERE ticker_lower = LOWER(${mint.brc20.tick}) AND minted_supply < max + SELECT d.id, d.decimals, d."limit", d.max, d.minted_supply, d.self_mint, i.genesis_id + FROM brc20_deploys d + INNER JOIN inscriptions i ON i.id = d.inscription_id + WHERE d.ticker_lower = LOWER(${mint.brc20.tick}) AND d.minted_supply < d.max ), validated_mint AS ( SELECT @@ -279,6 +273,10 @@ export class Brc20PgStore extends BasePgStoreModule { FROM mint_data WHERE ("limit" IS NULL OR ${mint.brc20.amt}::numeric <= "limit") AND (SCALE(${mint.brc20.amt}::numeric) <= decimals) + AND ( + self_mint = FALSE OR + (self_mint = TRUE AND genesis_id = ${mint.reveal.inscription.parent}) + ) ), mint_insert AS ( INSERT INTO brc20_mints (inscription_id, brc20_deploy_id, block_height, tx_id, address, amount) ( @@ -340,20 +338,13 @@ export class Brc20PgStore extends BasePgStoreModule { pointer: DbLocationPointerInsert; }): Promise { if (transfer.reveal.location.transfer_type != DbLocationTransferType.transferred) return; - // Check the following conditions: - // * Do we have enough available balance to do this transfer? const transferRes = await this.sql` - WITH balance_data AS ( - SELECT b.brc20_deploy_id, COALESCE(SUM(b.avail_balance), 0) AS avail_balance - FROM brc20_balances AS b - INNER JOIN brc20_deploys AS d ON b.brc20_deploy_id = d.id - WHERE d.ticker_lower = LOWER(${transfer.brc20.tick}) - AND b.address = ${transfer.pointer.address} - GROUP BY b.brc20_deploy_id - ), - validated_transfer AS ( - SELECT * FROM balance_data - WHERE avail_balance >= ${transfer.brc20.amt}::numeric + WITH validated_transfer AS ( + SELECT brc20_deploy_id, avail_balance + FROM brc20_total_balances + WHERE brc20_deploy_id = (SELECT id FROM brc20_deploys WHERE ticker_lower = LOWER(${transfer.brc20.tick})) + AND address = ${transfer.pointer.address} + AND avail_balance >= ${transfer.brc20.amt}::numeric ), transfer_insert AS ( INSERT INTO brc20_transfers (inscription_id, brc20_deploy_id, block_height, tx_id, from_address, to_address, amount) ( diff --git a/src/pg/brc20/helpers.ts b/src/pg/brc20/helpers.ts index 3b1f1958..6aec1697 100644 --- a/src/pg/brc20/helpers.ts +++ b/src/pg/brc20/helpers.ts @@ -2,7 +2,7 @@ import { Static, Type } from '@fastify/type-provider-typebox'; import { TypeCompiler } from '@sinclair/typebox/compiler'; import BigNumber from 'bignumber.js'; import { hexToBuffer } from '../../api/util/helpers'; -import { InscriptionData } from '../types'; +import { DbLocationTransferType, InscriptionRevealData } from '../types'; const Brc20TickerSchema = Type.String({ minLength: 1 }); const Brc20NumberSchema = Type.RegEx(/^((\d+)|(\d*\.?\d+))$/); @@ -15,6 +15,7 @@ const Brc20DeploySchema = Type.Object( max: Brc20NumberSchema, lim: Type.Optional(Brc20NumberSchema), dec: Type.Optional(Type.RegEx(/^\d+$/)), + self_mint: Type.Optional(Type.Literal('true')), }, { additionalProperties: true } ); @@ -46,28 +47,42 @@ const Brc20Schema = Type.Union([Brc20DeploySchema, Brc20MintSchema, Brc20Transfe const Brc20C = TypeCompiler.Compile(Brc20Schema); export type Brc20 = Static; -const UINT64_MAX = BigNumber('18446744073709551615'); // 20 digits +export const UINT64_MAX = BigNumber('18446744073709551615'); // 20 digits // Only compare against `UINT64_MAX` if the number is at least the same number of digits. const numExceedsMax = (num: string) => num.length >= 20 && UINT64_MAX.isLessThan(num); -// For testing only -export function brc20FromInscription(inscription: InscriptionData): Brc20 | undefined { - if (inscription.number < 0) return; - if (inscription.mime_type !== 'text/plain' && inscription.mime_type !== 'application/json') - return; - const buf = hexToBuffer(inscription.content as string).toString('utf-8'); - return brc20FromInscriptionContent(buf); -} +/** + * Activation block height for + * https://l1f.discourse.group/t/brc-20-proposal-for-issuance-and-burn-enhancements-brc20-ip-1/621/1 + */ +export const BRC20_SELF_MINT_ACTIVATION_BLOCK = 837090; -export function brc20FromInscriptionContent(content: string): Brc20 | undefined { +export function brc20FromInscription(reveal: InscriptionRevealData): Brc20 | undefined { + if ( + reveal.inscription.classic_number < 0 || + reveal.inscription.number < 0 || + reveal.location.transfer_type != DbLocationTransferType.transferred || + !['text/plain', 'application/json'].includes(reveal.inscription.mime_type) + ) + return; try { - const json = JSON.parse(content); + const json = JSON.parse(hexToBuffer(reveal.inscription.content as string).toString('utf-8')); if (Brc20C.Check(json)) { // Check ticker byte length - if (Buffer.from(json.tick).length !== 4) return; + const tick = Buffer.from(json.tick); + if (json.op === 'deploy') { + if ( + tick.length === 5 && + (reveal.location.block_height < BRC20_SELF_MINT_ACTIVATION_BLOCK || + json.self_mint !== 'true') + ) + return; + } + if (tick.length < 4 || tick.length > 5) return; // Check numeric values. if (json.op === 'deploy') { - if (parseFloat(json.max) == 0 || numExceedsMax(json.max)) return; + if ((parseFloat(json.max) == 0 && json.self_mint !== 'true') || numExceedsMax(json.max)) + return; if (json.lim && (parseFloat(json.lim) == 0 || numExceedsMax(json.lim))) return; if (json.dec && parseFloat(json.dec) > 18) return; } else { diff --git a/src/pg/brc20/types.ts b/src/pg/brc20/types.ts index 269397da..5b28258a 100644 --- a/src/pg/brc20/types.ts +++ b/src/pg/brc20/types.ts @@ -20,6 +20,7 @@ export type DbBrc20DeployInsert = { decimals: string; limit: string | null; tx_count: number; + self_mint: boolean; }; export type DbBrc20MintInsert = { @@ -78,6 +79,7 @@ export type DbBrc20Token = { timestamp: number; minted_supply: string; tx_count: string; + self_mint: boolean; }; export type DbBrc20TokenWithSupply = DbBrc20Token & { @@ -188,6 +190,7 @@ export const BRC20_DEPLOYS_COLUMNS = [ 'limit', 'minted_supply', 'tx_count', + 'self_mint', ]; export const BRC20_TRANSFERS_COLUMNS = [ diff --git a/src/pg/helpers.ts b/src/pg/helpers.ts index 25d5d504..033a1ece 100644 --- a/src/pg/helpers.ts +++ b/src/pg/helpers.ts @@ -1,12 +1,10 @@ -import { PgBytea, toEnumValue } from '@hirosystems/api-toolkit'; +import { PgBytea, logger, toEnumValue } from '@hirosystems/api-toolkit'; import { hexToBuffer, normalizedHexString, parseSatPoint } from '../api/util/helpers'; import { - BadPayloadRequestError, BitcoinEvent, BitcoinInscriptionRevealed, BitcoinInscriptionTransferred, } from '@hirosystems/chainhook-client'; -import { ENV } from '../env'; import { DbLocationTransferType, InscriptionEventData, @@ -15,29 +13,6 @@ import { } from './types'; import { OrdinalSatoshi } from '../api/util/ordinal-satoshi'; -/** - * Check if writing a block would create an inscription number gap - * @param currentNumber - Current max blessed number - * @param newNumbers - New blessed numbers to be inserted - */ -export function assertNoBlockInscriptionGap(args: { - currentNumber: number; - newNumbers: number[]; - currentBlockHeight: number; - newBlockHeight: number; -}) { - if (!ENV.INSCRIPTION_GAP_DETECTION_ENABLED) return; - args.newNumbers.sort((a, b) => a - b); - for (let n = 0; n < args.newNumbers.length; n++) { - const curr = args.currentNumber + n; - const next = args.newNumbers[n]; - if (next !== curr + 1) - throw new BadPayloadRequestError( - `Block inscription gap detected: Attempting to insert #${next} (${args.newBlockHeight}) but current max is #${curr}. Chain tip is at ${args.currentBlockHeight}.` - ); - } -} - /** * Returns a list of referenced inscription ids from inscription content. * @param content - Inscription content @@ -98,12 +73,25 @@ function updateFromOrdhookInscriptionRevealed(args: { const satoshi = new OrdinalSatoshi(args.reveal.ordinal_number); const satpoint = parseSatPoint(args.reveal.satpoint_post_inscription); const recursive_refs = getInscriptionRecursion(args.reveal.content_bytes); - const contentType = removeNullBytes(args.reveal.content_type); + const content_type = removeNullBytes(args.reveal.content_type); + let transfer_type = DbLocationTransferType.transferred; + if (args.reveal.inscriber_address == null || args.reveal.inscriber_address == '') { + if (args.reveal.inscription_output_value == 0) { + if (args.reveal.inscription_pointer !== 0 && args.reveal.inscription_pointer !== null) { + logger.warn( + `Detected inscription reveal with no address and no output value but a valid pointer ${args.reveal.inscription_id}` + ); + } + transfer_type = DbLocationTransferType.spentInFees; + } else { + transfer_type = DbLocationTransferType.burnt; + } + } return { inscription: { genesis_id: args.reveal.inscription_id, - mime_type: contentType.split(';')[0], - content_type: contentType, + mime_type: content_type.split(';')[0], + content_type, content_length: args.reveal.content_length, number: args.reveal.inscription_number.jubilee, classic_number: args.reveal.inscription_number.classic, @@ -131,7 +119,7 @@ function updateFromOrdhookInscriptionRevealed(args: { prev_offset: null, value: args.reveal.inscription_output_value.toString(), timestamp: args.timestamp, - transfer_type: DbLocationTransferType.transferred, + transfer_type, }, recursive_refs, }; diff --git a/src/pg/pg-store.ts b/src/pg/pg-store.ts index 5f643294..f08df0c2 100644 --- a/src/pg/pg-store.ts +++ b/src/pg/pg-store.ts @@ -17,7 +17,7 @@ import { ENV } from '../env'; import { Brc20PgStore } from './brc20/brc20-pg-store'; import { CountsPgStore } from './counts/counts-pg-store'; import { getIndexResultCountType } from './counts/helpers'; -import { assertNoBlockInscriptionGap, revealInsertsFromOrdhookEvent } from './helpers'; +import { revealInsertsFromOrdhookEvent } from './helpers'; import { DbFullyLocatedInscriptionResult, DbInscriptionContent, @@ -33,7 +33,6 @@ import { DbPaginatedResult, InscriptionEventData, LOCATIONS_COLUMNS, - InscriptionRevealData, InscriptionInsert, LocationInsert, LocationData, @@ -109,11 +108,9 @@ export class PgStore extends BasePgStore { // Check where we're at in terms of ingestion, e.g. block height and max blessed inscription // number. This will let us determine if we should skip ingesting this block or throw an // error if a gap is detected. - const currentBlessedNumber = (await this.getMaxInscriptionNumber()) ?? -1; const currentBlockHeight = await this.getChainTipBlockHeight(); const event = applyEvent as BitcoinEvent; if ( - ENV.INSCRIPTION_GAP_DETECTION_ENABLED && event.block_identifier.index <= currentBlockHeight && event.block_identifier.index !== ORDINALS_GENESIS_BLOCK ) { @@ -125,15 +122,6 @@ export class PgStore extends BasePgStore { logger.info(`PgStore ingesting block ${event.block_identifier.index}`); const time = stopwatch(); const writes = revealInsertsFromOrdhookEvent(event); - const newBlessedNumbers = writes - .filter(w => 'inscription' in w && w.inscription.number >= 0) - .map(w => (w as InscriptionRevealData).inscription.number ?? 0); - assertNoBlockInscriptionGap({ - currentNumber: currentBlessedNumber, - newNumbers: newBlessedNumbers, - currentBlockHeight: currentBlockHeight, - newBlockHeight: event.block_identifier.index, - }); for (const writeChunk of batchIterate(writes, INSERT_BATCH_SIZE)) await this.insertInscriptions(writeChunk, payload.chainhook.is_streaming_blocks); updatedBlockHeightMin = Math.min(updatedBlockHeightMin, event.block_identifier.index); diff --git a/tests/brc-20/brc20.test.ts b/tests/brc-20/brc20.test.ts index 9f5a8608..8f2132f3 100644 --- a/tests/brc-20/brc20.test.ts +++ b/tests/brc-20/brc20.test.ts @@ -1,9 +1,9 @@ import { runMigrations } from '@hirosystems/api-toolkit'; import { buildApiServer } from '../../src/api/init'; import { Brc20ActivityResponse, Brc20TokenResponse } from '../../src/api/schemas'; -import { brc20FromInscription } from '../../src/pg/brc20/helpers'; +import { BRC20_SELF_MINT_ACTIVATION_BLOCK, brc20FromInscription } from '../../src/pg/brc20/helpers'; import { MIGRATIONS_DIR, PgStore } from '../../src/pg/pg-store'; -import { InscriptionData } from '../../src/pg/types'; +import { DbLocationTransferType, InscriptionRevealData } from '../../src/pg/types'; import { TestChainhookPayloadBuilder, TestFastifyServer, @@ -86,26 +86,44 @@ describe('BRC-20', () => { }); describe('token standard validation', () => { - const testInsert = (json: any): InscriptionData => { + const testInsert = (json: any, block_height: number = 830000): InscriptionRevealData => { const content = Buffer.from(JSON.stringify(json), 'utf-8'); - const insert: InscriptionData = { - genesis_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', - number: 0, - classic_number: 0, - mime_type: 'application/json', - content_type: 'application/json', - content_length: content.length, - content: `0x${content.toString('hex')}`, - fee: '200', - curse_type: null, - sat_ordinal: '2000000', - sat_rarity: 'common', - sat_coinbase_height: 110, - recursive: false, - metadata: null, - parent: null, + return { + inscription: { + genesis_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', + number: 0, + classic_number: 0, + mime_type: 'application/json', + content_type: 'application/json', + content_length: content.length, + content: `0x${content.toString('hex')}`, + fee: '200', + curse_type: null, + sat_ordinal: '2000000', + sat_rarity: 'common', + sat_coinbase_height: 110, + recursive: false, + metadata: null, + parent: null, + }, + recursive_refs: [], + location: { + genesis_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', + block_height, + block_hash: '00000000000000000002c5c0aba96f981642a6dca109e6b3564925c21a98aa3e', + tx_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', + tx_index: 0, + address: 'bc1pdjd6q33l0ca9nuudu2hr5qrs9u5dt6nl0z7fvu8kv4y8w4fzdpysc80028', + output: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc:0', + offset: '0', + prev_output: null, + prev_offset: null, + value: '9999', + transfer_type: DbLocationTransferType.transferred, + block_transfer_index: null, + timestamp: 1091091019, + }, }; - return insert; }; test('ignores incorrect MIME type', () => { @@ -118,29 +136,48 @@ describe('BRC-20', () => { }), 'utf-8' ); - const insert: InscriptionData = { - genesis_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', - number: 0, - classic_number: 0, - mime_type: 'foo/bar', - content_type: 'foo/bar;x=1', - content_length: content.length, - content: `0x${content.toString('hex')}`, - fee: '200', - curse_type: null, - sat_ordinal: '2000000', - sat_rarity: 'common', - sat_coinbase_height: 110, - recursive: false, - metadata: null, - parent: null, + const insert: InscriptionRevealData = { + inscription: { + genesis_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', + number: 0, + classic_number: 0, + mime_type: 'foo/bar', + content_type: 'foo/bar;x=1', + content_length: content.length, + content: `0x${content.toString('hex')}`, + fee: '200', + curse_type: null, + sat_ordinal: '2000000', + sat_rarity: 'common', + sat_coinbase_height: 110, + recursive: false, + metadata: null, + parent: null, + }, + recursive_refs: [], + location: { + genesis_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', + block_height: 830000, + block_hash: '00000000000000000002c5c0aba96f981642a6dca109e6b3564925c21a98aa3e', + tx_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', + tx_index: 0, + address: 'bc1pdjd6q33l0ca9nuudu2hr5qrs9u5dt6nl0z7fvu8kv4y8w4fzdpysc80028', + output: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc:0', + offset: '0', + prev_output: null, + prev_offset: null, + value: '9999', + transfer_type: DbLocationTransferType.transferred, + block_transfer_index: null, + timestamp: 1091091019, + }, }; expect(brc20FromInscription(insert)).toBeUndefined(); - insert.content_type = 'application/json'; - insert.mime_type = 'application/json'; + insert.inscription.content_type = 'application/json'; + insert.inscription.mime_type = 'application/json'; expect(brc20FromInscription(insert)).not.toBeUndefined(); - insert.content_type = 'text/plain;charset=utf-8'; - insert.mime_type = 'text/plain'; + insert.inscription.content_type = 'text/plain;charset=utf-8'; + insert.inscription.mime_type = 'text/plain'; expect(brc20FromInscription(insert)).not.toBeUndefined(); }); @@ -149,22 +186,139 @@ describe('BRC-20', () => { '{"p": "brc-20", "op": "deploy", "tick": "PEPE", "max": "21000000"', 'utf-8' ); - const insert: InscriptionData = { - genesis_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', - number: 0, - classic_number: 0, - mime_type: 'application/json', - content_type: 'application/json', - content_length: content.length, - content: `0x${content.toString('hex')}`, - fee: '200', - curse_type: null, - sat_ordinal: '2000000', - sat_rarity: 'common', - sat_coinbase_height: 110, - recursive: false, - metadata: null, - parent: null, + const insert: InscriptionRevealData = { + inscription: { + genesis_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', + number: 0, + classic_number: 0, + mime_type: 'application/json', + content_type: 'application/json', + content_length: content.length, + content: `0x${content.toString('hex')}`, + fee: '200', + curse_type: null, + sat_ordinal: '2000000', + sat_rarity: 'common', + sat_coinbase_height: 110, + recursive: false, + metadata: null, + parent: null, + }, + recursive_refs: [], + location: { + genesis_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', + block_height: 830000, + block_hash: '00000000000000000002c5c0aba96f981642a6dca109e6b3564925c21a98aa3e', + tx_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', + tx_index: 0, + address: 'bc1pdjd6q33l0ca9nuudu2hr5qrs9u5dt6nl0z7fvu8kv4y8w4fzdpysc80028', + output: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc:0', + offset: '0', + prev_output: null, + prev_offset: null, + value: '9999', + transfer_type: DbLocationTransferType.transferred, + block_transfer_index: null, + timestamp: 1091091019, + }, + }; + expect(brc20FromInscription(insert)).toBeUndefined(); + }); + + test('ignores inscriptions spent as fees', () => { + const content = Buffer.from( + JSON.stringify({ + p: 'brc-20', + op: 'deploy', + tick: 'PEPE', + max: '21000000', + }), + 'utf-8' + ); + const insert: InscriptionRevealData = { + inscription: { + genesis_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', + number: 0, + classic_number: 0, + mime_type: 'application/json', + content_type: 'application/json', + content_length: content.length, + content: `0x${content.toString('hex')}`, + fee: '200', + curse_type: null, + sat_ordinal: '2000000', + sat_rarity: 'common', + sat_coinbase_height: 110, + recursive: false, + metadata: null, + parent: null, + }, + recursive_refs: [], + location: { + genesis_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', + block_height: 830000, + block_hash: '00000000000000000002c5c0aba96f981642a6dca109e6b3564925c21a98aa3e', + tx_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', + tx_index: 0, + address: '', + output: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc:0', + offset: '0', + prev_output: null, + prev_offset: null, + value: '0', + transfer_type: DbLocationTransferType.spentInFees, + block_transfer_index: null, + timestamp: 1091091019, + }, + }; + expect(brc20FromInscription(insert)).toBeUndefined(); + }); + + test('ignores burnt inscriptions', () => { + const content = Buffer.from( + JSON.stringify({ + p: 'brc-20', + op: 'deploy', + tick: 'PEPE', + max: '21000000', + }), + 'utf-8' + ); + const insert: InscriptionRevealData = { + inscription: { + genesis_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', + number: 0, + classic_number: 0, + mime_type: 'application/json', + content_type: 'application/json', + content_length: content.length, + content: `0x${content.toString('hex')}`, + fee: '200', + curse_type: null, + sat_ordinal: '2000000', + sat_rarity: 'common', + sat_coinbase_height: 110, + recursive: false, + metadata: null, + parent: null, + }, + recursive_refs: [], + location: { + genesis_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', + block_height: 830000, + block_hash: '00000000000000000002c5c0aba96f981642a6dca109e6b3564925c21a98aa3e', + tx_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', + tx_index: 0, + address: '', + output: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc:0', + offset: '0', + prev_output: null, + prev_offset: null, + value: '1000', + transfer_type: DbLocationTransferType.burnt, + block_transfer_index: null, + timestamp: 1091091019, + }, }; expect(brc20FromInscription(insert)).toBeUndefined(); }); @@ -189,7 +343,7 @@ describe('BRC-20', () => { expect(brc20FromInscription(insert)).toBeUndefined(); }); - test('tick must be 4 bytes wide', () => { + test('tick must be 4 or 5 bytes wide', () => { const insert = testInsert({ p: 'brc-20', op: 'deploy', @@ -220,6 +374,41 @@ describe('BRC-20', () => { expect(brc20FromInscription(insert4)).toBeUndefined(); }); + test('deploy self_mint tick must be 5 bytes wide', () => { + const insert = testInsert( + { + p: 'brc-20', + op: 'deploy', + tick: '$PEPE', // 5 bytes + max: '21000000', + self_mint: 'true', + }, + 840000 + ); + expect(brc20FromInscription(insert)).not.toBeUndefined(); + const insert2 = testInsert( + { + p: 'brc-20', + op: 'deploy', + tick: '$PEPE', // 5 bytes but no self_mint + max: '21000000', + }, + 840000 + ); + expect(brc20FromInscription(insert2)).toBeUndefined(); + const insert4 = testInsert( + { + p: 'brc-20', + op: 'deploy', + tick: '$PEPE', // Correct but earlier than activation + max: '21000000', + self_mint: 'true', + }, + 820000 + ); + expect(brc20FromInscription(insert4)).toBeUndefined(); + }); + test('all fields must be strings', () => { const insert1 = testInsert({ p: 'brc-20', @@ -347,6 +536,17 @@ describe('BRC-20', () => { }); // `dec` can have a value of 0 expect(brc20FromInscription(insert1c)).not.toBeUndefined(); + const insert1d = testInsert( + { + p: 'brc-20', + op: 'deploy', + tick: '$PEPE', + max: '0', // self mints can be max 0 + self_mint: 'true', + }, + 840000 + ); + expect(brc20FromInscription(insert1d)).not.toBeUndefined(); const insert2a = testInsert({ p: 'brc-20', op: 'mint', @@ -529,10 +729,102 @@ describe('BRC-20', () => { deploy_timestamp: 1677811111000, minted_supply: '0.000000000000000000', tx_count: 1, + self_mint: false, }, ]); }); + test('deploy with self_mint is ignored before activation height', async () => { + await db.updateInscriptions( + new TestChainhookPayloadBuilder() + .apply() + .block({ + height: BRC20_GENESIS_BLOCK, + hash: '00000000000000000002a90330a99f67e3f01eb2ce070b45930581e82fb7a91d', + timestamp: 1677811111, + }) + .transaction({ + hash: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', + }) + .inscriptionRevealed( + brc20Reveal({ + json: { + p: 'brc-20', + op: 'deploy', + tick: '$PEPE', + max: '21000000', + self_mint: 'true', + }, + number: 0, + ordinal_number: 0, + tx_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', + address: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td', + }) + ) + .build() + ); + const response1 = await fastify.inject({ + method: 'GET', + url: `/ordinals/brc-20/tokens?ticker=$PEPE`, + }); + expect(response1.statusCode).toBe(200); + const responseJson1 = response1.json(); + expect(responseJson1.total).toBe(0); + }); + + test('deploy with self_mint is saved', async () => { + await db.updateInscriptions( + new TestChainhookPayloadBuilder() + .apply() + .block({ + height: BRC20_SELF_MINT_ACTIVATION_BLOCK, + hash: '00000000000000000002a90330a99f67e3f01eb2ce070b45930581e82fb7a91d', + timestamp: 1677811111, + }) + .transaction({ + hash: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', + }) + .inscriptionRevealed( + brc20Reveal({ + json: { + p: 'brc-20', + op: 'deploy', + tick: '$PEPE', + max: '21000000', + self_mint: 'true', + }, + number: 0, + ordinal_number: 0, + tx_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', + address: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td', + }) + ) + .build() + ); + const response1 = await fastify.inject({ + method: 'GET', + url: `/ordinals/brc-20/tokens?ticker=$PEPE`, + }); + expect(response1.statusCode).toBe(200); + const responseJson1 = response1.json(); + expect(responseJson1.total).toBe(1); + expect(responseJson1.results[0]).toStrictEqual({ + address: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td', + block_height: 837090, + decimals: 18, + deploy_timestamp: 1677811111000, + id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', + max_supply: '21000000.000000000000000000', + mint_limit: null, + self_mint: true, + minted_supply: '0.000000000000000000', + number: 0, + ticker: '$PEPE', + tx_count: 1, + tx_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', + }); + }); + test('ignores deploys for existing token', async () => { await db.updateInscriptions( new TestChainhookPayloadBuilder() @@ -607,6 +899,7 @@ describe('BRC-20', () => { deploy_timestamp: 1677803510000, minted_supply: '0.000000000000000000', tx_count: 1, + self_mint: false, }, ]); }); @@ -685,6 +978,7 @@ describe('BRC-20', () => { deploy_timestamp: 1677803510000, minted_supply: '0.000000000000000000', tx_count: 1, + self_mint: false, }, ]); const response2 = await fastify.inject({ @@ -708,6 +1002,7 @@ describe('BRC-20', () => { deploy_timestamp: 1677803510000, minted_supply: '0.000000000000000000', tx_count: 1, + self_mint: false, }, ]); }); @@ -881,13 +1176,13 @@ describe('BRC-20', () => { ); }); - test('rollback mints deduct balance correctly', async () => { + test('valid self mints are saved and balance reflected', async () => { const address = 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td'; await db.updateInscriptions( new TestChainhookPayloadBuilder() .apply() .block({ - height: BRC20_GENESIS_BLOCK, + height: BRC20_SELF_MINT_ACTIVATION_BLOCK, hash: '00000000000000000002a90330a99f67e3f01eb2ce070b45930581e82fb7a91d', }) .transaction({ @@ -898,8 +1193,9 @@ describe('BRC-20', () => { json: { p: 'brc-20', op: 'deploy', - tick: 'PEPE', + tick: '$PEPE', max: '21000000', + self_mint: 'true', }, number: 0, ordinal_number: 0, @@ -913,7 +1209,7 @@ describe('BRC-20', () => { new TestChainhookPayloadBuilder() .apply() .block({ - height: BRC20_GENESIS_BLOCK + 1, + height: BRC20_SELF_MINT_ACTIVATION_BLOCK + 1, hash: '0000000000000000000098d8f2663891d439f6bb7de230d4e9f6bcc2e85452bf', }) .transaction({ @@ -924,40 +1220,59 @@ describe('BRC-20', () => { json: { p: 'brc-20', op: 'mint', - tick: 'PEPE', + tick: '$PEPE', amt: '250000', }, number: 1, ordinal_number: 1, tx_id: '8aec77f855549d98cb9fb5f35e02a03f9a2354fd05a5f89fc610b32c3b01f99f', address: address, + parent: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', }) ) .build() ); - // Rollback + + const response1 = await fastify.inject({ + method: 'GET', + url: `/ordinals/brc-20/balances/${address}`, + }); + expect(response1.statusCode).toBe(200); + const responseJson1 = response1.json(); + expect(responseJson1.total).toBe(1); + expect(responseJson1.results).toStrictEqual([ + { + ticker: '$PEPE', + available_balance: '250000.000000000000000000', + overall_balance: '250000.000000000000000000', + transferrable_balance: '0.000000000000000000', + }, + ]); + + // New mint await db.updateInscriptions( new TestChainhookPayloadBuilder() - .rollback() + .apply() .block({ - height: BRC20_GENESIS_BLOCK + 2, + height: BRC20_SELF_MINT_ACTIVATION_BLOCK + 2, hash: '0000000000000000000077163227125e51d838787d6af031bc9b55a3a1cc1b2c', }) .transaction({ - hash: '8aec77f855549d98cb9fb5f35e02a03f9a2354fd05a5f89fc610b32c3b01f99f', + hash: '7a1adbc3e93ddf8d7c4e0ba75aa11c98c431521dd850be8b955feedb716d8bec', }) .inscriptionRevealed( brc20Reveal({ json: { p: 'brc-20', op: 'mint', - tick: 'PEPE', - amt: '250000', + tick: '$pepe', + amt: '100000', }, - number: 1, - ordinal_number: 1, - tx_id: '8aec77f855549d98cb9fb5f35e02a03f9a2354fd05a5f89fc610b32c3b01f99f', - address: address, + number: 2, + ordinal_number: 2, + tx_id: '7a1adbc3e93ddf8d7c4e0ba75aa11c98c431521dd850be8b955feedb716d8bec', + address: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td', + parent: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', }) ) .build() @@ -969,23 +1284,37 @@ describe('BRC-20', () => { }); expect(response2.statusCode).toBe(200); const responseJson2 = response2.json(); - expect(responseJson2.total).toBe(0); - expect(responseJson2.results).toStrictEqual([]); + expect(responseJson2.total).toBe(1); + expect(responseJson2.results).toStrictEqual([ + { + ticker: '$PEPE', + available_balance: '350000.000000000000000000', + overall_balance: '350000.000000000000000000', + transferrable_balance: '0.000000000000000000', + }, + ]); const response3 = await fastify.inject({ method: 'GET', - url: `/ordinals/brc-20/tokens/PEPE`, + url: `/ordinals/brc-20/tokens?ticker=$PEPE`, }); - expect(response3.json().token.minted_supply).toBe('0.000000000000000000'); + expect(response3.statusCode).toBe(200); + const responseJson3 = response3.json(); + expect(responseJson3.total).toBe(1); + expect(responseJson3.results).toEqual( + expect.arrayContaining([ + expect.objectContaining({ ticker: '$PEPE', minted_supply: '350000.000000000000000000' }), + ]) + ); }); - test('numbers should not have more decimal digits than "dec" of ticker', async () => { + test('self mints with invalid parent inscription are ignored', async () => { const address = 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td'; await db.updateInscriptions( new TestChainhookPayloadBuilder() .apply() .block({ - height: BRC20_GENESIS_BLOCK, + height: BRC20_SELF_MINT_ACTIVATION_BLOCK, hash: '00000000000000000002a90330a99f67e3f01eb2ce070b45930581e82fb7a91d', }) .transaction({ @@ -996,9 +1325,9 @@ describe('BRC-20', () => { json: { p: 'brc-20', op: 'deploy', - tick: 'PEPE', + tick: '$PEPE', max: '21000000', - dec: '1', + self_mint: 'true', }, number: 0, ordinal_number: 0, @@ -1012,7 +1341,7 @@ describe('BRC-20', () => { new TestChainhookPayloadBuilder() .apply() .block({ - height: BRC20_GENESIS_BLOCK + 1, + height: BRC20_SELF_MINT_ACTIVATION_BLOCK + 1, hash: '0000000000000000000098d8f2663891d439f6bb7de230d4e9f6bcc2e85452bf', }) .transaction({ @@ -1023,35 +1352,48 @@ describe('BRC-20', () => { json: { p: 'brc-20', op: 'mint', - tick: 'PEPE', - amt: '250000.000', // Invalid decimal count + tick: '$PEPE', + amt: '250000', }, number: 1, ordinal_number: 1, tx_id: '8aec77f855549d98cb9fb5f35e02a03f9a2354fd05a5f89fc610b32c3b01f99f', address: address, + // no parent }) ) .build() ); - const response2 = await fastify.inject({ + const response1 = await fastify.inject({ method: 'GET', url: `/ordinals/brc-20/balances/${address}`, }); - expect(response2.statusCode).toBe(200); - const responseJson2 = response2.json(); - expect(responseJson2.total).toBe(0); - expect(responseJson2.results).toStrictEqual([]); + expect(response1.statusCode).toBe(200); + const responseJson1 = response1.json(); + expect(responseJson1.total).toBe(0); + + const response3 = await fastify.inject({ + method: 'GET', + url: `/ordinals/brc-20/tokens?ticker=$PEPE`, + }); + expect(response3.statusCode).toBe(200); + const responseJson3 = response3.json(); + expect(responseJson3.total).toBe(1); + expect(responseJson3.results).toEqual( + expect.arrayContaining([ + expect.objectContaining({ ticker: '$PEPE', minted_supply: '0.000000000000000000' }), + ]) + ); }); - test('mint exceeds token supply', async () => { + test('valid self mints for tokens with max 0 are saved', async () => { const address = 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td'; await db.updateInscriptions( new TestChainhookPayloadBuilder() .apply() .block({ - height: BRC20_GENESIS_BLOCK, + height: BRC20_SELF_MINT_ACTIVATION_BLOCK, hash: '00000000000000000002a90330a99f67e3f01eb2ce070b45930581e82fb7a91d', }) .transaction({ @@ -1062,9 +1404,9 @@ describe('BRC-20', () => { json: { p: 'brc-20', op: 'deploy', - tick: 'PEPE', - max: '2500', - dec: '1', + tick: '$PEPE', + max: '0', + self_mint: 'true', }, number: 0, ordinal_number: 0, @@ -1078,32 +1420,328 @@ describe('BRC-20', () => { new TestChainhookPayloadBuilder() .apply() .block({ - height: BRC20_GENESIS_BLOCK + 1, + height: BRC20_SELF_MINT_ACTIVATION_BLOCK + 1, hash: '0000000000000000000098d8f2663891d439f6bb7de230d4e9f6bcc2e85452bf', }) .transaction({ - hash: '3b55f624eaa4f8de6c42e0c490176b67123a83094384f658611faf7bfb85dd0f', + hash: '8aec77f855549d98cb9fb5f35e02a03f9a2354fd05a5f89fc610b32c3b01f99f', }) .inscriptionRevealed( brc20Reveal({ json: { p: 'brc-20', op: 'mint', - tick: 'PEPE', - amt: '1000', + tick: '$PEPE', + amt: '250000', }, number: 1, ordinal_number: 1, - tx_id: '3b55f624eaa4f8de6c42e0c490176b67123a83094384f658611faf7bfb85dd0f', + tx_id: '8aec77f855549d98cb9fb5f35e02a03f9a2354fd05a5f89fc610b32c3b01f99f', address: address, + parent: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', }) ) - .transaction({ - hash: '7e09bda2cba34bca648cca6d79a074940d39b6137150d3a3edcf80c0e01419a5', - }) - .inscriptionRevealed( - brc20Reveal({ - json: { + .build() + ); + + const response1 = await fastify.inject({ + method: 'GET', + url: `/ordinals/brc-20/balances/${address}`, + }); + expect(response1.statusCode).toBe(200); + const responseJson1 = response1.json(); + expect(responseJson1.total).toBe(1); + expect(responseJson1.results).toStrictEqual([ + { + ticker: '$PEPE', + available_balance: '250000.000000000000000000', + overall_balance: '250000.000000000000000000', + transferrable_balance: '0.000000000000000000', + }, + ]); + + // New mint + await db.updateInscriptions( + new TestChainhookPayloadBuilder() + .apply() + .block({ + height: BRC20_SELF_MINT_ACTIVATION_BLOCK + 2, + hash: '0000000000000000000077163227125e51d838787d6af031bc9b55a3a1cc1b2c', + }) + .transaction({ + hash: '7a1adbc3e93ddf8d7c4e0ba75aa11c98c431521dd850be8b955feedb716d8bec', + }) + .inscriptionRevealed( + brc20Reveal({ + json: { + p: 'brc-20', + op: 'mint', + tick: '$pepe', + amt: '100000', + }, + number: 2, + ordinal_number: 2, + tx_id: '7a1adbc3e93ddf8d7c4e0ba75aa11c98c431521dd850be8b955feedb716d8bec', + address: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td', + parent: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', + }) + ) + .build() + ); + + const response2 = await fastify.inject({ + method: 'GET', + url: `/ordinals/brc-20/balances/${address}`, + }); + expect(response2.statusCode).toBe(200); + const responseJson2 = response2.json(); + expect(responseJson2.total).toBe(1); + expect(responseJson2.results).toStrictEqual([ + { + ticker: '$PEPE', + available_balance: '350000.000000000000000000', + overall_balance: '350000.000000000000000000', + transferrable_balance: '0.000000000000000000', + }, + ]); + + const response3 = await fastify.inject({ + method: 'GET', + url: `/ordinals/brc-20/tokens?ticker=$PEPE`, + }); + expect(response3.statusCode).toBe(200); + const responseJson3 = response3.json(); + expect(responseJson3.total).toBe(1); + expect(responseJson3.results).toEqual( + expect.arrayContaining([ + expect.objectContaining({ ticker: '$PEPE', minted_supply: '350000.000000000000000000' }), + ]) + ); + }); + + test('rollback mints deduct balance correctly', async () => { + const address = 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td'; + await db.updateInscriptions( + new TestChainhookPayloadBuilder() + .apply() + .block({ + height: BRC20_GENESIS_BLOCK, + hash: '00000000000000000002a90330a99f67e3f01eb2ce070b45930581e82fb7a91d', + }) + .transaction({ + hash: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', + }) + .inscriptionRevealed( + brc20Reveal({ + json: { + p: 'brc-20', + op: 'deploy', + tick: 'PEPE', + max: '21000000', + }, + number: 0, + ordinal_number: 0, + tx_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', + address: address, + }) + ) + .build() + ); + await db.updateInscriptions( + new TestChainhookPayloadBuilder() + .apply() + .block({ + height: BRC20_GENESIS_BLOCK + 1, + hash: '0000000000000000000098d8f2663891d439f6bb7de230d4e9f6bcc2e85452bf', + }) + .transaction({ + hash: '8aec77f855549d98cb9fb5f35e02a03f9a2354fd05a5f89fc610b32c3b01f99f', + }) + .inscriptionRevealed( + brc20Reveal({ + json: { + p: 'brc-20', + op: 'mint', + tick: 'PEPE', + amt: '250000', + }, + number: 1, + ordinal_number: 1, + tx_id: '8aec77f855549d98cb9fb5f35e02a03f9a2354fd05a5f89fc610b32c3b01f99f', + address: address, + }) + ) + .build() + ); + // Rollback + await db.updateInscriptions( + new TestChainhookPayloadBuilder() + .rollback() + .block({ + height: BRC20_GENESIS_BLOCK + 2, + hash: '0000000000000000000077163227125e51d838787d6af031bc9b55a3a1cc1b2c', + }) + .transaction({ + hash: '8aec77f855549d98cb9fb5f35e02a03f9a2354fd05a5f89fc610b32c3b01f99f', + }) + .inscriptionRevealed( + brc20Reveal({ + json: { + p: 'brc-20', + op: 'mint', + tick: 'PEPE', + amt: '250000', + }, + number: 1, + ordinal_number: 1, + tx_id: '8aec77f855549d98cb9fb5f35e02a03f9a2354fd05a5f89fc610b32c3b01f99f', + address: address, + }) + ) + .build() + ); + + const response2 = await fastify.inject({ + method: 'GET', + url: `/ordinals/brc-20/balances/${address}`, + }); + expect(response2.statusCode).toBe(200); + const responseJson2 = response2.json(); + expect(responseJson2.total).toBe(0); + expect(responseJson2.results).toStrictEqual([]); + + const response3 = await fastify.inject({ + method: 'GET', + url: `/ordinals/brc-20/tokens/PEPE`, + }); + expect(response3.json().token.minted_supply).toBe('0.000000000000000000'); + }); + + test('numbers should not have more decimal digits than "dec" of ticker', async () => { + const address = 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td'; + await db.updateInscriptions( + new TestChainhookPayloadBuilder() + .apply() + .block({ + height: BRC20_GENESIS_BLOCK, + hash: '00000000000000000002a90330a99f67e3f01eb2ce070b45930581e82fb7a91d', + }) + .transaction({ + hash: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', + }) + .inscriptionRevealed( + brc20Reveal({ + json: { + p: 'brc-20', + op: 'deploy', + tick: 'PEPE', + max: '21000000', + dec: '1', + }, + number: 0, + ordinal_number: 0, + tx_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', + address: address, + }) + ) + .build() + ); + await db.updateInscriptions( + new TestChainhookPayloadBuilder() + .apply() + .block({ + height: BRC20_GENESIS_BLOCK + 1, + hash: '0000000000000000000098d8f2663891d439f6bb7de230d4e9f6bcc2e85452bf', + }) + .transaction({ + hash: '8aec77f855549d98cb9fb5f35e02a03f9a2354fd05a5f89fc610b32c3b01f99f', + }) + .inscriptionRevealed( + brc20Reveal({ + json: { + p: 'brc-20', + op: 'mint', + tick: 'PEPE', + amt: '250000.000', // Invalid decimal count + }, + number: 1, + ordinal_number: 1, + tx_id: '8aec77f855549d98cb9fb5f35e02a03f9a2354fd05a5f89fc610b32c3b01f99f', + address: address, + }) + ) + .build() + ); + + const response2 = await fastify.inject({ + method: 'GET', + url: `/ordinals/brc-20/balances/${address}`, + }); + expect(response2.statusCode).toBe(200); + const responseJson2 = response2.json(); + expect(responseJson2.total).toBe(0); + expect(responseJson2.results).toStrictEqual([]); + }); + + test('mint exceeds token supply', async () => { + const address = 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td'; + await db.updateInscriptions( + new TestChainhookPayloadBuilder() + .apply() + .block({ + height: BRC20_GENESIS_BLOCK, + hash: '00000000000000000002a90330a99f67e3f01eb2ce070b45930581e82fb7a91d', + }) + .transaction({ + hash: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', + }) + .inscriptionRevealed( + brc20Reveal({ + json: { + p: 'brc-20', + op: 'deploy', + tick: 'PEPE', + max: '2500', + dec: '1', + }, + number: 0, + ordinal_number: 0, + tx_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', + address: address, + }) + ) + .build() + ); + await db.updateInscriptions( + new TestChainhookPayloadBuilder() + .apply() + .block({ + height: BRC20_GENESIS_BLOCK + 1, + hash: '0000000000000000000098d8f2663891d439f6bb7de230d4e9f6bcc2e85452bf', + }) + .transaction({ + hash: '3b55f624eaa4f8de6c42e0c490176b67123a83094384f658611faf7bfb85dd0f', + }) + .inscriptionRevealed( + brc20Reveal({ + json: { + p: 'brc-20', + op: 'mint', + tick: 'PEPE', + amt: '1000', + }, + number: 1, + ordinal_number: 1, + tx_id: '3b55f624eaa4f8de6c42e0c490176b67123a83094384f658611faf7bfb85dd0f', + address: address, + }) + ) + .transaction({ + hash: '7e09bda2cba34bca648cca6d79a074940d39b6137150d3a3edcf80c0e01419a5', + }) + .inscriptionRevealed( + brc20Reveal({ + json: { p: 'brc-20', op: 'mint', tick: 'PEPE', @@ -1609,6 +2247,145 @@ describe('BRC-20', () => { expect(prevBlockJson2.results[0]).toBeUndefined(); }); + test('send balance for self_mint token to address', async () => { + const address = 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td'; + const address2 = '3QNjwPDRafjBm9XxJpshgk3ksMJh3TFxTU'; + await db.updateInscriptions( + new TestChainhookPayloadBuilder() + .apply() + .block({ + height: BRC20_SELF_MINT_ACTIVATION_BLOCK, + hash: '00000000000000000002a90330a99f67e3f01eb2ce070b45930581e82fb7a91d', + }) + .transaction({ + hash: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', + }) + .inscriptionRevealed( + brc20Reveal({ + json: { + p: 'brc-20', + op: 'deploy', + tick: '$PEPE', + max: '0', + self_mint: 'true', + }, + number: 0, + ordinal_number: 0, + tx_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', + address: address, + }) + ) + .build() + ); + await db.updateInscriptions( + new TestChainhookPayloadBuilder() + .apply() + .block({ + height: BRC20_SELF_MINT_ACTIVATION_BLOCK + 1, + hash: '0000000000000000000098d8f2663891d439f6bb7de230d4e9f6bcc2e85452bf', + }) + .transaction({ + hash: '3b55f624eaa4f8de6c42e0c490176b67123a83094384f658611faf7bfb85dd0f', + }) + .inscriptionRevealed( + brc20Reveal({ + json: { + p: 'brc-20', + op: 'mint', + tick: '$PEPE', + amt: '10000', + }, + number: 1, + ordinal_number: 1, + tx_id: '3b55f624eaa4f8de6c42e0c490176b67123a83094384f658611faf7bfb85dd0f', + address: address, + parent: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', + }) + ) + .build() + ); + await db.updateInscriptions( + new TestChainhookPayloadBuilder() + .apply() + .block({ + height: BRC20_SELF_MINT_ACTIVATION_BLOCK + 2, + hash: '00000000000000000002b14f0c5dde0b2fc74d022e860696bd64f1f652756674', + }) + .transaction({ + hash: 'eee52b22397ea4a4aefe6a39931315e93a157091f5a994216c0aa9c8c6fef47a', + }) + .inscriptionRevealed( + brc20Reveal({ + json: { + p: 'brc-20', + op: 'transfer', + tick: '$PEPE', + amt: '9000', + }, + number: 2, + ordinal_number: 2, + tx_id: 'eee52b22397ea4a4aefe6a39931315e93a157091f5a994216c0aa9c8c6fef47a', + address: address, + }) + ) + .build() + ); + await db.updateInscriptions( + new TestChainhookPayloadBuilder() + .apply() + .block({ + height: BRC20_SELF_MINT_ACTIVATION_BLOCK + 3, + hash: '00000000000000000003feae13d107f0f2c4fb4dd08fb2a8b1ab553512e77f03', + }) + .transaction({ + hash: '7edaa48337a94da327b6262830505f116775a32db5ad4ad46e87ecea33f21bac', + }) + .inscriptionTransferred({ + ordinal_number: 2, + destination: { type: 'transferred', value: address2 }, + satpoint_pre_transfer: + 'eee52b22397ea4a4aefe6a39931315e93a157091f5a994216c0aa9c8c6fef47a:0:0', + satpoint_post_transfer: + '7edaa48337a94da327b6262830505f116775a32db5ad4ad46e87ecea33f21bac:0:0', + post_transfer_output_value: null, + tx_index: 0, + }) + .build() + ); + + const response1 = await fastify.inject({ + method: 'GET', + url: `/ordinals/brc-20/balances/${address}`, + }); + expect(response1.statusCode).toBe(200); + const json1 = response1.json(); + expect(json1.total).toBe(1); + expect(json1.results).toStrictEqual([ + { + available_balance: '1000.000000000000000000', + overall_balance: '1000.000000000000000000', + ticker: '$PEPE', + transferrable_balance: '0.000000000000000000', + }, + ]); + + const response2 = await fastify.inject({ + method: 'GET', + url: `/ordinals/brc-20/balances/${address2}`, + }); + expect(response2.statusCode).toBe(200); + const json2 = response2.json(); + expect(json2.total).toBe(1); + expect(json2.results).toStrictEqual([ + { + available_balance: '9000.000000000000000000', + overall_balance: '9000.000000000000000000', + ticker: '$PEPE', + transferrable_balance: '0.000000000000000000', + }, + ]); + }); + test('sending transfer as fee returns amount to sender', async () => { const address = 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td'; await deployAndMintPEPE(address); @@ -2042,6 +2819,7 @@ describe('BRC-20', () => { deploy_timestamp: 1677803510000, minted_supply: '0.000000000000000000', tx_count: 1, + self_mint: false, }, supply: { max_supply: '21000000.000000000000000000', diff --git a/tests/helpers.ts b/tests/helpers.ts index f26f86b3..f2c70674 100644 --- a/tests/helpers.ts +++ b/tests/helpers.ts @@ -115,6 +115,7 @@ export function brc20Reveal(args: { address: string; tx_id: string; ordinal_number: number; + parent?: string; }): BitcoinInscriptionRevealed { const content = Buffer.from(JSON.stringify(args.json), 'utf-8'); const reveal: BitcoinInscriptionRevealed = { @@ -141,7 +142,7 @@ export function brc20Reveal(args: { delegate: null, metaprotocol: null, metadata: undefined, - parent: null, + parent: args.parent ?? null, }; return reveal; } diff --git a/tests/ordhook/server.test.ts b/tests/ordhook/server.test.ts index 09c96a88..e8035854 100644 --- a/tests/ordhook/server.test.ts +++ b/tests/ordhook/server.test.ts @@ -435,307 +435,117 @@ describe('EventServer', () => { }, ]); }); - }); - describe('gap detection', () => { - test('server rejects payload with first inscription gap', async () => { + test('inscriptions revealed as fee', async () => { await db.updateInscriptions( new TestChainhookPayloadBuilder() .apply() .block({ - height: 778575, - hash: '0x00000000000000000002a90330a99f67e3f01eb2ce070b45930581e82fb7a91d', - timestamp: 1676913207, + height: 832574, + hash: '000000000000000000020c8145de25b1e1e0a6312e377827a3015e15fdd574cd', }) .transaction({ - hash: '9f4a9b73b0713c5da01c0a47f97c6c001af9028d6bdd9e264dfacbc4e6790201', + hash: '53957f47697096cef4ad24dae6357b3d7ffe1e3eb9216ce0bb01d6b6a2c8cf4a', }) .inscriptionRevealed({ - content_bytes: '0x48656C6C6F', - content_type: 'text/plain;charset=utf-8', - content_length: 5, - inscription_number: { classic: 0, jubilee: 0 }, - inscription_fee: 705, - inscription_id: '9f4a9b73b0713c5da01c0a47f97c6c001af9028d6bdd9e264dfacbc4e6790201i0', - inscription_output_value: 10000, - inscriber_address: 'bc1pscktlmn99gyzlvymvrezh6vwd0l4kg06tg5rvssw0czg8873gz5sdkteqj', - ordinal_number: 257418248345364, - ordinal_block_height: 650000, - ordinal_offset: 0, - satpoint_post_inscription: - '9f4a9b73b0713c5da01c0a47f97c6c001af9028d6bdd9e264dfacbc4e6790201:0:0', - inscription_input_index: 0, - transfers_pre_inscription: 0, - tx_index: 0, + content_bytes: + '0x7b2270223a226272632d3230222c226f70223a226d696e74222c227469636b223a22656f7262222c22616d74223a223130227d', + content_length: 51, + content_type: 'text/plain', curse_type: null, - inscription_pointer: null, - delegate: null, - metaprotocol: null, + delegate: '', + inscriber_address: '', + inscription_fee: 3210, + inscription_id: '53957f47697096cef4ad24dae6357b3d7ffe1e3eb9216ce0bb01d6b6a2c8cf4ai0', + inscription_input_index: 0, + inscription_number: { + classic: 0, + jubilee: 0, + }, + inscription_output_value: 0, + inscription_pointer: 1, metadata: null, - parent: null, - }) - .build() - ); - const errorPayload = new TestChainhookPayloadBuilder() - .apply() - .block({ - height: 778576, - hash: '00000000000000000002a90330a99f67e3f01eb2ce070b45930581e82fb7a91d', - timestamp: 1676913207, - }) - .transaction({ - hash: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', - }) - .inscriptionRevealed({ - content_bytes: '0x48656C6C6F', - content_type: 'text/plain;charset=utf-8', - content_length: 5, - inscription_number: { classic: 5, jubilee: 5 }, // Gap at 5 - inscription_fee: 705, - inscription_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', - inscription_output_value: 10000, - inscriber_address: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td', - ordinal_number: 1050000000000000, - ordinal_block_height: 650000, - ordinal_offset: 0, - satpoint_post_inscription: - '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc:0:0', - inscription_input_index: 0, - transfers_pre_inscription: 0, - tx_index: 0, - curse_type: null, - inscription_pointer: null, - delegate: null, - metaprotocol: null, - metadata: null, - parent: null, - }) - .build(); - await expect(db.updateInscriptions(errorPayload)).rejects.toThrow(BadPayloadRequestError); - const response = await server['fastify'].inject({ - method: 'POST', - url: `/payload`, - headers: { authorization: `Bearer ${ENV.ORDHOOK_NODE_AUTH_TOKEN}` }, - payload: errorPayload, - }); - expect(response.statusCode).toBe(400); - }); - - test('server rejects payload with intermediate inscription gap', async () => { - await db.updateInscriptions( - new TestChainhookPayloadBuilder() - .apply() - .block({ - height: 778575, - hash: '0x00000000000000000002a90330a99f67e3f01eb2ce070b45930581e82fb7a91d', - timestamp: 1676913207, - }) - .transaction({ - hash: '9f4a9b73b0713c5da01c0a47f97c6c001af9028d6bdd9e264dfacbc4e6790201', - }) - .inscriptionRevealed({ - content_bytes: '0x48656C6C6F', - content_type: 'text/plain;charset=utf-8', - content_length: 5, - inscription_number: { classic: 0, jubilee: 0 }, - inscription_fee: 705, - inscription_id: '9f4a9b73b0713c5da01c0a47f97c6c001af9028d6bdd9e264dfacbc4e6790201i0', - inscription_output_value: 10000, - inscriber_address: 'bc1pscktlmn99gyzlvymvrezh6vwd0l4kg06tg5rvssw0czg8873gz5sdkteqj', - ordinal_number: 257418248345364, - ordinal_block_height: 650000, - ordinal_offset: 0, + metaprotocol: '', + ordinal_block_height: 203651, + ordinal_number: 1018259086681705, + ordinal_offset: 4086681705, + parent: '', satpoint_post_inscription: - '9f4a9b73b0713c5da01c0a47f97c6c001af9028d6bdd9e264dfacbc4e6790201:0:0', - inscription_input_index: 0, + '53957f47697096cef4ad24dae6357b3d7ffe1e3eb9216ce0bb01d6b6a2c8cf4a:0:665136296', transfers_pre_inscription: 0, - tx_index: 0, - curse_type: null, - inscription_pointer: null, - delegate: null, - metaprotocol: null, - metadata: null, - parent: null, + tx_index: 2486, }) .build() ); - const errorPayload = new TestChainhookPayloadBuilder() - .apply() - .block({ - height: 778576, - hash: '00000000000000000002a90330a99f67e3f01eb2ce070b45930581e82fb7a91d', - timestamp: 1676913207, - }) - .transaction({ - hash: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', - }) - .inscriptionRevealed({ - content_bytes: '0x48656C6C6F', - content_type: 'text/plain;charset=utf-8', - content_length: 5, - inscription_number: { classic: 1, jubilee: 1 }, - inscription_fee: 705, - inscription_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', - inscription_output_value: 10000, - inscriber_address: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td', - ordinal_number: 1050000000000000, - ordinal_block_height: 650000, - ordinal_offset: 0, - satpoint_post_inscription: - '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc:0:0', - inscription_input_index: 0, - transfers_pre_inscription: 0, - tx_index: 0, - curse_type: null, - inscription_pointer: null, - delegate: null, - metaprotocol: null, - metadata: null, - parent: null, - }) - .transaction({ - hash: '6891d374a17ba85f6b5514f2f7edc301c1c860284dff5a5c6e88ab3a20fcd8a5', - }) - .inscriptionRevealed({ - content_bytes: '0x48656C6C6F', - content_type: 'text/plain;charset=utf-8', - content_length: 5, - inscription_number: { classic: 4, jubilee: 4 }, // Gap - inscription_fee: 705, - inscription_id: '6891d374a17ba85f6b5514f2f7edc301c1c860284dff5a5c6e88ab3a20fcd8a5o0', - inscription_output_value: 10000, - inscriber_address: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td', - ordinal_number: 1050000000000000, - ordinal_block_height: 650000, - ordinal_offset: 0, - satpoint_post_inscription: - '6891d374a17ba85f6b5514f2f7edc301c1c860284dff5a5c6e88ab3a20fcd8a5:0:0', - inscription_input_index: 0, - transfers_pre_inscription: 0, - tx_index: 0, - curse_type: null, - inscription_pointer: null, - delegate: null, - metaprotocol: null, - metadata: null, - parent: null, - }) - .build(); - await expect(db.updateInscriptions(errorPayload)).rejects.toThrow(BadPayloadRequestError); - const response = await server['fastify'].inject({ - method: 'POST', - url: `/payload`, - headers: { authorization: `Bearer ${ENV.ORDHOOK_NODE_AUTH_TOKEN}` }, - payload: errorPayload, + const response = await fastify.inject({ + method: 'GET', + url: `/ordinals/inscriptions/53957f47697096cef4ad24dae6357b3d7ffe1e3eb9216ce0bb01d6b6a2c8cf4ai0`, }); - expect(response.statusCode).toBe(400); + expect(response.statusCode).toBe(200); + const status = await db.sql<{ transfer_type: string }[]>` + SELECT transfer_type + FROM locations + WHERE genesis_id = '53957f47697096cef4ad24dae6357b3d7ffe1e3eb9216ce0bb01d6b6a2c8cf4ai0' + `; + expect(status[0].transfer_type).toBe('spent_in_fees'); }); - test('server accepts payload with unordered unbound inscriptions', async () => { + test('inscriptions revealed as burnt', async () => { await db.updateInscriptions( new TestChainhookPayloadBuilder() .apply() .block({ - height: 778575, - hash: '0x00000000000000000002a90330a99f67e3f01eb2ce070b45930581e82fb7a91d', - timestamp: 1676913207, + height: 832574, + hash: '000000000000000000020c8145de25b1e1e0a6312e377827a3015e15fdd574cd', }) .transaction({ - hash: '9f4a9b73b0713c5da01c0a47f97c6c001af9028d6bdd9e264dfacbc4e6790201', + hash: '53957f47697096cef4ad24dae6357b3d7ffe1e3eb9216ce0bb01d6b6a2c8cf4a', }) .inscriptionRevealed({ - content_bytes: '0x48656C6C6F', - content_type: 'text/plain;charset=utf-8', - content_length: 5, - inscription_number: { classic: 0, jubilee: 0 }, - inscription_fee: 705, - inscription_id: '9f4a9b73b0713c5da01c0a47f97c6c001af9028d6bdd9e264dfacbc4e6790201i0', - inscription_output_value: 10000, - inscriber_address: 'bc1pscktlmn99gyzlvymvrezh6vwd0l4kg06tg5rvssw0czg8873gz5sdkteqj', - ordinal_number: 257418248345364, - ordinal_block_height: 650000, - ordinal_offset: 0, - satpoint_post_inscription: - '9f4a9b73b0713c5da01c0a47f97c6c001af9028d6bdd9e264dfacbc4e6790201:0:0', - inscription_input_index: 0, - transfers_pre_inscription: 0, - tx_index: 0, + content_bytes: + '0x7b2270223a226272632d3230222c226f70223a226d696e74222c227469636b223a22656f7262222c22616d74223a223130227d', + content_length: 51, + content_type: 'text/plain', curse_type: null, - inscription_pointer: null, - delegate: null, - metaprotocol: null, + delegate: '', + inscriber_address: '', + inscription_fee: 3210, + inscription_id: '53957f47697096cef4ad24dae6357b3d7ffe1e3eb9216ce0bb01d6b6a2c8cf4ai0', + inscription_input_index: 0, + inscription_number: { + classic: 0, + jubilee: 0, + }, + inscription_output_value: 1000, + inscription_pointer: 0, metadata: null, - parent: null, + metaprotocol: '', + ordinal_block_height: 203651, + ordinal_number: 1018259086681705, + ordinal_offset: 4086681705, + parent: '', + satpoint_post_inscription: + '53957f47697096cef4ad24dae6357b3d7ffe1e3eb9216ce0bb01d6b6a2c8cf4a:0:665136296', + transfers_pre_inscription: 0, + tx_index: 2486, }) .build() ); - const unboundPayload = new TestChainhookPayloadBuilder() - .apply() - .block({ - height: 778576, - hash: '00000000000000000002a90330a99f67e3f01eb2ce070b45930581e82fb7a91d', - timestamp: 1676913207, - }) - .transaction({ - hash: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', - }) - .inscriptionRevealed({ - content_bytes: '0x48656C6C6F', - content_type: 'text/plain;charset=utf-8', - content_length: 5, - inscription_number: { classic: 2, jubilee: 2 }, - inscription_fee: 705, - inscription_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', - inscription_output_value: 10000, - inscriber_address: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td', - ordinal_number: 1050000000000000, - ordinal_block_height: 650000, - ordinal_offset: 0, - satpoint_post_inscription: - '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc:0:0', - inscription_input_index: 0, - transfers_pre_inscription: 0, - tx_index: 0, - curse_type: null, - inscription_pointer: null, - delegate: null, - metaprotocol: null, - metadata: null, - parent: null, - }) - .transaction({ - hash: '6891d374a17ba85f6b5514f2f7edc301c1c860284dff5a5c6e88ab3a20fcd8a5', - }) - .inscriptionRevealed({ - content_bytes: '0x48656C6C6F', - content_type: 'text/plain;charset=utf-8', - content_length: 5, - inscription_number: { classic: 1, jubilee: 1 }, - inscription_fee: 705, - inscription_id: '6891d374a17ba85f6b5514f2f7edc301c1c860284dff5a5c6e88ab3a20fcd8a5o0', - inscription_output_value: 10000, - inscriber_address: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td', - ordinal_number: 0, // Unbounded - ordinal_block_height: 650000, - ordinal_offset: 0, - satpoint_post_inscription: - '6891d374a17ba85f6b5514f2f7edc301c1c860284dff5a5c6e88ab3a20fcd8a5:0:0', - inscription_input_index: 0, - transfers_pre_inscription: 0, - tx_index: 0, - curse_type: null, - inscription_pointer: null, - delegate: null, - metaprotocol: null, - metadata: null, - parent: null, - }) - .build(); - await expect(db.updateInscriptions(unboundPayload)).resolves.not.toThrow( - BadPayloadRequestError - ); + const response = await fastify.inject({ + method: 'GET', + url: `/ordinals/inscriptions/53957f47697096cef4ad24dae6357b3d7ffe1e3eb9216ce0bb01d6b6a2c8cf4ai0`, + }); + expect(response.statusCode).toBe(200); + const status = await db.sql<{ transfer_type: string }[]>` + SELECT transfer_type + FROM locations + WHERE genesis_id = '53957f47697096cef4ad24dae6357b3d7ffe1e3eb9216ce0bb01d6b6a2c8cf4ai0' + `; + expect(status[0].transfer_type).toBe('burnt'); }); + }); + describe('gap detection', () => { test('server ignores past blocks', async () => { const payload = new TestChainhookPayloadBuilder() .apply()