Skip to content

Commit ae78773

Browse files
authored
feat: tx ordering options (#2005)
* feat: ordering options for `/extendex/v1/tx` endpoint * test: add tx ordering tests * docs: openapi docs for tx ordering options * ci: disable subnets test (not working)
1 parent 03a91ed commit ae78773

File tree

6 files changed

+238
-2
lines changed

6 files changed

+238
-2
lines changed

.github/workflows/ci.yml

+1
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,7 @@ jobs:
413413
parallel: true
414414

415415
test-subnets:
416+
if: false
416417
runs-on: ubuntu-latest
417418
steps:
418419
- uses: actions/checkout@v3

docs/openapi.yaml

+18
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,24 @@ paths:
219219
items:
220220
type: string
221221
enum: [coinbase, token_transfer, smart_contract, contract_call, poison_microblock, tenure_change]
222+
- name: sort_by
223+
in: query
224+
description: Option to sort results by block height, timestamp, or fee
225+
required: false
226+
schema:
227+
type: string
228+
enum: [block_height, burn_block_time, fee]
229+
example: burn_block_time
230+
default: block_height
231+
- name: order
232+
in: query
233+
description: Option to sort results in ascending or descending order
234+
required: false
235+
schema:
236+
type: string
237+
enum: [asc, desc]
238+
example: desc
239+
default: desc
222240
- name: unanchored
223241
in: query
224242
description: Include transaction data from unanchored (i.e. unconfirmed) microblocks
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/** @param { import("node-pg-migrate").MigrationBuilder } pgm */
2+
exports.up = pgm => {
3+
pgm.createIndex('txs', 'burn_block_time');
4+
pgm.createIndex('txs', 'fee_rate');
5+
};
6+
7+
/** @param { import("node-pg-migrate").MigrationBuilder } pgm */
8+
exports.down = pgm => {
9+
pgm.dropIndex('txs', 'burn_block_time');
10+
pgm.dropIndex('txs', 'fee_rate');
11+
};

src/api/routes/tx.ts

+31
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,43 @@ export function createTxRouter(db: PgStore): express.Router {
6565
txTypeFilter = [];
6666
}
6767

68+
let order: 'asc' | 'desc' | undefined;
69+
if (req.query.order) {
70+
if (
71+
typeof req.query.order === 'string' &&
72+
(req.query.order === 'asc' || req.query.order === 'desc')
73+
) {
74+
order = req.query.order;
75+
} else {
76+
throw new InvalidRequestError(
77+
`The "order" query parameter must be a 'desc' or 'asc'`,
78+
InvalidRequestErrorType.invalid_param
79+
);
80+
}
81+
}
82+
83+
let sortBy: 'block_height' | 'burn_block_time' | 'fee' | undefined;
84+
if (req.query.sort_by) {
85+
if (
86+
typeof req.query.sort_by === 'string' &&
87+
['block_height', 'burn_block_time', 'fee'].includes(req.query.sort_by)
88+
) {
89+
sortBy = req.query.sort_by as typeof sortBy;
90+
} else {
91+
throw new InvalidRequestError(
92+
`The "sort_by" query parameter must be 'block_height', 'burn_block_time', or 'fee'`,
93+
InvalidRequestErrorType.invalid_param
94+
);
95+
}
96+
}
6897
const includeUnanchored = isUnanchoredRequest(req, res, next);
6998
const { results: txResults, total } = await db.getTxList({
7099
offset,
71100
limit,
72101
txTypeFilter,
73102
includeUnanchored,
103+
order,
104+
sortBy,
74105
});
75106
const results = txResults.map(tx => parseDbTx(tx));
76107
const response: TransactionResults = { limit, offset, total, results };

src/datastore/pg-store.ts

+25-2
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ import {
102102
import * as path from 'path';
103103
import { PgStoreV2 } from './pg-store-v2';
104104
import { MempoolOrderByParam, OrderParam } from '../api/query-helpers';
105+
import { Fragment } from 'postgres';
105106

106107
export const MIGRATIONS_DIR = path.join(REPO_DIR, 'migrations');
107108

@@ -1415,16 +1416,38 @@ export class PgStore extends BasePgStore {
14151416
offset,
14161417
txTypeFilter,
14171418
includeUnanchored,
1419+
order,
1420+
sortBy,
14181421
}: {
14191422
limit: number;
14201423
offset: number;
14211424
txTypeFilter: TransactionType[];
14221425
includeUnanchored: boolean;
1426+
order?: 'desc' | 'asc';
1427+
sortBy?: 'block_height' | 'burn_block_time' | 'fee';
14231428
}): Promise<{ results: DbTx[]; total: number }> {
14241429
let totalQuery: { count: number }[];
14251430
let resultQuery: ContractTxQueryResult[];
14261431
return await this.sqlTransaction(async sql => {
14271432
const maxHeight = await this.getMaxBlockHeight(sql, { includeUnanchored });
1433+
const orderSql = order === 'asc' ? sql`ASC` : sql`DESC`;
1434+
1435+
let orderBySql: Fragment;
1436+
switch (sortBy) {
1437+
case undefined:
1438+
case 'block_height':
1439+
orderBySql = sql`ORDER BY block_height ${orderSql}, microblock_sequence ${orderSql}, tx_index ${orderSql}`;
1440+
break;
1441+
case 'burn_block_time':
1442+
orderBySql = sql`ORDER BY burn_block_time ${orderSql}, block_height ${orderSql}, microblock_sequence ${orderSql}, tx_index ${orderSql}`;
1443+
break;
1444+
case 'fee':
1445+
orderBySql = sql`ORDER BY fee_rate ${orderSql}, block_height ${orderSql}, microblock_sequence ${orderSql}, tx_index ${orderSql}`;
1446+
break;
1447+
default:
1448+
throw new Error(`Invalid sortBy param: ${sortBy}`);
1449+
}
1450+
14281451
if (txTypeFilter.length === 0) {
14291452
totalQuery = await sql<{ count: number }[]>`
14301453
SELECT ${includeUnanchored ? sql('tx_count_unanchored') : sql('tx_count')} AS count
@@ -1434,7 +1457,7 @@ export class PgStore extends BasePgStore {
14341457
SELECT ${sql(TX_COLUMNS)}, ${abiColumn(sql)}
14351458
FROM txs
14361459
WHERE canonical = true AND microblock_canonical = true AND block_height <= ${maxHeight}
1437-
ORDER BY block_height DESC, microblock_sequence DESC, tx_index DESC
1460+
${orderBySql}
14381461
LIMIT ${limit}
14391462
OFFSET ${offset}
14401463
`;
@@ -1451,7 +1474,7 @@ export class PgStore extends BasePgStore {
14511474
FROM txs
14521475
WHERE canonical = true AND microblock_canonical = true
14531476
AND type_id IN ${sql(txTypeIds)} AND block_height <= ${maxHeight}
1454-
ORDER BY block_height DESC, microblock_sequence DESC, tx_index DESC
1477+
${orderBySql}
14551478
LIMIT ${limit}
14561479
OFFSET ${offset}
14571480
`;

src/tests/tx-tests.ts

+152
Original file line numberDiff line numberDiff line change
@@ -1938,6 +1938,158 @@ describe('tx tests', () => {
19381938
expect(JSON.parse(fetchTx.text)).toEqual(expectedResp);
19391939
});
19401940

1941+
test('tx list - order by', async () => {
1942+
const block1 = new TestBlockBuilder({ block_height: 1, index_block_hash: '0x01' })
1943+
.addTx({
1944+
tx_id: '0x1234',
1945+
fee_rate: 1n,
1946+
burn_block_time: 2,
1947+
})
1948+
.build();
1949+
1950+
await db.update(block1);
1951+
1952+
const block2 = new TestBlockBuilder({
1953+
block_height: 2,
1954+
index_block_hash: '0x02',
1955+
parent_block_hash: block1.block.block_hash,
1956+
parent_index_block_hash: block1.block.index_block_hash,
1957+
})
1958+
.addTx({
1959+
tx_id: '0x2234',
1960+
fee_rate: 3n,
1961+
burn_block_time: 1,
1962+
})
1963+
.build();
1964+
await db.update(block2);
1965+
1966+
const block3 = new TestBlockBuilder({
1967+
block_height: 3,
1968+
index_block_hash: '0x03',
1969+
parent_block_hash: block2.block.block_hash,
1970+
parent_index_block_hash: block2.block.index_block_hash,
1971+
})
1972+
.addTx({
1973+
tx_id: '0x3234',
1974+
fee_rate: 2n,
1975+
burn_block_time: 3,
1976+
})
1977+
.build();
1978+
await db.update(block3);
1979+
1980+
const txsReqAsc = await supertest(api.server).get(`/extended/v1/tx?order=asc`);
1981+
expect(txsReqAsc.status).toBe(200);
1982+
expect(txsReqAsc.body).toEqual(
1983+
expect.objectContaining({
1984+
results: [
1985+
expect.objectContaining({
1986+
tx_id: block1.txs[0].tx.tx_id,
1987+
}),
1988+
expect.objectContaining({
1989+
tx_id: block2.txs[0].tx.tx_id,
1990+
}),
1991+
expect.objectContaining({
1992+
tx_id: block3.txs[0].tx.tx_id,
1993+
}),
1994+
],
1995+
})
1996+
);
1997+
1998+
const txsReqDesc = await supertest(api.server).get(`/extended/v1/tx?order=desc`);
1999+
expect(txsReqDesc.status).toBe(200);
2000+
expect(txsReqDesc.body).toEqual(
2001+
expect.objectContaining({
2002+
results: [
2003+
expect.objectContaining({
2004+
tx_id: block3.txs[0].tx.tx_id,
2005+
}),
2006+
expect.objectContaining({
2007+
tx_id: block2.txs[0].tx.tx_id,
2008+
}),
2009+
expect.objectContaining({
2010+
tx_id: block1.txs[0].tx.tx_id,
2011+
}),
2012+
],
2013+
})
2014+
);
2015+
2016+
const txsReqTimeDesc = await supertest(api.server).get(
2017+
`/extended/v1/tx?sort_by=burn_block_time&order=desc`
2018+
);
2019+
expect(txsReqTimeDesc.status).toBe(200);
2020+
expect(txsReqTimeDesc.body).toEqual(
2021+
expect.objectContaining({
2022+
results: [
2023+
expect.objectContaining({
2024+
tx_id: block3.txs[0].tx.tx_id,
2025+
}),
2026+
expect.objectContaining({
2027+
tx_id: block1.txs[0].tx.tx_id,
2028+
}),
2029+
expect.objectContaining({
2030+
tx_id: block2.txs[0].tx.tx_id,
2031+
}),
2032+
],
2033+
})
2034+
);
2035+
2036+
const txsReqTimeAsc = await supertest(api.server).get(
2037+
`/extended/v1/tx?sort_by=burn_block_time&order=asc`
2038+
);
2039+
expect(txsReqTimeAsc.status).toBe(200);
2040+
expect(txsReqTimeAsc.body).toEqual(
2041+
expect.objectContaining({
2042+
results: [
2043+
expect.objectContaining({
2044+
tx_id: block2.txs[0].tx.tx_id,
2045+
}),
2046+
expect.objectContaining({
2047+
tx_id: block1.txs[0].tx.tx_id,
2048+
}),
2049+
expect.objectContaining({
2050+
tx_id: block3.txs[0].tx.tx_id,
2051+
}),
2052+
],
2053+
})
2054+
);
2055+
2056+
const txsReqFeeDesc = await supertest(api.server).get(`/extended/v1/tx?sort_by=fee&order=desc`);
2057+
expect(txsReqFeeDesc.status).toBe(200);
2058+
expect(txsReqFeeDesc.body).toEqual(
2059+
expect.objectContaining({
2060+
results: [
2061+
expect.objectContaining({
2062+
tx_id: block2.txs[0].tx.tx_id,
2063+
}),
2064+
expect.objectContaining({
2065+
tx_id: block3.txs[0].tx.tx_id,
2066+
}),
2067+
expect.objectContaining({
2068+
tx_id: block1.txs[0].tx.tx_id,
2069+
}),
2070+
],
2071+
})
2072+
);
2073+
2074+
const txsReqFeeAsc = await supertest(api.server).get(`/extended/v1/tx?sort_by=fee&order=asc`);
2075+
expect(txsReqFeeAsc.status).toBe(200);
2076+
expect(txsReqFeeAsc.body).toEqual(
2077+
expect.objectContaining({
2078+
results: [
2079+
expect.objectContaining({
2080+
tx_id: block1.txs[0].tx.tx_id,
2081+
}),
2082+
expect.objectContaining({
2083+
tx_id: block3.txs[0].tx.tx_id,
2084+
}),
2085+
expect.objectContaining({
2086+
tx_id: block2.txs[0].tx.tx_id,
2087+
}),
2088+
],
2089+
})
2090+
);
2091+
});
2092+
19412093
test('fetch raw tx', async () => {
19422094
const block: DbBlock = {
19432095
block_hash: '0x1234',

0 commit comments

Comments
 (0)