fix: add parsed abi to mempool tx endpoints (#904)

This commit is contained in:
Rafael Cárdenas
2021-12-14 12:05:56 -06:00
committed by GitHub
parent ee61346ac9
commit dfcc591d2a
3 changed files with 294 additions and 26 deletions

View File

@@ -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) {

View File

@@ -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',

View File

@@ -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;
}
}