From 07a512bbe12999d5c4eb8fd869fbdc777731d0db Mon Sep 17 00:00:00 2001 From: Rafael Cardenas Date: Thu, 18 Apr 2024 09:57:46 -0600 Subject: [PATCH 01/13] chore: progress --- 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 - .../1711465842961_brc20-deploy-self-mint.ts | 19 - ...ploys.ts => 1711575178681_brc20-tokens.ts} | 41 +- migrations/1711575178682_brc20-operations.ts | 52 ++ ... => 1711575178683_brc20-total-balances.ts} | 10 +- ...711575178684_brc20-counts-by-operation.ts} | 4 +- ...8686_brc20-counts-by-address-operation.ts} | 14 +- package-lock.json | 33 +- package.json | 2 +- src/pg/brc20/brc20-pg-store.ts | 515 ++++++------------ src/pg/brc20/helpers.ts | 96 ---- src/pg/brc20/types.ts | 91 +--- src/pg/pg-store.ts | 5 +- 25 files changed, 299 insertions(+), 1067 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} (83%) 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%) delete mode 100644 src/pg/brc20/helpers.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/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..d8ebd9d3 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,23 @@ 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: 1, + }, }); - 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']); } diff --git a/migrations/1711575178682_brc20-operations.ts b/migrations/1711575178682_brc20-operations.ts new file mode 100644 index 00000000..023937da --- /dev/null +++ b/migrations/1711575178682_brc20-operations.ts @@ -0,0 +1,52 @@ +/* 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_events', { + genesis_id: { + type: 'string', + notNull: true, + }, + brc20_token_ticker: { + type: 'string', + notNull: true, + }, + block_height: { + type: 'bigint', + notNull: true, + }, + tx_index: { + type: 'bigint', + notNull: true, + }, + address: { + type: 'text', + }, + avail_balance: { + type: 'numeric', + notNull: true, + }, + trans_balance: { + type: 'numeric', + notNull: true, + }, + operation: { + type: 'brc20_operation', + notNull: true, + }, + }); + pgm.createConstraint( + 'brc20_operation', + 'brc20_operation_unique', + 'UNIQUE(genesis_id, operation)' + ); +} diff --git a/migrations/1694021174916_brc20-total-balances.ts b/migrations/1711575178683_brc20-total-balances.ts similarity index 83% rename from migrations/1694021174916_brc20-total-balances.ts rename to migrations/1711575178683_brc20-total-balances.ts index c2d66828..5f03d801 100644 --- a/migrations/1694021174916_brc20-total-balances.ts +++ b/migrations/1711575178683_brc20-total-balances.ts @@ -9,8 +9,8 @@ export function up(pgm: MigrationBuilder): void { type: 'bigserial', primaryKey: true, }, - brc20_deploy_id: { - type: 'bigint', + brc20_token_ticker: { + type: 'string', notNull: true, }, address: { @@ -33,16 +33,16 @@ 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' + 'FOREIGN KEY(brc20_token_ticker) REFERENCES brc20_tokens(ticker) ON DELETE CASCADE' ); pgm.createConstraint( 'brc20_total_balances', 'brc20_total_balances_unique', - 'UNIQUE(brc20_deploy_id, address)' + 'UNIQUE(brc20_token_ticker, address)' ); pgm.createIndex('brc20_total_balances', ['address']); pgm.createIndex('brc20_total_balances', [ - 'brc20_deploy_id', + 'brc20_token_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..4172dece 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": "file:../chainhook/components/client/typescript", "@semantic-release/changelog": "^6.0.3", "@semantic-release/commit-analyzer": "^10.0.4", "@semantic-release/git": "^10.0.1", @@ -98,7 +98,6 @@ "../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", @@ -1299,15 +1298,8 @@ } }, "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==", - "dependencies": { - "@fastify/type-provider-typebox": "^3.2.0", - "fastify": "^4.15.0", - "pino": "^8.11.0", - "undici": "^5.21.2" - } + "resolved": "../chainhook/components/client/typescript", + "link": true }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.7", @@ -19743,13 +19735,26 @@ } }, "@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": "file:../chainhook/components/client/typescript", "requires": { "@fastify/type-provider-typebox": "^3.2.0", + "@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", "fastify": "^4.15.0", + "jest": "^29.5.0", "pino": "^8.11.0", + "prettier": "^2.8.7", + "rimraf": "^4.4.1", + "ts-jest": "^29.0.5", + "ts-node": "^10.9.1", + "typescript": "^5.0.2", "undici": "^5.21.2" } }, diff --git a/package.json b/package.json index 08d55d38..fa9ce8a5 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": "file:../chainhook/components/client/typescript", "@semantic-release/changelog": "^6.0.3", "@semantic-release/commit-analyzer": "^10.0.4", "@semantic-release/git": "^10.0.1", diff --git a/src/pg/brc20/brc20-pg-store.ts b/src/pg/brc20/brc20-pg-store.ts index 8641a620..62aa657d 100644 --- a/src/pg/brc20/brc20-pg-store.ts +++ b/src/pg/brc20/brc20-pg-store.ts @@ -1,14 +1,10 @@ -import { BasePgStoreModule, logger } from '@hirosystems/api-toolkit'; +import { BasePgStoreModule, PgSqlClient } from '@hirosystems/api-toolkit'; import * as postgres from 'postgres'; import { DbInscriptionIndexPaging, InscriptionData, - DbLocationPointerInsert, - DbLocationTransferType, DbPaginatedResult, - InscriptionEventData, LocationData, - InscriptionRevealData, } from '../types'; import { BRC20_DEPLOYS_COLUMNS, @@ -17,7 +13,7 @@ import { DbBrc20Balance, DbBrc20BalanceTypeId, DbBrc20DeployEvent, - DbBrc20DeployInsert, + DbBrc20TokenInsert, DbBrc20Event, DbBrc20EventOperation, DbBrc20Holder, @@ -25,377 +21,182 @@ import { DbBrc20Token, DbBrc20TokenWithSupply, DbBrc20TransferEvent, + DbBrc20OperationInsert, + DbBrc20Operation, } from './types'; -import { Brc20Deploy, Brc20Mint, Brc20Transfer, UINT64_MAX, brc20FromInscription } from './helpers'; import { Brc20TokenOrderBy } from '../../api/schemas'; import { objRemoveUndefinedValues } from '../helpers'; +import { BitcoinEvent } from '@hirosystems/chainhook-client'; +import BigNumber from 'bignumber.js'; -/** The block at which BRC-20 activity began */ -export const BRC20_GENESIS_BLOCK = 779832; +function increaseOperationCount(map: Map, operation: DbBrc20Operation) { + const current = map.get(operation); + if (current == undefined) { + map.set(operation, 1); + } else { + map.set(operation, current + 1); + } +} + +function increaseAddressOperationCount( + map: Map>, + address: string, + operation: DbBrc20Operation +) { + const current = map.get(address); + if (current == undefined) { + const opMap = new Map(); + increaseOperationCount(opMap, operation); + map.set(address, opMap); + } else { + increaseOperationCount(current, operation); + } +} + +function increaseTotalBalance( + map: Map>, + ticker: string, + address: string, + availBalance: string, + transBalance: string +) { + // +} export class Brc20PgStore extends BasePgStoreModule { sqlOr(partials: postgres.PendingQuery[] | 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): Promise { + await this.sqlWriteTransaction(async sql => { + const tokens: DbBrc20TokenInsert[] = []; + const operations: DbBrc20OperationInsert[] = []; + const operationCounts = new Map(); + const addressOperationCounts = new Map>(); + const totalBalanceChanges = new Map>(); + for (const tx of event.transactions) { + if (tx.metadata.brc20_operation) { + const operation = tx.metadata.brc20_operation; + if ('deploy' in operation) { + tokens.push({ + genesis_id: operation.deploy.inscription_id, + block_height: event.block_identifier.index.toString(), + 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, + }); + operations.push({ + genesis_id: operation.deploy.inscription_id, + brc20_token_ticker: operation.deploy.tick, + block_height: event.block_identifier.index.toString(), + tx_index: '0', + address: operation.deploy.address, + avail_balance: '0', + trans_balance: '0', + operation: DbBrc20Operation.deploy, + }); + increaseOperationCount(operationCounts, DbBrc20Operation.deploy); + increaseAddressOperationCount( + addressOperationCounts, + operation.deploy.address, + DbBrc20Operation.deploy + ); + } else if ('mint' in operation) { + operations.push({ + genesis_id: operation.mint.inscription_id, + brc20_token_ticker: operation.mint.tick, + block_height: event.block_identifier.index.toString(), + tx_index: '0', + address: operation.mint.address, + avail_balance: operation.mint.amt, + trans_balance: '0', + operation: DbBrc20Operation.mint, + }); + increaseOperationCount(operationCounts, DbBrc20Operation.mint); + increaseAddressOperationCount( + addressOperationCounts, + operation.mint.address, + DbBrc20Operation.mint + ); + } else if ('transfer' in operation) { + operations.push({ + genesis_id: operation.transfer.inscription_id, + brc20_token_ticker: operation.transfer.tick, + block_height: event.block_identifier.index.toString(), + tx_index: '0', + address: operation.transfer.address, + avail_balance: BigNumber(operation.transfer.amt).negated().toString(), + trans_balance: operation.transfer.amt, + operation: DbBrc20Operation.transfer, + }); + increaseOperationCount(operationCounts, DbBrc20Operation.transfer); + increaseAddressOperationCount( + addressOperationCounts, + operation.transfer.address, + DbBrc20Operation.deploy + ); + } else if ('transfer_send' in operation) { + operations.push({ + genesis_id: operation.transfer_send.inscription_id, + brc20_token_ticker: operation.transfer_send.tick, + block_height: event.block_identifier.index.toString(), + tx_index: '0', + address: operation.transfer_send.sender_address, + avail_balance: '0', + trans_balance: BigNumber(operation.transfer_send.amt).negated().toString(), + operation: DbBrc20Operation.transferSend, + }); + operations.push({ + genesis_id: operation.transfer_send.inscription_id, + brc20_token_ticker: operation.transfer_send.tick, + block_height: event.block_identifier.index.toString(), + tx_index: '0', + address: operation.transfer_send.receiver_address, + avail_balance: operation.transfer_send.amt, + trans_balance: '0', + operation: DbBrc20Operation.transferReceive, + }); + increaseOperationCount(operationCounts, DbBrc20Operation.transferSend); + increaseAddressOperationCount( + addressOperationCounts, + operation.transfer_send.sender_address, + DbBrc20Operation.transferSend + ); + increaseAddressOperationCount( + addressOperationCounts, + operation.transfer_send.receiver_address, + DbBrc20Operation.transferReceive + ); } } - } else { - await this.applyTransfer({ reveal, pointer }); } - } - } - - 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} - `; - 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 - ) - `; - if (sendRes.count) - logger.info( - `Brc20PgStore send transfer to ${toAddress} at block ${args.pointer.block_height}` - ); + await this.insertTokens(sql, tokens); + await this.insertOperations(sql, operations); }); } - 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 - ) + private async insertTokens(sql: PgSqlClient, tokens: DbBrc20TokenInsert[]): Promise { + if (!tokens.length) return; + await sql` + INSERT INTO brc20_tokens ${this.sql(tokens)} + ON CONFLICT (ticker) DO NOTHING `; - 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 - ) + private async insertOperations( + sql: PgSqlClient, + operations: DbBrc20OperationInsert[] + ): Promise { + if (!operations.length) return; + await sql` + INSERT INTO brc20_operations ${this.sql(operations)} + ON CONFLICT ON CONSTRAINT brc20_operation_unique DO NOTHING `; - 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 { diff --git a/src/pg/brc20/helpers.ts b/src/pg/brc20/helpers.ts deleted file mode 100644 index 6aec1697..00000000 --- a/src/pg/brc20/helpers.ts +++ /dev/null @@ -1,96 +0,0 @@ -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'; - -const Brc20TickerSchema = Type.String({ minLength: 1 }); -const Brc20NumberSchema = Type.RegEx(/^((\d+)|(\d*\.?\d+))$/); - -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; - -const Brc20MintSchema = Type.Object( - { - p: Type.Literal('brc-20'), - op: Type.Literal('mint'), - tick: Brc20TickerSchema, - amt: Brc20NumberSchema, - }, - { additionalProperties: true } -); -export type Brc20Mint = Static; - -const Brc20TransferSchema = Type.Object( - { - p: Type.Literal('brc-20'), - op: Type.Literal('transfer'), - tick: Brc20TickerSchema, - amt: Brc20NumberSchema, - }, - { additionalProperties: true } -); -export type Brc20Transfer = Static; - -const Brc20Schema = Type.Union([Brc20DeploySchema, Brc20MintSchema, Brc20TransferSchema]); -const Brc20C = TypeCompiler.Compile(Brc20Schema); -export type Brc20 = Static; - -export const UINT64_MAX = BigNumber('18446744073709551615'); // 20 digits -// Only compare against `UINT64_MAX` if the number is at least the same number of digits. -const numExceedsMax = (num: string) => num.length >= 20 && UINT64_MAX.isLessThan(num); - -/** - * Activation block height for - * https://l1f.discourse.group/t/brc-20-proposal-for-issuance-and-burn-enhancements-brc20-ip-1/621/1 - */ -export const BRC20_SELF_MINT_ACTIVATION_BLOCK = 837090; - -export function brc20FromInscription(reveal: InscriptionRevealData): Brc20 | undefined { - if ( - reveal.inscription.classic_number < 0 || - 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; - } else { - if (parseFloat(json.amt) == 0 || numExceedsMax(json.amt)) return; - } - 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..a4721dd1 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; +export type DbBrc20OperationInsert = { + genesis_id: string; + brc20_token_ticker: string; + block_height: PgNumeric; + tx_index: PgNumeric; address: 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; + 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 = { @@ -192,14 +164,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..ede946dc 100644 --- a/src/pg/pg-store.ts +++ b/src/pg/pg-store.ts @@ -125,6 +125,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); + if (ENV.BRC20_BLOCK_SCAN_ENABLED) await this.brc20.updateBrc20Operations(event); logger.info( `PgStore ingested block ${event.block_identifier.index} in ${time.getElapsedSeconds()}s` ); @@ -574,11 +575,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 }); }); } From 719b4ec1a2622b1f1946f9213bf6a3a9f3100212 Mon Sep 17 00:00:00 2001 From: Rafael Cardenas Date: Sat, 20 Apr 2024 19:56:23 -0600 Subject: [PATCH 02/13] feat: new apply --- .../1708471015438_remove-unused-indexes.ts | 18 - migrations/1711575178682_brc20-operations.ts | 6 +- src/env.ts | 3 - src/pg/brc20/brc20-pg-store.ts | 415 +- src/pg/pg-store.ts | 2 +- tests/brc-20/api.test.ts | 1317 ++++++ tests/brc-20/brc20.test.ts | 4043 ++--------------- tests/helpers.ts | 107 +- 8 files changed, 1992 insertions(+), 3919 deletions(-) create mode 100644 tests/brc-20/api.test.ts 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/1711575178682_brc20-operations.ts b/migrations/1711575178682_brc20-operations.ts index 023937da..7bfba2ed 100644 --- a/migrations/1711575178682_brc20-operations.ts +++ b/migrations/1711575178682_brc20-operations.ts @@ -11,7 +11,7 @@ export function up(pgm: MigrationBuilder): void { 'transfer_send', 'transfer_receive', ]); - pgm.createTable('brc20_events', { + pgm.createTable('brc20_operations', { genesis_id: { type: 'string', notNull: true, @@ -45,8 +45,8 @@ export function up(pgm: MigrationBuilder): void { }, }); pgm.createConstraint( - 'brc20_operation', - 'brc20_operation_unique', + 'brc20_operations', + 'brc20_operations_unique', 'UNIQUE(genesis_id, operation)' ); } 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/pg/brc20/brc20-pg-store.ts b/src/pg/brc20/brc20-pg-store.ts index 62aa657d..d3e2c5c0 100644 --- a/src/pg/brc20/brc20-pg-store.ts +++ b/src/pg/brc20/brc20-pg-store.ts @@ -1,4 +1,4 @@ -import { BasePgStoreModule, PgSqlClient } from '@hirosystems/api-toolkit'; +import { BasePgStoreModule, logger } from '@hirosystems/api-toolkit'; import * as postgres from 'postgres'; import { DbInscriptionIndexPaging, @@ -53,14 +53,36 @@ function increaseAddressOperationCount( } } -function increaseTotalBalance( - map: Map>, +interface AddressBalanceData { + avail: BigNumber; + trans: BigNumber; + total: BigNumber; +} +function updateAddressBalance( + map: Map>, ticker: string, address: string, - availBalance: string, - transBalance: string + availBalance: BigNumber, + transBalance: BigNumber, + totalBalance: BigNumber ) { - // + const current = map.get(address); + if (current === undefined) { + const opMap = new Map(); + opMap.set(ticker, { avail: availBalance, trans: transBalance, total: totalBalance }); + map.set(address, opMap); + } else { + const currentTick = current.get(ticker); + if (currentTick === undefined) { + current.set(ticker, { avail: availBalance, trans: transBalance, total: totalBalance }); + } else { + current.set(ticker, { + avail: availBalance.plus(currentTick.avail), + trans: transBalance.plus(currentTick.trans), + total: totalBalance.plus(currentTick.total), + }); + } + } } export class Brc20PgStore extends BasePgStoreModule { @@ -70,18 +92,21 @@ export class Brc20PgStore extends BasePgStoreModule { async updateBrc20Operations(event: BitcoinEvent): Promise { await this.sqlWriteTransaction(async sql => { + const block_height = event.block_identifier.index.toString(); + // Keep all DB changes in memory, write them at the end. const tokens: DbBrc20TokenInsert[] = []; const operations: DbBrc20OperationInsert[] = []; const operationCounts = new Map(); const addressOperationCounts = new Map>(); - const totalBalanceChanges = new Map>(); + const totalBalanceChanges = new Map>(); 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) { tokens.push({ + block_height, genesis_id: operation.deploy.inscription_id, - block_height: event.block_identifier.index.toString(), tx_id: tx.transaction_identifier.hash, address: operation.deploy.address, ticker: operation.deploy.tick, @@ -91,10 +116,10 @@ export class Brc20PgStore extends BasePgStoreModule { self_mint: operation.deploy.self_mint, }); operations.push({ + block_height, + tx_index, genesis_id: operation.deploy.inscription_id, brc20_token_ticker: operation.deploy.tick, - block_height: event.block_identifier.index.toString(), - tx_index: '0', address: operation.deploy.address, avail_balance: '0', trans_balance: '0', @@ -106,12 +131,15 @@ export class Brc20PgStore extends BasePgStoreModule { operation.deploy.address, DbBrc20Operation.deploy ); + logger.info( + `Brc20PgStore deploy ${operation.deploy.tick} by ${operation.deploy.address} at height ${block_height}` + ); } else if ('mint' in operation) { operations.push({ + block_height, + tx_index, genesis_id: operation.mint.inscription_id, brc20_token_ticker: operation.mint.tick, - block_height: event.block_identifier.index.toString(), - tx_index: '0', address: operation.mint.address, avail_balance: operation.mint.amt, trans_balance: '0', @@ -123,12 +151,23 @@ export class Brc20PgStore extends BasePgStoreModule { operation.mint.address, DbBrc20Operation.mint ); + updateAddressBalance( + totalBalanceChanges, + operation.mint.tick, + operation.mint.address, + BigNumber(operation.mint.amt), + BigNumber(0), + BigNumber(operation.mint.amt) + ); + logger.info( + `Brc20PgStore mint ${operation.mint.tick} ${operation.mint.amt} by ${operation.mint.address} at height ${block_height}` + ); } else if ('transfer' in operation) { operations.push({ + block_height, + tx_index, genesis_id: operation.transfer.inscription_id, brc20_token_ticker: operation.transfer.tick, - block_height: event.block_identifier.index.toString(), - tx_index: '0', address: operation.transfer.address, avail_balance: BigNumber(operation.transfer.amt).negated().toString(), trans_balance: operation.transfer.amt, @@ -140,22 +179,33 @@ export class Brc20PgStore extends BasePgStoreModule { operation.transfer.address, DbBrc20Operation.deploy ); + updateAddressBalance( + totalBalanceChanges, + operation.transfer.tick, + operation.transfer.address, + BigNumber(operation.transfer.amt).negated(), + BigNumber(operation.transfer.amt), + BigNumber(0) + ); + logger.info( + `Brc20PgStore transfer ${operation.transfer.tick} ${operation.transfer.amt} by ${operation.transfer.address} at height ${block_height}` + ); } else if ('transfer_send' in operation) { operations.push({ + block_height, + tx_index, genesis_id: operation.transfer_send.inscription_id, brc20_token_ticker: operation.transfer_send.tick, - block_height: event.block_identifier.index.toString(), - tx_index: '0', address: operation.transfer_send.sender_address, avail_balance: '0', trans_balance: BigNumber(operation.transfer_send.amt).negated().toString(), operation: DbBrc20Operation.transferSend, }); operations.push({ + block_height, + tx_index, genesis_id: operation.transfer_send.inscription_id, brc20_token_ticker: operation.transfer_send.tick, - block_height: event.block_identifier.index.toString(), - tx_index: '0', address: operation.transfer_send.receiver_address, avail_balance: operation.transfer_send.amt, trans_balance: '0', @@ -172,235 +222,126 @@ export class Brc20PgStore extends BasePgStoreModule { operation.transfer_send.receiver_address, DbBrc20Operation.transferReceive ); + updateAddressBalance( + totalBalanceChanges, + operation.transfer_send.tick, + operation.transfer_send.sender_address, + BigNumber('0'), + BigNumber(operation.transfer_send.amt).negated(), + BigNumber(operation.transfer_send.amt).negated() + ); + updateAddressBalance( + totalBalanceChanges, + operation.transfer_send.tick, + operation.transfer_send.receiver_address, + BigNumber(operation.transfer_send.amt), + BigNumber(0), + BigNumber(operation.transfer_send.amt) + ); + logger.info( + `Brc20PgStore 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}` + ); } } } - await this.insertTokens(sql, tokens); - await this.insertOperations(sql, operations); + if (tokens.length) + await sql` + INSERT INTO brc20_tokens ${sql(tokens)} + ON CONFLICT (ticker) DO NOTHING + `; + if (operations.length) + await sql` + INSERT INTO brc20_operations ${sql(operations)} + ON CONFLICT ON CONSTRAINT brc20_operations_unique DO NOTHING + `; + if (operationCounts.size) { + const entries = []; + for (const [operation, count] of operationCounts) { + entries.push({ operation, count }); + } + await sql` + INSERT INTO brc20_counts_by_operation ${sql(entries)} + ON CONFLICT (operation) DO UPDATE SET + count = brc20_counts_by_operation.count + EXCLUDED.count + `; + } + if (addressOperationCounts.size) { + const entries = []; + for (const [address, map] of addressOperationCounts) { + for (const [operation, count] of map) { + entries.push({ address, operation, count }); + } + } + await sql` + INSERT INTO brc20_counts_by_address_operation ${sql(entries)} + ON CONFLICT (address, operation) DO UPDATE SET + count = brc20_counts_by_address_operation.count + EXCLUDED.count + `; + } + if (totalBalanceChanges.size) { + const entries = []; + for (const [address, map] of totalBalanceChanges) { + for (const [ticker, values] of map) { + entries.push({ + brc20_token_ticker: ticker, + address, + avail_balance: values.avail.toString(), + trans_balance: values.trans.toString(), + total_balance: values.total.toString(), + }); + } + } + await sql` + INSERT INTO brc20_total_balances ${sql(entries)} + ON CONFLICT ON CONSTRAINT brc20_total_balances_unique 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 insertTokens(sql: PgSqlClient, tokens: DbBrc20TokenInsert[]): Promise { - if (!tokens.length) return; - await sql` - INSERT INTO brc20_tokens ${this.sql(tokens)} - ON CONFLICT (ticker) DO NOTHING - `; - } - - private async insertOperations( - sql: PgSqlClient, - operations: DbBrc20OperationInsert[] - ): Promise { - if (!operations.length) return; - await sql` - INSERT INTO brc20_operations ${this.sql(operations)} - ON CONFLICT ON CONSTRAINT brc20_operation_unique DO NOTHING - `; - } - 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; - } - } + // 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; + // } + // } } 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; - } - } - } - - 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} - `; - if (addressRes.count === 0) return; - const returnedToSender = addressRes[0].returned_to_sender; - 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} - `; - }); + // 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; + // } + // } } async getTokens( diff --git a/src/pg/pg-store.ts b/src/pg/pg-store.ts index ede946dc..a32e0468 100644 --- a/src/pg/pg-store.ts +++ b/src/pg/pg-store.ts @@ -125,7 +125,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); - if (ENV.BRC20_BLOCK_SCAN_ENABLED) await this.brc20.updateBrc20Operations(event); + await this.brc20.updateBrc20Operations(event); logger.info( `PgStore ingested block ${event.block_identifier.index} in ${time.getElapsedSeconds()}s` ); diff --git a/tests/brc-20/api.test.ts b/tests/brc-20/api.test.ts new file mode 100644 index 00000000..404f1408 --- /dev/null +++ b/tests/brc-20/api.test.ts @@ -0,0 +1,1317 @@ +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, + }, + }) + .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 }) + .brc20({ + deploy: { + inscription_id: `${transferHash}i0`, + tick: 'PEPE', + max: '21000000', + lim: '21000000', + dec: '18', + address: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td', + self_mint: false, + }, + }) + .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, + }, + }) + .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, + }, + }) + .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, + }, + }) + .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, + }, + }) + .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', + }, + }) + .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, + }, + }) + .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', + }, + }) + .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', + }, + }) + .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, + }, + }) + .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, + }, + }) + .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', + }, + }) + .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', + }, + }) + .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', + }, + }) + .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', + }) + .brc20({ + transfer_send: { + tick: 'PEPE', + inscription_id: `${transferHash}i0`, + amt: '9000', + sender_address: addressA, + receiver_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(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, + }, + }) + .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', + }, + }) + .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', + }, + }) + .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', + }, + }) + .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', + }, + }) + .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() }) + .brc20({ + deploy: { + inscription_id: `${randomHash()}i0`, + tick: 'PEPE', + max: '21000000', + lim: '21000000', + dec: '18', + address: addressA, + self_mint: false, + }, + }) + .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, + }, + }) + .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', + }, + }) + .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, + }, + }) + .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..8d538ef9 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,20 +39,17 @@ describe('BRC-20', () => { .transaction({ hash: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', }) - .inscriptionRevealed( - brc20Reveal({ - json: { - p: 'brc-20', - op: 'deploy', - tick: 'PEPE', - max: '21000000', - }, - number: 0, - ordinal_number: 0, - tx_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', + .brc20({ + deploy: { + inscription_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', + tick: 'PEPE', + max: '21000000', + lim: '1000', + dec: '18', address: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td', - }) - ) + self_mint: false, + }, + }) .build() ); const response1 = await fastify.inject({ @@ -734,44 +78,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,21 +90,17 @@ describe('BRC-20', () => { .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', + .brc20({ + deploy: { + inscription_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', + tick: '$PEPE', + max: '21000000', + lim: '1000', + dec: '18', address: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td', - }) - ) + self_mint: true, + }, + }) .build() ); const response1 = await fastify.inject({ @@ -824,8 +126,11 @@ describe('BRC-20', () => { 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,20 +141,17 @@ describe('BRC-20', () => { .transaction({ hash: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', }) - .inscriptionRevealed( - brc20Reveal({ - json: { - p: 'brc-20', - op: 'deploy', - tick: 'PEPE', - max: '21000000', - }, - number: 0, - ordinal_number: 0, - tx_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', + .brc20({ + deploy: { + inscription_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', + tick: 'PEPE', + max: '21000000', + lim: '250000', + dec: '18', address: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td', - }) - ) + self_mint: false, + }, + }) .build() ); await db.updateInscriptions( @@ -857,385 +159,139 @@ describe('BRC-20', () => { .apply() .block({ height: BRC20_GENESIS_BLOCK + 1, - hash: '000000000000000000021a0207fa97024506baaa74396822fb0a07ac20e70148', + hash: '0000000000000000000098d8f2663891d439f6bb7de230d4e9f6bcc2e85452bf', }) .transaction({ - hash: '3f8067a6e9b45308b5a090c2987feeb2d08cbaf814ef2ffabad7c381b62f5f7e', + hash: '8aec77f855549d98cb9fb5f35e02a03f9a2354fd05a5f89fc610b32c3b01f99f', + }) + .brc20({ + mint: { + tick: 'PEPE', + amt: '250000', + inscription_id: '8aec77f855549d98cb9fb5f35e02a03f9a2354fd05a5f89fc610b32c3b01f99fi0', + address, + }, }) - .inscriptionRevealed( - brc20Reveal({ - json: { - p: 'brc-20', - op: 'deploy', - tick: 'PEPE', - max: '19000000', - }, - number: 1, - ordinal_number: 1, - tx_id: '3f8067a6e9b45308b5a090c2987feeb2d08cbaf814ef2ffabad7c381b62f5f7e', - address: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td', - }) - ) .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, + 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', + }) + .brc20({ + mint: { + tick: 'PEPE', + amt: '100000', + inscription_id: '7a1adbc3e93ddf8d7c4e0ba75aa11c98c431521dd850be8b955feedb716d8beci0', + 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(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('ignores case insensitive deploy for existing token', 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', - max: '21000000', - }, - number: 0, - ordinal_number: 0, - tx_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', - address: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td', - }) - ) + .brc20({ + deploy: { + inscription_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', + tick: '$PEPE', + max: '21000000', + lim: '21000000', + dec: '18', + address, + self_mint: true, + }, + }) .build() ); await db.updateInscriptions( new TestChainhookPayloadBuilder() .apply() .block({ - height: BRC20_GENESIS_BLOCK + 1, - hash: '000000000000000000021a0207fa97024506baaa74396822fb0a07ac20e70148', + height: BRC20_SELF_MINT_ACTIVATION_BLOCK + 1, + hash: '0000000000000000000098d8f2663891d439f6bb7de230d4e9f6bcc2e85452bf', }) .transaction({ - hash: '3f8067a6e9b45308b5a090c2987feeb2d08cbaf814ef2ffabad7c381b62f5f7e', + hash: '8aec77f855549d98cb9fb5f35e02a03f9a2354fd05a5f89fc610b32c3b01f99f', + }) + .brc20({ + mint: { + inscription_id: '8aec77f855549d98cb9fb5f35e02a03f9a2354fd05a5f89fc610b32c3b01f99fi0', + tick: '$PEPE', + address, + amt: '250000', + }, }) - .inscriptionRevealed( - brc20Reveal({ - json: { - p: 'brc-20', - op: 'deploy', - tick: 'pepe', - max: '19000000', - }, - number: 1, - ordinal_number: 1, - tx_id: '3f8067a6e9b45308b5a090c2987feeb2d08cbaf814ef2ffabad7c381b62f5f7e', - address: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td', - }) - ) .build() ); + const response1 = await fastify.inject({ method: 'GET', - url: `/ordinals/brc-20/tokens?ticker=PEPE`, - }); - expect(response1.statusCode).toBe(200); - const responseJson1 = response1.json(); - expect(responseJson1.total).toBe(1); - expect(responseJson1.results).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 - }); - 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, - }, - ]); - }); - - test('ignores deploy from classic cursed inscription', 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, - 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, - }) - ) - .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, - parent: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', - }) - ) - .build() - ); - - const response1 = await fastify.inject({ - method: 'GET', - url: `/ordinals/brc-20/balances/${address}`, + url: `/ordinals/brc-20/balances/${address}`, }); expect(response1.statusCode).toBe(200); const responseJson1 = response1.json(); @@ -1260,21 +316,14 @@ describe('BRC-20', () => { .transaction({ hash: '7a1adbc3e93ddf8d7c4e0ba75aa11c98c431521dd850be8b955feedb716d8bec', }) - .inscriptionRevealed( - brc20Reveal({ - json: { - p: 'brc-20', - op: 'mint', - tick: '$pepe', - amt: '100000', - }, - number: 2, - ordinal_number: 2, - tx_id: '7a1adbc3e93ddf8d7c4e0ba75aa11c98c431521dd850be8b955feedb716d8bec', + .brc20({ + mint: { + inscription_id: '7a1adbc3e93ddf8d7c4e0ba75aa11c98c431521dd850be8b955feedb716d8beci0', + tick: '$PEPE', address: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td', - parent: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', - }) - ) + amt: '100000', + }, + }) .build() ); @@ -1308,85 +357,6 @@ describe('BRC-20', () => { ); }); - 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`, - }); - expect(response3.statusCode).toBe(200); - const responseJson3 = response3.json(); - expect(responseJson3.total).toBe(1); - expect(responseJson3.results).toEqual( - expect.arrayContaining([ - expect.objectContaining({ ticker: '$PEPE', minted_supply: '0.000000000000000000' }), - ]) - ); - }); - test('valid self mints for tokens with max 0 are saved', async () => { const address = 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td'; await db.updateInscriptions( @@ -1399,21 +369,17 @@ describe('BRC-20', () => { .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, - }) - ) + .brc20({ + deploy: { + inscription_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', + tick: '$PEPE', + max: '0', + lim: '250000', + dec: '18', + address, + self_mint: true, + }, + }) .build() ); await db.updateInscriptions( @@ -1426,21 +392,14 @@ describe('BRC-20', () => { .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, - parent: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', - }) - ) + .brc20({ + mint: { + inscription_id: '8aec77f855549d98cb9fb5f35e02a03f9a2354fd05a5f89fc610b32c3b01f99fi0', + tick: '$PEPE', + address, + amt: '250000', + }, + }) .build() ); @@ -1471,21 +430,14 @@ describe('BRC-20', () => { .transaction({ hash: '7a1adbc3e93ddf8d7c4e0ba75aa11c98c431521dd850be8b955feedb716d8bec', }) - .inscriptionRevealed( - brc20Reveal({ - json: { - p: 'brc-20', - op: 'mint', - tick: '$pepe', - amt: '100000', - }, - number: 2, - ordinal_number: 2, - tx_id: '7a1adbc3e93ddf8d7c4e0ba75aa11c98c431521dd850be8b955feedb716d8bec', - address: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td', - parent: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', - }) - ) + .brc20({ + mint: { + inscription_id: '7a1adbc3e93ddf8d7c4e0ba75aa11c98c431521dd850be8b955feedb716d8beci0', + tick: '$PEPE', + address, + amt: '100000', + }, + }) .build() ); @@ -1531,20 +483,17 @@ describe('BRC-20', () => { .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, - }) - ) + .brc20({ + deploy: { + inscription_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', + tick: 'PEPE', + max: '21000000', + lim: '21000000', + dec: '18', + address, + self_mint: false, + }, + }) .build() ); await db.updateInscriptions( @@ -1557,20 +506,14 @@ describe('BRC-20', () => { .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, - }) - ) + .brc20({ + mint: { + inscription_id: '8aec77f855549d98cb9fb5f35e02a03f9a2354fd05a5f89fc610b32c3b01f99fi0', + tick: 'PEPE', + address, + amt: '250000', + }, + }) .build() ); // Rollback @@ -1584,20 +527,14 @@ describe('BRC-20', () => { .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, - }) - ) + .brc20({ + mint: { + inscription_id: '8aec77f855549d98cb9fb5f35e02a03f9a2354fd05a5f89fc610b32c3b01f99fi0', + tick: 'PEPE', + address, + amt: '250000', + }, + }) .build() ); @@ -1616,779 +553,61 @@ describe('BRC-20', () => { }); 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', - }) - .transaction({ - hash: '8aec77f855549d98cb9fb5f35e02a03f9a2354fd05a5f89fc610b32c3b01f99f', - }) - .inscriptionRevealed( - brc20Reveal({ - json: { - p: 'brc-20', - op: 'mint', - tick: 'PEPE', - amt: '250000.000', // Invalid decimal count - }, - number: 1, - ordinal_number: 1, - tx_id: '8aec77f855549d98cb9fb5f35e02a03f9a2354fd05a5f89fc610b32c3b01f99f', - address: address, - }) - ) - .build() - ); - - const response2 = await fastify.inject({ - method: 'GET', - url: `/ordinals/brc-20/balances/${address}`, - }); - expect(response2.statusCode).toBe(200); - const responseJson2 = response2.json(); - expect(responseJson2.total).toBe(0); - expect(responseJson2.results).toStrictEqual([]); - }); - - test('mint exceeds token supply', async () => { - const address = 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td'; - await db.updateInscriptions( - new TestChainhookPayloadBuilder() - .apply() - .block({ - height: BRC20_GENESIS_BLOCK, - hash: '00000000000000000002a90330a99f67e3f01eb2ce070b45930581e82fb7a91d', - }) - .transaction({ - hash: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', - }) - .inscriptionRevealed( - brc20Reveal({ - json: { - p: 'brc-20', - op: 'deploy', - tick: 'PEPE', - max: '2500', - dec: '1', - }, - number: 0, - ordinal_number: 0, - tx_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', - address: address, - }) - ) - .build() - ); - await db.updateInscriptions( - new TestChainhookPayloadBuilder() - .apply() - .block({ - height: BRC20_GENESIS_BLOCK + 1, - hash: '0000000000000000000098d8f2663891d439f6bb7de230d4e9f6bcc2e85452bf', - }) - .transaction({ - hash: '3b55f624eaa4f8de6c42e0c490176b67123a83094384f658611faf7bfb85dd0f', - }) - .inscriptionRevealed( - brc20Reveal({ - json: { - p: 'brc-20', - op: 'mint', - tick: 'PEPE', - amt: '1000', - }, - number: 1, - ordinal_number: 1, - tx_id: '3b55f624eaa4f8de6c42e0c490176b67123a83094384f658611faf7bfb85dd0f', - address: address, - }) - ) - .transaction({ - hash: '7e09bda2cba34bca648cca6d79a074940d39b6137150d3a3edcf80c0e01419a5', - }) - .inscriptionRevealed( - brc20Reveal({ - json: { - p: 'brc-20', - op: 'mint', - tick: 'PEPE', - amt: '1000', - }, - number: 2, - ordinal_number: 2, - tx_id: '7e09bda2cba34bca648cca6d79a074940d39b6137150d3a3edcf80c0e01419a5', - address: address, - }) - ) - .transaction({ - hash: '8aec77f855549d98cb9fb5f35e02a03f9a2354fd05a5f89fc610b32c3b01f99f', - }) - .inscriptionRevealed( - brc20Reveal({ - json: { - p: 'brc-20', - op: 'mint', - tick: 'PEPE', - amt: '5000000000', // Exceeds supply - }, - number: 3, - ordinal_number: 3, - tx_id: '8aec77f855549d98cb9fb5f35e02a03f9a2354fd05a5f89fc610b32c3b01f99f', - address: address, - }) - ) - .build() - ); - - const response2 = await fastify.inject({ - method: 'GET', - url: `/ordinals/brc-20/balances/${address}?ticker=PEPE`, - }); - expect(response2.statusCode).toBe(200); - const responseJson2 = response2.json(); - expect(responseJson2.total).toBe(1); - expect(responseJson2.results).toStrictEqual([ - { - available_balance: '2500.0', // Max capacity - overall_balance: '2500.0', - ticker: 'PEPE', - transferrable_balance: '0.0', - }, - ]); - - // 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, - }) - ) - .transaction({ - hash: '7edaa48337a94da327b6262830505f116775a32db5ad4ad46e87ecea33f21bac', - }) - .inscriptionRevealed( - brc20Reveal({ - json: { - p: 'brc-20', - op: 'transfer', - tick: 'PEPE', - amt: '2000', // Will exceed available balance - }, - number: 3, - ordinal_number: 3, - tx_id: '7edaa48337a94da327b6262830505f116775a32db5ad4ad46e87ecea33f21bac', - 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: '1000.000000000000000000', - overall_balance: '10000.000000000000000000', - ticker: 'PEPE', - transferrable_balance: '9000.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', + height: BRC20_GENESIS_BLOCK + 2, + hash: '00000000000000000002b14f0c5dde0b2fc74d022e860696bd64f1f652756674', }) .transaction({ - hash: '7edaa48337a94da327b6262830505f116775a32db5ad4ad46e87ecea33f21bac', + hash: 'eee52b22397ea4a4aefe6a39931315e93a157091f5a994216c0aa9c8c6fef47a', }) - .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: { + inscription_id: 'eee52b22397ea4a4aefe6a39931315e93a157091f5a994216c0aa9c8c6fef47ai0', + tick: 'PEPE', + address, + amt: '2000', + }, }) .build() ); - const response1 = await fastify.inject({ + const response = 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([ + expect(response.statusCode).toBe(200); + const json = response.json(); + expect(json.total).toBe(1); + expect(json.results).toStrictEqual([ { - available_balance: '1000.000000000000000000', - overall_balance: '1000.000000000000000000', - ticker: '$PEPE', - transferrable_balance: '0.000000000000000000', + 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/${address2}`, + url: `/ordinals/brc-20/balances/${address}?block_height=779833`, }); - 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', - }, - ]); + expect(json2.results[0].available_balance).toBe('10000.000000000000000000'); }); - test('sending transfer as fee returns amount to sender', async () => { + test('multiple transfers in block', async () => { const address = 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td'; - await deployAndMintPEPE(address); + await deployAndMintPEPE(db, address); await db.updateInscriptions( new TestChainhookPayloadBuilder() .apply() @@ -2399,65 +618,49 @@ describe('BRC-20', () => { .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', + .brc20({ + transfer: { + inscription_id: 'eee52b22397ea4a4aefe6a39931315e93a157091f5a994216c0aa9c8c6fef47ai0', + tick: 'PEPE', + address, + amt: '9000', + }, }) .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, + .brc20({ + transfer: { + inscription_id: '7edaa48337a94da327b6262830505f116775a32db5ad4ad46e87ecea33f21baci0', + tick: 'PEPE', + address, + amt: '1000', + }, }) .build() ); - const response1 = await fastify.inject({ + const response = 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([ + expect(response.statusCode).toBe(200); + const json = response.json(); + expect(json.total).toBe(1); + expect(json.results).toStrictEqual([ { - available_balance: '10000.000000000000000000', + available_balance: '1000.000000000000000000', overall_balance: '10000.000000000000000000', ticker: 'PEPE', - transferrable_balance: '0.000000000000000000', + transferrable_balance: '9000.000000000000000000', }, ]); }); - test('sending transfer to unspendable output does not return to sender', async () => { + test('send balance to address', async () => { const address = 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td'; - await deployAndMintPEPE(address); + const address2 = '3QNjwPDRafjBm9XxJpshgk3ksMJh3TFxTU'; + await deployAndMintPEPE(db, address); await db.updateInscriptions( new TestChainhookPayloadBuilder() .apply() @@ -2468,20 +671,14 @@ describe('BRC-20', () => { .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, - }) - ) + .brc20({ + transfer: { + inscription_id: 'eee52b22397ea4a4aefe6a39931315e93a157091f5a994216c0aa9c8c6fef47ai0', + tick: 'PEPE', + address, + amt: '9000', + }, + }) .build() ); await db.updateInscriptions( @@ -2494,15 +691,14 @@ 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, + }, }) .build() ); @@ -2522,86 +718,126 @@ describe('BRC-20', () => { 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', + }) + .brc20({ + deploy: { + inscription_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', + tick: '$PEPE', + max: '0', + lim: '21000000', + dec: '18', + address, + self_mint: true, + }, }) - .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: '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', + }, + }) + .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', + }, }) .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', + hash: '7edaa48337a94da327b6262830505f116775a32db5ad4ad46e87ecea33f21bac', }) - .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, + .brc20({ + transfer_send: { + inscription_id: 'eee52b22397ea4a4aefe6a39931315e93a157091f5a994216c0aa9c8c6fef47ai0', + tick: '$PEPE', + amt: '9000', + sender_address: address, + receiver_address: address2, + }, }) .build() ); - // Balances only reflect the first transfer. const response1 = await fastify.inject({ method: 'GET', url: `/ordinals/brc-20/balances/${address}`, @@ -2613,7 +849,7 @@ describe('BRC-20', () => { { available_balance: '1000.000000000000000000', overall_balance: '1000.000000000000000000', - ticker: 'PEPE', + ticker: '$PEPE', transferrable_balance: '0.000000000000000000', }, ]); @@ -2629,7 +865,7 @@ describe('BRC-20', () => { { available_balance: '9000.000000000000000000', overall_balance: '9000.000000000000000000', - ticker: 'PEPE', + ticker: '$PEPE', transferrable_balance: '0.000000000000000000', }, ]); @@ -2637,7 +873,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,20 +885,14 @@ describe('BRC-20', () => { .transaction({ hash: '825a25b64b5d99ca30e04e53cc9a3020412e1054eb2a7523eb075ddd6d983205', }) - .inscriptionRevealed( - brc20Reveal({ - json: { - p: 'brc-20', - op: 'transfer', - tick: 'PEPE', - amt: '20', - }, - number: 2, - ordinal_number: 2, - tx_id: '825a25b64b5d99ca30e04e53cc9a3020412e1054eb2a7523eb075ddd6d983205', - address: address, - }) - ) + .brc20({ + transfer: { + inscription_id: '825a25b64b5d99ca30e04e53cc9a3020412e1054eb2a7523eb075ddd6d983205i0', + tick: 'PEPE', + address, + amt: '20', + }, + }) .build() ); await db.updateInscriptions( @@ -2675,15 +905,14 @@ 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, + }, }) .build() ); @@ -2709,20 +938,14 @@ describe('BRC-20', () => { .transaction({ hash: '09a812f72275892b4858880cf3821004a6e8885817159b340639afe9952ac053', }) - .inscriptionRevealed( - brc20Reveal({ - json: { - p: 'brc-20', - op: 'transfer', - tick: 'PEPE', - amt: '20', - }, - number: 3, - ordinal_number: 3, - tx_id: '09a812f72275892b4858880cf3821004a6e8885817159b340639afe9952ac053', + .brc20({ + transfer: { + inscription_id: '09a812f72275892b4858880cf3821004a6e8885817159b340639afe9952ac053i0', + tick: 'PEPE', address: address2, - }) - ) + amt: '20', + }, + }) .build() ); response = await fastify.inject({ @@ -2747,15 +970,14 @@ 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, + }, }) .build() ); @@ -2774,1393 +996,11 @@ describe('BRC-20', () => { }); }); - 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 const transferPEPE = new TestChainhookPayloadBuilder() @@ -4172,20 +1012,14 @@ describe('BRC-20', () => { .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, - }) - ) + .brc20({ + transfer: { + inscription_id: 'eee52b22397ea4a4aefe6a39931315e93a157091f5a994216c0aa9c8c6fef47ai0', + tick: 'PEPE', + address, + amt: '9000', + }, + }) .build(); await db.updateInscriptions(transferPEPE); const sendPEPE = new TestChainhookPayloadBuilder() @@ -4197,15 +1031,14 @@ 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, + }, }) .build(); await db.updateInscriptions(sendPEPE); @@ -4219,20 +1052,17 @@ describe('BRC-20', () => { .transaction({ hash: '8354e85e87fa2df8b3a06ec0b9d395559b95174530cb19447fc4df5f6d4ca84d', }) - .inscriptionRevealed( - brc20Reveal({ - json: { - p: 'brc-20', - op: 'deploy', - tick: '🔥', - max: '1000', - }, - number: 3, - ordinal_number: 3, - tx_id: '8354e85e87fa2df8b3a06ec0b9d395559b95174530cb19447fc4df5f6d4ca84d', - address: address, - }) - ) + .brc20({ + deploy: { + inscription_id: '8354e85e87fa2df8b3a06ec0b9d395559b95174530cb19447fc4df5f6d4ca84di0', + tick: '🔥', + max: '1000', + lim: '1000', + dec: '18', + address, + self_mint: false, + }, + }) .build(); await db.updateInscriptions(deployFIRE); const mintFIRE = new TestChainhookPayloadBuilder() @@ -4244,20 +1074,14 @@ describe('BRC-20', () => { .transaction({ hash: '81f4ee2c247c5f5c0d3a6753fef706df410ea61c2aa6d370003b98beb041b887', }) - .inscriptionRevealed( - brc20Reveal({ - json: { - p: 'brc-20', - op: 'mint', - tick: '🔥', - amt: '500', - }, - number: 4, - ordinal_number: 4, - tx_id: '81f4ee2c247c5f5c0d3a6753fef706df410ea61c2aa6d370003b98beb041b887', - address: address, - }) - ) + .brc20({ + mint: { + inscription_id: '81f4ee2c247c5f5c0d3a6753fef706df410ea61c2aa6d370003b98beb041b887i0', + tick: '🔥', + address, + amt: '500', + }, + }) .build(); await db.updateInscriptions(mintFIRE); // Transfer and send 🔥 to self @@ -4270,20 +1094,14 @@ describe('BRC-20', () => { .transaction({ hash: 'c1c7f1d5c10a30605a8a5285ca3465a4f75758ed9b7f201e5ef62727e179966f', }) - .inscriptionRevealed( - brc20Reveal({ - json: { - p: 'brc-20', - op: 'transfer', - tick: '🔥', - amt: '100', - }, - number: 5, - ordinal_number: 5, - tx_id: 'c1c7f1d5c10a30605a8a5285ca3465a4f75758ed9b7f201e5ef62727e179966f', - address: address, - }) - ) + .brc20({ + transfer: { + inscription_id: 'c1c7f1d5c10a30605a8a5285ca3465a4f75758ed9b7f201e5ef62727e179966fi0', + tick: '🔥', + address, + amt: '100', + }, + }) .build(); await db.updateInscriptions(transferFIRE); const sendFIRE = new TestChainhookPayloadBuilder() @@ -4295,15 +1113,14 @@ 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, + }, }) .build(); await db.updateInscriptions(sendFIRE); diff --git a/tests/helpers.ts b/tests/helpers.ts index f2c70674..c79be365 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,17 @@ export class TestChainhookPayloadBuilder { return this; } - build(): Payload { + brc20(args: BitcoinBrc20Operation): this { + this.lastBlockTx.metadata.brc20_operation = args; + return this; + } + + build(): BitcoinPayload { return this.payload; } } -export function rollBack(payload: Payload) { +export function rollBack(payload: BitcoinPayload) { return { ...payload, apply: [], @@ -108,45 +117,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 +133,52 @@ 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, + }, + }) + .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, + }, + }) + .build() + ); +} From 0f85e8a4b9d73e2e113acd25ee584522ea4a82ae Mon Sep 17 00:00:00 2001 From: Rafael Cardenas Date: Sat, 20 Apr 2024 23:19:02 -0600 Subject: [PATCH 03/13] fix: minted supply --- .../1711575178683_brc20-total-balances.ts | 12 +- src/pg/brc20/brc20-pg-store.ts | 72 +++--- tests/brc-20/api.test.ts | 228 +++++++++--------- tests/brc-20/brc20.test.ts | 134 +++++----- tests/helpers.ts | 56 +++++ 5 files changed, 281 insertions(+), 221 deletions(-) diff --git a/migrations/1711575178683_brc20-total-balances.ts b/migrations/1711575178683_brc20-total-balances.ts index 5f03d801..f0aba616 100644 --- a/migrations/1711575178683_brc20-total-balances.ts +++ b/migrations/1711575178683_brc20-total-balances.ts @@ -5,10 +5,6 @@ export const shorthands: ColumnDefinitions | undefined = undefined; export function up(pgm: MigrationBuilder): void { pgm.createTable('brc20_total_balances', { - id: { - type: 'bigserial', - primaryKey: true, - }, brc20_token_ticker: { type: 'string', notNull: true, @@ -35,11 +31,9 @@ export function up(pgm: MigrationBuilder): void { 'brc20_total_balances_brc20_deploy_id_fk', 'FOREIGN KEY(brc20_token_ticker) REFERENCES brc20_tokens(ticker) ON DELETE CASCADE' ); - pgm.createConstraint( - 'brc20_total_balances', - 'brc20_total_balances_unique', - 'UNIQUE(brc20_token_ticker, address)' - ); + pgm.createConstraint('brc20_total_balances', 'brc20_total_balances_pkey', { + primaryKey: ['brc20_token_ticker', 'address'], + }); pgm.createIndex('brc20_total_balances', ['address']); pgm.createIndex('brc20_total_balances', [ 'brc20_token_ticker', diff --git a/src/pg/brc20/brc20-pg-store.ts b/src/pg/brc20/brc20-pg-store.ts index d3e2c5c0..e52fb1df 100644 --- a/src/pg/brc20/brc20-pg-store.ts +++ b/src/pg/brc20/brc20-pg-store.ts @@ -11,16 +11,11 @@ import { BRC20_OPERATIONS, DbBrc20Activity, DbBrc20Balance, - DbBrc20BalanceTypeId, - DbBrc20DeployEvent, DbBrc20TokenInsert, - DbBrc20Event, DbBrc20EventOperation, DbBrc20Holder, - DbBrc20MintEvent, DbBrc20Token, DbBrc20TokenWithSupply, - DbBrc20TransferEvent, DbBrc20OperationInsert, DbBrc20Operation, } from './types'; @@ -38,6 +33,15 @@ function increaseOperationCount(map: Map, operation: D } } +function increaseTokenMintedSupply(map: Map, ticker: string, amount: BigNumber) { + const current = map.get(ticker); + if (current == undefined) { + map.set(ticker, amount); + } else { + map.set(ticker, current.plus(amount)); + } +} + function increaseAddressOperationCount( map: Map>, address: string, @@ -96,6 +100,7 @@ export class Brc20PgStore extends BasePgStoreModule { // Keep all DB changes in memory, write them at the end. const tokens: DbBrc20TokenInsert[] = []; const operations: DbBrc20OperationInsert[] = []; + const tokenMintSupplies = new Map(); const operationCounts = new Map(); const addressOperationCounts = new Map>(); const totalBalanceChanges = new Map>(); @@ -145,6 +150,8 @@ export class Brc20PgStore extends BasePgStoreModule { trans_balance: '0', operation: DbBrc20Operation.mint, }); + const amt = BigNumber(operation.mint.amt); + increaseTokenMintedSupply(tokenMintSupplies, operation.mint.tick, amt); increaseOperationCount(operationCounts, DbBrc20Operation.mint); increaseAddressOperationCount( addressOperationCounts, @@ -155,9 +162,9 @@ export class Brc20PgStore extends BasePgStoreModule { totalBalanceChanges, operation.mint.tick, operation.mint.address, - BigNumber(operation.mint.amt), + amt, BigNumber(0), - BigNumber(operation.mint.amt) + amt ); logger.info( `Brc20PgStore mint ${operation.mint.tick} ${operation.mint.amt} by ${operation.mint.address} at height ${block_height}` @@ -173,6 +180,7 @@ export class Brc20PgStore extends BasePgStoreModule { trans_balance: operation.transfer.amt, operation: DbBrc20Operation.transfer, }); + const amt = BigNumber(operation.transfer.amt); increaseOperationCount(operationCounts, DbBrc20Operation.transfer); increaseAddressOperationCount( addressOperationCounts, @@ -183,8 +191,8 @@ export class Brc20PgStore extends BasePgStoreModule { totalBalanceChanges, operation.transfer.tick, operation.transfer.address, - BigNumber(operation.transfer.amt).negated(), - BigNumber(operation.transfer.amt), + amt.negated(), + amt, BigNumber(0) ); logger.info( @@ -211,6 +219,7 @@ export class Brc20PgStore extends BasePgStoreModule { trans_balance: '0', operation: DbBrc20Operation.transferReceive, }); + const amt = BigNumber(operation.transfer_send.amt); increaseOperationCount(operationCounts, DbBrc20Operation.transferSend); increaseAddressOperationCount( addressOperationCounts, @@ -227,16 +236,16 @@ export class Brc20PgStore extends BasePgStoreModule { operation.transfer_send.tick, operation.transfer_send.sender_address, BigNumber('0'), - BigNumber(operation.transfer_send.amt).negated(), - BigNumber(operation.transfer_send.amt).negated() + amt.negated(), + amt.negated() ); updateAddressBalance( totalBalanceChanges, operation.transfer_send.tick, operation.transfer_send.receiver_address, - BigNumber(operation.transfer_send.amt), + amt, BigNumber(0), - BigNumber(operation.transfer_send.amt) + amt ); logger.info( `Brc20PgStore 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}` @@ -254,6 +263,12 @@ export class Brc20PgStore extends BasePgStoreModule { INSERT INTO brc20_operations ${sql(operations)} ON CONFLICT ON CONSTRAINT brc20_operations_unique DO NOTHING `; + for (const [ticker, amount] of tokenMintSupplies) { + await sql` + UPDATE brc20_tokens SET minted_supply = minted_supply + ${amount.toString()} + WHERE ticker = ${ticker} + `; + } if (operationCounts.size) { const entries = []; for (const [operation, count] of operationCounts) { @@ -293,7 +308,7 @@ export class Brc20PgStore extends BasePgStoreModule { } await sql` INSERT INTO brc20_total_balances ${sql(entries)} - ON CONFLICT ON CONSTRAINT brc20_total_balances_unique DO UPDATE SET + ON CONFLICT (brc20_token_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 @@ -348,29 +363,30 @@ export class Brc20PgStore extends BasePgStoreModule { args: { ticker?: string[]; order_by?: Brc20TokenOrderBy } & DbInscriptionIndexPaging ): Promise> { const tickerPrefixCondition = this.sqlOr( - args.ticker?.map(t => this.sql`d.ticker_lower LIKE LOWER(${t}) || '%'`) + 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} @@ -390,15 +406,9 @@ 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 = this.sqlOr(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` @@ -421,11 +431,11 @@ export class Brc20PgStore extends BasePgStoreModule { : 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.brc20_token_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} diff --git a/tests/brc-20/api.test.ts b/tests/brc-20/api.test.ts index 404f1408..02392f89 100644 --- a/tests/brc-20/api.test.ts +++ b/tests/brc-20/api.test.ts @@ -39,7 +39,7 @@ describe('BRC-20 API', () => { .brc20({ deploy: { inscription_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', - tick: 'PEPE', + tick: 'pepe', max: '21000000', lim: '21000000', dec: '18', @@ -51,7 +51,7 @@ describe('BRC-20 API', () => { ); const response = await fastify.inject({ method: 'GET', - url: `/ordinals/brc-20/tokens/PEPE`, + url: `/ordinals/brc-20/tokens/pepe`, }); expect(response.statusCode).toBe(200); expect(response.json()).toStrictEqual({ @@ -61,7 +61,7 @@ describe('BRC-20 API', () => { block_height: BRC20_GENESIS_BLOCK, tx_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', address: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td', - ticker: 'PEPE', + ticker: 'pepe', max_supply: '21000000.000000000000000000', mint_limit: null, decimals: 18, @@ -92,7 +92,7 @@ describe('BRC-20 API', () => { .brc20({ deploy: { inscription_id: `${transferHash}i0`, - tick: 'PEPE', + tick: 'pepe', max: '21000000', lim: '21000000', dec: '18', @@ -113,7 +113,7 @@ describe('BRC-20 API', () => { .brc20({ deploy: { inscription_id: `${transferHash}i0`, - tick: 'PEER', + tick: 'peer', max: '21000000', lim: '21000000', dec: '18', @@ -134,7 +134,7 @@ describe('BRC-20 API', () => { .brc20({ deploy: { inscription_id: `${transferHash}i0`, - tick: 'ABCD', + tick: 'abcd', max: '21000000', lim: '21000000', dec: '18', @@ -155,7 +155,7 @@ describe('BRC-20 API', () => { .brc20({ deploy: { inscription_id: `${transferHash}i0`, - tick: 'DCBA', + tick: 'dcba', max: '21000000', lim: '21000000', dec: '18', @@ -174,9 +174,9 @@ describe('BRC-20 API', () => { expect(responseJson.total).toBe(3); expect(responseJson.results).toEqual( expect.arrayContaining([ - expect.objectContaining({ ticker: 'PEPE' }), - expect.objectContaining({ ticker: 'PEER' }), - expect.objectContaining({ ticker: 'ABCD' }), + expect.objectContaining({ ticker: 'pepe' }), + expect.objectContaining({ ticker: 'peer' }), + expect.objectContaining({ ticker: 'abcd' }), ]) ); }); @@ -188,7 +188,7 @@ describe('BRC-20 API', () => { const addressA = 'bc1q6uwuet65rm6xvlz7ztw2gvdmmay5uaycu03mqz'; const addressB = 'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4'; - // A deploys PEPE + // A deploys pepe let number = inscriptionNumbers.next().value; await db.updateInscriptions( new TestChainhookPayloadBuilder() @@ -198,7 +198,7 @@ describe('BRC-20 API', () => { .brc20({ deploy: { inscription_id: `${randomHash()}i0`, - tick: 'PEPE', + tick: 'pepe', max: '21000000', lim: '21000000', dec: '18', @@ -209,7 +209,7 @@ describe('BRC-20 API', () => { .build() ); - // A mints 10000 PEPE 10 times (will later be rolled back) + // A mints 10000 pepe 10 times (will later be rolled back) const pepeMints = []; for (let i = 0; i < 10; i++) { const txHash = randomHash(); @@ -221,7 +221,7 @@ describe('BRC-20 API', () => { .brc20({ mint: { inscription_id: `${txHash}i0`, - tick: 'PEPE', + tick: 'pepe', address: addressA, amt: '10000', }, @@ -231,7 +231,7 @@ describe('BRC-20 API', () => { await db.updateInscriptions(payload); } - // B deploys ABCD + // B deploys abcd number = inscriptionNumbers.next().value; await db.updateInscriptions( new TestChainhookPayloadBuilder() @@ -241,7 +241,7 @@ describe('BRC-20 API', () => { .brc20({ deploy: { inscription_id: `${randomHash()}i0`, - tick: 'ABCD', + tick: 'abcd', max: '21000000', lim: '21000000', dec: '18', @@ -252,7 +252,7 @@ describe('BRC-20 API', () => { .build() ); - // B mints 10000 ABCD + // B mints 10000 abcd number = inscriptionNumbers.next().value; await db.updateInscriptions( new TestChainhookPayloadBuilder() @@ -262,7 +262,7 @@ describe('BRC-20 API', () => { .brc20({ mint: { inscription_id: `${randomHash()}i0`, - tick: 'ABCD', + tick: 'abcd', address: addressA, amt: '10000', }, @@ -270,7 +270,7 @@ describe('BRC-20 API', () => { .build() ); - // B send 1000 ABCD to A + // B send 1000 abcd to A // (create inscription, transfer) const txHashTransfer = randomHash(); number = inscriptionNumbers.next().value; @@ -281,7 +281,7 @@ describe('BRC-20 API', () => { .brc20({ transfer: { inscription_id: `${txHashTransfer}i0`, - tick: 'ABCD', + tick: 'abcd', address: addressB, amt: '1000', }, @@ -296,7 +296,7 @@ describe('BRC-20 API', () => { .transaction({ hash: txHashTransferSend }) .brc20({ transfer_send: { - tick: 'ABCD', + tick: 'abcd', inscription_id: `${txHashTransfer}i0`, amt: '1000', sender_address: addressB, @@ -317,13 +317,13 @@ describe('BRC-20 API', () => { // WITHOUT tx_count sort: expect(json.results).toEqual([ - // The first result is the token with the latest activity (ABCD) + // The first result is the token with the latest activity (abcd) expect.objectContaining({ - ticker: 'ABCD', + ticker: 'abcd', tx_count: 4, } as Brc20TokenResponse), expect.objectContaining({ - ticker: 'PEPE', + ticker: 'pepe', tx_count: 11, } as Brc20TokenResponse), ]); @@ -337,25 +337,25 @@ describe('BRC-20 API', () => { expect(json.total).toBe(2); expect(json.results).toHaveLength(2); - // WITH tx_count sort: The first result is the most active token (PEPE) + // WITH tx_count sort: The first result is the most active token (pepe) expect(json.results).toEqual([ expect.objectContaining({ - ticker: 'PEPE', + ticker: 'pepe', tx_count: 11, } as Brc20TokenResponse), expect.objectContaining({ - ticker: 'ABCD', + ticker: 'abcd', tx_count: 4, } as Brc20TokenResponse), ]); - // Rollback PEPE mints + // 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) + // 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`, @@ -366,16 +366,16 @@ describe('BRC-20 API', () => { expect(json.results).toHaveLength(2); expect(json.results).toEqual([ expect.objectContaining({ - ticker: 'ABCD', + ticker: 'abcd', tx_count: 4, } as Brc20TokenResponse), expect.objectContaining({ - ticker: 'PEPE', + ticker: 'pepe', tx_count: 1, // only the deploy remains } as Brc20TokenResponse), ]); - // Rollback ABCD transfer + // Rollback abcd transfer await db.updateInscriptions({ ...payloadTransferSend, apply: [], @@ -397,11 +397,11 @@ describe('BRC-20 API', () => { expect(json.results).toHaveLength(2); expect(json.results).toEqual([ expect.objectContaining({ - ticker: 'ABCD', + ticker: 'abcd', tx_count: 2, // only the deploy and mint remain } as Brc20TokenResponse), expect.objectContaining({ - ticker: 'PEPE', + ticker: 'pepe', tx_count: 1, } as Brc20TokenResponse), ]); @@ -416,7 +416,7 @@ describe('BRC-20 API', () => { const addressA = 'bc1q6uwuet65rm6xvlz7ztw2gvdmmay5uaycu03mqz'; const addressB = 'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4'; - // A deploys PEPE + // A deploys pepe let number = inscriptionNumbers.next().value; await db.updateInscriptions( new TestChainhookPayloadBuilder() @@ -426,7 +426,7 @@ describe('BRC-20 API', () => { .brc20({ deploy: { inscription_id: `${randomHash()}i0`, - tick: 'PEPE', + tick: 'pepe', max: '21000000', lim: '21000000', dec: '18', @@ -437,10 +437,10 @@ describe('BRC-20 API', () => { .build() ); - // Verify that the PEPE deploy is in the activity feed + // Verify that the pepe deploy is in the activity feed let response = await fastify.inject({ method: 'GET', - url: `/ordinals/brc-20/activity?ticker=PEPE`, + url: `/ordinals/brc-20/activity?ticker=pepe`, }); expect(response.statusCode).toBe(200); let json = response.json(); @@ -449,7 +449,7 @@ describe('BRC-20 API', () => { expect.arrayContaining([ expect.objectContaining({ operation: 'deploy', - ticker: 'PEPE', + ticker: 'pepe', address: addressA, deploy: expect.objectContaining({ max_supply: '21000000.000000000000000000', @@ -458,7 +458,7 @@ describe('BRC-20 API', () => { ]) ); - // A mints 10000 PEPE + // A mints 10000 pepe number = inscriptionNumbers.next().value; await db.updateInscriptions( new TestChainhookPayloadBuilder() @@ -468,7 +468,7 @@ describe('BRC-20 API', () => { .brc20({ mint: { inscription_id: `${randomHash()}i0`, - tick: 'PEPE', + tick: 'pepe', address: addressA, amt: '10000', }, @@ -478,7 +478,7 @@ describe('BRC-20 API', () => { response = await fastify.inject({ method: 'GET', - url: `/ordinals/brc-20/activity?ticker=PEPE`, + url: `/ordinals/brc-20/activity?ticker=pepe`, }); expect(response.statusCode).toBe(200); json = response.json(); @@ -487,11 +487,11 @@ describe('BRC-20 API', () => { expect.arrayContaining([ expect.objectContaining({ operation: 'deploy', - ticker: 'PEPE', + ticker: 'pepe', } as Brc20ActivityResponse), expect.objectContaining({ operation: 'mint', - ticker: 'PEPE', + ticker: 'pepe', address: addressA, mint: { amount: '10000.000000000000000000', @@ -500,7 +500,7 @@ describe('BRC-20 API', () => { ]) ); - // B mints 10000 PEPE + // B mints 10000 pepe number = inscriptionNumbers.next().value; await db.updateInscriptions( new TestChainhookPayloadBuilder() @@ -510,7 +510,7 @@ describe('BRC-20 API', () => { .brc20({ mint: { inscription_id: `${randomHash()}i0`, - tick: 'PEPE', + tick: 'pepe', address: addressB, amt: '10000', }, @@ -520,7 +520,7 @@ describe('BRC-20 API', () => { response = await fastify.inject({ method: 'GET', - url: `/ordinals/brc-20/activity?ticker=PEPE`, + url: `/ordinals/brc-20/activity?ticker=pepe`, }); expect(response.statusCode).toBe(200); json = response.json(); @@ -529,7 +529,7 @@ describe('BRC-20 API', () => { expect.arrayContaining([ expect.objectContaining({ operation: 'mint', - ticker: 'PEPE', + ticker: 'pepe', address: addressB, mint: { amount: '10000.000000000000000000', @@ -538,7 +538,7 @@ describe('BRC-20 API', () => { ]) ); - // A creates transfer of 9000 PEPE + // A creates transfer of 9000 pepe const transferHash = randomHash(); number = inscriptionNumbers.next().value; await db.updateInscriptions( @@ -549,7 +549,7 @@ describe('BRC-20 API', () => { .brc20({ transfer: { inscription_id: `${transferHash}i0`, - tick: 'PEPE', + tick: 'pepe', address: addressA, amt: '9000', }, @@ -559,7 +559,7 @@ describe('BRC-20 API', () => { response = await fastify.inject({ method: 'GET', - url: `/ordinals/brc-20/activity?ticker=PEPE`, + url: `/ordinals/brc-20/activity?ticker=pepe`, }); expect(response.statusCode).toBe(200); json = response.json(); @@ -568,7 +568,7 @@ describe('BRC-20 API', () => { expect.arrayContaining([ expect.objectContaining({ operation: 'transfer', - ticker: 'PEPE', + ticker: 'pepe', address: addressA, tx_id: transferHash, transfer: { @@ -596,7 +596,7 @@ describe('BRC-20 API', () => { }) .brc20({ transfer_send: { - tick: 'PEPE', + tick: 'pepe', inscription_id: `${transferHash}i0`, amt: '9000', sender_address: addressA, @@ -608,7 +608,7 @@ describe('BRC-20 API', () => { response = await fastify.inject({ method: 'GET', - url: `/ordinals/brc-20/activity?ticker=PEPE`, + url: `/ordinals/brc-20/activity?ticker=pepe`, }); expect(response.statusCode).toBe(200); json = response.json(); @@ -617,7 +617,7 @@ describe('BRC-20 API', () => { expect.arrayContaining([ expect.objectContaining({ operation: 'transfer_send', - ticker: 'PEPE', + ticker: 'pepe', tx_id: expect.not.stringMatching(transferHash), address: addressB, transfer_send: { @@ -631,7 +631,7 @@ describe('BRC-20 API', () => { response = await fastify.inject({ method: 'GET', - url: `/ordinals/brc-20/activity?ticker=PEPE&operation=transfer_send`, + url: `/ordinals/brc-20/activity?ticker=pepe&operation=transfer_send`, }); expect(response.statusCode).toBe(200); json = response.json(); @@ -640,7 +640,7 @@ describe('BRC-20 API', () => { expect.arrayContaining([ expect.objectContaining({ operation: 'transfer_send', - ticker: 'PEPE', + ticker: 'pepe', tx_id: expect.not.stringMatching(transferHash), address: addressB, transfer_send: { @@ -679,7 +679,7 @@ describe('BRC-20 API', () => { .brc20({ deploy: { inscription_id: `${randomHash()}i0`, - tick: 'PEPE', + tick: 'pepe', max: '21000000', lim: '21000000', dec: '18', @@ -690,10 +690,10 @@ describe('BRC-20 API', () => { .build() ); - // Verify that the PEPE deploy is in the activity feed + // Verify that the pepe deploy is in the activity feed let response = await fastify.inject({ method: 'GET', - url: `/ordinals/brc-20/activity?ticker=PEPE`, + url: `/ordinals/brc-20/activity?ticker=pepe`, }); expect(response.statusCode).toBe(200); let json = response.json(); @@ -702,7 +702,7 @@ describe('BRC-20 API', () => { expect.arrayContaining([ expect.objectContaining({ operation: 'deploy', - ticker: 'PEPE', + ticker: 'pepe', address: addressA, deploy: expect.objectContaining({ max_supply: '21000000.000000000000000000', @@ -721,7 +721,7 @@ describe('BRC-20 API', () => { .brc20({ mint: { inscription_id: `${randomHash()}i0`, - tick: 'PEPE', + tick: 'pepe', address: addressA, amt: '1000', }, @@ -729,10 +729,10 @@ describe('BRC-20 API', () => { .build() ); - // Verify that the PEPE mint is in the activity feed + // Verify that the pepe mint is in the activity feed response = await fastify.inject({ method: 'GET', - url: `/ordinals/brc-20/activity?ticker=PEPE`, + url: `/ordinals/brc-20/activity?ticker=pepe`, }); expect(response.statusCode).toBe(200); json = response.json(); @@ -741,7 +741,7 @@ describe('BRC-20 API', () => { expect.arrayContaining([ expect.objectContaining({ operation: 'mint', - ticker: 'PEPE', + ticker: 'pepe', address: addressA, mint: { amount: '1000.000000000000000000', @@ -751,7 +751,7 @@ describe('BRC-20 API', () => { ); response = await fastify.inject({ method: 'GET', - url: `/ordinals/brc-20/activity?ticker=PEPE&address=${addressA}`, + url: `/ordinals/brc-20/activity?ticker=pepe&address=${addressA}`, }); expect(response.statusCode).toBe(200); json = response.json(); @@ -760,7 +760,7 @@ describe('BRC-20 API', () => { expect.arrayContaining([ expect.objectContaining({ operation: 'deploy', - ticker: 'PEPE', + ticker: 'pepe', address: addressA, deploy: expect.objectContaining({ max_supply: '21000000.000000000000000000', @@ -768,7 +768,7 @@ describe('BRC-20 API', () => { } as Brc20ActivityResponse), expect.objectContaining({ operation: 'mint', - ticker: 'PEPE', + ticker: 'pepe', address: addressA, mint: { amount: '1000.000000000000000000', @@ -787,7 +787,7 @@ describe('BRC-20 API', () => { .brc20({ mint: { inscription_id: `${randomHash()}i0`, - tick: 'PEPE', + tick: 'pepe', address: addressB, amt: '2000', }, @@ -795,10 +795,10 @@ describe('BRC-20 API', () => { .build() ); - // Verify that the PEPE mint is in the activity feed + // Verify that the pepe mint is in the activity feed response = await fastify.inject({ method: 'GET', - url: `/ordinals/brc-20/activity?ticker=PEPE`, + url: `/ordinals/brc-20/activity?ticker=pepe`, }); expect(response.statusCode).toBe(200); json = response.json(); @@ -807,7 +807,7 @@ describe('BRC-20 API', () => { expect.arrayContaining([ expect.objectContaining({ operation: 'mint', - ticker: 'PEPE', + ticker: 'pepe', address: addressB, mint: { amount: '2000.000000000000000000', @@ -827,7 +827,7 @@ describe('BRC-20 API', () => { .brc20({ transfer: { inscription_id: `${transferHashAB}i0`, - tick: 'PEPE', + tick: 'pepe', address: addressA, amt: '1000', }, @@ -835,10 +835,10 @@ describe('BRC-20 API', () => { .build() ); - // Verify that the PEPE transfer is in the activity feed + // Verify that the pepe transfer is in the activity feed response = await fastify.inject({ method: 'GET', - url: `/ordinals/brc-20/activity?ticker=PEPE`, + url: `/ordinals/brc-20/activity?ticker=pepe`, }); expect(response.statusCode).toBe(200); json = response.json(); @@ -847,7 +847,7 @@ describe('BRC-20 API', () => { expect.arrayContaining([ expect.objectContaining({ operation: 'transfer', - ticker: 'PEPE', + ticker: 'pepe', address: addressA, tx_id: transferHashAB, transfer: { @@ -859,7 +859,7 @@ describe('BRC-20 API', () => { ); response = await fastify.inject({ method: 'GET', - url: `/ordinals/brc-20/activity?ticker=PEPE&address=${addressA}`, + url: `/ordinals/brc-20/activity?ticker=pepe&address=${addressA}`, }); expect(response.statusCode).toBe(200); json = response.json(); @@ -868,7 +868,7 @@ describe('BRC-20 API', () => { expect.arrayContaining([ expect.objectContaining({ operation: 'transfer', - ticker: 'PEPE', + ticker: 'pepe', address: addressA, tx_id: transferHashAB, transfer: { @@ -890,7 +890,7 @@ describe('BRC-20 API', () => { .brc20({ transfer: { inscription_id: `${transferHashBC}i0`, - tick: 'PEPE', + tick: 'pepe', address: addressB, amt: '2000', }, @@ -898,10 +898,10 @@ describe('BRC-20 API', () => { .build() ); - // Verify that the PEPE transfer is in the activity feed + // Verify that the pepe transfer is in the activity feed response = await fastify.inject({ method: 'GET', - url: `/ordinals/brc-20/activity?ticker=PEPE`, + url: `/ordinals/brc-20/activity?ticker=pepe`, }); expect(response.statusCode).toBe(200); json = response.json(); @@ -910,7 +910,7 @@ describe('BRC-20 API', () => { expect.arrayContaining([ expect.objectContaining({ operation: 'transfer', - ticker: 'PEPE', + ticker: 'pepe', address: addressB, tx_id: transferHashBC, transfer: { @@ -941,7 +941,7 @@ describe('BRC-20 API', () => { // A gets the transfer send in its feed response = await fastify.inject({ method: 'GET', - url: `/ordinals/brc-20/activity?ticker=PEPE&address=${addressA}`, + url: `/ordinals/brc-20/activity?ticker=pepe&address=${addressA}`, }); expect(response.statusCode).toBe(200); json = response.json(); @@ -950,7 +950,7 @@ describe('BRC-20 API', () => { expect.arrayContaining([ expect.objectContaining({ operation: 'transfer_send', - ticker: 'PEPE', + ticker: 'pepe', tx_id: expect.not.stringMatching(transferHashAB), address: addressB, transfer_send: { @@ -964,7 +964,7 @@ describe('BRC-20 API', () => { // B gets the transfer send in its feed response = await fastify.inject({ method: 'GET', - url: `/ordinals/brc-20/activity?ticker=PEPE&address=${addressB}`, + url: `/ordinals/brc-20/activity?ticker=pepe&address=${addressB}`, }); expect(response.statusCode).toBe(200); json = response.json(); @@ -973,7 +973,7 @@ describe('BRC-20 API', () => { expect.arrayContaining([ expect.objectContaining({ operation: 'transfer_send', - ticker: 'PEPE', + ticker: 'pepe', tx_id: expect.not.stringMatching(transferHashAB), address: addressB, transfer_send: { @@ -985,10 +985,10 @@ describe('BRC-20 API', () => { ]) ); - // Verify that the PEPE transfer_send is in the activity feed + // Verify that the pepe transfer_send is in the activity feed response = await fastify.inject({ method: 'GET', - url: `/ordinals/brc-20/activity?ticker=PEPE`, + url: `/ordinals/brc-20/activity?ticker=pepe`, }); expect(response.statusCode).toBe(200); json = response.json(); @@ -997,7 +997,7 @@ describe('BRC-20 API', () => { expect.arrayContaining([ expect.objectContaining({ operation: 'transfer_send', - ticker: 'PEPE', + ticker: 'pepe', tx_id: expect.not.stringMatching(transferHashAB), address: addressB, transfer_send: { @@ -1027,10 +1027,10 @@ describe('BRC-20 API', () => { .build() ); - // Verify that the PEPE transfer_send is in the activity feed + // Verify that the pepe transfer_send is in the activity feed response = await fastify.inject({ method: 'GET', - url: `/ordinals/brc-20/activity?ticker=PEPE`, + url: `/ordinals/brc-20/activity?ticker=pepe`, }); expect(response.statusCode).toBe(200); json = response.json(); @@ -1039,7 +1039,7 @@ describe('BRC-20 API', () => { expect.arrayContaining([ expect.objectContaining({ operation: 'transfer_send', - ticker: 'PEPE', + ticker: 'pepe', tx_id: expect.not.stringMatching(transferHashBC), address: addressC, transfer_send: { @@ -1053,7 +1053,7 @@ describe('BRC-20 API', () => { // B gets the transfer send in its feed response = await fastify.inject({ method: 'GET', - url: `/ordinals/brc-20/activity?ticker=PEPE&address=${addressB}`, + url: `/ordinals/brc-20/activity?ticker=pepe&address=${addressB}`, }); expect(response.statusCode).toBe(200); json = response.json(); @@ -1062,7 +1062,7 @@ describe('BRC-20 API', () => { expect.arrayContaining([ expect.objectContaining({ operation: 'transfer_send', - ticker: 'PEPE', + ticker: 'pepe', tx_id: expect.not.stringMatching(transferHashBC), address: addressC, transfer_send: { @@ -1076,7 +1076,7 @@ describe('BRC-20 API', () => { // C gets the transfer send in its feed response = await fastify.inject({ method: 'GET', - url: `/ordinals/brc-20/activity?ticker=PEPE&address=${addressC}`, + url: `/ordinals/brc-20/activity?ticker=pepe&address=${addressC}`, }); expect(response.statusCode).toBe(200); json = response.json(); @@ -1085,7 +1085,7 @@ describe('BRC-20 API', () => { expect.arrayContaining([ expect.objectContaining({ operation: 'transfer_send', - ticker: 'PEPE', + ticker: 'pepe', tx_id: expect.not.stringMatching(transferHashBC), address: addressC, transfer_send: { @@ -1103,7 +1103,7 @@ describe('BRC-20 API', () => { const blockHeights = incrementing(BRC20_GENESIS_BLOCK); const addressA = 'bc1q6uwuet65rm6xvlz7ztw2gvdmmay5uaycu03mqz'; - // Step 1: Create a token PEPE + // Step 1: Create a token pepe let number = inscriptionNumbers.next().value; await db.updateInscriptions( new TestChainhookPayloadBuilder() @@ -1113,7 +1113,7 @@ describe('BRC-20 API', () => { .brc20({ deploy: { inscription_id: `${randomHash()}i0`, - tick: 'PEPE', + tick: 'pepe', max: '21000000', lim: '21000000', dec: '18', @@ -1124,7 +1124,7 @@ describe('BRC-20 API', () => { .build() ); - // Verify that the PEPE deploy is in the activity feed + // Verify that the pepe deploy is in the activity feed let response = await fastify.inject({ method: 'GET', url: `/ordinals/brc-20/activity`, @@ -1136,7 +1136,7 @@ describe('BRC-20 API', () => { expect.arrayContaining([ expect.objectContaining({ operation: 'deploy', - ticker: 'PEPE', + ticker: 'pepe', address: addressA, deploy: expect.objectContaining({ max_supply: '21000000.000000000000000000', @@ -1145,7 +1145,7 @@ describe('BRC-20 API', () => { ]) ); - // Step 2: Create a token PEER + // Step 2: Create a token peer number = inscriptionNumbers.next().value; await db.updateInscriptions( new TestChainhookPayloadBuilder() @@ -1155,7 +1155,7 @@ describe('BRC-20 API', () => { .brc20({ deploy: { inscription_id: `${randomHash()}i0`, - tick: 'PEER', + tick: 'peer', max: '21000000', lim: '21000000', dec: '18', @@ -1166,7 +1166,7 @@ describe('BRC-20 API', () => { .build() ); - // Verify that the PEER deploy is in the activity feed + // Verify that the peer deploy is in the activity feed response = await fastify.inject({ method: 'GET', url: `/ordinals/brc-20/activity`, @@ -1178,7 +1178,7 @@ describe('BRC-20 API', () => { expect.arrayContaining([ expect.objectContaining({ operation: 'deploy', - ticker: 'PEER', + ticker: 'peer', address: addressA, deploy: expect.objectContaining({ max_supply: '21000000.000000000000000000', @@ -1190,14 +1190,14 @@ describe('BRC-20 API', () => { // 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}`, + 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 + // 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}`, @@ -1209,7 +1209,7 @@ describe('BRC-20 API', () => { expect.arrayContaining([ expect.objectContaining({ operation: 'deploy', - ticker: 'PEPE', + ticker: 'pepe', address: addressA, deploy: expect.objectContaining({ max_supply: '21000000.000000000000000000', @@ -1217,11 +1217,11 @@ describe('BRC-20 API', () => { } as Brc20ActivityResponse), ]) ); - // Should NOT include PEER at this block height + // Should NOT include peer at this block height expect(json.results).not.toEqual( expect.arrayContaining([ expect.objectContaining({ - ticker: 'PEER', + ticker: 'peer', } as Brc20ActivityResponse), ]) ); @@ -1245,7 +1245,7 @@ describe('BRC-20 API', () => { .brc20({ mint: { inscription_id: '633648e0e1ddcab8dea0496a561f2b08c486ae619b5634d7bb55d7f0cd32ef16i0', - tick: 'PEPE', + tick: 'pepe', address: 'bc1qp9jgp9qtlhgvwjnxclj6kav6nr2fq09c206pyl', amt: '2000', }, @@ -1255,7 +1255,7 @@ describe('BRC-20 API', () => { const response = await fastify.inject({ method: 'GET', - url: `/ordinals/brc-20/tokens/PEPE/holders`, + url: `/ordinals/brc-20/tokens/pepe/holders`, }); expect(response.statusCode).toBe(200); const json = response.json(); @@ -1286,7 +1286,7 @@ describe('BRC-20 API', () => { .brc20({ deploy: { inscription_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', - tick: 'PEPE', + tick: 'pepe', max: '250000', lim: '250000', dec: '18', @@ -1298,7 +1298,7 @@ describe('BRC-20 API', () => { ); const response = await fastify.inject({ method: 'GET', - url: `/ordinals/brc-20/tokens/PEPE/holders`, + url: `/ordinals/brc-20/tokens/pepe/holders`, }); expect(response.statusCode).toBe(200); const json = response.json(); @@ -1309,7 +1309,7 @@ describe('BRC-20 API', () => { test('shows 404 on token not found', async () => { const response = await fastify.inject({ method: 'GET', - url: `/ordinals/brc-20/tokens/PEPE/holders`, + 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 8d538ef9..122d47cf 100644 --- a/tests/brc-20/brc20.test.ts +++ b/tests/brc-20/brc20.test.ts @@ -42,7 +42,7 @@ describe('BRC-20', () => { .brc20({ deploy: { inscription_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', - tick: 'PEPE', + tick: 'pepe', max: '21000000', lim: '1000', dec: '18', @@ -54,7 +54,7 @@ describe('BRC-20', () => { ); 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(); @@ -68,7 +68,7 @@ describe('BRC-20', () => { number: 0, mint_limit: null, max_supply: '21000000.000000000000000000', - ticker: 'PEPE', + ticker: 'pepe', tx_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', deploy_timestamp: 1677811111000, minted_supply: '0.000000000000000000', @@ -93,7 +93,7 @@ describe('BRC-20', () => { .brc20({ deploy: { inscription_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', - tick: '$PEPE', + tick: '$pepe', max: '21000000', lim: '1000', dec: '18', @@ -105,7 +105,7 @@ describe('BRC-20', () => { ); 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(); @@ -121,7 +121,7 @@ describe('BRC-20', () => { self_mint: true, minted_supply: '0.000000000000000000', number: 0, - ticker: '$PEPE', + ticker: '$pepe', tx_count: 1, tx_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', }); @@ -144,7 +144,7 @@ describe('BRC-20', () => { .brc20({ deploy: { inscription_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', - tick: 'PEPE', + tick: 'pepe', max: '21000000', lim: '250000', dec: '18', @@ -166,7 +166,7 @@ describe('BRC-20', () => { }) .brc20({ mint: { - tick: 'PEPE', + tick: 'pepe', amt: '250000', inscription_id: '8aec77f855549d98cb9fb5f35e02a03f9a2354fd05a5f89fc610b32c3b01f99fi0', address, @@ -184,7 +184,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', @@ -204,7 +204,7 @@ describe('BRC-20', () => { }) .brc20({ mint: { - tick: 'PEPE', + tick: 'pepe', amt: '100000', inscription_id: '7a1adbc3e93ddf8d7c4e0ba75aa11c98c431521dd850be8b955feedb716d8beci0', address, @@ -222,7 +222,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', @@ -231,14 +231,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' }), ]) ); }); @@ -258,7 +258,7 @@ describe('BRC-20', () => { .brc20({ deploy: { inscription_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', - tick: '$PEPE', + tick: '$pepe', max: '21000000', lim: '21000000', dec: '18', @@ -281,7 +281,7 @@ describe('BRC-20', () => { .brc20({ mint: { inscription_id: '8aec77f855549d98cb9fb5f35e02a03f9a2354fd05a5f89fc610b32c3b01f99fi0', - tick: '$PEPE', + tick: '$pepe', address, amt: '250000', }, @@ -298,7 +298,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', @@ -319,7 +319,7 @@ describe('BRC-20', () => { .brc20({ mint: { inscription_id: '7a1adbc3e93ddf8d7c4e0ba75aa11c98c431521dd850be8b955feedb716d8beci0', - tick: '$PEPE', + tick: '$pepe', address: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td', amt: '100000', }, @@ -336,7 +336,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', @@ -345,14 +345,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' }), ]) ); }); @@ -372,7 +372,7 @@ describe('BRC-20', () => { .brc20({ deploy: { inscription_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', - tick: '$PEPE', + tick: '$pepe', max: '0', lim: '250000', dec: '18', @@ -395,7 +395,7 @@ describe('BRC-20', () => { .brc20({ mint: { inscription_id: '8aec77f855549d98cb9fb5f35e02a03f9a2354fd05a5f89fc610b32c3b01f99fi0', - tick: '$PEPE', + tick: '$pepe', address, amt: '250000', }, @@ -412,7 +412,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', @@ -433,7 +433,7 @@ describe('BRC-20', () => { .brc20({ mint: { inscription_id: '7a1adbc3e93ddf8d7c4e0ba75aa11c98c431521dd850be8b955feedb716d8beci0', - tick: '$PEPE', + tick: '$pepe', address, amt: '100000', }, @@ -450,7 +450,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', @@ -459,14 +459,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' }), ]) ); }); @@ -486,7 +486,7 @@ describe('BRC-20', () => { .brc20({ deploy: { inscription_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', - tick: 'PEPE', + tick: 'pepe', max: '21000000', lim: '21000000', dec: '18', @@ -509,7 +509,7 @@ describe('BRC-20', () => { .brc20({ mint: { inscription_id: '8aec77f855549d98cb9fb5f35e02a03f9a2354fd05a5f89fc610b32c3b01f99fi0', - tick: 'PEPE', + tick: 'pepe', address, amt: '250000', }, @@ -530,7 +530,7 @@ describe('BRC-20', () => { .brc20({ mint: { inscription_id: '8aec77f855549d98cb9fb5f35e02a03f9a2354fd05a5f89fc610b32c3b01f99fi0', - tick: 'PEPE', + tick: 'pepe', address, amt: '250000', }, @@ -549,7 +549,7 @@ 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'); }); @@ -572,7 +572,7 @@ describe('BRC-20', () => { .brc20({ transfer: { inscription_id: 'eee52b22397ea4a4aefe6a39931315e93a157091f5a994216c0aa9c8c6fef47ai0', - tick: 'PEPE', + tick: 'pepe', address, amt: '2000', }, @@ -591,7 +591,7 @@ describe('BRC-20', () => { { available_balance: '8000.000000000000000000', overall_balance: '10000.000000000000000000', - ticker: 'PEPE', + ticker: 'pepe', transferrable_balance: '2000.000000000000000000', }, ]); @@ -621,7 +621,7 @@ describe('BRC-20', () => { .brc20({ transfer: { inscription_id: 'eee52b22397ea4a4aefe6a39931315e93a157091f5a994216c0aa9c8c6fef47ai0', - tick: 'PEPE', + tick: 'pepe', address, amt: '9000', }, @@ -632,7 +632,7 @@ describe('BRC-20', () => { .brc20({ transfer: { inscription_id: '7edaa48337a94da327b6262830505f116775a32db5ad4ad46e87ecea33f21baci0', - tick: 'PEPE', + tick: 'pepe', address, amt: '1000', }, @@ -651,7 +651,7 @@ describe('BRC-20', () => { { available_balance: '1000.000000000000000000', overall_balance: '10000.000000000000000000', - ticker: 'PEPE', + ticker: 'pepe', transferrable_balance: '9000.000000000000000000', }, ]); @@ -674,7 +674,7 @@ describe('BRC-20', () => { .brc20({ transfer: { inscription_id: 'eee52b22397ea4a4aefe6a39931315e93a157091f5a994216c0aa9c8c6fef47ai0', - tick: 'PEPE', + tick: 'pepe', address, amt: '9000', }, @@ -693,7 +693,7 @@ describe('BRC-20', () => { }) .brc20({ transfer_send: { - tick: 'PEPE', + tick: 'pepe', inscription_id: 'eee52b22397ea4a4aefe6a39931315e93a157091f5a994216c0aa9c8c6fef47ai0', amt: '9000', sender_address: address, @@ -714,7 +714,7 @@ describe('BRC-20', () => { { available_balance: '1000.000000000000000000', overall_balance: '1000.000000000000000000', - ticker: 'PEPE', + ticker: 'pepe', transferrable_balance: '0.000000000000000000', }, ]); @@ -730,7 +730,7 @@ describe('BRC-20', () => { { available_balance: '9000.000000000000000000', overall_balance: '9000.000000000000000000', - ticker: 'PEPE', + ticker: 'pepe', transferrable_balance: '0.000000000000000000', }, ]); @@ -766,7 +766,7 @@ describe('BRC-20', () => { .brc20({ deploy: { inscription_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', - tick: '$PEPE', + tick: '$pepe', max: '0', lim: '21000000', dec: '18', @@ -789,7 +789,7 @@ describe('BRC-20', () => { .brc20({ mint: { inscription_id: '3b55f624eaa4f8de6c42e0c490176b67123a83094384f658611faf7bfb85dd0fi0', - tick: '$PEPE', + tick: '$pepe', address, amt: '10000', }, @@ -809,7 +809,7 @@ describe('BRC-20', () => { .brc20({ transfer: { inscription_id: 'eee52b22397ea4a4aefe6a39931315e93a157091f5a994216c0aa9c8c6fef47ai0', - tick: '$PEPE', + tick: '$pepe', address, amt: '9000', }, @@ -829,7 +829,7 @@ describe('BRC-20', () => { .brc20({ transfer_send: { inscription_id: 'eee52b22397ea4a4aefe6a39931315e93a157091f5a994216c0aa9c8c6fef47ai0', - tick: '$PEPE', + tick: '$pepe', amt: '9000', sender_address: address, receiver_address: address2, @@ -849,7 +849,7 @@ describe('BRC-20', () => { { available_balance: '1000.000000000000000000', overall_balance: '1000.000000000000000000', - ticker: '$PEPE', + ticker: '$pepe', transferrable_balance: '0.000000000000000000', }, ]); @@ -865,7 +865,7 @@ describe('BRC-20', () => { { available_balance: '9000.000000000000000000', overall_balance: '9000.000000000000000000', - ticker: '$PEPE', + ticker: '$pepe', transferrable_balance: '0.000000000000000000', }, ]); @@ -888,7 +888,7 @@ describe('BRC-20', () => { .brc20({ transfer: { inscription_id: '825a25b64b5d99ca30e04e53cc9a3020412e1054eb2a7523eb075ddd6d983205i0', - tick: 'PEPE', + tick: 'pepe', address, amt: '20', }, @@ -908,7 +908,7 @@ describe('BRC-20', () => { .brc20({ transfer_send: { inscription_id: '825a25b64b5d99ca30e04e53cc9a3020412e1054eb2a7523eb075ddd6d983205i0', - tick: 'PEPE', + tick: 'pepe', amt: '20', sender_address: address, receiver_address: address2, @@ -924,7 +924,7 @@ describe('BRC-20', () => { { available_balance: '20.000000000000000000', overall_balance: '20.000000000000000000', - ticker: 'PEPE', + ticker: 'pepe', transferrable_balance: '0.000000000000000000', }, ]); @@ -941,7 +941,7 @@ describe('BRC-20', () => { .brc20({ transfer: { inscription_id: '09a812f72275892b4858880cf3821004a6e8885817159b340639afe9952ac053i0', - tick: 'PEPE', + tick: 'pepe', address: address2, amt: '20', }, @@ -956,7 +956,7 @@ describe('BRC-20', () => { { available_balance: '0.000000000000000000', overall_balance: '20.000000000000000000', - ticker: 'PEPE', + ticker: 'pepe', transferrable_balance: '20.000000000000000000', }, ]); @@ -973,7 +973,7 @@ describe('BRC-20', () => { .brc20({ transfer_send: { inscription_id: '09a812f72275892b4858880cf3821004a6e8885817159b340639afe9952ac053i0', - tick: 'PEPE', + tick: 'pepe', amt: '20', sender_address: address2, receiver_address: address2, @@ -989,7 +989,7 @@ describe('BRC-20', () => { { available_balance: '20.000000000000000000', overall_balance: '20.000000000000000000', - ticker: 'PEPE', + ticker: 'pepe', transferrable_balance: '0.000000000000000000', }, ]); @@ -1002,7 +1002,7 @@ describe('BRC-20', () => { const address2 = '3QNjwPDRafjBm9XxJpshgk3ksMJh3TFxTU'; await deployAndMintPEPE(db, address); - // Transfer and send PEPE + // Transfer and send pepe const transferPEPE = new TestChainhookPayloadBuilder() .apply() .block({ @@ -1015,7 +1015,7 @@ describe('BRC-20', () => { .brc20({ transfer: { inscription_id: 'eee52b22397ea4a4aefe6a39931315e93a157091f5a994216c0aa9c8c6fef47ai0', - tick: 'PEPE', + tick: 'pepe', address, amt: '9000', }, @@ -1034,7 +1034,7 @@ describe('BRC-20', () => { .brc20({ transfer_send: { inscription_id: 'eee52b22397ea4a4aefe6a39931315e93a157091f5a994216c0aa9c8c6fef47ai0', - tick: 'PEPE', + tick: 'pepe', amt: '9000', sender_address: address, receiver_address: address2, @@ -1136,7 +1136,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); @@ -1154,7 +1154,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', @@ -1173,7 +1173,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', @@ -1268,7 +1268,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', @@ -1311,7 +1311,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', @@ -1325,7 +1325,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', @@ -1335,14 +1335,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); @@ -1362,7 +1362,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', @@ -1372,14 +1372,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 c79be365..049ca053 100644 --- a/tests/helpers.ts +++ b/tests/helpers.ts @@ -101,6 +101,62 @@ export class TestChainhookPayloadBuilder { brc20(args: BitcoinBrc20Operation): this { this.lastBlockTx.metadata.brc20_operation = args; + if ('transfer_send' in args) { + this.lastBlockTx.metadata.ordinal_operations.push({ + inscription_transferred: { + ordinal_number: this.lastBlock.block_identifier.index, + 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: this.lastBlock.block_identifier.index, + classic: this.lastBlock.block_identifier.index, + }, + inscription_fee: 2000, + inscription_id, + inscription_output_value: 10000, + inscriber_address, + ordinal_number: this.lastBlock.block_identifier.index, + 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; } From 03adaf5ce8536ade4c107d8f8045cab520c84730 Mon Sep 17 00:00:00 2001 From: Rafael Cardenas Date: Mon, 22 Apr 2024 10:54:09 -0600 Subject: [PATCH 04/13] fix: track tx counts --- migrations/1711575178681_brc20-tokens.ts | 2 +- src/pg/brc20/brc20-pg-store.ts | 152 +++++------------------ src/pg/brc20/helpers.ts | 83 +++++++++++++ src/pg/pg-store.ts | 4 +- tests/brc-20/api.test.ts | 4 +- tests/helpers.ts | 4 +- 6 files changed, 118 insertions(+), 131 deletions(-) create mode 100644 src/pg/brc20/helpers.ts diff --git a/migrations/1711575178681_brc20-tokens.ts b/migrations/1711575178681_brc20-tokens.ts index d8ebd9d3..0017a913 100644 --- a/migrations/1711575178681_brc20-tokens.ts +++ b/migrations/1711575178681_brc20-tokens.ts @@ -51,7 +51,7 @@ export function up(pgm: MigrationBuilder): void { }, tx_count: { type: 'bigint', - default: 1, + default: 0, }, }); pgm.createIndex('brc20_tokens', ['genesis_id']); diff --git a/src/pg/brc20/brc20-pg-store.ts b/src/pg/brc20/brc20-pg-store.ts index e52fb1df..193c040c 100644 --- a/src/pg/brc20/brc20-pg-store.ts +++ b/src/pg/brc20/brc20-pg-store.ts @@ -7,7 +7,6 @@ import { LocationData, } from '../types'; import { - BRC20_DEPLOYS_COLUMNS, BRC20_OPERATIONS, DbBrc20Activity, DbBrc20Balance, @@ -23,84 +22,28 @@ import { Brc20TokenOrderBy } from '../../api/schemas'; import { objRemoveUndefinedValues } from '../helpers'; import { BitcoinEvent } from '@hirosystems/chainhook-client'; import BigNumber from 'bignumber.js'; - -function increaseOperationCount(map: Map, operation: DbBrc20Operation) { - const current = map.get(operation); - if (current == undefined) { - map.set(operation, 1); - } else { - map.set(operation, current + 1); - } -} - -function increaseTokenMintedSupply(map: Map, ticker: string, amount: BigNumber) { - const current = map.get(ticker); - if (current == undefined) { - map.set(ticker, amount); - } else { - map.set(ticker, current.plus(amount)); - } -} - -function increaseAddressOperationCount( - map: Map>, - address: string, - operation: DbBrc20Operation -) { - const current = map.get(address); - if (current == undefined) { - const opMap = new Map(); - increaseOperationCount(opMap, operation); - map.set(address, opMap); - } else { - increaseOperationCount(current, operation); - } -} - -interface AddressBalanceData { - avail: BigNumber; - trans: BigNumber; - total: BigNumber; -} -function updateAddressBalance( - map: Map>, - ticker: string, - address: string, - availBalance: BigNumber, - transBalance: BigNumber, - totalBalance: BigNumber -) { - const current = map.get(address); - if (current === undefined) { - const opMap = new Map(); - opMap.set(ticker, { avail: availBalance, trans: transBalance, total: totalBalance }); - map.set(address, opMap); - } else { - const currentTick = current.get(ticker); - if (currentTick === undefined) { - current.set(ticker, { avail: availBalance, trans: transBalance, total: totalBalance }); - } else { - current.set(ticker, { - avail: availBalance.plus(currentTick.avail), - trans: transBalance.plus(currentTick.trans), - total: totalBalance.plus(currentTick.total), - }); - } - } -} +import { + AddressBalanceData, + increaseAddressOperationCount, + increaseOperationCount, + increaseTokenMintedSupply, + increaseTokenTxCount, + updateAddressBalance, +} from './helpers'; export class Brc20PgStore extends BasePgStoreModule { sqlOr(partials: postgres.PendingQuery[] | undefined) { return partials?.reduce((acc, curr) => this.sql`${acc} OR ${curr}`); } - async updateBrc20Operations(event: BitcoinEvent): Promise { + async applyBrc20Operations(event: BitcoinEvent): Promise { await this.sqlWriteTransaction(async sql => { const block_height = event.block_identifier.index.toString(); // Keep all DB changes in memory, write them at the end. const tokens: DbBrc20TokenInsert[] = []; const operations: DbBrc20OperationInsert[] = []; const tokenMintSupplies = new Map(); + const tokenTxCounts = new Map(); const operationCounts = new Map(); const addressOperationCounts = new Map>(); const totalBalanceChanges = new Map>(); @@ -136,6 +79,7 @@ export class Brc20PgStore extends BasePgStoreModule { operation.deploy.address, DbBrc20Operation.deploy ); + increaseTokenTxCount(tokenTxCounts, operation.deploy.tick, 1); logger.info( `Brc20PgStore deploy ${operation.deploy.tick} by ${operation.deploy.address} at height ${block_height}` ); @@ -152,6 +96,7 @@ export class Brc20PgStore extends BasePgStoreModule { }); const amt = BigNumber(operation.mint.amt); increaseTokenMintedSupply(tokenMintSupplies, operation.mint.tick, amt); + increaseTokenTxCount(tokenTxCounts, operation.mint.tick, 1); increaseOperationCount(operationCounts, DbBrc20Operation.mint); increaseAddressOperationCount( addressOperationCounts, @@ -182,6 +127,7 @@ export class Brc20PgStore extends BasePgStoreModule { }); const amt = BigNumber(operation.transfer.amt); increaseOperationCount(operationCounts, DbBrc20Operation.transfer); + increaseTokenTxCount(tokenTxCounts, operation.transfer.tick, 1); increaseAddressOperationCount( addressOperationCounts, operation.transfer.address, @@ -221,6 +167,7 @@ export class Brc20PgStore extends BasePgStoreModule { }); const amt = BigNumber(operation.transfer_send.amt); increaseOperationCount(operationCounts, DbBrc20Operation.transferSend); + increaseTokenTxCount(tokenTxCounts, operation.transfer_send.tick, 1); increaseAddressOperationCount( addressOperationCounts, operation.transfer_send.sender_address, @@ -263,12 +210,15 @@ export class Brc20PgStore extends BasePgStoreModule { INSERT INTO brc20_operations ${sql(operations)} ON CONFLICT ON CONSTRAINT brc20_operations_unique DO NOTHING `; - for (const [ticker, amount] of tokenMintSupplies) { + for (const [ticker, amount] of tokenMintSupplies) await sql` UPDATE brc20_tokens SET minted_supply = minted_supply + ${amount.toString()} WHERE ticker = ${ticker} `; - } + for (const [ticker, num] of tokenTxCounts) + await sql` + UPDATE brc20_tokens SET tx_count = tx_count + ${num} WHERE ticker = ${ticker} + `; if (operationCounts.size) { const entries = []; for (const [operation, count] of operationCounts) { @@ -317,48 +267,6 @@ export class Brc20PgStore extends BasePgStoreModule { }); } - 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; - // } - // } - } - - 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; - // } - // } - } - async getTokens( args: { ticker?: string[]; order_by?: Brc20TokenOrderBy } & DbInscriptionIndexPaging ): Promise> { @@ -418,13 +326,12 @@ 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.brc20_token_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 ` @@ -451,18 +358,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 brc20_token_ticker = (SELECT ticker FROM token) AND total_balance > 0 ) SELECT *, COALESCE((SELECT count FROM holders), 0) AS holders FROM token diff --git a/src/pg/brc20/helpers.ts b/src/pg/brc20/helpers.ts new file mode 100644 index 00000000..064c11db --- /dev/null +++ b/src/pg/brc20/helpers.ts @@ -0,0 +1,83 @@ +import BigNumber from 'bignumber.js'; +import { DbBrc20Operation } from './types'; + +export function increaseOperationCount( + map: Map, + operation: DbBrc20Operation +) { + const current = map.get(operation); + if (current == undefined) { + map.set(operation, 1); + } else { + map.set(operation, current + 1); + } +} + +export function increaseTokenMintedSupply( + map: Map, + ticker: string, + amount: BigNumber +) { + const current = map.get(ticker); + if (current == undefined) { + map.set(ticker, amount); + } else { + map.set(ticker, current.plus(amount)); + } +} + +export function increaseTokenTxCount(map: Map, ticker: string, delta: number) { + const current = map.get(ticker); + if (current == undefined) { + map.set(ticker, 1); + } else { + map.set(ticker, current + delta); + } +} + +export function increaseAddressOperationCount( + map: Map>, + address: string, + operation: DbBrc20Operation +) { + const current = map.get(address); + if (current == undefined) { + const opMap = new Map(); + increaseOperationCount(opMap, operation); + map.set(address, opMap); + } else { + increaseOperationCount(current, operation); + } +} + +export interface AddressBalanceData { + avail: BigNumber; + trans: BigNumber; + total: BigNumber; +} +export function updateAddressBalance( + map: Map>, + ticker: string, + address: string, + availBalance: BigNumber, + transBalance: BigNumber, + totalBalance: BigNumber +) { + const current = map.get(address); + if (current === undefined) { + const opMap = new Map(); + opMap.set(ticker, { avail: availBalance, trans: transBalance, total: totalBalance }); + map.set(address, opMap); + } else { + const currentTick = current.get(ticker); + if (currentTick === undefined) { + current.set(ticker, { avail: availBalance, trans: transBalance, total: totalBalance }); + } else { + current.set(ticker, { + avail: availBalance.plus(currentTick.avail), + trans: transBalance.plus(currentTick.trans), + total: totalBalance.plus(currentTick.total), + }); + } + } +} diff --git a/src/pg/pg-store.ts b/src/pg/pg-store.ts index a32e0468..7f4b92d0 100644 --- a/src/pg/pg-store.ts +++ b/src/pg/pg-store.ts @@ -125,7 +125,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); + await this.brc20.applyBrc20Operations(event); logger.info( `PgStore ingested block ${event.block_identifier.index} in ${time.getElapsedSeconds()}s` ); @@ -627,7 +627,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, @@ -637,7 +636,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 index 02392f89..be0f4ee2 100644 --- a/tests/brc-20/api.test.ts +++ b/tests/brc-20/api.test.ts @@ -57,13 +57,13 @@ describe('BRC-20 API', () => { expect(response.json()).toStrictEqual({ token: { id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', - number: 0, + number: 779832, block_height: BRC20_GENESIS_BLOCK, tx_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', address: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td', ticker: 'pepe', max_supply: '21000000.000000000000000000', - mint_limit: null, + mint_limit: '21000000.000000000000000000', decimals: 18, deploy_timestamp: 1677803510000, minted_supply: '0.000000000000000000', diff --git a/tests/helpers.ts b/tests/helpers.ts index 049ca053..566dd6fa 100644 --- a/tests/helpers.ts +++ b/tests/helpers.ts @@ -206,7 +206,7 @@ export async function deployAndMintPEPE(db: PgStore, address: string) { }) .brc20({ deploy: { - tick: 'PEPE', + tick: 'pepe', max: '250000', dec: '18', lim: '250000', @@ -229,7 +229,7 @@ export async function deployAndMintPEPE(db: PgStore, address: string) { }) .brc20({ mint: { - tick: 'PEPE', + tick: 'pepe', amt: '10000', inscription_id: '3b55f624eaa4f8de6c42e0c490176b67123a83094384f658611faf7bfb85dd0fi0', address, From 2b260ceaf92c1b2c4854ffa2553df041bb24756b Mon Sep 17 00:00:00 2001 From: Rafael Cardenas Date: Mon, 22 Apr 2024 16:34:17 -0600 Subject: [PATCH 05/13] feat: rollbacks --- migrations/1711575178681_brc20-tokens.ts | 1 + migrations/1711575178682_brc20-operations.ts | 18 +- .../1711575178683_brc20-total-balances.ts | 13 +- src/api/util/helpers.ts | 15 +- src/pg/brc20/brc20-pg-store.ts | 350 +++++----- src/pg/brc20/helpers.ts | 150 ++-- src/pg/brc20/types.ts | 23 +- src/pg/pg-store.ts | 5 +- tests/brc-20/api.test.ts | 538 +++++++++------ tests/brc-20/brc20.test.ts | 652 +++++++++++------- tests/helpers.ts | 53 +- 11 files changed, 1040 insertions(+), 778 deletions(-) diff --git a/migrations/1711575178681_brc20-tokens.ts b/migrations/1711575178681_brc20-tokens.ts index 0017a913..48cd3670 100644 --- a/migrations/1711575178681_brc20-tokens.ts +++ b/migrations/1711575178681_brc20-tokens.ts @@ -55,4 +55,5 @@ export function up(pgm: MigrationBuilder): void { }, }); 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 index 7bfba2ed..9b57046c 100644 --- a/migrations/1711575178682_brc20-operations.ts +++ b/migrations/1711575178682_brc20-operations.ts @@ -16,10 +16,14 @@ export function up(pgm: MigrationBuilder): void { type: 'string', notNull: true, }, - brc20_token_ticker: { + ticker: { type: 'string', notNull: true, }, + operation: { + type: 'brc20_operation', + notNull: true, + }, block_height: { type: 'bigint', notNull: true, @@ -39,14 +43,14 @@ export function up(pgm: MigrationBuilder): void { type: 'numeric', notNull: true, }, - operation: { - type: 'brc20_operation', - notNull: true, - }, + }); + pgm.createConstraint('brc20_operations', 'brc20_operations_pkey', { + primaryKey: ['genesis_id', 'operation'], }); pgm.createConstraint( 'brc20_operations', - 'brc20_operations_unique', - 'UNIQUE(genesis_id, operation)' + 'brc20_operations_ticker_fk', + 'FOREIGN KEY(ticker) REFERENCES brc20_tokens(ticker) ON DELETE CASCADE' ); + pgm.createIndex('brc20_operations', ['block_height', 'tx_index']); } diff --git a/migrations/1711575178683_brc20-total-balances.ts b/migrations/1711575178683_brc20-total-balances.ts index f0aba616..6af59f2a 100644 --- a/migrations/1711575178683_brc20-total-balances.ts +++ b/migrations/1711575178683_brc20-total-balances.ts @@ -5,7 +5,7 @@ export const shorthands: ColumnDefinitions | undefined = undefined; export function up(pgm: MigrationBuilder): void { pgm.createTable('brc20_total_balances', { - brc20_token_ticker: { + ticker: { type: 'string', notNull: true, }, @@ -28,15 +28,12 @@ export function up(pgm: MigrationBuilder): void { }); pgm.createConstraint( 'brc20_total_balances', - 'brc20_total_balances_brc20_deploy_id_fk', - 'FOREIGN KEY(brc20_token_ticker) REFERENCES brc20_tokens(ticker) ON DELETE CASCADE' + '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: ['brc20_token_ticker', 'address'], + primaryKey: ['ticker', 'address'], }); pgm.createIndex('brc20_total_balances', ['address']); - pgm.createIndex('brc20_total_balances', [ - 'brc20_token_ticker', - { name: 'total_balance', sort: 'DESC' }, - ]); + pgm.createIndex('brc20_total_balances', ['ticker', { name: 'total_balance', sort: 'DESC' }]); } diff --git a/src/api/util/helpers.ts b/src/api/util/helpers.ts index 9b7815eb..2ad11e48 100644 --- a/src/api/util/helpers.ts +++ b/src/api/util/helpers.ts @@ -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/pg/brc20/brc20-pg-store.ts b/src/pg/brc20/brc20-pg-store.ts index 193c040c..0eb46528 100644 --- a/src/pg/brc20/brc20-pg-store.ts +++ b/src/pg/brc20/brc20-pg-store.ts @@ -1,58 +1,33 @@ -import { BasePgStoreModule, logger } from '@hirosystems/api-toolkit'; -import * as postgres from 'postgres'; -import { - DbInscriptionIndexPaging, - InscriptionData, - DbPaginatedResult, - LocationData, -} from '../types'; +import { BasePgStoreModule, PgSqlClient, batchIterate, logger } from '@hirosystems/api-toolkit'; +import { DbInscriptionIndexPaging, DbPaginatedResult } from '../types'; import { BRC20_OPERATIONS, DbBrc20Activity, DbBrc20Balance, - DbBrc20TokenInsert, DbBrc20EventOperation, DbBrc20Holder, DbBrc20Token, DbBrc20TokenWithSupply, - DbBrc20OperationInsert, DbBrc20Operation, } from './types'; import { Brc20TokenOrderBy } from '../../api/schemas'; import { objRemoveUndefinedValues } from '../helpers'; import { BitcoinEvent } from '@hirosystems/chainhook-client'; import BigNumber from 'bignumber.js'; -import { - AddressBalanceData, - increaseAddressOperationCount, - increaseOperationCount, - increaseTokenMintedSupply, - increaseTokenTxCount, - updateAddressBalance, -} from './helpers'; +import { Brc20BlockCache, sqlOr } from './helpers'; +import { INSERT_BATCH_SIZE } from '../pg-store'; export class Brc20PgStore extends BasePgStoreModule { - sqlOr(partials: postgres.PendingQuery[] | undefined) { - return partials?.reduce((acc, curr) => this.sql`${acc} OR ${curr}`); - } - - async applyBrc20Operations(event: BitcoinEvent): Promise { + async updateBrc20Operations(event: BitcoinEvent, apply: boolean = true): Promise { await this.sqlWriteTransaction(async sql => { const block_height = event.block_identifier.index.toString(); - // Keep all DB changes in memory, write them at the end. - const tokens: DbBrc20TokenInsert[] = []; - const operations: DbBrc20OperationInsert[] = []; - const tokenMintSupplies = new Map(); - const tokenTxCounts = new Map(); - const operationCounts = new Map(); - const addressOperationCounts = new Map>(); - const totalBalanceChanges = new Map>(); + 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) { - tokens.push({ + cache.tokens.push({ block_height, genesis_id: operation.deploy.inscription_id, tx_id: tx.transaction_identifier.hash, @@ -63,48 +38,39 @@ export class Brc20PgStore extends BasePgStoreModule { decimals: operation.deploy.dec, self_mint: operation.deploy.self_mint, }); - operations.push({ + cache.operations.push({ block_height, tx_index, genesis_id: operation.deploy.inscription_id, - brc20_token_ticker: operation.deploy.tick, + ticker: operation.deploy.tick, address: operation.deploy.address, avail_balance: '0', trans_balance: '0', operation: DbBrc20Operation.deploy, }); - increaseOperationCount(operationCounts, DbBrc20Operation.deploy); - increaseAddressOperationCount( - addressOperationCounts, - operation.deploy.address, - DbBrc20Operation.deploy - ); - increaseTokenTxCount(tokenTxCounts, operation.deploy.tick, 1); + cache.increaseOperationCount(DbBrc20Operation.deploy); + cache.increaseAddressOperationCount(operation.deploy.address, DbBrc20Operation.deploy); + cache.increaseTokenTxCount(operation.deploy.tick); logger.info( `Brc20PgStore deploy ${operation.deploy.tick} by ${operation.deploy.address} at height ${block_height}` ); } else if ('mint' in operation) { - operations.push({ + cache.operations.push({ block_height, tx_index, genesis_id: operation.mint.inscription_id, - brc20_token_ticker: operation.mint.tick, + 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); - increaseTokenMintedSupply(tokenMintSupplies, operation.mint.tick, amt); - increaseTokenTxCount(tokenTxCounts, operation.mint.tick, 1); - increaseOperationCount(operationCounts, DbBrc20Operation.mint); - increaseAddressOperationCount( - addressOperationCounts, - operation.mint.address, - DbBrc20Operation.mint - ); - updateAddressBalance( - totalBalanceChanges, + 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, @@ -115,26 +81,24 @@ export class Brc20PgStore extends BasePgStoreModule { `Brc20PgStore mint ${operation.mint.tick} ${operation.mint.amt} by ${operation.mint.address} at height ${block_height}` ); } else if ('transfer' in operation) { - operations.push({ + cache.operations.push({ block_height, tx_index, genesis_id: operation.transfer.inscription_id, - brc20_token_ticker: operation.transfer.tick, + 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); - increaseOperationCount(operationCounts, DbBrc20Operation.transfer); - increaseTokenTxCount(tokenTxCounts, operation.transfer.tick, 1); - increaseAddressOperationCount( - addressOperationCounts, + cache.increaseOperationCount(DbBrc20Operation.transfer); + cache.increaseTokenTxCount(operation.transfer.tick); + cache.increaseAddressOperationCount( operation.transfer.address, DbBrc20Operation.deploy ); - updateAddressBalance( - totalBalanceChanges, + cache.updateAddressBalance( operation.transfer.tick, operation.transfer.address, amt.negated(), @@ -145,49 +109,45 @@ export class Brc20PgStore extends BasePgStoreModule { `Brc20PgStore transfer ${operation.transfer.tick} ${operation.transfer.amt} by ${operation.transfer.address} at height ${block_height}` ); } else if ('transfer_send' in operation) { - operations.push({ + cache.operations.push({ block_height, tx_index, genesis_id: operation.transfer_send.inscription_id, - brc20_token_ticker: operation.transfer_send.tick, + 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, }); - operations.push({ + cache.operations.push({ block_height, tx_index, genesis_id: operation.transfer_send.inscription_id, - brc20_token_ticker: operation.transfer_send.tick, + 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); - increaseOperationCount(operationCounts, DbBrc20Operation.transferSend); - increaseTokenTxCount(tokenTxCounts, operation.transfer_send.tick, 1); - increaseAddressOperationCount( - addressOperationCounts, + cache.increaseOperationCount(DbBrc20Operation.transferSend); + cache.increaseTokenTxCount(operation.transfer_send.tick); + cache.increaseAddressOperationCount( operation.transfer_send.sender_address, DbBrc20Operation.transferSend ); - increaseAddressOperationCount( - addressOperationCounts, + cache.increaseAddressOperationCount( operation.transfer_send.receiver_address, DbBrc20Operation.transferReceive ); - updateAddressBalance( - totalBalanceChanges, + cache.updateAddressBalance( operation.transfer_send.tick, operation.transfer_send.sender_address, BigNumber('0'), amt.negated(), amt.negated() ); - updateAddressBalance( - totalBalanceChanges, + cache.updateAddressBalance( operation.transfer_send.tick, operation.transfer_send.receiver_address, amt, @@ -200,77 +160,144 @@ export class Brc20PgStore extends BasePgStoreModule { } } } - if (tokens.length) + if (apply) await this.applyOperations(sql, cache); + else await this.rollBackOperations(sql, cache); + }); + } + + 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(tokens)} + INSERT INTO brc20_tokens ${sql(batch)} ON CONFLICT (ticker) DO NOTHING `; - if (operations.length) - await sql` - INSERT INTO brc20_operations ${sql(operations)} - ON CONFLICT ON CONSTRAINT brc20_operations_unique DO NOTHING - `; - for (const [ticker, amount] of tokenMintSupplies) + if (cache.operations.length) + for await (const batch of batchIterate(cache.operations, INSERT_BATCH_SIZE)) await sql` - UPDATE brc20_tokens SET minted_supply = minted_supply + ${amount.toString()} - WHERE ticker = ${ticker} + INSERT INTO brc20_operations ${sql(batch)} + ON CONFLICT (genesis_id, operation) DO NOTHING `; - for (const [ticker, num] of tokenTxCounts) - await sql` - UPDATE brc20_tokens SET tx_count = tx_count + ${num} WHERE ticker = ${ticker} - `; - if (operationCounts.size) { - const entries = []; - for (const [operation, count] of operationCounts) { - entries.push({ operation, count }); - } + for (const [ticker, amount] of cache.tokenMintSupplies) + await sql` + UPDATE brc20_tokens SET minted_supply = minted_supply + ${amount.toString()} + WHERE ticker = ${ticker} + `; + 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(entries)} + INSERT INTO brc20_counts_by_operation ${sql(batch)} ON CONFLICT (operation) DO UPDATE SET count = brc20_counts_by_operation.count + EXCLUDED.count `; - } - if (addressOperationCounts.size) { - const entries = []; - for (const [address, map] of addressOperationCounts) { - for (const [operation, count] of map) { - entries.push({ address, operation, count }); - } - } + } + 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(entries)} + 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 (totalBalanceChanges.size) { - const entries = []; - for (const [address, map] of totalBalanceChanges) { - for (const [ticker, values] of map) { - entries.push({ - brc20_token_ticker: ticker, - address, - avail_balance: values.avail.toString(), - trans_balance: values.trans.toString(), - total_balance: values.total.toString(), - }); - } - } + } + 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(entries)} - ON CONFLICT (brc20_token_ticker, address) DO UPDATE SET + 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 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.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} + `; + for (const [ticker, num] of cache.tokenTxCounts) + await sql` + UPDATE brc20_tokens SET tx_count = tx_count - ${num} WHERE ticker = ${ticker} + `; + 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( + const tickerPrefixCondition = sqlOr( + this.sql, args.ticker?.map(t => this.sql`d.ticker LIKE LOWER(${t}) || '%'`) ); const orderBy = @@ -314,7 +341,10 @@ export class Brc20PgStore extends BasePgStoreModule { block_height?: number; } & DbInscriptionIndexPaging ): Promise> { - const ticker = this.sqlOr(args.ticker?.map(t => this.sql`d.ticker 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 })[]>` ${ @@ -327,7 +357,7 @@ export class Brc20PgStore extends BasePgStoreModule { SUM(b.avail_balance + b.trans_balance) AS total_balance, COUNT(*) OVER() as total FROM brc20_operations AS b - INNER JOIN brc20_tokens AS d ON d.ticker = b.brc20_token_ticker + INNER JOIN brc20_tokens AS d ON d.ticker = b.ticker WHERE b.address = ${args.address} AND b.block_height <= ${args.block_height} @@ -338,7 +368,7 @@ export class Brc20PgStore extends BasePgStoreModule { : 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_tokens AS d ON d.ticker = b.brc20_token_ticker + INNER JOIN brc20_tokens AS d ON d.ticker = b.ticker WHERE b.total_balance > 0 AND b.address = ${args.address} @@ -368,7 +398,7 @@ export class Brc20PgStore extends BasePgStoreModule { holders AS ( SELECT COUNT(*) AS count FROM brc20_total_balances - WHERE brc20_token_ticker = (SELECT ticker 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 @@ -426,51 +456,34 @@ export class Brc20PgStore extends BasePgStoreModule { 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})`) - ); - 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 + ${filters.operation ? sql`WHERE operation IN ${sql(filters.operation)}` : 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} + ${filters.operation ? sql`AND operation IN ${sql(filters.operation)}` : 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, d.ticker, l.genesis_id AS inscription_id, l.block_height, @@ -483,26 +496,37 @@ export class Brc20PgStore extends BasePgStoreModule { 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, + CASE + WHEN e.operation = 'transfer_send' THEN ( + SELECT address FROM brc20_operations AS o2 + WHERE o2.genesis_id = e.genesis_id AND o2.operation = 'transfer_receive' + ) + ELSE NULL + END AS to_address, ${ 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``} + ${ + filters.operation + ? sql`AND e.operation IN ${sql( + filters.operation.filter(i => i !== 'transfer_receive') + )}` + : 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 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 064c11db..b7e86a7c 100644 --- a/src/pg/brc20/helpers.ts +++ b/src/pg/brc20/helpers.ts @@ -1,83 +1,97 @@ import BigNumber from 'bignumber.js'; -import { DbBrc20Operation } from './types'; +import { DbBrc20Operation, DbBrc20OperationInsert, DbBrc20TokenInsert } from './types'; +import * as postgres from 'postgres'; +import { PgSqlClient } from '@hirosystems/api-toolkit'; -export function increaseOperationCount( - map: Map, - operation: DbBrc20Operation +export function sqlOr( + sql: PgSqlClient, + partials: postgres.PendingQuery[] | undefined ) { - const current = map.get(operation); - if (current == undefined) { - map.set(operation, 1); - } else { - map.set(operation, current + 1); - } + return partials?.reduce((acc, curr) => sql`${acc} OR ${curr}`); } -export function increaseTokenMintedSupply( - map: Map, - ticker: string, - amount: BigNumber -) { - const current = map.get(ticker); - if (current == undefined) { - map.set(ticker, amount); - } else { - map.set(ticker, current.plus(amount)); - } +export interface AddressBalanceData { + avail: BigNumber; + trans: BigNumber; + total: BigNumber; } -export function increaseTokenTxCount(map: Map, ticker: string, delta: number) { - const current = map.get(ticker); - if (current == undefined) { - map.set(ticker, 1); - } else { - map.set(ticker, current + delta); +export class Brc20BlockCache { + tokens: DbBrc20TokenInsert[] = []; + operations: DbBrc20OperationInsert[] = []; + tokenMintSupplies = new Map(); + tokenTxCounts = new Map(); + operationCounts = new Map(); + addressOperationCounts = new Map>(); + totalBalanceChanges = new Map>(); + + 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); + } } -} -export function increaseAddressOperationCount( - map: Map>, - address: string, - operation: DbBrc20Operation -) { - const current = map.get(address); - if (current == undefined) { - const opMap = new Map(); - increaseOperationCount(opMap, operation); - map.set(address, opMap); - } else { - increaseOperationCount(current, operation); + 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 interface AddressBalanceData { - avail: BigNumber; - trans: BigNumber; - total: BigNumber; -} -export function updateAddressBalance( - map: Map>, - ticker: string, - address: string, - availBalance: BigNumber, - transBalance: BigNumber, - totalBalance: BigNumber -) { - const current = map.get(address); - if (current === undefined) { - const opMap = new Map(); - opMap.set(ticker, { avail: availBalance, trans: transBalance, total: totalBalance }); - map.set(address, opMap); - } else { - const currentTick = current.get(ticker); - if (currentTick === undefined) { - current.set(ticker, { avail: availBalance, trans: transBalance, total: totalBalance }); + increaseTokenTxCount(ticker: string) { + const current = this.tokenTxCounts.get(ticker); + if (current == undefined) { + this.tokenTxCounts.set(ticker, 1); + } else { + this.tokenTxCounts.set(ticker, current + 1); + } + } + + 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); + } + } + + 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 { - current.set(ticker, { - avail: availBalance.plus(currentTick.avail), - trans: transBalance.plus(currentTick.trans), - total: totalBalance.plus(currentTick.total), - }); + const currentTick = current.get(ticker); + if (currentTick === undefined) { + current.set(ticker, { avail: availBalance, trans: transBalance, total: totalBalance }); + } else { + current.set(ticker, { + avail: availBalance.plus(currentTick.avail), + trans: transBalance.plus(currentTick.trans), + total: totalBalance.plus(currentTick.total), + }); + } } } } diff --git a/src/pg/brc20/types.ts b/src/pg/brc20/types.ts index a4721dd1..9737dfb6 100644 --- a/src/pg/brc20/types.ts +++ b/src/pg/brc20/types.ts @@ -22,7 +22,7 @@ export enum DbBrc20Operation { export type DbBrc20OperationInsert = { genesis_id: string; - brc20_token_ticker: string; + ticker: string; block_height: PgNumeric; tx_index: PgNumeric; address: string; @@ -117,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; @@ -131,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', diff --git a/src/pg/pg-store.ts b/src/pg/pg-store.ts index 7f4b92d0..21d1e8e6 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, false); for (const writeChunk of batchIterate(rollbacks, 1000)) await this.rollBackInscriptions(writeChunk); updatedBlockHeightMin = Math.min(updatedBlockHeightMin, event.block_identifier.index); @@ -125,7 +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.applyBrc20Operations(event); + await this.brc20.updateBrc20Operations(event); logger.info( `PgStore ingested block ${event.block_identifier.index} in ${time.getElapsedSeconds()}s` ); diff --git a/tests/brc-20/api.test.ts b/tests/brc-20/api.test.ts index be0f4ee2..a4e16afe 100644 --- a/tests/brc-20/api.test.ts +++ b/tests/brc-20/api.test.ts @@ -36,17 +36,21 @@ describe('BRC-20 API', () => { .transaction({ hash: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', }) - .brc20({ - deploy: { - inscription_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', - tick: 'pepe', - max: '21000000', - lim: '21000000', - dec: '18', - address: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td', - self_mint: false, + .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({ @@ -57,7 +61,7 @@ describe('BRC-20 API', () => { expect(response.json()).toStrictEqual({ token: { id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', - number: 779832, + number: 0, block_height: BRC20_GENESIS_BLOCK, tx_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', address: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td', @@ -89,17 +93,20 @@ describe('BRC-20 API', () => { .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, + .brc20( + { + deploy: { + inscription_id: `${transferHash}i0`, + tick: 'pepe', + max: '21000000', + lim: '21000000', + dec: '18', + address: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td', + self_mint: false, + }, }, - }) + { inscription_number: 0 } + ) .build() ); @@ -110,17 +117,20 @@ describe('BRC-20 API', () => { .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, + .brc20( + { + deploy: { + inscription_id: `${transferHash}i0`, + tick: 'peer', + max: '21000000', + lim: '21000000', + dec: '18', + address: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td', + self_mint: false, + }, }, - }) + { inscription_number: 1 } + ) .build() ); @@ -131,17 +141,20 @@ describe('BRC-20 API', () => { .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, + .brc20( + { + deploy: { + inscription_id: `${transferHash}i0`, + tick: 'abcd', + max: '21000000', + lim: '21000000', + dec: '18', + address: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td', + self_mint: false, + }, }, - }) + { inscription_number: 2 } + ) .build() ); @@ -152,17 +165,20 @@ describe('BRC-20 API', () => { .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, + .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({ @@ -195,17 +211,20 @@ describe('BRC-20 API', () => { .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, + .brc20( + { + deploy: { + inscription_id: `${randomHash()}i0`, + tick: 'pepe', + max: '21000000', + lim: '21000000', + dec: '18', + address: addressA, + self_mint: false, + }, }, - }) + { inscription_number: 0 } + ) .build() ); @@ -218,14 +237,17 @@ describe('BRC-20 API', () => { .apply() .block({ height: blockHeights.next().value }) .transaction({ hash: txHash }) - .brc20({ - mint: { - inscription_id: `${txHash}i0`, - tick: 'pepe', - address: addressA, - amt: '10000', + .brc20( + { + mint: { + inscription_id: `${txHash}i0`, + tick: 'pepe', + address: addressA, + amt: '10000', + }, }, - }) + { inscription_number: i + 1 } + ) .build(); pepeMints.push(payload); await db.updateInscriptions(payload); @@ -238,17 +260,20 @@ describe('BRC-20 API', () => { .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, + .brc20( + { + deploy: { + inscription_id: `${randomHash()}i0`, + tick: 'abcd', + max: '21000000', + lim: '21000000', + dec: '18', + address: addressB, + self_mint: false, + }, }, - }) + { inscription_number: 11 } + ) .build() ); @@ -259,14 +284,17 @@ describe('BRC-20 API', () => { .apply() .block({ height: blockHeights.next().value }) .transaction({ hash: randomHash() }) - .brc20({ - mint: { - inscription_id: `${randomHash()}i0`, - tick: 'abcd', - address: addressA, - amt: '10000', + .brc20( + { + mint: { + inscription_id: `${randomHash()}i0`, + tick: 'abcd', + address: addressA, + amt: '10000', + }, }, - }) + { inscription_number: 12 } + ) .build() ); @@ -278,14 +306,17 @@ describe('BRC-20 API', () => { .apply() .block({ height: blockHeights.next().value }) .transaction({ hash: txHashTransfer }) - .brc20({ - transfer: { - inscription_id: `${txHashTransfer}i0`, - tick: 'abcd', - address: addressB, - amt: '1000', + .brc20( + { + transfer: { + inscription_id: `${txHashTransfer}i0`, + tick: 'abcd', + address: addressB, + amt: '1000', + }, }, - }) + { inscription_number: 13 } + ) .build(); await db.updateInscriptions(payloadTransfer); // (send inscription, transfer_send) @@ -294,15 +325,18 @@ describe('BRC-20 API', () => { .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, + .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); @@ -423,17 +457,20 @@ describe('BRC-20 API', () => { .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, + .brc20( + { + deploy: { + inscription_id: `${randomHash()}i0`, + tick: 'pepe', + max: '21000000', + lim: '21000000', + dec: '18', + address: addressA, + self_mint: false, + }, }, - }) + { inscription_number: 0 } + ) .build() ); @@ -465,14 +502,17 @@ describe('BRC-20 API', () => { .apply() .block({ height: blockHeights.next().value }) .transaction({ hash: randomHash() }) - .brc20({ - mint: { - inscription_id: `${randomHash()}i0`, - tick: 'pepe', - address: addressA, - amt: '10000', + .brc20( + { + mint: { + inscription_id: `${randomHash()}i0`, + tick: 'pepe', + address: addressA, + amt: '10000', + }, }, - }) + { inscription_number: 1 } + ) .build() ); @@ -507,14 +547,17 @@ describe('BRC-20 API', () => { .apply() .block({ height: blockHeights.next().value }) .transaction({ hash: randomHash() }) - .brc20({ - mint: { - inscription_id: `${randomHash()}i0`, - tick: 'pepe', - address: addressB, - amt: '10000', + .brc20( + { + mint: { + inscription_id: `${randomHash()}i0`, + tick: 'pepe', + address: addressB, + amt: '10000', + }, }, - }) + { inscription_number: 2 } + ) .build() ); @@ -546,14 +589,17 @@ describe('BRC-20 API', () => { .apply() .block({ height: blockHeights.next().value }) .transaction({ hash: transferHash }) - .brc20({ - transfer: { - inscription_id: `${transferHash}i0`, - tick: 'pepe', - address: addressA, - amt: '9000', + .brc20( + { + transfer: { + inscription_id: `${transferHash}i0`, + tick: 'pepe', + address: addressA, + amt: '9000', + }, }, - }) + { inscription_number: 3 } + ) .build() ); @@ -594,15 +640,18 @@ describe('BRC-20 API', () => { satpoint_post_transfer: '7edaa48337a94da327b6262830505f116775a32db5ad4ad46e87ecea33f21bac:0:0', }) - .brc20({ - transfer_send: { - tick: 'pepe', - inscription_id: `${transferHash}i0`, - amt: '9000', - sender_address: addressA, - receiver_address: addressB, + .brc20( + { + transfer_send: { + tick: 'pepe', + inscription_id: `${transferHash}i0`, + amt: '9000', + sender_address: addressA, + receiver_address: addressB, + }, }, - }) + { inscription_number: 3 } + ) .build() ); @@ -676,17 +725,20 @@ describe('BRC-20 API', () => { .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, + .brc20( + { + deploy: { + inscription_id: `${randomHash()}i0`, + tick: 'pepe', + max: '21000000', + lim: '21000000', + dec: '18', + address: addressA, + self_mint: false, + }, }, - }) + { inscription_number: 0 } + ) .build() ); @@ -718,14 +770,17 @@ describe('BRC-20 API', () => { .apply() .block({ height: blockHeights.next().value }) .transaction({ hash: randomHash() }) - .brc20({ - mint: { - inscription_id: `${randomHash()}i0`, - tick: 'pepe', - address: addressA, - amt: '1000', + .brc20( + { + mint: { + inscription_id: `${randomHash()}i0`, + tick: 'pepe', + address: addressA, + amt: '1000', + }, }, - }) + { inscription_number: 1 } + ) .build() ); @@ -784,14 +839,17 @@ describe('BRC-20 API', () => { .apply() .block({ height: blockHeights.next().value }) .transaction({ hash: randomHash() }) - .brc20({ - mint: { - inscription_id: `${randomHash()}i0`, - tick: 'pepe', - address: addressB, - amt: '2000', + .brc20( + { + mint: { + inscription_id: `${randomHash()}i0`, + tick: 'pepe', + address: addressB, + amt: '2000', + }, }, - }) + { inscription_number: 2 } + ) .build() ); @@ -824,14 +882,17 @@ describe('BRC-20 API', () => { .apply() .block({ height: blockHeights.next().value }) .transaction({ hash: transferHashAB }) - .brc20({ - transfer: { - inscription_id: `${transferHashAB}i0`, - tick: 'pepe', - address: addressA, - amt: '1000', + .brc20( + { + transfer: { + inscription_id: `${transferHashAB}i0`, + tick: 'pepe', + address: addressA, + amt: '1000', + }, }, - }) + { inscription_number: 3 } + ) .build() ); @@ -887,14 +948,17 @@ describe('BRC-20 API', () => { .apply() .block({ height: blockHeights.next().value }) .transaction({ hash: transferHashBC }) - .brc20({ - transfer: { - inscription_id: `${transferHashBC}i0`, - tick: 'pepe', - address: addressB, - amt: '2000', + .brc20( + { + transfer: { + inscription_id: `${transferHashBC}i0`, + tick: 'pepe', + address: addressB, + amt: '2000', + }, }, - }) + { inscription_number: 4 } + ) .build() ); @@ -928,14 +992,18 @@ describe('BRC-20 API', () => { .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`, - }) + .brc20( + { + transfer_send: { + tick: 'pepe', + inscription_id: `${transferHashAB}i0`, + amt: '1000', + sender_address: addressA, + receiver_address: addressB, + }, + }, + { inscription_number: 3 } + ) .build() ); // A gets the transfer send in its feed @@ -1024,6 +1092,18 @@ describe('BRC-20 API', () => { satpoint_pre_transfer: `${transferHashBC}:0:0`, satpoint_post_transfer: `${transferHashBCSend}:0:0`, }) + .brc20( + { + transfer_send: { + tick: 'pepe', + inscription_id: `${transferHashBC}i0`, + amt: '1000', + sender_address: addressB, + receiver_address: addressC, + }, + }, + { inscription_number: 4 } + ) .build() ); @@ -1110,17 +1190,20 @@ describe('BRC-20 API', () => { .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, + .brc20( + { + deploy: { + inscription_id: `${randomHash()}i0`, + tick: 'pepe', + max: '21000000', + lim: '21000000', + dec: '18', + address: addressA, + self_mint: false, + }, }, - }) + { inscription_number: 0 } + ) .build() ); @@ -1152,17 +1235,20 @@ describe('BRC-20 API', () => { .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, + .brc20( + { + deploy: { + inscription_id: `${randomHash()}i0`, + tick: 'peer', + max: '21000000', + lim: '21000000', + dec: '18', + address: addressA, + self_mint: false, + }, }, - }) + { inscription_number: 1 } + ) .build() ); @@ -1242,14 +1328,18 @@ describe('BRC-20 API', () => { .transaction({ hash: '633648e0e1ddcab8dea0496a561f2b08c486ae619b5634d7bb55d7f0cd32ef16', }) - .brc20({ - mint: { - inscription_id: '633648e0e1ddcab8dea0496a561f2b08c486ae619b5634d7bb55d7f0cd32ef16i0', - tick: 'pepe', - address: 'bc1qp9jgp9qtlhgvwjnxclj6kav6nr2fq09c206pyl', - amt: '2000', + .brc20( + { + mint: { + inscription_id: + '633648e0e1ddcab8dea0496a561f2b08c486ae619b5634d7bb55d7f0cd32ef16i0', + tick: 'pepe', + address: 'bc1qp9jgp9qtlhgvwjnxclj6kav6nr2fq09c206pyl', + amt: '2000', + }, }, - }) + { inscription_number: 2 } + ) .build() ); @@ -1283,17 +1373,21 @@ describe('BRC-20 API', () => { .transaction({ hash: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', }) - .brc20({ - deploy: { - inscription_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', - tick: 'pepe', - max: '250000', - lim: '250000', - dec: '18', - address: 'bc1qp9jgp9qtlhgvwjnxclj6kav6nr2fq09c206pyl', - self_mint: false, + .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({ diff --git a/tests/brc-20/brc20.test.ts b/tests/brc-20/brc20.test.ts index 122d47cf..05693cd2 100644 --- a/tests/brc-20/brc20.test.ts +++ b/tests/brc-20/brc20.test.ts @@ -39,17 +39,21 @@ describe('BRC-20', () => { .transaction({ hash: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', }) - .brc20({ - deploy: { - inscription_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', - tick: 'pepe', - max: '21000000', - lim: '1000', - dec: '18', - address: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td', - self_mint: false, + .brc20( + { + deploy: { + inscription_id: + '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', + tick: 'pepe', + max: '21000000', + lim: '1000', + dec: '18', + address: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td', + self_mint: false, + }, }, - }) + { inscription_number: 0 } + ) .build() ); const response1 = await fastify.inject({ @@ -66,7 +70,7 @@ describe('BRC-20', () => { decimals: 18, id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', number: 0, - mint_limit: null, + mint_limit: '1000.000000000000000000', max_supply: '21000000.000000000000000000', ticker: 'pepe', tx_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', @@ -90,17 +94,21 @@ describe('BRC-20', () => { .transaction({ hash: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', }) - .brc20({ - deploy: { - inscription_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', - tick: '$pepe', - max: '21000000', - lim: '1000', - dec: '18', - address: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td', - self_mint: true, + .brc20( + { + deploy: { + inscription_id: + '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', + tick: '$pepe', + max: '21000000', + lim: '1000', + dec: '18', + address: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td', + self_mint: true, + }, }, - }) + { inscription_number: 0 } + ) .build() ); const response1 = await fastify.inject({ @@ -117,7 +125,7 @@ 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, @@ -141,17 +149,21 @@ describe('BRC-20', () => { .transaction({ hash: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', }) - .brc20({ - deploy: { - inscription_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', - tick: 'pepe', - max: '21000000', - lim: '250000', - dec: '18', - address: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td', - self_mint: false, + .brc20( + { + deploy: { + inscription_id: + '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', + tick: 'pepe', + max: '21000000', + lim: '250000', + dec: '18', + address: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td', + self_mint: false, + }, }, - }) + { inscription_number: 0 } + ) .build() ); await db.updateInscriptions( @@ -164,14 +176,18 @@ describe('BRC-20', () => { .transaction({ hash: '8aec77f855549d98cb9fb5f35e02a03f9a2354fd05a5f89fc610b32c3b01f99f', }) - .brc20({ - mint: { - tick: 'pepe', - amt: '250000', - inscription_id: '8aec77f855549d98cb9fb5f35e02a03f9a2354fd05a5f89fc610b32c3b01f99fi0', - address, + .brc20( + { + mint: { + tick: 'pepe', + amt: '250000', + inscription_id: + '8aec77f855549d98cb9fb5f35e02a03f9a2354fd05a5f89fc610b32c3b01f99fi0', + address, + }, }, - }) + { inscription_number: 1 } + ) .build() ); @@ -202,14 +218,18 @@ describe('BRC-20', () => { .transaction({ hash: '7a1adbc3e93ddf8d7c4e0ba75aa11c98c431521dd850be8b955feedb716d8bec', }) - .brc20({ - mint: { - tick: 'pepe', - amt: '100000', - inscription_id: '7a1adbc3e93ddf8d7c4e0ba75aa11c98c431521dd850be8b955feedb716d8beci0', - address, + .brc20( + { + mint: { + tick: 'pepe', + amt: '100000', + inscription_id: + '7a1adbc3e93ddf8d7c4e0ba75aa11c98c431521dd850be8b955feedb716d8beci0', + address, + }, }, - }) + { inscription_number: 2 } + ) .build() ); @@ -255,17 +275,21 @@ describe('BRC-20', () => { .transaction({ hash: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', }) - .brc20({ - deploy: { - inscription_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', - tick: '$pepe', - max: '21000000', - lim: '21000000', - dec: '18', - address, - self_mint: true, + .brc20( + { + deploy: { + inscription_id: + '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', + tick: '$pepe', + max: '21000000', + lim: '21000000', + dec: '18', + address, + self_mint: true, + }, }, - }) + { inscription_number: 0 } + ) .build() ); await db.updateInscriptions( @@ -278,14 +302,18 @@ describe('BRC-20', () => { .transaction({ hash: '8aec77f855549d98cb9fb5f35e02a03f9a2354fd05a5f89fc610b32c3b01f99f', }) - .brc20({ - mint: { - inscription_id: '8aec77f855549d98cb9fb5f35e02a03f9a2354fd05a5f89fc610b32c3b01f99fi0', - tick: '$pepe', - address, - amt: '250000', + .brc20( + { + mint: { + inscription_id: + '8aec77f855549d98cb9fb5f35e02a03f9a2354fd05a5f89fc610b32c3b01f99fi0', + tick: '$pepe', + address, + amt: '250000', + }, }, - }) + { inscription_number: 1 } + ) .build() ); @@ -316,14 +344,18 @@ describe('BRC-20', () => { .transaction({ hash: '7a1adbc3e93ddf8d7c4e0ba75aa11c98c431521dd850be8b955feedb716d8bec', }) - .brc20({ - mint: { - inscription_id: '7a1adbc3e93ddf8d7c4e0ba75aa11c98c431521dd850be8b955feedb716d8beci0', - tick: '$pepe', - address: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td', - amt: '100000', + .brc20( + { + mint: { + inscription_id: + '7a1adbc3e93ddf8d7c4e0ba75aa11c98c431521dd850be8b955feedb716d8beci0', + tick: '$pepe', + address: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td', + amt: '100000', + }, }, - }) + { inscription_number: 2 } + ) .build() ); @@ -369,17 +401,21 @@ describe('BRC-20', () => { .transaction({ hash: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', }) - .brc20({ - deploy: { - inscription_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', - tick: '$pepe', - max: '0', - lim: '250000', - dec: '18', - address, - self_mint: true, + .brc20( + { + deploy: { + inscription_id: + '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', + tick: '$pepe', + max: '0', + lim: '250000', + dec: '18', + address, + self_mint: true, + }, }, - }) + { inscription_number: 0 } + ) .build() ); await db.updateInscriptions( @@ -392,14 +428,18 @@ describe('BRC-20', () => { .transaction({ hash: '8aec77f855549d98cb9fb5f35e02a03f9a2354fd05a5f89fc610b32c3b01f99f', }) - .brc20({ - mint: { - inscription_id: '8aec77f855549d98cb9fb5f35e02a03f9a2354fd05a5f89fc610b32c3b01f99fi0', - tick: '$pepe', - address, - amt: '250000', + .brc20( + { + mint: { + inscription_id: + '8aec77f855549d98cb9fb5f35e02a03f9a2354fd05a5f89fc610b32c3b01f99fi0', + tick: '$pepe', + address, + amt: '250000', + }, }, - }) + { inscription_number: 1 } + ) .build() ); @@ -430,14 +470,18 @@ describe('BRC-20', () => { .transaction({ hash: '7a1adbc3e93ddf8d7c4e0ba75aa11c98c431521dd850be8b955feedb716d8bec', }) - .brc20({ - mint: { - inscription_id: '7a1adbc3e93ddf8d7c4e0ba75aa11c98c431521dd850be8b955feedb716d8beci0', - tick: '$pepe', - address, - amt: '100000', + .brc20( + { + mint: { + inscription_id: + '7a1adbc3e93ddf8d7c4e0ba75aa11c98c431521dd850be8b955feedb716d8beci0', + tick: '$pepe', + address, + amt: '100000', + }, }, - }) + { inscription_number: 2 } + ) .build() ); @@ -483,17 +527,21 @@ describe('BRC-20', () => { .transaction({ hash: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', }) - .brc20({ - deploy: { - inscription_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', - tick: 'pepe', - max: '21000000', - lim: '21000000', - dec: '18', - address, - self_mint: false, + .brc20( + { + deploy: { + inscription_id: + '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', + tick: 'pepe', + max: '21000000', + lim: '21000000', + dec: '18', + address, + self_mint: false, + }, }, - }) + { inscription_number: 0 } + ) .build() ); await db.updateInscriptions( @@ -506,14 +554,18 @@ describe('BRC-20', () => { .transaction({ hash: '8aec77f855549d98cb9fb5f35e02a03f9a2354fd05a5f89fc610b32c3b01f99f', }) - .brc20({ - mint: { - inscription_id: '8aec77f855549d98cb9fb5f35e02a03f9a2354fd05a5f89fc610b32c3b01f99fi0', - tick: 'pepe', - address, - amt: '250000', + .brc20( + { + mint: { + inscription_id: + '8aec77f855549d98cb9fb5f35e02a03f9a2354fd05a5f89fc610b32c3b01f99fi0', + tick: 'pepe', + address, + amt: '250000', + }, }, - }) + { inscription_number: 1 } + ) .build() ); // Rollback @@ -527,14 +579,18 @@ describe('BRC-20', () => { .transaction({ hash: '8aec77f855549d98cb9fb5f35e02a03f9a2354fd05a5f89fc610b32c3b01f99f', }) - .brc20({ - mint: { - inscription_id: '8aec77f855549d98cb9fb5f35e02a03f9a2354fd05a5f89fc610b32c3b01f99fi0', - tick: 'pepe', - address, - amt: '250000', + .brc20( + { + mint: { + inscription_id: + '8aec77f855549d98cb9fb5f35e02a03f9a2354fd05a5f89fc610b32c3b01f99fi0', + tick: 'pepe', + address, + amt: '250000', + }, }, - }) + { inscription_number: 1 } + ) .build() ); @@ -569,14 +625,18 @@ describe('BRC-20', () => { .transaction({ hash: 'eee52b22397ea4a4aefe6a39931315e93a157091f5a994216c0aa9c8c6fef47a', }) - .brc20({ - transfer: { - inscription_id: 'eee52b22397ea4a4aefe6a39931315e93a157091f5a994216c0aa9c8c6fef47ai0', - tick: 'pepe', - address, - amt: '2000', + .brc20( + { + transfer: { + inscription_id: + 'eee52b22397ea4a4aefe6a39931315e93a157091f5a994216c0aa9c8c6fef47ai0', + tick: 'pepe', + address, + amt: '2000', + }, }, - }) + { inscription_number: 2 } + ) .build() ); @@ -618,25 +678,33 @@ describe('BRC-20', () => { .transaction({ hash: 'eee52b22397ea4a4aefe6a39931315e93a157091f5a994216c0aa9c8c6fef47a', }) - .brc20({ - transfer: { - inscription_id: 'eee52b22397ea4a4aefe6a39931315e93a157091f5a994216c0aa9c8c6fef47ai0', - tick: 'pepe', - address, - amt: '9000', + .brc20( + { + transfer: { + inscription_id: + 'eee52b22397ea4a4aefe6a39931315e93a157091f5a994216c0aa9c8c6fef47ai0', + tick: 'pepe', + address, + amt: '9000', + }, }, - }) + { inscription_number: 2 } + ) .transaction({ hash: '7edaa48337a94da327b6262830505f116775a32db5ad4ad46e87ecea33f21bac', }) - .brc20({ - transfer: { - inscription_id: '7edaa48337a94da327b6262830505f116775a32db5ad4ad46e87ecea33f21baci0', - tick: 'pepe', - address, - amt: '1000', + .brc20( + { + transfer: { + inscription_id: + '7edaa48337a94da327b6262830505f116775a32db5ad4ad46e87ecea33f21baci0', + tick: 'pepe', + address, + amt: '1000', + }, }, - }) + { inscription_number: 3 } + ) .build() ); @@ -671,14 +739,18 @@ describe('BRC-20', () => { .transaction({ hash: 'eee52b22397ea4a4aefe6a39931315e93a157091f5a994216c0aa9c8c6fef47a', }) - .brc20({ - transfer: { - inscription_id: 'eee52b22397ea4a4aefe6a39931315e93a157091f5a994216c0aa9c8c6fef47ai0', - tick: 'pepe', - address, - amt: '9000', + .brc20( + { + transfer: { + inscription_id: + 'eee52b22397ea4a4aefe6a39931315e93a157091f5a994216c0aa9c8c6fef47ai0', + tick: 'pepe', + address, + amt: '9000', + }, }, - }) + { inscription_number: 2 } + ) .build() ); await db.updateInscriptions( @@ -691,15 +763,19 @@ describe('BRC-20', () => { .transaction({ hash: '7edaa48337a94da327b6262830505f116775a32db5ad4ad46e87ecea33f21bac', }) - .brc20({ - transfer_send: { - tick: 'pepe', - inscription_id: 'eee52b22397ea4a4aefe6a39931315e93a157091f5a994216c0aa9c8c6fef47ai0', - amt: '9000', - sender_address: address, - receiver_address: address2, + .brc20( + { + transfer_send: { + tick: 'pepe', + inscription_id: + 'eee52b22397ea4a4aefe6a39931315e93a157091f5a994216c0aa9c8c6fef47ai0', + amt: '9000', + sender_address: address, + receiver_address: address2, + }, }, - }) + { inscription_number: 2 } + ) .build() ); @@ -763,17 +839,21 @@ describe('BRC-20', () => { .transaction({ hash: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', }) - .brc20({ - deploy: { - inscription_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', - tick: '$pepe', - max: '0', - lim: '21000000', - dec: '18', - address, - self_mint: true, + .brc20( + { + deploy: { + inscription_id: + '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', + tick: '$pepe', + max: '0', + lim: '21000000', + dec: '18', + address, + self_mint: true, + }, }, - }) + { inscription_number: 0 } + ) .build() ); await db.updateInscriptions( @@ -786,14 +866,18 @@ describe('BRC-20', () => { .transaction({ hash: '3b55f624eaa4f8de6c42e0c490176b67123a83094384f658611faf7bfb85dd0f', }) - .brc20({ - mint: { - inscription_id: '3b55f624eaa4f8de6c42e0c490176b67123a83094384f658611faf7bfb85dd0fi0', - tick: '$pepe', - address, - amt: '10000', + .brc20( + { + mint: { + inscription_id: + '3b55f624eaa4f8de6c42e0c490176b67123a83094384f658611faf7bfb85dd0fi0', + tick: '$pepe', + address, + amt: '10000', + }, }, - }) + { inscription_number: 1 } + ) .build() ); await db.updateInscriptions( @@ -806,14 +890,18 @@ describe('BRC-20', () => { .transaction({ hash: 'eee52b22397ea4a4aefe6a39931315e93a157091f5a994216c0aa9c8c6fef47a', }) - .brc20({ - transfer: { - inscription_id: 'eee52b22397ea4a4aefe6a39931315e93a157091f5a994216c0aa9c8c6fef47ai0', - tick: '$pepe', - address, - amt: '9000', + .brc20( + { + transfer: { + inscription_id: + 'eee52b22397ea4a4aefe6a39931315e93a157091f5a994216c0aa9c8c6fef47ai0', + tick: '$pepe', + address, + amt: '9000', + }, }, - }) + { inscription_number: 2 } + ) .build() ); await db.updateInscriptions( @@ -826,15 +914,19 @@ describe('BRC-20', () => { .transaction({ hash: '7edaa48337a94da327b6262830505f116775a32db5ad4ad46e87ecea33f21bac', }) - .brc20({ - transfer_send: { - inscription_id: 'eee52b22397ea4a4aefe6a39931315e93a157091f5a994216c0aa9c8c6fef47ai0', - tick: '$pepe', - amt: '9000', - sender_address: address, - receiver_address: address2, + .brc20( + { + transfer_send: { + inscription_id: + 'eee52b22397ea4a4aefe6a39931315e93a157091f5a994216c0aa9c8c6fef47ai0', + tick: '$pepe', + amt: '9000', + sender_address: address, + receiver_address: address2, + }, }, - }) + { inscription_number: 2 } + ) .build() ); @@ -885,14 +977,18 @@ describe('BRC-20', () => { .transaction({ hash: '825a25b64b5d99ca30e04e53cc9a3020412e1054eb2a7523eb075ddd6d983205', }) - .brc20({ - transfer: { - inscription_id: '825a25b64b5d99ca30e04e53cc9a3020412e1054eb2a7523eb075ddd6d983205i0', - tick: 'pepe', - address, - amt: '20', + .brc20( + { + transfer: { + inscription_id: + '825a25b64b5d99ca30e04e53cc9a3020412e1054eb2a7523eb075ddd6d983205i0', + tick: 'pepe', + address, + amt: '20', + }, }, - }) + { inscription_number: 2 } + ) .build() ); await db.updateInscriptions( @@ -905,15 +1001,19 @@ describe('BRC-20', () => { .transaction({ hash: '486815e61723d03af344e1256d7e0c028a8e9e71eb38157f4bf069eb94292ee1', }) - .brc20({ - transfer_send: { - inscription_id: '825a25b64b5d99ca30e04e53cc9a3020412e1054eb2a7523eb075ddd6d983205i0', - tick: 'pepe', - amt: '20', - sender_address: address, - receiver_address: address2, + .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({ @@ -938,14 +1038,18 @@ describe('BRC-20', () => { .transaction({ hash: '09a812f72275892b4858880cf3821004a6e8885817159b340639afe9952ac053', }) - .brc20({ - transfer: { - inscription_id: '09a812f72275892b4858880cf3821004a6e8885817159b340639afe9952ac053i0', - tick: 'pepe', - address: address2, - amt: '20', + .brc20( + { + transfer: { + inscription_id: + '09a812f72275892b4858880cf3821004a6e8885817159b340639afe9952ac053i0', + tick: 'pepe', + address: address2, + amt: '20', + }, }, - }) + { inscription_number: 3 } + ) .build() ); response = await fastify.inject({ @@ -970,15 +1074,19 @@ describe('BRC-20', () => { .transaction({ hash: '26c0c3acbb1c87e682ade86220ba06e649d7599ecfc49a71495f1bdd04efbbb4', }) - .brc20({ - transfer_send: { - inscription_id: '09a812f72275892b4858880cf3821004a6e8885817159b340639afe9952ac053i0', - tick: 'pepe', - amt: '20', - sender_address: address2, - receiver_address: address2, + .brc20( + { + transfer_send: { + inscription_id: + '09a812f72275892b4858880cf3821004a6e8885817159b340639afe9952ac053i0', + tick: 'pepe', + amt: '20', + sender_address: address2, + receiver_address: address2, + }, }, - }) + { inscription_number: 3 } + ) .build() ); response = await fastify.inject({ @@ -1012,14 +1120,17 @@ describe('BRC-20', () => { .transaction({ hash: 'eee52b22397ea4a4aefe6a39931315e93a157091f5a994216c0aa9c8c6fef47a', }) - .brc20({ - transfer: { - inscription_id: 'eee52b22397ea4a4aefe6a39931315e93a157091f5a994216c0aa9c8c6fef47ai0', - tick: 'pepe', - address, - amt: '9000', + .brc20( + { + transfer: { + inscription_id: 'eee52b22397ea4a4aefe6a39931315e93a157091f5a994216c0aa9c8c6fef47ai0', + tick: 'pepe', + address, + amt: '9000', + }, }, - }) + { inscription_number: 2 } + ) .build(); await db.updateInscriptions(transferPEPE); const sendPEPE = new TestChainhookPayloadBuilder() @@ -1031,15 +1142,18 @@ describe('BRC-20', () => { .transaction({ hash: '7edaa48337a94da327b6262830505f116775a32db5ad4ad46e87ecea33f21bac', }) - .brc20({ - transfer_send: { - inscription_id: 'eee52b22397ea4a4aefe6a39931315e93a157091f5a994216c0aa9c8c6fef47ai0', - tick: 'pepe', - amt: '9000', - sender_address: address, - receiver_address: address2, + .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 @@ -1052,17 +1166,20 @@ describe('BRC-20', () => { .transaction({ hash: '8354e85e87fa2df8b3a06ec0b9d395559b95174530cb19447fc4df5f6d4ca84d', }) - .brc20({ - deploy: { - inscription_id: '8354e85e87fa2df8b3a06ec0b9d395559b95174530cb19447fc4df5f6d4ca84di0', - tick: '🔥', - max: '1000', - lim: '1000', - dec: '18', - address, - self_mint: false, + .brc20( + { + deploy: { + inscription_id: '8354e85e87fa2df8b3a06ec0b9d395559b95174530cb19447fc4df5f6d4ca84di0', + tick: '🔥', + max: '1000', + lim: '1000', + dec: '18', + address, + self_mint: false, + }, }, - }) + { inscription_number: 3 } + ) .build(); await db.updateInscriptions(deployFIRE); const mintFIRE = new TestChainhookPayloadBuilder() @@ -1074,14 +1191,17 @@ describe('BRC-20', () => { .transaction({ hash: '81f4ee2c247c5f5c0d3a6753fef706df410ea61c2aa6d370003b98beb041b887', }) - .brc20({ - mint: { - inscription_id: '81f4ee2c247c5f5c0d3a6753fef706df410ea61c2aa6d370003b98beb041b887i0', - tick: '🔥', - address, - amt: '500', + .brc20( + { + mint: { + inscription_id: '81f4ee2c247c5f5c0d3a6753fef706df410ea61c2aa6d370003b98beb041b887i0', + tick: '🔥', + address, + amt: '500', + }, }, - }) + { inscription_number: 4 } + ) .build(); await db.updateInscriptions(mintFIRE); // Transfer and send 🔥 to self @@ -1094,14 +1214,17 @@ describe('BRC-20', () => { .transaction({ hash: 'c1c7f1d5c10a30605a8a5285ca3465a4f75758ed9b7f201e5ef62727e179966f', }) - .brc20({ - transfer: { - inscription_id: 'c1c7f1d5c10a30605a8a5285ca3465a4f75758ed9b7f201e5ef62727e179966fi0', - tick: '🔥', - address, - amt: '100', + .brc20( + { + transfer: { + inscription_id: 'c1c7f1d5c10a30605a8a5285ca3465a4f75758ed9b7f201e5ef62727e179966fi0', + tick: '🔥', + address, + amt: '100', + }, }, - }) + { inscription_number: 5 } + ) .build(); await db.updateInscriptions(transferFIRE); const sendFIRE = new TestChainhookPayloadBuilder() @@ -1113,15 +1236,18 @@ describe('BRC-20', () => { .transaction({ hash: 'a00d01a3e772ce2219ddf3fe2fe4053be071262d9594f11f018fdada7179ae2d', }) - .brc20({ - transfer_send: { - tick: '🔥', - inscription_id: 'c1c7f1d5c10a30605a8a5285ca3465a4f75758ed9b7f201e5ef62727e179966fi0', - amt: '100', - sender_address: address, - receiver_address: address, + .brc20( + { + transfer_send: { + tick: '🔥', + inscription_id: 'c1c7f1d5c10a30605a8a5285ca3465a4f75758ed9b7f201e5ef62727e179966fi0', + amt: '100', + sender_address: address, + receiver_address: address, + }, }, - }) + { inscription_number: 5 } + ) .build(); await db.updateInscriptions(sendFIRE); diff --git a/tests/helpers.ts b/tests/helpers.ts index 566dd6fa..84750cf4 100644 --- a/tests/helpers.ts +++ b/tests/helpers.ts @@ -99,12 +99,15 @@ export class TestChainhookPayloadBuilder { return this; } - brc20(args: BitcoinBrc20Operation): this { + 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: this.lastBlock.block_identifier.index, + ordinal_number: opts.ordinal_number ?? opts.inscription_number, destination: { type: 'transferred', value: args.transfer_send.receiver_address, @@ -134,14 +137,14 @@ export class TestChainhookPayloadBuilder { content_type: 'text/plain;charset=utf-8', content_length: 3, inscription_number: { - jubilee: this.lastBlock.block_identifier.index, - classic: this.lastBlock.block_identifier.index, + jubilee: opts.inscription_number, + classic: opts.inscription_number, }, inscription_fee: 2000, inscription_id, inscription_output_value: 10000, inscriber_address, - ordinal_number: this.lastBlock.block_identifier.index, + 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`, @@ -204,17 +207,20 @@ export async function deployAndMintPEPE(db: PgStore, address: string) { .transaction({ hash: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', }) - .brc20({ - deploy: { - tick: 'pepe', - max: '250000', - dec: '18', - lim: '250000', - inscription_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', - address, - self_mint: false, + .brc20( + { + deploy: { + tick: 'pepe', + max: '250000', + dec: '18', + lim: '250000', + inscription_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', + address, + self_mint: false, + }, }, - }) + { inscription_number: 0 } + ) .build() ); await db.updateInscriptions( @@ -227,14 +233,17 @@ export async function deployAndMintPEPE(db: PgStore, address: string) { .transaction({ hash: '3b55f624eaa4f8de6c42e0c490176b67123a83094384f658611faf7bfb85dd0f', }) - .brc20({ - mint: { - tick: 'pepe', - amt: '10000', - inscription_id: '3b55f624eaa4f8de6c42e0c490176b67123a83094384f658611faf7bfb85dd0fi0', - address, + .brc20( + { + mint: { + tick: 'pepe', + amt: '10000', + inscription_id: '3b55f624eaa4f8de6c42e0c490176b67123a83094384f658611faf7bfb85dd0fi0', + address, + }, }, - }) + { inscription_number: 1 } + ) .build() ); } From c052d33354661b7234e2513f6a3de52649d00607 Mon Sep 17 00:00:00 2001 From: Rafael Cardenas Date: Mon, 22 Apr 2024 20:48:25 -0600 Subject: [PATCH 06/13] fix: transfer rollbacks --- migrations/1711575178682_brc20-operations.ts | 7 +++ src/pg/brc20/brc20-pg-store.ts | 58 ++++++++++---------- src/pg/brc20/helpers.ts | 1 + 3 files changed, 38 insertions(+), 28 deletions(-) diff --git a/migrations/1711575178682_brc20-operations.ts b/migrations/1711575178682_brc20-operations.ts index 9b57046c..a60053fa 100644 --- a/migrations/1711575178682_brc20-operations.ts +++ b/migrations/1711575178682_brc20-operations.ts @@ -34,6 +34,12 @@ export function up(pgm: MigrationBuilder): void { }, 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', @@ -53,4 +59,5 @@ export function up(pgm: MigrationBuilder): void { 'FOREIGN KEY(ticker) REFERENCES brc20_tokens(ticker) ON DELETE CASCADE' ); pgm.createIndex('brc20_operations', ['block_height', 'tx_index']); + pgm.createIndex('brc20_operations', ['address', 'to_address']); } diff --git a/src/pg/brc20/brc20-pg-store.ts b/src/pg/brc20/brc20-pg-store.ts index 0eb46528..9baba3ce 100644 --- a/src/pg/brc20/brc20-pg-store.ts +++ b/src/pg/brc20/brc20-pg-store.ts @@ -96,7 +96,7 @@ export class Brc20PgStore extends BasePgStoreModule { cache.increaseTokenTxCount(operation.transfer.tick); cache.increaseAddressOperationCount( operation.transfer.address, - DbBrc20Operation.deploy + DbBrc20Operation.transfer ); cache.updateAddressBalance( operation.transfer.tick, @@ -119,6 +119,10 @@ export class Brc20PgStore extends BasePgStoreModule { 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, @@ -136,10 +140,14 @@ export class Brc20PgStore extends BasePgStoreModule { operation.transfer_send.sender_address, DbBrc20Operation.transferSend ); - cache.increaseAddressOperationCount( - operation.transfer_send.receiver_address, - DbBrc20Operation.transferReceive - ); + 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, @@ -178,6 +186,11 @@ export class Brc20PgStore extends BasePgStoreModule { 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' + `; for (const [ticker, amount] of cache.tokenMintSupplies) await sql` UPDATE brc20_tokens SET minted_supply = minted_supply + ${amount.toString()} @@ -251,15 +264,6 @@ export class Brc20PgStore extends BasePgStoreModule { WHERE address = ${address} AND operation = ${operation} `; } - 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` @@ -277,6 +281,11 @@ export class Brc20PgStore extends BasePgStoreModule { await sql` 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)) @@ -455,6 +464,7 @@ export class Brc20PgStore extends BasePgStoreModule { filters.address != undefined && filters.address != ''); const needsTickerCount = filterLength === 1 && filters.ticker && filters.ticker.length > 0; + const operationsFilter = filters.operation?.filter(i => i !== 'transfer_receive'); return this.sqlTransaction(async sql => { const results = await sql<(DbBrc20Activity & { total: number })[]>` @@ -463,14 +473,14 @@ export class Brc20PgStore extends BasePgStoreModule { ? sql` SELECT COALESCE(SUM(count), 0) AS count FROM brc20_counts_by_operation - ${filters.operation ? sql`WHERE operation IN ${sql(filters.operation)}` : sql``} + ${operationsFilter ? sql`WHERE operation IN ${sql(operationsFilter)}` : sql``} ` : needsAddressEventCount ? sql` SELECT SUM(count) AS count FROM brc20_counts_by_address_operation WHERE address = ${filters.address} - ${filters.operation ? sql`AND operation IN ${sql(filters.operation)}` : sql``} + ${operationsFilter ? sql`AND operation IN ${sql(operationsFilter)}` : sql``} ` : needsTickerCount && filters.ticker !== undefined ? sql` @@ -490,19 +500,13 @@ export class Brc20PgStore extends BasePgStoreModule { l.block_hash, l.tx_id, l.address, + e.to_address, l.timestamp, l.output, l.offset, d.max AS deploy_max, d.limit AS deploy_limit, d.decimals AS deploy_decimals, - CASE - WHEN e.operation = 'transfer_send' THEN ( - SELECT address FROM brc20_operations AS o2 - WHERE o2.genesis_id = e.genesis_id AND o2.operation = 'transfer_receive' - ) - ELSE NULL - END AS to_address, ${ needsGlobalEventCount || needsAddressEventCount || needsTickerCount ? sql`(SELECT count FROM event_count)` @@ -513,17 +517,15 @@ export class Brc20PgStore extends BasePgStoreModule { 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.filter(i => i !== 'transfer_receive') - )}` + 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 to_address = ${filters.address})` + ? sql`AND (e.address = ${filters.address} OR e.to_address = ${filters.address})` : sql`` } ORDER BY e.block_height DESC, e.tx_index DESC diff --git a/src/pg/brc20/helpers.ts b/src/pg/brc20/helpers.ts index b7e86a7c..1d04e767 100644 --- a/src/pg/brc20/helpers.ts +++ b/src/pg/brc20/helpers.ts @@ -24,6 +24,7 @@ export class Brc20BlockCache { operationCounts = new Map(); addressOperationCounts = new Map>(); totalBalanceChanges = new Map>(); + transferReceivers = new Map(); increaseOperationCount(operation: DbBrc20Operation) { this.increaseOperationCountInternal(this.operationCounts, operation); From 8c8b6a5f32a4f59d11562382896ed082ad4f0ff9 Mon Sep 17 00:00:00 2001 From: Rafael Cardenas Date: Mon, 22 Apr 2024 21:03:26 -0600 Subject: [PATCH 07/13] fix: activity addresses --- src/api/util/helpers.ts | 2 +- src/pg/brc20/brc20-pg-store.ts | 4 ++-- tests/brc-20/api.test.ts | 9 --------- tests/brc-20/brc20.test.ts | 4 ++-- 4 files changed, 5 insertions(+), 14 deletions(-) diff --git a/src/api/util/helpers.ts b/src/api/util/helpers.ts index 2ad11e48..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}`, diff --git a/src/pg/brc20/brc20-pg-store.ts b/src/pg/brc20/brc20-pg-store.ts index 9baba3ce..d31ec7b7 100644 --- a/src/pg/brc20/brc20-pg-store.ts +++ b/src/pg/brc20/brc20-pg-store.ts @@ -494,13 +494,13 @@ export class Brc20PgStore extends BasePgStoreModule { 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, - e.to_address, l.timestamp, l.output, l.offset, diff --git a/tests/brc-20/api.test.ts b/tests/brc-20/api.test.ts index a4e16afe..e38d7b87 100644 --- a/tests/brc-20/api.test.ts +++ b/tests/brc-20/api.test.ts @@ -631,15 +631,6 @@ describe('BRC-20 API', () => { .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', - }) .brc20( { transfer_send: { diff --git a/tests/brc-20/brc20.test.ts b/tests/brc-20/brc20.test.ts index 05693cd2..4bd7fef3 100644 --- a/tests/brc-20/brc20.test.ts +++ b/tests/brc-20/brc20.test.ts @@ -717,10 +717,10 @@ 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', + transferrable_balance: '10000.000000000000000000', }, ]); }); From 13f95d5811696fccd552337f648897d8d4555890 Mon Sep 17 00:00:00 2001 From: Rafael Cardenas Date: Mon, 22 Apr 2024 21:55:11 -0600 Subject: [PATCH 08/13] fix: multiple transfer test --- tests/brc-20/api.test.ts | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/tests/brc-20/api.test.ts b/tests/brc-20/api.test.ts index e38d7b87..642609eb 100644 --- a/tests/brc-20/api.test.ts +++ b/tests/brc-20/api.test.ts @@ -728,7 +728,7 @@ describe('BRC-20 API', () => { self_mint: false, }, }, - { inscription_number: 0 } + { inscription_number: number } ) .build() ); @@ -770,7 +770,7 @@ describe('BRC-20 API', () => { amt: '1000', }, }, - { inscription_number: 1 } + { inscription_number: number } ) .build() ); @@ -839,7 +839,7 @@ describe('BRC-20 API', () => { amt: '2000', }, }, - { inscription_number: 2 } + { inscription_number: number } ) .build() ); @@ -882,7 +882,7 @@ describe('BRC-20 API', () => { amt: '1000', }, }, - { inscription_number: 3 } + { inscription_number: numberAB } ) .build() ); @@ -948,7 +948,7 @@ describe('BRC-20 API', () => { amt: '2000', }, }, - { inscription_number: 4 } + { inscription_number: numberBC } ) .build() ); @@ -993,7 +993,7 @@ describe('BRC-20 API', () => { receiver_address: addressB, }, }, - { inscription_number: 3 } + { inscription_number: numberAB } ) .build() ); @@ -1075,25 +1075,17 @@ describe('BRC-20 API', () => { .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`, - }) .brc20( { transfer_send: { tick: 'pepe', inscription_id: `${transferHashBC}i0`, - amt: '1000', + amt: '2000', sender_address: addressB, receiver_address: addressC, }, }, - { inscription_number: 4 } + { inscription_number: numberBC } ) .build() ); From 0e40d32e58d5044e2904bfae3e4b149c000bf796 Mon Sep 17 00:00:00 2001 From: Rafael Cardenas Date: Mon, 22 Apr 2024 22:02:51 -0600 Subject: [PATCH 09/13] fix: holders --- src/pg/brc20/brc20-pg-store.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/pg/brc20/brc20-pg-store.ts b/src/pg/brc20/brc20-pg-store.ts index d31ec7b7..28e976e5 100644 --- a/src/pg/brc20/brc20-pg-store.ts +++ b/src/pg/brc20/brc20-pg-store.ts @@ -422,15 +422,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} `; From 19441155b2fc556f16cb0a6e281e44f6cf3d2c7f Mon Sep 17 00:00:00 2001 From: Rafael Cardenas Date: Mon, 22 Apr 2024 22:18:41 -0600 Subject: [PATCH 10/13] fix: operation indexes --- migrations/1711575178682_brc20-operations.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/migrations/1711575178682_brc20-operations.ts b/migrations/1711575178682_brc20-operations.ts index a60053fa..42582577 100644 --- a/migrations/1711575178682_brc20-operations.ts +++ b/migrations/1711575178682_brc20-operations.ts @@ -58,6 +58,11 @@ export function up(pgm: MigrationBuilder): void { 'brc20_operations_ticker_fk', 'FOREIGN KEY(ticker) REFERENCES brc20_tokens(ticker) ON DELETE CASCADE' ); - pgm.createIndex('brc20_operations', ['block_height', 'tx_index']); + 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']); } From 5be32526eadc4f41d797a57bd1d33429d2a31ced Mon Sep 17 00:00:00 2001 From: Rafael Cardenas Date: Tue, 23 Apr 2024 00:07:20 -0600 Subject: [PATCH 11/13] fix: style --- src/pg/brc20/brc20-pg-store.ts | 14 ++++++-------- src/pg/pg-store.ts | 4 ++-- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/pg/brc20/brc20-pg-store.ts b/src/pg/brc20/brc20-pg-store.ts index 28e976e5..1699e041 100644 --- a/src/pg/brc20/brc20-pg-store.ts +++ b/src/pg/brc20/brc20-pg-store.ts @@ -1,10 +1,8 @@ import { BasePgStoreModule, PgSqlClient, batchIterate, logger } from '@hirosystems/api-toolkit'; import { DbInscriptionIndexPaging, DbPaginatedResult } from '../types'; import { - BRC20_OPERATIONS, DbBrc20Activity, DbBrc20Balance, - DbBrc20EventOperation, DbBrc20Holder, DbBrc20Token, DbBrc20TokenWithSupply, @@ -18,7 +16,7 @@ import { Brc20BlockCache, sqlOr } from './helpers'; import { INSERT_BATCH_SIZE } from '../pg-store'; export class Brc20PgStore extends BasePgStoreModule { - async updateBrc20Operations(event: BitcoinEvent, apply: boolean = true): Promise { + 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(); @@ -52,7 +50,7 @@ export class Brc20PgStore extends BasePgStoreModule { cache.increaseAddressOperationCount(operation.deploy.address, DbBrc20Operation.deploy); cache.increaseTokenTxCount(operation.deploy.tick); logger.info( - `Brc20PgStore deploy ${operation.deploy.tick} by ${operation.deploy.address} at height ${block_height}` + `Brc20PgStore ${direction} deploy ${operation.deploy.tick} by ${operation.deploy.address} at height ${block_height}` ); } else if ('mint' in operation) { cache.operations.push({ @@ -78,7 +76,7 @@ export class Brc20PgStore extends BasePgStoreModule { amt ); logger.info( - `Brc20PgStore mint ${operation.mint.tick} ${operation.mint.amt} by ${operation.mint.address} at height ${block_height}` + `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({ @@ -106,7 +104,7 @@ export class Brc20PgStore extends BasePgStoreModule { BigNumber(0) ); logger.info( - `Brc20PgStore transfer ${operation.transfer.tick} ${operation.transfer.amt} by ${operation.transfer.address} at height ${block_height}` + `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({ @@ -163,12 +161,12 @@ export class Brc20PgStore extends BasePgStoreModule { amt ); logger.info( - `Brc20PgStore 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}` + `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}` ); } } } - if (apply) await this.applyOperations(sql, cache); + if (direction === 'apply') await this.applyOperations(sql, cache); else await this.rollBackOperations(sql, cache); }); } diff --git a/src/pg/pg-store.ts b/src/pg/pg-store.ts index 21d1e8e6..433cf3b3 100644 --- a/src/pg/pg-store.ts +++ b/src/pg/pg-store.ts @@ -92,7 +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, false); + 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); @@ -126,7 +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); + await this.brc20.updateBrc20Operations(event, 'apply'); logger.info( `PgStore ingested block ${event.block_identifier.index} in ${time.getElapsedSeconds()}s` ); From 164c15cf593d2d6416652f94641947a3e7fb931d Mon Sep 17 00:00:00 2001 From: Rafael Cardenas Date: Tue, 23 Apr 2024 09:51:28 -0600 Subject: [PATCH 12/13] chore: upgrade chainhook client --- package-lock.json | 60 ++++++++++------------------------------------- package.json | 2 +- 2 files changed, 14 insertions(+), 48 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4172dece..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": "file:../chainhook/components/client/typescript", + "@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,34 +95,6 @@ "node": ">=18" } }, - "../chainhook/components/client/typescript": { - "name": "@hirosystems/chainhook-client", - "version": "1.4.2", - "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", @@ -1298,8 +1270,15 @@ } }, "node_modules/@hirosystems/chainhook-client": { - "resolved": "../chainhook/components/client/typescript", - "link": true + "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", + "pino": "^8.11.0", + "undici": "^5.21.2" + } }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.7", @@ -19735,26 +19714,13 @@ } }, "@hirosystems/chainhook-client": { - "version": "file:../chainhook/components/client/typescript", + "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", - "@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", "fastify": "^4.15.0", - "jest": "^29.5.0", "pino": "^8.11.0", - "prettier": "^2.8.7", - "rimraf": "^4.4.1", - "ts-jest": "^29.0.5", - "ts-node": "^10.9.1", - "typescript": "^5.0.2", "undici": "^5.21.2" } }, diff --git a/package.json b/package.json index fa9ce8a5..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": "file:../chainhook/components/client/typescript", + "@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", From 82eeeed40e9f67fd9b2426b5ca0de5178e3c823c Mon Sep 17 00:00:00 2001 From: Rafael Cardenas Date: Tue, 23 Apr 2024 09:57:27 -0600 Subject: [PATCH 13/13] fix: add brc20 to default predicate --- src/ordhook/server.ts | 1 + 1 file changed, 1 insertion(+) 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