Skip to content

Commit 60c3517

Browse files
committed
feat: tx list function name fuzzy search
1 parent 2b6aa6a commit 60c3517

File tree

4 files changed

+138
-0
lines changed

4 files changed

+138
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/** @param { import("node-pg-migrate").MigrationBuilder } pgm */
2+
exports.up = pgm => {
3+
pgm.sql(`
4+
DO $$
5+
BEGIN
6+
IF EXISTS (
7+
SELECT 1
8+
FROM pg_available_extensions
9+
WHERE name = 'pg_trgm'
10+
) THEN
11+
CREATE EXTENSION IF NOT EXISTS pg_trgm;
12+
13+
CREATE INDEX IF NOT EXISTS idx_contract_call_function_name_trgm
14+
ON txs
15+
USING gin (contract_call_function_name gin_trgm_ops);
16+
END IF;
17+
END
18+
$$;
19+
`);
20+
};
21+
22+
/** @param { import("node-pg-migrate").MigrationBuilder } pgm */
23+
exports.down = pgm => {
24+
pgm.sql(`
25+
DROP INDEX IF EXISTS idx_contract_call_function_name_trgm;
26+
`);
27+
28+
pgm.sql('DROP EXTENSION IF EXISTS pg_trgm;');
29+
};

src/api/routes/tx.ts

+12
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,12 @@ export const TxRoutes: FastifyPluginAsync<
113113
examples: [1706745599],
114114
})
115115
),
116+
search_term: Type.Optional(
117+
Type.String({
118+
description: 'Option to search for transactions by a search term',
119+
examples: ['swap'],
120+
})
121+
),
116122
contract_id: Type.Optional(
117123
Type.String({
118124
description: 'Option to filter results by contract ID',
@@ -178,6 +184,11 @@ export const TxRoutes: FastifyPluginAsync<
178184
contractId = req.query.contract_id;
179185
}
180186

187+
let searchTerm: string | undefined;
188+
if (typeof req.query.search_term === 'string') {
189+
searchTerm = req.query.search_term;
190+
}
191+
181192
const { results: txResults, total } = await fastify.db.getTxList({
182193
offset,
183194
limit,
@@ -188,6 +199,7 @@ export const TxRoutes: FastifyPluginAsync<
188199
startTime: req.query.start_time,
189200
endTime: req.query.end_time,
190201
contractId,
202+
searchTerm,
191203
functionName: req.query.function_name,
192204
nonce: req.query.nonce,
193205
order: req.query.order,

src/datastore/pg-store.ts

+27
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ import { parseBlockParam } from '../api/routes/v2/schemas';
101101

102102
export const MIGRATIONS_DIR = path.join(REPO_DIR, 'migrations');
103103

104+
const TRGM_SIMILARITY_THRESHOLD = 0.3;
105+
104106
/**
105107
* This is the main interface between the API and the Postgres database. It contains all methods that
106108
* query the DB in search for blockchain data to be returned via endpoints or WebSockets/Socket.IO.
@@ -1406,6 +1408,17 @@ export class PgStore extends BasePgStore {
14061408
}
14071409
}
14081410

1411+
async isPgTrgmInstalled(sql: PgSqlClient): Promise<boolean> {
1412+
const result = await sql`
1413+
SELECT EXISTS (
1414+
SELECT 1
1415+
FROM pg_extension
1416+
WHERE extname = 'pg_trgm'
1417+
) as installed;
1418+
`;
1419+
return !!result[0].installed;
1420+
}
1421+
14091422
async getTxList({
14101423
limit,
14111424
offset,
@@ -1416,6 +1429,7 @@ export class PgStore extends BasePgStore {
14161429
startTime,
14171430
endTime,
14181431
contractId,
1432+
searchTerm,
14191433
functionName,
14201434
nonce,
14211435
order,
@@ -1430,6 +1444,7 @@ export class PgStore extends BasePgStore {
14301444
startTime?: number;
14311445
endTime?: number;
14321446
contractId?: string;
1447+
searchTerm?: string;
14331448
functionName?: string;
14341449
nonce?: number;
14351450
order?: 'desc' | 'asc';
@@ -1468,6 +1483,15 @@ export class PgStore extends BasePgStore {
14681483
const contractIdFilterSql = contractId
14691484
? sql`AND contract_call_contract_id = ${contractId}`
14701485
: sql``;
1486+
1487+
const pgTrgmInstalled = await this.isPgTrgmInstalled(sql);
1488+
1489+
const searchTermFilterSql = searchTerm
1490+
? pgTrgmInstalled
1491+
? sql`AND similarity(contract_call_function_name, ${searchTerm}) > ${TRGM_SIMILARITY_THRESHOLD}`
1492+
: sql`AND contract_call_function_name ILIKE '%' || ${searchTerm} || '%'`
1493+
: sql``;
1494+
14711495
const contractFuncFilterSql = functionName
14721496
? sql`AND contract_call_function_name = ${functionName}`
14731497
: sql``;
@@ -1479,6 +1503,7 @@ export class PgStore extends BasePgStore {
14791503
!startTime &&
14801504
!endTime &&
14811505
!contractId &&
1506+
!searchTerm &&
14821507
!functionName &&
14831508
!nonce;
14841509

@@ -1497,6 +1522,7 @@ export class PgStore extends BasePgStore {
14971522
${startTimeFilterSql}
14981523
${endTimeFilterSql}
14991524
${contractIdFilterSql}
1525+
${searchTermFilterSql}
15001526
${contractFuncFilterSql}
15011527
${nonceFilterSql}
15021528
`;
@@ -1511,6 +1537,7 @@ export class PgStore extends BasePgStore {
15111537
${startTimeFilterSql}
15121538
${endTimeFilterSql}
15131539
${contractIdFilterSql}
1540+
${searchTermFilterSql}
15141541
${contractFuncFilterSql}
15151542
${nonceFilterSql}
15161543
${orderBySql}

tests/api/tx.test.ts

+70
Original file line numberDiff line numberDiff line change
@@ -2326,6 +2326,76 @@ describe('tx tests', () => {
23262326
);
23272327
});
23282328

2329+
test('tx list - filter by searchTerm using trigram', async () => {
2330+
const transferTokenTx = {
2331+
tx_id: '0x1111',
2332+
contract_call_function_name: 'transferToken',
2333+
};
2334+
2335+
const stakeTokenTx = {
2336+
tx_id: '0x2222',
2337+
contract_call_function_name: 'stakeToken',
2338+
};
2339+
2340+
const burnTokenTx = {
2341+
tx_id: '0x3333',
2342+
contract_call_function_name: 'burnToken',
2343+
};
2344+
2345+
const block1 = new TestBlockBuilder({ block_height: 1, index_block_hash: '0x01' })
2346+
.addTx(transferTokenTx)
2347+
.build();
2348+
await db.update(block1);
2349+
2350+
const block2 = new TestBlockBuilder({
2351+
block_height: 2,
2352+
index_block_hash: '0x02',
2353+
parent_block_hash: block1.block.block_hash,
2354+
parent_index_block_hash: block1.block.index_block_hash,
2355+
})
2356+
.addTx(stakeTokenTx)
2357+
.addTx(burnTokenTx)
2358+
.build();
2359+
await db.update(block2);
2360+
2361+
const searchTerm = 'transfer';
2362+
2363+
const txsReq = await supertest(api.server).get(`/extended/v1/tx?search_term=${searchTerm}`);
2364+
expect(txsReq.status).toBe(200);
2365+
expect(txsReq.body).toEqual(
2366+
expect.objectContaining({
2367+
results: [
2368+
expect.objectContaining({
2369+
tx_id: transferTokenTx.tx_id,
2370+
}),
2371+
],
2372+
})
2373+
);
2374+
2375+
const broadSearchTerm = 'token';
2376+
2377+
const txsReqBroad = await supertest(api.server).get(
2378+
`/extended/v1/tx?search_term=${broadSearchTerm}`
2379+
);
2380+
expect(txsReqBroad.status).toBe(200);
2381+
2382+
expect(txsReqBroad.body).toEqual(
2383+
expect.objectContaining({
2384+
results: [
2385+
expect.objectContaining({
2386+
tx_id: burnTokenTx.tx_id,
2387+
}),
2388+
expect.objectContaining({
2389+
tx_id: stakeTokenTx.tx_id,
2390+
}),
2391+
expect.objectContaining({
2392+
tx_id: transferTokenTx.tx_id,
2393+
}),
2394+
],
2395+
})
2396+
);
2397+
});
2398+
23292399
test('tx list - filter by contract id/name', async () => {
23302400
const testContractAddr = 'ST27W5M8BRKA7C5MZE2R1S1F4XTPHFWFRNHA9M04Y.hello-world';
23312401
const testContractFnName = 'test-contract-fn';

0 commit comments

Comments
 (0)