Skip to content

Commit 542973c

Browse files
authored
feat: tx to/from address options (#2012)
1 parent ae78773 commit 542973c

File tree

4 files changed

+186
-33
lines changed

4 files changed

+186
-33
lines changed

docs/openapi.yaml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,18 @@ paths:
219219
items:
220220
type: string
221221
enum: [coinbase, token_transfer, smart_contract, contract_call, poison_microblock, tenure_change]
222+
- name: from_address
223+
in: query
224+
description: Option to filter results by sender address
225+
required: false
226+
schema:
227+
type: string
228+
- name: to_address
229+
in: query
230+
description: Option to filter results by recipient address
231+
required: false
232+
schema:
233+
type: string
222234
- name: sort_by
223235
in: query
224236
description: Option to sort results by block height, timestamp, or fee

src/api/routes/tx.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,28 @@ export function createTxRouter(db: PgStore): express.Router {
8080
}
8181
}
8282

83+
let fromAddress: string | undefined;
84+
if (typeof req.query.from_address === 'string') {
85+
if (!isValidC32Address(req.query.from_address)) {
86+
throw new InvalidRequestError(
87+
`Invalid query parameter for "from_address": "${req.query.from_address}" is not a valid STX address`,
88+
InvalidRequestErrorType.invalid_param
89+
);
90+
}
91+
fromAddress = req.query.from_address;
92+
}
93+
94+
let toAddress: string | undefined;
95+
if (typeof req.query.to_address === 'string') {
96+
if (!isValidPrincipal(req.query.to_address)) {
97+
throw new InvalidRequestError(
98+
`Invalid query parameter for "to_address": "${req.query.to_address}" is not a valid STX address`,
99+
InvalidRequestErrorType.invalid_param
100+
);
101+
}
102+
toAddress = req.query.to_address;
103+
}
104+
83105
let sortBy: 'block_height' | 'burn_block_time' | 'fee' | undefined;
84106
if (req.query.sort_by) {
85107
if (
@@ -100,6 +122,8 @@ export function createTxRouter(db: PgStore): express.Router {
100122
limit,
101123
txTypeFilter,
102124
includeUnanchored,
125+
fromAddress,
126+
toAddress,
103127
order,
104128
sortBy,
105129
});

src/datastore/pg-store.ts

Lines changed: 42 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1416,18 +1416,20 @@ export class PgStore extends BasePgStore {
14161416
offset,
14171417
txTypeFilter,
14181418
includeUnanchored,
1419+
fromAddress,
1420+
toAddress,
14191421
order,
14201422
sortBy,
14211423
}: {
14221424
limit: number;
14231425
offset: number;
14241426
txTypeFilter: TransactionType[];
14251427
includeUnanchored: boolean;
1428+
fromAddress?: string;
1429+
toAddress?: string;
14261430
order?: 'desc' | 'asc';
14271431
sortBy?: 'block_height' | 'burn_block_time' | 'fee';
14281432
}): Promise<{ results: DbTx[]; total: number }> {
1429-
let totalQuery: { count: number }[];
1430-
let resultQuery: ContractTxQueryResult[];
14311433
return await this.sqlTransaction(async sql => {
14321434
const maxHeight = await this.getMaxBlockHeight(sql, { includeUnanchored });
14331435
const orderSql = order === 'asc' ? sql`ASC` : sql`DESC`;
@@ -1448,37 +1450,44 @@ export class PgStore extends BasePgStore {
14481450
throw new Error(`Invalid sortBy param: ${sortBy}`);
14491451
}
14501452

1451-
if (txTypeFilter.length === 0) {
1452-
totalQuery = await sql<{ count: number }[]>`
1453-
SELECT ${includeUnanchored ? sql('tx_count_unanchored') : sql('tx_count')} AS count
1454-
FROM chain_tip
1455-
`;
1456-
resultQuery = await sql<ContractTxQueryResult[]>`
1457-
SELECT ${sql(TX_COLUMNS)}, ${abiColumn(sql)}
1458-
FROM txs
1459-
WHERE canonical = true AND microblock_canonical = true AND block_height <= ${maxHeight}
1460-
${orderBySql}
1461-
LIMIT ${limit}
1462-
OFFSET ${offset}
1463-
`;
1464-
} else {
1465-
const txTypeIds = txTypeFilter.flatMap<number>(t => getTxTypeId(t));
1466-
totalQuery = await sql<{ count: number }[]>`
1467-
SELECT COUNT(*)::integer
1468-
FROM txs
1469-
WHERE canonical = true AND microblock_canonical = true
1470-
AND type_id IN ${sql(txTypeIds)} AND block_height <= ${maxHeight}
1471-
`;
1472-
resultQuery = await sql<ContractTxQueryResult[]>`
1473-
SELECT ${sql(TX_COLUMNS)}, ${abiColumn(sql)}
1474-
FROM txs
1475-
WHERE canonical = true AND microblock_canonical = true
1476-
AND type_id IN ${sql(txTypeIds)} AND block_height <= ${maxHeight}
1477-
${orderBySql}
1478-
LIMIT ${limit}
1479-
OFFSET ${offset}
1480-
`;
1481-
}
1453+
const txTypeFilterSql =
1454+
txTypeFilter.length > 0
1455+
? sql`AND type_id IN ${sql(txTypeFilter.flatMap<number>(t => getTxTypeId(t)))}`
1456+
: sql``;
1457+
const fromAddressFilterSql = fromAddress ? sql`AND sender_address = ${fromAddress}` : sql``;
1458+
const toAddressFilterSql = toAddress
1459+
? sql`AND token_transfer_recipient_address = ${toAddress}`
1460+
: sql``;
1461+
1462+
const noFilters = txTypeFilter.length === 0 && !fromAddress && !toAddress;
1463+
1464+
const totalQuery: { count: number }[] = noFilters
1465+
? await sql<{ count: number }[]>`
1466+
SELECT ${includeUnanchored ? sql('tx_count_unanchored') : sql('tx_count')} AS count
1467+
FROM chain_tip
1468+
`
1469+
: await sql<{ count: number }[]>`
1470+
SELECT COUNT(*)::integer AS count
1471+
FROM txs
1472+
WHERE canonical = true AND microblock_canonical = true AND block_height <= ${maxHeight}
1473+
${txTypeFilterSql}
1474+
${fromAddressFilterSql}
1475+
${toAddressFilterSql}
1476+
1477+
`;
1478+
1479+
const resultQuery: ContractTxQueryResult[] = await sql<ContractTxQueryResult[]>`
1480+
SELECT ${sql(TX_COLUMNS)}, ${abiColumn(sql)}
1481+
FROM txs
1482+
WHERE canonical = true AND microblock_canonical = true AND block_height <= ${maxHeight}
1483+
${txTypeFilterSql}
1484+
${fromAddressFilterSql}
1485+
${toAddressFilterSql}
1486+
${orderBySql}
1487+
LIMIT ${limit}
1488+
OFFSET ${offset}
1489+
`;
1490+
14821491
const parsed = resultQuery.map(r => parseTxQueryResult(r));
14831492
return { results: parsed, total: totalQuery[0].count };
14841493
});

src/tests/tx-tests.ts

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2090,6 +2090,114 @@ describe('tx tests', () => {
20902090
);
20912091
});
20922092

2093+
test('tx list - filter by to/from address', async () => {
2094+
const fromAddress = 'ST1HB1T8WRNBYB0Y3T7WXZS38NKKPTBR3EG9EPJKR';
2095+
const toAddress = 'STRYYQQ9M8KAF4NS7WNZQYY59X93XEKR31JP64CP';
2096+
const differentAddress = 'STF9B75ADQAVXQHNEQ6KGHXTG7JP305J2GRWF3A2';
2097+
2098+
const block1 = new TestBlockBuilder({ block_height: 1, index_block_hash: '0x01' })
2099+
.addTx({
2100+
tx_id: '0x0001',
2101+
sender_address: fromAddress,
2102+
token_transfer_recipient_address: toAddress,
2103+
})
2104+
.build();
2105+
await db.update(block1);
2106+
2107+
const block2 = new TestBlockBuilder({
2108+
block_height: 2,
2109+
index_block_hash: '0x02',
2110+
parent_block_hash: block1.block.block_hash,
2111+
parent_index_block_hash: block1.block.index_block_hash,
2112+
})
2113+
.addTx({
2114+
tx_id: '0x0002',
2115+
sender_address: fromAddress,
2116+
token_transfer_recipient_address: toAddress,
2117+
})
2118+
.addTx({
2119+
tx_id: '0x0003',
2120+
sender_address: fromAddress,
2121+
token_transfer_recipient_address: toAddress,
2122+
})
2123+
.addTx({
2124+
tx_id: '0x0004',
2125+
sender_address: fromAddress,
2126+
token_transfer_recipient_address: differentAddress,
2127+
})
2128+
.addTx({
2129+
tx_id: '0x0005',
2130+
sender_address: differentAddress,
2131+
token_transfer_recipient_address: toAddress,
2132+
})
2133+
.build();
2134+
await db.update(block2);
2135+
2136+
const txsReqFrom = await supertest(api.server).get(
2137+
`/extended/v1/tx?from_address=${fromAddress}`
2138+
);
2139+
expect(txsReqFrom.status).toBe(200);
2140+
expect(txsReqFrom.body).toEqual(
2141+
expect.objectContaining({
2142+
results: [
2143+
expect.objectContaining({
2144+
tx_id: block2.txs[2].tx.tx_id,
2145+
}),
2146+
expect.objectContaining({
2147+
tx_id: block2.txs[1].tx.tx_id,
2148+
}),
2149+
expect.objectContaining({
2150+
tx_id: block2.txs[0].tx.tx_id,
2151+
}),
2152+
expect.objectContaining({
2153+
tx_id: block1.txs[0].tx.tx_id,
2154+
}),
2155+
],
2156+
})
2157+
);
2158+
2159+
const txsReqTo = await supertest(api.server).get(`/extended/v1/tx?to_address=${toAddress}`);
2160+
expect(txsReqTo.status).toBe(200);
2161+
expect(txsReqTo.body).toEqual(
2162+
expect.objectContaining({
2163+
results: [
2164+
expect.objectContaining({
2165+
tx_id: block2.txs[3].tx.tx_id,
2166+
}),
2167+
expect.objectContaining({
2168+
tx_id: block2.txs[1].tx.tx_id,
2169+
}),
2170+
expect.objectContaining({
2171+
tx_id: block2.txs[0].tx.tx_id,
2172+
}),
2173+
expect.objectContaining({
2174+
tx_id: block1.txs[0].tx.tx_id,
2175+
}),
2176+
],
2177+
})
2178+
);
2179+
2180+
const txsReqFromTo = await supertest(api.server).get(
2181+
`/extended/v1/tx?from_address=${fromAddress}&to_address=${toAddress}`
2182+
);
2183+
expect(txsReqFromTo.status).toBe(200);
2184+
expect(txsReqFromTo.body).toEqual(
2185+
expect.objectContaining({
2186+
results: [
2187+
expect.objectContaining({
2188+
tx_id: block2.txs[1].tx.tx_id,
2189+
}),
2190+
expect.objectContaining({
2191+
tx_id: block2.txs[0].tx.tx_id,
2192+
}),
2193+
expect.objectContaining({
2194+
tx_id: block1.txs[0].tx.tx_id,
2195+
}),
2196+
],
2197+
})
2198+
);
2199+
});
2200+
20932201
test('fetch raw tx', async () => {
20942202
const block: DbBlock = {
20952203
block_hash: '0x1234',

0 commit comments

Comments
 (0)