diff --git a/migrations/1734712921681_drop-nft-custody-unanchored.js b/migrations/1734712921681_drop-nft-custody-unanchored.js new file mode 100644 index 000000000..e3818ca6a --- /dev/null +++ b/migrations/1734712921681_drop-nft-custody-unanchored.js @@ -0,0 +1,87 @@ +/* eslint-disable camelcase */ + +exports.shorthands = undefined; + +exports.up = pgm => { + pgm.dropTable('nft_custody_unanchored'); +}; + +exports.down = pgm => { + pgm.createTable('nft_custody_unanchored', { + asset_identifier: { + type: 'string', + notNull: true, + }, + value: { + type: 'bytea', + notNull: true, + }, + recipient: { + type: 'text', + }, + block_height: { + type: 'integer', + notNull: true, + }, + index_block_hash: { + type: 'bytea', + notNull: true, + }, + parent_index_block_hash: { + type: 'bytea', + notNull: true, + }, + microblock_hash: { + type: 'bytea', + notNull: true, + }, + microblock_sequence: { + type: 'integer', + notNull: true, + }, + tx_id: { + type: 'bytea', + notNull: true, + }, + tx_index: { + type: 'smallint', + notNull: true, + }, + event_index: { + type: 'integer', + notNull: true, + }, + }); + pgm.createConstraint('nft_custody_unanchored', 'nft_custody_unanchored_unique', 'UNIQUE(asset_identifier, value)'); + pgm.createIndex('nft_custody_unanchored', ['recipient', 'asset_identifier']); + pgm.createIndex('nft_custody_unanchored', 'value'); + pgm.createIndex('nft_custody_unanchored', [ + { name: 'block_height', sort: 'DESC' }, + { name: 'microblock_sequence', sort: 'DESC' }, + { name: 'tx_index', sort: 'DESC' }, + { name: 'event_index', sort: 'DESC' } + ]); + pgm.sql(` + INSERT INTO nft_custody_unanchored (asset_identifier, value, recipient, tx_id, block_height, index_block_hash, parent_index_block_hash, microblock_hash, microblock_sequence, tx_index, event_index) ( + SELECT + DISTINCT ON(asset_identifier, value) asset_identifier, value, recipient, tx_id, nft.block_height, + nft.index_block_hash, nft.parent_index_block_hash, nft.microblock_hash, nft.microblock_sequence, nft.tx_index, nft.event_index + FROM + nft_events AS nft + INNER JOIN + txs USING (tx_id) + WHERE + txs.canonical = true + AND txs.microblock_canonical = true + AND nft.canonical = true + AND nft.microblock_canonical = true + ORDER BY + asset_identifier, + value, + txs.block_height DESC, + txs.microblock_sequence DESC, + txs.tx_index DESC, + nft.event_index DESC + ) + `); +}; diff --git a/src/api/routes/tokens.ts b/src/api/routes/tokens.ts index 101ba2084..60775e653 100644 --- a/src/api/routes/tokens.ts +++ b/src/api/routes/tokens.ts @@ -49,7 +49,6 @@ export const TokenRoutes: FastifyPluginAsync< ), limit: LimitParam(ResourceType.Token, 'Limit', 'max number of tokens to fetch'), offset: OffsetParam('Offset', 'index of first tokens to fetch'), - unanchored: UnanchoredParamSchema, tx_metadata: Type.Boolean({ default: false, description: @@ -95,7 +94,6 @@ export const TokenRoutes: FastifyPluginAsync< const limit = getPagingQueryLimit(ResourceType.Token, req.query.limit); const offset = parsePagingQueryInput(req.query.offset ?? 0); - const includeUnanchored = req.query.unanchored ?? false; const includeTxMetadata = req.query.tx_metadata ?? false; const { results, total } = await fastify.db.getNftHoldings({ @@ -103,7 +101,6 @@ export const TokenRoutes: FastifyPluginAsync< assetIdentifiers: assetIdentifiers, offset: offset, limit: limit, - includeUnanchored: includeUnanchored, includeTxMetadata: includeTxMetadata, }); const parsedResults = results.map(result => { diff --git a/src/datastore/pg-store.ts b/src/datastore/pg-store.ts index 4bf5871e8..92859f119 100644 --- a/src/datastore/pg-store.ts +++ b/src/datastore/pg-store.ts @@ -3356,16 +3356,12 @@ export class PgStore extends BasePgStore { assetIdentifiers?: string[]; limit: number; offset: number; - includeUnanchored: boolean; includeTxMetadata: boolean; }): Promise<{ results: NftHoldingInfoWithTxMetadata[]; total: number }> { const queryArgs: (string | string[] | number)[] = [args.principal, args.limit, args.offset]; if (args.assetIdentifiers) { queryArgs.push(args.assetIdentifiers); } - const nftCustody = args.includeUnanchored - ? this.sql(`nft_custody_unanchored`) - : this.sql(`nft_custody`); const assetIdFilter = args.assetIdentifiers && args.assetIdentifiers.length > 0 ? this.sql`AND nft.asset_identifier IN ${this.sql(args.assetIdentifiers)}` @@ -3375,7 +3371,7 @@ export class PgStore extends BasePgStore { >` WITH nft AS ( SELECT *, (COUNT(*) OVER())::INTEGER AS count - FROM ${nftCustody} AS nft + FROM nft_custody AS nft WHERE nft.recipient = ${args.principal} ${assetIdFilter} ORDER BY block_height DESC, microblock_sequence DESC, tx_index DESC, event_index DESC diff --git a/src/datastore/pg-write-store.ts b/src/datastore/pg-write-store.ts index 99cf8f0aa..bf4691e53 100644 --- a/src/datastore/pg-write-store.ts +++ b/src/datastore/pg-write-store.ts @@ -1416,10 +1416,9 @@ export class PgWriteStore extends PgStore { INSERT INTO nft_events ${sql(nftEventInserts)} `; if (tx.canonical && tx.microblock_canonical) { - const table = microblock ? sql`nft_custody_unanchored` : sql`nft_custody`; await sql` - INSERT INTO ${table} ${sql(Array.from(custodyInsertsMap.values()))} - ON CONFLICT ON CONSTRAINT ${table}_unique DO UPDATE SET + INSERT INTO nft_custody ${sql(Array.from(custodyInsertsMap.values()))} + ON CONFLICT ON CONSTRAINT nft_custody_unique DO UPDATE SET tx_id = EXCLUDED.tx_id, index_block_hash = EXCLUDED.index_block_hash, parent_index_block_hash = EXCLUDED.parent_index_block_hash, @@ -1431,22 +1430,22 @@ export class PgWriteStore extends PgStore { block_height = EXCLUDED.block_height WHERE ( - EXCLUDED.block_height > ${table}.block_height + EXCLUDED.block_height > nft_custody.block_height ) OR ( - EXCLUDED.block_height = ${table}.block_height - AND EXCLUDED.microblock_sequence > ${table}.microblock_sequence + EXCLUDED.block_height = nft_custody.block_height + AND EXCLUDED.microblock_sequence > nft_custody.microblock_sequence ) OR ( - EXCLUDED.block_height = ${table}.block_height - AND EXCLUDED.microblock_sequence = ${table}.microblock_sequence - AND EXCLUDED.tx_index > ${table}.tx_index + EXCLUDED.block_height = nft_custody.block_height + AND EXCLUDED.microblock_sequence = nft_custody.microblock_sequence + AND EXCLUDED.tx_index > nft_custody.tx_index ) OR ( - EXCLUDED.block_height = ${table}.block_height - AND EXCLUDED.microblock_sequence = ${table}.microblock_sequence - AND EXCLUDED.tx_index = ${table}.tx_index - AND EXCLUDED.event_index > ${table}.event_index + EXCLUDED.block_height = nft_custody.block_height + AND EXCLUDED.microblock_sequence = nft_custody.microblock_sequence + AND EXCLUDED.tx_index = nft_custody.tx_index + AND EXCLUDED.event_index > nft_custody.event_index ) `; } @@ -2515,10 +2514,6 @@ export class PgWriteStore extends PgStore { AND (index_block_hash = ${args.indexBlockHash} OR index_block_hash = '\\x'::bytea) AND tx_id IN ${sql(txIds)} `; - await this.updateNftCustodyFromReOrg(sql, { - index_block_hash: args.indexBlockHash, - microblocks: args.microblocks, - }); } // Update unanchored tx count in `chain_tip` table @@ -2539,54 +2534,46 @@ export class PgWriteStore extends PgStore { sql: PgSqlClient, args: { index_block_hash: string; - microblocks: string[]; } ): Promise { - for (const table of [sql`nft_custody`, sql`nft_custody_unanchored`]) { - await sql` - INSERT INTO ${table} - (asset_identifier, value, tx_id, index_block_hash, parent_index_block_hash, microblock_hash, - microblock_sequence, recipient, event_index, tx_index, block_height) - ( - SELECT - DISTINCT ON(asset_identifier, value) asset_identifier, value, tx_id, txs.index_block_hash, - txs.parent_index_block_hash, txs.microblock_hash, txs.microblock_sequence, recipient, - nft.event_index, txs.tx_index, txs.block_height - FROM - nft_events AS nft - INNER JOIN - txs USING (tx_id) - WHERE - txs.canonical = true - AND txs.microblock_canonical = true - AND nft.canonical = true - AND nft.microblock_canonical = true - AND nft.index_block_hash = ${args.index_block_hash} - ${ - args.microblocks.length > 0 - ? sql`AND nft.microblock_hash IN ${sql(args.microblocks)}` - : sql`` - } - ORDER BY - asset_identifier, - value, - txs.block_height DESC, - txs.microblock_sequence DESC, - txs.tx_index DESC, - nft.event_index DESC - ) - ON CONFLICT ON CONSTRAINT ${table}_unique DO UPDATE SET - tx_id = EXCLUDED.tx_id, - index_block_hash = EXCLUDED.index_block_hash, - parent_index_block_hash = EXCLUDED.parent_index_block_hash, - microblock_hash = EXCLUDED.microblock_hash, - microblock_sequence = EXCLUDED.microblock_sequence, - recipient = EXCLUDED.recipient, - event_index = EXCLUDED.event_index, - tx_index = EXCLUDED.tx_index, - block_height = EXCLUDED.block_height - `; - } + await sql` + INSERT INTO nft_custody + (asset_identifier, value, tx_id, index_block_hash, parent_index_block_hash, microblock_hash, + microblock_sequence, recipient, event_index, tx_index, block_height) + ( + SELECT + DISTINCT ON(asset_identifier, value) asset_identifier, value, tx_id, txs.index_block_hash, + txs.parent_index_block_hash, txs.microblock_hash, txs.microblock_sequence, recipient, + nft.event_index, txs.tx_index, txs.block_height + FROM + nft_events AS nft + INNER JOIN + txs USING (tx_id) + WHERE + txs.canonical = true + AND txs.microblock_canonical = true + AND nft.canonical = true + AND nft.microblock_canonical = true + AND nft.index_block_hash = ${args.index_block_hash} + ORDER BY + asset_identifier, + value, + txs.block_height DESC, + txs.microblock_sequence DESC, + txs.tx_index DESC, + nft.event_index DESC + ) + ON CONFLICT ON CONSTRAINT nft_custody_unique DO UPDATE SET + tx_id = EXCLUDED.tx_id, + index_block_hash = EXCLUDED.index_block_hash, + parent_index_block_hash = EXCLUDED.parent_index_block_hash, + microblock_hash = EXCLUDED.microblock_hash, + microblock_sequence = EXCLUDED.microblock_sequence, + recipient = EXCLUDED.recipient, + event_index = EXCLUDED.event_index, + tx_index = EXCLUDED.tx_index, + block_height = EXCLUDED.block_height + `; } /** @@ -3050,10 +3037,7 @@ export class PgWriteStore extends PgStore { updatedEntities.markedNonCanonical.nftEvents += nftResult.count; } if (nftResult.count) - await this.updateNftCustodyFromReOrg(sql, { - index_block_hash: indexBlockHash, - microblocks: [], - }); + await this.updateNftCustodyFromReOrg(sql, { index_block_hash: indexBlockHash }); }); q.enqueue(async () => { const pox2Result = await sql` diff --git a/tests/api/datastore.test.ts b/tests/api/datastore.test.ts index e2f4174ae..22d2b664b 100644 --- a/tests/api/datastore.test.ts +++ b/tests/api/datastore.test.ts @@ -5884,7 +5884,6 @@ describe('postgres datastore', () => { limit: 10, offset: 0, includeTxMetadata: false, - includeUnanchored: true, }) ).resolves.not.toThrow(); // Tx list details with empty txIds diff --git a/tests/api/token.test.ts b/tests/api/token.test.ts index d6f55e5f6..554ac7fb6 100644 --- a/tests/api/token.test.ts +++ b/tests/api/token.test.ts @@ -164,24 +164,6 @@ describe('/extended/v1/tokens tests', () => { .build(); await db.updateMicroblocks(microblock1); - // Request: unanchored shows addr2 with 0 NFTs - const request7 = await supertest(api.server).get( - `/extended/v1/tokens/nft/holdings?principal=${addr2}&unanchored=true` - ); - expect(request7.status).toBe(200); - expect(request7.type).toBe('application/json'); - const result7 = JSON.parse(request7.text); - expect(result7.total).toEqual(0); - - // Request: anchored shows addr2 still with 1 NFT - const request8 = await supertest(api.server).get( - `/extended/v1/tokens/nft/holdings?principal=${addr2}` - ); - expect(request8.status).toBe(200); - expect(request8.type).toBe('application/json'); - const result8 = JSON.parse(request8.text); - expect(result8.total).toEqual(1); - // Confirm unanchored txs const block4 = new TestBlockBuilder({ block_height: 4, @@ -189,19 +171,16 @@ describe('/extended/v1/tokens tests', () => { parent_index_block_hash: '0x03', parent_microblock_hash: '0x11', }) - .addTx({ tx_id: '0x5555' }) + .addTx({ tx_id: '0x5499' }) + .addTxNftEvent({ + asset_identifier: assetId2, + asset_event_type_id: DbAssetEventTypeId.Transfer, + sender: addr2, + recipient: addr3, + }) .build(); await db.update(block4); - // Request: unanchored still shows addr2 with 0 NFTs - const request9 = await supertest(api.server).get( - `/extended/v1/tokens/nft/holdings?principal=${addr2}&unanchored=true` - ); - expect(request9.status).toBe(200); - expect(request9.type).toBe('application/json'); - const result9 = JSON.parse(request9.text); - expect(result9.total).toEqual(0); - // Request: anchored now shows addr2 with 0 NFTs const request10 = await supertest(api.server).get( `/extended/v1/tokens/nft/holdings?principal=${addr2}` @@ -249,31 +228,19 @@ describe('/extended/v1/tokens tests', () => { .build(); await db.updateMicroblocks(microblock2); - // Request: addr2 still has 0 NFTs unanchored - const request12 = await supertest(api.server).get( - `/extended/v1/tokens/nft/holdings?principal=${addr2}&unanchored=true` - ); - expect(request12.status).toBe(200); - expect(request12.type).toBe('application/json'); - const result12 = JSON.parse(request12.text); - expect(result12.total).toEqual(0); - - // Request: addr2 still has 0 NFTs anchored - const request13 = await supertest(api.server).get( - `/extended/v1/tokens/nft/holdings?principal=${addr2}` - ); - expect(request13.status).toBe(200); - expect(request13.type).toBe('application/json'); - const result13 = JSON.parse(request13.text); - expect(result13.total).toEqual(0); - // Confirm txs const block6 = new TestBlockBuilder({ block_height: 6, index_block_hash: '0x06', parent_index_block_hash: '0x05', }) - .addTx({ tx_id: '0xf7f8' }) + .addTx({ tx_id: '0xf7f7', microblock_canonical: false }) + .addTxNftEvent({ + asset_identifier: assetId2, + asset_event_type_id: DbAssetEventTypeId.Transfer, + sender: addr3, + recipient: addr2, + }) .build(); await db.update(block6); @@ -314,7 +281,23 @@ describe('/extended/v1/tokens tests', () => { index_block_hash: '0x07', parent_index_block_hash: '0x06', }) - .addTx({ tx_id: '0x100b' }) + .addTx({ tx_id: '0x1009' }) + .addTxStxEvent({ event_index: 0 }) + .addTxNftEvent({ + asset_identifier: assetId2, + asset_event_type_id: DbAssetEventTypeId.Transfer, + sender: addr3, + recipient: addr2, + event_index: 1, // Higher event index + }) + .addTx({ tx_id: '0x100a' }) + .addTxNftEvent({ + asset_identifier: assetId2, + asset_event_type_id: DbAssetEventTypeId.Transfer, + sender: addr2, + recipient: addr3, + event_index: 0, // Lower event index but higher microblock index + }) .build(); await db.update(block7);