diff --git a/src/api/controllers/db-controller.ts b/src/api/controllers/db-controller.ts index 92448aaf8..c613f9818 100644 --- a/src/api/controllers/db-controller.ts +++ b/src/api/controllers/db-controller.ts @@ -563,7 +563,7 @@ export async function getRosettaBlockFromDataStore( export async function getUnanchoredTxsFromDataStore(db: PgStore): Promise { const dbTxs = await db.getUnanchoredTxs(); - const parsedTxs = dbTxs.txs.map(dbTx => parseDbTx(dbTx)); + const parsedTxs = dbTxs.txs.map(dbTx => parseDbTx(dbTx, false)); return parsedTxs; } @@ -864,6 +864,7 @@ export async function getRosettaTransactionFromDataStore( interface GetTxArgs { txId: string; includeUnanchored: boolean; + excludeFunctionArgs: boolean; } interface GetTxFromDbTxArgs extends GetTxArgs { @@ -878,6 +879,7 @@ interface GetTxsWithEventsArgs extends GetTxsArgs { interface GetTxsArgs { txIds: string[]; includeUnanchored: boolean; + excludeFunctionArgs: boolean; } interface GetTxWithEventsArgs extends GetTxArgs { @@ -905,7 +907,10 @@ function parseDbBaseTx(dbTx: DbTx | DbMempoolTx): BaseTransaction { return tx; } -function parseDbTxTypeMetadata(dbTx: DbTx | DbMempoolTx): TransactionMetadata { +function parseDbTxTypeMetadata( + dbTx: DbTx | DbMempoolTx, + excludeFunctionArgs: boolean +): TransactionMetadata { switch (dbTx.type_id) { case DbTxTypeId.TokenTransfer: { const metadata: TokenTransferTransactionMetadata = { @@ -965,7 +970,7 @@ function parseDbTxTypeMetadata(dbTx: DbTx | DbMempoolTx): TransactionMetadata { return metadata; } case DbTxTypeId.ContractCall: { - return parseContractCallMetadata(dbTx); + return parseContractCallMetadata(dbTx, excludeFunctionArgs); } case DbTxTypeId.PoisonMicroblock: { const metadata: PoisonMicroblockTransactionMetadata = { @@ -1052,7 +1057,10 @@ function parseDbTxTypeMetadata(dbTx: DbTx | DbMempoolTx): TransactionMetadata { } } -export function parseContractCallMetadata(tx: BaseTx): ContractCallTransactionMetadata { +export function parseContractCallMetadata( + tx: BaseTx, + excludeFunctionArgs: boolean +): ContractCallTransactionMetadata { const contractId = unwrapOptional( tx.contract_call_contract_id, () => 'Unexpected nullish contract_call_contract_id' @@ -1063,6 +1071,7 @@ export function parseContractCallMetadata(tx: BaseTx): ContractCallTransactionMe ); let functionAbi: ClarityAbiFunction | undefined; const abi = tx.abi; + if (abi) { const contractAbi: ClarityAbi = JSON.parse(abi); functionAbi = contractAbi.functions.find(fn => fn.name === functionName); @@ -1071,30 +1080,42 @@ export function parseContractCallMetadata(tx: BaseTx): ContractCallTransactionMe } } - const functionArgs = tx.contract_call_function_args - ? decodeClarityValueList(tx.contract_call_function_args).map((c, fnArgIndex) => { - const functionArgAbi = functionAbi - ? functionAbi.args[fnArgIndex++] - : { name: '', type: undefined }; + const contractCall: { + contract_id: string; + function_name: string; + function_signature: string; + function_args?: { + hex: string; + repr: string; + name: string; + type: string; + }[]; + } = { + contract_id: contractId, + function_name: functionName, + function_signature: functionAbi ? abiFunctionToString(functionAbi) : '', + }; + + // Only process function_args if not excluded + if (!excludeFunctionArgs && tx.contract_call_function_args) { + contractCall.function_args = decodeClarityValueList(tx.contract_call_function_args).map( + (c, idx) => { + const functionArgAbi = functionAbi ? functionAbi.args[idx] : { name: '', type: undefined }; return { hex: c.hex, repr: c.repr, - name: functionArgAbi.name, - type: functionArgAbi.type + name: functionArgAbi?.name || '', + type: functionArgAbi?.type ? getTypeString(functionArgAbi.type) : decodeClarityValueToTypeName(c.hex), }; - }) - : undefined; + } + ); + } const metadata: ContractCallTransactionMetadata = { tx_type: 'contract_call', - contract_call: { - contract_id: contractId, - function_name: functionName, - function_signature: functionAbi ? abiFunctionToString(functionAbi) : '', - function_args: functionArgs, - }, + contract_call: contractCall, }; return metadata; } @@ -1154,10 +1175,10 @@ function parseDbAbstractMempoolTx( return abstractMempoolTx; } -export function parseDbTx(dbTx: DbTx): Transaction { +export function parseDbTx(dbTx: DbTx, excludeFunctionArgs: boolean): Transaction { const baseTx = parseDbBaseTx(dbTx); const abstractTx = parseDbAbstractTx(dbTx, baseTx); - const txMetadata = parseDbTxTypeMetadata(dbTx); + const txMetadata = parseDbTxTypeMetadata(dbTx, excludeFunctionArgs); const result: Transaction = { ...abstractTx, ...txMetadata, @@ -1165,10 +1186,13 @@ export function parseDbTx(dbTx: DbTx): Transaction { return result; } -export function parseDbMempoolTx(dbMempoolTx: DbMempoolTx): MempoolTransaction { +export function parseDbMempoolTx( + dbMempoolTx: DbMempoolTx, + excludeFunctionArgs: boolean +): MempoolTransaction { const baseTx = parseDbBaseTx(dbMempoolTx); const abstractTx = parseDbAbstractMempoolTx(dbMempoolTx, baseTx); - const txMetadata = parseDbTxTypeMetadata(dbMempoolTx); + const txMetadata = parseDbTxTypeMetadata(dbMempoolTx, excludeFunctionArgs); const result: MempoolTransaction = { ...abstractTx, ...txMetadata, @@ -1189,7 +1213,9 @@ export async function getMempoolTxsFromDataStore( return []; } - const parsedMempoolTxs = mempoolTxsQuery.map(tx => parseDbMempoolTx(tx)); + const parsedMempoolTxs = mempoolTxsQuery.map(tx => + parseDbMempoolTx(tx, args.excludeFunctionArgs) + ); return parsedMempoolTxs; } @@ -1211,7 +1237,7 @@ async function getTxsFromDataStore( } // parsing txQuery - const parsedTxs = txQuery.map(tx => parseDbTx(tx)); + const parsedTxs = txQuery.map(tx => parseDbTx(tx, args.excludeFunctionArgs)); // incase transaction events are requested if ('eventLimit' in args) { @@ -1261,7 +1287,7 @@ export async function getTxFromDataStore( dbTx = txQuery.result; } - const parsedTx = parseDbTx(dbTx); + const parsedTx = parseDbTx(dbTx, args.excludeFunctionArgs); // If tx events are requested if ('eventLimit' in args) { @@ -1315,6 +1341,7 @@ export async function searchTxs( const mempoolTxsQuery = await getMempoolTxsFromDataStore(db, { txIds: mempoolTxs, includeUnanchored: args.includeUnanchored, + excludeFunctionArgs: args.excludeFunctionArgs, }); // merging found mempool transaction in found transactions object diff --git a/src/api/routes/address.ts b/src/api/routes/address.ts index c4f5a55c1..63fa9ca9d 100644 --- a/src/api/routes/address.ts +++ b/src/api/routes/address.ts @@ -33,6 +33,7 @@ import { PrincipalSchema, UnanchoredParamSchema, UntilBlockSchema, + ExcludeFunctionArgsParamSchema, } from '../schemas/params'; import { AddressBalance, @@ -290,6 +291,7 @@ export const AddressRoutes: FastifyPluginAsync< ), unanchored: UnanchoredParamSchema, until_block: UntilBlockSchema, + exclude_function_args: ExcludeFunctionArgsParamSchema, }), response: { 200: AddressTransactionsListResponseSchema, @@ -302,6 +304,7 @@ export const AddressRoutes: FastifyPluginAsync< const untilBlock = parseUntilBlockQuery(req.query.until_block, req.query.unanchored); const limit = getPagingQueryLimit(ResourceType.Tx, req.query.limit); const offset = req.query.offset ?? 0; + const excludeFunctionArgs = req.query.exclude_function_args ?? false; const response = await fastify.db.sqlTransaction(async sql => { const blockParams = getBlockParams(req.query.height, req.query.unanchored); @@ -327,7 +330,7 @@ export const AddressRoutes: FastifyPluginAsync< blockHeight, atSingleBlock, }); - const results = txResults.map(dbTx => parseDbTx(dbTx)); + const results = txResults.map(dbTx => parseDbTx(dbTx, excludeFunctionArgs)); const response = { limit, offset, total, results }; return response; }); @@ -376,6 +379,7 @@ export const AddressRoutes: FastifyPluginAsync< txId: results.tx.tx_id, dbTx: results.tx, includeUnanchored: false, + excludeFunctionArgs: false, }); if (!txQuery.found) { throw new Error('unexpected tx not found -- fix tx enumeration query'); @@ -468,6 +472,7 @@ export const AddressRoutes: FastifyPluginAsync< txId: entry.tx.tx_id, dbTx: entry.tx, includeUnanchored: blockParams.includeUnanchored ?? false, + excludeFunctionArgs: false, }); if (!txQuery.found) { throw new Error('unexpected tx not found -- fix tx enumeration query'); @@ -671,6 +676,7 @@ export const AddressRoutes: FastifyPluginAsync< limit: LimitParam(ResourceType.Tx), offset: OffsetParam(), unanchored: UnanchoredParamSchema, + exclude_function_args: ExcludeFunctionArgsParamSchema, }), response: { 200: PaginatedResponse(MempoolTransactionSchema, { @@ -690,13 +696,16 @@ export const AddressRoutes: FastifyPluginAsync< ); } const includeUnanchored = req.query.unanchored ?? false; + const excludeFunctionArgs = req.query.exclude_function_args ?? false; const { results: txResults, total } = await fastify.db.getMempoolTxList({ offset, limit, address, includeUnanchored, }); - const results: MempoolTransaction[] = txResults.map(tx => parseDbMempoolTx(tx)); + const results: MempoolTransaction[] = txResults.map(tx => + parseDbMempoolTx(tx, excludeFunctionArgs) + ); const response = { limit, offset, total, results }; await reply.send(response); } diff --git a/src/api/routes/block.ts b/src/api/routes/block.ts index 00837abbd..3876ee498 100644 --- a/src/api/routes/block.ts +++ b/src/api/routes/block.ts @@ -159,6 +159,7 @@ export const BlockRoutes: FastifyPluginAsync< if (!block.found) { throw new NotFoundError(`cannot find block by hash`); } + await reply.send(block.result); } ); diff --git a/src/api/routes/search.ts b/src/api/routes/search.ts index 54d3d9f7e..170c5ef51 100644 --- a/src/api/routes/search.ts +++ b/src/api/routes/search.ts @@ -114,7 +114,7 @@ export const SearchRoutes: FastifyPluginAsync< }, }; if (includeMetadata) { - txResult.metadata = parseDbTx(txData); + txResult.metadata = parseDbTx(txData, false); } return { found: true, result: txResult }; } else if (queryResult.result.entity_type === 'mempool_tx_id') { @@ -127,7 +127,7 @@ export const SearchRoutes: FastifyPluginAsync< }, }; if (includeMetadata) { - txResult.metadata = parseDbMempoolTx(txData); + txResult.metadata = parseDbMempoolTx(txData, false); } return { found: true, result: txResult }; } else { @@ -176,7 +176,7 @@ export const SearchRoutes: FastifyPluginAsync< }, }; if (includeMetadata) { - contractResult.metadata = parseDbTx(txData); + contractResult.metadata = parseDbTx(txData, false); } return { found: true, result: contractResult }; } else { @@ -191,7 +191,7 @@ export const SearchRoutes: FastifyPluginAsync< }, }; if (includeMetadata) { - contractResult.metadata = parseDbMempoolTx(txData); + contractResult.metadata = parseDbMempoolTx(txData, false); } return { found: true, result: contractResult }; } diff --git a/src/api/routes/tokens.ts b/src/api/routes/tokens.ts index 60775e653..e978bb812 100644 --- a/src/api/routes/tokens.ts +++ b/src/api/routes/tokens.ts @@ -116,7 +116,7 @@ export const TokenRoutes: FastifyPluginAsync< if (includeTxMetadata && result.tx) { return { ...parsedNftData, - tx: parseDbTx(result.tx), + tx: parseDbTx(result.tx, false), }; } return { ...parsedNftData, tx_id: result.nft_holding_info.tx_id }; @@ -225,7 +225,7 @@ export const TokenRoutes: FastifyPluginAsync< if (includeTxMetadata && result.tx) { return { ...parsedNftData, - tx: parseDbTx(result.tx), + tx: parseDbTx(result.tx, false), }; } return { ...parsedNftData, tx_id: result.nft_event.tx_id }; @@ -331,7 +331,7 @@ export const TokenRoutes: FastifyPluginAsync< if (includeTxMetadata && result.tx) { return { ...parsedNftData, - tx: parseDbTx(result.tx), + tx: parseDbTx(result.tx, false), }; } return { ...parsedNftData, tx_id: result.nft_event.tx_id }; diff --git a/src/api/routes/tx.ts b/src/api/routes/tx.ts index 9902b2f5e..0c33d9104 100644 --- a/src/api/routes/tx.ts +++ b/src/api/routes/tx.ts @@ -24,6 +24,7 @@ import { Type, TypeBoxTypeProvider } from '@fastify/type-provider-typebox'; import { AddressParamSchema, BlockHeightSchema, + ExcludeFunctionArgsParamSchema, LimitParam, MempoolOrderByParamSchema, OffsetParam, @@ -133,6 +134,7 @@ export const TxRoutes: FastifyPluginAsync< examples: [123], }) ), + exclude_function_args: ExcludeFunctionArgsParamSchema, }), response: { 200: TransactionResultsSchema, @@ -142,6 +144,7 @@ export const TxRoutes: FastifyPluginAsync< async (req, reply) => { const limit = getPagingQueryLimit(ResourceType.Tx, req.query.limit); const offset = parsePagingQueryInput(req.query.offset ?? 0); + const excludeFunctionArgs = req.query.exclude_function_args ?? false; const txTypeFilter = parseTxTypeStrings(req.query.type ?? []); @@ -193,7 +196,7 @@ export const TxRoutes: FastifyPluginAsync< order: req.query.order, sortBy: req.query.sort_by, }); - const results = txResults.map(tx => parseDbTx(tx)); + const results = txResults.map(tx => parseDbTx(tx, excludeFunctionArgs)); await reply.send({ limit, offset, total, results }); } ); @@ -218,6 +221,7 @@ export const TxRoutes: FastifyPluginAsync< event_limit: LimitParam(ResourceType.Event), event_offset: OffsetParam(), unanchored: UnanchoredParamSchema, + exclude_function_args: ExcludeFunctionArgsParamSchema, }), response: { 200: TransactionSearchResponseSchema, @@ -228,12 +232,14 @@ export const TxRoutes: FastifyPluginAsync< const eventLimit = getPagingQueryLimit(ResourceType.Event, req.query.event_limit); const eventOffset = parsePagingQueryInput(req.query.event_offset ?? 0); const includeUnanchored = req.query.unanchored ?? false; + const excludeFunctionArgs = req.query.exclude_function_args ?? false; req.query.tx_id.forEach(tx => validateRequestHexInput(tx)); const txQuery = await searchTxs(fastify.db, { txIds: req.query.tx_id, eventLimit, eventOffset, includeUnanchored, + excludeFunctionArgs, }); await reply.send(txQuery); } @@ -259,6 +265,7 @@ export const TxRoutes: FastifyPluginAsync< unanchored: UnanchoredParamSchema, offset: OffsetParam(), limit: LimitParam(ResourceType.Tx), + exclude_function_args: ExcludeFunctionArgsParamSchema, }), response: { 200: MempoolTransactionListResponse, @@ -268,6 +275,7 @@ export const TxRoutes: FastifyPluginAsync< async (req, reply) => { const limit = getPagingQueryLimit(ResourceType.Tx, req.query.limit); const offset = parsePagingQueryInput(req.query.offset ?? 0); + const excludeFunctionArgs = req.query.exclude_function_args ?? false; const addrParams: (string | undefined)[] = [ req.query.sender_address, @@ -312,7 +320,7 @@ export const TxRoutes: FastifyPluginAsync< address, }); - const results = txResults.map(tx => parseDbMempoolTx(tx)); + const results = txResults.map(tx => parseDbMempoolTx(tx, excludeFunctionArgs)); const response = { limit, offset, total, results }; await reply.send(response); } @@ -337,6 +345,7 @@ export const TxRoutes: FastifyPluginAsync< querystring: Type.Object({ offset: OffsetParam(), limit: LimitParam(ResourceType.Tx), + exclude_function_args: ExcludeFunctionArgsParamSchema, }), response: { 200: PaginatedResponse(MempoolTransactionSchema, { @@ -348,11 +357,12 @@ export const TxRoutes: FastifyPluginAsync< async (req, reply) => { const limit = getPagingQueryLimit(ResourceType.Tx, req.query.limit); const offset = parsePagingQueryInput(req.query.offset ?? 0); + const excludeFunctionArgs = req.query.exclude_function_args ?? false; const { results: txResults, total } = await fastify.db.getDroppedTxs({ offset, limit, }); - const results = txResults.map(tx => parseDbMempoolTx(tx)); + const results = txResults.map(tx => parseDbMempoolTx(tx, excludeFunctionArgs)); const response = { limit, offset, total, results }; await reply.send(response); } @@ -484,6 +494,7 @@ export const TxRoutes: FastifyPluginAsync< event_limit: LimitParam(ResourceType.Event, undefined, undefined, 100), event_offset: OffsetParam(), unanchored: UnanchoredParamSchema, + exclude_function_args: ExcludeFunctionArgsParamSchema, }), response: { 200: Type.Union([TransactionSchema, MempoolTransactionSchema]), @@ -501,6 +512,7 @@ export const TxRoutes: FastifyPluginAsync< const eventLimit = getPagingQueryLimit(ResourceType.Event, req.query['event_limit'], 100); const eventOffset = parsePagingQueryInput(req.query['event_offset'] ?? 0); const includeUnanchored = req.query.unanchored ?? false; + const excludeFunctionArgs = req.query.exclude_function_args ?? false; validateRequestHexInput(tx_id); const txQuery = await searchTx(fastify.db, { @@ -508,6 +520,7 @@ export const TxRoutes: FastifyPluginAsync< eventLimit, eventOffset, includeUnanchored, + excludeFunctionArgs, }); if (!txQuery.found) { throw new NotFoundError(`could not find transaction by ID`); @@ -576,6 +589,7 @@ export const TxRoutes: FastifyPluginAsync< querystring: Type.Object({ offset: OffsetParam(), limit: LimitParam(ResourceType.Tx, undefined, undefined, 200), + exclude_function_args: ExcludeFunctionArgsParamSchema, }), response: { 200: PaginatedResponse(TransactionSchema, { description: 'List of transactions' }), @@ -587,13 +601,14 @@ export const TxRoutes: FastifyPluginAsync< const limit = getPagingQueryLimit(ResourceType.Tx, req.query['limit'], 200); const offset = parsePagingQueryInput(req.query['offset'] ?? 0); + const excludeFunctionArgs = req.query.exclude_function_args ?? false; validateRequestHexInput(block_hash); const result = await fastify.db.getTxsFromBlock({ hash: block_hash }, limit, offset); if (!result.found) { throw new NotFoundError(`no block found by hash`); } const dbTxs = result.result; - const results = dbTxs.results.map(dbTx => parseDbTx(dbTx)); + const results = dbTxs.results.map(dbTx => parseDbTx(dbTx, excludeFunctionArgs)); await reply.send({ limit: limit, @@ -622,6 +637,7 @@ export const TxRoutes: FastifyPluginAsync< querystring: Type.Object({ offset: OffsetParam(), limit: LimitParam(ResourceType.Tx), + exclude_function_args: ExcludeFunctionArgsParamSchema, }), response: { 200: PaginatedResponse(TransactionSchema, { description: 'List of transactions' }), @@ -633,12 +649,13 @@ export const TxRoutes: FastifyPluginAsync< const limit = getPagingQueryLimit(ResourceType.Tx, req.query['limit']); const offset = parsePagingQueryInput(req.query['offset'] ?? 0); + const excludeFunctionArgs = req.query.exclude_function_args ?? false; const result = await fastify.db.getTxsFromBlock({ height: height }, limit, offset); if (!result.found) { throw new NotFoundError(`no block found at height ${height}`); } const dbTxs = result.result; - const results = dbTxs.results.map(dbTx => parseDbTx(dbTx)); + const results = dbTxs.results.map(dbTx => parseDbTx(dbTx, excludeFunctionArgs)); await reply.send({ limit: limit, diff --git a/src/api/routes/v2/addresses.ts b/src/api/routes/v2/addresses.ts index e0c05ea49..3ea1b3803 100644 --- a/src/api/routes/v2/addresses.ts +++ b/src/api/routes/v2/addresses.ts @@ -12,7 +12,12 @@ import { InvalidRequestError, NotFoundError } from '../../../errors'; import { FastifyPluginAsync } from 'fastify'; import { Type, TypeBoxTypeProvider } from '@fastify/type-provider-typebox'; import { Server } from 'node:http'; -import { LimitParam, OffsetParam, PrincipalSchema } from '../../schemas/params'; +import { + LimitParam, + OffsetParam, + PrincipalSchema, + ExcludeFunctionArgsParamSchema, +} from '../../schemas/params'; import { getPagingQueryLimit, ResourceType } from '../../pagination'; import { PaginatedResponse } from '../../schemas/util'; import { @@ -46,6 +51,7 @@ export const AddressRoutesV2: FastifyPluginAsync< querystring: Type.Object({ limit: LimitParam(ResourceType.Tx), offset: OffsetParam(), + exclude_function_args: ExcludeFunctionArgsParamSchema, }), response: { 200: PaginatedResponse(AddressTransactionSchema), @@ -55,6 +61,7 @@ export const AddressRoutesV2: FastifyPluginAsync< async (req, reply) => { const params = req.params; const query = req.query; + const excludeFunctionArgs = req.query.exclude_function_args ?? false; try { const { limit, offset, results, total } = await fastify.db.v2.getAddressTransactions({ @@ -62,7 +69,7 @@ export const AddressRoutesV2: FastifyPluginAsync< ...query, }); const transfers: AddressTransaction[] = results.map(r => - parseDbTxWithAccountTransferSummary(r) + parseDbTxWithAccountTransferSummary(r, excludeFunctionArgs) ); await reply.send({ limit, diff --git a/src/api/routes/v2/blocks.ts b/src/api/routes/v2/blocks.ts index afc25cbef..2b1cbe221 100644 --- a/src/api/routes/v2/blocks.ts +++ b/src/api/routes/v2/blocks.ts @@ -169,7 +169,7 @@ export const BlockRoutesV2: FastifyPluginAsync< limit, offset, total, - results: results.map(r => parseDbTx(r)), + results: results.map(r => parseDbTx(r, false)), }; await reply.send(response); } catch (error) { diff --git a/src/api/routes/v2/helpers.ts b/src/api/routes/v2/helpers.ts index 5a5b0e8c4..d1df78669 100644 --- a/src/api/routes/v2/helpers.ts +++ b/src/api/routes/v2/helpers.ts @@ -89,10 +89,11 @@ export function parseDbSmartContractStatusArray( } export function parseDbTxWithAccountTransferSummary( - tx: DbTxWithAddressTransfers + tx: DbTxWithAddressTransfers, + excludeFunctionArgs: boolean = false ): AddressTransaction { return { - tx: parseDbTx(tx), + tx: parseDbTx(tx, excludeFunctionArgs), stx_sent: tx.stx_sent.toString(), stx_received: tx.stx_received.toString(), events: { diff --git a/src/api/routes/ws/web-socket-transmitter.ts b/src/api/routes/ws/web-socket-transmitter.ts index eb5132ac7..3140caa7a 100644 --- a/src/api/routes/ws/web-socket-transmitter.ts +++ b/src/api/routes/ws/web-socket-transmitter.ts @@ -152,6 +152,7 @@ export class WebSocketTransmitter { const mempoolTxs = await getMempoolTxsFromDataStore(this.db, { txIds: [txId], includeUnanchored: true, + excludeFunctionArgs: false, }); if (mempoolTxs.length > 0) { await this.send('mempoolTransaction', mempoolTxs[0]); @@ -168,6 +169,7 @@ export class WebSocketTransmitter { const txQuery = await getTxFromDataStore(this.db, { txId: txId, includeUnanchored: true, + excludeFunctionArgs: false, }); if (txQuery.found) { return txQuery.result; @@ -176,6 +178,7 @@ export class WebSocketTransmitter { const mempoolTxs = await getMempoolTxsFromDataStore(this.db, { txIds: [txId], includeUnanchored: true, + excludeFunctionArgs: false, }); if (mempoolTxs.length > 0) { return mempoolTxs[0]; @@ -228,7 +231,7 @@ export class WebSocketTransmitter { } const addressTxs = dbTxsQuery.results; for (const addressTx of addressTxs) { - const parsedTx = parseDbTx(addressTx.tx); + const parsedTx = parseDbTx(addressTx.tx, false); const result: AddressTransactionWithTransfers = { tx: parsedTx, stx_sent: addressTx.stx_sent.toString(), diff --git a/src/api/schemas/params.ts b/src/api/schemas/params.ts index de4892bf3..0fa825e51 100644 --- a/src/api/schemas/params.ts +++ b/src/api/schemas/params.ts @@ -115,3 +115,12 @@ export const OrderParamSchema = Type.Enum( description: 'Results order', } ); + +export const ExcludeFunctionArgsParamSchema = Type.Optional( + Type.Boolean({ + default: false, + description: + 'Exclude function_args from contract call responses for smaller transaction sizes.', + examples: [true, false], + }) +); diff --git a/src/datastore/pg-write-store.ts b/src/datastore/pg-write-store.ts index 6a21705f5..9dca46c08 100644 --- a/src/datastore/pg-write-store.ts +++ b/src/datastore/pg-write-store.ts @@ -2227,7 +2227,7 @@ export class PgWriteStore extends PgStore { const values: SmartContractInsertValues[] = batch.map(smartContract => ({ tx_id: smartContract.tx_id, canonical: smartContract.canonical, - clarity_version: smartContract.clarity_version, + clarity_version: smartContract.clarity_version ?? null, contract_id: smartContract.contract_id, block_height: smartContract.block_height, index_block_hash: tx.index_block_hash, diff --git a/src/rosetta/rosetta-helpers.ts b/src/rosetta/rosetta-helpers.ts index 5e8c4e294..e7be28cc3 100644 --- a/src/rosetta/rosetta-helpers.ts +++ b/src/rosetta/rosetta-helpers.ts @@ -681,7 +681,11 @@ async function makeCallContractOperation( }, }; - const parsed_tx = await getTxFromDataStore(db, { txId: tx.tx_id, includeUnanchored: false }); + const parsed_tx = await getTxFromDataStore(db, { + txId: tx.tx_id, + includeUnanchored: false, + excludeFunctionArgs: false, // Rosetta requires function_args + }); if (!parsed_tx.found) { throw new Error('unexpected tx not found -- could not get contract from data store'); } @@ -868,7 +872,7 @@ function parseStackingContractCall( } function parseGenericContractCall(operation: RosettaOperation, tx: BaseTx) { - const metadata = parseContractCallMetadata(tx); + const metadata = parseContractCallMetadata(tx, false); // Rosetta requires function_args operation.metadata = metadata.contract_call; } diff --git a/tests/api/tx.test.ts b/tests/api/tx.test.ts index b2ea4e260..5871de087 100644 --- a/tests/api/tx.test.ts +++ b/tests/api/tx.test.ts @@ -450,7 +450,11 @@ describe('tx tests', () => { ], }); - const txQuery = await getTxFromDataStore(db, { txId: dbTx.tx_id, includeUnanchored: false }); + const txQuery = await getTxFromDataStore(db, { + txId: dbTx.tx_id, + includeUnanchored: false, + excludeFunctionArgs: false, + }); expect(txQuery.found).toBe(true); if (!txQuery.found) { throw Error('not found'); @@ -602,7 +606,11 @@ describe('tx tests', () => { ], }); - const txQuery = await getTxFromDataStore(db, { txId: dbTx.tx_id, includeUnanchored: false }); + const txQuery = await getTxFromDataStore(db, { + txId: dbTx.tx_id, + includeUnanchored: false, + excludeFunctionArgs: false, + }); expect(txQuery.found).toBe(true); if (!txQuery.found) { throw Error('not found'); @@ -753,7 +761,11 @@ describe('tx tests', () => { ], }); - const txQuery = await getTxFromDataStore(db, { txId: dbTx.tx_id, includeUnanchored: false }); + const txQuery = await getTxFromDataStore(db, { + txId: dbTx.tx_id, + includeUnanchored: false, + excludeFunctionArgs: false, + }); expect(txQuery.found).toBe(true); if (!txQuery.found) { throw Error('not found'); @@ -938,7 +950,11 @@ describe('tx tests', () => { ], }); - const txQuery = await getTxFromDataStore(db, { txId: dbTx.tx_id, includeUnanchored: false }); + const txQuery = await getTxFromDataStore(db, { + txId: dbTx.tx_id, + includeUnanchored: false, + excludeFunctionArgs: false, + }); expect(txQuery.found).toBe(true); if (!txQuery.found) { throw Error('not found'); @@ -1145,7 +1161,11 @@ describe('tx tests', () => { abi: JSON.stringify(contractAbi), }, ]); - const txQuery = await getTxFromDataStore(db, { txId: dbTx.tx_id, includeUnanchored: false }); + const txQuery = await getTxFromDataStore(db, { + txId: dbTx.tx_id, + includeUnanchored: false, + excludeFunctionArgs: false, + }); expect(txQuery.found).toBe(true); if (!txQuery.found) { throw Error('not found'); @@ -1569,7 +1589,11 @@ describe('tx tests', () => { }, ], }); - const txQuery = await getTxFromDataStore(db, { txId: dbTx.tx_id, includeUnanchored: false }); + const txQuery = await getTxFromDataStore(db, { + txId: dbTx.tx_id, + includeUnanchored: false, + excludeFunctionArgs: false, + }); expect(txQuery.found).toBe(true); if (!txQuery.found) { throw Error('not found'); @@ -1782,7 +1806,11 @@ describe('tx tests', () => { ], }); - const txQuery = await getTxFromDataStore(db, { txId: dbTx.tx_id, includeUnanchored: false }); + const txQuery = await getTxFromDataStore(db, { + txId: dbTx.tx_id, + includeUnanchored: false, + excludeFunctionArgs: false, + }); expect(txQuery.found).toBe(true); if (!txQuery.found) { throw Error('not found'); @@ -1940,7 +1968,11 @@ describe('tx tests', () => { ], }); - const txQuery = await getTxFromDataStore(db, { txId: dbTx.tx_id, includeUnanchored: false }); + const txQuery = await getTxFromDataStore(db, { + txId: dbTx.tx_id, + includeUnanchored: false, + excludeFunctionArgs: false, + }); expect(txQuery.found).toBe(true); if (!txQuery.found) { throw Error('not found'); @@ -4531,4 +4563,231 @@ describe('tx tests', () => { }) ); }); + + test('exclude_function_args works for single contract-call tx', async () => { + const block = new TestBlockBuilder({ + block_height: 1, + index_block_hash: '0x01', + block_hash: '0x01', + }) + .addTx({ + tx_id: '0x1234000000000000000000000000000000000000000000000000000000000000', + type_id: DbTxTypeId.ContractCall, + contract_call_contract_id: 'SP000000000000000000002Q6VF78.pox-4', + contract_call_function_name: 'delegate-stx', + contract_call_function_args: bufferToHex( + createClarityValueArray( + uintCV(1000000), + stringAsciiCV('STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6') + ) + ), + }) + .build(); + + await db.update(block); + + // Test with exclude_function_args=true + const resExcluded = await supertest(api.server) + .get( + '/extended/v1/tx/0x1234000000000000000000000000000000000000000000000000000000000000?exclude_function_args=true' + ) + .expect(200); + + expect(resExcluded.body.tx_type).toBe('contract_call'); + expect(resExcluded.body.contract_call.function_args).toBeUndefined(); + expect(resExcluded.body.contract_call.function_name).toBe('delegate-stx'); + expect(resExcluded.body.contract_call.contract_id).toBe('SP000000000000000000002Q6VF78.pox-4'); + }); + + test('default behavior still returns function_args', async () => { + const block = new TestBlockBuilder({ + block_height: 1, + index_block_hash: '0x02', + block_hash: '0x02', + }) + .addTx({ + tx_id: '0x2345000000000000000000000000000000000000000000000000000000000000', + type_id: DbTxTypeId.ContractCall, + contract_call_contract_id: 'SP000000000000000000002Q6VF78.pox-4', + contract_call_function_name: 'delegate-stx', + contract_call_function_args: bufferToHex( + createClarityValueArray( + uintCV(1000000), + stringAsciiCV('STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6') + ) + ), + }) + .build(); + + await db.update(block); + + // Test default behavior (no parameter) + const resDefault = await supertest(api.server) + .get('/extended/v1/tx/0x2345000000000000000000000000000000000000000000000000000000000000') + .expect(200); + + expect(resDefault.body.tx_type).toBe('contract_call'); + expect(Array.isArray(resDefault.body.contract_call.function_args)).toBe(true); + expect(resDefault.body.contract_call.function_args.length).toBe(2); + // Note: argument names may be empty if ABI is not available, but that's ok for this test + expect(resDefault.body.contract_call.function_args[0]).toHaveProperty('hex'); + expect(resDefault.body.contract_call.function_args[0]).toHaveProperty('repr'); + + // Test with exclude_function_args=false (explicit false) + const resFalse = await supertest(api.server) + .get( + '/extended/v1/tx/0x2345000000000000000000000000000000000000000000000000000000000000?exclude_function_args=false' + ) + .expect(200); + + expect(resFalse.body.tx_type).toBe('contract_call'); + expect(Array.isArray(resFalse.body.contract_call.function_args)).toBe(true); + expect(resFalse.body.contract_call.function_args.length).toBe(2); + expect(resFalse.body.contract_call.function_args[0]).toHaveProperty('hex'); + expect(resFalse.body.contract_call.function_args[0]).toHaveProperty('repr'); + }); + + test('transaction list endpoint respects exclude_function_args flag', async () => { + const block = new TestBlockBuilder({ + block_height: 1, + index_block_hash: '0x03', + block_hash: '0x03', + }) + .addTx({ + tx_id: '0x3456000000000000000000000000000000000000000000000000000000000000', + type_id: DbTxTypeId.ContractCall, + contract_call_contract_id: 'SP000000000000000000002Q6VF78.pox-4', + contract_call_function_name: 'delegate-stx', + contract_call_function_args: bufferToHex( + createClarityValueArray( + uintCV(1000000), + stringAsciiCV('STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6') + ) + ), + }) + .build(); + + await db.update(block); + + // Test transaction list with exclude_function_args=true + const resExcluded = await supertest(api.server) + .get('/extended/v1/tx?exclude_function_args=true&limit=1') + .expect(200); + + expect(resExcluded.body.results).toHaveLength(1); + const contractCallTx = resExcluded.body.results.find( + (tx: any) => tx.tx_type === 'contract_call' + ); + expect(contractCallTx).toBeDefined(); + expect(contractCallTx.contract_call.function_args).toBeUndefined(); + + // Test transaction list with exclude_function_args=false + const resIncluded = await supertest(api.server) + .get('/extended/v1/tx?exclude_function_args=false&limit=1') + .expect(200); + + expect(resIncluded.body.results).toHaveLength(1); + const contractCallTxIncluded = resIncluded.body.results.find( + (tx: any) => tx.tx_type === 'contract_call' + ); + expect(contractCallTxIncluded).toBeDefined(); + expect(Array.isArray(contractCallTxIncluded.contract_call.function_args)).toBe(true); + expect(contractCallTxIncluded.contract_call.function_args.length).toBe(2); + }); + + test('multiple transactions endpoint respects exclude_function_args flag', async () => { + const block = new TestBlockBuilder({ + block_height: 1, + index_block_hash: '0x04', + block_hash: '0x04', + }) + .addTx({ + tx_id: '0x4567000000000000000000000000000000000000000000000000000000000000', + type_id: DbTxTypeId.ContractCall, + contract_call_contract_id: 'SP000000000000000000002Q6VF78.pox-4', + contract_call_function_name: 'delegate-stx', + contract_call_function_args: bufferToHex( + createClarityValueArray( + uintCV(1000000), + stringAsciiCV('STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6') + ) + ), + }) + .build(); + + await db.update(block); + + // Test multiple transactions with exclude_function_args=true + const resExcluded = await supertest(api.server) + .get( + '/extended/v1/tx/multiple?tx_id=0x4567000000000000000000000000000000000000000000000000000000000000&exclude_function_args=true' + ) + .expect(200); + + const txId = '0x4567000000000000000000000000000000000000000000000000000000000000'; + expect(resExcluded.body[txId]).toBeDefined(); + expect(resExcluded.body[txId].found).toBe(true); + expect(resExcluded.body[txId].result.tx_type).toBe('contract_call'); + expect(resExcluded.body[txId].result.contract_call.function_args).toBeUndefined(); + + // Test multiple transactions with exclude_function_args=false + const resIncluded = await supertest(api.server) + .get( + '/extended/v1/tx/multiple?tx_id=0x4567000000000000000000000000000000000000000000000000000000000000&exclude_function_args=false' + ) + .expect(200); + + expect(resIncluded.body[txId]).toBeDefined(); + expect(resIncluded.body[txId].found).toBe(true); + expect(resIncluded.body[txId].result.tx_type).toBe('contract_call'); + expect(Array.isArray(resIncluded.body[txId].result.contract_call.function_args)).toBe(true); + expect(resIncluded.body[txId].result.contract_call.function_args.length).toBe(2); + }); + + // New indirect regression test: ensure contract-call transactions inside a block still + // respect the exclude_function_args flag when fetched individually + test('block contract-call txs respect exclude_function_args flag (indirect check)', async () => { + const block = new TestBlockBuilder({ + block_height: 1, + index_block_hash: '0x06', + block_hash: '0x06', + }) + .addTx({ + tx_id: '0xabcdef0000000000000000000000000000000000000000000000000000000000', + type_id: DbTxTypeId.ContractCall, + contract_call_contract_id: 'SP000000000000000000002Q6VF78.pox-4', + contract_call_function_name: 'delegate-stx', + contract_call_function_args: bufferToHex( + createClarityValueArray( + uintCV(1000000), + stringAsciiCV('STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6') + ) + ), + }) + .addTx({ + tx_id: '0xbcdefa0000000000000000000000000000000000000000000000000000000000', + type_id: DbTxTypeId.TokenTransfer, + token_transfer_recipient_address: 'STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6', + token_transfer_amount: 100n, + }) + .build(); + + await db.update(block); + + // 1. Fetch block to get the contract-call tx id + const blockRes = await supertest(api.server) + .get(`/extended/v1/block/${block.block.block_hash}`) + .expect(200); + + const contractCallTxId = blockRes.body.txs.find((id: string) => id === block.txs[0].tx.tx_id); + expect(contractCallTxId).toBe(block.txs[0].tx.tx_id); + + // 2. Fetch that tx with exclude_function_args=true and assert omission + const txRes = await supertest(api.server) + .get(`/extended/v1/tx/${contractCallTxId}?exclude_function_args=true`) + .expect(200); + + expect(txRes.body.tx_type).toBe('contract_call'); + expect(txRes.body.contract_call.function_args).toBeUndefined(); + }); }); diff --git a/tests/utils/test-builders.ts b/tests/utils/test-builders.ts index d1f51792c..5ee5bdf3d 100644 --- a/tests/utils/test-builders.ts +++ b/tests/utils/test-builders.ts @@ -244,7 +244,7 @@ function testTx(args?: TestTxArgs): DataStoreTxEventData { microblock_hash: args?.microblock_hash ?? MICROBLOCK_HASH, token_transfer_amount: args?.token_transfer_amount ?? TOKEN_TRANSFER_AMOUNT, token_transfer_recipient_address: args?.token_transfer_recipient_address ?? RECIPIENT_ADDRESS, - token_transfer_memo: args?.token_transfer_memo ?? '', + token_transfer_memo: args?.token_transfer_memo ?? '0x', smart_contract_clarity_version: args?.smart_contract_clarity_version, smart_contract_contract_id: args?.smart_contract_contract_id, smart_contract_source_code: args?.smart_contract_source_code, @@ -344,7 +344,7 @@ export function testMempoolTx(args?: TestMempoolTxArgs): DbMempoolTxRaw { origin_hash_mode: 1, sender_address: args?.sender_address ?? SENDER_ADDRESS, token_transfer_amount: args?.token_transfer_amount ?? 1234n, - token_transfer_memo: args?.token_transfer_memo ?? '', + token_transfer_memo: args?.token_transfer_memo ?? '0x', token_transfer_recipient_address: args?.token_transfer_recipient_address ?? RECIPIENT_ADDRESS, smart_contract_clarity_version: args?.smart_contract_clarity_version, smart_contract_contract_id: args?.smart_contract_contract_id ?? CONTRACT_ID,