diff --git a/src/api/init.ts b/src/api/init.ts index 546c2d4086..57589b2fa7 100644 --- a/src/api/init.ts +++ b/src/api/init.ts @@ -45,6 +45,7 @@ import { createPox3EventsRouter } from './routes/pox3'; import { isPgConnectionError } from '../datastore/helpers'; import { createStackingRouter } from './routes/stacking'; import { logger, loggerMiddleware } from '../logger'; +import { createMempoolRouter } from './v2/mempool'; export interface ApiServer { expressApp: express.Express; @@ -220,6 +221,21 @@ export async function startApiServer(opts: { })() ); + app.use( + '/extended/v2', + (() => { + const router = express.Router(); + router.use(cors()); + router.use((req, res, next) => { + // Set caching on all routes to be disabled by default, individual routes can override + res.set('Cache-Control', 'no-store'); + next(); + }); + router.use('/mempool', createMempoolRouter(datastore)); + return router; + })() + ); + app.use( '/extended/beta', (() => { diff --git a/src/api/v2/mempool.ts b/src/api/v2/mempool.ts new file mode 100644 index 0000000000..c106b06711 --- /dev/null +++ b/src/api/v2/mempool.ts @@ -0,0 +1,55 @@ +import * as express from 'express'; +import { asyncHandler } from '../async-handler'; +import { + ETagType, + getETagCacheHandler, + setETagCacheHeaders, +} from '../controllers/cache-controller'; +import { PgStore } from '../../datastore/pg-store'; +import { DbMempoolFeePriority, DbTxTypeId } from '../../datastore/common'; +import { MempoolFeePriorities } from '../../../docs/generated'; + +function parseMempoolFeePriority(fees: DbMempoolFeePriority[]): MempoolFeePriorities { + const out: MempoolFeePriorities = { + all: { no_priority: 0, low_priority: 0, medium_priority: 0, high_priority: 0 }, + }; + for (const fee of fees) { + const value = { + no_priority: fee.no_priority, + low_priority: fee.low_priority, + medium_priority: fee.medium_priority, + high_priority: fee.high_priority, + }; + if (fee.type_id == null) out.all = value; + else + switch (fee.type_id) { + case DbTxTypeId.TokenTransfer: + out.token_transfer = value; + break; + case DbTxTypeId.ContractCall: + out.contract_call = value; + break; + case DbTxTypeId.SmartContract: + case DbTxTypeId.VersionedSmartContract: + out.smart_contract = value; + break; + } + } + return out; +} + +export function createMempoolRouter(db: PgStore): express.Router { + const router = express.Router(); + const mempoolCacheHandler = getETagCacheHandler(db, ETagType.mempool); + + router.get( + '/fees', + mempoolCacheHandler, + asyncHandler(async (req, res, next) => { + setETagCacheHeaders(res); + res.status(200).json(parseMempoolFeePriority(await db.getMempoolFeePriority())); + }) + ); + + return router; +} diff --git a/src/datastore/common.ts b/src/datastore/common.ts index 7378ac50f1..41f2cf7b9b 100644 --- a/src/datastore/common.ts +++ b/src/datastore/common.ts @@ -235,6 +235,14 @@ export interface DbMempoolStats { >; } +export interface DbMempoolFeePriority { + type_id: DbTxTypeId | null; + high_priority: number; + medium_priority: number; + low_priority: number; + no_priority: number; +} + export interface DbMempoolTx extends BaseTx { pruned: boolean; diff --git a/src/datastore/pg-store.ts b/src/datastore/pg-store.ts index 88d1776816..6e88170d53 100644 --- a/src/datastore/pg-store.ts +++ b/src/datastore/pg-store.ts @@ -38,6 +38,7 @@ import { DbGetBlockWithMetadataOpts, DbGetBlockWithMetadataResponse, DbInboundStxTransfer, + DbMempoolFeePriority, DbMempoolStats, DbMempoolTx, DbMicroblock, @@ -1286,6 +1287,44 @@ export class PgStore { }; } + async getMempoolFeePriority(): Promise { + const txFeesQuery = await this.sql` + WITH fees AS ( + ( + SELECT + NULL AS type_id, + ROUND(PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY fee_rate ASC)) AS high_priority, + ROUND(PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY fee_rate ASC)) AS medium_priority, + ROUND(PERCENTILE_CONT(0.50) WITHIN GROUP (ORDER BY fee_rate ASC)) AS low_priority, + ROUND(PERCENTILE_CONT(0.25) WITHIN GROUP (ORDER BY fee_rate ASC)) AS no_priority + FROM mempool_txs + WHERE pruned = FALSE + ) + UNION + ( + WITH txs_grouped AS ( + SELECT + (CASE type_id WHEN 6 THEN 1 ELSE type_id END) AS type_id, + fee_rate + FROM mempool_txs + WHERE pruned = FALSE + AND type_id NOT IN (4, 5) + ) + SELECT + type_id, + ROUND(PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY fee_rate ASC)) AS high_priority, + ROUND(PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY fee_rate ASC)) AS medium_priority, + ROUND(PERCENTILE_CONT(0.50) WITHIN GROUP (ORDER BY fee_rate ASC)) AS low_priority, + ROUND(PERCENTILE_CONT(0.25) WITHIN GROUP (ORDER BY fee_rate ASC)) AS no_priority + FROM txs_grouped + GROUP BY type_id + ) + ) + SELECT * FROM fees ORDER BY type_id ASC NULLS FIRST + `; + return txFeesQuery; + } + async getMempoolTxList({ limit, offset, diff --git a/src/test-utils/test-builders.ts b/src/test-utils/test-builders.ts index 700dbe790f..8146396528 100644 --- a/src/test-utils/test-builders.ts +++ b/src/test-utils/test-builders.ts @@ -16,7 +16,6 @@ import { DbBnsNamespace, DbEventTypeId, DbFtEvent, - DbMempoolTx, DbMempoolTxRaw, DbMicroblockPartial, DbMinerReward, @@ -259,6 +258,8 @@ interface TestMempoolTxArgs { smart_contract_contract_id?: string; status?: DbTxStatus; token_transfer_recipient_address?: string; + token_transfer_amount?: bigint; + token_transfer_memo?: string; tx_id?: string; type_id?: DbTxTypeId; nonce?: number; @@ -287,8 +288,8 @@ export function testMempoolTx(args?: TestMempoolTxArgs): DbMempoolTxRaw { sponsor_address: undefined, origin_hash_mode: 1, sender_address: args?.sender_address ?? SENDER_ADDRESS, - token_transfer_amount: 1234n, - token_transfer_memo: '', + token_transfer_amount: args?.token_transfer_amount ?? 1234n, + token_transfer_memo: args?.token_transfer_memo ?? '', token_transfer_recipient_address: args?.token_transfer_recipient_address ?? RECIPIENT_ADDRESS, smart_contract_clarity_version: args?.smart_contract_clarity_version, smart_contract_contract_id: args?.smart_contract_contract_id ?? CONTRACT_ID, diff --git a/src/tests/mempool-tests.ts b/src/tests/mempool-tests.ts index b44440fae4..6fdf48a691 100644 --- a/src/tests/mempool-tests.ts +++ b/src/tests/mempool-tests.ts @@ -1532,4 +1532,102 @@ describe('mempool tests', () => { const txResult2 = await supertest(api.server).get(`/extended/v1/tx/${txId}`); expect(txResult2.body.tx_status).toBe('success'); }); + + test('returns fee priorities for mempool transactions', async () => { + const mempoolTxs: DbMempoolTxRaw[] = []; + for (let i = 0; i < 10; i++) { + const sender_address = 'SP25YGP221F01S9SSCGN114MKDAK9VRK8P3KXGEMB'; + const tx_id = `0x00000${i}`; + const fee_rate = BigInt(100000 * i); + const nonce = i; + if (i < 3) { + mempoolTxs.push({ + tx_id, + nonce, + fee_rate, + type_id: DbTxTypeId.ContractCall, + anchor_mode: 3, + raw_tx: bufferToHexPrefixString(Buffer.from('test-raw-mempool-tx')), + status: 1, + post_conditions: '0x01f5', + sponsored: false, + sponsor_address: undefined, + contract_call_contract_id: 'SP32AEEF6WW5Y0NMJ1S8SBSZDAY8R5J32NBZFPKKZ.free-punks-v0', + contract_call_function_name: 'test-func', + contract_call_function_args: '0x00', + sender_address, + origin_hash_mode: 1, + pruned: false, + receipt_time: 1616063078, + }); + } else if (i < 6) { + mempoolTxs.push({ + tx_id, + nonce, + type_id: DbTxTypeId.SmartContract, + fee_rate, + anchor_mode: 3, + raw_tx: bufferToHexPrefixString(Buffer.from('test-raw-mempool-tx')), + status: 1, + post_conditions: '0x01f5', + sponsored: false, + sponsor_address: undefined, + sender_address, + origin_hash_mode: 1, + pruned: false, + smart_contract_contract_id: 'some-versioned-smart-contract', + smart_contract_source_code: '(some-versioned-contract-src)', + receipt_time: 1616063078, + }); + } else { + mempoolTxs.push({ + tx_id, + nonce, + type_id: DbTxTypeId.TokenTransfer, + fee_rate, + anchor_mode: 3, + raw_tx: bufferToHexPrefixString(Buffer.from('test-raw-mempool-tx')), + status: 1, + post_conditions: '0x01f5', + sponsored: false, + sponsor_address: undefined, + sender_address, + token_transfer_amount: 100n, + token_transfer_memo: '0x010101', + token_transfer_recipient_address: 'STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6', + origin_hash_mode: 1, + pruned: false, + receipt_time: 1616063078, + }); + } + } + await db.updateMempoolTxs({ mempoolTxs }); + const result = await supertest(api.server).get(`/extended/v2/mempool/fees`); + expect(result.body).toBe({ + all: { + high_priority: 855000, + low_priority: 450000, + medium_priority: 675000, + no_priority: 225000, + }, + contract_call: { + high_priority: 190000, + low_priority: 100000, + medium_priority: 150000, + no_priority: 50000, + }, + smart_contract: { + high_priority: 490000, + low_priority: 400000, + medium_priority: 450000, + no_priority: 350000, + }, + token_transfer: { + high_priority: 885000, + low_priority: 750000, + medium_priority: 825000, + no_priority: 675000, + }, + }); + }); });