diff --git a/src/api/routes/address.ts b/src/api/routes/address.ts index 6b35c1f3..ae4bf66a 100644 --- a/src/api/routes/address.ts +++ b/src/api/routes/address.ts @@ -470,7 +470,7 @@ export function createAddressRouter(db: DataStore, chainId: ChainID): express.Ro ); /** - * DEPRECATED: Use `/extended/v1/tokens/nft/holdings`. + * @deprecated Use `/extended/v1/tokens/nft/holdings` instead. */ router.get( '/:stx_address/nft_events', diff --git a/src/api/routes/tokens/tokens.ts b/src/api/routes/tokens/tokens.ts index 4e6a4c87..7f940537 100644 --- a/src/api/routes/tokens/tokens.ts +++ b/src/api/routes/tokens/tokens.ts @@ -13,8 +13,9 @@ import { isNftMetadataEnabled, } from '../../../event-stream/tokens-contract-handler'; import { bufferToHexPrefixString, isValidPrincipal } from '../../../helpers'; -import { isUnanchoredRequest } from '../../../api/query-helpers'; +import { booleanValueForParam, isUnanchoredRequest } from '../../../api/query-helpers'; import { cvToString, deserializeCV } from '@stacks/transactions'; +import { getTxFromDataStore } from 'src/api/controllers/db-controller'; const MAX_TOKENS_PER_REQUEST = 200; const parseTokenQueryLimit = parseLimitQuery({ @@ -29,7 +30,7 @@ export function createTokenRouter(db: DataStore): express.Router { router.get( '/nft/holdings', asyncHandler(async (req, res, next) => { - const principal = req.query.principal ?? ''; + const principal = req.query.principal; if (typeof principal !== 'string' || !isValidPrincipal(principal)) { res.status(400).json({ error: `Invalid or missing principal` }); return; @@ -37,6 +38,7 @@ export function createTokenRouter(db: DataStore): express.Router { const limit = parseTokenQueryLimit(req.query.limit ?? 50); const offset = parsePagingQueryInput(req.query.offset ?? 0); const includeUnanchored = isUnanchoredRequest(req, res, next); + const includeTxMetadata = booleanValueForParam(req, res, next, 'tx_metadata'); const { results, total } = await db.getNftHoldings({ principal: principal, @@ -44,14 +46,23 @@ export function createTokenRouter(db: DataStore): express.Router { limit: limit, includeUnanchored: includeUnanchored, }); - const parsedResults = results.map(result => ({ - asset_identifier: result.asset_identifier, - value: { - hex: bufferToHexPrefixString(result.value), - repr: cvToString(deserializeCV(result.value)), - }, - tx_id: bufferToHexPrefixString(result.tx_id), - })); + const parsedResults = await Promise.all( + results.map(async result => { + const txId = bufferToHexPrefixString(result.tx_id); + return { + asset_identifier: result.asset_identifier, + value: { + hex: bufferToHexPrefixString(result.value), + repr: cvToString(deserializeCV(result.value)), + }, + tx_id: txId, + tx: includeTxMetadata + ? (await getTxFromDataStore(db, { txId: txId, includeUnanchored: includeUnanchored })) + .result + : undefined, + }; + }) + ); const response = { limit: limit, diff --git a/src/datastore/common.ts b/src/datastore/common.ts index 022e7dc2..fa8ea2a8 100644 --- a/src/datastore/common.ts +++ b/src/datastore/common.ts @@ -810,6 +810,11 @@ export interface DataStore extends DataStoreEventEmitter { getRawTx(txId: string): Promise>; + /** + * Returns a list of NFTs owned by the given principal with the optional transaction + * that gave them the ownership of said token. + * @param args - Query arguments + */ getNftHoldings(args: { principal: string; limit: number; @@ -817,6 +822,9 @@ export interface DataStore extends DataStoreEventEmitter { includeUnanchored: boolean; }): Promise<{ results: NftHoldingInfo[]; total: number }>; + /** + * @deprecated Use `getNftHoldings` instead. + */ getAddressNFTEvent(args: { stxAddress: string; blockHeight: number; diff --git a/src/datastore/postgres-store.ts b/src/datastore/postgres-store.ts index 79295f9f..b15fc90f 100644 --- a/src/datastore/postgres-store.ts +++ b/src/datastore/postgres-store.ts @@ -3681,22 +3681,13 @@ export class PgDataStore const maxBlockHeight = await this.getMaxBlockHeight(client, { includeUnanchored }); const result = await client.query( ` - SELECT ${TX_COLUMNS}, - CASE - WHEN txs.type_id = $3 THEN ( - SELECT abi - FROM smart_contracts - WHERE smart_contracts.contract_id = txs.contract_call_contract_id - ORDER BY abi != 'null' DESC, canonical DESC, microblock_canonical DESC, block_height DESC - LIMIT 1 - ) - END as abi + SELECT ${TX_COLUMNS}, ${abiColumn()} FROM txs WHERE tx_id = $1 AND block_height <= $2 ORDER BY canonical DESC, microblock_canonical DESC, block_height DESC LIMIT 1 `, - [hexToBuffer(txId), maxBlockHeight, DbTxTypeId.ContractCall] + [hexToBuffer(txId), maxBlockHeight] ); if (result.rowCount === 0) { return { found: false } as const; @@ -5933,7 +5924,7 @@ export class PgDataStore includeUnanchored: boolean; }): Promise<{ results: NftHoldingInfo[]; total: number }> { return this.queryTx(async client => { - const dbResults = await client.query( + const nftResults = await client.query( ` SELECT *, (COUNT(*) OVER())::integer FROM ${args.includeUnanchored ? 'nft_custody_unanchored' : 'nft_custody'} @@ -5943,20 +5934,17 @@ export class PgDataStore `, [args.principal, args.limit, args.offset] ); - const count: number = dbResults.rows.length > 0 ? parseInt(dbResults.rows[0].count) : 0; - const holdings: NftHoldingInfo[] = dbResults.rows.map(row => ({ + const holdings: NftHoldingInfo[] = nftResults.rows.map(row => ({ asset_identifier: row.asset_identifier, value: row.value, recipient: row.recipient, tx_id: row.tx_id, })); + const count: number = nftResults.rows.length > 0 ? nftResults.rows[0].count : 0; return { results: holdings, total: count }; }); } - /** - * DEPRECATED: Use `getNftHoldings`. - */ async getAddressNFTEvent(args: { stxAddress: string; limit: number;