mirror of
https://github.com/alexgo-io/stacks-blockchain-api.git
synced 2026-05-13 20:16:45 +08:00
feat: mempool stats endpoint and prometheus metrics (#1241)
* feat: implement endpoint for various mempool stats * chore: remove coinbase tx entries from mempool stats (will always be empty) * chore: update prometheus mempool stats on new block, microblock, and mempool tx * chore: add mempool tx byte size stats * chore: add indexes to speedup mempool table queries * chore: move `registerMempoolPromStats` fn to db helper module * docs: add OpenAPI schema for new mempool stats endpoint * chore: tests for mempool stats endpoint * chore: restore mempool garbage collect env var after test * chore: bytea serialization tests * chore: strict hex string handling for bytea value serializer * chore: improved mempool_tx indexing * chore: unit test fix
This commit is contained in:
86
docs/api/transaction/get-mempool-stats.example.json
Normal file
86
docs/api/transaction/get-mempool-stats.example.json
Normal file
@@ -0,0 +1,86 @@
|
||||
{
|
||||
"tx_type_counts": {
|
||||
"token_transfer": 130,
|
||||
"smart_contract": 2,
|
||||
"contract_call": 310,
|
||||
"poison_microblock": 0
|
||||
},
|
||||
"tx_simple_fee_averages": {
|
||||
"token_transfer": {
|
||||
"p25": 3000,
|
||||
"p50": 3000,
|
||||
"p75": 6000,
|
||||
"p95": 401199.9999999995
|
||||
},
|
||||
"smart_contract": {
|
||||
"p25": 837500,
|
||||
"p50": 925000,
|
||||
"p75": 1012500,
|
||||
"p95": 1082500
|
||||
},
|
||||
"contract_call": {
|
||||
"p25": 3000,
|
||||
"p50": 10368,
|
||||
"p75": 100000,
|
||||
"p95": 1000000
|
||||
},
|
||||
"poison_microblock": {
|
||||
"p25": null,
|
||||
"p50": null,
|
||||
"p75": null,
|
||||
"p95": null
|
||||
}
|
||||
},
|
||||
"tx_ages": {
|
||||
"token_transfer": {
|
||||
"p25": 167.5,
|
||||
"p50": 45,
|
||||
"p75": 1,
|
||||
"p95": 0
|
||||
},
|
||||
"smart_contract": {
|
||||
"p25": 185.5,
|
||||
"p50": 129,
|
||||
"p75": 72.5,
|
||||
"p95": 27.30000000000001
|
||||
},
|
||||
"contract_call": {
|
||||
"p25": 189,
|
||||
"p50": 127.5,
|
||||
"p75": 9.5,
|
||||
"p95": 0
|
||||
},
|
||||
"poison_microblock": {
|
||||
"p25": null,
|
||||
"p50": null,
|
||||
"p75": null,
|
||||
"p95": null
|
||||
}
|
||||
},
|
||||
"tx_byte_sizes": {
|
||||
"token_transfer": {
|
||||
"p25": 180,
|
||||
"p50": 180,
|
||||
"p75": 180,
|
||||
"p95": 180
|
||||
},
|
||||
"smart_contract": {
|
||||
"p25": 706.75,
|
||||
"p50": 814.5,
|
||||
"p75": 922.25,
|
||||
"p95": 1008.45
|
||||
},
|
||||
"contract_call": {
|
||||
"p25": 291,
|
||||
"p50": 435,
|
||||
"p75": 462,
|
||||
"p95": 597
|
||||
},
|
||||
"poison_microblock": {
|
||||
"p25": null,
|
||||
"p50": null,
|
||||
"p75": null,
|
||||
"p95": null
|
||||
}
|
||||
}
|
||||
}
|
||||
414
docs/api/transaction/get-mempool-stats.schema.json
Normal file
414
docs/api/transaction/get-mempool-stats.schema.json
Normal file
@@ -0,0 +1,414 @@
|
||||
{
|
||||
"description": "GET request that returns stats on mempool transactions",
|
||||
"title": "MempoolTransactionStatsResponse",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"tx_type_counts",
|
||||
"tx_simple_fee_averages",
|
||||
"tx_ages",
|
||||
"tx_byte_sizes"
|
||||
],
|
||||
"properties": {
|
||||
"tx_type_counts": {
|
||||
"type": "object",
|
||||
"description": "Number of tranasction in the mempool, broken down by transaction type.",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"token_transfer",
|
||||
"smart_contract",
|
||||
"contract_call",
|
||||
"poison_microblock"
|
||||
],
|
||||
"properties": {
|
||||
"token_transfer": {
|
||||
"type": "number"
|
||||
},
|
||||
"smart_contract": {
|
||||
"type": "number"
|
||||
},
|
||||
"contract_call": {
|
||||
"type": "number"
|
||||
},
|
||||
"poison_microblock": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
},
|
||||
"tx_simple_fee_averages": {
|
||||
"type": "object",
|
||||
"description": "The simple mean (average) transaction fee, broken down by transaction type. Note that this does not factor in actual execution costs. The average fee is not a reliable metric for calculating a fee for a new transaction.",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"token_transfer",
|
||||
"smart_contract",
|
||||
"contract_call",
|
||||
"poison_microblock"
|
||||
],
|
||||
"properties": {
|
||||
"token_transfer": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"p25",
|
||||
"p50",
|
||||
"p75",
|
||||
"p95"
|
||||
],
|
||||
"properties": {
|
||||
"p25": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
},
|
||||
"p50": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
},
|
||||
"p75": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
},
|
||||
"p95": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"smart_contract": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"p25",
|
||||
"p50",
|
||||
"p75",
|
||||
"p95"
|
||||
],
|
||||
"properties": {
|
||||
"p25": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
},
|
||||
"p50": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
},
|
||||
"p75": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
},
|
||||
"p95": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"contract_call": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"p25",
|
||||
"p50",
|
||||
"p75",
|
||||
"p95"
|
||||
],
|
||||
"properties": {
|
||||
"p25": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
},
|
||||
"p50": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
},
|
||||
"p75": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
},
|
||||
"p95": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"poison_microblock": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"p25",
|
||||
"p50",
|
||||
"p75",
|
||||
"p95"
|
||||
],
|
||||
"properties": {
|
||||
"p25": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
},
|
||||
"p50": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
},
|
||||
"p75": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
},
|
||||
"p95": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tx_ages": {
|
||||
"type": "object",
|
||||
"description": "The average time (in blocks) that transactions have lived in the mempool. The start block height is simply the current chain-tip of when the attached Stacks node receives the transaction. This timing can be different across Stacks nodes / API instances due to propagation timing differences in the p2p network.",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"token_transfer",
|
||||
"smart_contract",
|
||||
"contract_call",
|
||||
"poison_microblock"
|
||||
],
|
||||
"properties": {
|
||||
"token_transfer": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"p25",
|
||||
"p50",
|
||||
"p75",
|
||||
"p95"
|
||||
],
|
||||
"properties": {
|
||||
"p25": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
},
|
||||
"p50": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
},
|
||||
"p75": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
},
|
||||
"p95": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"smart_contract": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"p25",
|
||||
"p50",
|
||||
"p75",
|
||||
"p95"
|
||||
],
|
||||
"properties": {
|
||||
"p25": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
},
|
||||
"p50": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
},
|
||||
"p75": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
},
|
||||
"p95": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"contract_call": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"p25",
|
||||
"p50",
|
||||
"p75",
|
||||
"p95"
|
||||
],
|
||||
"properties": {
|
||||
"p25": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
},
|
||||
"p50": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
},
|
||||
"p75": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
},
|
||||
"p95": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"poison_microblock": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"p25",
|
||||
"p50",
|
||||
"p75",
|
||||
"p95"
|
||||
],
|
||||
"properties": {
|
||||
"p25": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
},
|
||||
"p50": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
},
|
||||
"p75": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
},
|
||||
"p95": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tx_byte_sizes": {
|
||||
"type": "object",
|
||||
"description": "The average byte size of transactions in the mempool, broken down by transaction type.",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"token_transfer",
|
||||
"smart_contract",
|
||||
"contract_call",
|
||||
"poison_microblock"
|
||||
],
|
||||
"properties": {
|
||||
"token_transfer": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"p25",
|
||||
"p50",
|
||||
"p75",
|
||||
"p95"
|
||||
],
|
||||
"properties": {
|
||||
"p25": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
},
|
||||
"p50": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
},
|
||||
"p75": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
},
|
||||
"p95": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"smart_contract": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"p25",
|
||||
"p50",
|
||||
"p75",
|
||||
"p95"
|
||||
],
|
||||
"properties": {
|
||||
"p25": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
},
|
||||
"p50": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
},
|
||||
"p75": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
},
|
||||
"p95": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"contract_call": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"p25",
|
||||
"p50",
|
||||
"p75",
|
||||
"p95"
|
||||
],
|
||||
"properties": {
|
||||
"p25": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
},
|
||||
"p50": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
},
|
||||
"p75": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
},
|
||||
"p95": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"poison_microblock": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"p25",
|
||||
"p50",
|
||||
"p75",
|
||||
"p95"
|
||||
],
|
||||
"properties": {
|
||||
"p25": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
},
|
||||
"p50": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
},
|
||||
"p75": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
},
|
||||
"p95": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
102
docs/generated.d.ts
vendored
102
docs/generated.d.ts
vendored
@@ -99,6 +99,7 @@ export type SchemaMergeRootStub =
|
||||
| NonFungibleTokenHoldingsList
|
||||
| NonFungibleTokenMintList
|
||||
| NonFungibleTokensMetadataList
|
||||
| MempoolTransactionStatsResponse
|
||||
| MempoolTransactionListResponse
|
||||
| GetRawTransactionResult
|
||||
| TransactionEventsResponse
|
||||
@@ -3095,6 +3096,107 @@ export interface NonFungibleTokenMetadata {
|
||||
*/
|
||||
sender_address: string;
|
||||
}
|
||||
/**
|
||||
* GET request that returns stats on mempool transactions
|
||||
*/
|
||||
export interface MempoolTransactionStatsResponse {
|
||||
/**
|
||||
* Number of tranasction in the mempool, broken down by transaction type.
|
||||
*/
|
||||
tx_type_counts: {
|
||||
token_transfer: number;
|
||||
smart_contract: number;
|
||||
contract_call: number;
|
||||
poison_microblock: number;
|
||||
};
|
||||
/**
|
||||
* The simple mean (average) transaction fee, broken down by transaction type. Note that this does not factor in actual execution costs. The average fee is not a reliable metric for calculating a fee for a new transaction.
|
||||
*/
|
||||
tx_simple_fee_averages: {
|
||||
token_transfer: {
|
||||
p25: number;
|
||||
p50: number;
|
||||
p75: number;
|
||||
p95: number;
|
||||
};
|
||||
smart_contract: {
|
||||
p25: number;
|
||||
p50: number;
|
||||
p75: number;
|
||||
p95: number;
|
||||
};
|
||||
contract_call: {
|
||||
p25: number;
|
||||
p50: number;
|
||||
p75: number;
|
||||
p95: number;
|
||||
};
|
||||
poison_microblock: {
|
||||
p25: number;
|
||||
p50: number;
|
||||
p75: number;
|
||||
p95: number;
|
||||
};
|
||||
};
|
||||
/**
|
||||
* The average time (in blocks) that transactions have lived in the mempool. The start block height is simply the current chain-tip of when the attached Stacks node receives the transaction. This timing can be different across Stacks nodes / API instances due to propagation timing differences in the p2p network.
|
||||
*/
|
||||
tx_ages: {
|
||||
token_transfer: {
|
||||
p25: number;
|
||||
p50: number;
|
||||
p75: number;
|
||||
p95: number;
|
||||
};
|
||||
smart_contract: {
|
||||
p25: number;
|
||||
p50: number;
|
||||
p75: number;
|
||||
p95: number;
|
||||
};
|
||||
contract_call: {
|
||||
p25: number;
|
||||
p50: number;
|
||||
p75: number;
|
||||
p95: number;
|
||||
};
|
||||
poison_microblock: {
|
||||
p25: number;
|
||||
p50: number;
|
||||
p75: number;
|
||||
p95: number;
|
||||
};
|
||||
};
|
||||
/**
|
||||
* The average byte size of transactions in the mempool, broken down by transaction type.
|
||||
*/
|
||||
tx_byte_sizes: {
|
||||
token_transfer: {
|
||||
p25: number;
|
||||
p50: number;
|
||||
p75: number;
|
||||
p95: number;
|
||||
};
|
||||
smart_contract: {
|
||||
p25: number;
|
||||
p50: number;
|
||||
p75: number;
|
||||
p95: number;
|
||||
};
|
||||
contract_call: {
|
||||
p25: number;
|
||||
p50: number;
|
||||
p75: number;
|
||||
p95: number;
|
||||
};
|
||||
poison_microblock: {
|
||||
p25: number;
|
||||
p50: number;
|
||||
p75: number;
|
||||
p95: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
/**
|
||||
* GET request that returns transactions
|
||||
*/
|
||||
|
||||
@@ -326,6 +326,25 @@ paths:
|
||||
example:
|
||||
$ref: ./api/transaction/get-mempool-transactions.example.json
|
||||
|
||||
/extended/v1/tx/mempool/stats:
|
||||
get:
|
||||
summary: Get statistics for mempool transactions
|
||||
tags:
|
||||
- Transactions
|
||||
operationId: get_mempool_transaction_stats
|
||||
description: |
|
||||
Queries for transactions counts, age (by block height), fees (simple average), and size.
|
||||
All results broken down by transaction type and percentiles (p25, p50, p75, p95).
|
||||
responses:
|
||||
200:
|
||||
description: Statistics for mempool transactions
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: ./api/transaction/get-mempool-stats.schema.json
|
||||
example:
|
||||
$ref: ./api/transaction/get-mempool-stats.example.json
|
||||
|
||||
/extended/v1/tx/multiple:
|
||||
parameters:
|
||||
- name: tx_id
|
||||
|
||||
@@ -138,7 +138,7 @@ export function createTxRouter(db: PgStore): express.Router {
|
||||
'/mempool',
|
||||
mempoolCacheHandler,
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const limit = parseTxQueryLimit(req.query.limit ?? 96);
|
||||
const limit = parseMempoolTxQueryLimit(req.query.limit ?? 96);
|
||||
const offset = parsePagingQueryInput(req.query.offset ?? 0);
|
||||
|
||||
let addrParams: (string | undefined)[];
|
||||
@@ -212,6 +212,16 @@ export function createTxRouter(db: PgStore): express.Router {
|
||||
})
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/mempool/stats',
|
||||
mempoolCacheHandler,
|
||||
asyncHandler(async (req, res) => {
|
||||
const queryResult = await db.getMempoolStats({ lastBlockCount: undefined });
|
||||
setETagCacheHeaders(res, ETagType.mempool);
|
||||
res.json(queryResult);
|
||||
})
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/events',
|
||||
cacheHandler,
|
||||
|
||||
@@ -184,6 +184,37 @@ export interface DbTx extends BaseTx {
|
||||
execution_cost_write_length: number;
|
||||
}
|
||||
|
||||
export interface DbMempoolStats {
|
||||
tx_type_counts: Record<string, number>;
|
||||
tx_simple_fee_averages: Record<
|
||||
string,
|
||||
{
|
||||
p25: number | null;
|
||||
p50: number | null;
|
||||
p75: number | null;
|
||||
p95: number | null;
|
||||
}
|
||||
>;
|
||||
tx_ages: Record<
|
||||
string,
|
||||
{
|
||||
p25: number | null;
|
||||
p50: number | null;
|
||||
p75: number | null;
|
||||
p95: number | null;
|
||||
}
|
||||
>;
|
||||
tx_byte_sizes: Record<
|
||||
string,
|
||||
{
|
||||
p25: number | null;
|
||||
p50: number | null;
|
||||
p75: number | null;
|
||||
p95: number | null;
|
||||
}
|
||||
>;
|
||||
}
|
||||
|
||||
export interface DbMempoolTx extends BaseTx {
|
||||
pruned: boolean;
|
||||
raw_tx: string;
|
||||
|
||||
@@ -21,13 +21,35 @@ const PG_TYPE_MAPPINGS = {
|
||||
bytea: {
|
||||
to: 17,
|
||||
from: [17],
|
||||
serialize: (x: any) =>
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
x instanceof Uint8Array
|
||||
? `\\x${Buffer.from(x, x.byteOffset, x.byteLength).toString('hex')}`
|
||||
: typeof x === 'string' && has0xPrefix(x)
|
||||
? `\\x${x.slice(2)}`
|
||||
: x,
|
||||
serialize: (x: any) => {
|
||||
if (typeof x === 'string') {
|
||||
if (/^(0x|0X)[a-fA-F0-9]*$/.test(x)) {
|
||||
// hex string with "0x" prefix
|
||||
if (x.length % 2 !== 0) {
|
||||
throw new Error(`Hex string is an odd number of digits: "${x}"`);
|
||||
}
|
||||
return '\\x' + x.slice(2);
|
||||
} else if (x.length === 0) {
|
||||
return '\\x';
|
||||
} else if (/^\\x[a-fA-F0-9]*$/.test(x)) {
|
||||
// hex string with "\x" prefix (already encoded for postgres)
|
||||
if (x.length % 2 !== 0) {
|
||||
throw new Error(`Hex string is an odd number of digits: "${x}"`);
|
||||
}
|
||||
return x;
|
||||
} else {
|
||||
throw new Error(`String value for bytea column does not have 0x prefix: "${x}"`);
|
||||
}
|
||||
} else if (Buffer.isBuffer(x)) {
|
||||
return '\\x' + x.toString('hex');
|
||||
} else if (ArrayBuffer.isView(x)) {
|
||||
return '\\x' + Buffer.from(x.buffer, x.byteOffset, x.byteLength).toString('hex');
|
||||
} else {
|
||||
throw new Error(
|
||||
`Cannot serialize unexpected type "${x.constructor.name}" to bytea hex string`
|
||||
);
|
||||
}
|
||||
},
|
||||
parse: (x: any) => `0x${x.slice(2)}`,
|
||||
},
|
||||
};
|
||||
@@ -69,7 +91,7 @@ export async function connectPostgres({
|
||||
const timeElapsed = initTimer.getElapsed();
|
||||
if (timeElapsed - lastElapsedLog > 2000) {
|
||||
lastElapsedLog = timeElapsed;
|
||||
logError('Pg connection failed, retrying..');
|
||||
logError(`Pg connection failed: ${error}, retrying..`);
|
||||
}
|
||||
connectionError = error;
|
||||
await timeout(100);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { hexToBuffer, parseEnum } from '../helpers';
|
||||
import { hexToBuffer, logError, parseEnum } from '../helpers';
|
||||
import {
|
||||
BlockQueryResult,
|
||||
ContractTxQueryResult,
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
DbFaucetRequest,
|
||||
DbFaucetRequestCurrency,
|
||||
DbFtEvent,
|
||||
DbMempoolStats,
|
||||
DbMempoolTx,
|
||||
DbMicroblock,
|
||||
DbNftEvent,
|
||||
@@ -38,9 +39,11 @@ import {
|
||||
} from 'stacks-encoding-native-js';
|
||||
import { getTxSenderAddress } from '../event-stream/reader';
|
||||
import postgres = require('postgres');
|
||||
import * as prom from 'prom-client';
|
||||
import { PgSqlClient } from './connection';
|
||||
import { NftEvent } from 'docs/generated';
|
||||
import { getAssetEventTypeString } from '../api/controllers/db-controller';
|
||||
import { PgStoreEventEmitter } from './pg-store-event-emitter';
|
||||
|
||||
export const TX_COLUMNS = [
|
||||
'tx_id',
|
||||
@@ -799,3 +802,59 @@ export function createDbTxFromCoreMsg(msg: CoreNodeParsedTxMessage): DbTx {
|
||||
extractTransactionPayload(parsedTx, dbTx);
|
||||
return dbTx;
|
||||
}
|
||||
|
||||
export function registerMempoolPromStats(pgEvents: PgStoreEventEmitter) {
|
||||
const mempoolTxCountGauge = new prom.Gauge({
|
||||
name: `mempool_tx_count`,
|
||||
help: 'Number of txs in the mempool, by tx type',
|
||||
labelNames: ['type'] as const,
|
||||
});
|
||||
const mempoolTxFeeAvgGauge = new prom.Gauge({
|
||||
name: `mempool_tx_fee_average`,
|
||||
help: 'Simple average of tx fees in the mempool, by tx type',
|
||||
labelNames: ['type', 'percentile'] as const,
|
||||
});
|
||||
const mempoolTxAgeGauge = new prom.Gauge({
|
||||
name: `mempool_tx_age`,
|
||||
help: 'Average age (by block) of txs in the mempool, by tx type',
|
||||
labelNames: ['type', 'percentile'] as const,
|
||||
});
|
||||
const mempoolTxSizeGauge = new prom.Gauge({
|
||||
name: `mempool_tx_byte_size`,
|
||||
help: 'Average byte size of txs in the mempool, by tx type',
|
||||
labelNames: ['type', 'percentile'] as const,
|
||||
});
|
||||
const updatePromMempoolStats = (mempoolStats: DbMempoolStats) => {
|
||||
for (const txType in mempoolStats.tx_type_counts) {
|
||||
const entry = mempoolStats.tx_type_counts[txType];
|
||||
mempoolTxCountGauge.set({ type: txType }, entry);
|
||||
}
|
||||
for (const txType in mempoolStats.tx_simple_fee_averages) {
|
||||
const entries = mempoolStats.tx_simple_fee_averages[txType];
|
||||
Object.entries(entries).forEach(([p, num]) => {
|
||||
mempoolTxFeeAvgGauge.set({ type: txType, percentile: p }, num ?? -1);
|
||||
});
|
||||
}
|
||||
for (const txType in mempoolStats.tx_ages) {
|
||||
const entries = mempoolStats.tx_ages[txType];
|
||||
Object.entries(entries).forEach(([p, num]) => {
|
||||
mempoolTxAgeGauge.set({ type: txType, percentile: p }, num ?? -1);
|
||||
});
|
||||
}
|
||||
for (const txType in mempoolStats.tx_byte_sizes) {
|
||||
const entries = mempoolStats.tx_byte_sizes[txType];
|
||||
Object.entries(entries).forEach(([p, num]) => {
|
||||
mempoolTxSizeGauge.set({ type: txType, percentile: p }, num ?? -1);
|
||||
});
|
||||
}
|
||||
};
|
||||
pgEvents.addListener('mempoolStatsUpdate', mempoolStats => {
|
||||
setImmediate(() => {
|
||||
try {
|
||||
updatePromMempoolStats(mempoolStats);
|
||||
} catch (error) {
|
||||
logError(`Error updating prometheus mempool stats`, error);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { EventEmitter } from 'events';
|
||||
import StrictEventEmitter from 'strict-event-emitter-types';
|
||||
import { DbMempoolStats } from './common';
|
||||
|
||||
type DataStoreEventEmitter = StrictEventEmitter<
|
||||
EventEmitter,
|
||||
@@ -12,6 +13,7 @@ type DataStoreEventEmitter = StrictEventEmitter<
|
||||
nameUpdate: (info: string) => void;
|
||||
tokensUpdate: (contractID: string) => void;
|
||||
tokenMetadataUpdateQueued: (queueId: number) => void;
|
||||
mempoolStatsUpdate: (mempoolStats: DbMempoolStats) => void;
|
||||
}
|
||||
>;
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
TransactionType,
|
||||
} from '@stacks/stacks-blockchain-api-types';
|
||||
import { ChainID, ClarityAbi } from '@stacks/transactions';
|
||||
import { getTxTypeId } from '../api/controllers/db-controller';
|
||||
import { getTxTypeId, getTxTypeString } from '../api/controllers/db-controller';
|
||||
import {
|
||||
assertNotNullish,
|
||||
FoundOrNot,
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
DbGetBlockWithMetadataOpts,
|
||||
DbGetBlockWithMetadataResponse,
|
||||
DbInboundStxTransfer,
|
||||
DbMempoolStats,
|
||||
DbMempoolTx,
|
||||
DbMicroblock,
|
||||
DbMinerReward,
|
||||
@@ -1056,6 +1057,169 @@ export class PgStore {
|
||||
});
|
||||
}
|
||||
|
||||
async getMempoolStats({ lastBlockCount }: { lastBlockCount?: number }): Promise<DbMempoolStats> {
|
||||
return await this.sql.begin(async sql => {
|
||||
return await this.getMempoolStatsInternal({ sql, lastBlockCount });
|
||||
});
|
||||
}
|
||||
|
||||
async getMempoolStatsInternal({
|
||||
sql,
|
||||
lastBlockCount,
|
||||
}: {
|
||||
sql: PgSqlClient;
|
||||
lastBlockCount?: number;
|
||||
}): Promise<DbMempoolStats> {
|
||||
let blockHeightCondition = sql``;
|
||||
const chainTipHeight = await this.getMaxBlockHeight(sql, { includeUnanchored: true });
|
||||
if (lastBlockCount) {
|
||||
const maxBlockHeight = chainTipHeight - lastBlockCount;
|
||||
blockHeightCondition = sql` AND receipt_block_height >= ${maxBlockHeight} `;
|
||||
}
|
||||
|
||||
const txTypes = [
|
||||
DbTxTypeId.TokenTransfer,
|
||||
DbTxTypeId.SmartContract,
|
||||
DbTxTypeId.ContractCall,
|
||||
DbTxTypeId.PoisonMicroblock,
|
||||
];
|
||||
|
||||
const txTypeCountsQuery = await sql<{ type_id: DbTxTypeId; count: number }[]>`
|
||||
SELECT
|
||||
type_id,
|
||||
count(*)::integer count
|
||||
FROM mempool_txs
|
||||
WHERE pruned = false
|
||||
${blockHeightCondition}
|
||||
GROUP BY type_id
|
||||
`;
|
||||
const txTypeCounts: Record<string, number> = {};
|
||||
for (const typeId of txTypes) {
|
||||
const count = txTypeCountsQuery.find(r => r.type_id === typeId)?.count ?? 0;
|
||||
txTypeCounts[getTxTypeString(typeId)] = count;
|
||||
}
|
||||
|
||||
const txFeesQuery = await sql<
|
||||
{ type_id: DbTxTypeId; p25: number; p50: number; p75: number; p95: number }[]
|
||||
>`
|
||||
SELECT
|
||||
type_id,
|
||||
percentile_cont(0.25) within group (order by fee_rate asc) as p25,
|
||||
percentile_cont(0.50) within group (order by fee_rate asc) as p50,
|
||||
percentile_cont(0.75) within group (order by fee_rate asc) as p75,
|
||||
percentile_cont(0.95) within group (order by fee_rate asc) as p95
|
||||
FROM mempool_txs
|
||||
WHERE pruned = false
|
||||
${blockHeightCondition}
|
||||
GROUP BY type_id
|
||||
`;
|
||||
const txFees: Record<
|
||||
string,
|
||||
{ p25: number | null; p50: number | null; p75: number | null; p95: number | null }
|
||||
> = {};
|
||||
for (const typeId of txTypes) {
|
||||
const percentiles = txFeesQuery.find(r => r.type_id === typeId);
|
||||
txFees[getTxTypeString(typeId)] = {
|
||||
p25: percentiles?.p25 ?? null,
|
||||
p50: percentiles?.p50 ?? null,
|
||||
p75: percentiles?.p75 ?? null,
|
||||
p95: percentiles?.p95 ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
const txAgesQuery = await sql<
|
||||
{
|
||||
type_id: DbTxTypeId;
|
||||
p25: number;
|
||||
p50: number;
|
||||
p75: number;
|
||||
p95: number;
|
||||
}[]
|
||||
>`
|
||||
WITH mempool_unpruned AS (
|
||||
SELECT
|
||||
type_id,
|
||||
receipt_block_height
|
||||
FROM mempool_txs
|
||||
WHERE pruned = false
|
||||
${blockHeightCondition}
|
||||
),
|
||||
mempool_ages AS (
|
||||
SELECT
|
||||
type_id,
|
||||
${chainTipHeight} - receipt_block_height as age
|
||||
FROM mempool_unpruned
|
||||
)
|
||||
SELECT
|
||||
type_id,
|
||||
percentile_cont(0.25) within group (order by age asc) as p25,
|
||||
percentile_cont(0.50) within group (order by age asc) as p50,
|
||||
percentile_cont(0.75) within group (order by age asc) as p75,
|
||||
percentile_cont(0.95) within group (order by age asc) as p95
|
||||
FROM mempool_ages
|
||||
GROUP BY type_id
|
||||
`;
|
||||
const txAges: Record<
|
||||
string,
|
||||
{ p25: number | null; p50: number | null; p75: number | null; p95: number | null }
|
||||
> = {};
|
||||
for (const typeId of txTypes) {
|
||||
const percentiles = txAgesQuery.find(r => r.type_id === typeId);
|
||||
txAges[getTxTypeString(typeId)] = {
|
||||
p25: percentiles?.p25 ?? null,
|
||||
p50: percentiles?.p50 ?? null,
|
||||
p75: percentiles?.p75 ?? null,
|
||||
p95: percentiles?.p95 ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
const txSizesQuery = await sql<
|
||||
{
|
||||
type_id: DbTxTypeId;
|
||||
p25: number;
|
||||
p50: number;
|
||||
p75: number;
|
||||
p95: number;
|
||||
}[]
|
||||
>`
|
||||
WITH mempool_unpruned AS (
|
||||
SELECT
|
||||
type_id, tx_size
|
||||
FROM mempool_txs
|
||||
WHERE pruned = false
|
||||
${blockHeightCondition}
|
||||
)
|
||||
SELECT
|
||||
type_id,
|
||||
percentile_cont(0.25) within group (order by tx_size asc) as p25,
|
||||
percentile_cont(0.50) within group (order by tx_size asc) as p50,
|
||||
percentile_cont(0.75) within group (order by tx_size asc) as p75,
|
||||
percentile_cont(0.95) within group (order by tx_size asc) as p95
|
||||
FROM mempool_unpruned
|
||||
GROUP BY type_id
|
||||
`;
|
||||
const txSizes: Record<
|
||||
string,
|
||||
{ p25: number | null; p50: number | null; p75: number | null; p95: number | null }
|
||||
> = {};
|
||||
for (const typeId of txTypes) {
|
||||
const percentiles = txSizesQuery.find(r => r.type_id === typeId);
|
||||
txSizes[getTxTypeString(typeId)] = {
|
||||
p25: percentiles?.p25 ?? null,
|
||||
p50: percentiles?.p50 ?? null,
|
||||
p75: percentiles?.p75 ?? null,
|
||||
p95: percentiles?.p95 ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
tx_type_counts: txTypeCounts,
|
||||
tx_simple_fee_averages: txFees,
|
||||
tx_ages: txAges,
|
||||
tx_byte_sizes: txSizes,
|
||||
};
|
||||
}
|
||||
|
||||
async getMempoolTxList({
|
||||
limit,
|
||||
offset,
|
||||
|
||||
@@ -392,6 +392,11 @@ export class PgWriteStore extends PgStore {
|
||||
tokenMetadataQueueEntries.push(queueEntry);
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.isEventReplay) {
|
||||
const mempoolStats = await this.getMempoolStatsInternal({ sql });
|
||||
this.eventEmitter.emit('mempoolStatsUpdate', mempoolStats);
|
||||
}
|
||||
});
|
||||
|
||||
// Skip sending `PgNotifier` updates altogether if we're in the genesis block since this block is the
|
||||
@@ -644,6 +649,11 @@ export class PgWriteStore extends PgStore {
|
||||
await this.refreshNftCustody(sql, txs, true);
|
||||
await this.refreshMaterializedView(sql, 'chain_tip');
|
||||
|
||||
if (!this.isEventReplay) {
|
||||
const mempoolStats = await this.getMempoolStatsInternal({ sql });
|
||||
this.eventEmitter.emit('mempoolStatsUpdate', mempoolStats);
|
||||
}
|
||||
|
||||
if (this.notifier) {
|
||||
for (const microblock of dbMicroblocks) {
|
||||
await this.notifier.sendMicroblock({ microblockHash: microblock.microblock_hash });
|
||||
@@ -1210,6 +1220,11 @@ export class PgWriteStore extends PgStore {
|
||||
}
|
||||
}
|
||||
await this.refreshMaterializedView(sql, 'mempool_digest');
|
||||
|
||||
if (!this.isEventReplay) {
|
||||
const mempoolStats = await this.getMempoolStatsInternal({ sql });
|
||||
this.eventEmitter.emit('mempoolStatsUpdate', mempoolStats);
|
||||
}
|
||||
});
|
||||
for (const tx of updatedTxs) {
|
||||
await this.notifier?.sendTx({ txId: tx.tx_id });
|
||||
|
||||
@@ -27,6 +27,7 @@ import { PgStore } from './datastore/pg-store';
|
||||
import { PgWriteStore } from './datastore/pg-write-store';
|
||||
import { isFtMetadataEnabled, isNftMetadataEnabled } from './token-metadata/helpers';
|
||||
import { TokensProcessorQueue } from './token-metadata/tokens-processor-queue';
|
||||
import { registerMempoolPromStats } from './datastore/helpers';
|
||||
|
||||
enum StacksApiMode {
|
||||
/**
|
||||
@@ -122,6 +123,8 @@ async function init(): Promise<void> {
|
||||
skipMigrations: false,
|
||||
});
|
||||
|
||||
registerMempoolPromStats(dbWriteStore.eventEmitter);
|
||||
|
||||
if (apiMode === StacksApiMode.default || apiMode === StacksApiMode.writeOnly) {
|
||||
if (isProdEnv) {
|
||||
await importV1TokenOfferingData(dbWriteStore);
|
||||
|
||||
16
src/migrations/1659435823368_mempool_indexes.ts
Normal file
16
src/migrations/1659435823368_mempool_indexes.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { MigrationBuilder, ColumnDefinitions } from 'node-pg-migrate';
|
||||
|
||||
export async function up(pgm: MigrationBuilder): Promise<void> {
|
||||
pgm.addColumn('mempool_txs', {
|
||||
tx_size: {
|
||||
type: 'integer',
|
||||
notNull: true,
|
||||
expressionGenerated: 'length(raw_tx)'
|
||||
}
|
||||
});
|
||||
|
||||
pgm.createIndex('mempool_txs', ['type_id', 'receipt_block_height'], { where: 'pruned = false'});
|
||||
pgm.createIndex('mempool_txs', ['type_id', 'fee_rate'], { where: 'pruned = false'});
|
||||
pgm.createIndex('mempool_txs', ['type_id', 'tx_size'], { where: 'pruned = false'});
|
||||
|
||||
}
|
||||
@@ -207,7 +207,7 @@ function testTx(args?: TestTxArgs): DataStoreTxEventData {
|
||||
sponsor_address: undefined,
|
||||
sender_address: args?.sender_address ?? SENDER_ADDRESS,
|
||||
origin_hash_mode: 1,
|
||||
coinbase_payload: 'hi',
|
||||
coinbase_payload: bufferToHexPrefixString(Buffer.from('hi')),
|
||||
event_count: 0,
|
||||
parent_index_block_hash: args?.parent_index_block_hash ?? INDEX_BLOCK_HASH,
|
||||
parent_block_hash: BLOCK_HASH,
|
||||
@@ -252,6 +252,8 @@ interface TestMempoolTxArgs {
|
||||
tx_id?: string;
|
||||
type_id?: DbTxTypeId;
|
||||
nonce?: number;
|
||||
fee_rate?: bigint;
|
||||
raw_tx?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -265,12 +267,12 @@ export function testMempoolTx(args?: TestMempoolTxArgs): DbMempoolTx {
|
||||
tx_id: args?.tx_id ?? TX_ID,
|
||||
anchor_mode: 3,
|
||||
nonce: args?.nonce ?? 0,
|
||||
raw_tx: 'test-raw-tx',
|
||||
raw_tx: args?.raw_tx ?? '0x01234567',
|
||||
type_id: args?.type_id ?? DbTxTypeId.TokenTransfer,
|
||||
receipt_time: (new Date().getTime() / 1000) | 0,
|
||||
status: args?.status ?? DbTxStatus.Pending,
|
||||
post_conditions: '0x01f5',
|
||||
fee_rate: 1234n,
|
||||
fee_rate: args?.fee_rate ?? 1234n,
|
||||
sponsored: false,
|
||||
sponsor_address: undefined,
|
||||
origin_hash_mode: 1,
|
||||
|
||||
@@ -140,7 +140,7 @@ describe('api tests', () => {
|
||||
burn_block_time: dbBlock1.burn_block_time,
|
||||
parent_burn_block_time: 0,
|
||||
type_id: DbTxTypeId.Coinbase,
|
||||
coinbase_payload: 'coinbase hi',
|
||||
coinbase_payload: bufferToHexPrefixString(Buffer.from('coinbase hi')),
|
||||
status: 1,
|
||||
raw_result: '0x0100000000000000000000000000000001', // u1
|
||||
canonical: true,
|
||||
@@ -740,11 +740,11 @@ describe('api tests', () => {
|
||||
tx_id: '0x8912000000000000000000000000000000000000000000000000000000000000',
|
||||
anchor_mode: 3,
|
||||
nonce: 0,
|
||||
raw_tx: 'test-raw-tx',
|
||||
raw_tx: bufferToHexPrefixString(Buffer.from('test-raw-tx')),
|
||||
type_id: DbTxTypeId.Coinbase,
|
||||
status: DbTxStatus.Pending,
|
||||
receipt_time: 1594307695,
|
||||
coinbase_payload: 'coinbase hi',
|
||||
coinbase_payload: bufferToHexPrefixString(Buffer.from('coinbase hi')),
|
||||
post_conditions: '0x01f5',
|
||||
fee_rate: 1234n,
|
||||
sponsored: false,
|
||||
@@ -822,9 +822,9 @@ describe('api tests', () => {
|
||||
tx_id: '0x8915000000000000000000000000000000000000000000000000000000000000',
|
||||
anchor_mode: 3,
|
||||
nonce: 1000,
|
||||
raw_tx: 'test-raw-tx',
|
||||
raw_tx: bufferToHexPrefixString(Buffer.from('test-raw-tx')),
|
||||
type_id: DbTxTypeId.Coinbase,
|
||||
coinbase_payload: 'coinbase hi',
|
||||
coinbase_payload: bufferToHexPrefixString(Buffer.from('coinbase hi')),
|
||||
post_conditions: '0x01f5',
|
||||
fee_rate: 1234n,
|
||||
sponsored: true,
|
||||
@@ -903,11 +903,11 @@ describe('api tests', () => {
|
||||
tx_id: '0x8912000000000000000000000000000000000000000000000000000000000000',
|
||||
anchor_mode: 3,
|
||||
nonce: 0,
|
||||
raw_tx: 'test-raw-tx',
|
||||
raw_tx: bufferToHexPrefixString(Buffer.from('test-raw-tx')),
|
||||
type_id: DbTxTypeId.Coinbase,
|
||||
status: DbTxStatus.Pending,
|
||||
receipt_time: 1594307695,
|
||||
coinbase_payload: 'coinbase hi',
|
||||
coinbase_payload: bufferToHexPrefixString(Buffer.from('coinbase hi')),
|
||||
post_conditions: '0x01f5',
|
||||
fee_rate: 1234n,
|
||||
sponsored: false,
|
||||
@@ -947,11 +947,11 @@ describe('api tests', () => {
|
||||
tx_id: '0x8912000000000000000000000000000000000000000000000000000000000000',
|
||||
anchor_mode: 3,
|
||||
nonce: 0,
|
||||
raw_tx: 'test-raw-tx',
|
||||
raw_tx: bufferToHexPrefixString(Buffer.from('test-raw-tx')),
|
||||
type_id: DbTxTypeId.Coinbase,
|
||||
status: DbTxStatus.Pending,
|
||||
receipt_time: 1594307695,
|
||||
coinbase_payload: 'coinbase hi',
|
||||
coinbase_payload: bufferToHexPrefixString(Buffer.from('coinbase hi')),
|
||||
post_conditions: '0x01f5',
|
||||
fee_rate: 1234n,
|
||||
sponsored: true,
|
||||
@@ -992,11 +992,11 @@ describe('api tests', () => {
|
||||
tx_id: '0x8912000000000000000000000000000000000000000000000000000000000000',
|
||||
anchor_mode: 3,
|
||||
nonce: 0,
|
||||
raw_tx: 'test-raw-tx',
|
||||
raw_tx: bufferToHexPrefixString(Buffer.from('test-raw-tx')),
|
||||
type_id: DbTxTypeId.Coinbase,
|
||||
status: DbTxStatus.Pending,
|
||||
receipt_time: 1594307695,
|
||||
coinbase_payload: 'coinbase hi',
|
||||
coinbase_payload: bufferToHexPrefixString(Buffer.from('coinbase hi')),
|
||||
post_conditions: '0x01f5',
|
||||
fee_rate: 1234n,
|
||||
sponsored: true,
|
||||
@@ -1280,10 +1280,10 @@ describe('api tests', () => {
|
||||
tx_id: `0x891200000000000000000000000000000000000000000000000000000000000${i}`,
|
||||
anchor_mode: 3,
|
||||
nonce: 0,
|
||||
raw_tx: 'test-raw-tx',
|
||||
raw_tx: bufferToHexPrefixString(Buffer.from('test-raw-tx')),
|
||||
type_id: DbTxTypeId.Coinbase,
|
||||
receipt_time: (new Date(`2020-07-09T15:14:0${i}Z`).getTime() / 1000) | 0,
|
||||
coinbase_payload: 'coinbase hi',
|
||||
coinbase_payload: bufferToHexPrefixString(Buffer.from('coinbase hi')),
|
||||
status: 1,
|
||||
post_conditions: '0x01f5',
|
||||
fee_rate: 1234n,
|
||||
@@ -1419,7 +1419,7 @@ describe('api tests', () => {
|
||||
tx_id: `0x89120000000000000000000000000000000000000000000000000000000000${paddedIndex}`,
|
||||
anchor_mode: 3,
|
||||
nonce: 0,
|
||||
raw_tx: 'test-raw-tx',
|
||||
raw_tx: bufferToHexPrefixString(Buffer.from('test-raw-tx')),
|
||||
type_id: xfer.type_id,
|
||||
receipt_time: (new Date(`2020-07-09T15:14:${paddedIndex}Z`).getTime() / 1000) | 0,
|
||||
status: 1,
|
||||
@@ -1849,7 +1849,7 @@ describe('api tests', () => {
|
||||
burn_block_time: 2837565,
|
||||
parent_burn_block_time: 1626122935,
|
||||
type_id: DbTxTypeId.Coinbase,
|
||||
coinbase_payload: 'coinbase hi',
|
||||
coinbase_payload: bufferToHexPrefixString(Buffer.from('coinbase hi')),
|
||||
status: 1,
|
||||
raw_result: '0x0100000000000000000000000000000001', // u1
|
||||
canonical: true,
|
||||
@@ -1878,10 +1878,10 @@ describe('api tests', () => {
|
||||
tx_id: '0x8912000000000000000000000000000000000000000000000000000000000000',
|
||||
anchor_mode: 3,
|
||||
nonce: 0,
|
||||
raw_tx: 'test-raw-tx',
|
||||
raw_tx: bufferToHexPrefixString(Buffer.from('test-raw-tx')),
|
||||
type_id: DbTxTypeId.Coinbase,
|
||||
receipt_time: 123456,
|
||||
coinbase_payload: 'coinbase hi',
|
||||
coinbase_payload: bufferToHexPrefixString(Buffer.from('coinbase hi')),
|
||||
status: 1,
|
||||
post_conditions: '0x01f5',
|
||||
fee_rate: 1234n,
|
||||
@@ -2261,7 +2261,7 @@ describe('api tests', () => {
|
||||
burn_block_time: 2837565,
|
||||
parent_burn_block_time: 1626122935,
|
||||
type_id: DbTxTypeId.Coinbase,
|
||||
coinbase_payload: 'coinbase hi',
|
||||
coinbase_payload: bufferToHexPrefixString(Buffer.from('coinbase hi')),
|
||||
status: 1,
|
||||
raw_result: '0x0100000000000000000000000000000001', // u1
|
||||
canonical: true,
|
||||
@@ -2289,10 +2289,10 @@ describe('api tests', () => {
|
||||
tx_id: '0x8912000000000000000000000000000000000000000000000000000000000000',
|
||||
anchor_mode: 3,
|
||||
nonce: 0,
|
||||
raw_tx: 'test-raw-tx',
|
||||
raw_tx: bufferToHexPrefixString(Buffer.from('test-raw-tx')),
|
||||
type_id: DbTxTypeId.Coinbase,
|
||||
receipt_time: 123456,
|
||||
coinbase_payload: 'coinbase hi',
|
||||
coinbase_payload: bufferToHexPrefixString(Buffer.from('coinbase hi')),
|
||||
status: 1,
|
||||
post_conditions: '0x01f5',
|
||||
fee_rate: 1234n,
|
||||
@@ -2591,7 +2591,7 @@ describe('api tests', () => {
|
||||
parent_burn_block_time: 1626122935,
|
||||
type_id: DbTxTypeId.TokenTransfer,
|
||||
token_transfer_amount: 1n,
|
||||
token_transfer_memo: 'hi',
|
||||
token_transfer_memo: bufferToHexPrefixString(Buffer.from('hi')),
|
||||
token_transfer_recipient_address: 'none',
|
||||
status: 1,
|
||||
raw_result: '0x0100000000000000000000000000000001', // u1
|
||||
@@ -2642,7 +2642,7 @@ describe('api tests', () => {
|
||||
parent_burn_block_time: 1626122935,
|
||||
type_id: DbTxTypeId.TokenTransfer,
|
||||
token_transfer_amount: 1n,
|
||||
token_transfer_memo: 'hi',
|
||||
token_transfer_memo: bufferToHexPrefixString(Buffer.from('hi')),
|
||||
token_transfer_recipient_address: addr2,
|
||||
status: 1,
|
||||
raw_result: '0x0100000000000000000000000000000001', // u1
|
||||
@@ -2911,7 +2911,7 @@ describe('api tests', () => {
|
||||
tx_id: '0x1111882200000000000000000000000000000000000000000000000000000000',
|
||||
anchor_mode: 3,
|
||||
nonce: 0,
|
||||
raw_tx: 'test-raw-tx',
|
||||
raw_tx: bufferToHexPrefixString(Buffer.from('test-raw-tx')),
|
||||
receipt_time: 123456,
|
||||
smart_contract_contract_id: contractAddr2,
|
||||
smart_contract_source_code: '(some-src)',
|
||||
@@ -3026,7 +3026,7 @@ describe('api tests', () => {
|
||||
parent_burn_block_time: 1626122935,
|
||||
type_id: DbTxTypeId.TokenTransfer,
|
||||
token_transfer_amount: 1n,
|
||||
token_transfer_memo: 'hi',
|
||||
token_transfer_memo: bufferToHexPrefixString(Buffer.from('hi')),
|
||||
token_transfer_recipient_address: 'none',
|
||||
status: 1,
|
||||
raw_result: '0x0100000000000000000000000000000001', // u1
|
||||
@@ -3063,7 +3063,7 @@ describe('api tests', () => {
|
||||
parent_burn_block_time: 1626122935,
|
||||
type_id: DbTxTypeId.TokenTransfer,
|
||||
token_transfer_amount: 1n,
|
||||
token_transfer_memo: 'hi',
|
||||
token_transfer_memo: bufferToHexPrefixString(Buffer.from('hi')),
|
||||
token_transfer_recipient_address: addr2,
|
||||
status: 1,
|
||||
raw_result: '0x0100000000000000000000000000000001', // u1
|
||||
@@ -3509,7 +3509,7 @@ describe('api tests', () => {
|
||||
tx_id: '0x1111882200000000000000000000000000000000000000000000000000000000',
|
||||
anchor_mode: 3,
|
||||
nonce: 0,
|
||||
raw_tx: 'test-raw-tx',
|
||||
raw_tx: bufferToHexPrefixString(Buffer.from('test-raw-tx')),
|
||||
receipt_time: 123456,
|
||||
smart_contract_contract_id: contractAddr2,
|
||||
smart_contract_source_code: '(some-src)',
|
||||
@@ -3652,7 +3652,7 @@ describe('api tests', () => {
|
||||
parent_burn_block_time: 1626122935,
|
||||
type_id: DbTxTypeId.TokenTransfer,
|
||||
token_transfer_amount: BigInt(amount),
|
||||
token_transfer_memo: 'hi',
|
||||
token_transfer_memo: bufferToHexPrefixString(Buffer.from('hi')),
|
||||
token_transfer_recipient_address: recipient,
|
||||
status: 1,
|
||||
raw_result: '0x0100000000000000000000000000000001', // u1
|
||||
@@ -4420,7 +4420,7 @@ describe('api tests', () => {
|
||||
parent_burn_block_time: 1626122935,
|
||||
type_id: DbTxTypeId.TokenTransfer,
|
||||
token_transfer_amount: BigInt(amount),
|
||||
token_transfer_memo: 'hi',
|
||||
token_transfer_memo: bufferToHexPrefixString(Buffer.from('hi')),
|
||||
token_transfer_recipient_address: recipient,
|
||||
status: 1,
|
||||
raw_result: '0x0100000000000000000000000000000001', // u1
|
||||
@@ -4469,7 +4469,7 @@ describe('api tests', () => {
|
||||
burn_block_time: block.burn_block_time,
|
||||
parent_burn_block_time: 1626122935,
|
||||
type_id: DbTxTypeId.Coinbase,
|
||||
coinbase_payload: 'coinbase hi',
|
||||
coinbase_payload: bufferToHexPrefixString(Buffer.from('coinbase hi')),
|
||||
status: 1,
|
||||
raw_result: '0x0100000000000000000000000000000001', // u1
|
||||
canonical: true,
|
||||
@@ -5606,7 +5606,7 @@ describe('api tests', () => {
|
||||
sponsor_address: undefined,
|
||||
origin_hash_mode: 1,
|
||||
token_transfer_amount: 50n,
|
||||
token_transfer_memo: 'hi',
|
||||
token_transfer_memo: bufferToHexPrefixString(Buffer.from('hi')),
|
||||
token_transfer_recipient_address: contractId,
|
||||
event_count: 1,
|
||||
parent_index_block_hash: block2.block.index_block_hash,
|
||||
@@ -5703,7 +5703,7 @@ describe('api tests', () => {
|
||||
sponsor_address: undefined,
|
||||
sender_address: 'sender-addr',
|
||||
origin_hash_mode: 1,
|
||||
coinbase_payload: 'hi',
|
||||
coinbase_payload: bufferToHexPrefixString(Buffer.from('hi')),
|
||||
event_count: 0,
|
||||
execution_cost_read_count: 0,
|
||||
execution_cost_read_length: 0,
|
||||
@@ -5931,7 +5931,7 @@ describe('api tests', () => {
|
||||
sponsor_address: undefined,
|
||||
sender_address: 'sender-addr',
|
||||
origin_hash_mode: 1,
|
||||
coinbase_payload: 'hi',
|
||||
coinbase_payload: bufferToHexPrefixString(Buffer.from('hi')),
|
||||
event_count: 0,
|
||||
execution_cost_read_count: 0,
|
||||
execution_cost_read_length: 0,
|
||||
@@ -7237,7 +7237,7 @@ describe('api tests', () => {
|
||||
burn_block_time: 1594647995,
|
||||
parent_burn_block_time: 1626122935,
|
||||
type_id: DbTxTypeId.Coinbase,
|
||||
coinbase_payload: 'coinbase hi',
|
||||
coinbase_payload: bufferToHexPrefixString(Buffer.from('coinbase hi')),
|
||||
status: 1,
|
||||
raw_result: '0x0100000000000000000000000000000001', // u1
|
||||
canonical: true,
|
||||
@@ -7987,7 +7987,7 @@ describe('api tests', () => {
|
||||
tx_id: '0x521234',
|
||||
anchor_mode: 3,
|
||||
nonce: 1,
|
||||
raw_tx: 'test-raw-mempool-tx',
|
||||
raw_tx: bufferToHexPrefixString(Buffer.from('test-raw-mempool-tx')),
|
||||
type_id: DbTxTypeId.Coinbase,
|
||||
status: 1,
|
||||
post_conditions: '0x01f5',
|
||||
@@ -7997,7 +7997,7 @@ describe('api tests', () => {
|
||||
sender_address: senderAddress,
|
||||
sponsor_nonce: 3,
|
||||
origin_hash_mode: 1,
|
||||
coinbase_payload: 'hi',
|
||||
coinbase_payload: bufferToHexPrefixString(Buffer.from('hi')),
|
||||
pruned: false,
|
||||
receipt_time: 1616063078,
|
||||
};
|
||||
@@ -8039,7 +8039,7 @@ describe('api tests', () => {
|
||||
tx_id: '0x52123456',
|
||||
anchor_mode: 3,
|
||||
nonce: 1,
|
||||
raw_tx: 'test-raw-mempool-tx',
|
||||
raw_tx: bufferToHexPrefixString(Buffer.from('test-raw-mempool-tx')),
|
||||
type_id: DbTxTypeId.Coinbase,
|
||||
status: 1,
|
||||
post_conditions: '0x01f5',
|
||||
@@ -8049,7 +8049,7 @@ describe('api tests', () => {
|
||||
sender_address: senderAddress,
|
||||
sponsor_nonce: 6,
|
||||
origin_hash_mode: 1,
|
||||
coinbase_payload: 'hi',
|
||||
coinbase_payload: bufferToHexPrefixString(Buffer.from('hi')),
|
||||
pruned: false,
|
||||
receipt_time: 1616063078,
|
||||
};
|
||||
@@ -8617,7 +8617,7 @@ describe('api tests', () => {
|
||||
tx_index: 0,
|
||||
anchor_mode: 3,
|
||||
nonce: 0,
|
||||
raw_tx: 'test-raw-tx',
|
||||
raw_tx: bufferToHexPrefixString(Buffer.from('test-raw-tx')),
|
||||
index_block_hash: block.index_block_hash,
|
||||
block_hash: block.block_hash,
|
||||
block_height: block.block_height,
|
||||
@@ -8638,7 +8638,7 @@ describe('api tests', () => {
|
||||
sponsor_address: undefined,
|
||||
sender_address: 'sender-addr',
|
||||
origin_hash_mode: 1,
|
||||
coinbase_payload: 'hi',
|
||||
coinbase_payload: bufferToHexPrefixString(Buffer.from('hi')),
|
||||
event_count: 0,
|
||||
execution_cost_read_count: 0,
|
||||
execution_cost_read_length: 0,
|
||||
@@ -8670,7 +8670,7 @@ describe('api tests', () => {
|
||||
tx_id: '0x521234',
|
||||
anchor_mode: 3,
|
||||
nonce: 0,
|
||||
raw_tx: 'test-raw-mempool-tx',
|
||||
raw_tx: bufferToHexPrefixString(Buffer.from('test-raw-mempool-tx')),
|
||||
type_id: DbTxTypeId.Coinbase,
|
||||
status: 1,
|
||||
post_conditions: '0x01f5',
|
||||
@@ -8679,7 +8679,7 @@ describe('api tests', () => {
|
||||
sponsor_address: undefined,
|
||||
sender_address: 'sender-addr',
|
||||
origin_hash_mode: 1,
|
||||
coinbase_payload: 'hi',
|
||||
coinbase_payload: bufferToHexPrefixString(Buffer.from('hi')),
|
||||
pruned: false,
|
||||
receipt_time: 1616063078,
|
||||
};
|
||||
@@ -8730,7 +8730,7 @@ describe('api tests', () => {
|
||||
tx_index: 0,
|
||||
anchor_mode: 3,
|
||||
nonce: 0,
|
||||
raw_tx: 'test-raw-tx',
|
||||
raw_tx: bufferToHexPrefixString(Buffer.from('test-raw-tx')),
|
||||
index_block_hash: '0x1234',
|
||||
block_hash: block.block_hash,
|
||||
block_height: block.block_height,
|
||||
@@ -8751,7 +8751,7 @@ describe('api tests', () => {
|
||||
sponsor_address: undefined,
|
||||
sender_address: 'sender-addr',
|
||||
origin_hash_mode: 1,
|
||||
coinbase_payload: 'hi',
|
||||
coinbase_payload: bufferToHexPrefixString(Buffer.from('hi')),
|
||||
event_count: 0,
|
||||
execution_cost_read_count: 0,
|
||||
execution_cost_read_length: 0,
|
||||
@@ -8848,7 +8848,7 @@ describe('api tests', () => {
|
||||
parent_burn_block_time: 1626122935,
|
||||
type_id: DbTxTypeId.TokenTransfer,
|
||||
token_transfer_amount: 1n,
|
||||
token_transfer_memo: 'hi',
|
||||
token_transfer_memo: bufferToHexPrefixString(Buffer.from('hi')),
|
||||
token_transfer_recipient_address: 'none',
|
||||
status: 1,
|
||||
raw_result: '0x0100000000000000000000000000000001', // u1
|
||||
@@ -8956,7 +8956,7 @@ describe('api tests', () => {
|
||||
parent_burn_block_time: 1626124935,
|
||||
type_id: DbTxTypeId.TokenTransfer,
|
||||
token_transfer_amount: 1n,
|
||||
token_transfer_memo: 'hi',
|
||||
token_transfer_memo: bufferToHexPrefixString(Buffer.from('hi')),
|
||||
token_transfer_recipient_address: 'none',
|
||||
status: 1,
|
||||
raw_result: '0x0100000000000000000000000000000001', // u1
|
||||
@@ -9073,7 +9073,7 @@ describe('api tests', () => {
|
||||
burn_block_time: 1594647995,
|
||||
parent_burn_block_time: 1626122935,
|
||||
type_id: DbTxTypeId.Coinbase,
|
||||
coinbase_payload: 'coinbase hi',
|
||||
coinbase_payload: bufferToHexPrefixString(Buffer.from('coinbase hi')),
|
||||
status: 1,
|
||||
raw_result: '0x0100000000000000000000000000000001', // u1
|
||||
canonical: true,
|
||||
@@ -9212,7 +9212,7 @@ describe('api tests', () => {
|
||||
tx_id: '0x521234',
|
||||
anchor_mode: 3,
|
||||
nonce: 0,
|
||||
raw_tx: 'test-raw-mempool-tx',
|
||||
raw_tx: bufferToHexPrefixString(Buffer.from('test-raw-mempool-tx')),
|
||||
type_id: DbTxTypeId.Coinbase,
|
||||
status: 1,
|
||||
post_conditions: '0x01f5',
|
||||
@@ -9221,7 +9221,7 @@ describe('api tests', () => {
|
||||
sponsor_address: undefined,
|
||||
sender_address: senderAddress,
|
||||
origin_hash_mode: 1,
|
||||
coinbase_payload: 'hi',
|
||||
coinbase_payload: bufferToHexPrefixString(Buffer.from('hi')),
|
||||
pruned: false,
|
||||
receipt_time: 1616063078,
|
||||
};
|
||||
@@ -9239,7 +9239,7 @@ describe('api tests', () => {
|
||||
tx_id: '0x521234',
|
||||
anchor_mode: 3,
|
||||
nonce: 0,
|
||||
raw_tx: 'test-raw-mempool-tx',
|
||||
raw_tx: bufferToHexPrefixString(Buffer.from('test-raw-mempool-tx')),
|
||||
type_id: DbTxTypeId.Coinbase,
|
||||
status: 1,
|
||||
post_conditions: '0x01f5',
|
||||
@@ -9248,7 +9248,7 @@ describe('api tests', () => {
|
||||
sponsor_address: undefined,
|
||||
sender_address: senderAddress,
|
||||
origin_hash_mode: 1,
|
||||
coinbase_payload: 'hi',
|
||||
coinbase_payload: bufferToHexPrefixString(Buffer.from('hi')),
|
||||
pruned: false,
|
||||
receipt_time: 1616063078,
|
||||
};
|
||||
@@ -9284,7 +9284,7 @@ describe('api tests', () => {
|
||||
tx_id: '0x521234',
|
||||
anchor_mode: 3,
|
||||
nonce: 0,
|
||||
raw_tx: 'test-raw-mempool-tx',
|
||||
raw_tx: bufferToHexPrefixString(Buffer.from('test-raw-mempool-tx')),
|
||||
type_id: DbTxTypeId.Coinbase,
|
||||
status: 1,
|
||||
post_conditions: '0x01f5',
|
||||
@@ -9293,7 +9293,7 @@ describe('api tests', () => {
|
||||
sponsor_address: undefined,
|
||||
sender_address: senderAddress,
|
||||
origin_hash_mode: 1,
|
||||
coinbase_payload: 'hi',
|
||||
coinbase_payload: bufferToHexPrefixString(Buffer.from('hi')),
|
||||
pruned: false,
|
||||
receipt_time: 1616063078,
|
||||
};
|
||||
@@ -9366,7 +9366,7 @@ describe('api tests', () => {
|
||||
burn_block_time: block.burn_block_time,
|
||||
parent_burn_block_time: 1626122935,
|
||||
type_id: DbTxTypeId.Coinbase,
|
||||
coinbase_payload: 'coinbase hi',
|
||||
coinbase_payload: bufferToHexPrefixString(Buffer.from('coinbase hi')),
|
||||
status: 1,
|
||||
raw_result: '0x0100000000000000000000000000000001', // u1
|
||||
canonical: true,
|
||||
@@ -9455,7 +9455,7 @@ describe('api tests', () => {
|
||||
burn_block_time: block.burn_block_time,
|
||||
parent_burn_block_time: 1626122935,
|
||||
type_id: DbTxTypeId.Coinbase,
|
||||
coinbase_payload: 'coinbase hi',
|
||||
coinbase_payload: bufferToHexPrefixString(Buffer.from('coinbase hi')),
|
||||
status: 1,
|
||||
raw_result: '0x0100000000000000000000000000000001', // u1
|
||||
canonical: true,
|
||||
@@ -9678,9 +9678,9 @@ describe('api tests', () => {
|
||||
tx_id: '0x8912000000000000000000000000000000000000000000000000000000000000',
|
||||
anchor_mode: 3,
|
||||
nonce: 0,
|
||||
raw_tx: 'test-raw-tx',
|
||||
raw_tx: bufferToHexPrefixString(Buffer.from('test-raw-tx')),
|
||||
type_id: DbTxTypeId.Coinbase,
|
||||
coinbase_payload: 'coinbase hi',
|
||||
coinbase_payload: bufferToHexPrefixString(Buffer.from('coinbase hi')),
|
||||
post_conditions: '0x01f5',
|
||||
fee_rate: 1234n,
|
||||
sponsored: true,
|
||||
@@ -9708,9 +9708,9 @@ describe('api tests', () => {
|
||||
tx_id: '0x8912000000000000000000000000000000000000000000000000000000000001',
|
||||
anchor_mode: 3,
|
||||
nonce: 0,
|
||||
raw_tx: 'test-raw-tx',
|
||||
raw_tx: bufferToHexPrefixString(Buffer.from('test-raw-tx')),
|
||||
type_id: DbTxTypeId.Coinbase,
|
||||
coinbase_payload: 'coinbase hi',
|
||||
coinbase_payload: bufferToHexPrefixString(Buffer.from('coinbase hi')),
|
||||
post_conditions: '0x01f5',
|
||||
fee_rate: 1234n,
|
||||
sponsored: true,
|
||||
|
||||
@@ -4,7 +4,7 @@ import { getBlockFromDataStore } from '../api/controllers/db-controller';
|
||||
import { DbBlock, DbMicroblockPartial, DbTx, DbTxStatus, DbTxTypeId } from '../datastore/common';
|
||||
import { startApiServer, ApiServer } from '../api/init';
|
||||
import { PoolClient } from 'pg';
|
||||
import { I32_MAX } from '../helpers';
|
||||
import { bufferToHexPrefixString, I32_MAX } from '../helpers';
|
||||
import { parseIfNoneMatchHeader } from '../api/controllers/cache-controller';
|
||||
import { TestBlockBuilder, testMempoolTx } from '../test-utils/test-builders';
|
||||
import { PgWriteStore } from '../datastore/pg-write-store';
|
||||
@@ -104,7 +104,7 @@ describe('cache-control tests', () => {
|
||||
burn_block_time: 1594647995,
|
||||
parent_burn_block_time: 1626122935,
|
||||
type_id: DbTxTypeId.Coinbase,
|
||||
coinbase_payload: 'coinbase hi',
|
||||
coinbase_payload: bufferToHexPrefixString(Buffer.from('coinbase hi')),
|
||||
status: 1,
|
||||
raw_result: '0x0100000000000000000000000000000001', // u1
|
||||
canonical: true,
|
||||
@@ -248,7 +248,7 @@ describe('cache-control tests', () => {
|
||||
sponsor_address: undefined,
|
||||
origin_hash_mode: 1,
|
||||
token_transfer_amount: 50n,
|
||||
token_transfer_memo: 'hi',
|
||||
token_transfer_memo: bufferToHexPrefixString(Buffer.from('hi')),
|
||||
token_transfer_recipient_address: addr2,
|
||||
event_count: 1,
|
||||
parent_index_block_hash: block1.index_block_hash,
|
||||
|
||||
@@ -26,7 +26,7 @@ import * as assert from 'assert';
|
||||
import { PgWriteStore } from '../datastore/pg-write-store';
|
||||
import { cycleMigrations, runMigrations } from '../datastore/migrations';
|
||||
import { getPostgres, PgSqlClient } from '../datastore/connection';
|
||||
import { bnsNameCV, I32_MAX } from '../helpers';
|
||||
import { bnsNameCV, bufferToHexPrefixString, I32_MAX } from '../helpers';
|
||||
import { ChainID, intCV, serializeCV } from '@stacks/transactions';
|
||||
|
||||
function testEnvVars(
|
||||
@@ -82,6 +82,78 @@ describe('postgres datastore', () => {
|
||||
client = db.sql;
|
||||
});
|
||||
|
||||
test('bytea column serialization', async () => {
|
||||
const vectors = [
|
||||
{
|
||||
from: '0x0001',
|
||||
to: '0x0001',
|
||||
},
|
||||
{
|
||||
from: '0X0002',
|
||||
to: '0x0002',
|
||||
},
|
||||
{
|
||||
from: '0xFfF3',
|
||||
to: '0xfff3',
|
||||
},
|
||||
{
|
||||
from: Buffer.from('0004', 'hex'),
|
||||
to: '0x0004',
|
||||
},
|
||||
{
|
||||
from: new Uint16Array(new Uint8Array([0x00, 0x05]).buffer),
|
||||
to: '0x0005',
|
||||
},
|
||||
{
|
||||
from: '\\x0006',
|
||||
to: '0x0006',
|
||||
},
|
||||
{
|
||||
from: '\\xfFf7',
|
||||
to: '0xfff7',
|
||||
},
|
||||
{
|
||||
from: '\\x',
|
||||
to: '0x',
|
||||
},
|
||||
{
|
||||
from: '',
|
||||
to: '0x',
|
||||
},
|
||||
{
|
||||
from: Buffer.alloc(0),
|
||||
to: '0x',
|
||||
},
|
||||
];
|
||||
await db.sql.begin(async sql => {
|
||||
await sql`
|
||||
CREATE TEMPORARY TABLE bytea_testing(
|
||||
value bytea NOT NULL
|
||||
) ON COMMIT DROP
|
||||
`;
|
||||
for (const v of vectors) {
|
||||
const query = await sql<{ value: string }[]>`
|
||||
insert into bytea_testing (value) values (${v.from})
|
||||
returning value
|
||||
`;
|
||||
expect(query[0].value).toBe(v.to);
|
||||
}
|
||||
});
|
||||
const badInputs = ['0x123', '1234', '0xnoop', new Date(), 1234];
|
||||
for (const input of badInputs) {
|
||||
const query = async () =>
|
||||
db.sql.begin(async sql => {
|
||||
await sql`
|
||||
CREATE TEMPORARY TABLE bytea_testing(
|
||||
value bytea NOT NULL
|
||||
) ON COMMIT DROP
|
||||
`;
|
||||
return await sql`insert into bytea_testing (value) values (${input})`;
|
||||
});
|
||||
await expect(query()).rejects.toThrow();
|
||||
}
|
||||
});
|
||||
|
||||
test('postgres uri config', () => {
|
||||
const uri =
|
||||
'postgresql://test_user:secret_password@database.server.com:3211/test_db?ssl=true¤tSchema=test_schema&application_name=test-conn-str';
|
||||
@@ -825,7 +897,7 @@ describe('postgres datastore', () => {
|
||||
parent_burn_block_time: 1626122935,
|
||||
type_id: DbTxTypeId.TokenTransfer,
|
||||
token_transfer_amount: BigInt(amount),
|
||||
token_transfer_memo: 'hi',
|
||||
token_transfer_memo: bufferToHexPrefixString(Buffer.from('hi')),
|
||||
token_transfer_recipient_address: recipient,
|
||||
status: 1,
|
||||
raw_result: '0x0100000000000000000000000000000001', // u1
|
||||
@@ -3137,7 +3209,7 @@ describe('postgres datastore', () => {
|
||||
type_id: DbTxTypeId.TokenTransfer,
|
||||
receipt_time: 123456,
|
||||
token_transfer_amount: 1n,
|
||||
token_transfer_memo: 'hi',
|
||||
token_transfer_memo: bufferToHexPrefixString(Buffer.from('hi')),
|
||||
token_transfer_recipient_address: 'stx-recipient-addr',
|
||||
status: DbTxStatus.Pending,
|
||||
post_conditions: '0x',
|
||||
@@ -3406,7 +3478,7 @@ describe('postgres datastore', () => {
|
||||
sponsor_address: undefined,
|
||||
sender_address: 'sender-addr',
|
||||
origin_hash_mode: 1,
|
||||
coinbase_payload: 'hi',
|
||||
coinbase_payload: bufferToHexPrefixString(Buffer.from('hi')),
|
||||
event_count: 1,
|
||||
parent_index_block_hash: '0x00',
|
||||
parent_block_hash: '0x00',
|
||||
@@ -3441,7 +3513,7 @@ describe('postgres datastore', () => {
|
||||
sponsor_address: undefined,
|
||||
sender_address: 'sender-addr',
|
||||
origin_hash_mode: 1,
|
||||
coinbase_payload: 'hi',
|
||||
coinbase_payload: bufferToHexPrefixString(Buffer.from('hi')),
|
||||
event_count: 0,
|
||||
parent_index_block_hash: '0x00',
|
||||
parent_block_hash: '0x00',
|
||||
@@ -3631,7 +3703,7 @@ describe('postgres datastore', () => {
|
||||
sponsor_address: undefined,
|
||||
sender_address: 'sender-addr',
|
||||
origin_hash_mode: 1,
|
||||
coinbase_payload: 'hi',
|
||||
coinbase_payload: bufferToHexPrefixString(Buffer.from('hi')),
|
||||
event_count: 1,
|
||||
parent_index_block_hash: '0x00',
|
||||
parent_block_hash: '0x00',
|
||||
@@ -3666,7 +3738,7 @@ describe('postgres datastore', () => {
|
||||
sender_address: 'sender-addr',
|
||||
sponsor_address: undefined,
|
||||
origin_hash_mode: 1,
|
||||
coinbase_payload: 'hi',
|
||||
coinbase_payload: bufferToHexPrefixString(Buffer.from('hi')),
|
||||
event_count: 1,
|
||||
parent_index_block_hash: '0x00',
|
||||
parent_block_hash: '0x00',
|
||||
@@ -3888,7 +3960,7 @@ describe('postgres datastore', () => {
|
||||
sponsor_address: undefined,
|
||||
sender_address: 'sender-addr',
|
||||
origin_hash_mode: 1,
|
||||
coinbase_payload: 'hi',
|
||||
coinbase_payload: bufferToHexPrefixString(Buffer.from('hi')),
|
||||
event_count: 0,
|
||||
parent_index_block_hash: '0x00',
|
||||
parent_block_hash: '0x00',
|
||||
@@ -4186,7 +4258,7 @@ describe('postgres datastore', () => {
|
||||
sponsor_address: undefined,
|
||||
sender_address: 'sender-addr',
|
||||
origin_hash_mode: 1,
|
||||
coinbase_payload: 'hi',
|
||||
coinbase_payload: bufferToHexPrefixString(Buffer.from('hi')),
|
||||
event_count: 0,
|
||||
parent_index_block_hash: '0x00',
|
||||
parent_block_hash: '0x00',
|
||||
@@ -4249,7 +4321,7 @@ describe('postgres datastore', () => {
|
||||
tx_index: 0,
|
||||
anchor_mode: 3,
|
||||
nonce: 0,
|
||||
raw_tx: 'abc',
|
||||
raw_tx: bufferToHexPrefixString(Buffer.from('abc')),
|
||||
index_block_hash: '0x1234',
|
||||
block_hash: '0x5678',
|
||||
block_height: block1.block_height,
|
||||
@@ -4265,7 +4337,7 @@ describe('postgres datastore', () => {
|
||||
sponsor_address: undefined,
|
||||
sender_address: 'sender-addr',
|
||||
origin_hash_mode: 1,
|
||||
coinbase_payload: 'hi',
|
||||
coinbase_payload: bufferToHexPrefixString(Buffer.from('hi')),
|
||||
event_count: 0,
|
||||
parent_index_block_hash: '0x00',
|
||||
parent_block_hash: '0x00',
|
||||
@@ -4343,7 +4415,7 @@ describe('postgres datastore', () => {
|
||||
sponsor_address: undefined,
|
||||
sender_address: 'sender-addr',
|
||||
origin_hash_mode: 1,
|
||||
coinbase_payload: 'hi',
|
||||
coinbase_payload: bufferToHexPrefixString(Buffer.from('hi')),
|
||||
event_count: 4,
|
||||
parent_index_block_hash: '0x00',
|
||||
parent_block_hash: '0x00',
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import * as supertest from 'supertest';
|
||||
import { ChainID } from '@stacks/transactions';
|
||||
import { startApiServer, ApiServer } from '../api/init';
|
||||
import { PgSqlClient } from '../datastore/connection';
|
||||
import { TestBlockBuilder, testMempoolTx } from '../test-utils/test-builders';
|
||||
import { PgWriteStore } from '../datastore/pg-write-store';
|
||||
import { cycleMigrations, runMigrations } from '../datastore/migrations';
|
||||
import { DbTxTypeId } from '../datastore/common';
|
||||
|
||||
describe('mempool tests', () => {
|
||||
let db: PgWriteStore;
|
||||
@@ -23,8 +25,43 @@ describe('mempool tests', () => {
|
||||
});
|
||||
|
||||
test('garbage collection', async () => {
|
||||
const garbageThresholdOrig = process.env.STACKS_MEMPOOL_TX_GARBAGE_COLLECTION_THRESHOLD;
|
||||
process.env.STACKS_MEMPOOL_TX_GARBAGE_COLLECTION_THRESHOLD = '2';
|
||||
try {
|
||||
// Insert 5 blocks with 1 mempool tx each.
|
||||
for (let block_height = 1; block_height <= 5; block_height++) {
|
||||
const block = new TestBlockBuilder({
|
||||
block_height: block_height,
|
||||
index_block_hash: `0x0${block_height}`,
|
||||
parent_index_block_hash: `0x0${block_height - 1}`,
|
||||
})
|
||||
.addTx({ tx_id: `0x111${block_height}` })
|
||||
.build();
|
||||
await db.update(block);
|
||||
const mempoolTx = testMempoolTx({ tx_id: `0x0${block_height}` });
|
||||
await db.updateMempoolTxs({ mempoolTxs: [mempoolTx] });
|
||||
}
|
||||
|
||||
// Make sure we only have mempool txs for block_height >= 3
|
||||
const mempoolTxResult = await db.getMempoolTxList({
|
||||
limit: 10,
|
||||
offset: 0,
|
||||
includeUnanchored: false,
|
||||
});
|
||||
const mempoolTxs = mempoolTxResult.results;
|
||||
expect(mempoolTxs.length).toEqual(3);
|
||||
const txIds = mempoolTxs.map(e => e.tx_id).sort();
|
||||
expect(txIds).toEqual(['0x03', '0x04', '0x05']);
|
||||
} finally {
|
||||
if (typeof garbageThresholdOrig === 'undefined') {
|
||||
delete process.env.STACKS_MEMPOOL_TX_GARBAGE_COLLECTION_THRESHOLD;
|
||||
} else {
|
||||
process.env.STACKS_MEMPOOL_TX_GARBAGE_COLLECTION_THRESHOLD = garbageThresholdOrig;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('mempool stats', async () => {
|
||||
// Insert 5 blocks with 1 mempool tx each.
|
||||
for (let block_height = 1; block_height <= 5; block_height++) {
|
||||
const block = new TestBlockBuilder({
|
||||
@@ -35,20 +72,57 @@ describe('mempool tests', () => {
|
||||
.addTx({ tx_id: `0x111${block_height}` })
|
||||
.build();
|
||||
await db.update(block);
|
||||
const mempoolTx = testMempoolTx({ tx_id: `0x0${block_height}` });
|
||||
await db.updateMempoolTxs({ mempoolTxs: [mempoolTx] });
|
||||
const mempoolTx1 = testMempoolTx({
|
||||
tx_id: `0x0${block_height}`,
|
||||
type_id: DbTxTypeId.TokenTransfer,
|
||||
fee_rate: BigInt(100 * block_height),
|
||||
raw_tx: '0x' + 'ff'.repeat(block_height),
|
||||
});
|
||||
const mempoolTx2 = testMempoolTx({
|
||||
tx_id: `0x1${block_height}`,
|
||||
type_id: DbTxTypeId.ContractCall,
|
||||
fee_rate: BigInt(200 * block_height),
|
||||
raw_tx: '0x' + 'ff'.repeat(block_height + 10),
|
||||
});
|
||||
const mempoolTx3 = testMempoolTx({
|
||||
tx_id: `0x2${block_height}`,
|
||||
type_id: DbTxTypeId.SmartContract,
|
||||
fee_rate: BigInt(300 * block_height),
|
||||
raw_tx: '0x' + 'ff'.repeat(block_height + 20),
|
||||
});
|
||||
await db.updateMempoolTxs({ mempoolTxs: [mempoolTx1, mempoolTx2, mempoolTx3] });
|
||||
}
|
||||
|
||||
// Make sure we only have mempool txs for block_height >= 3
|
||||
const mempoolTxResult = await db.getMempoolTxList({
|
||||
limit: 10,
|
||||
offset: 0,
|
||||
includeUnanchored: false,
|
||||
});
|
||||
const mempoolTxs = mempoolTxResult.results;
|
||||
expect(mempoolTxs.length).toEqual(3);
|
||||
const txIds = mempoolTxs.map(e => e.tx_id).sort();
|
||||
expect(txIds).toEqual(['0x03', '0x04', '0x05']);
|
||||
const result = await supertest(api.server).get(`/extended/v1/tx/mempool/stats`);
|
||||
expect(result.status).toBe(200);
|
||||
expect(result.type).toBe('application/json');
|
||||
const expectedResp1 = {
|
||||
tx_type_counts: {
|
||||
token_transfer: 5,
|
||||
smart_contract: 5,
|
||||
contract_call: 5,
|
||||
poison_microblock: 0,
|
||||
},
|
||||
tx_simple_fee_averages: {
|
||||
token_transfer: { p25: 200, p50: 300, p75: 400, p95: 480 },
|
||||
smart_contract: { p25: 600, p50: 900, p75: 1200, p95: 1440 },
|
||||
contract_call: { p25: 400, p50: 600, p75: 800, p95: 960 },
|
||||
poison_microblock: { p25: null, p50: null, p75: null, p95: null },
|
||||
},
|
||||
tx_ages: {
|
||||
token_transfer: { p25: 2, p50: 3, p75: 4, p95: 4.8 },
|
||||
smart_contract: { p25: 2, p50: 3, p75: 4, p95: 4.8 },
|
||||
contract_call: { p25: 2, p50: 3, p75: 4, p95: 4.8 },
|
||||
poison_microblock: { p25: null, p50: null, p75: null, p95: null },
|
||||
},
|
||||
tx_byte_sizes: {
|
||||
token_transfer: { p25: 2, p50: 3, p75: 4, p95: 4.8 },
|
||||
smart_contract: { p25: 22, p50: 23, p75: 24, p95: 24.8 },
|
||||
contract_call: { p25: 12, p50: 13, p75: 14, p95: 14.8 },
|
||||
poison_microblock: { p25: null, p50: null, p75: null, p95: null },
|
||||
},
|
||||
};
|
||||
expect(JSON.parse(result.text)).toEqual(expectedResp1);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
@@ -307,7 +307,7 @@ describe('microblock tests', () => {
|
||||
sponsor_address: undefined,
|
||||
sender_address: addr1,
|
||||
origin_hash_mode: 1,
|
||||
coinbase_payload: 'hi',
|
||||
coinbase_payload: bufferToHexPrefixString(Buffer.from('hi')),
|
||||
event_count: 1,
|
||||
parent_index_block_hash: block1.parent_index_block_hash,
|
||||
parent_block_hash: block1.parent_block_hash,
|
||||
@@ -418,7 +418,7 @@ describe('microblock tests', () => {
|
||||
sponsor_address: undefined,
|
||||
origin_hash_mode: 1,
|
||||
token_transfer_amount: 50n,
|
||||
token_transfer_memo: 'hi',
|
||||
token_transfer_memo: bufferToHexPrefixString(Buffer.from('hi')),
|
||||
token_transfer_recipient_address: addr2,
|
||||
event_count: 1,
|
||||
parent_index_block_hash: block1.index_block_hash,
|
||||
@@ -458,7 +458,7 @@ describe('microblock tests', () => {
|
||||
sponsor_address: undefined,
|
||||
origin_hash_mode: 1,
|
||||
token_transfer_amount: 50n,
|
||||
token_transfer_memo: 'hi',
|
||||
token_transfer_memo: bufferToHexPrefixString(Buffer.from('hi')),
|
||||
token_transfer_recipient_address: addr2,
|
||||
event_count: 1,
|
||||
parent_index_block_hash: block1.index_block_hash,
|
||||
|
||||
Reference in New Issue
Block a user