Skip to content

Commit 9c2fd78

Browse files
authored
feat: tx list contract id/name filter options (#2018)
* feat: tx list contract id/name filter options * test: fix smart contract test
1 parent e7c224b commit 9c2fd78

File tree

7 files changed

+221
-12
lines changed

7 files changed

+221
-12
lines changed

docs/openapi.yaml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,20 @@ paths:
254254
schema:
255255
type: integer
256256
example: 1706745599
257+
- name: contract_id
258+
in: query
259+
description: Filter by contract call transactions involving this contract ID
260+
required: false
261+
schema:
262+
type: string
263+
example: "SP000000000000000000002Q6VF78.pox-4"
264+
- name: function_name
265+
in: query
266+
description: Filter by contract call transactions involving this function name
267+
required: false
268+
schema:
269+
type: string
270+
example: "delegate-stx"
257271
- name: order
258272
in: query
259273
description: Option to sort results in ascending or descending order
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/** @param { import("node-pg-migrate").MigrationBuilder } pgm */
2+
exports.up = pgm => {
3+
pgm.createIndex('txs', 'contract_call_function_name');
4+
};
5+
6+
/** @param { import("node-pg-migrate").MigrationBuilder } pgm */
7+
exports.down = pgm => {
8+
pgm.dropIndex('txs', 'contract_call_function_name');
9+
};

src/api/routes/tx.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,22 @@ export function createTxRouter(db: PgStore): express.Router {
124124
endTime = parseInt(req.query.end_time);
125125
}
126126

127+
let contractId: string | undefined;
128+
if (typeof req.query.contract_id === 'string') {
129+
if (!isValidPrincipal(req.query.contract_id)) {
130+
throw new InvalidRequestError(
131+
`Invalid query parameter for "contract_id": "${req.query.contract_id}" is not a valid principal`,
132+
InvalidRequestErrorType.invalid_param
133+
);
134+
}
135+
contractId = req.query.contract_id;
136+
}
137+
138+
let functionName: string | undefined;
139+
if (typeof req.query.function_name === 'string') {
140+
functionName = req.query.function_name;
141+
}
142+
127143
let sortBy: 'block_height' | 'burn_block_time' | 'fee' | undefined;
128144
if (req.query.sort_by) {
129145
if (
@@ -148,6 +164,8 @@ export function createTxRouter(db: PgStore): express.Router {
148164
toAddress,
149165
startTime,
150166
endTime,
167+
contractId,
168+
functionName,
151169
order,
152170
sortBy,
153171
});

src/datastore/pg-store.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1420,6 +1420,8 @@ export class PgStore extends BasePgStore {
14201420
toAddress,
14211421
startTime,
14221422
endTime,
1423+
contractId,
1424+
functionName,
14231425
order,
14241426
sortBy,
14251427
}: {
@@ -1431,6 +1433,8 @@ export class PgStore extends BasePgStore {
14311433
toAddress?: string;
14321434
startTime?: number;
14331435
endTime?: number;
1436+
contractId?: string;
1437+
functionName?: string;
14341438
order?: 'desc' | 'asc';
14351439
sortBy?: 'block_height' | 'burn_block_time' | 'fee';
14361440
}): Promise<{ results: DbTx[]; total: number }> {
@@ -1464,6 +1468,12 @@ export class PgStore extends BasePgStore {
14641468
: sql``;
14651469
const startTimeFilterSql = startTime ? sql`AND burn_block_time >= ${startTime}` : sql``;
14661470
const endTimeFilterSql = endTime ? sql`AND burn_block_time <= ${endTime}` : sql``;
1471+
const contractIdFilterSql = contractId
1472+
? sql`AND contract_call_contract_id = ${contractId}`
1473+
: sql``;
1474+
const contractFuncFilterSql = functionName
1475+
? sql`AND contract_call_function_name = ${functionName}`
1476+
: sql``;
14671477
const noFilters =
14681478
txTypeFilter.length === 0 && !fromAddress && !toAddress && !startTime && !endTime;
14691479

@@ -1481,6 +1491,8 @@ export class PgStore extends BasePgStore {
14811491
${toAddressFilterSql}
14821492
${startTimeFilterSql}
14831493
${endTimeFilterSql}
1494+
${contractIdFilterSql}
1495+
${contractFuncFilterSql}
14841496
`;
14851497

14861498
const resultQuery: ContractTxQueryResult[] = await sql<ContractTxQueryResult[]>`
@@ -1492,6 +1504,8 @@ export class PgStore extends BasePgStore {
14921504
${toAddressFilterSql}
14931505
${startTimeFilterSql}
14941506
${endTimeFilterSql}
1507+
${contractIdFilterSql}
1508+
${contractFuncFilterSql}
14951509
${orderBySql}
14961510
LIMIT ${limit}
14971511
OFFSET ${offset}

src/test-utils/test-builders.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,20 @@ function testTx(args?: TestTxArgs): DataStoreTxEventData {
256256
pox3Events: [],
257257
pox4Events: [],
258258
};
259+
if (
260+
data.tx.type_id === DbTxTypeId.SmartContract ||
261+
data.tx.type_id === DbTxTypeId.VersionedSmartContract
262+
) {
263+
data.smartContracts.push({
264+
tx_id: data.tx.tx_id,
265+
canonical: data.tx.canonical,
266+
contract_id: data.tx.smart_contract_contract_id as string,
267+
block_height: data.tx.block_height,
268+
clarity_version: data.tx.smart_contract_clarity_version as number,
269+
source_code: data.tx.smart_contract_source_code as string,
270+
abi: data.tx.abi as string,
271+
});
272+
}
259273
return data;
260274
}
261275

src/tests/smart-contract-tests.ts

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1740,12 +1740,8 @@ describe('smart contract tests', () => {
17401740
type_id: DbTxTypeId.SmartContract,
17411741
smart_contract_contract_id: 'SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.contract-1',
17421742
smart_contract_source_code: '(some-contract-src)',
1743-
})
1744-
.addTxSmartContract({
1745-
tx_id: '0x1234',
1746-
block_height: 1,
1747-
contract_id: 'SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.contract-1',
1748-
contract_source: '(some-contract-src)',
1743+
smart_contract_clarity_version: 1,
1744+
abi: JSON.stringify({ some: 'abi' }),
17491745
})
17501746
.build();
17511747
await db.update(block1);
@@ -1759,12 +1755,8 @@ describe('smart contract tests', () => {
17591755
type_id: DbTxTypeId.SmartContract,
17601756
smart_contract_contract_id: 'SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.contract-2',
17611757
smart_contract_source_code: '(some-contract-src)',
1762-
})
1763-
.addTxSmartContract({
1764-
tx_id: '0x1222',
1765-
block_height: 2,
1766-
contract_id: 'SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.contract-2',
1767-
contract_source: '(some-contract-src)',
1758+
smart_contract_clarity_version: 1,
1759+
abi: JSON.stringify({ some: 'abi' }),
17681760
})
17691761
.build();
17701762
await db.update(block2);

src/tests/tx-tests.ts

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
publicKeyToAddress,
1919
AddressVersion,
2020
bufferCV,
21+
stringAsciiCV,
2122
} from '@stacks/transactions';
2223
import { createClarityValueArray } from '../stacks-encoding-helpers';
2324
import { decodeTransaction, TxPayloadVersionedSmartContract } from 'stacks-encoding-native-js';
@@ -2321,6 +2322,153 @@ describe('tx tests', () => {
23212322
);
23222323
});
23232324

2325+
test('tx list - filter by contract id/name', async () => {
2326+
const testContractAddr = 'ST27W5M8BRKA7C5MZE2R1S1F4XTPHFWFRNHA9M04Y.hello-world';
2327+
const testContractFnName = 'test-contract-fn';
2328+
const testContractFnName2 = 'test-contract-fn-2';
2329+
const contractJsonAbi = {
2330+
maps: [],
2331+
functions: [
2332+
{
2333+
args: [
2334+
{ type: 'uint128', name: 'amount' },
2335+
{ type: 'string-ascii', name: 'desc' },
2336+
],
2337+
name: testContractFnName,
2338+
access: 'public',
2339+
outputs: {
2340+
type: {
2341+
response: {
2342+
ok: 'uint128',
2343+
error: 'none',
2344+
},
2345+
},
2346+
},
2347+
},
2348+
{
2349+
args: [
2350+
{ type: 'uint128', name: 'amount' },
2351+
{ type: 'string-ascii', name: 'desc' },
2352+
],
2353+
name: testContractFnName2,
2354+
access: 'public',
2355+
outputs: {
2356+
type: {
2357+
response: {
2358+
ok: 'uint128',
2359+
error: 'none',
2360+
},
2361+
},
2362+
},
2363+
},
2364+
],
2365+
variables: [],
2366+
fungible_tokens: [],
2367+
non_fungible_tokens: [],
2368+
};
2369+
const block1 = new TestBlockBuilder({
2370+
block_height: 1,
2371+
index_block_hash: '0x01',
2372+
burn_block_time: 1710000000,
2373+
})
2374+
.addTx({
2375+
tx_id: '0x0001',
2376+
fee_rate: 1n,
2377+
type_id: DbTxTypeId.SmartContract,
2378+
smart_contract_contract_id: testContractAddr,
2379+
smart_contract_clarity_version: 1,
2380+
smart_contract_source_code: '(some-contract-src)',
2381+
abi: JSON.stringify(contractJsonAbi),
2382+
})
2383+
.build();
2384+
2385+
await db.update(block1);
2386+
2387+
const block2 = new TestBlockBuilder({
2388+
block_height: 2,
2389+
index_block_hash: '0x02',
2390+
parent_block_hash: block1.block.block_hash,
2391+
parent_index_block_hash: block1.block.index_block_hash,
2392+
burn_block_time: 1720000000,
2393+
})
2394+
.addTx({
2395+
tx_id: '0x1234',
2396+
fee_rate: 1n,
2397+
type_id: DbTxTypeId.ContractCall,
2398+
contract_call_contract_id: testContractAddr,
2399+
contract_call_function_name: testContractFnName,
2400+
contract_call_function_args: bufferToHex(
2401+
createClarityValueArray(uintCV(123456), stringAsciiCV('hello'))
2402+
),
2403+
})
2404+
.build();
2405+
await db.update(block2);
2406+
2407+
const block3 = new TestBlockBuilder({
2408+
block_height: 3,
2409+
index_block_hash: '0x03',
2410+
parent_block_hash: block2.block.block_hash,
2411+
parent_index_block_hash: block2.block.index_block_hash,
2412+
burn_block_time: 1730000000,
2413+
})
2414+
.addTx({
2415+
tx_id: '0x2234',
2416+
type_id: DbTxTypeId.ContractCall,
2417+
contract_call_contract_id: testContractAddr,
2418+
contract_call_function_name: testContractFnName2,
2419+
contract_call_function_args: bufferToHex(
2420+
createClarityValueArray(uintCV(123456), stringAsciiCV('hello'))
2421+
),
2422+
})
2423+
.build();
2424+
await db.update(block3);
2425+
2426+
const txsReq1 = await supertest(api.server).get(
2427+
`/extended/v1/tx?contract_id=${testContractAddr}&function_name=${testContractFnName}`
2428+
);
2429+
expect(txsReq1.status).toBe(200);
2430+
expect(txsReq1.body).toEqual(
2431+
expect.objectContaining({
2432+
results: [
2433+
expect.objectContaining({
2434+
tx_id: block2.txs[0].tx.tx_id,
2435+
}),
2436+
],
2437+
})
2438+
);
2439+
2440+
const txsReq2 = await supertest(api.server).get(
2441+
`/extended/v1/tx?contract_id=${testContractAddr}`
2442+
);
2443+
expect(txsReq2.status).toBe(200);
2444+
expect(txsReq2.body).toEqual(
2445+
expect.objectContaining({
2446+
results: [
2447+
expect.objectContaining({
2448+
tx_id: block3.txs[0].tx.tx_id,
2449+
}),
2450+
expect.objectContaining({
2451+
tx_id: block2.txs[0].tx.tx_id,
2452+
}),
2453+
],
2454+
})
2455+
);
2456+
2457+
const txsReq3 = await supertest(api.server).get(
2458+
`/extended/v1/tx?function_name=${testContractFnName2}`
2459+
);
2460+
expect(txsReq3.status).toBe(200);
2461+
expect(txsReq3.body).toEqual(
2462+
expect.objectContaining({
2463+
results: [
2464+
expect.objectContaining({
2465+
tx_id: block3.txs[0].tx.tx_id,
2466+
}),
2467+
],
2468+
})
2469+
);
2470+
});
2471+
23242472
test('fetch raw tx', async () => {
23252473
const block: DbBlock = {
23262474
block_hash: '0x1234',

0 commit comments

Comments
 (0)