diff --git a/webserver/server/app/controllers/AssetUtxosController.ts b/webserver/server/app/controllers/AssetUtxosController.ts index 2e7c781f..61dca483 100644 --- a/webserver/server/app/controllers/AssetUtxosController.ts +++ b/webserver/server/app/controllers/AssetUtxosController.ts @@ -27,13 +27,15 @@ export class AssetUtxosController extends Controller { ErrorShape > ): Promise { - if (requestBody.assets.length > ASSET_UTXOS_LIMIT.ASSETS) { + const assetsLength = + (requestBody.fingerprints?.length || 0) + (requestBody.policyIds?.length || 0); + if (assetsLength > ASSET_UTXOS_LIMIT.ASSETS) { // eslint-disable-next-line @typescript-eslint/no-unsafe-return return errorResponse( StatusCodes.BAD_REQUEST, genErrorMessage(Errors.AssetLimitExceeded, { limit: ASSET_UTXOS_LIMIT.ASSETS, - found: requestBody.assets.length, + found: assetsLength, }) ); } @@ -41,12 +43,13 @@ export class AssetUtxosController extends Controller { const response = await tx(pool, async dbTx => { const data = await getAssetUtxos({ range: requestBody.range, - assets: requestBody.assets.map(asset => { + fingerprints: requestBody.fingerprints?.map(asset => { const decoded = bech32.decode(asset); const payload = bech32.fromWords(decoded.words); return Buffer.from(payload); }), + policyIds: requestBody.policyIds?.map(policyId => Buffer.from(policyId, 'hex')), dbTx, }); @@ -69,6 +72,8 @@ export class AssetUtxosController extends Controller { amount: data.amount ? data.amount : undefined, slot: data.slot, cip14Fingerprint: bech32.encode('asset', bech32.toWords(data.cip14_fingerprint)), + policyId: Buffer.from(data.policy_id).toString('hex'), + assetName: Buffer.from(data.asset_name).toString('hex'), }; }); }); diff --git a/webserver/server/app/models/asset/assetUtxos.queries.ts b/webserver/server/app/models/asset/assetUtxos.queries.ts index e3d41ec0..6e521290 100644 --- a/webserver/server/app/models/asset/assetUtxos.queries.ts +++ b/webserver/server/app/models/asset/assetUtxos.queries.ts @@ -3,18 +3,21 @@ import { PreparedQuery } from '@pgtyped/query'; /** 'AssetUtxos' parameters type */ export interface IAssetUtxosParams { - fingerprints: readonly (Buffer)[]; + fingerprints: readonly (Buffer | null | void)[]; max_slot: number; min_slot: number; + policyIds: readonly (Buffer | null | void)[]; } /** 'AssetUtxos' return type */ export interface IAssetUtxosResult { address_raw: Buffer; amount: string | null; + asset_name: Buffer; cip14_fingerprint: Buffer; output_index: number; output_tx_hash: string | null; + policy_id: Buffer; slot: number; tx_hash: string | null; } @@ -25,7 +28,7 @@ export interface IAssetUtxosQuery { result: IAssetUtxosResult; } -const assetUtxosIR: any = {"usedParamSet":{"fingerprints":true,"min_slot":true,"max_slot":true},"params":[{"name":"fingerprints","required":true,"transform":{"type":"array_spread"},"locs":[{"a":677,"b":690}]},{"name":"min_slot","required":true,"transform":{"type":"scalar"},"locs":[{"a":712,"b":721}]},{"name":"max_slot","required":true,"transform":{"type":"scalar"},"locs":[{"a":744,"b":753}]}],"statement":"SELECT ENCODE(TXO.HASH,\n 'hex') OUTPUT_TX_HASH,\n \"TransactionOutput\".OUTPUT_INDEX,\n\t\"NativeAsset\".CIP14_FINGERPRINT,\n\t\"AssetUtxo\".AMOUNT,\n\t\"Block\".SLOT,\n\tENCODE(\"Transaction\".HASH,\n 'hex') TX_HASH,\n\t\"Address\".PAYLOAD ADDRESS_RAW\nFROM \"AssetUtxo\"\nJOIN \"Transaction\" ON \"AssetUtxo\".TX_ID = \"Transaction\".ID\nJOIN \"TransactionOutput\" ON \"AssetUtxo\".UTXO_ID = \"TransactionOutput\".ID\nJOIN \"Transaction\" TXO ON \"TransactionOutput\".TX_ID = TXO.ID\nJOIN \"Address\" ON \"Address\".id = \"TransactionOutput\".address_id\nJOIN \"NativeAsset\" ON \"AssetUtxo\".ASSET_ID = \"NativeAsset\".ID\nJOIN \"Block\" ON \"Transaction\".BLOCK_ID = \"Block\".ID\nWHERE \n\t\"NativeAsset\".CIP14_FINGERPRINT IN :fingerprints! AND\n\t\"Block\".SLOT > :min_slot! AND\n\t\"Block\".SLOT <= :max_slot!\nORDER BY \"Transaction\".ID ASC"}; +const assetUtxosIR: any = {"usedParamSet":{"fingerprints":true,"policyIds":true,"min_slot":true,"max_slot":true},"params":[{"name":"fingerprints","required":false,"transform":{"type":"array_spread"},"locs":[{"a":731,"b":743}]},{"name":"policyIds","required":false,"transform":{"type":"array_spread"},"locs":[{"a":777,"b":786}]},{"name":"min_slot","required":true,"transform":{"type":"scalar"},"locs":[{"a":811,"b":820}]},{"name":"max_slot","required":true,"transform":{"type":"scalar"},"locs":[{"a":843,"b":852}]}],"statement":"SELECT ENCODE(TXO.HASH,\n 'hex') OUTPUT_TX_HASH,\n \"TransactionOutput\".OUTPUT_INDEX,\n\t\"NativeAsset\".CIP14_FINGERPRINT,\n\t\"NativeAsset\".POLICY_ID,\n\t\"NativeAsset\".ASSET_NAME,\n\t\"AssetUtxo\".AMOUNT,\n\t\"Block\".SLOT,\n\tENCODE(\"Transaction\".HASH,\n 'hex') TX_HASH,\n\t\"Address\".PAYLOAD ADDRESS_RAW\nFROM \"AssetUtxo\"\nJOIN \"Transaction\" ON \"AssetUtxo\".TX_ID = \"Transaction\".ID\nJOIN \"TransactionOutput\" ON \"AssetUtxo\".UTXO_ID = \"TransactionOutput\".ID\nJOIN \"Transaction\" TXO ON \"TransactionOutput\".TX_ID = TXO.ID\nJOIN \"Address\" ON \"Address\".id = \"TransactionOutput\".address_id\nJOIN \"NativeAsset\" ON \"AssetUtxo\".ASSET_ID = \"NativeAsset\".ID\nJOIN \"Block\" ON \"Transaction\".BLOCK_ID = \"Block\".ID\nWHERE \n\t(\"NativeAsset\".CIP14_FINGERPRINT IN :fingerprints\n\t\tOR \"NativeAsset\".POLICY_ID IN :policyIds\n\t) AND\n\t\"Block\".SLOT > :min_slot! AND\n\t\"Block\".SLOT <= :max_slot!\nORDER BY \"Transaction\".ID, \"AssetUtxo\".ID ASC"}; /** * Query generated from SQL: @@ -34,6 +37,8 @@ const assetUtxosIR: any = {"usedParamSet":{"fingerprints":true,"min_slot":true," * 'hex') OUTPUT_TX_HASH, * "TransactionOutput".OUTPUT_INDEX, * "NativeAsset".CIP14_FINGERPRINT, + * "NativeAsset".POLICY_ID, + * "NativeAsset".ASSET_NAME, * "AssetUtxo".AMOUNT, * "Block".SLOT, * ENCODE("Transaction".HASH, @@ -47,10 +52,12 @@ const assetUtxosIR: any = {"usedParamSet":{"fingerprints":true,"min_slot":true," * JOIN "NativeAsset" ON "AssetUtxo".ASSET_ID = "NativeAsset".ID * JOIN "Block" ON "Transaction".BLOCK_ID = "Block".ID * WHERE - * "NativeAsset".CIP14_FINGERPRINT IN :fingerprints! AND + * ("NativeAsset".CIP14_FINGERPRINT IN :fingerprints + * OR "NativeAsset".POLICY_ID IN :policyIds + * ) AND * "Block".SLOT > :min_slot! AND * "Block".SLOT <= :max_slot! - * ORDER BY "Transaction".ID ASC + * ORDER BY "Transaction".ID, "AssetUtxo".ID ASC * ``` */ export const assetUtxos = new PreparedQuery(assetUtxosIR); diff --git a/webserver/server/app/models/asset/assetUtxos.sql b/webserver/server/app/models/asset/assetUtxos.sql index 44b5d097..796bce42 100644 --- a/webserver/server/app/models/asset/assetUtxos.sql +++ b/webserver/server/app/models/asset/assetUtxos.sql @@ -1,11 +1,14 @@ /* @name AssetUtxos @param fingerprints -> (...) +@param policyIds -> (...) */ SELECT ENCODE(TXO.HASH, 'hex') OUTPUT_TX_HASH, "TransactionOutput".OUTPUT_INDEX, "NativeAsset".CIP14_FINGERPRINT, + "NativeAsset".POLICY_ID, + "NativeAsset".ASSET_NAME, "AssetUtxo".AMOUNT, "Block".SLOT, ENCODE("Transaction".HASH, @@ -19,7 +22,9 @@ JOIN "Address" ON "Address".id = "TransactionOutput".address_id JOIN "NativeAsset" ON "AssetUtxo".ASSET_ID = "NativeAsset".ID JOIN "Block" ON "Transaction".BLOCK_ID = "Block".ID WHERE - "NativeAsset".CIP14_FINGERPRINT IN :fingerprints! AND + ("NativeAsset".CIP14_FINGERPRINT IN :fingerprints + OR "NativeAsset".POLICY_ID IN :policyIds + ) AND "Block".SLOT > :min_slot! AND "Block".SLOT <= :max_slot! ORDER BY "Transaction".ID, "AssetUtxo".ID ASC; diff --git a/webserver/server/app/services/AssetUtxos.ts b/webserver/server/app/services/AssetUtxos.ts index 0a8561be..d714603b 100644 --- a/webserver/server/app/services/AssetUtxos.ts +++ b/webserver/server/app/services/AssetUtxos.ts @@ -7,14 +7,21 @@ export async function getAssetUtxos(request: { minSlot: number; maxSlot: number; }; - assets: Buffer[]; + fingerprints?: Buffer[]; + policyIds?: Buffer[]; dbTx: PoolClient; }): Promise { return await assetUtxos.run( { max_slot: request.range.maxSlot, min_slot: request.range.minSlot, - fingerprints: request.assets, + // pgtyped doesn't seem to have a way to have an optional array parameter, + // and an empty spread expansion fails with postgres. Since none of these + // fields is nullable, an array with null should be equivalent to an empty + // array. + fingerprints: + request.fingerprints && request.fingerprints.length > 0 ? request.fingerprints : [null], + policyIds: request.policyIds && request.policyIds.length > 0 ? request.policyIds : [null], }, request.dbTx ); diff --git a/webserver/shared/models/AssetUtxos.ts b/webserver/shared/models/AssetUtxos.ts index 4919044c..ccd8bb7a 100644 --- a/webserver/shared/models/AssetUtxos.ts +++ b/webserver/shared/models/AssetUtxos.ts @@ -5,7 +5,8 @@ export type Cip14Fingerprint = string; export type AssetUtxosRequest = { range: { minSlot: number; maxSlot: number }, - assets: Cip14Fingerprint[] + fingerprints?: Cip14Fingerprint[], + policyIds?: string[] }; export type AssetUtxosResponse = { @@ -22,6 +23,8 @@ export type AssetUtxosResponse = { index: number, }, cip14Fingerprint: string, + policyId: string, + assetName: string, paymentCred: string, slot: number txId: string,