mirror of
https://github.com/alexgo-io/stacks-blockchain-api.git
synced 2026-01-12 22:43:34 +08:00
fix: add parsed abi to mempool tx endpoints (#904)
This commit is contained in:
@@ -3451,20 +3451,11 @@ export class PgDataStore
|
||||
const hexTxIds = args.txIds.map(txId => hexToBuffer(txId));
|
||||
const result = await client.query<MempoolTxQueryResult>(
|
||||
`
|
||||
SELECT ${MEMPOOL_TX_COLUMNS},
|
||||
CASE
|
||||
WHEN mempool_txs.type_id = $2 THEN (
|
||||
SELECT abi
|
||||
FROM smart_contracts
|
||||
WHERE smart_contracts.contract_id = mempool_txs.contract_call_contract_id
|
||||
ORDER BY abi != 'null' DESC, canonical DESC, microblock_canonical DESC, block_height DESC
|
||||
LIMIT 1
|
||||
)
|
||||
END as abi
|
||||
SELECT ${MEMPOOL_TX_COLUMNS}, ${abiColumn('mempool_txs')}
|
||||
FROM mempool_txs
|
||||
WHERE tx_id = ANY($1)
|
||||
`,
|
||||
[hexTxIds, DbTxTypeId.ContractCall]
|
||||
[hexTxIds]
|
||||
);
|
||||
return await this.parseMempoolTransactions(result, client, args.includeUnanchored);
|
||||
});
|
||||
@@ -3482,20 +3473,11 @@ export class PgDataStore
|
||||
return this.queryTx(async client => {
|
||||
const result = await client.query<MempoolTxQueryResult>(
|
||||
`
|
||||
SELECT ${MEMPOOL_TX_COLUMNS},
|
||||
CASE
|
||||
WHEN mempool_txs.type_id = $2 THEN (
|
||||
SELECT abi
|
||||
FROM smart_contracts
|
||||
WHERE smart_contracts.contract_id = mempool_txs.contract_call_contract_id
|
||||
ORDER BY abi != 'null' DESC, canonical DESC, microblock_canonical DESC, block_height DESC
|
||||
LIMIT 1
|
||||
)
|
||||
END as abi
|
||||
SELECT ${MEMPOOL_TX_COLUMNS}, ${abiColumn('mempool_txs')}
|
||||
FROM mempool_txs
|
||||
WHERE tx_id = $1
|
||||
`,
|
||||
[hexToBuffer(txId), DbTxTypeId.ContractCall]
|
||||
[hexToBuffer(txId)]
|
||||
);
|
||||
// Treat the tx as "not pruned" if it's in an unconfirmed microblock and the caller is has not opted-in to unanchored data.
|
||||
if (result.rows[0]?.pruned && !includeUnanchored) {
|
||||
@@ -3548,7 +3530,7 @@ export class PgDataStore
|
||||
const selectCols = MEMPOOL_TX_COLUMNS.replace('tx_id', 'mempool.tx_id');
|
||||
const resultQuery = await client.query<MempoolTxQueryResult & { count: string }>(
|
||||
`
|
||||
SELECT ${selectCols}, COUNT(*) OVER() AS count
|
||||
SELECT ${selectCols}, ${abiColumn('mempool')}, COUNT(*) OVER() AS count
|
||||
FROM (
|
||||
SELECT *
|
||||
FROM mempool_txs
|
||||
@@ -3633,7 +3615,7 @@ export class PgDataStore
|
||||
);
|
||||
const resultQuery = await client.query<MempoolTxQueryResult>(
|
||||
`
|
||||
SELECT ${MEMPOOL_TX_COLUMNS}
|
||||
SELECT ${MEMPOOL_TX_COLUMNS}, ${abiColumn('mempool_txs')}
|
||||
FROM mempool_txs
|
||||
WHERE ${whereCondition}
|
||||
ORDER BY receipt_time DESC
|
||||
@@ -5689,7 +5671,10 @@ export class PgDataStore
|
||||
}
|
||||
|
||||
const txMempoolQuery = await client.query<MempoolTxQueryResult>(
|
||||
`SELECT ${MEMPOOL_TX_COLUMNS} FROM mempool_txs WHERE pruned = false AND tx_id = $1 LIMIT 1`,
|
||||
`
|
||||
SELECT ${MEMPOOL_TX_COLUMNS}, ${abiColumn('mempool_txs')}
|
||||
FROM mempool_txs WHERE pruned = false AND tx_id = $1 LIMIT 1
|
||||
`,
|
||||
[hexToBuffer(hash)]
|
||||
);
|
||||
if (txMempoolQuery.rowCount > 0) {
|
||||
@@ -5736,7 +5721,10 @@ export class PgDataStore
|
||||
return await this.query(async client => {
|
||||
if (isContract) {
|
||||
const contractMempoolTxResult = await client.query<MempoolTxQueryResult>(
|
||||
`SELECT ${MEMPOOL_TX_COLUMNS} from mempool_txs WHERE pruned = false AND smart_contract_contract_id = $1 LIMIT 1`,
|
||||
`
|
||||
SELECT ${MEMPOOL_TX_COLUMNS}, ${abiColumn('mempool_txs')}
|
||||
FROM mempool_txs WHERE pruned = false AND smart_contract_contract_id = $1 LIMIT 1
|
||||
`,
|
||||
[principal]
|
||||
);
|
||||
if (contractMempoolTxResult.rowCount > 0) {
|
||||
|
||||
@@ -50,6 +50,7 @@ import { PoolClient } from 'pg';
|
||||
import { bufferToHexPrefixString, I32_MAX, microStxToStx, STACKS_DECIMAL_PLACES } from '../helpers';
|
||||
import { FEE_RATE } from './../api/routes/fee-rate';
|
||||
import { Block, FeeRateRequest } from 'docs/generated';
|
||||
import { TestBlockBuilder, TestMempoolTxBuilder } from './test-helpers';
|
||||
|
||||
describe('api tests', () => {
|
||||
let db: PgDataStore;
|
||||
@@ -1707,6 +1708,75 @@ describe('api tests', () => {
|
||||
expect(JSON.parse(searchResult7.text)).toEqual(expectedResp7);
|
||||
});
|
||||
|
||||
test('mempool - contract_call tx abi details are retrieved', async () => {
|
||||
const block1 = new TestBlockBuilder()
|
||||
.addTx()
|
||||
.addTxSmartContract()
|
||||
.addTxContractLogEvent()
|
||||
.build();
|
||||
await db.update(block1);
|
||||
|
||||
const mempoolTx1 = new TestMempoolTxBuilder({
|
||||
type_id: DbTxTypeId.ContractCall,
|
||||
tx_id: '0x1232000000000000000000000000000000000000000000000000000000000000',
|
||||
}).build();
|
||||
await db.updateMempoolTxs({ mempoolTxs: [mempoolTx1] });
|
||||
|
||||
const expectedContractDetails = {
|
||||
contract_id: 'ST27W5M8BRKA7C5MZE2R1S1F4XTPHFWFRNHA9M04Y.hello-world',
|
||||
function_args: [
|
||||
{
|
||||
hex: '0x010000000000000000000000000001e240',
|
||||
name: 'amount',
|
||||
repr: 'u123456',
|
||||
type: 'uint',
|
||||
},
|
||||
],
|
||||
function_name: 'test-contract-fn',
|
||||
function_signature: '(define-public (test-contract-fn (amount uint)))',
|
||||
};
|
||||
|
||||
// Mempool txs
|
||||
const mempoolResults = await supertest(api.server).get(`/extended/v1/tx/mempool`);
|
||||
expect(mempoolResults.status).toBe(200);
|
||||
expect(mempoolResults.type).toBe('application/json');
|
||||
expect(JSON.parse(mempoolResults.text).results[0].contract_call).toEqual(
|
||||
expectedContractDetails
|
||||
);
|
||||
|
||||
// Search mempool tx metadata
|
||||
const searchResults = await supertest(api.server).get(
|
||||
`/extended/v1/search/${mempoolTx1.tx_id}?include_metadata=true`
|
||||
);
|
||||
expect(searchResults.status).toBe(200);
|
||||
expect(searchResults.type).toBe('application/json');
|
||||
expect(JSON.parse(searchResults.text).result.metadata.contract_call).toEqual(
|
||||
expectedContractDetails
|
||||
);
|
||||
|
||||
// Search principal metadata
|
||||
const searchPrincipalResults = await supertest(api.server).get(
|
||||
`/extended/v1/search/${expectedContractDetails.contract_id}?include_metadata=true`
|
||||
);
|
||||
expect(searchPrincipalResults.status).toBe(200);
|
||||
expect(searchPrincipalResults.type).toBe('application/json');
|
||||
expect(JSON.parse(searchPrincipalResults.text).result.metadata.contract_call).toEqual(
|
||||
expectedContractDetails
|
||||
);
|
||||
|
||||
// Dropped mempool tx
|
||||
await db.dropMempoolTxs({
|
||||
status: DbTxStatus.DroppedReplaceAcrossFork,
|
||||
txIds: [mempoolTx1.tx_id],
|
||||
});
|
||||
const mempoolDropResults = await supertest(api.server).get(`/extended/v1/tx/mempool/dropped`);
|
||||
expect(mempoolDropResults.status).toBe(200);
|
||||
expect(mempoolDropResults.type).toBe('application/json');
|
||||
expect(JSON.parse(mempoolDropResults.text).results[0].contract_call).toEqual(
|
||||
expectedContractDetails
|
||||
);
|
||||
});
|
||||
|
||||
test('search term - hash', async () => {
|
||||
const block: DbBlock = {
|
||||
block_hash: '0x1234000000000000000000000000000000000000000000000000000000000000',
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
import { I32_MAX } from '../helpers';
|
||||
import {
|
||||
DataStoreBlockUpdateData,
|
||||
DbEventTypeId,
|
||||
DbMempoolTx,
|
||||
DbTxTypeId,
|
||||
} from '../datastore/common';
|
||||
import { bufferCVFromString, serializeCV, uintCV } from '@stacks/transactions';
|
||||
import { createClarityValueArray } from '../p2p/tx';
|
||||
|
||||
// Hack to avoid jest outputting 'Your test suite must contain at least one test.'
|
||||
// https://stackoverflow.com/a/59864054/794962
|
||||
test.skip('test-ignore-kludge', () => 1);
|
||||
@@ -66,3 +76,203 @@ export function withEnvVars(...envVars: TestEnvVar[]) {
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder that creates a test block with any number of transactions and events so populating
|
||||
* the DB for testing becomes easier.
|
||||
*
|
||||
* The output of `build()` can be used in a `db.update()` call to process the block just as
|
||||
* if it came from the Event Server.
|
||||
*/
|
||||
export class TestBlockBuilder {
|
||||
// Default values when none given. Useful when they are irrelevant for a particular test.
|
||||
public static readonly SENDER_ADDRESS = 'SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27';
|
||||
public static readonly CONTRACT_ID = 'ST27W5M8BRKA7C5MZE2R1S1F4XTPHFWFRNHA9M04Y.hello-world';
|
||||
public static readonly CONTRACT_ABI = {
|
||||
maps: [],
|
||||
functions: [
|
||||
{
|
||||
args: [{ type: 'uint128', name: 'amount' }],
|
||||
name: 'test-contract-fn',
|
||||
access: 'public',
|
||||
outputs: {
|
||||
type: {
|
||||
response: {
|
||||
ok: 'uint128',
|
||||
error: 'none',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
variables: [],
|
||||
fungible_tokens: [],
|
||||
non_fungible_tokens: [],
|
||||
};
|
||||
public static readonly CONTRACT_SOURCE = '(some-contract-src)';
|
||||
public static readonly CONTRACT_CALL_FUNCTION_NAME = 'test-contract-fn';
|
||||
|
||||
private data: DataStoreBlockUpdateData;
|
||||
private txIndex = 0;
|
||||
|
||||
constructor(args?: { block_height?: number; block_hash?: string }) {
|
||||
this.data = {
|
||||
block: {
|
||||
block_hash: args?.block_hash ?? '0x1234',
|
||||
index_block_hash: '0xdeadbeef',
|
||||
parent_index_block_hash: '0x00',
|
||||
parent_block_hash: '0xff0011',
|
||||
parent_microblock_hash: '',
|
||||
block_height: args?.block_height ?? 1,
|
||||
burn_block_time: 94869286,
|
||||
burn_block_hash: '0x1234',
|
||||
burn_block_height: 123,
|
||||
miner_txid: '0x4321',
|
||||
canonical: true,
|
||||
parent_microblock_sequence: 0,
|
||||
execution_cost_read_count: 0,
|
||||
execution_cost_read_length: 0,
|
||||
execution_cost_runtime: 0,
|
||||
execution_cost_write_count: 0,
|
||||
execution_cost_write_length: 0,
|
||||
},
|
||||
microblocks: [],
|
||||
minerRewards: [],
|
||||
txs: [],
|
||||
};
|
||||
}
|
||||
|
||||
addTx(args?: {
|
||||
sender_address?: string;
|
||||
type_id?: DbTxTypeId;
|
||||
tx_id?: string;
|
||||
}): TestBlockBuilder {
|
||||
this.data.txs.push({
|
||||
tx: {
|
||||
tx_id: args?.tx_id ?? '0x01',
|
||||
tx_index: 0,
|
||||
anchor_mode: 3,
|
||||
nonce: 0,
|
||||
raw_tx: Buffer.alloc(0),
|
||||
index_block_hash: this.data.block.index_block_hash,
|
||||
block_hash: this.data.block.block_hash,
|
||||
block_height: this.data.block.block_height,
|
||||
burn_block_time: this.data.block.burn_block_time,
|
||||
parent_burn_block_time: 1626122935,
|
||||
type_id: args?.type_id ?? DbTxTypeId.Coinbase,
|
||||
status: 1,
|
||||
raw_result: '0x0100000000000000000000000000000001', // u1
|
||||
canonical: true,
|
||||
post_conditions: Buffer.from([0x01, 0xf5]),
|
||||
fee_rate: 1234n,
|
||||
sponsored: false,
|
||||
sponsor_address: undefined,
|
||||
sender_address: args?.sender_address ?? TestBlockBuilder.SENDER_ADDRESS,
|
||||
origin_hash_mode: 1,
|
||||
coinbase_payload: Buffer.from('hi'),
|
||||
event_count: 1,
|
||||
parent_index_block_hash: '',
|
||||
parent_block_hash: '',
|
||||
microblock_canonical: true,
|
||||
microblock_sequence: I32_MAX,
|
||||
microblock_hash: '',
|
||||
execution_cost_read_count: 0,
|
||||
execution_cost_read_length: 0,
|
||||
execution_cost_runtime: 0,
|
||||
execution_cost_write_count: 0,
|
||||
execution_cost_write_length: 0,
|
||||
},
|
||||
stxLockEvents: [],
|
||||
stxEvents: [],
|
||||
ftEvents: [],
|
||||
nftEvents: [],
|
||||
contractLogEvents: [],
|
||||
smartContracts: [],
|
||||
names: [],
|
||||
namespaces: [],
|
||||
});
|
||||
this.txIndex = this.data.txs.length - 1;
|
||||
return this;
|
||||
}
|
||||
|
||||
addTxContractLogEvent(args?: { contract_identifier?: string }): TestBlockBuilder {
|
||||
this.data.txs[this.txIndex].contractLogEvents.push({
|
||||
event_index: 4,
|
||||
tx_id: this.data.txs[this.txIndex].tx.tx_id,
|
||||
tx_index: 0,
|
||||
block_height: this.data.block.block_height,
|
||||
canonical: true,
|
||||
event_type: DbEventTypeId.SmartContractLog,
|
||||
contract_identifier: args?.contract_identifier ?? TestBlockBuilder.CONTRACT_ID,
|
||||
topic: 'some-topic',
|
||||
value: serializeCV(bufferCVFromString('some val')),
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
addTxSmartContract(args?: { contract_id?: string; abi?: string }): TestBlockBuilder {
|
||||
this.data.txs[this.txIndex].smartContracts.push({
|
||||
tx_id: this.data.txs[this.txIndex].tx.tx_id,
|
||||
canonical: true,
|
||||
block_height: this.data.block.block_height,
|
||||
contract_id: args?.contract_id ?? TestBlockBuilder.CONTRACT_ID,
|
||||
source_code: TestBlockBuilder.CONTRACT_SOURCE,
|
||||
abi: args?.abi ?? JSON.stringify(TestBlockBuilder.CONTRACT_ABI),
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
build(): DataStoreBlockUpdateData {
|
||||
return this.data;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder that creates a test mempool transaction so populating the DB for testing becomes easier.
|
||||
*
|
||||
* The output of `build()` can be used in a `db.updateMempoolTxs()` call to process the tx just as
|
||||
* if it came from the Event Server.
|
||||
*/
|
||||
export class TestMempoolTxBuilder {
|
||||
data: DbMempoolTx;
|
||||
|
||||
constructor(args?: {
|
||||
type_id?: DbTxTypeId;
|
||||
sender_address?: string;
|
||||
tx_id?: string;
|
||||
smart_contract_contract_id?: string;
|
||||
contract_call_contract_id?: string;
|
||||
contract_call_function_name?: string;
|
||||
contract_call_function_args?: Buffer;
|
||||
}) {
|
||||
// If not given, default values are taken from `TestBlockBuilder` for consistency.
|
||||
this.data = {
|
||||
pruned: false,
|
||||
tx_id: args?.tx_id ?? `0x1234`,
|
||||
anchor_mode: 3,
|
||||
nonce: 0,
|
||||
raw_tx: Buffer.from('test-raw-tx'),
|
||||
type_id: args?.type_id ?? DbTxTypeId.TokenTransfer,
|
||||
receipt_time: (new Date().getTime() / 1000) | 0,
|
||||
status: 1,
|
||||
post_conditions: Buffer.from([0x01, 0xf5]),
|
||||
fee_rate: 1234n,
|
||||
sponsored: false,
|
||||
sponsor_address: undefined,
|
||||
origin_hash_mode: 1,
|
||||
sender_address: args?.sender_address ?? TestBlockBuilder.SENDER_ADDRESS,
|
||||
token_transfer_amount: 1234n,
|
||||
token_transfer_memo: Buffer.alloc(0),
|
||||
smart_contract_contract_id: args?.smart_contract_contract_id ?? TestBlockBuilder.CONTRACT_ID,
|
||||
contract_call_contract_id: args?.contract_call_contract_id ?? TestBlockBuilder.CONTRACT_ID,
|
||||
contract_call_function_name:
|
||||
args?.contract_call_function_name ?? TestBlockBuilder.CONTRACT_CALL_FUNCTION_NAME,
|
||||
contract_call_function_args:
|
||||
args?.contract_call_function_args ?? createClarityValueArray(uintCV(123456)),
|
||||
};
|
||||
}
|
||||
|
||||
build(): DbMempoolTx {
|
||||
return this.data;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user