From d0a6d48d998ff74bd602bdf0e173e5b6583aef20 Mon Sep 17 00:00:00 2001 From: g0drlc Date: Thu, 7 Mar 2024 08:22:04 -0600 Subject: [PATCH 01/16] fix: use brc20_total_balances table when inserting new transfer (#321) --- src/pg/brc20/brc20-pg-store.ts | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/src/pg/brc20/brc20-pg-store.ts b/src/pg/brc20/brc20-pg-store.ts index 357795a..45745ba 100644 --- a/src/pg/brc20/brc20-pg-store.ts +++ b/src/pg/brc20/brc20-pg-store.ts @@ -340,20 +340,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) ( From 0375161b1440523b37fa342d06ed9042e875350d Mon Sep 17 00:00:00 2001 From: g0drlc Date: Thu, 7 Mar 2024 14:24:03 +0000 Subject: [PATCH 02/16] chore(release): 3.0.2-beta.1 [skip ci] ## [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)) --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fec752f..fd12d29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## [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) From 750f516dfe9a484794f44f0ccd8b0895fe68bb29 Mon Sep 17 00:00:00 2001 From: g0drlc Date: Thu, 7 Mar 2024 10:20:22 -0600 Subject: [PATCH 03/16] fix: filter correct content types for brc20 (#323) * fix: filter correct content types for brc20 * fix: filter correct content types for brc20 --- src/pg/brc20/brc20-pg-store.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pg/brc20/brc20-pg-store.ts b/src/pg/brc20/brc20-pg-store.ts index 45745ba..e04a573 100644 --- a/src/pg/brc20/brc20-pg-store.ts +++ b/src/pg/brc20/brc20-pg-store.ts @@ -9,7 +9,6 @@ import { DbPaginatedResult, InscriptionEventData, LocationData, - InscriptionRevealData, } from '../types'; import { BRC20_DEPLOYS_COLUMNS, @@ -50,7 +49,8 @@ export class Brc20PgStore extends BasePgStoreModule { if ( reveal.inscription.classic_number < 0 || reveal.inscription.number < 0 || - reveal.location.transfer_type != DbLocationTransferType.transferred + reveal.location.transfer_type != DbLocationTransferType.transferred || + !['text/plain', 'application/json'].includes(reveal.inscription.mime_type) ) continue; const brc20 = brc20FromInscriptionContent( From 50cb71ade0b4491b84d8c432ae1bdf20b6ebe36a Mon Sep 17 00:00:00 2001 From: g0drlc Date: Thu, 7 Mar 2024 16:22:50 +0000 Subject: [PATCH 04/16] chore(release): 3.0.2-beta.2 [skip ci] ## [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)) --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fd12d29..db38986 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## [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) From d1687c14411c88ab3e35960648b4b86c856150fe Mon Sep 17 00:00:00 2001 From: g0drlc Date: Thu, 7 Mar 2024 12:14:27 -0600 Subject: [PATCH 05/16] test: unify brc-20 parsing tests (#324) * test: unify brc-20 parsing tests * chore: remove unused import --- src/pg/brc20/brc20-pg-store.ts | 14 +-- src/pg/brc20/helpers.ts | 20 ++-- tests/brc-20/brc20.test.ts | 166 ++++++++++++++++++++++----------- 3 files changed, 122 insertions(+), 78 deletions(-) diff --git a/src/pg/brc20/brc20-pg-store.ts b/src/pg/brc20/brc20-pg-store.ts index e04a573..fa3d6da 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, @@ -26,7 +25,7 @@ import { DbBrc20TokenWithSupply, DbBrc20TransferEvent, } from './types'; -import { Brc20Deploy, Brc20Mint, Brc20Transfer, brc20FromInscriptionContent } from './helpers'; +import { Brc20Deploy, Brc20Mint, Brc20Transfer, brc20FromInscription } from './helpers'; import { Brc20TokenOrderBy } from '../../api/schemas'; import { objRemoveUndefinedValues } from '../helpers'; @@ -46,16 +45,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 || - !['text/plain', 'application/json'].includes(reveal.inscription.mime_type) - ) - continue; - const brc20 = brc20FromInscriptionContent( - hexToBuffer(reveal.inscription.content as string).toString('utf-8') - ); + const brc20 = brc20FromInscription(reveal); if (brc20) { switch (brc20.op) { case 'deploy': diff --git a/src/pg/brc20/helpers.ts b/src/pg/brc20/helpers.ts index 3b1f195..854609d 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+))$/); @@ -50,18 +50,16 @@ 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') +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; - const buf = hexToBuffer(inscription.content as string).toString('utf-8'); - return brc20FromInscriptionContent(buf); -} - -export function brc20FromInscriptionContent(content: string): Brc20 | undefined { 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; diff --git a/tests/brc-20/brc20.test.ts b/tests/brc-20/brc20.test.ts index 9f5a860..efedf8c 100644 --- a/tests/brc-20/brc20.test.ts +++ b/tests/brc-20/brc20.test.ts @@ -3,7 +3,7 @@ import { buildApiServer } from '../../src/api/init'; import { Brc20ActivityResponse, Brc20TokenResponse } from '../../src/api/schemas'; import { 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): 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: 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, + }, }; - 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,41 @@ 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(); }); From c7f6a2f1b62d1861fd3ede33855653baa01e1aff Mon Sep 17 00:00:00 2001 From: g0drlc Date: Thu, 7 Mar 2024 17:45:05 -0600 Subject: [PATCH 06/16] fix: check only the first blessed inscription in next block on gap detection (#325) --- src/pg/helpers.ts | 26 ++--- src/pg/pg-store.ts | 13 +-- tests/ordhook/server.test.ts | 215 ----------------------------------- 3 files changed, 18 insertions(+), 236 deletions(-) diff --git a/src/pg/helpers.ts b/src/pg/helpers.ts index 25d5d50..e992a69 100644 --- a/src/pg/helpers.ts +++ b/src/pg/helpers.ts @@ -16,26 +16,26 @@ import { import { OrdinalSatoshi } from '../api/util/ordinal-satoshi'; /** - * Check if writing a block would create an inscription number gap + * Check if writing the next block would create an inscription number gap * @param currentNumber - Current max blessed number - * @param newNumbers - New blessed numbers to be inserted + * @param writes - Incoming inscription event data + * @param currentBlockHeight - Current height + * @param nextBlockHeight - Height to be inserted */ export function assertNoBlockInscriptionGap(args: { currentNumber: number; - newNumbers: number[]; + writes: InscriptionEventData[]; currentBlockHeight: number; - newBlockHeight: number; + nextBlockHeight: 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}.` - ); - } + const nextReveal = args.writes.find(w => 'inscription' in w && w.inscription.number >= 0); + if (!nextReveal) return; + const next = (nextReveal as InscriptionRevealData).inscription.number; + if (next !== args.currentNumber + 1) + throw new BadPayloadRequestError( + `Block inscription gap detected: Attempting to insert #${next} (${args.nextBlockHeight}) but current max is #${args.currentNumber}. Chain tip is at ${args.currentBlockHeight}.` + ); } /** diff --git a/src/pg/pg-store.ts b/src/pg/pg-store.ts index 5f64329..20b33ab 100644 --- a/src/pg/pg-store.ts +++ b/src/pg/pg-store.ts @@ -109,7 +109,7 @@ 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 currentNumber = (await this.getMaxInscriptionNumber()) ?? -1; const currentBlockHeight = await this.getChainTipBlockHeight(); const event = applyEvent as BitcoinEvent; if ( @@ -125,14 +125,11 @@ 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, + currentNumber, + writes, + currentBlockHeight, + nextBlockHeight: event.block_identifier.index, }); for (const writeChunk of batchIterate(writes, INSERT_BATCH_SIZE)) await this.insertInscriptions(writeChunk, payload.chainhook.is_streaming_blocks); diff --git a/tests/ordhook/server.test.ts b/tests/ordhook/server.test.ts index 09c96a8..05c7a3c 100644 --- a/tests/ordhook/server.test.ts +++ b/tests/ordhook/server.test.ts @@ -521,221 +521,6 @@ describe('EventServer', () => { 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, - satpoint_post_inscription: - '9f4a9b73b0713c5da01c0a47f97c6c001af9028d6bdd9e264dfacbc4e6790201: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() - ); - 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, - }); - expect(response.statusCode).toBe(400); - }); - - test('server accepts payload with unordered unbound inscriptions', 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, - satpoint_post_inscription: - '9f4a9b73b0713c5da01c0a47f97c6c001af9028d6bdd9e264dfacbc4e6790201: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() - ); - 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 - ); - }); - test('server ignores past blocks', async () => { const payload = new TestChainhookPayloadBuilder() .apply() From ee0dded8cbd35324dfa66cf96520374ba71c5459 Mon Sep 17 00:00:00 2001 From: g0drlc Date: Thu, 7 Mar 2024 23:47:04 +0000 Subject: [PATCH 07/16] chore(release): 3.0.2-beta.3 [skip ci] ## [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)) --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index db38986..2a41730 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## [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) From 1856dcf23fb1db02e43f8b27aadb5977389fc751 Mon Sep 17 00:00:00 2001 From: g0drlc Date: Fri, 8 Mar 2024 09:58:48 -0600 Subject: [PATCH 08/16] fix: calculate transfer type correctly upon reveal (#326) --- src/pg/helpers.ts | 23 +++++++-- tests/brc-20/brc20.test.ts | 98 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 116 insertions(+), 5 deletions(-) diff --git a/src/pg/helpers.ts b/src/pg/helpers.ts index e992a69..f156a2a 100644 --- a/src/pg/helpers.ts +++ b/src/pg/helpers.ts @@ -1,4 +1,4 @@ -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, @@ -98,12 +98,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 +144,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/tests/brc-20/brc20.test.ts b/tests/brc-20/brc20.test.ts index efedf8c..ff1b42b 100644 --- a/tests/brc-20/brc20.test.ts +++ b/tests/brc-20/brc20.test.ts @@ -225,6 +225,104 @@ describe('BRC-20', () => { 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(); + }); + test('ignores incorrect p field', () => { const insert = testInsert({ p: 'brc20', // incorrect From 872793a9ca17021297cdc626149a83ae0ff599d7 Mon Sep 17 00:00:00 2001 From: g0drlc Date: Fri, 8 Mar 2024 16:00:45 +0000 Subject: [PATCH 09/16] chore(release): 3.0.2-beta.4 [skip ci] ## [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)) --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a41730..77083f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## [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) From 65b2c0cf271b9a841a83dd1edeb410f7cfbfdfe3 Mon Sep 17 00:00:00 2001 From: g0drlc Date: Fri, 8 Mar 2024 11:04:04 -0600 Subject: [PATCH 10/16] test: inscription reveals as fee or burnt (#327) --- tests/ordhook/server.test.ts | 108 +++++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) diff --git a/tests/ordhook/server.test.ts b/tests/ordhook/server.test.ts index 05c7a3c..b44c4e4 100644 --- a/tests/ordhook/server.test.ts +++ b/tests/ordhook/server.test.ts @@ -435,6 +435,114 @@ describe('EventServer', () => { }, ]); }); + + test('inscriptions revealed as fee', async () => { + await db.updateInscriptions( + new TestChainhookPayloadBuilder() + .apply() + .block({ + height: 832574, + hash: '000000000000000000020c8145de25b1e1e0a6312e377827a3015e15fdd574cd', + }) + .transaction({ + hash: '53957f47697096cef4ad24dae6357b3d7ffe1e3eb9216ce0bb01d6b6a2c8cf4a', + }) + .inscriptionRevealed({ + content_bytes: + '0x7b2270223a226272632d3230222c226f70223a226d696e74222c227469636b223a22656f7262222c22616d74223a223130227d', + content_length: 51, + content_type: 'text/plain', + curse_type: 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, + 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 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('spent_in_fees'); + }); + + test('inscriptions revealed as burnt', async () => { + await db.updateInscriptions( + new TestChainhookPayloadBuilder() + .apply() + .block({ + height: 832574, + hash: '000000000000000000020c8145de25b1e1e0a6312e377827a3015e15fdd574cd', + }) + .transaction({ + hash: '53957f47697096cef4ad24dae6357b3d7ffe1e3eb9216ce0bb01d6b6a2c8cf4a', + }) + .inscriptionRevealed({ + content_bytes: + '0x7b2270223a226272632d3230222c226f70223a226d696e74222c227469636b223a22656f7262222c22616d74223a223130227d', + content_length: 51, + content_type: 'text/plain', + curse_type: 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, + 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 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', () => { From f5b97eb11e7de5a5308153d1d4bbad4c23d91b12 Mon Sep 17 00:00:00 2001 From: g0drlc Date: Sat, 9 Mar 2024 00:24:01 -0600 Subject: [PATCH 11/16] fix: ignore spent as fee on gap check (#328) --- src/pg/helpers.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/pg/helpers.ts b/src/pg/helpers.ts index f156a2a..607ea80 100644 --- a/src/pg/helpers.ts +++ b/src/pg/helpers.ts @@ -29,7 +29,14 @@ export function assertNoBlockInscriptionGap(args: { nextBlockHeight: number; }) { if (!ENV.INSCRIPTION_GAP_DETECTION_ENABLED) return; - const nextReveal = args.writes.find(w => 'inscription' in w && w.inscription.number >= 0); + const nextReveal = args.writes.find( + w => + 'inscription' in w && + w.inscription.number >= 0 && + // Spent as fee come first in the block + w.location.address != null && + w.location.address != '' + ); if (!nextReveal) return; const next = (nextReveal as InscriptionRevealData).inscription.number; if (next !== args.currentNumber + 1) From 20dc833d06e2d5fbbcfe1c2d24fa3f80c62a414a Mon Sep 17 00:00:00 2001 From: g0drlc Date: Sat, 9 Mar 2024 06:25:56 +0000 Subject: [PATCH 12/16] chore(release): 3.0.2-beta.5 [skip ci] ## [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)) --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 77083f8..0e27668 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## [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) From 94352eaf7d084e74d7e85ec2f21a9de442d1b9d1 Mon Sep 17 00:00:00 2001 From: g0drlc Date: Sat, 9 Mar 2024 12:50:32 -0600 Subject: [PATCH 13/16] fix: remove gap detection (#330) --- src/env.ts | 2 - src/pg/helpers.ts | 32 -------------- src/pg/pg-store.ts | 11 +---- tests/ordhook/server.test.ts | 83 ------------------------------------ 4 files changed, 1 insertion(+), 127 deletions(-) diff --git a/src/env.ts b/src/env.ts index 21aeef6..956d42f 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/helpers.ts b/src/pg/helpers.ts index 607ea80..033a1ec 100644 --- a/src/pg/helpers.ts +++ b/src/pg/helpers.ts @@ -1,12 +1,10 @@ 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,36 +13,6 @@ import { } from './types'; import { OrdinalSatoshi } from '../api/util/ordinal-satoshi'; -/** - * Check if writing the next block would create an inscription number gap - * @param currentNumber - Current max blessed number - * @param writes - Incoming inscription event data - * @param currentBlockHeight - Current height - * @param nextBlockHeight - Height to be inserted - */ -export function assertNoBlockInscriptionGap(args: { - currentNumber: number; - writes: InscriptionEventData[]; - currentBlockHeight: number; - nextBlockHeight: number; -}) { - if (!ENV.INSCRIPTION_GAP_DETECTION_ENABLED) return; - const nextReveal = args.writes.find( - w => - 'inscription' in w && - w.inscription.number >= 0 && - // Spent as fee come first in the block - w.location.address != null && - w.location.address != '' - ); - if (!nextReveal) return; - const next = (nextReveal as InscriptionRevealData).inscription.number; - if (next !== args.currentNumber + 1) - throw new BadPayloadRequestError( - `Block inscription gap detected: Attempting to insert #${next} (${args.nextBlockHeight}) but current max is #${args.currentNumber}. Chain tip is at ${args.currentBlockHeight}.` - ); -} - /** * Returns a list of referenced inscription ids from inscription content. * @param content - Inscription content diff --git a/src/pg/pg-store.ts b/src/pg/pg-store.ts index 20b33ab..f08df0c 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 currentNumber = (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,12 +122,6 @@ export class PgStore extends BasePgStore { logger.info(`PgStore ingesting block ${event.block_identifier.index}`); const time = stopwatch(); const writes = revealInsertsFromOrdhookEvent(event); - assertNoBlockInscriptionGap({ - currentNumber, - writes, - currentBlockHeight, - nextBlockHeight: 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/ordhook/server.test.ts b/tests/ordhook/server.test.ts index b44c4e4..e803585 100644 --- a/tests/ordhook/server.test.ts +++ b/tests/ordhook/server.test.ts @@ -546,89 +546,6 @@ describe('EventServer', () => { }); describe('gap detection', () => { - test('server rejects payload with first 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, - satpoint_post_inscription: - '9f4a9b73b0713c5da01c0a47f97c6c001af9028d6bdd9e264dfacbc4e6790201: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() - ); - 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 ignores past blocks', async () => { const payload = new TestChainhookPayloadBuilder() .apply() From fc737b52cbb0f3aa49daf4e2d1973d36c698847b Mon Sep 17 00:00:00 2001 From: g0drlc Date: Sat, 9 Mar 2024 18:52:48 +0000 Subject: [PATCH 14/16] chore(release): 3.0.2-beta.6 [skip ci] ## [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)) --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e27668..c372d6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## [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) From f11ab8ba5d88bd099ce0ef06edc3ca85481225f7 Mon Sep 17 00:00:00 2001 From: g0drlc Date: Wed, 27 Mar 2024 10:56:46 -0600 Subject: [PATCH 15/16] feat: support self minted 5-byte wide BRC-20 tokens (#338) * feat: first features * test: invalid parent * feat: unlimited supply self mint * test: transfer for self mint token --- .../1711465842961_brc20-deploy-self-mint.ts | 19 + src/api/schemas.ts | 1 + src/api/util/helpers.ts | 1 + src/pg/brc20/brc20-pg-store.ts | 22 +- src/pg/brc20/helpers.ts | 23 +- src/pg/brc20/types.ts | 3 + tests/brc-20/brc20.test.ts | 768 ++++++++++++++++-- tests/helpers.ts | 3 +- 8 files changed, 757 insertions(+), 83 deletions(-) create mode 100644 migrations/1711465842961_brc20-deploy-self-mint.ts diff --git a/migrations/1711465842961_brc20-deploy-self-mint.ts b/migrations/1711465842961_brc20-deploy-self-mint.ts new file mode 100644 index 0000000..8cacf69 --- /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 973ba9f..053f38d 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 173e767..9b7815e 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/pg/brc20/brc20-pg-store.ts b/src/pg/brc20/brc20-pg-store.ts index fa3d6da..8641a62 100644 --- a/src/pg/brc20/brc20-pg-store.ts +++ b/src/pg/brc20/brc20-pg-store.ts @@ -8,6 +8,7 @@ import { DbPaginatedResult, InscriptionEventData, LocationData, + InscriptionRevealData, } from '../types'; import { BRC20_DEPLOYS_COLUMNS, @@ -25,7 +26,7 @@ import { DbBrc20TokenWithSupply, DbBrc20TransferEvent, } from './types'; -import { Brc20Deploy, Brc20Mint, Brc20Transfer, brc20FromInscription } from './helpers'; +import { Brc20Deploy, Brc20Mint, Brc20Transfer, UINT64_MAX, brc20FromInscription } from './helpers'; import { Brc20TokenOrderBy } from '../../api/schemas'; import { objRemoveUndefinedValues } from '../helpers'; @@ -198,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; @@ -208,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 ( @@ -248,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 @@ -269,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) ( diff --git a/src/pg/brc20/helpers.ts b/src/pg/brc20/helpers.ts index 854609d..6aec169 100644 --- a/src/pg/brc20/helpers.ts +++ b/src/pg/brc20/helpers.ts @@ -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,10 +47,16 @@ 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); +/** + * 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 brc20FromInscription(reveal: InscriptionRevealData): Brc20 | undefined { if ( reveal.inscription.classic_number < 0 || @@ -62,10 +69,20 @@ export function brc20FromInscription(reveal: InscriptionRevealData): Brc20 | und 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 269397d..5b28258 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/tests/brc-20/brc20.test.ts b/tests/brc-20/brc20.test.ts index ff1b42b..8f2132f 100644 --- a/tests/brc-20/brc20.test.ts +++ b/tests/brc-20/brc20.test.ts @@ -1,7 +1,7 @@ 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 { DbLocationTransferType, InscriptionRevealData } from '../../src/pg/types'; import { @@ -86,7 +86,7 @@ describe('BRC-20', () => { }); describe('token standard validation', () => { - const testInsert = (json: any): InscriptionRevealData => { + const testInsert = (json: any, block_height: number = 830000): InscriptionRevealData => { const content = Buffer.from(JSON.stringify(json), 'utf-8'); return { inscription: { @@ -109,7 +109,7 @@ describe('BRC-20', () => { recursive_refs: [], location: { genesis_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', - block_height: 830000, + block_height, block_hash: '00000000000000000002c5c0aba96f981642a6dca109e6b3564925c21a98aa3e', tx_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', tx_index: 0, @@ -343,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', @@ -374,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', @@ -501,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', @@ -683,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() @@ -761,6 +899,7 @@ describe('BRC-20', () => { deploy_timestamp: 1677803510000, minted_supply: '0.000000000000000000', tx_count: 1, + self_mint: false, }, ]); }); @@ -839,6 +978,7 @@ describe('BRC-20', () => { deploy_timestamp: 1677803510000, minted_supply: '0.000000000000000000', tx_count: 1, + self_mint: false, }, ]); const response2 = await fastify.inject({ @@ -862,6 +1002,7 @@ describe('BRC-20', () => { deploy_timestamp: 1677803510000, minted_supply: '0.000000000000000000', tx_count: 1, + self_mint: false, }, ]); }); @@ -1035,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({ @@ -1052,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, @@ -1067,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({ @@ -1078,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() @@ -1123,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({ @@ -1150,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, @@ -1166,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({ @@ -1177,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({ @@ -1216,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, @@ -1232,58 +1420,70 @@ 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', }) ) + .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: '7e09bda2cba34bca648cca6d79a074940d39b6137150d3a3edcf80c0e01419a5', + hash: '7a1adbc3e93ddf8d7c4e0ba75aa11c98c431521dd850be8b955feedb716d8bec', }) .inscriptionRevealed( brc20Reveal({ json: { p: 'brc-20', op: 'mint', - tick: 'PEPE', - amt: '1000', + tick: '$pepe', + amt: '100000', }, number: 2, ordinal_number: 2, - tx_id: '7e09bda2cba34bca648cca6d79a074940d39b6137150d3a3edcf80c0e01419a5', - address: address, - }) - ) - .transaction({ - hash: '8aec77f855549d98cb9fb5f35e02a03f9a2354fd05a5f89fc610b32c3b01f99f', - }) - .inscriptionRevealed( - brc20Reveal({ - json: { - p: 'brc-20', - op: 'mint', - tick: 'PEPE', - amt: '5000000000', // Exceeds supply - }, - number: 3, - ordinal_number: 3, - tx_id: '8aec77f855549d98cb9fb5f35e02a03f9a2354fd05a5f89fc610b32c3b01f99f', - address: address, + tx_id: '7a1adbc3e93ddf8d7c4e0ba75aa11c98c431521dd850be8b955feedb716d8bec', + address: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td', + parent: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', }) ) .build() @@ -1291,17 +1491,301 @@ describe('BRC-20', () => { const response2 = await fastify.inject({ method: 'GET', - url: `/ordinals/brc-20/balances/${address}?ticker=PEPE`, + url: `/ordinals/brc-20/balances/${address}`, }); expect(response2.statusCode).toBe(200); const responseJson2 = response2.json(); expect(responseJson2.total).toBe(1); expect(responseJson2.results).toStrictEqual([ { - available_balance: '2500.0', // Max capacity - overall_balance: '2500.0', - ticker: 'PEPE', - transferrable_balance: '0.0', + 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', + amt: '1000', + }, + number: 2, + ordinal_number: 2, + tx_id: '7e09bda2cba34bca648cca6d79a074940d39b6137150d3a3edcf80c0e01419a5', + address: address, + }) + ) + .transaction({ + hash: '8aec77f855549d98cb9fb5f35e02a03f9a2354fd05a5f89fc610b32c3b01f99f', + }) + .inscriptionRevealed( + brc20Reveal({ + json: { + p: 'brc-20', + op: 'mint', + tick: 'PEPE', + amt: '5000000000', // Exceeds supply + }, + number: 3, + ordinal_number: 3, + tx_id: '8aec77f855549d98cb9fb5f35e02a03f9a2354fd05a5f89fc610b32c3b01f99f', + address: address, + }) + ) + .build() + ); + + const response2 = await fastify.inject({ + method: 'GET', + url: `/ordinals/brc-20/balances/${address}?ticker=PEPE`, + }); + expect(response2.statusCode).toBe(200); + const responseJson2 = response2.json(); + expect(responseJson2.total).toBe(1); + expect(responseJson2.results).toStrictEqual([ + { + available_balance: '2500.0', // Max capacity + overall_balance: '2500.0', + ticker: 'PEPE', + transferrable_balance: '0.0', }, ]); @@ -1763,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); @@ -2196,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 f26f86b..f2c7067 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; } From e0ecce7d0c2f7e7e31ef987c10a732cb292654b3 Mon Sep 17 00:00:00 2001 From: g0drlc Date: Wed, 27 Mar 2024 16:58:49 +0000 Subject: [PATCH 16/16] chore(release): 3.1.0-beta.1 [skip ci] ## [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)) --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c372d6f..cd90ab7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## [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)