From 56a88518b1ffe549524941e4d94d6347d11c98f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20C=C3=A1rdenas?= Date: Tue, 23 Apr 2024 10:02:06 -0600 Subject: [PATCH] feat!: ingest BRC20 data from ordhook (#347) * chore: progress * feat: new apply * fix: minted supply * fix: track tx counts * feat: rollbacks * fix: transfer rollbacks * fix: activity addresses * fix: multiple transfer test * fix: holders * fix: operation indexes * fix: style * chore: upgrade chainhook client * fix: add brc20 to default predicate --- migrations/1684175792528_brc20-mints.ts | 51 - migrations/1684175795592_brc20-transfers.ts | 55 - migrations/1684175810998_brc20-balances.ts | 63 - migrations/1684344022290_brc20-events.ts | 60 - migrations/1692132685000_brc20-supply-view.ts | 17 - ...692188000000_brc20-deploys-ticker-index.ts | 15 - ...692853050488_brc20-mint-transfer-unique.ts | 18 - .../1692891772000_brc20-events-types.ts | 38 - ...693428793416_brc20-minted-supply-column.ts | 36 - .../1694081119000_brc20-counts-by-tx-count.ts | 13 - ...797181616_brc20-counts-by-address-event.ts | 69 - .../1695243716885_brc20-events-addresses.ts | 35 - ...c20-total-balances-address-deploy-index.ts | 14 - .../1708471015438_remove-unused-indexes.ts | 18 - .../1711465842961_brc20-deploy-self-mint.ts | 19 - ...ploys.ts => 1711575178681_brc20-tokens.ts} | 42 +- migrations/1711575178682_brc20-operations.ts | 68 + ... => 1711575178683_brc20-total-balances.ts} | 25 +- ...711575178684_brc20-counts-by-operation.ts} | 4 +- ...8686_brc20-counts-by-address-operation.ts} | 14 +- package-lock.json | 43 +- package.json | 2 +- src/api/util/helpers.ts | 17 +- src/env.ts | 3 - src/ordhook/server.ts | 1 + src/pg/brc20/brc20-pg-store.ts | 979 ++-- src/pg/brc20/helpers.ts | 166 +- src/pg/brc20/types.ts | 112 +- src/pg/pg-store.ts | 10 +- tests/brc-20/api.test.ts | 1394 ++++++ tests/brc-20/brc20.test.ts | 4073 ++--------------- tests/helpers.ts | 172 +- 32 files changed, 2612 insertions(+), 5034 deletions(-) delete mode 100644 migrations/1684175792528_brc20-mints.ts delete mode 100644 migrations/1684175795592_brc20-transfers.ts delete mode 100644 migrations/1684175810998_brc20-balances.ts delete mode 100644 migrations/1684344022290_brc20-events.ts delete mode 100644 migrations/1692132685000_brc20-supply-view.ts delete mode 100644 migrations/1692188000000_brc20-deploys-ticker-index.ts delete mode 100644 migrations/1692853050488_brc20-mint-transfer-unique.ts delete mode 100644 migrations/1692891772000_brc20-events-types.ts delete mode 100644 migrations/1693428793416_brc20-minted-supply-column.ts delete mode 100644 migrations/1694081119000_brc20-counts-by-tx-count.ts delete mode 100644 migrations/1694797181616_brc20-counts-by-address-event.ts delete mode 100644 migrations/1695243716885_brc20-events-addresses.ts delete mode 100644 migrations/1706894983174_brc20-total-balances-address-deploy-index.ts delete mode 100644 migrations/1711465842961_brc20-deploy-self-mint.ts rename migrations/{1684174644336_brc20-deploys.ts => 1711575178681_brc20-tokens.ts} (60%) create mode 100644 migrations/1711575178682_brc20-operations.ts rename migrations/{1694021174916_brc20-total-balances.ts => 1711575178683_brc20-total-balances.ts} (59%) rename migrations/{1694295793981_brc20-event-counts.ts => 1711575178684_brc20-counts-by-operation.ts} (86%) rename migrations/{1694299763914_brc20-token-count.ts => 1711575178686_brc20-counts-by-address-operation.ts} (55%) create mode 100644 tests/brc-20/api.test.ts diff --git a/migrations/1684175792528_brc20-mints.ts b/migrations/1684175792528_brc20-mints.ts deleted file mode 100644 index 9435bf73..00000000 --- a/migrations/1684175792528_brc20-mints.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* eslint-disable @typescript-eslint/naming-convention */ -import { MigrationBuilder, ColumnDefinitions } from 'node-pg-migrate'; - -export const shorthands: ColumnDefinitions | undefined = undefined; - -export function up(pgm: MigrationBuilder): void { - pgm.createTable('brc20_mints', { - id: { - type: 'bigserial', - primaryKey: true, - }, - inscription_id: { - type: 'bigint', - notNull: true, - }, - brc20_deploy_id: { - type: 'bigint', - notNull: true, - }, - block_height: { - type: 'bigint', - notNull: true, - }, - tx_id: { - type: 'text', - notNull: true, - }, - address: { - type: 'text', - notNull: true, - }, - amount: { - type: 'numeric', - notNull: true, - }, - }); - pgm.createConstraint( - 'brc20_mints', - 'brc20_mints_inscription_id_fk', - 'FOREIGN KEY(inscription_id) REFERENCES inscriptions(id) ON DELETE CASCADE' - ); - pgm.createConstraint( - 'brc20_mints', - 'brc20_mints_brc20_deploy_id_fk', - 'FOREIGN KEY(brc20_deploy_id) REFERENCES brc20_deploys(id) ON DELETE CASCADE' - ); - pgm.createIndex('brc20_mints', ['inscription_id']); - pgm.createIndex('brc20_mints', ['brc20_deploy_id']); - pgm.createIndex('brc20_mints', ['block_height']); - pgm.createIndex('brc20_mints', ['address']); -} diff --git a/migrations/1684175795592_brc20-transfers.ts b/migrations/1684175795592_brc20-transfers.ts deleted file mode 100644 index 30f08071..00000000 --- a/migrations/1684175795592_brc20-transfers.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* eslint-disable @typescript-eslint/naming-convention */ -import { MigrationBuilder, ColumnDefinitions } from 'node-pg-migrate'; - -export const shorthands: ColumnDefinitions | undefined = undefined; - -export function up(pgm: MigrationBuilder): void { - pgm.createTable('brc20_transfers', { - id: { - type: 'bigserial', - primaryKey: true, - }, - inscription_id: { - type: 'bigint', - notNull: true, - }, - brc20_deploy_id: { - type: 'bigint', - notNull: true, - }, - block_height: { - type: 'bigint', - notNull: true, - }, - tx_id: { - type: 'text', - notNull: true, - }, - from_address: { - type: 'text', - notNull: true, - }, - to_address: { - type: 'text', - }, - amount: { - type: 'numeric', - notNull: true, - }, - }); - pgm.createConstraint( - 'brc20_transfers', - 'brc20_transfers_inscription_id_fk', - 'FOREIGN KEY(inscription_id) REFERENCES inscriptions(id) ON DELETE CASCADE' - ); - pgm.createConstraint( - 'brc20_transfers', - 'brc20_transfers_brc20_deploy_id_fk', - 'FOREIGN KEY(brc20_deploy_id) REFERENCES brc20_deploys(id) ON DELETE CASCADE' - ); - pgm.createIndex('brc20_transfers', ['inscription_id']); - pgm.createIndex('brc20_transfers', ['brc20_deploy_id']); - pgm.createIndex('brc20_transfers', ['block_height']); - pgm.createIndex('brc20_transfers', ['from_address']); - pgm.createIndex('brc20_transfers', ['to_address']); -} diff --git a/migrations/1684175810998_brc20-balances.ts b/migrations/1684175810998_brc20-balances.ts deleted file mode 100644 index 4f918dff..00000000 --- a/migrations/1684175810998_brc20-balances.ts +++ /dev/null @@ -1,63 +0,0 @@ -/* eslint-disable @typescript-eslint/naming-convention */ -import { MigrationBuilder, ColumnDefinitions } from 'node-pg-migrate'; - -export const shorthands: ColumnDefinitions | undefined = undefined; - -export function up(pgm: MigrationBuilder): void { - pgm.createTable('brc20_balances', { - id: { - type: 'bigserial', - primaryKey: true, - }, - inscription_id: { - type: 'bigint', - notNull: true, - }, - location_id: { - type: 'bigint', - notNull: true, - }, - brc20_deploy_id: { - type: 'bigint', - notNull: true, - }, - address: { - type: 'text', - }, - avail_balance: { - type: 'numeric', - notNull: true, - }, - trans_balance: { - type: 'numeric', - notNull: true, - }, - type: { - type: 'smallint', - notNull: true, - }, - }); - pgm.createConstraint( - 'brc20_balances', - 'brc20_balances_inscription_id_fk', - 'FOREIGN KEY(inscription_id) REFERENCES inscriptions(id) ON DELETE CASCADE' - ); - pgm.createConstraint( - 'brc20_balances', - 'brc20_balances_location_id_fk', - 'FOREIGN KEY(location_id) REFERENCES locations(id) ON DELETE CASCADE' - ); - pgm.createConstraint( - 'brc20_balances', - 'brc20_balances_brc20_deploy_id_fk', - 'FOREIGN KEY(brc20_deploy_id) REFERENCES brc20_deploys(id) ON DELETE CASCADE' - ); - pgm.createConstraint( - 'brc20_balances', - 'brc20_balances_inscription_id_type_unique', - 'UNIQUE(inscription_id, type)' - ); - pgm.createIndex('brc20_balances', ['location_id']); - pgm.createIndex('brc20_balances', ['brc20_deploy_id']); - pgm.createIndex('brc20_balances', ['address']); -} diff --git a/migrations/1684344022290_brc20-events.ts b/migrations/1684344022290_brc20-events.ts deleted file mode 100644 index dfc0befc..00000000 --- a/migrations/1684344022290_brc20-events.ts +++ /dev/null @@ -1,60 +0,0 @@ -/* eslint-disable @typescript-eslint/naming-convention */ -import { MigrationBuilder, ColumnDefinitions } from 'node-pg-migrate'; - -export const shorthands: ColumnDefinitions | undefined = undefined; - -export function up(pgm: MigrationBuilder): void { - pgm.createTable('brc20_events', { - id: { - type: 'bigserial', - primaryKey: true, - }, - inscription_id: { - type: 'bigint', - notNull: true, - }, - brc20_deploy_id: { - type: 'bigint', - notNull: true, - }, - deploy_id: { - type: 'bigint', - }, - mint_id: { - type: 'bigint', - }, - transfer_id: { - type: 'bigint', - }, - }); - pgm.createConstraint( - 'brc20_events', - 'brc20_events_inscription_id_fk', - 'FOREIGN KEY(inscription_id) REFERENCES inscriptions(id) ON DELETE CASCADE' - ); - pgm.createConstraint( - 'brc20_events', - 'brc20_events_brc20_deploy_id_fk', - 'FOREIGN KEY(brc20_deploy_id) REFERENCES brc20_deploys(id) ON DELETE CASCADE' - ); - pgm.createConstraint( - 'brc20_events', - 'brc20_events_deploy_id_fk', - 'FOREIGN KEY(deploy_id) REFERENCES brc20_deploys(id) ON DELETE CASCADE' - ); - pgm.createConstraint( - 'brc20_events', - 'brc20_events_mint_id_fk', - 'FOREIGN KEY(mint_id) REFERENCES brc20_mints(id) ON DELETE CASCADE' - ); - pgm.createConstraint( - 'brc20_events', - 'brc20_events_transfer_id_fk', - 'FOREIGN KEY(transfer_id) REFERENCES brc20_transfers(id) ON DELETE CASCADE' - ); - pgm.createConstraint( - 'brc20_events', - 'brc20_valid_event', - 'CHECK(NUM_NONNULLS(deploy_id, mint_id, transfer_id) = 1)' - ); -} diff --git a/migrations/1692132685000_brc20-supply-view.ts b/migrations/1692132685000_brc20-supply-view.ts deleted file mode 100644 index 8ae5cb2f..00000000 --- a/migrations/1692132685000_brc20-supply-view.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* 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.createMaterializedView( - 'brc20_supplies', - { data: true }, - ` - SELECT brc20_deploy_id, SUM(amount) as minted_supply, MAX(block_height) as block_height - FROM brc20_mints - GROUP BY brc20_deploy_id - ` - ); - pgm.createIndex('brc20_supplies', ['brc20_deploy_id'], { unique: true }); -} diff --git a/migrations/1692188000000_brc20-deploys-ticker-index.ts b/migrations/1692188000000_brc20-deploys-ticker-index.ts deleted file mode 100644 index 4fd40203..00000000 --- a/migrations/1692188000000_brc20-deploys-ticker-index.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* eslint-disable @typescript-eslint/naming-convention */ -import { MigrationBuilder, ColumnDefinitions } from 'node-pg-migrate'; - -export const shorthands: ColumnDefinitions | undefined = undefined; - -export function up(pgm: MigrationBuilder): void { - pgm.addColumns('brc20_deploys', { - ticker_lower: { - type: 'text', - notNull: true, - expressionGenerated: '(LOWER(ticker))', - }, - }); - pgm.createIndex('brc20_deploys', ['ticker_lower']); -} diff --git a/migrations/1692853050488_brc20-mint-transfer-unique.ts b/migrations/1692853050488_brc20-mint-transfer-unique.ts deleted file mode 100644 index 2ad987e1..00000000 --- a/migrations/1692853050488_brc20-mint-transfer-unique.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* 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.dropIndex('brc20_transfers', ['inscription_id']); - pgm.createIndex('brc20_transfers', ['inscription_id'], { unique: true }); - pgm.dropIndex('brc20_mints', ['inscription_id']); - pgm.createIndex('brc20_mints', ['inscription_id'], { unique: true }); -} - -export function down(pgm: MigrationBuilder): void { - pgm.dropIndex('brc20_transfers', ['inscription_id'], { unique: true }); - pgm.createIndex('brc20_transfers', ['inscription_id']); - pgm.dropIndex('brc20_mints', ['inscription_id'], { unique: true }); - pgm.createIndex('brc20_mints', ['inscription_id']); -} diff --git a/migrations/1692891772000_brc20-events-types.ts b/migrations/1692891772000_brc20-events-types.ts deleted file mode 100644 index 4c575b94..00000000 --- a/migrations/1692891772000_brc20-events-types.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* 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.createType('brc20_operation', ['deploy', 'mint', 'transfer', 'transfer_send']); - pgm.addColumns('brc20_events', { - genesis_location_id: { - type: 'bigint', - references: '"locations"', - onDelete: 'CASCADE', - notNull: true, - unique: true, // only one event exists per location - }, - operation: { - type: 'brc20_operation', - notNull: true, - }, - }); - - pgm.createIndex('brc20_events', ['genesis_location_id']); - pgm.createIndex('brc20_events', ['operation']); - - pgm.createIndex('brc20_events', ['brc20_deploy_id']); - pgm.createIndex('brc20_events', ['transfer_id']); - pgm.createIndex('brc20_events', ['mint_id']); -} - -export function down(pgm: MigrationBuilder): void { - pgm.dropIndex('brc20_events', ['genesis_location_id']); - pgm.dropIndex('brc20_events', ['operation']); - pgm.dropColumns('brc20_events', ['genesis_location_id', 'operation']); - pgm.dropIndex('brc20_events', ['brc20_deploy_id']); - pgm.dropIndex('brc20_events', ['transfer_id']); - pgm.dropIndex('brc20_events', ['mint_id']); - pgm.dropType('brc20_operation'); -} diff --git a/migrations/1693428793416_brc20-minted-supply-column.ts b/migrations/1693428793416_brc20-minted-supply-column.ts deleted file mode 100644 index 55513825..00000000 --- a/migrations/1693428793416_brc20-minted-supply-column.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* 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', { - minted_supply: { - type: 'numeric', - default: 0, - }, - }); - pgm.sql(` - UPDATE brc20_deploys AS d - SET minted_supply = ( - SELECT COALESCE(SUM(amount), 0) AS minted_supply - FROM brc20_mints - WHERE brc20_deploy_id = d.id - ) - `); - pgm.dropMaterializedView('brc20_supplies'); -} - -export function down(pgm: MigrationBuilder): void { - pgm.dropColumn('brc20_deploys', ['minted_supply']); - pgm.createMaterializedView( - 'brc20_supplies', - { data: true }, - ` - SELECT brc20_deploy_id, SUM(amount) as minted_supply, MAX(block_height) as block_height - FROM brc20_mints - GROUP BY brc20_deploy_id - ` - ); - pgm.createIndex('brc20_supplies', ['brc20_deploy_id'], { unique: true }); -} diff --git a/migrations/1694081119000_brc20-counts-by-tx-count.ts b/migrations/1694081119000_brc20-counts-by-tx-count.ts deleted file mode 100644 index 12bb89d8..00000000 --- a/migrations/1694081119000_brc20-counts-by-tx-count.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* 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', { - tx_count: { - type: 'bigint', - default: 1, - }, - }); -} diff --git a/migrations/1694797181616_brc20-counts-by-address-event.ts b/migrations/1694797181616_brc20-counts-by-address-event.ts deleted file mode 100644 index b77c2895..00000000 --- a/migrations/1694797181616_brc20-counts-by-address-event.ts +++ /dev/null @@ -1,69 +0,0 @@ -/* eslint-disable @typescript-eslint/naming-convention */ -import { MigrationBuilder, ColumnDefinitions } from 'node-pg-migrate'; - -export const shorthands: ColumnDefinitions | undefined = undefined; - -export function up(pgm: MigrationBuilder): void { - pgm.createTable('brc20_counts_by_address_event_type', { - address: { - type: 'text', - notNull: true, - primaryKey: true, - }, - deploy: { - type: 'bigint', - notNull: true, - default: 0, - }, - mint: { - type: 'bigint', - notNull: true, - default: 0, - }, - transfer: { - type: 'bigint', - notNull: true, - default: 0, - }, - transfer_send: { - type: 'bigint', - notNull: true, - default: 0, - }, - }); - pgm.sql(` - INSERT INTO brc20_counts_by_address_event_type (address, deploy) ( - SELECT address, COUNT(*) AS deploy FROM brc20_deploys GROUP BY address - ) ON CONFLICT (address) DO UPDATE SET deploy = EXCLUDED.deploy - `); - pgm.sql(` - INSERT INTO brc20_counts_by_address_event_type (address, mint) ( - SELECT address, COUNT(*) AS mint FROM brc20_mints GROUP BY address - ) ON CONFLICT (address) DO UPDATE SET mint = EXCLUDED.mint - `); - pgm.sql(` - INSERT INTO brc20_counts_by_address_event_type (address, transfer) ( - SELECT from_address AS address, COUNT(*) AS transfer FROM brc20_transfers GROUP BY from_address - ) ON CONFLICT (address) DO UPDATE SET transfer = EXCLUDED.transfer - `); - pgm.sql(` - INSERT INTO brc20_counts_by_address_event_type (address, transfer_send) ( - SELECT from_address AS address, COUNT(*) AS transfer_send - FROM brc20_transfers - WHERE to_address IS NOT NULL - GROUP BY from_address - ) ON CONFLICT (address) DO UPDATE SET transfer_send = EXCLUDED.transfer_send - `); - pgm.sql(` - INSERT INTO brc20_counts_by_address_event_type (address, transfer_send) ( - SELECT to_address AS address, COUNT(*) AS transfer_send - FROM brc20_transfers - WHERE to_address <> from_address - GROUP BY to_address - ) ON CONFLICT (address) DO UPDATE SET transfer_send = brc20_counts_by_address_event_type.transfer_send + EXCLUDED.transfer_send - `); -} - -export function down(pgm: MigrationBuilder): void { - pgm.dropTable('brc20_counts_by_address_event_type'); -} diff --git a/migrations/1695243716885_brc20-events-addresses.ts b/migrations/1695243716885_brc20-events-addresses.ts deleted file mode 100644 index a91732e1..00000000 --- a/migrations/1695243716885_brc20-events-addresses.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* eslint-disable @typescript-eslint/naming-convention */ -import { MigrationBuilder, ColumnDefinitions } from 'node-pg-migrate'; - -export const shorthands: ColumnDefinitions | undefined = undefined; - -export function up(pgm: MigrationBuilder): void { - pgm.addColumns('brc20_events', { - address: { - type: 'text', - }, - from_address: { - type: 'text', - }, - }); - pgm.createIndex('brc20_events', ['address']); - pgm.createIndex('brc20_events', ['from_address']); - pgm.sql(` - UPDATE brc20_events - SET address = (SELECT address FROM locations WHERE id = brc20_events.genesis_location_id) - `); - pgm.sql(` - UPDATE brc20_events - SET from_address = (SELECT from_address FROM brc20_transfers WHERE id = brc20_events.transfer_id) - WHERE operation = 'transfer_send' - `); - pgm.alterColumn('brc20_events', 'address', { notNull: true }); - pgm.dropIndex('brc20_events', ['genesis_location_id']); // Covered by the unique index. -} - -export function down(pgm: MigrationBuilder): void { - pgm.dropIndex('brc20_events', ['address']); - pgm.dropIndex('brc20_events', ['from_address']); - pgm.dropColumns('brc20_events', ['address', 'from_address']); - pgm.createIndex('brc20_events', ['genesis_location_id']); -} diff --git a/migrations/1706894983174_brc20-total-balances-address-deploy-index.ts b/migrations/1706894983174_brc20-total-balances-address-deploy-index.ts deleted file mode 100644 index 25e79706..00000000 --- a/migrations/1706894983174_brc20-total-balances-address-deploy-index.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* 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.dropIndex('brc20_total_balances', ['address']); - pgm.createIndex('brc20_total_balances', ['address', 'brc20_deploy_id']); -} - -export function down(pgm: MigrationBuilder): void { - pgm.dropIndex('brc20_total_balances', ['address', 'brc20_deploy_id']); - pgm.createIndex('brc20_total_balances', ['address']); -} diff --git a/migrations/1708471015438_remove-unused-indexes.ts b/migrations/1708471015438_remove-unused-indexes.ts index 2ba978b7..1d94c6f7 100644 --- a/migrations/1708471015438_remove-unused-indexes.ts +++ b/migrations/1708471015438_remove-unused-indexes.ts @@ -7,15 +7,6 @@ export function up(pgm: MigrationBuilder): void { pgm.dropIndex('locations', ['prev_output']); pgm.dropIndex('locations', ['address']); pgm.dropIndex('current_locations', ['block_height']); - pgm.dropIndex('brc20_mints', ['address']); - pgm.dropIndex('brc20_mints', ['block_height']); - pgm.dropIndex('brc20_mints', ['brc20_deploy_id']); - pgm.dropIndex('brc20_transfers', ['to_address']); - pgm.dropIndex('brc20_transfers', ['from_address']); - pgm.dropIndex('brc20_transfers', ['brc20_deploy_id']); - pgm.dropIndex('brc20_transfers', ['block_height']); - pgm.dropIndex('brc20_deploys', ['address']); - pgm.dropIndex('brc20_deploys', ['block_height']); pgm.dropIndex('inscription_recursions', ['ref_inscription_genesis_id']); } @@ -23,14 +14,5 @@ export function down(pgm: MigrationBuilder): void { pgm.createIndex('locations', ['prev_output']); pgm.createIndex('locations', ['address']); pgm.createIndex('current_locations', ['block_height']); - pgm.createIndex('brc20_mints', ['address']); - pgm.createIndex('brc20_mints', ['block_height']); - pgm.createIndex('brc20_mints', ['brc20_deploy_id']); - pgm.createIndex('brc20_transfers', ['to_address']); - pgm.createIndex('brc20_transfers', ['from_address']); - pgm.createIndex('brc20_transfers', ['brc20_deploy_id']); - pgm.createIndex('brc20_transfers', ['block_height']); - pgm.createIndex('brc20_deploys', ['address']); - pgm.createIndex('brc20_deploys', ['block_height']); pgm.createIndex('inscription_recursions', ['ref_inscription_genesis_id']); } diff --git a/migrations/1711465842961_brc20-deploy-self-mint.ts b/migrations/1711465842961_brc20-deploy-self-mint.ts deleted file mode 100644 index 8cacf691..00000000 --- a/migrations/1711465842961_brc20-deploy-self-mint.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* 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/migrations/1684174644336_brc20-deploys.ts b/migrations/1711575178681_brc20-tokens.ts similarity index 60% rename from migrations/1684174644336_brc20-deploys.ts rename to migrations/1711575178681_brc20-tokens.ts index 3604fa03..48cd3670 100644 --- a/migrations/1684174644336_brc20-deploys.ts +++ b/migrations/1711575178681_brc20-tokens.ts @@ -4,13 +4,13 @@ import { MigrationBuilder, ColumnDefinitions } from 'node-pg-migrate'; export const shorthands: ColumnDefinitions | undefined = undefined; export function up(pgm: MigrationBuilder): void { - pgm.createTable('brc20_deploys', { - id: { - type: 'bigserial', + pgm.createTable('brc20_tokens', { + ticker: { + type: 'text', primaryKey: true, }, - inscription_id: { - type: 'bigint', + genesis_id: { + type: 'string', notNull: true, }, block_height: { @@ -25,10 +25,6 @@ export function up(pgm: MigrationBuilder): void { type: 'text', notNull: true, }, - ticker: { - type: 'text', - notNull: true, - }, max: { type: 'numeric', notNull: true, @@ -40,14 +36,24 @@ export function up(pgm: MigrationBuilder): void { type: 'int', notNull: true, }, + self_mint: { + type: 'boolean', + default: 'false', + notNull: true, + }, + minted_supply: { + type: 'numeric', + default: 0, + }, + burned_supply: { + type: 'numeric', + default: 0, + }, + tx_count: { + type: 'bigint', + default: 0, + }, }); - pgm.createConstraint( - 'brc20_deploys', - 'brc20_deploys_inscription_id_fk', - 'FOREIGN KEY(inscription_id) REFERENCES inscriptions(id) ON DELETE CASCADE' - ); - pgm.createIndex('brc20_deploys', ['inscription_id']); - pgm.createIndex('brc20_deploys', 'LOWER(ticker)', { unique: true }); - pgm.createIndex('brc20_deploys', ['block_height']); - pgm.createIndex('brc20_deploys', ['address']); + pgm.createIndex('brc20_tokens', ['genesis_id']); + pgm.createIndex('brc20_tokens', ['block_height']); } diff --git a/migrations/1711575178682_brc20-operations.ts b/migrations/1711575178682_brc20-operations.ts new file mode 100644 index 00000000..42582577 --- /dev/null +++ b/migrations/1711575178682_brc20-operations.ts @@ -0,0 +1,68 @@ +/* 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.createType('brc20_operation', [ + 'deploy', + 'mint', + 'transfer', + 'transfer_send', + 'transfer_receive', + ]); + pgm.createTable('brc20_operations', { + genesis_id: { + type: 'string', + notNull: true, + }, + ticker: { + type: 'string', + notNull: true, + }, + operation: { + type: 'brc20_operation', + notNull: true, + }, + block_height: { + type: 'bigint', + notNull: true, + }, + tx_index: { + type: 'bigint', + notNull: true, + }, + address: { + type: 'text', + notNull: true, + }, + // Only used when operation is `transfer_send`; used to optimize activity lookup for + // receiving addresses. + to_address: { + type: 'text', + }, + avail_balance: { + type: 'numeric', + notNull: true, + }, + trans_balance: { + type: 'numeric', + notNull: true, + }, + }); + pgm.createConstraint('brc20_operations', 'brc20_operations_pkey', { + primaryKey: ['genesis_id', 'operation'], + }); + pgm.createConstraint( + 'brc20_operations', + 'brc20_operations_ticker_fk', + 'FOREIGN KEY(ticker) REFERENCES brc20_tokens(ticker) ON DELETE CASCADE' + ); + pgm.createIndex('brc20_operations', ['operation']); + pgm.createIndex('brc20_operations', ['ticker', 'address']); + pgm.createIndex('brc20_operations', [ + { name: 'block_height', sort: 'DESC' }, + { name: 'tx_index', sort: 'DESC' }, + ]); + pgm.createIndex('brc20_operations', ['address', 'to_address']); +} diff --git a/migrations/1694021174916_brc20-total-balances.ts b/migrations/1711575178683_brc20-total-balances.ts similarity index 59% rename from migrations/1694021174916_brc20-total-balances.ts rename to migrations/1711575178683_brc20-total-balances.ts index c2d66828..6af59f2a 100644 --- a/migrations/1694021174916_brc20-total-balances.ts +++ b/migrations/1711575178683_brc20-total-balances.ts @@ -5,12 +5,8 @@ export const shorthands: ColumnDefinitions | undefined = undefined; export function up(pgm: MigrationBuilder): void { pgm.createTable('brc20_total_balances', { - id: { - type: 'bigserial', - primaryKey: true, - }, - brc20_deploy_id: { - type: 'bigint', + ticker: { + type: 'string', notNull: true, }, address: { @@ -32,17 +28,12 @@ export function up(pgm: MigrationBuilder): void { }); pgm.createConstraint( 'brc20_total_balances', - 'brc20_total_balances_brc20_deploy_id_fk', - 'FOREIGN KEY(brc20_deploy_id) REFERENCES brc20_deploys(id) ON DELETE CASCADE' - ); - pgm.createConstraint( - 'brc20_total_balances', - 'brc20_total_balances_unique', - 'UNIQUE(brc20_deploy_id, address)' + 'brc20_total_balances_ticker_fk', + 'FOREIGN KEY(ticker) REFERENCES brc20_tokens(ticker) ON DELETE CASCADE' ); + pgm.createConstraint('brc20_total_balances', 'brc20_total_balances_pkey', { + primaryKey: ['ticker', 'address'], + }); pgm.createIndex('brc20_total_balances', ['address']); - pgm.createIndex('brc20_total_balances', [ - 'brc20_deploy_id', - { name: 'total_balance', sort: 'DESC' }, - ]); + pgm.createIndex('brc20_total_balances', ['ticker', { name: 'total_balance', sort: 'DESC' }]); } diff --git a/migrations/1694295793981_brc20-event-counts.ts b/migrations/1711575178684_brc20-counts-by-operation.ts similarity index 86% rename from migrations/1694295793981_brc20-event-counts.ts rename to migrations/1711575178684_brc20-counts-by-operation.ts index 9bd0a6eb..52c7b754 100644 --- a/migrations/1694295793981_brc20-event-counts.ts +++ b/migrations/1711575178684_brc20-counts-by-operation.ts @@ -4,8 +4,8 @@ import { MigrationBuilder, ColumnDefinitions } from 'node-pg-migrate'; export const shorthands: ColumnDefinitions | undefined = undefined; export function up(pgm: MigrationBuilder): void { - pgm.createTable('brc20_counts_by_event_type', { - event_type: { + pgm.createTable('brc20_counts_by_operation', { + operation: { type: 'brc20_operation', notNull: true, primaryKey: true, diff --git a/migrations/1694299763914_brc20-token-count.ts b/migrations/1711575178686_brc20-counts-by-address-operation.ts similarity index 55% rename from migrations/1694299763914_brc20-token-count.ts rename to migrations/1711575178686_brc20-counts-by-address-operation.ts index de2ee291..9b1ff7a3 100644 --- a/migrations/1694299763914_brc20-token-count.ts +++ b/migrations/1711575178686_brc20-counts-by-address-operation.ts @@ -4,11 +4,14 @@ import { MigrationBuilder, ColumnDefinitions } from 'node-pg-migrate'; export const shorthands: ColumnDefinitions | undefined = undefined; export function up(pgm: MigrationBuilder): void { - pgm.createTable('brc20_counts_by_tokens', { - token_type: { + pgm.createTable('brc20_counts_by_address_operation', { + address: { type: 'text', notNull: true, - primaryKey: true, + }, + operation: { + type: 'brc20_operation', + notNull: true, }, count: { type: 'bigint', @@ -16,4 +19,9 @@ export function up(pgm: MigrationBuilder): void { default: 1, }, }); + pgm.createConstraint( + 'brc20_counts_by_address_operation', + 'brc20_counts_by_address_operation_pkey', + { primaryKey: ['address', 'operation'] } + ); } diff --git a/package-lock.json b/package-lock.json index 28f91c25..19c72ef7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "@fastify/swagger": "^8.3.1", "@fastify/type-provider-typebox": "^3.2.0", "@hirosystems/api-toolkit": "^1.4.0", - "@hirosystems/chainhook-client": "^1.7.0", + "@hirosystems/chainhook-client": "^1.8.0", "@semantic-release/changelog": "^6.0.3", "@semantic-release/commit-analyzer": "^10.0.4", "@semantic-release/git": "^10.0.1", @@ -95,35 +95,6 @@ "node": ">=18" } }, - "../chainhook/components/client/typescript": { - "name": "@hirosystems/chainhook-client", - "version": "1.4.2", - "extraneous": true, - "license": "Apache 2.0", - "dependencies": { - "@fastify/type-provider-typebox": "^3.2.0", - "fastify": "^4.15.0", - "pino": "^8.11.0", - "undici": "^5.21.2" - }, - "devDependencies": { - "@stacks/eslint-config": "^1.2.0", - "@types/jest": "^29.5.0", - "@types/node": "^18.15.7", - "@typescript-eslint/eslint-plugin": "^5.56.0", - "@typescript-eslint/parser": "^5.56.0", - "babel-jest": "^29.5.0", - "eslint": "^8.36.0", - "eslint-plugin-prettier": "^4.2.1", - "eslint-plugin-tsdoc": "^0.2.17", - "jest": "^29.5.0", - "prettier": "^2.8.7", - "rimraf": "^4.4.1", - "ts-jest": "^29.0.5", - "ts-node": "^10.9.1", - "typescript": "^5.0.2" - } - }, "node_modules/@ampproject/remapping": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", @@ -1299,9 +1270,9 @@ } }, "node_modules/@hirosystems/chainhook-client": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@hirosystems/chainhook-client/-/chainhook-client-1.7.0.tgz", - "integrity": "sha512-XRSbpu+Bxwvd8qqQTNcomfO8RYu+Dpnl9ZnB8EJE+tvJ4y3lUZD6Uk65368Us0Hbw+VNWnU2ibej7iqB6mGsOA==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@hirosystems/chainhook-client/-/chainhook-client-1.8.0.tgz", + "integrity": "sha512-BpYwrbxWuH0KGRyKq1T8nIiZUGaapOxz6yFZ653m6CJi7DS7kqOm2+v5X/DR0hbeZUmqriGMUJnROJ1tW08aEg==", "dependencies": { "@fastify/type-provider-typebox": "^3.2.0", "fastify": "^4.15.0", @@ -19743,9 +19714,9 @@ } }, "@hirosystems/chainhook-client": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@hirosystems/chainhook-client/-/chainhook-client-1.7.0.tgz", - "integrity": "sha512-XRSbpu+Bxwvd8qqQTNcomfO8RYu+Dpnl9ZnB8EJE+tvJ4y3lUZD6Uk65368Us0Hbw+VNWnU2ibej7iqB6mGsOA==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@hirosystems/chainhook-client/-/chainhook-client-1.8.0.tgz", + "integrity": "sha512-BpYwrbxWuH0KGRyKq1T8nIiZUGaapOxz6yFZ653m6CJi7DS7kqOm2+v5X/DR0hbeZUmqriGMUJnROJ1tW08aEg==", "requires": { "@fastify/type-provider-typebox": "^3.2.0", "fastify": "^4.15.0", diff --git a/package.json b/package.json index 08d55d38..132e5ff8 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "@fastify/swagger": "^8.3.1", "@fastify/type-provider-typebox": "^3.2.0", "@hirosystems/api-toolkit": "^1.4.0", - "@hirosystems/chainhook-client": "^1.7.0", + "@hirosystems/chainhook-client": "^1.8.0", "@semantic-release/changelog": "^6.0.3", "@semantic-release/commit-analyzer": "^10.0.4", "@semantic-release/git": "^10.0.1", diff --git a/src/api/util/helpers.ts b/src/api/util/helpers.ts index 9b7815eb..9ff8c520 100644 --- a/src/api/util/helpers.ts +++ b/src/api/util/helpers.ts @@ -146,7 +146,7 @@ export function parseBrc20Activities(items: DbBrc20Activity[]): Brc20ActivityRes const activity = { operation: i.operation, ticker: i.ticker, - address: i.address, + address: i.to_address ?? i.address, tx_id: i.tx_id, inscription_id: i.inscription_id, location: `${i.output}:${i.offset}`, @@ -169,22 +169,27 @@ export function parseBrc20Activities(items: DbBrc20Activity[]): Brc20ActivityRes return { ...activity, mint: { - amount: decimals(i.mint_amount, i.deploy_decimals), + amount: decimals(i.avail_balance, i.deploy_decimals), }, }; } case DbBrc20EventOperation.transfer: { - const [amount, from_address] = i.transfer_data.split(';'); return { ...activity, - transfer: { amount: decimals(amount, i.deploy_decimals), from_address }, + transfer: { + amount: decimals(i.trans_balance, i.deploy_decimals), + from_address: i.address, + }, }; } case DbBrc20EventOperation.transferSend: { - const [amount, from_address, to_address] = i.transfer_data.split(';'); return { ...activity, - transfer_send: { amount: decimals(amount, i.deploy_decimals), from_address, to_address }, + transfer_send: { + amount: decimals(BigNumber(i.trans_balance).abs().toString(), i.deploy_decimals), + from_address: i.address, + to_address: i.to_address ?? i.address, + }, }; } } diff --git a/src/env.ts b/src/env.ts index 956d42f3..fc8f0389 100644 --- a/src/env.ts +++ b/src/env.ts @@ -64,9 +64,6 @@ const schema = Type.Object({ PG_IDLE_TIMEOUT: Type.Number({ default: 30 }), PG_MAX_LIFETIME: Type.Number({ default: 60 }), PG_STATEMENT_TIMEOUT: Type.Number({ default: 60_000 }), - - /** Enables BRC-20 processing in write mode APIs */ - BRC20_BLOCK_SCAN_ENABLED: Type.Boolean({ default: true }), }); type Env = Static; diff --git a/src/ordhook/server.ts b/src/ordhook/server.ts index a414da32..7f604584 100644 --- a/src/ordhook/server.ts +++ b/src/ordhook/server.ts @@ -34,6 +34,7 @@ export async function startOrdhookServer(args: { db: PgStore }): Promise[] | undefined) { - return partials?.reduce((acc, curr) => this.sql`${acc} OR ${curr}`); - } - - async insertOperations(args: { - reveals: InscriptionEventData[]; - pointers: DbLocationPointerInsert[]; - }): Promise { - for (const [i, reveal] of args.reveals.entries()) { - const pointer = args.pointers[i]; - if (parseInt(pointer.block_height) < BRC20_GENESIS_BLOCK) continue; - if ('inscription' in reveal) { - const brc20 = brc20FromInscription(reveal); - if (brc20) { - switch (brc20.op) { - case 'deploy': - await this.insertDeploy({ brc20, reveal, pointer }); - break; - case 'mint': - await this.insertMint({ brc20, reveal, pointer }); - break; - case 'transfer': - await this.insertTransfer({ brc20, reveal, pointer }); - break; + async updateBrc20Operations(event: BitcoinEvent, direction: 'apply' | 'rollback'): Promise { + await this.sqlWriteTransaction(async sql => { + const block_height = event.block_identifier.index.toString(); + const cache = new Brc20BlockCache(); + for (const tx of event.transactions) { + const tx_index = tx.metadata.index.toString(); + if (tx.metadata.brc20_operation) { + const operation = tx.metadata.brc20_operation; + if ('deploy' in operation) { + cache.tokens.push({ + block_height, + genesis_id: operation.deploy.inscription_id, + tx_id: tx.transaction_identifier.hash, + address: operation.deploy.address, + ticker: operation.deploy.tick, + max: operation.deploy.max, + limit: operation.deploy.lim, + decimals: operation.deploy.dec, + self_mint: operation.deploy.self_mint, + }); + cache.operations.push({ + block_height, + tx_index, + genesis_id: operation.deploy.inscription_id, + ticker: operation.deploy.tick, + address: operation.deploy.address, + avail_balance: '0', + trans_balance: '0', + operation: DbBrc20Operation.deploy, + }); + cache.increaseOperationCount(DbBrc20Operation.deploy); + cache.increaseAddressOperationCount(operation.deploy.address, DbBrc20Operation.deploy); + cache.increaseTokenTxCount(operation.deploy.tick); + logger.info( + `Brc20PgStore ${direction} deploy ${operation.deploy.tick} by ${operation.deploy.address} at height ${block_height}` + ); + } else if ('mint' in operation) { + cache.operations.push({ + block_height, + tx_index, + genesis_id: operation.mint.inscription_id, + ticker: operation.mint.tick, + address: operation.mint.address, + avail_balance: operation.mint.amt, + trans_balance: '0', + operation: DbBrc20Operation.mint, + }); + const amt = BigNumber(operation.mint.amt); + cache.increaseTokenMintedSupply(operation.mint.tick, amt); + cache.increaseTokenTxCount(operation.mint.tick); + cache.increaseOperationCount(DbBrc20Operation.mint); + cache.increaseAddressOperationCount(operation.mint.address, DbBrc20Operation.mint); + cache.updateAddressBalance( + operation.mint.tick, + operation.mint.address, + amt, + BigNumber(0), + amt + ); + logger.info( + `Brc20PgStore ${direction} mint ${operation.mint.tick} ${operation.mint.amt} by ${operation.mint.address} at height ${block_height}` + ); + } else if ('transfer' in operation) { + cache.operations.push({ + block_height, + tx_index, + genesis_id: operation.transfer.inscription_id, + ticker: operation.transfer.tick, + address: operation.transfer.address, + avail_balance: BigNumber(operation.transfer.amt).negated().toString(), + trans_balance: operation.transfer.amt, + operation: DbBrc20Operation.transfer, + }); + const amt = BigNumber(operation.transfer.amt); + cache.increaseOperationCount(DbBrc20Operation.transfer); + cache.increaseTokenTxCount(operation.transfer.tick); + cache.increaseAddressOperationCount( + operation.transfer.address, + DbBrc20Operation.transfer + ); + cache.updateAddressBalance( + operation.transfer.tick, + operation.transfer.address, + amt.negated(), + amt, + BigNumber(0) + ); + logger.info( + `Brc20PgStore ${direction} transfer ${operation.transfer.tick} ${operation.transfer.amt} by ${operation.transfer.address} at height ${block_height}` + ); + } else if ('transfer_send' in operation) { + cache.operations.push({ + block_height, + tx_index, + genesis_id: operation.transfer_send.inscription_id, + ticker: operation.transfer_send.tick, + address: operation.transfer_send.sender_address, + avail_balance: '0', + trans_balance: BigNumber(operation.transfer_send.amt).negated().toString(), + operation: DbBrc20Operation.transferSend, + }); + cache.transferReceivers.set( + operation.transfer_send.inscription_id, + operation.transfer_send.receiver_address + ); + cache.operations.push({ + block_height, + tx_index, + genesis_id: operation.transfer_send.inscription_id, + ticker: operation.transfer_send.tick, + address: operation.transfer_send.receiver_address, + avail_balance: operation.transfer_send.amt, + trans_balance: '0', + operation: DbBrc20Operation.transferReceive, + }); + const amt = BigNumber(operation.transfer_send.amt); + cache.increaseOperationCount(DbBrc20Operation.transferSend); + cache.increaseTokenTxCount(operation.transfer_send.tick); + cache.increaseAddressOperationCount( + operation.transfer_send.sender_address, + DbBrc20Operation.transferSend + ); + if ( + operation.transfer_send.sender_address != operation.transfer_send.receiver_address + ) { + cache.increaseAddressOperationCount( + operation.transfer_send.receiver_address, + DbBrc20Operation.transferSend + ); + } + cache.updateAddressBalance( + operation.transfer_send.tick, + operation.transfer_send.sender_address, + BigNumber('0'), + amt.negated(), + amt.negated() + ); + cache.updateAddressBalance( + operation.transfer_send.tick, + operation.transfer_send.receiver_address, + amt, + BigNumber(0), + amt + ); + logger.info( + `Brc20PgStore ${direction} transfer_send ${operation.transfer_send.tick} ${operation.transfer_send.amt} from ${operation.transfer_send.sender_address} to ${operation.transfer_send.receiver_address} at height ${block_height}` + ); } } - } else { - await this.applyTransfer({ reveal, pointer }); } - } + if (direction === 'apply') await this.applyOperations(sql, cache); + else await this.rollBackOperations(sql, cache); + }); } - async applyTransfer(args: { - reveal: InscriptionEventData; - pointer: DbLocationPointerInsert; - }): Promise { - await this.sqlWriteTransaction(async sql => { - // Get the sender address for this transfer. We need to get this in a separate query to know - // if we should alter the write query to accomodate a "return to sender" scenario. - const fromAddressRes = await sql<{ from_address: string }[]>` - SELECT from_address FROM brc20_transfers WHERE inscription_id = ${args.pointer.inscription_id} + private async applyOperations(sql: PgSqlClient, cache: Brc20BlockCache) { + if (cache.tokens.length) + for await (const batch of batchIterate(cache.tokens, INSERT_BATCH_SIZE)) + await sql` + INSERT INTO brc20_tokens ${sql(batch)} + ON CONFLICT (ticker) DO NOTHING + `; + if (cache.operations.length) + for await (const batch of batchIterate(cache.operations, INSERT_BATCH_SIZE)) + await sql` + INSERT INTO brc20_operations ${sql(batch)} + ON CONFLICT (genesis_id, operation) DO NOTHING + `; + for (const [inscription_id, to_address] of cache.transferReceivers) + await sql` + UPDATE brc20_operations SET to_address = ${to_address} + WHERE genesis_id = ${inscription_id} AND operation = 'transfer_send' `; - if (fromAddressRes.count === 0) return; - const fromAddress = fromAddressRes[0].from_address; - // Is this transfer sent as fee or from the same sender? If so, we'll return the balance. - // Is it burnt? Mark as empty owner. - const returnToSender = - args.reveal.location.transfer_type == DbLocationTransferType.spentInFees || - fromAddress == args.pointer.address; - const toAddress = returnToSender - ? fromAddress - : args.reveal.location.transfer_type == DbLocationTransferType.burnt - ? '' - : args.pointer.address; - // Check if we have a valid transfer inscription emitted by this address that hasn't been sent - // to another address before. Use `LIMIT 3` as a quick way of checking if we have just inserted - // the first transfer for this inscription (genesis + transfer). - const sendRes = await sql` - WITH transfer_data AS ( - SELECT t.id, t.amount, t.brc20_deploy_id, t.from_address, ROW_NUMBER() OVER() - FROM locations AS l - INNER JOIN brc20_transfers AS t ON t.inscription_id = l.inscription_id - WHERE l.inscription_id = ${args.pointer.inscription_id} - AND ( - l.block_height < ${args.pointer.block_height} - OR (l.block_height = ${args.pointer.block_height} - AND l.tx_index <= ${args.pointer.tx_index}) - ) - LIMIT 3 - ), - validated_transfer AS ( - SELECT * FROM transfer_data - WHERE NOT EXISTS(SELECT id FROM transfer_data WHERE row_number = 3) - LIMIT 1 - ), - updated_transfer AS ( - UPDATE brc20_transfers - SET to_address = ${toAddress} - WHERE id = (SELECT id FROM validated_transfer) - ), - balance_insert_from AS ( - INSERT INTO brc20_balances (inscription_id, location_id, brc20_deploy_id, address, avail_balance, trans_balance, type) ( - SELECT ${args.pointer.inscription_id}, ${args.pointer.location_id}, brc20_deploy_id, - from_address, 0, -1 * amount, ${DbBrc20BalanceTypeId.transferFrom} - FROM validated_transfer - ) - ON CONFLICT ON CONSTRAINT brc20_balances_inscription_id_type_unique DO NOTHING - ), - balance_insert_to AS ( - INSERT INTO brc20_balances (inscription_id, location_id, brc20_deploy_id, address, avail_balance, trans_balance, type) ( - SELECT ${args.pointer.inscription_id}, ${args.pointer.location_id}, brc20_deploy_id, - ${toAddress}, amount, 0, ${DbBrc20BalanceTypeId.transferTo} - FROM validated_transfer - ) - ON CONFLICT ON CONSTRAINT brc20_balances_inscription_id_type_unique DO NOTHING - ), - ${ - returnToSender - ? sql` - total_balance_revert AS ( - UPDATE brc20_total_balances SET - avail_balance = avail_balance + (SELECT amount FROM validated_transfer), - trans_balance = trans_balance - (SELECT amount FROM validated_transfer) - WHERE brc20_deploy_id = (SELECT brc20_deploy_id FROM validated_transfer) - AND address = (SELECT from_address FROM validated_transfer) - ), - address_event_type_count_increase AS ( - INSERT INTO brc20_counts_by_address_event_type (address, transfer_send) - (SELECT from_address, 1 FROM validated_transfer) - ON CONFLICT (address) DO UPDATE SET transfer_send = brc20_counts_by_address_event_type.transfer_send + EXCLUDED.transfer_send - ) - ` - : sql` - total_balance_insert_from AS ( - UPDATE brc20_total_balances SET - trans_balance = trans_balance - (SELECT amount FROM validated_transfer), - total_balance = total_balance - (SELECT amount FROM validated_transfer) - WHERE brc20_deploy_id = (SELECT brc20_deploy_id FROM validated_transfer) - AND address = (SELECT from_address FROM validated_transfer) - ), - total_balance_insert_to AS ( - INSERT INTO brc20_total_balances (brc20_deploy_id, address, avail_balance, trans_balance, total_balance) ( - SELECT brc20_deploy_id, ${toAddress}, amount, 0, amount - FROM validated_transfer - ) - ON CONFLICT ON CONSTRAINT brc20_total_balances_unique DO UPDATE SET - avail_balance = brc20_total_balances.avail_balance + EXCLUDED.avail_balance, - total_balance = brc20_total_balances.total_balance + EXCLUDED.total_balance - ), - address_event_type_count_increase_from AS ( - INSERT INTO brc20_counts_by_address_event_type (address, transfer_send) - (SELECT from_address, 1 FROM validated_transfer) - ON CONFLICT (address) DO UPDATE SET transfer_send = brc20_counts_by_address_event_type.transfer_send + EXCLUDED.transfer_send - ), - address_event_type_count_increase_to AS ( - INSERT INTO brc20_counts_by_address_event_type (address, transfer_send) - (SELECT ${toAddress}, 1 FROM validated_transfer) - ON CONFLICT (address) DO UPDATE SET transfer_send = brc20_counts_by_address_event_type.transfer_send + EXCLUDED.transfer_send - ) - ` - }, deploy_update AS ( - UPDATE brc20_deploys - SET tx_count = tx_count + 1 - WHERE id = (SELECT brc20_deploy_id FROM validated_transfer) - ), - event_type_count_increase AS ( - INSERT INTO brc20_counts_by_event_type (event_type, count) - (SELECT 'transfer_send', COALESCE(COUNT(*), 0) FROM validated_transfer) - ON CONFLICT (event_type) DO UPDATE SET count = brc20_counts_by_event_type.count + EXCLUDED.count - ) - INSERT INTO brc20_events (operation, inscription_id, genesis_location_id, brc20_deploy_id, transfer_id, address, from_address) ( - SELECT 'transfer_send', ${args.pointer.inscription_id}, ${args.pointer.location_id}, - brc20_deploy_id, id, ${toAddress}, from_address - FROM validated_transfer - ) + for (const [ticker, amount] of cache.tokenMintSupplies) + await sql` + UPDATE brc20_tokens SET minted_supply = minted_supply + ${amount.toString()} + WHERE ticker = ${ticker} `; - if (sendRes.count) - logger.info( - `Brc20PgStore send transfer to ${toAddress} at block ${args.pointer.block_height}` - ); - }); - } - - private async insertDeploy(deploy: { - brc20: Brc20Deploy; - reveal: InscriptionRevealData; - pointer: DbLocationPointerInsert; - }): Promise { - if (deploy.reveal.location.transfer_type != DbLocationTransferType.transferred) return; - const insert: DbBrc20DeployInsert = { - inscription_id: deploy.pointer.inscription_id, - block_height: deploy.pointer.block_height, - tx_id: deploy.reveal.location.tx_id, - address: deploy.pointer.address as string, - ticker: deploy.brc20.tick, - 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 ( - INSERT INTO brc20_deploys ${this.sql(insert)} - ON CONFLICT (LOWER(ticker)) DO NOTHING - RETURNING id - ), - event_type_count_increase AS ( - INSERT INTO brc20_counts_by_event_type (event_type, count) - (SELECT 'deploy', COALESCE(COUNT(*), 0) FROM deploy_insert) - ON CONFLICT (event_type) DO UPDATE SET count = brc20_counts_by_event_type.count + EXCLUDED.count - ), - address_event_type_count_increase AS ( - INSERT INTO brc20_counts_by_address_event_type (address, deploy) - (SELECT ${deploy.pointer.address}, COALESCE(COUNT(*), 0) FROM deploy_insert) - ON CONFLICT (address) DO UPDATE SET deploy = brc20_counts_by_address_event_type.deploy + EXCLUDED.deploy - ), - token_count_increase AS ( - INSERT INTO brc20_counts_by_tokens (token_type, count) - (SELECT 'token', COALESCE(COUNT(*), 0) FROM deploy_insert) - ON CONFLICT (token_type) DO UPDATE SET count = brc20_counts_by_tokens.count + EXCLUDED.count - ) - INSERT INTO brc20_events (operation, inscription_id, genesis_location_id, brc20_deploy_id, deploy_id, address) ( - SELECT 'deploy', ${deploy.pointer.inscription_id}, ${deploy.pointer.location_id}, id, id, - ${deploy.pointer.address} - FROM deploy_insert - ) - `; - if (deployRes.count) - logger.info( - `Brc20PgStore deploy ${deploy.brc20.tick} by ${deploy.pointer.address} at block ${deploy.pointer.block_height}` - ); - } - - private async insertMint(mint: { - brc20: Brc20Mint; - 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 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 - id AS brc20_deploy_id, - LEAST(${mint.brc20.amt}::numeric, max - minted_supply) AS real_mint_amount - 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) ( - SELECT ${mint.pointer.inscription_id}, brc20_deploy_id, ${mint.pointer.block_height}, - ${mint.reveal.location.tx_id}, ${mint.pointer.address}, ${mint.brc20.amt}::numeric - FROM validated_mint - ) - ON CONFLICT (inscription_id) DO NOTHING - RETURNING id, brc20_deploy_id - ), - deploy_update AS ( - UPDATE brc20_deploys - SET - minted_supply = minted_supply + (SELECT real_mint_amount FROM validated_mint), - tx_count = tx_count + 1 - WHERE id = (SELECT brc20_deploy_id FROM validated_mint) - ), - balance_insert AS ( - INSERT INTO brc20_balances (inscription_id, location_id, brc20_deploy_id, address, avail_balance, trans_balance, type) ( - SELECT ${mint.pointer.inscription_id}, ${mint.pointer.location_id}, brc20_deploy_id, - ${mint.pointer.address}, real_mint_amount, 0, ${DbBrc20BalanceTypeId.mint} - FROM validated_mint - ) - ON CONFLICT ON CONSTRAINT brc20_balances_inscription_id_type_unique DO NOTHING - ), - total_balance_insert AS ( - INSERT INTO brc20_total_balances (brc20_deploy_id, address, avail_balance, trans_balance, total_balance) ( - SELECT brc20_deploy_id, ${mint.pointer.address}, real_mint_amount, 0, real_mint_amount - FROM validated_mint - ) - ON CONFLICT ON CONSTRAINT brc20_total_balances_unique DO UPDATE SET - avail_balance = brc20_total_balances.avail_balance + EXCLUDED.avail_balance, - total_balance = brc20_total_balances.total_balance + EXCLUDED.total_balance - ), - event_type_count_increase AS ( - INSERT INTO brc20_counts_by_event_type (event_type, count) - (SELECT 'mint', COALESCE(COUNT(*), 0) FROM validated_mint) - ON CONFLICT (event_type) DO UPDATE SET count = brc20_counts_by_event_type.count + EXCLUDED.count - ), - address_event_type_count_increase AS ( - INSERT INTO brc20_counts_by_address_event_type (address, mint) - (SELECT ${mint.pointer.address}, COALESCE(COUNT(*), 0) FROM validated_mint) - ON CONFLICT (address) DO UPDATE SET mint = brc20_counts_by_address_event_type.mint + EXCLUDED.mint - ) - INSERT INTO brc20_events (operation, inscription_id, genesis_location_id, brc20_deploy_id, mint_id, address) ( - SELECT 'mint', ${mint.pointer.inscription_id}, ${mint.pointer.location_id}, brc20_deploy_id, id, ${mint.pointer.address} - FROM mint_insert - ) - `; - if (mintRes.count) - logger.info( - `Brc20PgStore mint ${mint.brc20.tick} (${mint.brc20.amt}) by ${mint.pointer.address} at block ${mint.pointer.block_height}` - ); - } - - private async insertTransfer(transfer: { - brc20: Brc20Transfer; - reveal: InscriptionEventData; - pointer: DbLocationPointerInsert; - }): Promise { - if (transfer.reveal.location.transfer_type != DbLocationTransferType.transferred) return; - const transferRes = await this.sql` - 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) ( - SELECT ${transfer.pointer.inscription_id}, brc20_deploy_id, - ${transfer.pointer.block_height}, ${transfer.reveal.location.tx_id}, - ${transfer.pointer.address}, NULL, ${transfer.brc20.amt}::numeric - FROM validated_transfer - ) - ON CONFLICT (inscription_id) DO NOTHING - RETURNING id, brc20_deploy_id - ), - balance_insert AS ( - INSERT INTO brc20_balances (inscription_id, location_id, brc20_deploy_id, address, avail_balance, trans_balance, type) ( - SELECT ${transfer.pointer.inscription_id}, ${transfer.pointer.location_id}, brc20_deploy_id, - ${transfer.pointer.address}, -1 * ${transfer.brc20.amt}::numeric, - ${transfer.brc20.amt}::numeric, ${DbBrc20BalanceTypeId.transferIntent} - FROM validated_transfer - ) - ON CONFLICT ON CONSTRAINT brc20_balances_inscription_id_type_unique DO NOTHING - ), - total_balance_update AS ( - UPDATE brc20_total_balances SET - avail_balance = avail_balance - ${transfer.brc20.amt}::numeric, - trans_balance = trans_balance + ${transfer.brc20.amt}::numeric - WHERE brc20_deploy_id = (SELECT brc20_deploy_id FROM validated_transfer) - AND address = ${transfer.pointer.address} - ), - deploy_update AS ( - UPDATE brc20_deploys - SET tx_count = tx_count + 1 - WHERE id = (SELECT brc20_deploy_id FROM validated_transfer) - ), - event_type_count_increase AS ( - INSERT INTO brc20_counts_by_event_type (event_type, count) - (SELECT 'transfer', COALESCE(COUNT(*), 0) FROM validated_transfer) - ON CONFLICT (event_type) DO UPDATE SET count = brc20_counts_by_event_type.count + EXCLUDED.count - ), - address_event_type_count_increase AS ( - INSERT INTO brc20_counts_by_address_event_type (address, transfer) - (SELECT ${transfer.pointer.address}, COALESCE(COUNT(*), 0) FROM validated_transfer) - ON CONFLICT (address) DO UPDATE SET transfer = brc20_counts_by_address_event_type.transfer + EXCLUDED.transfer - ) - INSERT INTO brc20_events (operation, inscription_id, genesis_location_id, brc20_deploy_id, transfer_id, address) ( - SELECT 'transfer', ${transfer.pointer.inscription_id}, ${transfer.pointer.location_id}, brc20_deploy_id, id, ${transfer.pointer.address} - FROM transfer_insert - ) - `; - if (transferRes.count) - logger.info( - `Brc20PgStore transfer ${transfer.brc20.tick} (${transfer.brc20.amt}) by ${transfer.pointer.address} at block ${transfer.pointer.block_height}` - ); - } - - async rollBackInscription(args: { inscription: InscriptionData }): Promise { - const events = await this.sql` - SELECT e.* FROM brc20_events AS e - INNER JOIN inscriptions AS i ON i.id = e.inscription_id - WHERE i.genesis_id = ${args.inscription.genesis_id} - `; - if (events.count === 0) return; - // Traverse all activities generated by this inscription and roll back actions that are NOT - // otherwise handled by the ON DELETE CASCADE postgres constraint. - for (const event of events) { - switch (event.operation) { - case 'deploy': - await this.rollBackDeploy(event); - break; - case 'mint': - await this.rollBackMint(event); - break; - case 'transfer': - await this.rollBackTransfer(event); - break; - } + for (const [ticker, num] of cache.tokenTxCounts) + await sql` + UPDATE brc20_tokens SET tx_count = tx_count + ${num} WHERE ticker = ${ticker} + `; + if (cache.operationCounts.size) { + const entries = []; + for (const [operation, count] of cache.operationCounts) entries.push({ operation, count }); + for await (const batch of batchIterate(entries, INSERT_BATCH_SIZE)) + await sql` + INSERT INTO brc20_counts_by_operation ${sql(batch)} + ON CONFLICT (operation) DO UPDATE SET + count = brc20_counts_by_operation.count + EXCLUDED.count + `; } - } - - async rollBackLocation(args: { location: LocationData }): Promise { - const events = await this.sql` - SELECT e.* FROM brc20_events AS e - INNER JOIN locations AS l ON l.id = e.genesis_location_id - WHERE output = ${args.location.output} AND "offset" = ${args.location.offset} - `; - if (events.count === 0) return; - // Traverse all activities generated by this location and roll back actions that are NOT - // otherwise handled by the ON DELETE CASCADE postgres constraint. - for (const event of events) { - switch (event.operation) { - case 'transfer_send': - await this.rollBackTransferSend(event); - break; - } + if (cache.addressOperationCounts.size) { + const entries = []; + for (const [address, map] of cache.addressOperationCounts) + for (const [operation, count] of map) entries.push({ address, operation, count }); + for await (const batch of batchIterate(entries, INSERT_BATCH_SIZE)) + await sql` + INSERT INTO brc20_counts_by_address_operation ${sql(batch)} + ON CONFLICT (address, operation) DO UPDATE SET + count = brc20_counts_by_address_operation.count + EXCLUDED.count + `; + } + if (cache.totalBalanceChanges.size) { + const entries = []; + for (const [address, map] of cache.totalBalanceChanges) + for (const [ticker, values] of map) + entries.push({ + ticker, + address, + avail_balance: values.avail.toString(), + trans_balance: values.trans.toString(), + total_balance: values.total.toString(), + }); + for await (const batch of batchIterate(entries, INSERT_BATCH_SIZE)) + await sql` + INSERT INTO brc20_total_balances ${sql(batch)} + ON CONFLICT (ticker, address) DO UPDATE SET + avail_balance = brc20_total_balances.avail_balance + EXCLUDED.avail_balance, + trans_balance = brc20_total_balances.trans_balance + EXCLUDED.trans_balance, + total_balance = brc20_total_balances.total_balance + EXCLUDED.total_balance + `; } } - private async rollBackDeploy(activity: DbBrc20DeployEvent): Promise { - // - tx_count is lost successfully, since the deploy will be deleted. - await this.sql` - WITH decrease_event_count AS ( - UPDATE brc20_counts_by_event_type - SET count = count - 1 - WHERE event_type = 'deploy' - ), - decrease_address_event_count AS ( - UPDATE brc20_counts_by_address_event_type - SET deploy = deploy - 1 - WHERE address = (SELECT address FROM locations WHERE id = ${activity.genesis_location_id}) - ) - UPDATE brc20_counts_by_tokens - SET count = count - 1 - `; - } - - private async rollBackMint(activity: DbBrc20MintEvent): Promise { - // Get real minted amount and substract from places. - await this.sql` - WITH minted_balance AS ( - SELECT address, avail_balance - FROM brc20_balances - WHERE inscription_id = ${activity.inscription_id} AND type = ${DbBrc20BalanceTypeId.mint} - ), - deploy_update AS ( - UPDATE brc20_deploys - SET - minted_supply = minted_supply - (SELECT avail_balance FROM minted_balance), - tx_count = tx_count - 1 - WHERE id = ${activity.brc20_deploy_id} - ), - decrease_event_count AS ( - UPDATE brc20_counts_by_event_type - SET count = count - 1 - WHERE event_type = 'mint' - ), - decrease_address_event_count AS ( - UPDATE brc20_counts_by_address_event_type - SET mint = mint - 1 - WHERE address = (SELECT address FROM locations WHERE id = ${activity.genesis_location_id}) - ) - UPDATE brc20_total_balances SET - avail_balance = avail_balance - (SELECT avail_balance FROM minted_balance), - total_balance = total_balance - (SELECT avail_balance FROM minted_balance) - WHERE address = (SELECT address FROM minted_balance) - AND brc20_deploy_id = ${activity.brc20_deploy_id} - `; - } - - private async rollBackTransfer(activity: DbBrc20TransferEvent): Promise { - // Subtract tx_count per transfer event (transfer and transfer_send are - // separate events, so they will both be counted). - await this.sql` - WITH transferrable_balance AS ( - SELECT address, trans_balance - FROM brc20_balances - WHERE inscription_id = ${activity.inscription_id} AND type = ${DbBrc20BalanceTypeId.transferIntent} - ), - decrease_event_count AS ( - UPDATE brc20_counts_by_event_type - SET count = count - 1 - WHERE event_type = 'transfer' - ), - decrease_address_event_count AS ( - UPDATE brc20_counts_by_address_event_type - SET transfer = transfer - 1 - WHERE address = (SELECT address FROM locations WHERE id = ${activity.genesis_location_id}) - ), - decrease_tx_count AS ( - UPDATE brc20_deploys - SET tx_count = tx_count - 1 - WHERE id = ${activity.brc20_deploy_id} - ) - UPDATE brc20_total_balances SET - trans_balance = trans_balance - (SELECT trans_balance FROM transferrable_balance), - avail_balance = avail_balance + (SELECT trans_balance FROM transferrable_balance) - WHERE address = (SELECT address FROM transferrable_balance) - AND brc20_deploy_id = ${activity.brc20_deploy_id} - `; - } - - private async rollBackTransferSend(activity: DbBrc20TransferEvent): Promise { - await this.sqlWriteTransaction(async sql => { - // Get the sender/receiver address for this transfer. We need to get this in a separate query - // to know if we should alter the write query to accomodate a "return to sender" scenario. - const addressRes = await sql<{ returned_to_sender: boolean }[]>` - SELECT from_address = to_address AS returned_to_sender - FROM brc20_transfers - WHERE inscription_id = ${activity.inscription_id} + private async rollBackOperations(sql: PgSqlClient, cache: Brc20BlockCache) { + if (cache.totalBalanceChanges.size) { + for (const [address, map] of cache.totalBalanceChanges) + for (const [ticker, values] of map) + await sql` + UPDATE brc20_total_balances SET + avail_balance = avail_balance - ${values.avail}, + trans_balance = trans_balance - ${values.trans}, + total_balance = total_balance - ${values.total} + WHERE address = ${address} AND ticker = ${ticker} + `; + } + if (cache.addressOperationCounts.size) { + for (const [address, map] of cache.addressOperationCounts) + for (const [operation, count] of map) + await sql` + UPDATE brc20_counts_by_address_operation + SET count = count - ${count} + WHERE address = ${address} AND operation = ${operation} + `; + } + if (cache.operationCounts.size) { + for (const [operation, count] of cache.operationCounts) + await sql` + UPDATE brc20_counts_by_operation + SET count = count - ${count} + WHERE operation = ${operation} + `; + } + for (const [ticker, amount] of cache.tokenMintSupplies) + await sql` + UPDATE brc20_tokens SET minted_supply = minted_supply - ${amount.toString()} + WHERE ticker = ${ticker} `; - if (addressRes.count === 0) return; - const returnedToSender = addressRes[0].returned_to_sender; + for (const [ticker, num] of cache.tokenTxCounts) await sql` - WITH sent_balance_from AS ( - SELECT address, trans_balance - FROM brc20_balances - WHERE inscription_id = ${activity.inscription_id} - AND type = ${DbBrc20BalanceTypeId.transferFrom} - ), - sent_balance_to AS ( - SELECT address, avail_balance - FROM brc20_balances - WHERE inscription_id = ${activity.inscription_id} - AND type = ${DbBrc20BalanceTypeId.transferTo} - ), - decrease_event_count AS ( - UPDATE brc20_counts_by_event_type - SET count = count - 1 - WHERE event_type = 'transfer_send' - ), - ${ - returnedToSender - ? sql` - decrease_address_event_count AS ( - UPDATE brc20_counts_by_address_event_type - SET transfer_send = transfer_send - 1 - WHERE address = (SELECT address FROM sent_balance_from) - ), - undo_sent_balance AS ( - UPDATE brc20_total_balances SET - trans_balance = trans_balance - (SELECT trans_balance FROM sent_balance_from), - avail_balance = avail_balance + (SELECT trans_balance FROM sent_balance_from) - WHERE address = (SELECT address FROM sent_balance_from) - AND brc20_deploy_id = ${activity.brc20_deploy_id} - ) - ` - : sql` - decrease_address_event_count_from AS ( - UPDATE brc20_counts_by_address_event_type - SET transfer_send = transfer_send - 1 - WHERE address = (SELECT address FROM sent_balance_from) - ), - decrease_address_event_count_to AS ( - UPDATE brc20_counts_by_address_event_type - SET transfer_send = transfer_send - 1 - WHERE address = (SELECT address FROM sent_balance_to) - ), - undo_sent_balance_from AS ( - UPDATE brc20_total_balances SET - trans_balance = trans_balance - (SELECT trans_balance FROM sent_balance_from), - total_balance = total_balance - (SELECT trans_balance FROM sent_balance_from) - WHERE address = (SELECT address FROM sent_balance_from) - AND brc20_deploy_id = ${activity.brc20_deploy_id} - ), - undo_sent_balance_to AS ( - UPDATE brc20_total_balances SET - avail_balance = avail_balance - (SELECT avail_balance FROM sent_balance_to), - total_balance = total_balance - (SELECT avail_balance FROM sent_balance_to) - WHERE address = (SELECT address FROM sent_balance_to) - AND brc20_deploy_id = ${activity.brc20_deploy_id} - ) - ` - } - UPDATE brc20_deploys - SET tx_count = tx_count - 1 - WHERE id = ${activity.brc20_deploy_id} + UPDATE brc20_tokens SET tx_count = tx_count - ${num} WHERE ticker = ${ticker} `; - }); + for (const [inscription_id, _] of cache.transferReceivers) + await sql` + UPDATE brc20_operations SET to_address = NULL + WHERE genesis_id = ${inscription_id} AND operation = 'transfer_send' + `; + if (cache.operations.length) { + const blockHeights = cache.operations.map(o => o.block_height); + for await (const batch of batchIterate(blockHeights, INSERT_BATCH_SIZE)) + await sql` + DELETE FROM brc20_operations WHERE block_height IN ${sql(batch)} + `; + } + if (cache.tokens.length) { + const tickers = cache.tokens.map(t => t.ticker); + for await (const batch of batchIterate(tickers, INSERT_BATCH_SIZE)) + await sql` + DELETE FROM brc20_tokens WHERE ticker IN ${sql(batch)} + `; + } } async getTokens( args: { ticker?: string[]; order_by?: Brc20TokenOrderBy } & DbInscriptionIndexPaging ): Promise> { - const tickerPrefixCondition = this.sqlOr( - args.ticker?.map(t => this.sql`d.ticker_lower LIKE LOWER(${t}) || '%'`) + const tickerPrefixCondition = sqlOr( + this.sql, + args.ticker?.map(t => this.sql`d.ticker LIKE LOWER(${t}) || '%'`) ); const orderBy = args.order_by === Brc20TokenOrderBy.tx_count - ? this.sql`tx_count DESC` // tx_count + ? this.sql`d.tx_count DESC` // tx_count : this.sql`l.block_height DESC, l.tx_index DESC`; // default: `index` const results = await this.sql<(DbBrc20Token & { total: number })[]>` ${ args.ticker === undefined ? this.sql`WITH global_count AS ( - SELECT COALESCE(count, 0) AS count FROM brc20_counts_by_tokens + SELECT COALESCE(count, 0) AS count + FROM brc20_counts_by_operation + WHERE operation = 'deploy' )` : this.sql`` } SELECT - ${this.sql(BRC20_DEPLOYS_COLUMNS.map(c => `d.${c}`))}, - i.number, i.genesis_id, l.timestamp, + d.*, i.number, l.timestamp, ${ args.ticker ? this.sql`COUNT(*) OVER()` : this.sql`(SELECT count FROM global_count)` } AS total - FROM brc20_deploys AS d - INNER JOIN inscriptions AS i ON i.id = d.inscription_id - INNER JOIN genesis_locations AS g ON g.inscription_id = d.inscription_id + FROM brc20_tokens AS d + INNER JOIN inscriptions AS i ON i.genesis_id = d.genesis_id + INNER JOIN genesis_locations AS g ON g.inscription_id = i.id INNER JOIN locations AS l ON l.id = g.location_id ${tickerPrefixCondition ? this.sql`WHERE ${tickerPrefixCondition}` : this.sql``} ORDER BY ${orderBy} @@ -648,15 +348,12 @@ export class Brc20PgStore extends BasePgStoreModule { block_height?: number; } & DbInscriptionIndexPaging ): Promise> { - const ticker = this.sqlOr( - args.ticker?.map(t => this.sql`d.ticker_lower LIKE LOWER(${t}) || '%'`) + const ticker = sqlOr( + this.sql, + args.ticker?.map(t => this.sql`d.ticker LIKE LOWER(${t}) || '%'`) ); // Change selection table depending if we're filtering by block height or not. const results = await this.sql<(DbBrc20Balance & { total: number })[]>` - WITH token_ids AS ( - SELECT id FROM brc20_deploys AS d - WHERE ${ticker ? ticker : this.sql`FALSE`} - ) ${ args.block_height ? this.sql` @@ -666,24 +363,23 @@ export class Brc20PgStore extends BasePgStoreModule { SUM(b.trans_balance) AS trans_balance, SUM(b.avail_balance + b.trans_balance) AS total_balance, COUNT(*) OVER() as total - FROM brc20_balances AS b - INNER JOIN brc20_deploys AS d ON d.id = b.brc20_deploy_id - INNER JOIN locations AS l ON l.id = b.location_id + FROM brc20_operations AS b + INNER JOIN brc20_tokens AS d ON d.ticker = b.ticker WHERE b.address = ${args.address} - AND l.block_height <= ${args.block_height} - ${ticker ? this.sql`AND brc20_deploy_id IN (SELECT id FROM token_ids)` : this.sql``} + AND b.block_height <= ${args.block_height} + ${ticker ? this.sql`AND ${ticker}` : this.sql``} GROUP BY d.ticker, d.decimals HAVING SUM(b.avail_balance + b.trans_balance) > 0 ` : this.sql` SELECT d.ticker, d.decimals, b.avail_balance, b.trans_balance, b.total_balance, COUNT(*) OVER() as total FROM brc20_total_balances AS b - INNER JOIN brc20_deploys AS d ON d.id = b.brc20_deploy_id + INNER JOIN brc20_tokens AS d ON d.ticker = b.ticker WHERE b.total_balance > 0 AND b.address = ${args.address} - ${ticker ? this.sql`AND brc20_deploy_id IN (SELECT id FROM token_ids)` : this.sql``} + ${ticker ? this.sql`AND ${ticker}` : this.sql``} ` } LIMIT ${args.limit} @@ -699,18 +395,17 @@ export class Brc20PgStore extends BasePgStoreModule { const result = await this.sql` WITH token AS ( SELECT - ${this.sql(BRC20_DEPLOYS_COLUMNS.map(c => `d.${c}`))}, - i.number, i.genesis_id, l.timestamp - FROM brc20_deploys AS d - INNER JOIN inscriptions AS i ON i.id = d.inscription_id - INNER JOIN genesis_locations AS g ON g.inscription_id = d.inscription_id + d.*, i.number, i.genesis_id, l.timestamp + FROM brc20_tokens AS d + INNER JOIN inscriptions AS i ON i.genesis_id = d.genesis_id + INNER JOIN genesis_locations AS g ON g.inscription_id = i.id INNER JOIN locations AS l ON l.id = g.location_id - WHERE ticker_lower = LOWER(${args.ticker}) + WHERE d.ticker = LOWER(${args.ticker}) ), holders AS ( SELECT COUNT(*) AS count FROM brc20_total_balances - WHERE brc20_deploy_id = (SELECT id FROM token) AND total_balance > 0 + WHERE ticker = (SELECT ticker FROM token) AND total_balance > 0 ) SELECT *, COALESCE((SELECT count FROM holders), 0) AS holders FROM token @@ -725,15 +420,16 @@ export class Brc20PgStore extends BasePgStoreModule { ): Promise | undefined> { return await this.sqlTransaction(async sql => { const token = await sql<{ id: string; decimals: number }[]>` - SELECT id, decimals FROM brc20_deploys WHERE ticker_lower = LOWER(${args.ticker}) + SELECT ticker FROM brc20_tokens WHERE ticker = LOWER(${args.ticker}) `; if (token.count === 0) return; const results = await sql<(DbBrc20Holder & { total: number })[]>` SELECT - address, ${token[0].decimals}::int AS decimals, total_balance, COUNT(*) OVER() AS total - FROM brc20_total_balances - WHERE brc20_deploy_id = ${token[0].id} - ORDER BY total_balance DESC + b.address, d.decimals, b.total_balance, COUNT(*) OVER() AS total + FROM brc20_total_balances AS b + INNER JOIN brc20_tokens AS d USING (ticker) + WHERE b.ticker = LOWER(${args.ticker}) + ORDER BY b.total_balance DESC LIMIT ${args.limit} OFFSET ${args.offset} `; @@ -767,84 +463,71 @@ export class Brc20PgStore extends BasePgStoreModule { filters.address != undefined && filters.address != ''); const needsTickerCount = filterLength === 1 && filters.ticker && filters.ticker.length > 0; - - // Which operations do we need if we're filtering by address? - const sanitizedOperations: DbBrc20EventOperation[] = []; - for (const i of filters.operation ?? BRC20_OPERATIONS) - if (BRC20_OPERATIONS.includes(i)) sanitizedOperations?.push(i as DbBrc20EventOperation); - - // Which tickers are we filtering for? - const tickerConditions = this.sqlOr( - filters.ticker?.map(t => this.sql`ticker_lower = LOWER(${t})`) - ); + const operationsFilter = filters.operation?.filter(i => i !== 'transfer_receive'); return this.sqlTransaction(async sql => { - // The postgres query planner has trouble selecting an optimal plan when the WHERE condition - // checks any column from the `brc20_deploys` table. If the user is filtering by ticker, we - // should get the token IDs first and use those to filter directly in the `brc20_events` - // table. - const tickerIds = tickerConditions - ? (await sql<{ id: string }[]>`SELECT id FROM brc20_deploys WHERE ${tickerConditions}`).map( - i => i.id - ) - : undefined; const results = await sql<(DbBrc20Activity & { total: number })[]>` WITH event_count AS (${ - // Select count from the correct count cache table. needsGlobalEventCount ? sql` SELECT COALESCE(SUM(count), 0) AS count - FROM brc20_counts_by_event_type - ${filters.operation ? sql`WHERE event_type IN ${sql(filters.operation)}` : sql``} + FROM brc20_counts_by_operation + ${operationsFilter ? sql`WHERE operation IN ${sql(operationsFilter)}` : sql``} ` : needsAddressEventCount ? sql` - SELECT COALESCE(${sql.unsafe(sanitizedOperations.join('+'))}, 0) AS count - FROM brc20_counts_by_address_event_type + SELECT SUM(count) AS count + FROM brc20_counts_by_address_operation WHERE address = ${filters.address} + ${operationsFilter ? sql`AND operation IN ${sql(operationsFilter)}` : sql``} ` - : needsTickerCount && tickerIds !== undefined + : needsTickerCount && filters.ticker !== undefined ? sql` SELECT COALESCE(SUM(tx_count), 0) AS count - FROM brc20_deploys AS d - WHERE id IN ${sql(tickerIds)} + FROM brc20_tokens AS d + WHERE ticker IN ${sql(filters.ticker)} ` : sql`SELECT NULL AS count` }) SELECT e.operation, + e.avail_balance, + e.trans_balance, + e.address, + e.to_address, d.ticker, l.genesis_id AS inscription_id, l.block_height, l.block_hash, l.tx_id, - l.address, l.timestamp, l.output, l.offset, d.max AS deploy_max, d.limit AS deploy_limit, d.decimals AS deploy_decimals, - (SELECT amount FROM brc20_mints WHERE id = e.mint_id) AS mint_amount, - (SELECT amount || ';' || from_address || ';' || COALESCE(to_address, '') FROM brc20_transfers WHERE id = e.transfer_id) AS transfer_data, ${ needsGlobalEventCount || needsAddressEventCount || needsTickerCount ? sql`(SELECT count FROM event_count)` : sql`COUNT(*) OVER()` } AS total - FROM brc20_events AS e - INNER JOIN brc20_deploys AS d ON e.brc20_deploy_id = d.id - INNER JOIN locations AS l ON e.genesis_location_id = l.id + FROM brc20_operations AS e + INNER JOIN brc20_tokens AS d ON d.ticker = e.ticker + INNER JOIN locations AS l ON e.genesis_id = l.genesis_id AND e.block_height = l.block_height AND e.tx_index = l.tx_index WHERE TRUE - ${filters.operation ? sql`AND e.operation IN ${sql(filters.operation)}` : sql``} - ${tickerIds ? sql`AND e.brc20_deploy_id IN ${sql(tickerIds)}` : sql``} + ${ + operationsFilter + ? sql`AND e.operation IN ${sql(operationsFilter)}` + : sql`AND e.operation <> 'transfer_receive'` + } + ${filters.ticker ? sql`AND e.ticker IN ${sql(filters.ticker)}` : sql``} ${filters.block_height ? sql`AND l.block_height = ${filters.block_height}` : sql``} ${ filters.address - ? sql`AND (e.address = ${filters.address} OR e.from_address = ${filters.address})` + ? sql`AND (e.address = ${filters.address} OR e.to_address = ${filters.address})` : sql`` } - ORDER BY l.block_height DESC, l.tx_index DESC + ORDER BY e.block_height DESC, e.tx_index DESC LIMIT ${page.limit} OFFSET ${page.offset} `; diff --git a/src/pg/brc20/helpers.ts b/src/pg/brc20/helpers.ts index 6aec1697..1d04e767 100644 --- a/src/pg/brc20/helpers.ts +++ b/src/pg/brc20/helpers.ts @@ -1,96 +1,98 @@ -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 { DbLocationTransferType, InscriptionRevealData } from '../types'; +import { DbBrc20Operation, DbBrc20OperationInsert, DbBrc20TokenInsert } from './types'; +import * as postgres from 'postgres'; +import { PgSqlClient } from '@hirosystems/api-toolkit'; -const Brc20TickerSchema = Type.String({ minLength: 1 }); -const Brc20NumberSchema = Type.RegEx(/^((\d+)|(\d*\.?\d+))$/); +export function sqlOr( + sql: PgSqlClient, + partials: postgres.PendingQuery[] | undefined +) { + return partials?.reduce((acc, curr) => sql`${acc} OR ${curr}`); +} -const Brc20DeploySchema = Type.Object( - { - p: Type.Literal('brc-20'), - op: Type.Literal('deploy'), - tick: Brc20TickerSchema, - max: Brc20NumberSchema, - lim: Type.Optional(Brc20NumberSchema), - dec: Type.Optional(Type.RegEx(/^\d+$/)), - self_mint: Type.Optional(Type.Literal('true')), - }, - { additionalProperties: true } -); -export type Brc20Deploy = Static; +export interface AddressBalanceData { + avail: BigNumber; + trans: BigNumber; + total: BigNumber; +} -const Brc20MintSchema = Type.Object( - { - p: Type.Literal('brc-20'), - op: Type.Literal('mint'), - tick: Brc20TickerSchema, - amt: Brc20NumberSchema, - }, - { additionalProperties: true } -); -export type Brc20Mint = Static; +export class Brc20BlockCache { + tokens: DbBrc20TokenInsert[] = []; + operations: DbBrc20OperationInsert[] = []; + tokenMintSupplies = new Map(); + tokenTxCounts = new Map(); + operationCounts = new Map(); + addressOperationCounts = new Map>(); + totalBalanceChanges = new Map>(); + transferReceivers = new Map(); -const Brc20TransferSchema = Type.Object( - { - p: Type.Literal('brc-20'), - op: Type.Literal('transfer'), - tick: Brc20TickerSchema, - amt: Brc20NumberSchema, - }, - { additionalProperties: true } -); -export type Brc20Transfer = Static; + increaseOperationCount(operation: DbBrc20Operation) { + this.increaseOperationCountInternal(this.operationCounts, operation); + } + private increaseOperationCountInternal( + map: Map, + operation: DbBrc20Operation + ) { + const current = map.get(operation); + if (current == undefined) { + map.set(operation, 1); + } else { + map.set(operation, current + 1); + } + } -const Brc20Schema = Type.Union([Brc20DeploySchema, Brc20MintSchema, Brc20TransferSchema]); -const Brc20C = TypeCompiler.Compile(Brc20Schema); -export type Brc20 = Static; + increaseTokenMintedSupply(ticker: string, amount: BigNumber) { + const current = this.tokenMintSupplies.get(ticker); + if (current == undefined) { + this.tokenMintSupplies.set(ticker, amount); + } else { + this.tokenMintSupplies.set(ticker, current.plus(amount)); + } + } -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); + increaseTokenTxCount(ticker: string) { + const current = this.tokenTxCounts.get(ticker); + if (current == undefined) { + this.tokenTxCounts.set(ticker, 1); + } else { + this.tokenTxCounts.set(ticker, current + 1); + } + } -/** - * 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; + increaseAddressOperationCount(address: string, operation: DbBrc20Operation) { + const current = this.addressOperationCounts.get(address); + if (current == undefined) { + const opMap = new Map(); + this.increaseOperationCountInternal(opMap, operation); + this.addressOperationCounts.set(address, opMap); + } else { + this.increaseOperationCountInternal(current, operation); + } + } -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(hexToBuffer(reveal.inscription.content as string).toString('utf-8')); - if (Brc20C.Check(json)) { - // Check ticker byte length - 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 && 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; + updateAddressBalance( + ticker: string, + address: string, + availBalance: BigNumber, + transBalance: BigNumber, + totalBalance: BigNumber + ) { + const current = this.totalBalanceChanges.get(address); + if (current === undefined) { + const opMap = new Map(); + opMap.set(ticker, { avail: availBalance, trans: transBalance, total: totalBalance }); + this.totalBalanceChanges.set(address, opMap); + } else { + const currentTick = current.get(ticker); + if (currentTick === undefined) { + current.set(ticker, { avail: availBalance, trans: transBalance, total: totalBalance }); } else { - if (parseFloat(json.amt) == 0 || numExceedsMax(json.amt)) return; + current.set(ticker, { + avail: availBalance.plus(currentTick.avail), + trans: transBalance.plus(currentTick.trans), + total: totalBalance.plus(currentTick.total), + }); } - return json; } - } catch (error) { - // Not a BRC-20 inscription. } } diff --git a/src/pg/brc20/types.ts b/src/pg/brc20/types.ts index 5b28258a..9737dfb6 100644 --- a/src/pg/brc20/types.ts +++ b/src/pg/brc20/types.ts @@ -1,68 +1,40 @@ -import { DbLocationTransferType } from '../types'; +import { PgNumeric } from '@hirosystems/api-toolkit'; -export type DbBrc20Location = { - id: string; - inscription_id: string | null; - block_height: string; - tx_id: string; - tx_index: number; - address: string | null; - transfer_type: DbLocationTransferType; -}; - -export type DbBrc20DeployInsert = { - inscription_id: string; +export type DbBrc20TokenInsert = { + ticker: string; + genesis_id: string; block_height: string; tx_id: string; address: string; - ticker: string; - max: string; - decimals: string; - limit: string | null; - tx_count: number; + max: PgNumeric; + limit: PgNumeric; + decimals: PgNumeric; self_mint: boolean; }; -export type DbBrc20MintInsert = { - inscription_id: string; - brc20_deploy_id: string; - block_height: string; - tx_id: string; - address: string; - amount: string; -}; +export enum DbBrc20Operation { + deploy = 'deploy', + mint = 'mint', + transfer = 'transfer', + transferSend = 'transfer_send', + transferReceive = 'transfer_receive', +} -export type DbBrc20Deploy = { - id: string; - inscription_id: string; - block_height: string; - tx_id: string; - address: string; +export type DbBrc20OperationInsert = { + genesis_id: string; ticker: string; - max: string; - decimals: string; - limit?: string; -}; - -export type DbBrc20TransferInsert = { - inscription_id: string; - brc20_deploy_id: string; - block_height: string; - tx_id: string; - from_address: string; - to_address: string | null; - amount: string; + block_height: PgNumeric; + tx_index: PgNumeric; + address: string; + avail_balance: PgNumeric; + trans_balance: PgNumeric; + operation: DbBrc20Operation; }; -export type DbBrc20Transfer = { - id: string; - inscription_id: string; - brc20_deploy_id: string; - block_height: string; - tx_id: string; - from_address: string; - to_address?: string; - amount: string; +export type DbBrc20CountsByAddressInsert = { + address: string; + operation: DbBrc20Operation; + count: number; }; export type DbBrc20Token = { @@ -145,8 +117,10 @@ export type DbBrc20TransferEvent = BaseEvent & { export type DbBrc20Event = DbBrc20DeployEvent | DbBrc20MintEvent | DbBrc20TransferEvent; -type BaseActivity = { +export type DbBrc20Activity = { ticker: string; + avail_balance: string; + trans_balance: string; deploy_decimals: number; deploy_max: string; deploy_limit: string | null; @@ -159,25 +133,10 @@ type BaseActivity = { block_hash: string; tx_id: string; address: string; + to_address: string | null; timestamp: number; }; -export type DbBrc20DeployActivity = BaseActivity & { - operation: DbBrc20EventOperation.deploy; -}; - -export type DbBrc20MintActivity = BaseActivity & { - operation: DbBrc20EventOperation.mint; - mint_amount: string; -}; - -export type DbBrc20TransferActivity = BaseActivity & { - operation: DbBrc20EventOperation.transfer | DbBrc20EventOperation.transferSend; - transfer_data: string; -}; - -export type DbBrc20Activity = DbBrc20DeployActivity | DbBrc20MintActivity | DbBrc20TransferActivity; - export const BRC20_DEPLOYS_COLUMNS = [ 'id', 'inscription_id', @@ -192,14 +151,3 @@ export const BRC20_DEPLOYS_COLUMNS = [ 'tx_count', 'self_mint', ]; - -export const BRC20_TRANSFERS_COLUMNS = [ - 'id', - 'inscription_id', - 'brc20_deploy_id', - 'block_height', - 'tx_id', - 'from_address', - 'to_address', - 'amount', -]; diff --git a/src/pg/pg-store.ts b/src/pg/pg-store.ts index f08df0c2..433cf3b3 100644 --- a/src/pg/pg-store.ts +++ b/src/pg/pg-store.ts @@ -40,7 +40,7 @@ import { export const MIGRATIONS_DIR = path.join(__dirname, '../../migrations'); export const ORDINALS_GENESIS_BLOCK = 767430; -const INSERT_BATCH_SIZE = 4000; +export const INSERT_BATCH_SIZE = 4000; type InscriptionIdentifier = { genesis_id: string } | { number: number }; @@ -92,6 +92,7 @@ export class PgStore extends BasePgStore { logger.info(`PgStore rolling back block ${event.block_identifier.index}`); const time = stopwatch(); const rollbacks = revealInsertsFromOrdhookEvent(event); + await this.brc20.updateBrc20Operations(event, 'rollback'); for (const writeChunk of batchIterate(rollbacks, 1000)) await this.rollBackInscriptions(writeChunk); updatedBlockHeightMin = Math.min(updatedBlockHeightMin, event.block_identifier.index); @@ -125,6 +126,7 @@ export class PgStore extends BasePgStore { 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); + await this.brc20.updateBrc20Operations(event, 'apply'); logger.info( `PgStore ingested block ${event.block_identifier.index} in ${time.getElapsedSeconds()}s` ); @@ -574,11 +576,9 @@ export class PgStore extends BasePgStore { logger.info(`PgStore ${action} at block ${reveal.location.block_height}`); } - // 3. Recursions, Counts and BRC-20 + // 3. Recursions and counts await this.updateInscriptionRecursions(reveals); await this.counts.applyInscriptions(inscriptionInserts); - if (ENV.BRC20_BLOCK_SCAN_ENABLED) - await this.brc20.insertOperations({ reveals: revealOutputs, pointers }); }); } @@ -628,7 +628,6 @@ export class PgStore extends BasePgStore { // Roll back events in reverse so BRC-20 keeps a sane order. for (const rollback of rollbacks.reverse()) { if ('inscription' in rollback) { - await this.brc20.rollBackInscription({ inscription: rollback.inscription }); await this.counts.rollBackInscription({ inscription: rollback.inscription, location: rollback.location, @@ -638,7 +637,6 @@ export class PgStore extends BasePgStore { `PgStore rollback reveal #${rollback.inscription.number} (${rollback.inscription.genesis_id}) at block ${rollback.location.block_height}` ); } else { - await this.brc20.rollBackLocation({ location: rollback.location }); await this.recalculateCurrentLocationPointerFromLocationRollBack({ location: rollback.location, }); diff --git a/tests/brc-20/api.test.ts b/tests/brc-20/api.test.ts new file mode 100644 index 00000000..642609eb --- /dev/null +++ b/tests/brc-20/api.test.ts @@ -0,0 +1,1394 @@ +import { runMigrations } from '@hirosystems/api-toolkit'; +import { buildApiServer } from '../../src/api/init'; +import { Brc20ActivityResponse, Brc20TokenResponse } from '../../src/api/schemas'; +import { MIGRATIONS_DIR, PgStore } from '../../src/pg/pg-store'; +import { + BRC20_GENESIS_BLOCK, + TestChainhookPayloadBuilder, + TestFastifyServer, + deployAndMintPEPE, + incrementing, + randomHash, +} from '../helpers'; + +describe('BRC-20 API', () => { + let db: PgStore; + let fastify: TestFastifyServer; + + beforeEach(async () => { + await runMigrations(MIGRATIONS_DIR, 'up'); + db = await PgStore.connect({ skipMigrations: true }); + fastify = await buildApiServer({ db }); + }); + + afterEach(async () => { + await fastify.close(); + await db.close(); + await runMigrations(MIGRATIONS_DIR, 'down'); + }); + + describe('/brc-20/tokens', () => { + test('tokens endpoint', async () => { + await db.updateInscriptions( + new TestChainhookPayloadBuilder() + .apply() + .block({ height: BRC20_GENESIS_BLOCK }) + .transaction({ + hash: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', + }) + .brc20( + { + deploy: { + inscription_id: + '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', + tick: 'pepe', + max: '21000000', + lim: '21000000', + dec: '18', + address: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td', + self_mint: false, + }, + }, + { inscription_number: 0 } + ) + .build() + ); + const response = await fastify.inject({ + method: 'GET', + url: `/ordinals/brc-20/tokens/pepe`, + }); + expect(response.statusCode).toBe(200); + expect(response.json()).toStrictEqual({ + token: { + id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', + number: 0, + block_height: BRC20_GENESIS_BLOCK, + tx_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', + address: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td', + ticker: 'pepe', + max_supply: '21000000.000000000000000000', + mint_limit: '21000000.000000000000000000', + decimals: 18, + deploy_timestamp: 1677803510000, + minted_supply: '0.000000000000000000', + tx_count: 1, + self_mint: false, + }, + supply: { + max_supply: '21000000.000000000000000000', + minted_supply: '0.000000000000000000', + holders: 0, + }, + }); + }); + + test('tokens filter by ticker prefix', async () => { + const inscriptionNumbers = incrementing(0); + const blockHeights = incrementing(BRC20_GENESIS_BLOCK); + + let transferHash = randomHash(); + let number = inscriptionNumbers.next().value; + await db.updateInscriptions( + new TestChainhookPayloadBuilder() + .apply() + .block({ height: blockHeights.next().value }) + .transaction({ hash: transferHash }) + .brc20( + { + deploy: { + inscription_id: `${transferHash}i0`, + tick: 'pepe', + max: '21000000', + lim: '21000000', + dec: '18', + address: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td', + self_mint: false, + }, + }, + { inscription_number: 0 } + ) + .build() + ); + + transferHash = randomHash(); + number = inscriptionNumbers.next().value; + await db.updateInscriptions( + new TestChainhookPayloadBuilder() + .apply() + .block({ height: blockHeights.next().value }) + .transaction({ hash: transferHash }) + .brc20( + { + deploy: { + inscription_id: `${transferHash}i0`, + tick: 'peer', + max: '21000000', + lim: '21000000', + dec: '18', + address: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td', + self_mint: false, + }, + }, + { inscription_number: 1 } + ) + .build() + ); + + transferHash = randomHash(); + number = inscriptionNumbers.next().value; + await db.updateInscriptions( + new TestChainhookPayloadBuilder() + .apply() + .block({ height: blockHeights.next().value }) + .transaction({ hash: transferHash }) + .brc20( + { + deploy: { + inscription_id: `${transferHash}i0`, + tick: 'abcd', + max: '21000000', + lim: '21000000', + dec: '18', + address: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td', + self_mint: false, + }, + }, + { inscription_number: 2 } + ) + .build() + ); + + transferHash = randomHash(); + number = inscriptionNumbers.next().value; + await db.updateInscriptions( + new TestChainhookPayloadBuilder() + .apply() + .block({ height: blockHeights.next().value }) + .transaction({ hash: transferHash }) + .brc20( + { + deploy: { + inscription_id: `${transferHash}i0`, + tick: 'dcba', + max: '21000000', + lim: '21000000', + dec: '18', + address: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td', + self_mint: false, + }, + }, + { inscription_number: 3 } + ) + .build() + ); + const response = await fastify.inject({ + method: 'GET', + url: `/ordinals/brc-20/tokens?ticker=PE&ticker=AB`, + }); + expect(response.statusCode).toBe(200); + const responseJson = response.json(); + expect(responseJson.total).toBe(3); + expect(responseJson.results).toEqual( + expect.arrayContaining([ + expect.objectContaining({ ticker: 'pepe' }), + expect.objectContaining({ ticker: 'peer' }), + expect.objectContaining({ ticker: 'abcd' }), + ]) + ); + }); + + test('tokens using order_by tx_count', async () => { + // Setup + const inscriptionNumbers = incrementing(0); + const blockHeights = incrementing(BRC20_GENESIS_BLOCK); + const addressA = 'bc1q6uwuet65rm6xvlz7ztw2gvdmmay5uaycu03mqz'; + const addressB = 'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4'; + + // A deploys pepe + let number = inscriptionNumbers.next().value; + await db.updateInscriptions( + new TestChainhookPayloadBuilder() + .apply() + .block({ height: blockHeights.next().value }) + .transaction({ hash: randomHash() }) + .brc20( + { + deploy: { + inscription_id: `${randomHash()}i0`, + tick: 'pepe', + max: '21000000', + lim: '21000000', + dec: '18', + address: addressA, + self_mint: false, + }, + }, + { inscription_number: 0 } + ) + .build() + ); + + // A mints 10000 pepe 10 times (will later be rolled back) + const pepeMints = []; + for (let i = 0; i < 10; i++) { + const txHash = randomHash(); + number = inscriptionNumbers.next().value; + const payload = new TestChainhookPayloadBuilder() + .apply() + .block({ height: blockHeights.next().value }) + .transaction({ hash: txHash }) + .brc20( + { + mint: { + inscription_id: `${txHash}i0`, + tick: 'pepe', + address: addressA, + amt: '10000', + }, + }, + { inscription_number: i + 1 } + ) + .build(); + pepeMints.push(payload); + await db.updateInscriptions(payload); + } + + // B deploys abcd + number = inscriptionNumbers.next().value; + await db.updateInscriptions( + new TestChainhookPayloadBuilder() + .apply() + .block({ height: blockHeights.next().value }) + .transaction({ hash: randomHash() }) + .brc20( + { + deploy: { + inscription_id: `${randomHash()}i0`, + tick: 'abcd', + max: '21000000', + lim: '21000000', + dec: '18', + address: addressB, + self_mint: false, + }, + }, + { inscription_number: 11 } + ) + .build() + ); + + // B mints 10000 abcd + number = inscriptionNumbers.next().value; + await db.updateInscriptions( + new TestChainhookPayloadBuilder() + .apply() + .block({ height: blockHeights.next().value }) + .transaction({ hash: randomHash() }) + .brc20( + { + mint: { + inscription_id: `${randomHash()}i0`, + tick: 'abcd', + address: addressA, + amt: '10000', + }, + }, + { inscription_number: 12 } + ) + .build() + ); + + // B send 1000 abcd to A + // (create inscription, transfer) + const txHashTransfer = randomHash(); + number = inscriptionNumbers.next().value; + const payloadTransfer = new TestChainhookPayloadBuilder() + .apply() + .block({ height: blockHeights.next().value }) + .transaction({ hash: txHashTransfer }) + .brc20( + { + transfer: { + inscription_id: `${txHashTransfer}i0`, + tick: 'abcd', + address: addressB, + amt: '1000', + }, + }, + { inscription_number: 13 } + ) + .build(); + await db.updateInscriptions(payloadTransfer); + // (send inscription, transfer_send) + const txHashTransferSend = randomHash(); + const payloadTransferSend = new TestChainhookPayloadBuilder() + .apply() + .block({ height: blockHeights.next().value }) + .transaction({ hash: txHashTransferSend }) + .brc20( + { + transfer_send: { + tick: 'abcd', + inscription_id: `${txHashTransfer}i0`, + amt: '1000', + sender_address: addressB, + receiver_address: addressA, + }, + }, + { inscription_number: 13 } + ) + .build(); + await db.updateInscriptions(payloadTransferSend); + + let response = await fastify.inject({ + method: 'GET', + url: `/ordinals/brc-20/tokens`, + }); + expect(response.statusCode).toBe(200); + let json = response.json(); + expect(json.total).toBe(2); + expect(json.results).toHaveLength(2); + + // WITHOUT tx_count sort: + expect(json.results).toEqual([ + // The first result is the token with the latest activity (abcd) + expect.objectContaining({ + ticker: 'abcd', + tx_count: 4, + } as Brc20TokenResponse), + expect.objectContaining({ + ticker: 'pepe', + tx_count: 11, + } as Brc20TokenResponse), + ]); + + response = await fastify.inject({ + method: 'GET', + url: `/ordinals/brc-20/tokens?order_by=tx_count`, + }); + expect(response.statusCode).toBe(200); + json = response.json(); + expect(json.total).toBe(2); + expect(json.results).toHaveLength(2); + + // WITH tx_count sort: The first result is the most active token (pepe) + expect(json.results).toEqual([ + expect.objectContaining({ + ticker: 'pepe', + tx_count: 11, + } as Brc20TokenResponse), + expect.objectContaining({ + ticker: 'abcd', + tx_count: 4, + } as Brc20TokenResponse), + ]); + + // Rollback pepe mints + for (const payload of pepeMints) { + const payloadRollback = { ...payload, apply: [], rollback: payload.apply }; + await db.updateInscriptions(payloadRollback); + } + + // WITH tx_count sort: The first result is the most active token (now abcd) + response = await fastify.inject({ + method: 'GET', + url: `/ordinals/brc-20/tokens?order_by=tx_count`, + }); + expect(response.statusCode).toBe(200); + json = response.json(); + expect(json.total).toBe(2); + expect(json.results).toHaveLength(2); + expect(json.results).toEqual([ + expect.objectContaining({ + ticker: 'abcd', + tx_count: 4, + } as Brc20TokenResponse), + expect.objectContaining({ + ticker: 'pepe', + tx_count: 1, // only the deploy remains + } as Brc20TokenResponse), + ]); + + // Rollback abcd transfer + await db.updateInscriptions({ + ...payloadTransferSend, + apply: [], + rollback: payloadTransferSend.apply, + }); + await db.updateInscriptions({ + ...payloadTransfer, + apply: [], + rollback: payloadTransfer.apply, + }); + + response = await fastify.inject({ + method: 'GET', + url: `/ordinals/brc-20/tokens?order_by=tx_count`, + }); + expect(response.statusCode).toBe(200); + json = response.json(); + expect(json.total).toBe(2); + expect(json.results).toHaveLength(2); + expect(json.results).toEqual([ + expect.objectContaining({ + ticker: 'abcd', + tx_count: 2, // only the deploy and mint remain + } as Brc20TokenResponse), + expect.objectContaining({ + ticker: 'pepe', + tx_count: 1, + } as Brc20TokenResponse), + ]); + }); + }); + + describe('/brc-20/activity', () => { + test('activity for token transfers', async () => { + // Setup + const inscriptionNumbers = incrementing(0); + const blockHeights = incrementing(BRC20_GENESIS_BLOCK); + const addressA = 'bc1q6uwuet65rm6xvlz7ztw2gvdmmay5uaycu03mqz'; + const addressB = 'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4'; + + // A deploys pepe + let number = inscriptionNumbers.next().value; + await db.updateInscriptions( + new TestChainhookPayloadBuilder() + .apply() + .block({ height: blockHeights.next().value }) + .transaction({ hash: randomHash() }) + .brc20( + { + deploy: { + inscription_id: `${randomHash()}i0`, + tick: 'pepe', + max: '21000000', + lim: '21000000', + dec: '18', + address: addressA, + self_mint: false, + }, + }, + { inscription_number: 0 } + ) + .build() + ); + + // Verify that the pepe deploy is in the activity feed + let response = await fastify.inject({ + method: 'GET', + url: `/ordinals/brc-20/activity?ticker=pepe`, + }); + expect(response.statusCode).toBe(200); + let json = response.json(); + expect(json.total).toBe(1); + expect(json.results).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + operation: 'deploy', + ticker: 'pepe', + address: addressA, + deploy: expect.objectContaining({ + max_supply: '21000000.000000000000000000', + }), + } as Brc20ActivityResponse), + ]) + ); + + // A mints 10000 pepe + number = inscriptionNumbers.next().value; + await db.updateInscriptions( + new TestChainhookPayloadBuilder() + .apply() + .block({ height: blockHeights.next().value }) + .transaction({ hash: randomHash() }) + .brc20( + { + mint: { + inscription_id: `${randomHash()}i0`, + tick: 'pepe', + address: addressA, + amt: '10000', + }, + }, + { inscription_number: 1 } + ) + .build() + ); + + response = await fastify.inject({ + method: 'GET', + url: `/ordinals/brc-20/activity?ticker=pepe`, + }); + expect(response.statusCode).toBe(200); + json = response.json(); + expect(json.total).toBe(2); + expect(json.results).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + operation: 'deploy', + ticker: 'pepe', + } as Brc20ActivityResponse), + expect.objectContaining({ + operation: 'mint', + ticker: 'pepe', + address: addressA, + mint: { + amount: '10000.000000000000000000', + }, + } as Brc20ActivityResponse), + ]) + ); + + // B mints 10000 pepe + number = inscriptionNumbers.next().value; + await db.updateInscriptions( + new TestChainhookPayloadBuilder() + .apply() + .block({ height: blockHeights.next().value }) + .transaction({ hash: randomHash() }) + .brc20( + { + mint: { + inscription_id: `${randomHash()}i0`, + tick: 'pepe', + address: addressB, + amt: '10000', + }, + }, + { inscription_number: 2 } + ) + .build() + ); + + response = await fastify.inject({ + method: 'GET', + url: `/ordinals/brc-20/activity?ticker=pepe`, + }); + expect(response.statusCode).toBe(200); + json = response.json(); + expect(json.total).toBe(3); + expect(json.results).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + operation: 'mint', + ticker: 'pepe', + address: addressB, + mint: { + amount: '10000.000000000000000000', + }, + } as Brc20ActivityResponse), + ]) + ); + + // A creates transfer of 9000 pepe + const transferHash = randomHash(); + number = inscriptionNumbers.next().value; + await db.updateInscriptions( + new TestChainhookPayloadBuilder() + .apply() + .block({ height: blockHeights.next().value }) + .transaction({ hash: transferHash }) + .brc20( + { + transfer: { + inscription_id: `${transferHash}i0`, + tick: 'pepe', + address: addressA, + amt: '9000', + }, + }, + { inscription_number: 3 } + ) + .build() + ); + + response = await fastify.inject({ + method: 'GET', + url: `/ordinals/brc-20/activity?ticker=pepe`, + }); + expect(response.statusCode).toBe(200); + json = response.json(); + expect(json.total).toBe(4); + expect(json.results).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + operation: 'transfer', + ticker: 'pepe', + address: addressA, + tx_id: transferHash, + transfer: { + amount: '9000.000000000000000000', + from_address: addressA, + }, + } as Brc20ActivityResponse), + ]) + ); + + // A sends transfer inscription to B (aka transfer/sale) + await db.updateInscriptions( + new TestChainhookPayloadBuilder() + .apply() + .block({ height: blockHeights.next().value }) + .transaction({ hash: randomHash() }) + .brc20( + { + transfer_send: { + tick: 'pepe', + inscription_id: `${transferHash}i0`, + amt: '9000', + sender_address: addressA, + receiver_address: addressB, + }, + }, + { inscription_number: 3 } + ) + .build() + ); + + response = await fastify.inject({ + method: 'GET', + url: `/ordinals/brc-20/activity?ticker=pepe`, + }); + expect(response.statusCode).toBe(200); + json = response.json(); + expect(json.total).toBe(5); + expect(json.results).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + operation: 'transfer_send', + ticker: 'pepe', + tx_id: expect.not.stringMatching(transferHash), + address: addressB, + transfer_send: { + amount: '9000.000000000000000000', + from_address: addressA, + to_address: addressB, + }, + } as Brc20ActivityResponse), + ]) + ); + + response = await fastify.inject({ + method: 'GET', + url: `/ordinals/brc-20/activity?ticker=pepe&operation=transfer_send`, + }); + expect(response.statusCode).toBe(200); + json = response.json(); + expect(json.total).toBe(1); + expect(json.results).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + operation: 'transfer_send', + ticker: 'pepe', + tx_id: expect.not.stringMatching(transferHash), + address: addressB, + transfer_send: { + amount: '9000.000000000000000000', + from_address: addressA, + to_address: addressB, + }, + } as Brc20ActivityResponse), + ]) + ); + }); + + test('activity for multiple token transfers among three participants', async () => { + // Step 1: A deploys a token + // Step 2: A mints 1000 of the token + // Step 3: B mints 2000 of the token + // Step 4: A creates a transfer to B + // Step 5: B creates a transfer to C + // Step 6: A transfer_send the transfer to B + // Step 7: B transfer_send the transfer to C + + // Setup + const inscriptionNumbers = incrementing(0); + const blockHeights = incrementing(BRC20_GENESIS_BLOCK); + const addressA = 'bc1q6uwuet65rm6xvlz7ztw2gvdmmay5uaycu03mqz'; + const addressB = 'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4'; + const addressC = 'bc1q9d80h0q5d3f54w7w8c3l2sguf9uset4ydw9xj2'; + + // Step 1: A deploys a token + let number = inscriptionNumbers.next().value; + await db.updateInscriptions( + new TestChainhookPayloadBuilder() + .apply() + .block({ height: blockHeights.next().value }) + .transaction({ hash: randomHash() }) + .brc20( + { + deploy: { + inscription_id: `${randomHash()}i0`, + tick: 'pepe', + max: '21000000', + lim: '21000000', + dec: '18', + address: addressA, + self_mint: false, + }, + }, + { inscription_number: number } + ) + .build() + ); + + // Verify that the pepe deploy is in the activity feed + let response = await fastify.inject({ + method: 'GET', + url: `/ordinals/brc-20/activity?ticker=pepe`, + }); + expect(response.statusCode).toBe(200); + let json = response.json(); + expect(json.total).toBe(1); + expect(json.results).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + operation: 'deploy', + ticker: 'pepe', + address: addressA, + deploy: expect.objectContaining({ + max_supply: '21000000.000000000000000000', + }), + } as Brc20ActivityResponse), + ]) + ); + + // Step 2: A mints 1000 of the token + number = inscriptionNumbers.next().value; + await db.updateInscriptions( + new TestChainhookPayloadBuilder() + .apply() + .block({ height: blockHeights.next().value }) + .transaction({ hash: randomHash() }) + .brc20( + { + mint: { + inscription_id: `${randomHash()}i0`, + tick: 'pepe', + address: addressA, + amt: '1000', + }, + }, + { inscription_number: number } + ) + .build() + ); + + // Verify that the pepe mint is in the activity feed + response = await fastify.inject({ + method: 'GET', + url: `/ordinals/brc-20/activity?ticker=pepe`, + }); + expect(response.statusCode).toBe(200); + json = response.json(); + expect(json.total).toBe(2); + expect(json.results).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + operation: 'mint', + ticker: 'pepe', + address: addressA, + mint: { + amount: '1000.000000000000000000', + }, + } as Brc20ActivityResponse), + ]) + ); + response = await fastify.inject({ + method: 'GET', + url: `/ordinals/brc-20/activity?ticker=pepe&address=${addressA}`, + }); + expect(response.statusCode).toBe(200); + json = response.json(); + expect(json.total).toBe(2); + expect(json.results).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + operation: 'deploy', + ticker: 'pepe', + address: addressA, + deploy: expect.objectContaining({ + max_supply: '21000000.000000000000000000', + }), + } as Brc20ActivityResponse), + expect.objectContaining({ + operation: 'mint', + ticker: 'pepe', + address: addressA, + mint: { + amount: '1000.000000000000000000', + }, + } as Brc20ActivityResponse), + ]) + ); + + // Step 3: B mints 2000 of the token + number = inscriptionNumbers.next().value; + await db.updateInscriptions( + new TestChainhookPayloadBuilder() + .apply() + .block({ height: blockHeights.next().value }) + .transaction({ hash: randomHash() }) + .brc20( + { + mint: { + inscription_id: `${randomHash()}i0`, + tick: 'pepe', + address: addressB, + amt: '2000', + }, + }, + { inscription_number: number } + ) + .build() + ); + + // Verify that the pepe mint is in the activity feed + response = await fastify.inject({ + method: 'GET', + url: `/ordinals/brc-20/activity?ticker=pepe`, + }); + expect(response.statusCode).toBe(200); + json = response.json(); + expect(json.total).toBe(3); + expect(json.results).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + operation: 'mint', + ticker: 'pepe', + address: addressB, + mint: { + amount: '2000.000000000000000000', + }, + } as Brc20ActivityResponse), + ]) + ); + + // Step 4: A creates a transfer to B + const transferHashAB = randomHash(); + const numberAB = inscriptionNumbers.next().value; + await db.updateInscriptions( + new TestChainhookPayloadBuilder() + .apply() + .block({ height: blockHeights.next().value }) + .transaction({ hash: transferHashAB }) + .brc20( + { + transfer: { + inscription_id: `${transferHashAB}i0`, + tick: 'pepe', + address: addressA, + amt: '1000', + }, + }, + { inscription_number: numberAB } + ) + .build() + ); + + // Verify that the pepe transfer is in the activity feed + response = await fastify.inject({ + method: 'GET', + url: `/ordinals/brc-20/activity?ticker=pepe`, + }); + expect(response.statusCode).toBe(200); + json = response.json(); + expect(json.total).toBe(4); + expect(json.results).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + operation: 'transfer', + ticker: 'pepe', + address: addressA, + tx_id: transferHashAB, + transfer: { + amount: '1000.000000000000000000', + from_address: addressA, + }, + } as Brc20ActivityResponse), + ]) + ); + response = await fastify.inject({ + method: 'GET', + url: `/ordinals/brc-20/activity?ticker=pepe&address=${addressA}`, + }); + expect(response.statusCode).toBe(200); + json = response.json(); + expect(json.total).toBe(3); + expect(json.results).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + operation: 'transfer', + ticker: 'pepe', + address: addressA, + tx_id: transferHashAB, + transfer: { + amount: '1000.000000000000000000', + from_address: addressA, + }, + } as Brc20ActivityResponse), + ]) + ); + + // Step 5: B creates a transfer to C + const transferHashBC = randomHash(); + const numberBC = inscriptionNumbers.next().value; + await db.updateInscriptions( + new TestChainhookPayloadBuilder() + .apply() + .block({ height: blockHeights.next().value }) + .transaction({ hash: transferHashBC }) + .brc20( + { + transfer: { + inscription_id: `${transferHashBC}i0`, + tick: 'pepe', + address: addressB, + amt: '2000', + }, + }, + { inscription_number: numberBC } + ) + .build() + ); + + // Verify that the pepe transfer is in the activity feed + response = await fastify.inject({ + method: 'GET', + url: `/ordinals/brc-20/activity?ticker=pepe`, + }); + expect(response.statusCode).toBe(200); + json = response.json(); + expect(json.total).toBe(5); + expect(json.results).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + operation: 'transfer', + ticker: 'pepe', + address: addressB, + tx_id: transferHashBC, + transfer: { + amount: '2000.000000000000000000', + from_address: addressB, + }, + } as Brc20ActivityResponse), + ]) + ); + + // Step 6: A transfer_send the transfer to B + const transferHashABSend = randomHash(); + await db.updateInscriptions( + new TestChainhookPayloadBuilder() + .apply() + .block({ height: blockHeights.next().value }) + .transaction({ hash: transferHashABSend }) + .brc20( + { + transfer_send: { + tick: 'pepe', + inscription_id: `${transferHashAB}i0`, + amt: '1000', + sender_address: addressA, + receiver_address: addressB, + }, + }, + { inscription_number: numberAB } + ) + .build() + ); + // A gets the transfer send in its feed + response = await fastify.inject({ + method: 'GET', + url: `/ordinals/brc-20/activity?ticker=pepe&address=${addressA}`, + }); + expect(response.statusCode).toBe(200); + json = response.json(); + expect(json.total).toBe(4); + expect(json.results).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + operation: 'transfer_send', + ticker: 'pepe', + tx_id: expect.not.stringMatching(transferHashAB), + address: addressB, + transfer_send: { + amount: '1000.000000000000000000', + from_address: addressA, + to_address: addressB, + }, + } as Brc20ActivityResponse), + ]) + ); + // B gets the transfer send in its feed + response = await fastify.inject({ + method: 'GET', + url: `/ordinals/brc-20/activity?ticker=pepe&address=${addressB}`, + }); + expect(response.statusCode).toBe(200); + json = response.json(); + expect(json.total).toBe(3); + expect(json.results).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + operation: 'transfer_send', + ticker: 'pepe', + tx_id: expect.not.stringMatching(transferHashAB), + address: addressB, + transfer_send: { + amount: '1000.000000000000000000', + from_address: addressA, + to_address: addressB, + }, + } as Brc20ActivityResponse), + ]) + ); + + // Verify that the pepe transfer_send is in the activity feed + response = await fastify.inject({ + method: 'GET', + url: `/ordinals/brc-20/activity?ticker=pepe`, + }); + expect(response.statusCode).toBe(200); + json = response.json(); + expect(json.total).toBe(6); + expect(json.results).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + operation: 'transfer_send', + ticker: 'pepe', + tx_id: expect.not.stringMatching(transferHashAB), + address: addressB, + transfer_send: { + amount: '1000.000000000000000000', + from_address: addressA, + to_address: addressB, + }, + } as Brc20ActivityResponse), + ]) + ); + + // Step 7: B transfer_send the transfer to C + const transferHashBCSend = randomHash(); + await db.updateInscriptions( + new TestChainhookPayloadBuilder() + .apply() + .block({ height: blockHeights.next().value }) + .transaction({ hash: transferHashBCSend }) + .brc20( + { + transfer_send: { + tick: 'pepe', + inscription_id: `${transferHashBC}i0`, + amt: '2000', + sender_address: addressB, + receiver_address: addressC, + }, + }, + { inscription_number: numberBC } + ) + .build() + ); + + // Verify that the pepe transfer_send is in the activity feed + response = await fastify.inject({ + method: 'GET', + url: `/ordinals/brc-20/activity?ticker=pepe`, + }); + expect(response.statusCode).toBe(200); + json = response.json(); + expect(json.total).toBe(7); + expect(json.results).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + operation: 'transfer_send', + ticker: 'pepe', + tx_id: expect.not.stringMatching(transferHashBC), + address: addressC, + transfer_send: { + amount: '2000.000000000000000000', + from_address: addressB, + to_address: addressC, + }, + } as Brc20ActivityResponse), + ]) + ); + // B gets the transfer send in its feed + response = await fastify.inject({ + method: 'GET', + url: `/ordinals/brc-20/activity?ticker=pepe&address=${addressB}`, + }); + expect(response.statusCode).toBe(200); + json = response.json(); + expect(json.total).toBe(4); + expect(json.results).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + operation: 'transfer_send', + ticker: 'pepe', + tx_id: expect.not.stringMatching(transferHashBC), + address: addressC, + transfer_send: { + amount: '2000.000000000000000000', + from_address: addressB, + to_address: addressC, + }, + } as Brc20ActivityResponse), + ]) + ); + // C gets the transfer send in its feed + response = await fastify.inject({ + method: 'GET', + url: `/ordinals/brc-20/activity?ticker=pepe&address=${addressC}`, + }); + expect(response.statusCode).toBe(200); + json = response.json(); + expect(json.total).toBe(1); + expect(json.results).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + operation: 'transfer_send', + ticker: 'pepe', + tx_id: expect.not.stringMatching(transferHashBC), + address: addressC, + transfer_send: { + amount: '2000.000000000000000000', + from_address: addressB, + to_address: addressC, + }, + } as Brc20ActivityResponse), + ]) + ); + }); + + test('activity for multiple token creation', async () => { + const inscriptionNumbers = incrementing(0); + const blockHeights = incrementing(BRC20_GENESIS_BLOCK); + const addressA = 'bc1q6uwuet65rm6xvlz7ztw2gvdmmay5uaycu03mqz'; + + // Step 1: Create a token pepe + let number = inscriptionNumbers.next().value; + await db.updateInscriptions( + new TestChainhookPayloadBuilder() + .apply() + .block({ height: blockHeights.next().value }) + .transaction({ hash: randomHash() }) + .brc20( + { + deploy: { + inscription_id: `${randomHash()}i0`, + tick: 'pepe', + max: '21000000', + lim: '21000000', + dec: '18', + address: addressA, + self_mint: false, + }, + }, + { inscription_number: 0 } + ) + .build() + ); + + // Verify that the pepe deploy is in the activity feed + let response = await fastify.inject({ + method: 'GET', + url: `/ordinals/brc-20/activity`, + }); + expect(response.statusCode).toBe(200); + let json = response.json(); + expect(json.total).toBe(1); + expect(json.results).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + operation: 'deploy', + ticker: 'pepe', + address: addressA, + deploy: expect.objectContaining({ + max_supply: '21000000.000000000000000000', + }), + } as Brc20ActivityResponse), + ]) + ); + + // Step 2: Create a token peer + number = inscriptionNumbers.next().value; + await db.updateInscriptions( + new TestChainhookPayloadBuilder() + .apply() + .block({ height: blockHeights.next().value }) + .transaction({ hash: randomHash() }) + .brc20( + { + deploy: { + inscription_id: `${randomHash()}i0`, + tick: 'peer', + max: '21000000', + lim: '21000000', + dec: '18', + address: addressA, + self_mint: false, + }, + }, + { inscription_number: 1 } + ) + .build() + ); + + // Verify that the peer deploy is in the activity feed + response = await fastify.inject({ + method: 'GET', + url: `/ordinals/brc-20/activity`, + }); + expect(response.statusCode).toBe(200); + json = response.json(); + expect(json.total).toBe(2); + expect(json.results).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + operation: 'deploy', + ticker: 'peer', + address: addressA, + deploy: expect.objectContaining({ + max_supply: '21000000.000000000000000000', + }), + } as Brc20ActivityResponse), + ]) + ); + + // Verify that no events are available before the first block height + response = await fastify.inject({ + method: 'GET', + url: `/ordinals/brc-20/activity?ticker=peer&block_height=${BRC20_GENESIS_BLOCK}`, + }); + expect(response.statusCode).toBe(200); + json = response.json(); + expect(json.total).toBe(0); + expect(json.results).toEqual([]); + + // Verify that the peer deploy is not in the activity feed when using block_height parameter + response = await fastify.inject({ + method: 'GET', + url: `/ordinals/brc-20/activity?block_height=${BRC20_GENESIS_BLOCK}`, + }); + expect(response.statusCode).toBe(200); + json = response.json(); + expect(json.total).toBe(1); + expect(json.results).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + operation: 'deploy', + ticker: 'pepe', + address: addressA, + deploy: expect.objectContaining({ + max_supply: '21000000.000000000000000000', + }), + } as Brc20ActivityResponse), + ]) + ); + // Should NOT include peer at this block height + expect(json.results).not.toEqual( + expect.arrayContaining([ + expect.objectContaining({ + ticker: 'peer', + } as Brc20ActivityResponse), + ]) + ); + }); + }); + + describe('/brc-20/token/holders', () => { + test('displays holders for token', async () => { + const address = 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td'; + await deployAndMintPEPE(db, address); + await db.updateInscriptions( + new TestChainhookPayloadBuilder() + .apply() + .block({ + height: BRC20_GENESIS_BLOCK + 2, + hash: '0000000000000000000034dd2daec375371800da441b17651459b2220cbc1a6e', + }) + .transaction({ + hash: '633648e0e1ddcab8dea0496a561f2b08c486ae619b5634d7bb55d7f0cd32ef16', + }) + .brc20( + { + mint: { + inscription_id: + '633648e0e1ddcab8dea0496a561f2b08c486ae619b5634d7bb55d7f0cd32ef16i0', + tick: 'pepe', + address: 'bc1qp9jgp9qtlhgvwjnxclj6kav6nr2fq09c206pyl', + amt: '2000', + }, + }, + { inscription_number: 2 } + ) + .build() + ); + + const response = await fastify.inject({ + method: 'GET', + url: `/ordinals/brc-20/tokens/pepe/holders`, + }); + expect(response.statusCode).toBe(200); + const json = response.json(); + expect(json.total).toBe(2); + expect(json.results).toStrictEqual([ + { + address: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td', + overall_balance: '10000.000000000000000000', + }, + { + address: 'bc1qp9jgp9qtlhgvwjnxclj6kav6nr2fq09c206pyl', + overall_balance: '2000.000000000000000000', + }, + ]); + }); + + test('shows empty list on token with no holders', async () => { + await db.updateInscriptions( + new TestChainhookPayloadBuilder() + .apply() + .block({ + height: BRC20_GENESIS_BLOCK, + hash: '00000000000000000002a90330a99f67e3f01eb2ce070b45930581e82fb7a91d', + }) + .transaction({ + hash: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', + }) + .brc20( + { + deploy: { + inscription_id: + '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', + tick: 'pepe', + max: '250000', + lim: '250000', + dec: '18', + address: 'bc1qp9jgp9qtlhgvwjnxclj6kav6nr2fq09c206pyl', + self_mint: false, + }, + }, + { inscription_number: 0 } + ) + .build() + ); + const response = await fastify.inject({ + method: 'GET', + url: `/ordinals/brc-20/tokens/pepe/holders`, + }); + expect(response.statusCode).toBe(200); + const json = response.json(); + expect(json.total).toBe(0); + expect(json.results).toStrictEqual([]); + }); + + test('shows 404 on token not found', async () => { + const response = await fastify.inject({ + method: 'GET', + url: `/ordinals/brc-20/tokens/pepe/holders`, + }); + expect(response.statusCode).toBe(404); + }); + }); +}); diff --git a/tests/brc-20/brc20.test.ts b/tests/brc-20/brc20.test.ts index 8f2132f3..4bd7fef3 100644 --- a/tests/brc-20/brc20.test.ts +++ b/tests/brc-20/brc20.test.ts @@ -1,78 +1,19 @@ import { runMigrations } from '@hirosystems/api-toolkit'; import { buildApiServer } from '../../src/api/init'; -import { Brc20ActivityResponse, Brc20TokenResponse } from '../../src/api/schemas'; -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 { + BRC20_GENESIS_BLOCK, + BRC20_SELF_MINT_ACTIVATION_BLOCK, TestChainhookPayloadBuilder, TestFastifyServer, - brc20Reveal, - incrementing, - randomHash, + deployAndMintPEPE, rollBack, } from '../helpers'; -import { BRC20_GENESIS_BLOCK } from '../../src/pg/brc20/brc20-pg-store'; describe('BRC-20', () => { let db: PgStore; let fastify: TestFastifyServer; - const deployAndMintPEPE = async (address: string) => { - 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: '250000', - }, - 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: '10000', - }, - number: 1, - ordinal_number: 1, - tx_id: '3b55f624eaa4f8de6c42e0c490176b67123a83094384f658611faf7bfb85dd0f', - address: address, - }) - ) - .build() - ); - }; - beforeEach(async () => { await runMigrations(MIGRATIONS_DIR, 'up'); db = await PgStore.connect({ skipMigrations: true }); @@ -85,600 +26,6 @@ describe('BRC-20', () => { await runMigrations(MIGRATIONS_DIR, 'down'); }); - describe('token standard validation', () => { - const testInsert = (json: any, block_height: number = 830000): InscriptionRevealData => { - const content = Buffer.from(JSON.stringify(json), 'utf-8'); - 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, - }, - }; - }; - - test('ignores incorrect MIME type', () => { - 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: '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.inscription.content_type = 'application/json'; - insert.inscription.mime_type = 'application/json'; - expect(brc20FromInscription(insert)).not.toBeUndefined(); - insert.inscription.content_type = 'text/plain;charset=utf-8'; - insert.inscription.mime_type = 'text/plain'; - expect(brc20FromInscription(insert)).not.toBeUndefined(); - }); - - test('ignores invalid JSON', () => { - const content = Buffer.from( - '{"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: '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(); - }); - - test('ignores incorrect p field', () => { - const insert = testInsert({ - p: 'brc20', // incorrect - op: 'deploy', - tick: 'PEPE', - max: '21000000', - }); - expect(brc20FromInscription(insert)).toBeUndefined(); - }); - - test('ignores incorrect op field', () => { - const insert = testInsert({ - p: 'brc-20', - op: 'deploi', // incorrect - tick: 'PEPE', - max: '21000000', - }); - expect(brc20FromInscription(insert)).toBeUndefined(); - }); - - test('tick must be 4 or 5 bytes wide', () => { - const insert = testInsert({ - p: 'brc-20', - op: 'deploy', - tick: 'PEPETESTER', // more than 4 bytes - max: '21000000', - }); - expect(brc20FromInscription(insert)).toBeUndefined(); - const insert2 = testInsert({ - p: 'brc-20', - op: 'deploy', - tick: 'Pe P', // valid - max: '21000000', - }); - expect(brc20FromInscription(insert2)).not.toBeUndefined(); - const insert3 = testInsert({ - p: 'brc-20', - op: 'deploy', - tick: '🤬😉', // more than 4 bytes - max: '21000000', - }); - expect(brc20FromInscription(insert3)).toBeUndefined(); - const insert4 = testInsert({ - p: 'brc-20', - op: 'deploy', - tick: 'X', // less than 4 bytes - max: '21000000', - }); - 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', - op: 'deploy', - tick: 'PEPE', - max: 21000000, - }); - expect(brc20FromInscription(insert1)).toBeUndefined(); - const insert1a = testInsert({ - p: 'brc-20', - op: 'deploy', - tick: 'PEPE', - max: '21000000', - lim: 300, - }); - expect(brc20FromInscription(insert1a)).toBeUndefined(); - const insert1b = testInsert({ - p: 'brc-20', - op: 'deploy', - tick: 'PEPE', - max: '21000000', - lim: '300', - dec: 2, - }); - expect(brc20FromInscription(insert1b)).toBeUndefined(); - const insert2 = testInsert({ - p: 'brc-20', - op: 'mint', - tick: 'PEPE', - amt: 2, - }); - expect(brc20FromInscription(insert2)).toBeUndefined(); - const insert3 = testInsert({ - p: 'brc-20', - op: 'transfer', - tick: 'PEPE', - amt: 2, - }); - expect(brc20FromInscription(insert3)).toBeUndefined(); - }); - - test('ignores empty strings', () => { - const insert1 = testInsert({ - p: 'brc-20', - op: 'deploy', - tick: '', - max: '21000000', - }); - expect(brc20FromInscription(insert1)).toBeUndefined(); - const insert1a = testInsert({ - p: 'brc-20', - op: 'deploy', - tick: 'PEPE', - max: '', - }); - expect(brc20FromInscription(insert1a)).toBeUndefined(); - const insert1b = testInsert({ - p: 'brc-20', - op: 'deploy', - tick: 'PEPE', - max: '21000000', - lim: '', - }); - expect(brc20FromInscription(insert1b)).toBeUndefined(); - const insert1c = testInsert({ - p: 'brc-20', - op: 'deploy', - tick: 'PEPE', - max: '21000000', - lim: '200', - dec: '', - }); - expect(brc20FromInscription(insert1c)).toBeUndefined(); - const insert2 = testInsert({ - p: 'brc-20', - op: 'mint', - tick: '', - }); - expect(brc20FromInscription(insert2)).toBeUndefined(); - const insert2a = testInsert({ - p: 'brc-20', - op: 'mint', - tick: 'PEPE', - amt: '', - }); - expect(brc20FromInscription(insert2a)).toBeUndefined(); - const insert3 = testInsert({ - p: 'brc-20', - op: 'transfer', - tick: '', - }); - expect(brc20FromInscription(insert3)).toBeUndefined(); - const insert3a = testInsert({ - p: 'brc-20', - op: 'transfer', - tick: 'PEPE', - amt: '', - }); - expect(brc20FromInscription(insert3a)).toBeUndefined(); - }); - - test('numeric strings must not be zero', () => { - const insert1 = testInsert({ - p: 'brc-20', - op: 'deploy', - tick: 'PEPE', - max: '0', - }); - expect(brc20FromInscription(insert1)).toBeUndefined(); - const insert1b = testInsert({ - p: 'brc-20', - op: 'deploy', - tick: 'PEPE', - max: '21000000', - lim: '0.0', - }); - expect(brc20FromInscription(insert1b)).toBeUndefined(); - const insert1c = testInsert({ - p: 'brc-20', - op: 'deploy', - tick: 'PEPE', - max: '21000000', - lim: '200', - dec: '0', - }); - // `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', - tick: 'PEPE', - amt: '0', - }); - expect(brc20FromInscription(insert2a)).toBeUndefined(); - const insert3a = testInsert({ - p: 'brc-20', - op: 'transfer', - tick: 'PEPE', - amt: '.0000', - }); - expect(brc20FromInscription(insert3a)).toBeUndefined(); - }); - - test('numeric fields are not stripped/trimmed', () => { - const insert1 = testInsert({ - p: 'brc-20', - op: 'deploy', - tick: 'PEPE', - max: ' 200 ', - }); - expect(brc20FromInscription(insert1)).toBeUndefined(); - const insert1b = testInsert({ - p: 'brc-20', - op: 'deploy', - tick: 'PEPE', - max: '21000000', - lim: '+10000', - }); - expect(brc20FromInscription(insert1b)).toBeUndefined(); - const insert1c = testInsert({ - p: 'brc-20', - op: 'deploy', - tick: 'PEPE', - max: '21000000', - lim: '200', - dec: ' 0 ', - }); - expect(brc20FromInscription(insert1c)).toBeUndefined(); - const insert2a = testInsert({ - p: 'brc-20', - op: 'mint', - tick: 'PEPE', - amt: '.05 ', - }); - expect(brc20FromInscription(insert2a)).toBeUndefined(); - const insert3a = testInsert({ - p: 'brc-20', - op: 'transfer', - tick: 'PEPE', - amt: '-25.00', - }); - expect(brc20FromInscription(insert3a)).toBeUndefined(); - }); - - test('max value of dec is 18', () => { - const insert1c = testInsert({ - p: 'brc-20', - op: 'deploy', - tick: 'PEPE', - max: '21000000', - lim: '200', - dec: '20', - }); - expect(brc20FromInscription(insert1c)).toBeUndefined(); - }); - - test('max value of any numeric field is uint64_max', () => { - const insert1 = testInsert({ - p: 'brc-20', - op: 'deploy', - tick: 'PEPE', - max: '18446744073709551999', - }); - expect(brc20FromInscription(insert1)).toBeUndefined(); - const insert1b = testInsert({ - p: 'brc-20', - op: 'deploy', - tick: 'PEPE', - max: '21000000', - lim: '18446744073709551999', - }); - expect(brc20FromInscription(insert1b)).toBeUndefined(); - const insert2a = testInsert({ - p: 'brc-20', - op: 'mint', - tick: 'PEPE', - amt: '18446744073709551999', - }); - expect(brc20FromInscription(insert2a)).toBeUndefined(); - const insert3a = testInsert({ - p: 'brc-20', - op: 'transfer', - tick: 'PEPE', - amt: '18446744073709551999', - }); - expect(brc20FromInscription(insert3a)).toBeUndefined(); - }); - - test('valid JSONs can have additional properties', () => { - const insert1 = testInsert({ - p: 'brc-20', - op: 'deploy', - tick: 'PEPE', - max: '200', - foo: 'bar', - test: 1, - }); - expect(brc20FromInscription(insert1)).not.toBeUndefined(); - const insert2a = testInsert({ - p: 'brc-20', - op: 'mint', - tick: 'PEPE', - amt: '5', - foo: 'bar', - test: 1, - }); - expect(brc20FromInscription(insert2a)).not.toBeUndefined(); - const insert3a = testInsert({ - p: 'brc-20', - op: 'transfer', - tick: 'PEPE', - amt: '25', - foo: 'bar', - test: 1, - }); - expect(brc20FromInscription(insert3a)).not.toBeUndefined(); - }); - }); - describe('deploy', () => { test('deploy is saved', async () => { await db.updateInscriptions( @@ -692,25 +39,26 @@ describe('BRC-20', () => { .transaction({ hash: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', }) - .inscriptionRevealed( - brc20Reveal({ - json: { - p: 'brc-20', - op: 'deploy', - tick: 'PEPE', + .brc20( + { + deploy: { + inscription_id: + '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', + tick: 'pepe', max: '21000000', + lim: '1000', + dec: '18', + address: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td', + self_mint: false, }, - number: 0, - ordinal_number: 0, - tx_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', - address: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td', - }) + }, + { inscription_number: 0 } ) .build() ); const response1 = await fastify.inject({ method: 'GET', - url: `/ordinals/brc-20/tokens?ticker=PEPE`, + url: `/ordinals/brc-20/tokens?ticker=pepe`, }); expect(response1.statusCode).toBe(200); const responseJson1 = response1.json(); @@ -722,9 +70,9 @@ describe('BRC-20', () => { decimals: 18, id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', number: 0, - mint_limit: null, + mint_limit: '1000.000000000000000000', max_supply: '21000000.000000000000000000', - ticker: 'PEPE', + ticker: 'pepe', tx_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', deploy_timestamp: 1677811111000, minted_supply: '0.000000000000000000', @@ -734,44 +82,6 @@ describe('BRC-20', () => { ]); }); - 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() @@ -784,26 +94,26 @@ describe('BRC-20', () => { .transaction({ hash: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', }) - .inscriptionRevealed( - brc20Reveal({ - json: { - p: 'brc-20', - op: 'deploy', - tick: '$PEPE', + .brc20( + { + deploy: { + inscription_id: + '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', + tick: '$pepe', max: '21000000', - self_mint: 'true', + lim: '1000', + dec: '18', + address: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td', + self_mint: true, }, - number: 0, - ordinal_number: 0, - tx_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', - address: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td', - }) + }, + { inscription_number: 0 } ) .build() ); const response1 = await fastify.inject({ method: 'GET', - url: `/ordinals/brc-20/tokens?ticker=$PEPE`, + url: `/ordinals/brc-20/tokens?ticker=$pepe`, }); expect(response1.statusCode).toBe(200); const responseJson1 = response1.json(); @@ -815,17 +125,20 @@ describe('BRC-20', () => { deploy_timestamp: 1677811111000, id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', max_supply: '21000000.000000000000000000', - mint_limit: null, + mint_limit: '1000.000000000000000000', self_mint: true, minted_supply: '0.000000000000000000', number: 0, - ticker: '$PEPE', + ticker: '$pepe', tx_count: 1, tx_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', }); }); + }); - test('ignores deploys for existing token', async () => { + describe('mint', () => { + test('valid mints are saved and balance reflected', async () => { + const address = 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td'; await db.updateInscriptions( new TestChainhookPayloadBuilder() .apply() @@ -836,19 +149,20 @@ describe('BRC-20', () => { .transaction({ hash: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', }) - .inscriptionRevealed( - brc20Reveal({ - json: { - p: 'brc-20', - op: 'deploy', - tick: 'PEPE', + .brc20( + { + deploy: { + inscription_id: + '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', + tick: 'pepe', max: '21000000', + lim: '250000', + dec: '18', + address: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td', + self_mint: false, }, - number: 0, - ordinal_number: 0, - tx_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', - address: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td', - }) + }, + { inscription_number: 0 } ) .build() ); @@ -857,351 +171,124 @@ describe('BRC-20', () => { .apply() .block({ height: BRC20_GENESIS_BLOCK + 1, - hash: '000000000000000000021a0207fa97024506baaa74396822fb0a07ac20e70148', + hash: '0000000000000000000098d8f2663891d439f6bb7de230d4e9f6bcc2e85452bf', }) .transaction({ - hash: '3f8067a6e9b45308b5a090c2987feeb2d08cbaf814ef2ffabad7c381b62f5f7e', + hash: '8aec77f855549d98cb9fb5f35e02a03f9a2354fd05a5f89fc610b32c3b01f99f', }) - .inscriptionRevealed( - brc20Reveal({ - json: { - p: 'brc-20', - op: 'deploy', - tick: 'PEPE', - max: '19000000', + .brc20( + { + mint: { + tick: 'pepe', + amt: '250000', + inscription_id: + '8aec77f855549d98cb9fb5f35e02a03f9a2354fd05a5f89fc610b32c3b01f99fi0', + address, }, - number: 1, - ordinal_number: 1, - tx_id: '3f8067a6e9b45308b5a090c2987feeb2d08cbaf814ef2ffabad7c381b62f5f7e', - address: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td', - }) + }, + { inscription_number: 1 } ) .build() ); + const response1 = await fastify.inject({ method: 'GET', - url: `/ordinals/brc-20/tokens?ticker=PEPE`, + url: `/ordinals/brc-20/balances/${address}`, }); expect(response1.statusCode).toBe(200); const responseJson1 = response1.json(); expect(responseJson1.total).toBe(1); expect(responseJson1.results).toStrictEqual([ { - address: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td', - block_height: BRC20_GENESIS_BLOCK, - decimals: 18, - id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', - max_supply: '21000000.000000000000000000', - mint_limit: null, - number: 0, - ticker: 'PEPE', - tx_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', - deploy_timestamp: 1677803510000, - minted_supply: '0.000000000000000000', - tx_count: 1, - self_mint: false, + ticker: 'pepe', + available_balance: '250000.000000000000000000', + overall_balance: '250000.000000000000000000', + transferrable_balance: '0.000000000000000000', }, ]); - }); - test('ignores case insensitive deploy for existing token', async () => { - 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: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td', - }) - ) - .build() - ); + // New mint await db.updateInscriptions( new TestChainhookPayloadBuilder() .apply() .block({ - height: BRC20_GENESIS_BLOCK + 1, - hash: '000000000000000000021a0207fa97024506baaa74396822fb0a07ac20e70148', + height: BRC20_GENESIS_BLOCK + 2, + hash: '0000000000000000000077163227125e51d838787d6af031bc9b55a3a1cc1b2c', }) .transaction({ - hash: '3f8067a6e9b45308b5a090c2987feeb2d08cbaf814ef2ffabad7c381b62f5f7e', + hash: '7a1adbc3e93ddf8d7c4e0ba75aa11c98c431521dd850be8b955feedb716d8bec', }) - .inscriptionRevealed( - brc20Reveal({ - json: { - p: 'brc-20', - op: 'deploy', + .brc20( + { + mint: { tick: 'pepe', - max: '19000000', + amt: '100000', + inscription_id: + '7a1adbc3e93ddf8d7c4e0ba75aa11c98c431521dd850be8b955feedb716d8beci0', + address, }, - number: 1, - ordinal_number: 1, - tx_id: '3f8067a6e9b45308b5a090c2987feeb2d08cbaf814ef2ffabad7c381b62f5f7e', - address: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td', - }) + }, + { inscription_number: 2 } ) .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).toStrictEqual([ - { - address: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td', - block_height: BRC20_GENESIS_BLOCK, - decimals: 18, - id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', - max_supply: '21000000.000000000000000000', - mint_limit: null, - number: 0, - ticker: 'PEPE', - tx_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', - deploy_timestamp: 1677803510000, - minted_supply: '0.000000000000000000', - tx_count: 1, - self_mint: false, - }, - ]); + const response2 = await fastify.inject({ method: 'GET', - url: `/ordinals/brc-20/tokens?ticker=pepe`, // Lowercase + url: `/ordinals/brc-20/balances/${address}`, }); expect(response2.statusCode).toBe(200); const responseJson2 = response2.json(); expect(responseJson2.total).toBe(1); expect(responseJson2.results).toStrictEqual([ { - address: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td', - block_height: BRC20_GENESIS_BLOCK, - decimals: 18, - id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', - max_supply: '21000000.000000000000000000', - mint_limit: null, - number: 0, - ticker: 'PEPE', - tx_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', - deploy_timestamp: 1677803510000, - minted_supply: '0.000000000000000000', - tx_count: 1, - self_mint: false, + 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('ignores deploy from classic cursed inscription', 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({ hash: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', }) - .inscriptionRevealed( - brc20Reveal({ - json: { - p: 'brc-20', - op: 'deploy', - tick: 'PEPE', + .brc20( + { + deploy: { + inscription_id: + '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', + tick: '$pepe', max: '21000000', + lim: '21000000', + dec: '18', + address, + self_mint: true, }, - number: 0, - ordinal_number: 0, - classic_number: -1, - 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); - expect(responseJson1.results).toHaveLength(0); - }); - }); - - describe('mint', () => { - test('valid mints are saved and balance reflected', 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() - ); - - 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_GENESIS_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', - }) - ) - .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('valid self mints are saved and balance reflected', async () => { - const address = 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td'; - 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: '21000000', - self_mint: 'true', - }, - number: 0, - ordinal_number: 0, - tx_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', - address: address, - }) + }, + { inscription_number: 0 } ) .build() ); @@ -1215,20 +302,17 @@ describe('BRC-20', () => { .transaction({ hash: '8aec77f855549d98cb9fb5f35e02a03f9a2354fd05a5f89fc610b32c3b01f99f', }) - .inscriptionRevealed( - brc20Reveal({ - json: { - p: 'brc-20', - op: 'mint', - tick: '$PEPE', + .brc20( + { + mint: { + inscription_id: + '8aec77f855549d98cb9fb5f35e02a03f9a2354fd05a5f89fc610b32c3b01f99fi0', + tick: '$pepe', + address, amt: '250000', }, - number: 1, - ordinal_number: 1, - tx_id: '8aec77f855549d98cb9fb5f35e02a03f9a2354fd05a5f89fc610b32c3b01f99f', - address: address, - parent: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', - }) + }, + { inscription_number: 1 } ) .build() ); @@ -1242,7 +326,7 @@ describe('BRC-20', () => { expect(responseJson1.total).toBe(1); expect(responseJson1.results).toStrictEqual([ { - ticker: '$PEPE', + ticker: '$pepe', available_balance: '250000.000000000000000000', overall_balance: '250000.000000000000000000', transferrable_balance: '0.000000000000000000', @@ -1260,20 +344,17 @@ describe('BRC-20', () => { .transaction({ hash: '7a1adbc3e93ddf8d7c4e0ba75aa11c98c431521dd850be8b955feedb716d8bec', }) - .inscriptionRevealed( - brc20Reveal({ - json: { - p: 'brc-20', - op: 'mint', + .brc20( + { + mint: { + inscription_id: + '7a1adbc3e93ddf8d7c4e0ba75aa11c98c431521dd850be8b955feedb716d8beci0', tick: '$pepe', + address: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td', amt: '100000', }, - number: 2, - ordinal_number: 2, - tx_id: '7a1adbc3e93ddf8d7c4e0ba75aa11c98c431521dd850be8b955feedb716d8bec', - address: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td', - parent: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', - }) + }, + { inscription_number: 2 } ) .build() ); @@ -1287,7 +368,7 @@ describe('BRC-20', () => { expect(responseJson2.total).toBe(1); expect(responseJson2.results).toStrictEqual([ { - ticker: '$PEPE', + ticker: '$pepe', available_balance: '350000.000000000000000000', overall_balance: '350000.000000000000000000', transferrable_balance: '0.000000000000000000', @@ -1296,93 +377,14 @@ describe('BRC-20', () => { 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('self mints with invalid parent inscription are ignored', async () => { - const address = 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td'; - 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: '21000000', - 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: '8aec77f855549d98cb9fb5f35e02a03f9a2354fd05a5f89fc610b32c3b01f99f', - }) - .inscriptionRevealed( - brc20Reveal({ - json: { - p: 'brc-20', - op: 'mint', - tick: '$PEPE', - amt: '250000', - }, - number: 1, - ordinal_number: 1, - tx_id: '8aec77f855549d98cb9fb5f35e02a03f9a2354fd05a5f89fc610b32c3b01f99f', - address: address, - // no parent - }) - ) - .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(0); - - const response3 = await fastify.inject({ - method: 'GET', - url: `/ordinals/brc-20/tokens?ticker=$PEPE`, + 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' }), + expect.objectContaining({ ticker: '$pepe', minted_supply: '350000.000000000000000000' }), ]) ); }); @@ -1399,20 +401,20 @@ describe('BRC-20', () => { .transaction({ hash: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', }) - .inscriptionRevealed( - brc20Reveal({ - json: { - p: 'brc-20', - op: 'deploy', - tick: '$PEPE', + .brc20( + { + deploy: { + inscription_id: + '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', + tick: '$pepe', max: '0', - self_mint: 'true', + lim: '250000', + dec: '18', + address, + self_mint: true, }, - number: 0, - ordinal_number: 0, - tx_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', - address: address, - }) + }, + { inscription_number: 0 } ) .build() ); @@ -1426,20 +428,17 @@ describe('BRC-20', () => { .transaction({ hash: '8aec77f855549d98cb9fb5f35e02a03f9a2354fd05a5f89fc610b32c3b01f99f', }) - .inscriptionRevealed( - brc20Reveal({ - json: { - p: 'brc-20', - op: 'mint', - tick: '$PEPE', + .brc20( + { + mint: { + inscription_id: + '8aec77f855549d98cb9fb5f35e02a03f9a2354fd05a5f89fc610b32c3b01f99fi0', + tick: '$pepe', + address, amt: '250000', }, - number: 1, - ordinal_number: 1, - tx_id: '8aec77f855549d98cb9fb5f35e02a03f9a2354fd05a5f89fc610b32c3b01f99f', - address: address, - parent: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', - }) + }, + { inscription_number: 1 } ) .build() ); @@ -1453,7 +452,7 @@ describe('BRC-20', () => { expect(responseJson1.total).toBe(1); expect(responseJson1.results).toStrictEqual([ { - ticker: '$PEPE', + ticker: '$pepe', available_balance: '250000.000000000000000000', overall_balance: '250000.000000000000000000', transferrable_balance: '0.000000000000000000', @@ -1471,20 +470,17 @@ describe('BRC-20', () => { .transaction({ hash: '7a1adbc3e93ddf8d7c4e0ba75aa11c98c431521dd850be8b955feedb716d8bec', }) - .inscriptionRevealed( - brc20Reveal({ - json: { - p: 'brc-20', - op: 'mint', + .brc20( + { + mint: { + inscription_id: + '7a1adbc3e93ddf8d7c4e0ba75aa11c98c431521dd850be8b955feedb716d8beci0', tick: '$pepe', + address, amt: '100000', }, - number: 2, - ordinal_number: 2, - tx_id: '7a1adbc3e93ddf8d7c4e0ba75aa11c98c431521dd850be8b955feedb716d8bec', - address: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td', - parent: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', - }) + }, + { inscription_number: 2 } ) .build() ); @@ -1498,7 +494,7 @@ describe('BRC-20', () => { expect(responseJson2.total).toBe(1); expect(responseJson2.results).toStrictEqual([ { - ticker: '$PEPE', + ticker: '$pepe', available_balance: '350000.000000000000000000', overall_balance: '350000.000000000000000000', transferrable_balance: '0.000000000000000000', @@ -1507,14 +503,14 @@ describe('BRC-20', () => { const response3 = await fastify.inject({ method: 'GET', - url: `/ordinals/brc-20/tokens?ticker=$PEPE`, + 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' }), + expect.objectContaining({ ticker: '$pepe', minted_supply: '350000.000000000000000000' }), ]) ); }); @@ -1531,19 +527,20 @@ describe('BRC-20', () => { .transaction({ hash: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', }) - .inscriptionRevealed( - brc20Reveal({ - json: { - p: 'brc-20', - op: 'deploy', - tick: 'PEPE', + .brc20( + { + deploy: { + inscription_id: + '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', + tick: 'pepe', max: '21000000', + lim: '21000000', + dec: '18', + address, + self_mint: false, }, - number: 0, - ordinal_number: 0, - tx_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', - address: address, - }) + }, + { inscription_number: 0 } ) .build() ); @@ -1557,19 +554,17 @@ describe('BRC-20', () => { .transaction({ hash: '8aec77f855549d98cb9fb5f35e02a03f9a2354fd05a5f89fc610b32c3b01f99f', }) - .inscriptionRevealed( - brc20Reveal({ - json: { - p: 'brc-20', - op: 'mint', - tick: 'PEPE', + .brc20( + { + mint: { + inscription_id: + '8aec77f855549d98cb9fb5f35e02a03f9a2354fd05a5f89fc610b32c3b01f99fi0', + tick: 'pepe', + address, amt: '250000', }, - number: 1, - ordinal_number: 1, - tx_id: '8aec77f855549d98cb9fb5f35e02a03f9a2354fd05a5f89fc610b32c3b01f99f', - address: address, - }) + }, + { inscription_number: 1 } ) .build() ); @@ -1584,19 +579,17 @@ describe('BRC-20', () => { .transaction({ hash: '8aec77f855549d98cb9fb5f35e02a03f9a2354fd05a5f89fc610b32c3b01f99f', }) - .inscriptionRevealed( - brc20Reveal({ - json: { - p: 'brc-20', - op: 'mint', - tick: 'PEPE', + .brc20( + { + mint: { + inscription_id: + '8aec77f855549d98cb9fb5f35e02a03f9a2354fd05a5f89fc610b32c3b01f99fi0', + tick: 'pepe', + address, amt: '250000', }, - number: 1, - ordinal_number: 1, - tx_id: '8aec77f855549d98cb9fb5f35e02a03f9a2354fd05a5f89fc610b32c3b01f99f', - address: address, - }) + }, + { inscription_number: 1 } ) .build() ); @@ -1612,520 +605,105 @@ describe('BRC-20', () => { const response3 = await fastify.inject({ method: 'GET', - url: `/ordinals/brc-20/tokens/PEPE`, + 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 () => { + describe('transfer', () => { + test('available balance decreases on transfer inscription', async () => { const address = 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td'; + await deployAndMintPEPE(db, address); 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', + height: BRC20_GENESIS_BLOCK + 2, + hash: '00000000000000000002b14f0c5dde0b2fc74d022e860696bd64f1f652756674', }) .transaction({ - hash: '8aec77f855549d98cb9fb5f35e02a03f9a2354fd05a5f89fc610b32c3b01f99f', + hash: 'eee52b22397ea4a4aefe6a39931315e93a157091f5a994216c0aa9c8c6fef47a', }) - .inscriptionRevealed( - brc20Reveal({ - json: { - p: 'brc-20', - op: 'mint', - tick: 'PEPE', - amt: '250000.000', // Invalid decimal count + .brc20( + { + transfer: { + inscription_id: + 'eee52b22397ea4a4aefe6a39931315e93a157091f5a994216c0aa9c8c6fef47ai0', + tick: 'pepe', + address, + amt: '2000', }, - number: 1, - ordinal_number: 1, - tx_id: '8aec77f855549d98cb9fb5f35e02a03f9a2354fd05a5f89fc610b32c3b01f99f', - address: address, - }) + }, + { inscription_number: 2 } ) .build() ); - const response2 = await fastify.inject({ + const response = 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(response.statusCode).toBe(200); + const json = response.json(); + expect(json.total).toBe(1); + expect(json.results).toStrictEqual([ + { + available_balance: '8000.000000000000000000', + overall_balance: '10000.000000000000000000', + ticker: 'pepe', + transferrable_balance: '2000.000000000000000000', + }, + ]); + + // Balance at previous block + const response2 = await fastify.inject({ + method: 'GET', + url: `/ordinals/brc-20/balances/${address}?block_height=779833`, + }); + const json2 = response2.json(); + expect(json2.results[0].available_balance).toBe('10000.000000000000000000'); }); - test('mint exceeds token supply', async () => { + test('multiple transfers in block', async () => { const address = 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td'; + await deployAndMintPEPE(db, address); await db.updateInscriptions( new TestChainhookPayloadBuilder() .apply() .block({ - height: BRC20_GENESIS_BLOCK, - hash: '00000000000000000002a90330a99f67e3f01eb2ce070b45930581e82fb7a91d', + height: BRC20_GENESIS_BLOCK + 2, + hash: '00000000000000000002b14f0c5dde0b2fc74d022e860696bd64f1f652756674', }) .transaction({ - hash: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', + hash: 'eee52b22397ea4a4aefe6a39931315e93a157091f5a994216c0aa9c8c6fef47a', }) - .inscriptionRevealed( - brc20Reveal({ - json: { - p: 'brc-20', - op: 'deploy', - tick: 'PEPE', - max: '2500', - dec: '1', + .brc20( + { + transfer: { + inscription_id: + 'eee52b22397ea4a4aefe6a39931315e93a157091f5a994216c0aa9c8c6fef47ai0', + tick: 'pepe', + address, + amt: '9000', }, - 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', - }, - ]); - - // No more mints allowed - await db.updateInscriptions( - new TestChainhookPayloadBuilder() - .apply() - .block({ - height: BRC20_GENESIS_BLOCK + 2, - hash: '000000000000000000001f14513d722146fddab04a1855665a5eca22df288c3c', - }) - .transaction({ - hash: 'bf7a3e1a0647ca88f6539119b2defaec302683704ea270b3302e709597643548', - }) - .inscriptionRevealed( - brc20Reveal({ - json: { - p: 'brc-20', - op: 'mint', - tick: 'PEPE', - amt: '1000', - }, - number: 4, - ordinal_number: 4, - tx_id: 'bf7a3e1a0647ca88f6539119b2defaec302683704ea270b3302e709597643548', - address: address, - }) - ) - .build() - ); - - const response3 = await fastify.inject({ - method: 'GET', - url: `/ordinals/brc-20/balances/${address}`, - }); - expect(response3.statusCode).toBe(200); - const responseJson3 = response3.json(); - expect(responseJson3).toStrictEqual(responseJson2); - }); - - test('ignores mint for non-existent token', async () => { - const address = 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td'; - 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: 0, - ordinal_number: 0, - tx_id: '3b55f624eaa4f8de6c42e0c490176b67123a83094384f658611faf7bfb85dd0f', - 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 mint limit', 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', - lim: '100', - }, - 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', // Greater than limit - }, - number: 1, - ordinal_number: 1, - tx_id: '3b55f624eaa4f8de6c42e0c490176b67123a83094384f658611faf7bfb85dd0f', - 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([]); - }); - }); - - describe('transfer', () => { - test('available balance decreases on transfer inscription', async () => { - const address = 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td'; - await deployAndMintPEPE(address); - await db.updateInscriptions( - new TestChainhookPayloadBuilder() - .apply() - .block({ - height: BRC20_GENESIS_BLOCK + 2, - hash: '00000000000000000002b14f0c5dde0b2fc74d022e860696bd64f1f652756674', - }) - .transaction({ - hash: 'eee52b22397ea4a4aefe6a39931315e93a157091f5a994216c0aa9c8c6fef47a', - }) - .inscriptionRevealed( - brc20Reveal({ - json: { - p: 'brc-20', - op: 'transfer', - tick: 'PEPE', - amt: '2000', - }, - number: 2, - ordinal_number: 2, - tx_id: 'eee52b22397ea4a4aefe6a39931315e93a157091f5a994216c0aa9c8c6fef47a', - address: address, - }) - ) - .build() - ); - - const response = await fastify.inject({ - method: 'GET', - url: `/ordinals/brc-20/balances/${address}`, - }); - expect(response.statusCode).toBe(200); - const json = response.json(); - expect(json.total).toBe(1); - expect(json.results).toStrictEqual([ - { - available_balance: '8000.000000000000000000', - overall_balance: '10000.000000000000000000', - ticker: 'PEPE', - transferrable_balance: '2000.000000000000000000', - }, - ]); - - // Balance at previous block - const response2 = await fastify.inject({ - method: 'GET', - url: `/ordinals/brc-20/balances/${address}?block_height=779833`, - }); - const json2 = response2.json(); - expect(json2.results[0].available_balance).toBe('10000.000000000000000000'); - }); - - test('transfer ignored if token not found', async () => { - const address = 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td'; - await deployAndMintPEPE(address); - await db.updateInscriptions( - new TestChainhookPayloadBuilder() - .apply() - .block({ - height: BRC20_GENESIS_BLOCK + 2, - hash: '00000000000000000002b14f0c5dde0b2fc74d022e860696bd64f1f652756674', - }) - .transaction({ - hash: 'eee52b22397ea4a4aefe6a39931315e93a157091f5a994216c0aa9c8c6fef47a', - }) - .inscriptionRevealed( - brc20Reveal({ - json: { - p: 'brc-20', - op: 'transfer', - tick: 'TEST', // Not found - amt: '2000', - }, - number: 2, - ordinal_number: 2, - tx_id: 'eee52b22397ea4a4aefe6a39931315e93a157091f5a994216c0aa9c8c6fef47a', - address: address, - }) - ) - .build() - ); - - const response = await fastify.inject({ - method: 'GET', - url: `/ordinals/brc-20/balances/${address}`, - }); - expect(response.statusCode).toBe(200); - const json = response.json(); - expect(json.total).toBe(1); - expect(json.results).toStrictEqual([ - { - available_balance: '10000.000000000000000000', - overall_balance: '10000.000000000000000000', - ticker: 'PEPE', - transferrable_balance: '0.000000000000000000', - }, - ]); - }); - - test('cannot transfer more than available balance', async () => { - const address = 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td'; - await deployAndMintPEPE(address); - await db.updateInscriptions( - new TestChainhookPayloadBuilder() - .apply() - .block({ - height: BRC20_GENESIS_BLOCK + 2, - hash: '00000000000000000002b14f0c5dde0b2fc74d022e860696bd64f1f652756674', - }) - .transaction({ - hash: 'eee52b22397ea4a4aefe6a39931315e93a157091f5a994216c0aa9c8c6fef47a', - }) - .inscriptionRevealed( - brc20Reveal({ - json: { - p: 'brc-20', - op: 'transfer', - tick: 'PEPE', - amt: '5000000000', // More than was minted - }, - number: 2, - ordinal_number: 2, - tx_id: 'eee52b22397ea4a4aefe6a39931315e93a157091f5a994216c0aa9c8c6fef47a', - address: address, - }) - ) - .build() - ); - - const response = await fastify.inject({ - method: 'GET', - url: `/ordinals/brc-20/balances/${address}`, - }); - expect(response.statusCode).toBe(200); - const json = response.json(); - expect(json.total).toBe(1); - expect(json.results).toStrictEqual([ - { - available_balance: '10000.000000000000000000', - overall_balance: '10000.000000000000000000', - ticker: 'PEPE', - transferrable_balance: '0.000000000000000000', - }, - ]); - }); - - test('multiple transfers in block', async () => { - const address = 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td'; - await deployAndMintPEPE(address); - await db.updateInscriptions( - new TestChainhookPayloadBuilder() - .apply() - .block({ - height: BRC20_GENESIS_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, - }) + }, + { inscription_number: 2 } ) .transaction({ hash: '7edaa48337a94da327b6262830505f116775a32db5ad4ad46e87ecea33f21bac', }) - .inscriptionRevealed( - brc20Reveal({ - json: { - p: 'brc-20', - op: 'transfer', - tick: 'PEPE', - amt: '2000', // Will exceed available balance + .brc20( + { + transfer: { + inscription_id: + '7edaa48337a94da327b6262830505f116775a32db5ad4ad46e87ecea33f21baci0', + tick: 'pepe', + address, + amt: '1000', }, - number: 3, - ordinal_number: 3, - tx_id: '7edaa48337a94da327b6262830505f116775a32db5ad4ad46e87ecea33f21bac', - address: address, - }) + }, + { inscription_number: 3 } ) .build() ); @@ -2139,325 +717,18 @@ describe('BRC-20', () => { expect(json.total).toBe(1); expect(json.results).toStrictEqual([ { - available_balance: '1000.000000000000000000', + available_balance: '0.000000000000000000', overall_balance: '10000.000000000000000000', - ticker: 'PEPE', - transferrable_balance: '9000.000000000000000000', + ticker: 'pepe', + transferrable_balance: '10000.000000000000000000', }, ]); }); test('send balance to address', async () => { const address = 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td'; - const address2 = '3QNjwPDRafjBm9XxJpshgk3ksMJh3TFxTU'; - await deployAndMintPEPE(address); - await db.updateInscriptions( - new TestChainhookPayloadBuilder() - .apply() - .block({ - height: BRC20_GENESIS_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_GENESIS_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', - }, - ]); - - // Balance at previous block - const prevBlock1 = await fastify.inject({ - method: 'GET', - url: `/ordinals/brc-20/balances/${address}?block_height=779833`, - }); - const prevBlockJson1 = prevBlock1.json(); - expect(prevBlockJson1.results[0].available_balance).toBe('10000.000000000000000000'); - const prevBlock2 = await fastify.inject({ - method: 'GET', - url: `/ordinals/brc-20/balances/${address2}?block_height=779833`, - }); - const prevBlockJson2 = prevBlock2.json(); - 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); - await db.updateInscriptions( - new TestChainhookPayloadBuilder() - .apply() - .block({ - height: BRC20_GENESIS_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_GENESIS_BLOCK + 3, - hash: '00000000000000000003feae13d107f0f2c4fb4dd08fb2a8b1ab553512e77f03', - }) - .transaction({ - hash: '7edaa48337a94da327b6262830505f116775a32db5ad4ad46e87ecea33f21bac', - }) - .inscriptionTransferred({ - ordinal_number: 2, - destination: { type: 'spent_in_fees', value: '' }, - 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: '10000.000000000000000000', - overall_balance: '10000.000000000000000000', - ticker: 'PEPE', - transferrable_balance: '0.000000000000000000', - }, - ]); - }); - - test('sending transfer to unspendable output does not return to sender', async () => { - const address = 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td'; - await deployAndMintPEPE(address); + const address2 = '3QNjwPDRafjBm9XxJpshgk3ksMJh3TFxTU'; + await deployAndMintPEPE(db, address); await db.updateInscriptions( new TestChainhookPayloadBuilder() .apply() @@ -2468,19 +739,17 @@ describe('BRC-20', () => { .transaction({ hash: 'eee52b22397ea4a4aefe6a39931315e93a157091f5a994216c0aa9c8c6fef47a', }) - .inscriptionRevealed( - brc20Reveal({ - json: { - p: 'brc-20', - op: 'transfer', - tick: 'PEPE', + .brc20( + { + transfer: { + inscription_id: + 'eee52b22397ea4a4aefe6a39931315e93a157091f5a994216c0aa9c8c6fef47ai0', + tick: 'pepe', + address, amt: '9000', }, - number: 2, - ordinal_number: 2, - tx_id: 'eee52b22397ea4a4aefe6a39931315e93a157091f5a994216c0aa9c8c6fef47a', - address: address, - }) + }, + { inscription_number: 2 } ) .build() ); @@ -2494,16 +763,19 @@ describe('BRC-20', () => { .transaction({ hash: '7edaa48337a94da327b6262830505f116775a32db5ad4ad46e87ecea33f21bac', }) - .inscriptionTransferred({ - ordinal_number: 2, - destination: { type: 'burnt' }, - satpoint_pre_transfer: - 'eee52b22397ea4a4aefe6a39931315e93a157091f5a994216c0aa9c8c6fef47a:0:0', - satpoint_post_transfer: - '7edaa48337a94da327b6262830505f116775a32db5ad4ad46e87ecea33f21bac:0:0', - post_transfer_output_value: null, - tx_index: 0, - }) + .brc20( + { + transfer_send: { + tick: 'pepe', + inscription_id: + 'eee52b22397ea4a4aefe6a39931315e93a157091f5a994216c0aa9c8c6fef47ai0', + amt: '9000', + sender_address: address, + receiver_address: address2, + }, + }, + { inscription_number: 2 } + ) .build() ); @@ -2518,39 +790,69 @@ describe('BRC-20', () => { { available_balance: '1000.000000000000000000', overall_balance: '1000.000000000000000000', - ticker: 'PEPE', + 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', }, ]); + + // Balance at previous block + const prevBlock1 = await fastify.inject({ + method: 'GET', + url: `/ordinals/brc-20/balances/${address}?block_height=779833`, + }); + const prevBlockJson1 = prevBlock1.json(); + expect(prevBlockJson1.results[0].available_balance).toBe('10000.000000000000000000'); + const prevBlock2 = await fastify.inject({ + method: 'GET', + url: `/ordinals/brc-20/balances/${address2}?block_height=779833`, + }); + const prevBlockJson2 = prevBlock2.json(); + expect(prevBlockJson2.results[0]).toBeUndefined(); }); - test('cannot spend valid transfer twice', async () => { + test('send balance for self_mint token to address', async () => { const address = 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td'; const address2 = '3QNjwPDRafjBm9XxJpshgk3ksMJh3TFxTU'; - await deployAndMintPEPE(address); await db.updateInscriptions( new TestChainhookPayloadBuilder() .apply() .block({ - height: BRC20_GENESIS_BLOCK + 2, - hash: '00000000000000000002b14f0c5dde0b2fc74d022e860696bd64f1f652756674', + height: BRC20_SELF_MINT_ACTIVATION_BLOCK, + hash: '00000000000000000002a90330a99f67e3f01eb2ce070b45930581e82fb7a91d', }) .transaction({ - hash: 'eee52b22397ea4a4aefe6a39931315e93a157091f5a994216c0aa9c8c6fef47a', + hash: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', }) - .inscriptionRevealed( - brc20Reveal({ - json: { - p: 'brc-20', - op: 'transfer', - tick: 'PEPE', - amt: '9000', + .brc20( + { + deploy: { + inscription_id: + '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', + tick: '$pepe', + max: '0', + lim: '21000000', + dec: '18', + address, + self_mint: true, }, - number: 2, - ordinal_number: 2, - tx_id: 'eee52b22397ea4a4aefe6a39931315e93a157091f5a994216c0aa9c8c6fef47a', - address: address, - }) + }, + { inscription_number: 0 } ) .build() ); @@ -2558,50 +860,76 @@ describe('BRC-20', () => { new TestChainhookPayloadBuilder() .apply() .block({ - height: BRC20_GENESIS_BLOCK + 3, - hash: '000000000000000000016ddf56d0fe72476165acee9500d48d3e2aaf8412f489', + height: BRC20_SELF_MINT_ACTIVATION_BLOCK + 1, + hash: '0000000000000000000098d8f2663891d439f6bb7de230d4e9f6bcc2e85452bf', }) .transaction({ - hash: '7edaa48337a94da327b6262830505f116775a32db5ad4ad46e87ecea33f21bac', + hash: '3b55f624eaa4f8de6c42e0c490176b67123a83094384f658611faf7bfb85dd0f', + }) + .brc20( + { + mint: { + inscription_id: + '3b55f624eaa4f8de6c42e0c490176b67123a83094384f658611faf7bfb85dd0fi0', + tick: '$pepe', + address, + amt: '10000', + }, + }, + { inscription_number: 1 } + ) + .build() + ); + await db.updateInscriptions( + new TestChainhookPayloadBuilder() + .apply() + .block({ + height: BRC20_SELF_MINT_ACTIVATION_BLOCK + 2, + hash: '00000000000000000002b14f0c5dde0b2fc74d022e860696bd64f1f652756674', }) - .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, + .transaction({ + hash: 'eee52b22397ea4a4aefe6a39931315e93a157091f5a994216c0aa9c8c6fef47a', }) + .brc20( + { + transfer: { + inscription_id: + 'eee52b22397ea4a4aefe6a39931315e93a157091f5a994216c0aa9c8c6fef47ai0', + tick: '$pepe', + address, + amt: '9000', + }, + }, + { inscription_number: 2 } + ) .build() ); - // Attempt to transfer the same inscription back to the original address to change its - // balance. await db.updateInscriptions( new TestChainhookPayloadBuilder() .apply() .block({ - height: BRC20_GENESIS_BLOCK + 4, + height: BRC20_SELF_MINT_ACTIVATION_BLOCK + 3, hash: '00000000000000000003feae13d107f0f2c4fb4dd08fb2a8b1ab553512e77f03', }) .transaction({ - hash: '55bec906eadc9f5c120cc39555ba46e85e562eacd6217e4dd0b8552783286d0e', - }) - .inscriptionTransferred({ - ordinal_number: 2, - destination: { type: 'transferred', value: address }, - satpoint_pre_transfer: - '7edaa48337a94da327b6262830505f116775a32db5ad4ad46e87ecea33f21bac:0:0', - satpoint_post_transfer: - '55bec906eadc9f5c120cc39555ba46e85e562eacd6217e4dd0b8552783286d0e:0:0', - post_transfer_output_value: null, - tx_index: 0, + hash: '7edaa48337a94da327b6262830505f116775a32db5ad4ad46e87ecea33f21bac', }) + .brc20( + { + transfer_send: { + inscription_id: + 'eee52b22397ea4a4aefe6a39931315e93a157091f5a994216c0aa9c8c6fef47ai0', + tick: '$pepe', + amt: '9000', + sender_address: address, + receiver_address: address2, + }, + }, + { inscription_number: 2 } + ) .build() ); - // Balances only reflect the first transfer. const response1 = await fastify.inject({ method: 'GET', url: `/ordinals/brc-20/balances/${address}`, @@ -2613,7 +941,7 @@ describe('BRC-20', () => { { available_balance: '1000.000000000000000000', overall_balance: '1000.000000000000000000', - ticker: 'PEPE', + ticker: '$pepe', transferrable_balance: '0.000000000000000000', }, ]); @@ -2629,7 +957,7 @@ describe('BRC-20', () => { { available_balance: '9000.000000000000000000', overall_balance: '9000.000000000000000000', - ticker: 'PEPE', + ticker: '$pepe', transferrable_balance: '0.000000000000000000', }, ]); @@ -2637,7 +965,7 @@ describe('BRC-20', () => { test('explicit transfer to self restores balance correctly', async () => { const address = 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td'; - await deployAndMintPEPE(address); + await deployAndMintPEPE(db, address); const address2 = 'bc1ph8dp3lqhzpjphqcc3ucgsm7k3w4d74uwfpv8sv893kn3kpkqrdxqqy3cv6'; await db.updateInscriptions( new TestChainhookPayloadBuilder() @@ -2649,19 +977,17 @@ describe('BRC-20', () => { .transaction({ hash: '825a25b64b5d99ca30e04e53cc9a3020412e1054eb2a7523eb075ddd6d983205', }) - .inscriptionRevealed( - brc20Reveal({ - json: { - p: 'brc-20', - op: 'transfer', - tick: 'PEPE', + .brc20( + { + transfer: { + inscription_id: + '825a25b64b5d99ca30e04e53cc9a3020412e1054eb2a7523eb075ddd6d983205i0', + tick: 'pepe', + address, amt: '20', }, - number: 2, - ordinal_number: 2, - tx_id: '825a25b64b5d99ca30e04e53cc9a3020412e1054eb2a7523eb075ddd6d983205', - address: address, - }) + }, + { inscription_number: 2 } ) .build() ); @@ -2675,16 +1001,19 @@ describe('BRC-20', () => { .transaction({ hash: '486815e61723d03af344e1256d7e0c028a8e9e71eb38157f4bf069eb94292ee1', }) - .inscriptionTransferred({ - ordinal_number: 2, - destination: { type: 'transferred', value: address2 }, - satpoint_pre_transfer: - '825a25b64b5d99ca30e04e53cc9a3020412e1054eb2a7523eb075ddd6d983205:0:0', - satpoint_post_transfer: - '486815e61723d03af344e1256d7e0c028a8e9e71eb38157f4bf069eb94292ee1:0:0', - post_transfer_output_value: null, - tx_index: 0, - }) + .brc20( + { + transfer_send: { + inscription_id: + '825a25b64b5d99ca30e04e53cc9a3020412e1054eb2a7523eb075ddd6d983205i0', + tick: 'pepe', + amt: '20', + sender_address: address, + receiver_address: address2, + }, + }, + { inscription_number: 2 } + ) .build() ); let response = await fastify.inject({ @@ -2695,7 +1024,7 @@ describe('BRC-20', () => { { available_balance: '20.000000000000000000', overall_balance: '20.000000000000000000', - ticker: 'PEPE', + ticker: 'pepe', transferrable_balance: '0.000000000000000000', }, ]); @@ -2709,19 +1038,17 @@ describe('BRC-20', () => { .transaction({ hash: '09a812f72275892b4858880cf3821004a6e8885817159b340639afe9952ac053', }) - .inscriptionRevealed( - brc20Reveal({ - json: { - p: 'brc-20', - op: 'transfer', - tick: 'PEPE', + .brc20( + { + transfer: { + inscription_id: + '09a812f72275892b4858880cf3821004a6e8885817159b340639afe9952ac053i0', + tick: 'pepe', + address: address2, amt: '20', }, - number: 3, - ordinal_number: 3, - tx_id: '09a812f72275892b4858880cf3821004a6e8885817159b340639afe9952ac053', - address: address2, - }) + }, + { inscription_number: 3 } ) .build() ); @@ -2733,7 +1060,7 @@ describe('BRC-20', () => { { available_balance: '0.000000000000000000', overall_balance: '20.000000000000000000', - ticker: 'PEPE', + ticker: 'pepe', transferrable_balance: '20.000000000000000000', }, ]); @@ -2747,16 +1074,19 @@ describe('BRC-20', () => { .transaction({ hash: '26c0c3acbb1c87e682ade86220ba06e649d7599ecfc49a71495f1bdd04efbbb4', }) - .inscriptionTransferred({ - ordinal_number: 3, - destination: { type: 'transferred', value: address2 }, - satpoint_pre_transfer: - '486815e61723d03af344e1256d7e0c028a8e9e71eb38157f4bf069eb94292ee1:0:0', - satpoint_post_transfer: - '26c0c3acbb1c87e682ade86220ba06e649d7599ecfc49a71495f1bdd04efbbb4:0:0', - post_transfer_output_value: null, - tx_index: 0, - }) + .brc20( + { + transfer_send: { + inscription_id: + '09a812f72275892b4858880cf3821004a6e8885817159b340639afe9952ac053i0', + tick: 'pepe', + amt: '20', + sender_address: address2, + receiver_address: address2, + }, + }, + { inscription_number: 3 } + ) .build() ); response = await fastify.inject({ @@ -2767,1402 +1097,20 @@ describe('BRC-20', () => { { available_balance: '20.000000000000000000', overall_balance: '20.000000000000000000', - ticker: 'PEPE', + ticker: 'pepe', transferrable_balance: '0.000000000000000000', }, ]); }); }); - describe('routes', () => { - describe('/brc-20/tokens', () => { - test('tokens endpoint', async () => { - await db.updateInscriptions( - new TestChainhookPayloadBuilder() - .apply() - .block({ height: BRC20_GENESIS_BLOCK }) - .transaction({ - hash: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', - }) - .inscriptionRevealed( - brc20Reveal({ - json: { - p: 'brc-20', - op: 'deploy', - tick: 'PEPE', - max: '21000000', - }, - number: 0, - ordinal_number: 0, - tx_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', - address: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td', - }) - ) - .build() - ); - const response = await fastify.inject({ - method: 'GET', - url: `/ordinals/brc-20/tokens/PEPE`, - }); - expect(response.statusCode).toBe(200); - expect(response.json()).toStrictEqual({ - token: { - id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', - number: 0, - block_height: BRC20_GENESIS_BLOCK, - tx_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', - address: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td', - ticker: 'PEPE', - max_supply: '21000000.000000000000000000', - mint_limit: null, - decimals: 18, - deploy_timestamp: 1677803510000, - minted_supply: '0.000000000000000000', - tx_count: 1, - self_mint: false, - }, - supply: { - max_supply: '21000000.000000000000000000', - minted_supply: '0.000000000000000000', - holders: 0, - }, - }); - }); - - test('tokens filter by ticker prefix', async () => { - const inscriptionNumbers = incrementing(0); - const blockHeights = incrementing(BRC20_GENESIS_BLOCK); - - let transferHash = randomHash(); - let number = inscriptionNumbers.next().value; - await db.updateInscriptions( - new TestChainhookPayloadBuilder() - .apply() - .block({ height: blockHeights.next().value }) - .transaction({ hash: transferHash }) - .inscriptionRevealed( - brc20Reveal({ - json: { - p: 'brc-20', - op: 'deploy', - tick: 'PEPE', - max: '21000000', - }, - number: number, - ordinal_number: number, - tx_id: transferHash, - address: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td', - }) - ) - .build() - ); - - transferHash = randomHash(); - number = inscriptionNumbers.next().value; - await db.updateInscriptions( - new TestChainhookPayloadBuilder() - .apply() - .block({ height: blockHeights.next().value }) - .transaction({ hash: transferHash }) - .inscriptionRevealed( - brc20Reveal({ - json: { - p: 'brc-20', - op: 'deploy', - tick: 'PEER', - max: '21000000', - }, - number: number, - ordinal_number: number, - tx_id: transferHash, - address: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td', - }) - ) - .build() - ); - - transferHash = randomHash(); - number = inscriptionNumbers.next().value; - await db.updateInscriptions( - new TestChainhookPayloadBuilder() - .apply() - .block({ height: blockHeights.next().value }) - .transaction({ hash: transferHash }) - .inscriptionRevealed( - brc20Reveal({ - json: { - p: 'brc-20', - op: 'deploy', - tick: 'ABCD', - max: '21000000', - }, - number: number, - ordinal_number: number, - tx_id: transferHash, - address: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td', - }) - ) - .build() - ); - - transferHash = randomHash(); - number = inscriptionNumbers.next().value; - await db.updateInscriptions( - new TestChainhookPayloadBuilder() - .apply() - .block({ height: blockHeights.next().value }) - .transaction({ hash: transferHash }) - .inscriptionRevealed( - brc20Reveal({ - json: { - p: 'brc-20', - op: 'deploy', - tick: 'DCBA', - max: '21000000', - }, - number: number, - ordinal_number: number, - tx_id: transferHash, - address: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td', - }) - ) - .build() - ); - const response = await fastify.inject({ - method: 'GET', - url: `/ordinals/brc-20/tokens?ticker=PE&ticker=AB`, - }); - expect(response.statusCode).toBe(200); - const responseJson = response.json(); - expect(responseJson.total).toBe(3); - expect(responseJson.results).toEqual( - expect.arrayContaining([ - expect.objectContaining({ ticker: 'PEPE' }), - expect.objectContaining({ ticker: 'PEER' }), - expect.objectContaining({ ticker: 'ABCD' }), - ]) - ); - }); - - test('tokens using order_by tx_count', async () => { - // Setup - const inscriptionNumbers = incrementing(0); - const blockHeights = incrementing(BRC20_GENESIS_BLOCK); - const addressA = 'bc1q6uwuet65rm6xvlz7ztw2gvdmmay5uaycu03mqz'; - const addressB = 'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4'; - - // A deploys PEPE - let number = inscriptionNumbers.next().value; - await db.updateInscriptions( - new TestChainhookPayloadBuilder() - .apply() - .block({ height: blockHeights.next().value }) - .transaction({ hash: randomHash() }) - .inscriptionRevealed( - brc20Reveal({ - json: { - p: 'brc-20', - op: 'deploy', - tick: 'PEPE', - max: '21000000', - }, - number: number, - ordinal_number: number, - tx_id: randomHash(), - address: addressA, - }) - ) - .build() - ); - - // A mints 10000 PEPE 10 times (will later be rolled back) - const pepeMints = []; - for (let i = 0; i < 10; i++) { - const txHash = randomHash(); - number = inscriptionNumbers.next().value; - const payload = new TestChainhookPayloadBuilder() - .apply() - .block({ height: blockHeights.next().value }) - .transaction({ hash: txHash }) - .inscriptionRevealed( - brc20Reveal({ - json: { - p: 'brc-20', - op: 'mint', - tick: 'PEPE', - amt: '10000', - }, - number: number, - ordinal_number: number, - tx_id: txHash, - address: addressA, - }) - ) - .build(); - pepeMints.push(payload); - await db.updateInscriptions(payload); - } - - // B deploys ABCD - number = inscriptionNumbers.next().value; - await db.updateInscriptions( - new TestChainhookPayloadBuilder() - .apply() - .block({ height: blockHeights.next().value }) - .transaction({ hash: randomHash() }) - .inscriptionRevealed( - brc20Reveal({ - json: { - p: 'brc-20', - op: 'deploy', - tick: 'ABCD', - max: '21000000', - }, - number: number, - ordinal_number: number, - tx_id: randomHash(), - address: addressB, - }) - ) - .build() - ); - - // B mints 10000 ABCD - number = inscriptionNumbers.next().value; - await db.updateInscriptions( - new TestChainhookPayloadBuilder() - .apply() - .block({ height: blockHeights.next().value }) - .transaction({ hash: randomHash() }) - .inscriptionRevealed( - brc20Reveal({ - json: { - p: 'brc-20', - op: 'mint', - tick: 'ABCD', - amt: '10000', - }, - number, - ordinal_number: number, - tx_id: randomHash(), - address: addressB, - }) - ) - .build() - ); - - // B send 1000 ABCD to A - // (create inscription, transfer) - const txHashTransfer = randomHash(); - number = inscriptionNumbers.next().value; - const payloadTransfer = new TestChainhookPayloadBuilder() - .apply() - .block({ height: blockHeights.next().value }) - .transaction({ hash: txHashTransfer }) - .inscriptionRevealed( - brc20Reveal({ - json: { - p: 'brc-20', - op: 'transfer', - tick: 'ABCD', - amt: '1000', - }, - number, - ordinal_number: number, - tx_id: txHashTransfer, - address: addressB, - }) - ) - .build(); - await db.updateInscriptions(payloadTransfer); - // (send inscription, transfer_send) - const txHashTransferSend = randomHash(); - const payloadTransferSend = new TestChainhookPayloadBuilder() - .apply() - .block({ height: blockHeights.next().value }) - .transaction({ hash: txHashTransferSend }) - .inscriptionTransferred({ - ordinal_number: number, - destination: { type: 'transferred', value: addressA }, - satpoint_pre_transfer: `${txHashTransfer}:0:0`, - satpoint_post_transfer: `${txHashTransferSend}:0:0`, - post_transfer_output_value: null, - tx_index: 0, - }) - .build(); - await db.updateInscriptions(payloadTransferSend); - - let response = await fastify.inject({ - method: 'GET', - url: `/ordinals/brc-20/tokens`, - }); - expect(response.statusCode).toBe(200); - let json = response.json(); - expect(json.total).toBe(2); - expect(json.results).toHaveLength(2); - - // WITHOUT tx_count sort: - expect(json.results).toEqual([ - // The first result is the token with the latest activity (ABCD) - expect.objectContaining({ - ticker: 'ABCD', - tx_count: 4, - } as Brc20TokenResponse), - expect.objectContaining({ - ticker: 'PEPE', - tx_count: 11, - } as Brc20TokenResponse), - ]); - - response = await fastify.inject({ - method: 'GET', - url: `/ordinals/brc-20/tokens?order_by=tx_count`, - }); - expect(response.statusCode).toBe(200); - json = response.json(); - expect(json.total).toBe(2); - expect(json.results).toHaveLength(2); - - // WITH tx_count sort: The first result is the most active token (PEPE) - expect(json.results).toEqual([ - expect.objectContaining({ - ticker: 'PEPE', - tx_count: 11, - } as Brc20TokenResponse), - expect.objectContaining({ - ticker: 'ABCD', - tx_count: 4, - } as Brc20TokenResponse), - ]); - - // Rollback PEPE mints - for (const payload of pepeMints) { - const payloadRollback = { ...payload, apply: [], rollback: payload.apply }; - await db.updateInscriptions(payloadRollback); - } - - // WITH tx_count sort: The first result is the most active token (now ABCD) - response = await fastify.inject({ - method: 'GET', - url: `/ordinals/brc-20/tokens?order_by=tx_count`, - }); - expect(response.statusCode).toBe(200); - json = response.json(); - expect(json.total).toBe(2); - expect(json.results).toHaveLength(2); - expect(json.results).toEqual([ - expect.objectContaining({ - ticker: 'ABCD', - tx_count: 4, - } as Brc20TokenResponse), - expect.objectContaining({ - ticker: 'PEPE', - tx_count: 1, // only the deploy remains - } as Brc20TokenResponse), - ]); - - // Rollback ABCD transfer - await db.updateInscriptions({ - ...payloadTransferSend, - apply: [], - rollback: payloadTransferSend.apply, - }); - await db.updateInscriptions({ - ...payloadTransfer, - apply: [], - rollback: payloadTransfer.apply, - }); - - response = await fastify.inject({ - method: 'GET', - url: `/ordinals/brc-20/tokens?order_by=tx_count`, - }); - expect(response.statusCode).toBe(200); - json = response.json(); - expect(json.total).toBe(2); - expect(json.results).toHaveLength(2); - expect(json.results).toEqual([ - expect.objectContaining({ - ticker: 'ABCD', - tx_count: 2, // only the deploy and mint remain - } as Brc20TokenResponse), - expect.objectContaining({ - ticker: 'PEPE', - tx_count: 1, - } as Brc20TokenResponse), - ]); - }); - }); - - describe('/brc-20/activity', () => { - test('activity for token transfers', async () => { - // Setup - const inscriptionNumbers = incrementing(0); - const blockHeights = incrementing(BRC20_GENESIS_BLOCK); - const addressA = 'bc1q6uwuet65rm6xvlz7ztw2gvdmmay5uaycu03mqz'; - const addressB = 'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4'; - - // A deploys PEPE - let number = inscriptionNumbers.next().value; - await db.updateInscriptions( - new TestChainhookPayloadBuilder() - .apply() - .block({ height: blockHeights.next().value }) - .transaction({ hash: randomHash() }) - .inscriptionRevealed( - brc20Reveal({ - json: { - p: 'brc-20', - op: 'deploy', - tick: 'PEPE', - max: '21000000', - }, - number, - ordinal_number: number, - tx_id: randomHash(), - address: addressA, - }) - ) - .build() - ); - - // Verify that the PEPE deploy is in the activity feed - let response = await fastify.inject({ - method: 'GET', - url: `/ordinals/brc-20/activity?ticker=PEPE`, - }); - expect(response.statusCode).toBe(200); - let json = response.json(); - expect(json.total).toBe(1); - expect(json.results).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - operation: 'deploy', - ticker: 'PEPE', - address: addressA, - deploy: expect.objectContaining({ - max_supply: '21000000.000000000000000000', - }), - } as Brc20ActivityResponse), - ]) - ); - - // A mints 10000 PEPE - number = inscriptionNumbers.next().value; - await db.updateInscriptions( - new TestChainhookPayloadBuilder() - .apply() - .block({ height: blockHeights.next().value }) - .transaction({ hash: randomHash() }) - .inscriptionRevealed( - brc20Reveal({ - json: { - p: 'brc-20', - op: 'mint', - tick: 'PEPE', - amt: '10000', - }, - number, - ordinal_number: number, - tx_id: randomHash(), - address: addressA, - }) - ) - .build() - ); - - response = await fastify.inject({ - method: 'GET', - url: `/ordinals/brc-20/activity?ticker=PEPE`, - }); - expect(response.statusCode).toBe(200); - json = response.json(); - expect(json.total).toBe(2); - expect(json.results).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - operation: 'deploy', - ticker: 'PEPE', - } as Brc20ActivityResponse), - expect.objectContaining({ - operation: 'mint', - ticker: 'PEPE', - address: addressA, - mint: { - amount: '10000.000000000000000000', - }, - } as Brc20ActivityResponse), - ]) - ); - - // B mints 10000 PEPE - number = inscriptionNumbers.next().value; - await db.updateInscriptions( - new TestChainhookPayloadBuilder() - .apply() - .block({ height: blockHeights.next().value }) - .transaction({ hash: randomHash() }) - .inscriptionRevealed( - brc20Reveal({ - json: { - p: 'brc-20', - op: 'mint', - tick: 'PEPE', - amt: '10000', - }, - number, - ordinal_number: number, - tx_id: randomHash(), - address: addressB, - }) - ) - .build() - ); - - response = await fastify.inject({ - method: 'GET', - url: `/ordinals/brc-20/activity?ticker=PEPE`, - }); - expect(response.statusCode).toBe(200); - json = response.json(); - expect(json.total).toBe(3); - expect(json.results).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - operation: 'mint', - ticker: 'PEPE', - address: addressB, - mint: { - amount: '10000.000000000000000000', - }, - } as Brc20ActivityResponse), - ]) - ); - - // A creates transfer of 9000 PEPE - const transferHash = randomHash(); - number = inscriptionNumbers.next().value; - await db.updateInscriptions( - new TestChainhookPayloadBuilder() - .apply() - .block({ height: blockHeights.next().value }) - .transaction({ hash: transferHash }) - .inscriptionRevealed( - brc20Reveal({ - json: { - p: 'brc-20', - op: 'transfer', - tick: 'PEPE', - amt: '9000', - }, - number, - ordinal_number: number, - tx_id: transferHash, - address: addressA, - }) - ) - .build() - ); - - response = await fastify.inject({ - method: 'GET', - url: `/ordinals/brc-20/activity?ticker=PEPE`, - }); - expect(response.statusCode).toBe(200); - json = response.json(); - expect(json.total).toBe(4); - expect(json.results).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - operation: 'transfer', - ticker: 'PEPE', - address: addressA, - tx_id: transferHash, - transfer: { - amount: '9000.000000000000000000', - from_address: addressA, - }, - } as Brc20ActivityResponse), - ]) - ); - - // A sends transfer inscription to B (aka transfer/sale) - await db.updateInscriptions( - new TestChainhookPayloadBuilder() - .apply() - .block({ height: blockHeights.next().value }) - .transaction({ hash: randomHash() }) - .inscriptionTransferred({ - destination: { type: 'transferred', value: addressB }, - tx_index: 0, - ordinal_number: number, - post_transfer_output_value: null, - satpoint_pre_transfer: `${transferHash}:0:0`, - satpoint_post_transfer: - '7edaa48337a94da327b6262830505f116775a32db5ad4ad46e87ecea33f21bac:0:0', - }) - .build() - ); - - response = await fastify.inject({ - method: 'GET', - url: `/ordinals/brc-20/activity?ticker=PEPE`, - }); - expect(response.statusCode).toBe(200); - json = response.json(); - expect(json.total).toBe(5); - expect(json.results).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - operation: 'transfer_send', - ticker: 'PEPE', - tx_id: expect.not.stringMatching(transferHash), - address: addressB, - transfer_send: { - amount: '9000.000000000000000000', - from_address: addressA, - to_address: addressB, - }, - } as Brc20ActivityResponse), - ]) - ); - - response = await fastify.inject({ - method: 'GET', - url: `/ordinals/brc-20/activity?ticker=PEPE&operation=transfer_send`, - }); - expect(response.statusCode).toBe(200); - json = response.json(); - expect(json.total).toBe(1); - expect(json.results).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - operation: 'transfer_send', - ticker: 'PEPE', - tx_id: expect.not.stringMatching(transferHash), - address: addressB, - transfer_send: { - amount: '9000.000000000000000000', - from_address: addressA, - to_address: addressB, - }, - } as Brc20ActivityResponse), - ]) - ); - }); - - test('activity for multiple token transfers among three participants', async () => { - // Step 1: A deploys a token - // Step 2: A mints 1000 of the token - // Step 3: B mints 2000 of the token - // Step 4: A creates a transfer to B - // Step 5: B creates a transfer to C - // Step 6: A transfer_send the transfer to B - // Step 7: B transfer_send the transfer to C - - // Setup - const inscriptionNumbers = incrementing(0); - const blockHeights = incrementing(BRC20_GENESIS_BLOCK); - const addressA = 'bc1q6uwuet65rm6xvlz7ztw2gvdmmay5uaycu03mqz'; - const addressB = 'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4'; - const addressC = 'bc1q9d80h0q5d3f54w7w8c3l2sguf9uset4ydw9xj2'; - - // Step 1: A deploys a token - let number = inscriptionNumbers.next().value; - await db.updateInscriptions( - new TestChainhookPayloadBuilder() - .apply() - .block({ height: blockHeights.next().value }) - .transaction({ hash: randomHash() }) - .inscriptionRevealed( - brc20Reveal({ - json: { - p: 'brc-20', - op: 'deploy', - tick: 'PEPE', - max: '21000000', - }, - number, - ordinal_number: number, - tx_id: randomHash(), - address: addressA, - }) - ) - .build() - ); - - // Verify that the PEPE deploy is in the activity feed - let response = await fastify.inject({ - method: 'GET', - url: `/ordinals/brc-20/activity?ticker=PEPE`, - }); - expect(response.statusCode).toBe(200); - let json = response.json(); - expect(json.total).toBe(1); - expect(json.results).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - operation: 'deploy', - ticker: 'PEPE', - address: addressA, - deploy: expect.objectContaining({ - max_supply: '21000000.000000000000000000', - }), - } as Brc20ActivityResponse), - ]) - ); - - // Step 2: A mints 1000 of the token - number = inscriptionNumbers.next().value; - await db.updateInscriptions( - new TestChainhookPayloadBuilder() - .apply() - .block({ height: blockHeights.next().value }) - .transaction({ hash: randomHash() }) - .inscriptionRevealed( - brc20Reveal({ - json: { - p: 'brc-20', - op: 'mint', - tick: 'PEPE', - amt: '1000', - }, - number, - ordinal_number: number, - tx_id: randomHash(), - address: addressA, - }) - ) - .build() - ); - - // Verify that the PEPE mint is in the activity feed - response = await fastify.inject({ - method: 'GET', - url: `/ordinals/brc-20/activity?ticker=PEPE`, - }); - expect(response.statusCode).toBe(200); - json = response.json(); - expect(json.total).toBe(2); - expect(json.results).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - operation: 'mint', - ticker: 'PEPE', - address: addressA, - mint: { - amount: '1000.000000000000000000', - }, - } as Brc20ActivityResponse), - ]) - ); - response = await fastify.inject({ - method: 'GET', - url: `/ordinals/brc-20/activity?ticker=PEPE&address=${addressA}`, - }); - expect(response.statusCode).toBe(200); - json = response.json(); - expect(json.total).toBe(2); - expect(json.results).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - operation: 'deploy', - ticker: 'PEPE', - address: addressA, - deploy: expect.objectContaining({ - max_supply: '21000000.000000000000000000', - }), - } as Brc20ActivityResponse), - expect.objectContaining({ - operation: 'mint', - ticker: 'PEPE', - address: addressA, - mint: { - amount: '1000.000000000000000000', - }, - } as Brc20ActivityResponse), - ]) - ); - - // Step 3: B mints 2000 of the token - number = inscriptionNumbers.next().value; - await db.updateInscriptions( - new TestChainhookPayloadBuilder() - .apply() - .block({ height: blockHeights.next().value }) - .transaction({ hash: randomHash() }) - .inscriptionRevealed( - brc20Reveal({ - json: { - p: 'brc-20', - op: 'mint', - tick: 'PEPE', - amt: '2000', - }, - number, - ordinal_number: number, - tx_id: randomHash(), - address: addressB, - }) - ) - .build() - ); - - // Verify that the PEPE mint is in the activity feed - response = await fastify.inject({ - method: 'GET', - url: `/ordinals/brc-20/activity?ticker=PEPE`, - }); - expect(response.statusCode).toBe(200); - json = response.json(); - expect(json.total).toBe(3); - expect(json.results).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - operation: 'mint', - ticker: 'PEPE', - address: addressB, - mint: { - amount: '2000.000000000000000000', - }, - } as Brc20ActivityResponse), - ]) - ); - - // Step 4: A creates a transfer to B - const transferHashAB = randomHash(); - const numberAB = inscriptionNumbers.next().value; - await db.updateInscriptions( - new TestChainhookPayloadBuilder() - .apply() - .block({ height: blockHeights.next().value }) - .transaction({ hash: transferHashAB }) - .inscriptionRevealed( - brc20Reveal({ - json: { - p: 'brc-20', - op: 'transfer', - tick: 'PEPE', - amt: '1000', - }, - number: numberAB, - ordinal_number: numberAB, - tx_id: transferHashAB, - address: addressA, - }) - ) - .build() - ); - - // Verify that the PEPE transfer is in the activity feed - response = await fastify.inject({ - method: 'GET', - url: `/ordinals/brc-20/activity?ticker=PEPE`, - }); - expect(response.statusCode).toBe(200); - json = response.json(); - expect(json.total).toBe(4); - expect(json.results).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - operation: 'transfer', - ticker: 'PEPE', - address: addressA, - tx_id: transferHashAB, - transfer: { - amount: '1000.000000000000000000', - from_address: addressA, - }, - } as Brc20ActivityResponse), - ]) - ); - response = await fastify.inject({ - method: 'GET', - url: `/ordinals/brc-20/activity?ticker=PEPE&address=${addressA}`, - }); - expect(response.statusCode).toBe(200); - json = response.json(); - expect(json.total).toBe(3); - expect(json.results).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - operation: 'transfer', - ticker: 'PEPE', - address: addressA, - tx_id: transferHashAB, - transfer: { - amount: '1000.000000000000000000', - from_address: addressA, - }, - } as Brc20ActivityResponse), - ]) - ); - - // Step 5: B creates a transfer to C - const transferHashBC = randomHash(); - const numberBC = inscriptionNumbers.next().value; - await db.updateInscriptions( - new TestChainhookPayloadBuilder() - .apply() - .block({ height: blockHeights.next().value }) - .transaction({ hash: transferHashBC }) - .inscriptionRevealed( - brc20Reveal({ - json: { - p: 'brc-20', - op: 'transfer', - tick: 'PEPE', - amt: '2000', - }, - number: numberBC, - ordinal_number: numberBC, - tx_id: transferHashBC, - address: addressB, - }) - ) - .build() - ); - - // Verify that the PEPE transfer is in the activity feed - response = await fastify.inject({ - method: 'GET', - url: `/ordinals/brc-20/activity?ticker=PEPE`, - }); - expect(response.statusCode).toBe(200); - json = response.json(); - expect(json.total).toBe(5); - expect(json.results).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - operation: 'transfer', - ticker: 'PEPE', - address: addressB, - tx_id: transferHashBC, - transfer: { - amount: '2000.000000000000000000', - from_address: addressB, - }, - } as Brc20ActivityResponse), - ]) - ); - - // Step 6: A transfer_send the transfer to B - const transferHashABSend = randomHash(); - await db.updateInscriptions( - new TestChainhookPayloadBuilder() - .apply() - .block({ height: blockHeights.next().value }) - .transaction({ hash: transferHashABSend }) - .inscriptionTransferred({ - destination: { type: 'transferred', value: addressB }, - tx_index: 0, - ordinal_number: numberAB, - post_transfer_output_value: null, - satpoint_pre_transfer: `${transferHashAB}:0:0`, - satpoint_post_transfer: `${transferHashABSend}:0:0`, - }) - .build() - ); - // A gets the transfer send in its feed - response = await fastify.inject({ - method: 'GET', - url: `/ordinals/brc-20/activity?ticker=PEPE&address=${addressA}`, - }); - expect(response.statusCode).toBe(200); - json = response.json(); - expect(json.total).toBe(4); - expect(json.results).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - operation: 'transfer_send', - ticker: 'PEPE', - tx_id: expect.not.stringMatching(transferHashAB), - address: addressB, - transfer_send: { - amount: '1000.000000000000000000', - from_address: addressA, - to_address: addressB, - }, - } as Brc20ActivityResponse), - ]) - ); - // B gets the transfer send in its feed - response = await fastify.inject({ - method: 'GET', - url: `/ordinals/brc-20/activity?ticker=PEPE&address=${addressB}`, - }); - expect(response.statusCode).toBe(200); - json = response.json(); - expect(json.total).toBe(3); - expect(json.results).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - operation: 'transfer_send', - ticker: 'PEPE', - tx_id: expect.not.stringMatching(transferHashAB), - address: addressB, - transfer_send: { - amount: '1000.000000000000000000', - from_address: addressA, - to_address: addressB, - }, - } as Brc20ActivityResponse), - ]) - ); - - // Verify that the PEPE transfer_send is in the activity feed - response = await fastify.inject({ - method: 'GET', - url: `/ordinals/brc-20/activity?ticker=PEPE`, - }); - expect(response.statusCode).toBe(200); - json = response.json(); - expect(json.total).toBe(6); - expect(json.results).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - operation: 'transfer_send', - ticker: 'PEPE', - tx_id: expect.not.stringMatching(transferHashAB), - address: addressB, - transfer_send: { - amount: '1000.000000000000000000', - from_address: addressA, - to_address: addressB, - }, - } as Brc20ActivityResponse), - ]) - ); - - // Step 7: B transfer_send the transfer to C - const transferHashBCSend = randomHash(); - await db.updateInscriptions( - new TestChainhookPayloadBuilder() - .apply() - .block({ height: blockHeights.next().value }) - .transaction({ hash: transferHashBCSend }) - .inscriptionTransferred({ - destination: { type: 'transferred', value: addressC }, - tx_index: 0, - ordinal_number: numberBC, - post_transfer_output_value: null, - satpoint_pre_transfer: `${transferHashBC}:0:0`, - satpoint_post_transfer: `${transferHashBCSend}:0:0`, - }) - .build() - ); - - // Verify that the PEPE transfer_send is in the activity feed - response = await fastify.inject({ - method: 'GET', - url: `/ordinals/brc-20/activity?ticker=PEPE`, - }); - expect(response.statusCode).toBe(200); - json = response.json(); - expect(json.total).toBe(7); - expect(json.results).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - operation: 'transfer_send', - ticker: 'PEPE', - tx_id: expect.not.stringMatching(transferHashBC), - address: addressC, - transfer_send: { - amount: '2000.000000000000000000', - from_address: addressB, - to_address: addressC, - }, - } as Brc20ActivityResponse), - ]) - ); - // B gets the transfer send in its feed - response = await fastify.inject({ - method: 'GET', - url: `/ordinals/brc-20/activity?ticker=PEPE&address=${addressB}`, - }); - expect(response.statusCode).toBe(200); - json = response.json(); - expect(json.total).toBe(4); - expect(json.results).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - operation: 'transfer_send', - ticker: 'PEPE', - tx_id: expect.not.stringMatching(transferHashBC), - address: addressC, - transfer_send: { - amount: '2000.000000000000000000', - from_address: addressB, - to_address: addressC, - }, - } as Brc20ActivityResponse), - ]) - ); - // C gets the transfer send in its feed - response = await fastify.inject({ - method: 'GET', - url: `/ordinals/brc-20/activity?ticker=PEPE&address=${addressC}`, - }); - expect(response.statusCode).toBe(200); - json = response.json(); - expect(json.total).toBe(1); - expect(json.results).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - operation: 'transfer_send', - ticker: 'PEPE', - tx_id: expect.not.stringMatching(transferHashBC), - address: addressC, - transfer_send: { - amount: '2000.000000000000000000', - from_address: addressB, - to_address: addressC, - }, - } as Brc20ActivityResponse), - ]) - ); - }); - - test('activity for multiple token creation', async () => { - const inscriptionNumbers = incrementing(0); - const blockHeights = incrementing(BRC20_GENESIS_BLOCK); - const addressA = 'bc1q6uwuet65rm6xvlz7ztw2gvdmmay5uaycu03mqz'; - - // Step 1: Create a token PEPE - let number = inscriptionNumbers.next().value; - await db.updateInscriptions( - new TestChainhookPayloadBuilder() - .apply() - .block({ height: blockHeights.next().value }) - .transaction({ hash: randomHash() }) - .inscriptionRevealed( - brc20Reveal({ - json: { - p: 'brc-20', - op: 'deploy', - tick: 'PEPE', - max: '21000000', - }, - number, - ordinal_number: number, - tx_id: randomHash(), - address: addressA, - }) - ) - .build() - ); - - // Verify that the PEPE deploy is in the activity feed - let response = await fastify.inject({ - method: 'GET', - url: `/ordinals/brc-20/activity`, - }); - expect(response.statusCode).toBe(200); - let json = response.json(); - expect(json.total).toBe(1); - expect(json.results).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - operation: 'deploy', - ticker: 'PEPE', - address: addressA, - deploy: expect.objectContaining({ - max_supply: '21000000.000000000000000000', - }), - } as Brc20ActivityResponse), - ]) - ); - - // Step 2: Create a token PEER - number = inscriptionNumbers.next().value; - await db.updateInscriptions( - new TestChainhookPayloadBuilder() - .apply() - .block({ height: blockHeights.next().value }) - .transaction({ hash: randomHash() }) - .inscriptionRevealed( - brc20Reveal({ - json: { - p: 'brc-20', - op: 'deploy', - tick: 'PEER', - max: '21000000', - }, - number, - ordinal_number: number, - tx_id: randomHash(), - address: addressA, - }) - ) - .build() - ); - - // Verify that the PEER deploy is in the activity feed - response = await fastify.inject({ - method: 'GET', - url: `/ordinals/brc-20/activity`, - }); - expect(response.statusCode).toBe(200); - json = response.json(); - expect(json.total).toBe(2); - expect(json.results).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - operation: 'deploy', - ticker: 'PEER', - address: addressA, - deploy: expect.objectContaining({ - max_supply: '21000000.000000000000000000', - }), - } as Brc20ActivityResponse), - ]) - ); - - // Verify that no events are available before the first block height - response = await fastify.inject({ - method: 'GET', - url: `/ordinals/brc-20/activity?ticker=PEER&block_height=${BRC20_GENESIS_BLOCK}`, - }); - expect(response.statusCode).toBe(200); - json = response.json(); - expect(json.total).toBe(0); - expect(json.results).toEqual([]); - - // Verify that the PEER deploy is not in the activity feed when using block_height parameter - response = await fastify.inject({ - method: 'GET', - url: `/ordinals/brc-20/activity?block_height=${BRC20_GENESIS_BLOCK}`, - }); - expect(response.statusCode).toBe(200); - json = response.json(); - expect(json.total).toBe(1); - expect(json.results).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - operation: 'deploy', - ticker: 'PEPE', - address: addressA, - deploy: expect.objectContaining({ - max_supply: '21000000.000000000000000000', - }), - } as Brc20ActivityResponse), - ]) - ); - // Should NOT include PEER at this block height - expect(json.results).not.toEqual( - expect.arrayContaining([ - expect.objectContaining({ - ticker: 'PEER', - } as Brc20ActivityResponse), - ]) - ); - }); - }); - - describe('/brc-20/token/holders', () => { - test('displays holders for token', async () => { - const address = 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td'; - await deployAndMintPEPE(address); - await db.updateInscriptions( - new TestChainhookPayloadBuilder() - .apply() - .block({ - height: BRC20_GENESIS_BLOCK + 2, - hash: '0000000000000000000034dd2daec375371800da441b17651459b2220cbc1a6e', - }) - .transaction({ - hash: '633648e0e1ddcab8dea0496a561f2b08c486ae619b5634d7bb55d7f0cd32ef16', - }) - .inscriptionRevealed( - brc20Reveal({ - json: { - p: 'brc-20', - op: 'mint', - tick: 'PEPE', - amt: '2000', - }, - number: 2, - ordinal_number: 2, - tx_id: '633648e0e1ddcab8dea0496a561f2b08c486ae619b5634d7bb55d7f0cd32ef16', - address: 'bc1qp9jgp9qtlhgvwjnxclj6kav6nr2fq09c206pyl', - }) - ) - .build() - ); - - const response = await fastify.inject({ - method: 'GET', - url: `/ordinals/brc-20/tokens/PEPE/holders`, - }); - expect(response.statusCode).toBe(200); - const json = response.json(); - expect(json.total).toBe(2); - expect(json.results).toStrictEqual([ - { - address: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td', - overall_balance: '10000.000000000000000000', - }, - { - address: 'bc1qp9jgp9qtlhgvwjnxclj6kav6nr2fq09c206pyl', - overall_balance: '2000.000000000000000000', - }, - ]); - }); - - test('shows empty list on token with no holders', async () => { - 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: '250000', - }, - number: 0, - ordinal_number: 0, - tx_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', - address: 'bc1qp9jgp9qtlhgvwjnxclj6kav6nr2fq09c206pyl', - }) - ) - .build() - ); - const response = await fastify.inject({ - method: 'GET', - url: `/ordinals/brc-20/tokens/PEPE/holders`, - }); - expect(response.statusCode).toBe(200); - const json = response.json(); - expect(json.total).toBe(0); - expect(json.results).toStrictEqual([]); - }); - - test('shows 404 on token not found', async () => { - const response = await fastify.inject({ - method: 'GET', - url: `/ordinals/brc-20/tokens/PEPE/holders`, - }); - expect(response.statusCode).toBe(404); - }); - }); - }); - describe('rollbacks', () => { test('reflects rollbacks on balances and counts correctly', async () => { const address = 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td'; const address2 = '3QNjwPDRafjBm9XxJpshgk3ksMJh3TFxTU'; - await deployAndMintPEPE(address); + await deployAndMintPEPE(db, address); - // Transfer and send PEPE + // Transfer and send pepe const transferPEPE = new TestChainhookPayloadBuilder() .apply() .block({ @@ -4172,19 +1120,16 @@ describe('BRC-20', () => { .transaction({ hash: 'eee52b22397ea4a4aefe6a39931315e93a157091f5a994216c0aa9c8c6fef47a', }) - .inscriptionRevealed( - brc20Reveal({ - json: { - p: 'brc-20', - op: 'transfer', - tick: 'PEPE', + .brc20( + { + transfer: { + inscription_id: 'eee52b22397ea4a4aefe6a39931315e93a157091f5a994216c0aa9c8c6fef47ai0', + tick: 'pepe', + address, amt: '9000', }, - number: 2, - ordinal_number: 2, - tx_id: 'eee52b22397ea4a4aefe6a39931315e93a157091f5a994216c0aa9c8c6fef47a', - address: address, - }) + }, + { inscription_number: 2 } ) .build(); await db.updateInscriptions(transferPEPE); @@ -4197,16 +1142,18 @@ describe('BRC-20', () => { .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, - }) + .brc20( + { + transfer_send: { + inscription_id: 'eee52b22397ea4a4aefe6a39931315e93a157091f5a994216c0aa9c8c6fef47ai0', + tick: 'pepe', + amt: '9000', + sender_address: address, + receiver_address: address2, + }, + }, + { inscription_number: 2 } + ) .build(); await db.updateInscriptions(sendPEPE); // Deploy and mint 🔥 token @@ -4219,19 +1166,19 @@ describe('BRC-20', () => { .transaction({ hash: '8354e85e87fa2df8b3a06ec0b9d395559b95174530cb19447fc4df5f6d4ca84d', }) - .inscriptionRevealed( - brc20Reveal({ - json: { - p: 'brc-20', - op: 'deploy', + .brc20( + { + deploy: { + inscription_id: '8354e85e87fa2df8b3a06ec0b9d395559b95174530cb19447fc4df5f6d4ca84di0', tick: '🔥', max: '1000', + lim: '1000', + dec: '18', + address, + self_mint: false, }, - number: 3, - ordinal_number: 3, - tx_id: '8354e85e87fa2df8b3a06ec0b9d395559b95174530cb19447fc4df5f6d4ca84d', - address: address, - }) + }, + { inscription_number: 3 } ) .build(); await db.updateInscriptions(deployFIRE); @@ -4244,19 +1191,16 @@ describe('BRC-20', () => { .transaction({ hash: '81f4ee2c247c5f5c0d3a6753fef706df410ea61c2aa6d370003b98beb041b887', }) - .inscriptionRevealed( - brc20Reveal({ - json: { - p: 'brc-20', - op: 'mint', + .brc20( + { + mint: { + inscription_id: '81f4ee2c247c5f5c0d3a6753fef706df410ea61c2aa6d370003b98beb041b887i0', tick: '🔥', + address, amt: '500', }, - number: 4, - ordinal_number: 4, - tx_id: '81f4ee2c247c5f5c0d3a6753fef706df410ea61c2aa6d370003b98beb041b887', - address: address, - }) + }, + { inscription_number: 4 } ) .build(); await db.updateInscriptions(mintFIRE); @@ -4270,19 +1214,16 @@ describe('BRC-20', () => { .transaction({ hash: 'c1c7f1d5c10a30605a8a5285ca3465a4f75758ed9b7f201e5ef62727e179966f', }) - .inscriptionRevealed( - brc20Reveal({ - json: { - p: 'brc-20', - op: 'transfer', + .brc20( + { + transfer: { + inscription_id: 'c1c7f1d5c10a30605a8a5285ca3465a4f75758ed9b7f201e5ef62727e179966fi0', tick: '🔥', + address, amt: '100', }, - number: 5, - ordinal_number: 5, - tx_id: 'c1c7f1d5c10a30605a8a5285ca3465a4f75758ed9b7f201e5ef62727e179966f', - address: address, - }) + }, + { inscription_number: 5 } ) .build(); await db.updateInscriptions(transferFIRE); @@ -4295,16 +1236,18 @@ describe('BRC-20', () => { .transaction({ hash: 'a00d01a3e772ce2219ddf3fe2fe4053be071262d9594f11f018fdada7179ae2d', }) - .inscriptionTransferred({ - ordinal_number: 5, - destination: { type: 'transferred', value: address }, // To self - satpoint_pre_transfer: - 'c1c7f1d5c10a30605a8a5285ca3465a4f75758ed9b7f201e5ef62727e179966f:0:0', - satpoint_post_transfer: - 'a00d01a3e772ce2219ddf3fe2fe4053be071262d9594f11f018fdada7179ae2d:0:0', - post_transfer_output_value: null, - tx_index: 0, - }) + .brc20( + { + transfer_send: { + tick: '🔥', + inscription_id: 'c1c7f1d5c10a30605a8a5285ca3465a4f75758ed9b7f201e5ef62727e179966fi0', + amt: '100', + sender_address: address, + receiver_address: address, + }, + }, + { inscription_number: 5 } + ) .build(); await db.updateInscriptions(sendFIRE); @@ -4319,7 +1262,7 @@ describe('BRC-20', () => { expect(json.results[1].minted_supply).toBe('10000.000000000000000000'); request = await fastify.inject({ method: 'GET', - url: `/ordinals/brc-20/tokens/PEPE`, + url: `/ordinals/brc-20/tokens/pepe`, }); json = request.json(); expect(json.supply.holders).toBe(2); @@ -4337,7 +1280,7 @@ describe('BRC-20', () => { expect(json.total).toBe(2); expect(json.results).toHaveLength(2); expect(json.results[0]).toStrictEqual({ - ticker: 'PEPE', + ticker: 'pepe', available_balance: '1000.000000000000000000', transferrable_balance: '0.000000000000000000', overall_balance: '1000.000000000000000000', @@ -4356,7 +1299,7 @@ describe('BRC-20', () => { expect(json.total).toBe(1); expect(json.results).toHaveLength(1); expect(json.results[0]).toStrictEqual({ - ticker: 'PEPE', + ticker: 'pepe', available_balance: '9000.000000000000000000', transferrable_balance: '0.000000000000000000', overall_balance: '9000.000000000000000000', @@ -4451,7 +1394,7 @@ describe('BRC-20', () => { expect(json.total).toBe(1); expect(json.results).toHaveLength(1); expect(json.results[0]).toStrictEqual({ - ticker: 'PEPE', + ticker: 'pepe', available_balance: '1000.000000000000000000', transferrable_balance: '0.000000000000000000', overall_balance: '1000.000000000000000000', @@ -4494,7 +1437,7 @@ describe('BRC-20', () => { expect(json.total).toBe(1); expect(json.results).toHaveLength(1); expect(json.results[0]).toStrictEqual({ - ticker: 'PEPE', + ticker: 'pepe', available_balance: '1000.000000000000000000', transferrable_balance: '0.000000000000000000', overall_balance: '1000.000000000000000000', @@ -4508,7 +1451,7 @@ describe('BRC-20', () => { expect(json.results).toHaveLength(4); expect(json.results[0].operation).toBe('transfer_send'); - // Rollback 3: PEPE is un-sent + // Rollback 3: pepe is un-sent await db.updateInscriptions(rollBack(sendPEPE)); request = await fastify.inject({ method: 'GET', @@ -4518,14 +1461,14 @@ describe('BRC-20', () => { expect(json.total).toBe(1); expect(json.results).toHaveLength(1); expect(json.results[0]).toStrictEqual({ - ticker: 'PEPE', + ticker: 'pepe', available_balance: '1000.000000000000000000', transferrable_balance: '9000.000000000000000000', overall_balance: '10000.000000000000000000', }); request = await fastify.inject({ method: 'GET', - url: `/ordinals/brc-20/tokens/PEPE`, + url: `/ordinals/brc-20/tokens/pepe`, }); json = request.json(); expect(json.supply.holders).toBe(1); @@ -4545,7 +1488,7 @@ describe('BRC-20', () => { expect(json.results).toHaveLength(3); expect(json.results[0].operation).toBe('transfer'); - // Rollback 4: PEPE is un-transferred + // Rollback 4: pepe is un-transferred await db.updateInscriptions(rollBack(transferPEPE)); request = await fastify.inject({ method: 'GET', @@ -4555,14 +1498,14 @@ describe('BRC-20', () => { expect(json.total).toBe(1); expect(json.results).toHaveLength(1); expect(json.results[0]).toStrictEqual({ - ticker: 'PEPE', + ticker: 'pepe', available_balance: '10000.000000000000000000', transferrable_balance: '0.000000000000000000', overall_balance: '10000.000000000000000000', }); request = await fastify.inject({ method: 'GET', - url: `/ordinals/brc-20/tokens/PEPE`, + url: `/ordinals/brc-20/tokens/pepe`, }); json = request.json(); expect(json.supply.holders).toBe(1); diff --git a/tests/helpers.ts b/tests/helpers.ts index f2c70674..84750cf4 100644 --- a/tests/helpers.ts +++ b/tests/helpers.ts @@ -1,14 +1,15 @@ import { TypeBoxTypeProvider } from '@fastify/type-provider-typebox'; import { + BitcoinBrc20Operation, BitcoinEvent, BitcoinInscriptionRevealed, BitcoinInscriptionTransferred, + BitcoinPayload, BitcoinTransaction, - Payload, } from '@hirosystems/chainhook-client'; import { FastifyBaseLogger, FastifyInstance } from 'fastify'; import { IncomingMessage, Server, ServerResponse } from 'http'; -import { Brc20 } from '../src/pg/brc20/helpers'; +import { PgStore } from '../src/pg/pg-store'; export type TestFastifyServer = FastifyInstance< Server, @@ -19,7 +20,7 @@ export type TestFastifyServer = FastifyInstance< >; export class TestChainhookPayloadBuilder { - private payload: Payload = { + private payload: BitcoinPayload = { apply: [], rollback: [], chainhook: { @@ -27,6 +28,7 @@ export class TestChainhookPayloadBuilder { predicate: { scope: 'ordinals_protocol', operation: 'inscription_feed', + meta_protocols: ['brc-20'], }, is_streaming_blocks: true, }, @@ -38,6 +40,7 @@ export class TestChainhookPayloadBuilder { private get lastBlockTx(): BitcoinTransaction { return this.lastBlock.transactions[this.lastBlock.transactions.length - 1]; } + private txIndex = 0; streamingBlocks(streaming: boolean): this { this.payload.chainhook.is_streaming_blocks = streaming; @@ -80,6 +83,7 @@ export class TestChainhookPayloadBuilder { metadata: { ordinal_operations: [], proof: null, + index: this.txIndex++, }, }); return this; @@ -95,12 +99,76 @@ export class TestChainhookPayloadBuilder { return this; } - build(): Payload { + brc20( + args: BitcoinBrc20Operation, + opts: { inscription_number: number; ordinal_number?: number } + ): this { + this.lastBlockTx.metadata.brc20_operation = args; + if ('transfer_send' in args) { + this.lastBlockTx.metadata.ordinal_operations.push({ + inscription_transferred: { + ordinal_number: opts.ordinal_number ?? opts.inscription_number, + destination: { + type: 'transferred', + value: args.transfer_send.receiver_address, + }, + satpoint_pre_transfer: `${args.transfer_send.inscription_id.split('i')[0]}:0:0`, + satpoint_post_transfer: `${this.lastBlockTx.transaction_identifier.hash}:0:0`, + post_transfer_output_value: null, + tx_index: 0, + }, + }); + } else { + let inscription_id = ''; + let inscriber_address = ''; + if ('deploy' in args) { + inscription_id = args.deploy.inscription_id; + inscriber_address = args.deploy.address; + } else if ('mint' in args) { + inscription_id = args.mint.inscription_id; + inscriber_address = args.mint.address; + } else { + inscription_id = args.transfer.inscription_id; + inscriber_address = args.transfer.address; + } + this.lastBlockTx.metadata.ordinal_operations.push({ + inscription_revealed: { + content_bytes: `0x101010`, + content_type: 'text/plain;charset=utf-8', + content_length: 3, + inscription_number: { + jubilee: opts.inscription_number, + classic: opts.inscription_number, + }, + inscription_fee: 2000, + inscription_id, + inscription_output_value: 10000, + inscriber_address, + ordinal_number: opts.ordinal_number ?? opts.inscription_number, + ordinal_block_height: 0, + ordinal_offset: 0, + satpoint_post_inscription: `${inscription_id.split('i')[0]}:0:0`, + inscription_input_index: 0, + transfers_pre_inscription: 0, + tx_index: 0, + curse_type: null, + inscription_pointer: null, + delegate: null, + metaprotocol: null, + metadata: undefined, + parent: null, + }, + }); + } + return this; + } + + build(): BitcoinPayload { return this.payload; } } -export function rollBack(payload: Payload) { +export function rollBack(payload: BitcoinPayload) { return { ...payload, apply: [], @@ -108,45 +176,6 @@ export function rollBack(payload: Payload) { }; } -export function brc20Reveal(args: { - json: Brc20; - number: number; - classic_number?: number; - address: string; - tx_id: string; - ordinal_number: number; - parent?: string; -}): BitcoinInscriptionRevealed { - const content = Buffer.from(JSON.stringify(args.json), 'utf-8'); - const reveal: BitcoinInscriptionRevealed = { - content_bytes: `0x${content.toString('hex')}`, - content_type: 'text/plain;charset=utf-8', - content_length: content.length, - inscription_number: { - jubilee: args.number, - classic: args.classic_number ?? args.number, - }, - inscription_fee: 2000, - inscription_id: `${args.tx_id}i0`, - inscription_output_value: 10000, - inscriber_address: args.address, - ordinal_number: args.ordinal_number, - ordinal_block_height: 0, - ordinal_offset: 0, - satpoint_post_inscription: `${args.tx_id}:0:0`, - inscription_input_index: 0, - transfers_pre_inscription: 0, - tx_index: 0, - curse_type: null, - inscription_pointer: null, - delegate: null, - metaprotocol: null, - metadata: undefined, - parent: args.parent ?? null, - }; - return reveal; -} - /** Generate a random hash like string for testing */ export const randomHash = () => [...Array(64)].map(() => Math.floor(Math.random() * 16).toString(16)).join(''); @@ -163,3 +192,58 @@ export function* incrementing( current += step; } } + +export const BRC20_GENESIS_BLOCK = 779832; +export const BRC20_SELF_MINT_ACTIVATION_BLOCK = 837090; + +export async function deployAndMintPEPE(db: PgStore, address: string) { + await db.updateInscriptions( + new TestChainhookPayloadBuilder() + .apply() + .block({ + height: BRC20_GENESIS_BLOCK, + hash: '00000000000000000002a90330a99f67e3f01eb2ce070b45930581e82fb7a91d', + }) + .transaction({ + hash: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', + }) + .brc20( + { + deploy: { + tick: 'pepe', + max: '250000', + dec: '18', + lim: '250000', + inscription_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', + address, + self_mint: false, + }, + }, + { inscription_number: 0 } + ) + .build() + ); + await db.updateInscriptions( + new TestChainhookPayloadBuilder() + .apply() + .block({ + height: BRC20_GENESIS_BLOCK + 1, + hash: '0000000000000000000098d8f2663891d439f6bb7de230d4e9f6bcc2e85452bf', + }) + .transaction({ + hash: '3b55f624eaa4f8de6c42e0c490176b67123a83094384f658611faf7bfb85dd0f', + }) + .brc20( + { + mint: { + tick: 'pepe', + amt: '10000', + inscription_id: '3b55f624eaa4f8de6c42e0c490176b67123a83094384f658611faf7bfb85dd0fi0', + address, + }, + }, + { inscription_number: 1 } + ) + .build() + ); +}