mirror of
https://github.com/alexgo-io/stacks-blockchain-api.git
synced 2026-06-17 10:51:19 +08:00
feat: add /extended/v2/mempool/fees endpoint (#1795)
* docs: endpoints * feat: fees endpoint * test: strict eq
This commit is contained in:
26
docs/api/mempool/get-fee-priorities.example.json
Normal file
26
docs/api/mempool/get-fee-priorities.example.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"all": {
|
||||
"no_priority": 3000,
|
||||
"low_priority": 3000,
|
||||
"medium_priority": 6000,
|
||||
"high_priority": 401199
|
||||
},
|
||||
"token_transfer": {
|
||||
"no_priority": 3000,
|
||||
"low_priority": 3000,
|
||||
"medium_priority": 6000,
|
||||
"high_priority": 401199
|
||||
},
|
||||
"smart_contract": {
|
||||
"no_priority": 837500,
|
||||
"low_priority": 925000,
|
||||
"medium_priority": 1012500,
|
||||
"high_priority": 1082500
|
||||
},
|
||||
"contract_call": {
|
||||
"no_priority": 3000,
|
||||
"low_priority": 10368,
|
||||
"medium_priority": 100000,
|
||||
"high_priority": 1000000
|
||||
}
|
||||
}
|
||||
107
docs/api/mempool/get-fee-priorities.schema.json
Normal file
107
docs/api/mempool/get-fee-priorities.schema.json
Normal file
@@ -0,0 +1,107 @@
|
||||
{
|
||||
"description": "GET request that returns fee priorities from mempool transactions",
|
||||
"title": "MempoolFeePriorities",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"all"
|
||||
],
|
||||
"properties": {
|
||||
"all": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"no_priority",
|
||||
"low_priority",
|
||||
"medium_priority",
|
||||
"high_priority"
|
||||
],
|
||||
"properties": {
|
||||
"no_priority": {
|
||||
"type": "integer"
|
||||
},
|
||||
"low_priority": {
|
||||
"type": "integer"
|
||||
},
|
||||
"medium_priority": {
|
||||
"type": "integer"
|
||||
},
|
||||
"high_priority": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"token_transfer": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"no_priority",
|
||||
"low_priority",
|
||||
"medium_priority",
|
||||
"high_priority"
|
||||
],
|
||||
"properties": {
|
||||
"no_priority": {
|
||||
"type": "integer"
|
||||
},
|
||||
"low_priority": {
|
||||
"type": "integer"
|
||||
},
|
||||
"medium_priority": {
|
||||
"type": "integer"
|
||||
},
|
||||
"high_priority": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"smart_contract": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"no_priority",
|
||||
"low_priority",
|
||||
"medium_priority",
|
||||
"high_priority"
|
||||
],
|
||||
"properties": {
|
||||
"no_priority": {
|
||||
"type": "integer"
|
||||
},
|
||||
"low_priority": {
|
||||
"type": "integer"
|
||||
},
|
||||
"medium_priority": {
|
||||
"type": "integer"
|
||||
},
|
||||
"high_priority": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"contract_call": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"no_priority",
|
||||
"low_priority",
|
||||
"medium_priority",
|
||||
"high_priority"
|
||||
],
|
||||
"properties": {
|
||||
"no_priority": {
|
||||
"type": "integer"
|
||||
},
|
||||
"low_priority": {
|
||||
"type": "integer"
|
||||
},
|
||||
"medium_priority": {
|
||||
"type": "integer"
|
||||
},
|
||||
"high_priority": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
30
docs/generated.d.ts
vendored
30
docs/generated.d.ts
vendored
@@ -49,6 +49,7 @@ export type SchemaMergeRootStub =
|
||||
| GetStxSupplyLegacyFormatResponse
|
||||
| GetStxTotalSupplyPlainResponse
|
||||
| GetStxSupplyResponse
|
||||
| MempoolFeePriorities
|
||||
| MicroblockListResponse
|
||||
| UnanchoredTransactionListResponse
|
||||
| RosettaAccountBalanceRequest
|
||||
@@ -1694,6 +1695,35 @@ export interface GetStxSupplyResponse {
|
||||
*/
|
||||
block_height: number;
|
||||
}
|
||||
/**
|
||||
* GET request that returns fee priorities from mempool transactions
|
||||
*/
|
||||
export interface MempoolFeePriorities {
|
||||
all: {
|
||||
no_priority: number;
|
||||
low_priority: number;
|
||||
medium_priority: number;
|
||||
high_priority: number;
|
||||
};
|
||||
token_transfer?: {
|
||||
no_priority: number;
|
||||
low_priority: number;
|
||||
medium_priority: number;
|
||||
high_priority: number;
|
||||
};
|
||||
smart_contract?: {
|
||||
no_priority: number;
|
||||
low_priority: number;
|
||||
medium_priority: number;
|
||||
high_priority: number;
|
||||
};
|
||||
contract_call?: {
|
||||
no_priority: number;
|
||||
low_priority: number;
|
||||
medium_priority: number;
|
||||
high_priority: number;
|
||||
};
|
||||
}
|
||||
/**
|
||||
* GET request that returns microblocks
|
||||
*/
|
||||
|
||||
@@ -69,6 +69,8 @@ tags:
|
||||
externalDocs:
|
||||
description: Hiro Documentation - Transactions
|
||||
url: https://docs.hiro.so/get-started/transactions
|
||||
- name: Mempool
|
||||
description: Endpoints to obtain Mempool information
|
||||
|
||||
paths:
|
||||
/extended/v1/faucets/stx:
|
||||
@@ -296,6 +298,24 @@ paths:
|
||||
example:
|
||||
$ref: ./api/transaction/get-mempool-transactions.example.json
|
||||
|
||||
/extended/v2/mempool/fees:
|
||||
get:
|
||||
summary: Get mempool transaction fee priorities
|
||||
tags:
|
||||
- Mempool
|
||||
operationId: get_mempool_fee_priorities
|
||||
description: |
|
||||
Returns estimated fee priorities (in micro-STX) for all transactions that are currently in the mempool. Also returns priorities separated by transaction type.
|
||||
responses:
|
||||
200:
|
||||
description: Mempool fee priorities
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: ./api/transaction/get-mempool-transactions.schema.json
|
||||
example:
|
||||
$ref: ./api/transaction/get-mempool-transactions.example.json
|
||||
|
||||
/extended/v1/tx/mempool/dropped:
|
||||
get:
|
||||
summary: Get dropped mempool transactions
|
||||
|
||||
@@ -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',
|
||||
(() => {
|
||||
|
||||
55
src/api/v2/mempool.ts
Normal file
55
src/api/v2/mempool.ts
Normal file
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ import {
|
||||
DbGetBlockWithMetadataOpts,
|
||||
DbGetBlockWithMetadataResponse,
|
||||
DbInboundStxTransfer,
|
||||
DbMempoolFeePriority,
|
||||
DbMempoolStats,
|
||||
DbMempoolTx,
|
||||
DbMicroblock,
|
||||
@@ -1286,6 +1287,44 @@ export class PgStore {
|
||||
};
|
||||
}
|
||||
|
||||
async getMempoolFeePriority(): Promise<DbMempoolFeePriority[]> {
|
||||
const txFeesQuery = await this.sql<DbMempoolFeePriority[]>`
|
||||
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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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).toStrictEqual({
|
||||
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,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user