Skip to content

Commit 2d45b2e

Browse files
authored
feat: add order_by and order params to /extended/v1/tx/mempool (#1810)
* feat: sort mempool * test: age sort * fix: add enum types * chore: move v2 to proper folder * docs: add to openapi
1 parent cf73661 commit 2d45b2e

File tree

7 files changed

+150
-7
lines changed

7 files changed

+150
-7
lines changed

docs/openapi.yaml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,22 @@ paths:
263263
required: false
264264
schema:
265265
type: string
266+
- name: order_by
267+
in: query
268+
description: Option to sort results by transaction age, size, or fee rate.
269+
required: false
270+
schema:
271+
type: string
272+
enum: [age, size, fee]
273+
example: fee
274+
- name: order
275+
in: query
276+
description: Option to sort results in ascending or descending order.
277+
required: false
278+
schema:
279+
type: string
280+
enum: [asc, desc]
281+
example: asc
266282
- name: limit
267283
in: query
268284
description: max number of mempool transactions to fetch

src/api/init.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ import {
5252
import { createV2BlocksRouter } from './routes/v2/blocks';
5353
import { getReqQuery } from './query-helpers';
5454
import { createV2BurnBlocksRouter } from './routes/v2/burn-blocks';
55-
import { createMempoolRouter } from './v2/mempool';
55+
import { createMempoolRouter } from './routes/v2/mempool';
5656

5757
export interface ApiServer {
5858
expressApp: express.Express;

src/api/query-helpers.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,17 @@ import { InvalidRequestError, InvalidRequestErrorType } from '../errors';
55
import { DbEventTypeId } from './../datastore/common';
66
import { has0xPrefix, hexToBuffer } from '@hirosystems/api-toolkit';
77

8+
export enum MempoolOrderByParam {
9+
fee = 'fee',
10+
size = 'size',
11+
age = 'age',
12+
}
13+
14+
export enum OrderParam {
15+
asc = 'asc',
16+
desc = 'desc',
17+
}
18+
819
function handleBadRequest(res: Response, next: NextFunction, errorMessage: string): never {
920
const error = new InvalidRequestError(errorMessage, InvalidRequestErrorType.bad_request);
1021
res.status(400).json({ error: errorMessage });

src/api/routes/tx.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import {
1616
validateRequestHexInput,
1717
parseAddressOrTxId,
1818
parseEventTypeFilter,
19+
MempoolOrderByParam,
20+
OrderParam,
1921
} from '../query-helpers';
2022
import { getPagingQueryLimit, parsePagingQueryInput, ResourceType } from '../pagination';
2123
import { validate } from '../validate';
@@ -162,10 +164,33 @@ export function createTxRouter(db: PgStore): express.Router {
162164
InvalidRequestErrorType.invalid_param
163165
);
164166
}
167+
168+
const orderBy = req.query.order_by;
169+
if (
170+
orderBy !== undefined &&
171+
orderBy != MempoolOrderByParam.fee &&
172+
orderBy != MempoolOrderByParam.age &&
173+
orderBy != MempoolOrderByParam.size
174+
) {
175+
throw new InvalidRequestError(
176+
`The "order_by" param can only be 'fee', 'age', or 'size'`,
177+
InvalidRequestErrorType.invalid_param
178+
);
179+
}
180+
const order = req.query.order;
181+
if (order !== undefined && order != OrderParam.asc && order != OrderParam.desc) {
182+
throw new InvalidRequestError(
183+
`The "order" param can only be 'asc' or 'desc'`,
184+
InvalidRequestErrorType.invalid_param
185+
);
186+
}
187+
165188
const { results: txResults, total } = await db.getMempoolTxList({
166189
offset,
167190
limit,
168191
includeUnanchored,
192+
orderBy,
193+
order,
169194
senderAddress,
170195
recipientAddress,
171196
address,

src/api/v2/mempool.ts renamed to src/api/routes/v2/mempool.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import * as express from 'express';
2-
import { asyncHandler } from '../async-handler';
2+
import { asyncHandler } from '../../async-handler';
33
import {
44
ETagType,
55
getETagCacheHandler,
66
setETagCacheHeaders,
7-
} from '../controllers/cache-controller';
8-
import { PgStore } from '../../datastore/pg-store';
9-
import { DbMempoolFeePriority, DbTxTypeId } from '../../datastore/common';
10-
import { MempoolFeePriorities } from '../../../docs/generated';
7+
} from '../../controllers/cache-controller';
8+
import { PgStore } from '../../../datastore/pg-store';
9+
import { DbMempoolFeePriority, DbTxTypeId } from '../../../datastore/common';
10+
import { MempoolFeePriorities } from '../../../../docs/generated';
1111

1212
function parseMempoolFeePriority(fees: DbMempoolFeePriority[]): MempoolFeePriorities {
1313
const out: MempoolFeePriorities = {

src/datastore/pg-store.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ import {
101101
} from './connection';
102102
import * as path from 'path';
103103
import { PgStoreV2 } from './pg-store-v2';
104+
import { MempoolOrderByParam, OrderParam } from '../api/query-helpers';
104105

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

@@ -1289,13 +1290,17 @@ export class PgStore extends BasePgStore {
12891290
limit,
12901291
offset,
12911292
includeUnanchored,
1293+
orderBy,
1294+
order,
12921295
senderAddress,
12931296
recipientAddress,
12941297
address,
12951298
}: {
12961299
limit: number;
12971300
offset: number;
12981301
includeUnanchored: boolean;
1302+
orderBy?: MempoolOrderByParam;
1303+
order?: OrderParam;
12991304
senderAddress?: string;
13001305
recipientAddress?: string;
13011306
address?: string;
@@ -1310,6 +1315,9 @@ export class PgStore extends BasePgStore {
13101315
senderAddress || recipientAddress || address
13111316
? sql`(COUNT(*) OVER())::int AS count`
13121317
: sql`(SELECT mempool_tx_count FROM chain_tip) AS count`;
1318+
const orderBySql =
1319+
orderBy == 'fee' ? sql`fee_rate` : orderBy == 'size' ? sql`tx_size` : sql`receipt_time`;
1320+
const orderSql = order == 'asc' ? sql`ASC` : sql`DESC`;
13131321
const resultQuery = await sql<(MempoolTxQueryResult & { count: number })[]>`
13141322
SELECT ${unsafeCols(sql, [...MEMPOOL_TX_COLUMNS, abiColumn('mempool_txs')])}, ${count}
13151323
FROM mempool_txs
@@ -1333,7 +1341,7 @@ export class PgStore extends BasePgStore {
13331341
? sql`OR tx_id IN ${sql(unanchoredTxs)}`
13341342
: sql``
13351343
})
1336-
ORDER BY receipt_time DESC
1344+
ORDER BY ${orderBySql} ${orderSql}
13371345
LIMIT ${limit}
13381346
OFFSET ${offset}
13391347
`;

src/tests/mempool-tests.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1141,6 +1141,89 @@ describe('mempool tests', () => {
11411141
expect(JSON.parse(searchResult7.text)).toEqual(expectedResp7);
11421142
});
11431143

1144+
test('fetch mempool-tx list sorted', async () => {
1145+
const sendAddr = 'SP25YGP221F01S9SSCGN114MKDAK9VRK8P3KXGEMB';
1146+
const recvAddr = 'SP10EZK56MB87JYF5A704K7N18YAT6G6M09HY22GC';
1147+
1148+
const block = new TestBlockBuilder().addTx().build();
1149+
await db.update(block);
1150+
const txs: DbMempoolTxRaw[] = [];
1151+
for (let index = 0; index < 5; index++) {
1152+
const paddedIndex = ('00' + index).slice(-2);
1153+
const mempoolTx: DbMempoolTxRaw = {
1154+
pruned: false,
1155+
tx_id: `0x89120000000000000000000000000000000000000000000000000000000000${paddedIndex}`,
1156+
anchor_mode: 3,
1157+
nonce: 0,
1158+
raw_tx: bufferToHex(Buffer.from('x'.repeat(index + 1))),
1159+
type_id: DbTxTypeId.TokenTransfer,
1160+
receipt_time: (new Date(`2020-07-09T15:14:${paddedIndex}Z`).getTime() / 1000) | 0,
1161+
status: 1,
1162+
post_conditions: '0x01f5',
1163+
fee_rate: 100n * BigInt(index + 1),
1164+
sponsored: false,
1165+
sponsor_address: undefined,
1166+
origin_hash_mode: 1,
1167+
sender_address: sendAddr,
1168+
token_transfer_recipient_address: recvAddr,
1169+
token_transfer_amount: 1234n,
1170+
token_transfer_memo: '',
1171+
};
1172+
txs.push(mempoolTx);
1173+
}
1174+
await db.updateMempoolTxs({ mempoolTxs: txs });
1175+
1176+
let result = await supertest(api.server).get(`/extended/v1/tx/mempool?order_by=fee&order=desc`);
1177+
let json = JSON.parse(result.text);
1178+
expect(json.results[0].fee_rate).toBe('500');
1179+
expect(json.results[1].fee_rate).toBe('400');
1180+
expect(json.results[2].fee_rate).toBe('300');
1181+
expect(json.results[3].fee_rate).toBe('200');
1182+
expect(json.results[4].fee_rate).toBe('100');
1183+
1184+
result = await supertest(api.server).get(`/extended/v1/tx/mempool?order_by=fee&order=asc`);
1185+
json = JSON.parse(result.text);
1186+
expect(json.results[0].fee_rate).toBe('100');
1187+
expect(json.results[1].fee_rate).toBe('200');
1188+
expect(json.results[2].fee_rate).toBe('300');
1189+
expect(json.results[3].fee_rate).toBe('400');
1190+
expect(json.results[4].fee_rate).toBe('500');
1191+
1192+
// Larger transactions were set with higher fees.
1193+
result = await supertest(api.server).get(`/extended/v1/tx/mempool?order_by=size&order=desc`);
1194+
json = JSON.parse(result.text);
1195+
expect(json.results[0].fee_rate).toBe('500');
1196+
expect(json.results[1].fee_rate).toBe('400');
1197+
expect(json.results[2].fee_rate).toBe('300');
1198+
expect(json.results[3].fee_rate).toBe('200');
1199+
expect(json.results[4].fee_rate).toBe('100');
1200+
1201+
result = await supertest(api.server).get(`/extended/v1/tx/mempool?order_by=size&order=asc`);
1202+
json = JSON.parse(result.text);
1203+
expect(json.results[0].fee_rate).toBe('100');
1204+
expect(json.results[1].fee_rate).toBe('200');
1205+
expect(json.results[2].fee_rate).toBe('300');
1206+
expect(json.results[3].fee_rate).toBe('400');
1207+
expect(json.results[4].fee_rate).toBe('500');
1208+
1209+
// Newer transactions were set with higher fees.
1210+
result = await supertest(api.server).get(`/extended/v1/tx/mempool?order_by=age&order=desc`);
1211+
json = JSON.parse(result.text);
1212+
expect(json.results[0].fee_rate).toBe('500');
1213+
expect(json.results[1].fee_rate).toBe('400');
1214+
expect(json.results[2].fee_rate).toBe('300');
1215+
expect(json.results[3].fee_rate).toBe('200');
1216+
expect(json.results[4].fee_rate).toBe('100');
1217+
1218+
result = await supertest(api.server).get(`/extended/v1/tx/mempool?order_by=age&order=asc`);
1219+
json = JSON.parse(result.text);
1220+
expect(json.results[0].fee_rate).toBe('100');
1221+
expect(json.results[1].fee_rate).toBe('200');
1222+
expect(json.results[2].fee_rate).toBe('300');
1223+
expect(json.results[3].fee_rate).toBe('400');
1224+
expect(json.results[4].fee_rate).toBe('500');
1225+
});
1226+
11441227
test('mempool - contract_call tx abi details are retrieved', async () => {
11451228
const block1 = new TestBlockBuilder()
11461229
.addTx()

0 commit comments

Comments
 (0)