feat: endpoint for list of transactions #647

This commit is contained in:
M Hassan Tariq
2021-11-02 16:32:09 +05:00
committed by GitHub
parent 6e9807e09e
commit 7edc7b54a6
14 changed files with 1095 additions and 153 deletions

View File

@@ -0,0 +1,24 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"title": "TransactionFound",
"description": "This object returns transaction for found true",
"additionalProperties": false,
"required": ["found", "result"],
"properties": {
"found": {
"type": "boolean",
"enum": [true]
},
"result": {
"anyOf":[
{
"$ref": "../mempool-transactions/transaction.schema.json"
},
{
"$ref": "./transaction.schema.json"
}
]
}
}
}

View File

@@ -0,0 +1,15 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"title": "TransactionList",
"additionalProperties": {
"anyOf": [
{
"$ref": "./transaction-found.schema.json"
},
{
"$ref": "./transaction-not-found.schema.json"
}
]
}
}

View File

@@ -0,0 +1,24 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"title": "TransactionNotFound",
"description": "This object returns the id for not found transaction",
"additionalProperties": false,
"properties": {
"found": {
"type": "boolean",
"enum": [false]
},
"result": {
"type": "object",
"required": ["tx_id"],
"additionalProperties": false,
"properties": {
"tx_id": {
"type": "string"
}
}
}
},
"required": ["found", "result"]
}

View File

@@ -0,0 +1,97 @@
{
"0x8911000000000000000000000000000000000000000000000000000000000000": {
"found": true,
"result": {
"tx_id": "0x8911000000000000000000000000000000000000000000000000000000000000",
"nonce": 0,
"fee_rate": "1234",
"sender_address": "sender-addr",
"sponsored": true,
"sponsor_address": "sponsor-addr",
"post_condition_mode": "allow",
"post_conditions": [],
"anchor_mode": "any",
"is_unanchored": false,
"block_hash": "0x0123",
"parent_block_hash": "0x5678",
"block_height": 0,
"burn_block_time": 39486,
"burn_block_time_iso": "1970-01-01T10:58:06.000Z",
"parent_burn_block_time": 1626122935,
"parent_burn_block_time_iso": "2021-07-12T20:48:55.000Z",
"canonical": true,
"tx_index": 4,
"tx_status": "success",
"microblock_hash": "",
"microblock_sequence": 2147483647,
"microblock_canonical": true,
"event_count": 0,
"events": [],
"execution_cost_read_count": 0,
"execution_cost_read_length": 0,
"execution_cost_runtime": 0,
"execution_cost_write_count": 0,
"execution_cost_write_length": 0,
"tx_type": "coinbase"
}
},
"0x8915000000000000000000000000000000000000000000000000000000000000": {
"found": true,
"result": {
"tx_id": "0x8915000000000000000000000000000000000000000000000000000000000000",
"nonce": 1000,
"fee_rate": "1234",
"sender_address": "sender-addr",
"sponsored": true,
"sponsor_address": "sponsor-addr",
"post_condition_mode": "allow",
"post_conditions": [],
"anchor_mode": "any",
"is_unanchored": false,
"block_hash": "0x0123",
"parent_block_hash": "0x5678",
"block_height": 0,
"burn_block_time": 39486,
"burn_block_time_iso": "1970-01-01T10:58:06.000Z",
"parent_burn_block_time": 1626122935,
"parent_burn_block_time_iso": "2021-07-12T20:48:55.000Z",
"canonical": true,
"tx_index": 4,
"tx_status": "success",
"microblock_hash": "",
"microblock_sequence": 2147483647,
"microblock_canonical": true,
"event_count": 0,
"events": [],
"execution_cost_read_count": 0,
"execution_cost_read_length": 0,
"execution_cost_runtime": 0,
"execution_cost_write_count": 0,
"execution_cost_write_length": 0,
"tx_type": "coinbase"
}
},
"0x8912000000000000000000000000000000000000000000000000000000000000": {
"found": true,
"result": {
"tx_id": "0x8912000000000000000000000000000000000000000000000000000000000000",
"nonce": 0,
"fee_rate": "1234",
"sender_address": "sender-addr",
"sponsored": false,
"post_condition_mode": "allow",
"post_conditions": [],
"anchor_mode": "any",
"tx_status": "pending",
"receipt_time": 1594307695,
"receipt_time_iso": "2020-07-09T15:14:55.000Z",
"tx_type": "coinbase"
}
},
"0x8914000000000000000000000000000000000000000000000000000000000000": {
"found": false,
"result": {
"tx_id": "0x8914000000000000000000000000000000000000000000000000000000000000"
}
}
}

22
docs/generated.d.ts vendored
View File

@@ -227,7 +227,10 @@ export type SchemaMergeRootStub =
| PoisonMicroblockTransaction
| CoinbaseTransactionMetadata
| CoinbaseTransaction
| TransactionFound
| TransactionList
| TransactionMetadata
| TransactionNotFound
| TransactionStatus1
| TransactionType
| Transaction
@@ -3265,6 +3268,25 @@ export interface RosettaOperationIdentifier1 {
network_index?: number;
[k: string]: unknown | undefined;
}
/**
* This object returns transaction for found true
*/
export interface TransactionFound {
found: true;
result: MempoolTransaction | Transaction;
}
export interface TransactionList {
[k: string]: (TransactionFound | TransactionNotFound) | undefined;
}
/**
* This object returns the id for not found transaction
*/
export interface TransactionNotFound {
found: false;
result: {
tx_id: string;
};
}
export interface RpcAddressBalanceNotificationParams {
address: string;
balance: string;

View File

@@ -211,6 +211,58 @@ paths:
example:
$ref: ./api/transaction/get-mempool-transactions.example.json
/extended/v1/tx/multiple:
parameters:
- name: tx_id
in: query
description: Array of transaction ids
required: true
schema:
type: array
items:
type: string
- name: event_offset
in: query
schema:
type: integer
default: 0
description: The number of events to skip
- name: event_limit
in: query
schema:
type: integer
default: 96
description: The numbers of events to return
- name: unanchored
in: query
description: Include transaction data from unanchored (i.e. unconfirmed) microblocks
required: false
schema:
type: boolean
default: false
get:
summary: Get list of details for transactions
tags:
- Transactions
operationId: get_tx_list_details
description: |
Get an array of transactions by IDs
If using TypeScript, import typings for this response from our types package:
`import type { Transaction } from '@stacks/stacks-blockchain-api-types';`
responses:
200:
description: Returns list of transactions with their details for corresponding requested tx_ids.
content:
application/json:
schema:
$ref: ./entities/transactions/transaction-list.schema.json
example:
$ref: ./entities/transactions/transactions-list-detail.example.json
404:
description: Could not find any transaction by ID
/extended/v1/tx/{tx_id}:
parameters:
- name: tx_id

View File

@@ -16,6 +16,7 @@ import {
CoinbaseTransactionMetadata,
ContractCallTransaction,
ContractCallTransactionMetadata,
MempoolContractCallTransaction,
MempoolTransaction,
MempoolTransactionStatus,
Microblock,
@@ -34,7 +35,10 @@ import {
TransactionEventSmartContractLog,
TransactionEventStxAsset,
TransactionEventStxLock,
TransactionFound,
TransactionList,
TransactionMetadata,
TransactionNotFound,
TransactionStatus,
TransactionType,
} from '@stacks/stacks-blockchain-api-types';
@@ -51,6 +55,7 @@ import {
DbTx,
DbTxStatus,
DbTxTypeId,
DbSmartContract,
} from '../../datastore/common';
import {
unwrapOptional,
@@ -65,6 +70,8 @@ import {
import { readClarityValueArray, readTransactionPostConditions } from '../../p2p/tx';
import { serializePostCondition, serializePostConditionMode } from '../serializers/post-conditions';
import { getOperations, parseTransactionMemo, processUnlockingEvents } from '../../rosetta-helpers';
import { any } from 'bluebird';
import { push } from 'docker-compose';
export function parseTxTypeStrings(values: string[]): TransactionType[] {
return values.map(v => {
@@ -564,6 +571,16 @@ export interface GetTxFromDbTxArgs extends GetTxArgs {
dbTx: DbTx;
}
export interface GetTxsWithEventsArgs extends GetTxsArgs {
eventLimit: number;
eventOffset: number;
}
export interface GetTxsArgs {
txIds: string[];
includeUnanchored: boolean;
}
export interface GetTxWithEventsArgs extends GetTxArgs {
eventLimit: number;
eventOffset: number;
@@ -762,27 +779,96 @@ export function parseDbMempoolTx(dbMempoolTx: DbMempoolTx): MempoolTransaction {
return result;
}
export async function getMempoolTxFromDataStore(
export async function getMempoolTxsFromDataStore(
db: DataStore,
args: GetTxArgs
): Promise<FoundOrNot<MempoolTransaction>> {
const mempoolTxQuery = await db.getMempoolTx({
txId: args.txId,
args: GetTxsArgs
): Promise<MempoolTransaction[]> {
const mempoolTxsQuery = await db.getMempoolTxs({
txIds: args.txIds,
includePruned: true,
includeUnanchored: args.includeUnanchored,
});
if (!mempoolTxQuery.found) {
return { found: false };
if (mempoolTxsQuery.length === 0) {
return [];
}
const parsedMempoolTx = parseDbMempoolTx(mempoolTxQuery.result);
// If tx type is contract-call then fetch additional contract ABI details for a richer response
if (parsedMempoolTx.tx_type === 'contract_call') {
await getContractCallMetadata(db, mempoolTxQuery.result, parsedMempoolTx);
const parsedMempoolTxs = mempoolTxsQuery.map(tx => parseDbMempoolTx(tx));
// separating transactions with type contract_call
const contractCallTxs = parsedMempoolTxs.filter(tx => tx.tx_type === 'contract_call');
// getting contract call information for richer data
if (contractCallTxs.length > 0) {
const contracts = await getSmartContractsForTxList(db, mempoolTxsQuery);
const transactions = parseContractsWithMempoolTxs(contracts, mempoolTxsQuery);
if (transactions) {
const parsedTxs = transactions;
return parsedTxs;
}
}
return {
found: true,
result: parsedMempoolTx,
};
return parsedMempoolTxs;
}
export async function getTxsFromDataStore(
db: DataStore,
args: GetTxsArgs | GetTxsWithEventsArgs
): Promise<Transaction[]> {
// fetching all requested transactions from db
const txQuery = await db.getTxListDetails({
txIds: args.txIds,
includeUnanchored: args.includeUnanchored,
});
// returning empty array if no transaction was found
if (txQuery.length === 0) {
return [];
}
let events: DbEvent[] = [];
if ('eventLimit' in args) {
const txIdsAndIndexHash = txQuery.map(tx => {
return {
txId: tx.tx_id,
indexBlockHash: tx.index_block_hash,
};
});
events = (
await db.getTxListEvents({
txs: txIdsAndIndexHash,
limit: args.eventLimit,
offset: args.eventOffset,
})
).results;
}
// parsing txQuery
let parsedTxs = txQuery.map(tx => parseDbTx(tx));
// separating transactions with type contract_call
const contractCallTxs = parsedTxs.filter(tx => tx.tx_type === 'contract_call');
// getting contract call information for richer data
if (contractCallTxs.length > 0) {
const contracts = await getSmartContractsForTxList(db, txQuery);
const transactions = parseContractsWithDbTxs(contracts, txQuery);
if (transactions) {
parsedTxs = transactions;
}
}
// incase transaction events are requested
if ('eventLimit' in args) {
// this will insert all events in a single parsedTransaction. Only specific ones are to be added.
parsedTxs.forEach(
ptx =>
(ptx.events = events
.filter(event => event.tx_id === ptx.tx_id)
.map(event => parseDbEvent(event)))
);
}
return parsedTxs;
}
export async function getTxFromDataStore(
@@ -800,11 +886,18 @@ export async function getTxFromDataStore(
dbTx = txQuery.result;
}
const parsedTx = parseDbTx(dbTx);
let parsedTx = parseDbTx(dbTx);
// If tx type is contract-call then fetch additional contract ABI details for a richer response
if (parsedTx.tx_type === 'contract_call') {
await getContractCallMetadata(db, dbTx, parsedTx);
const transaction = await getContractCallMetadata(
db,
parseDbTx(dbTx) as ContractCallTransaction,
dbTx
);
if (transaction) {
parsedTx = transaction as ContractCallTransaction;
}
}
// If tx events are requested
@@ -824,63 +917,185 @@ export async function getTxFromDataStore(
};
}
function parseContractsWithDbTxs(contracts: DbSmartContract[], dbTxs: DbTx[]): Transaction[] {
const transactions: Transaction[] = [];
contracts.forEach(contract => {
const dbTx = dbTxs.find(tx => tx.contract_call_contract_id === contract.contract_id);
if (dbTx) {
const transaction = parseContractCallMetadata(
{ found: true, result: contract },
parseDbTx(dbTx) as ContractCallTransaction,
dbTx
);
if (transaction) {
transactions.push(transaction as Transaction);
}
}
});
return transactions;
}
function parseContractsWithMempoolTxs(
contracts: DbSmartContract[],
dbMempoolTx: DbMempoolTx[]
): MempoolTransaction[] {
const transactions: MempoolTransaction[] = [];
contracts.forEach(contract => {
const dbMempool = dbMempoolTx.find(tx => tx.contract_call_contract_id === contract.contract_id);
if (dbMempool) {
const transaction = parseContractCallMetadata(
{ found: true, result: contract },
parseDbMempoolTx(dbMempool) as MempoolContractCallTransaction,
dbMempool
);
if (transaction) {
transactions.push(transaction as MempoolTransaction);
}
}
});
return transactions;
}
async function getSmartContractsForTxList(
db: DataStore,
transactions: DbTx[] | DbMempoolTx[]
): Promise<DbSmartContract[]> {
const contractCallIds: string[] = [];
transactions.forEach((transaction: DbMempoolTx | DbTx) => {
if (transaction && transaction.contract_call_contract_id)
contractCallIds.push(transaction.contract_call_contract_id);
});
const contracts = await db.getSmartContractList(contractCallIds);
return contracts;
}
async function getContractCallMetadata(
db: DataStore,
dbTx: DbTx | DbMempoolTx,
parsedTx: Transaction | MempoolTransaction
): Promise<void> {
parsedTx: ContractCallTransaction | MempoolContractCallTransaction,
dbTransaction: DbTx | DbMempoolTx
): Promise<ContractCallTransaction | MempoolContractCallTransaction | undefined> {
// If tx type is contract-call then fetch additional contract ABI details for a richer response
if (parsedTx === undefined) {
return parsedTx;
}
if (parsedTx.tx_type === 'contract_call') {
const contract = await db.getSmartContract(parsedTx.contract_call.contract_id);
if (!contract.found) {
throw new Error(
`Failed to lookup smart contract by ID ${parsedTx.contract_call.contract_id}`
);
}
const contractAbi: ClarityAbi = JSON.parse(contract.result.abi);
const functionAbi = contractAbi.functions.find(
fn => fn.name === parsedTx.contract_call.function_name
);
if (!functionAbi) {
throw new Error(
`Could not find function name "${parsedTx.contract_call.function_name}" in ABI for ${parsedTx.contract_call.contract_id}`
);
}
parsedTx.contract_call.function_signature = abiFunctionToString(functionAbi);
if (dbTx.contract_call_function_args) {
parsedTx.contract_call.function_args = readClarityValueArray(
dbTx.contract_call_function_args
).map((c, fnArgIndex) => {
const functionArgAbi = functionAbi.args[fnArgIndex++];
return {
hex: bufferToHexPrefixString(serializeCV(c)),
repr: cvToString(c),
name: functionArgAbi.name,
type: getTypeString(functionArgAbi.type),
};
});
}
return parseContractCallMetadata(contract, parsedTx, dbTransaction);
}
}
function parseContractCallMetadata(
contract: FoundOrNot<DbSmartContract>,
parsedTx: ContractCallTransaction | MempoolContractCallTransaction,
dbTransaction: DbTx | DbMempoolTx
): ContractCallTransaction | MempoolContractCallTransaction {
if (!contract.found) {
throw new Error(`Failed to lookup smart contract by ID ${parsedTx.contract_call.contract_id}`);
}
const contractAbi: ClarityAbi = JSON.parse(contract.result.abi);
const functionAbi = contractAbi.functions.find(
fn => fn.name === parsedTx.contract_call.function_name
);
if (!functionAbi) {
throw new Error(
`Could not find function name "${parsedTx.contract_call.function_name}" in ABI for ${parsedTx.contract_call.contract_id}`
);
}
parsedTx.contract_call.function_signature = abiFunctionToString(functionAbi);
if (dbTransaction.contract_call_function_args) {
parsedTx.contract_call.function_args = readClarityValueArray(
dbTransaction.contract_call_function_args
).map((c, fnArgIndex) => {
const functionArgAbi = functionAbi.args[fnArgIndex++];
return {
hex: bufferToHexPrefixString(serializeCV(c)),
repr: cvToString(c),
name: functionArgAbi.name,
type: getTypeString(functionArgAbi.type),
};
});
}
return parsedTx;
}
export async function searchTxs(
db: DataStore,
args: GetTxsArgs | GetTxsWithEventsArgs
): Promise<TransactionList> {
const minedTxs = await getTxsFromDataStore(db, args);
const foundTransactions: TransactionFound[] = [];
const mempoolTxs: string[] = [];
minedTxs.forEach(tx => {
// filtering out mined transactions in canonical chain
if (tx.canonical && tx.microblock_canonical) {
foundTransactions.push({ found: true, result: tx });
}
// filtering out non canonical transactions to look into mempool table
if (!tx.canonical && !tx.microblock_canonical) {
mempoolTxs.push(tx.tx_id);
}
});
// filtering out tx_ids that were not mined / found
const notMinedTransactions: string[] = args.txIds.filter(
txId => !minedTxs.find(minedTx => txId === minedTx.tx_id)
);
// finding transactions that are not mined and are not canonical in mempool
mempoolTxs.push(...notMinedTransactions);
const mempoolTxsQuery = await getMempoolTxsFromDataStore(db, {
txIds: mempoolTxs,
includeUnanchored: args.includeUnanchored,
});
// merging found mempool transaction in found transactions object
foundTransactions.push(
...mempoolTxsQuery.map((mtx: Transaction | MempoolTransaction) => {
return { found: true, result: mtx } as TransactionFound;
})
);
// filtering out transactions that were not found anywhere
const notFoundTransactions: TransactionNotFound[] = args.txIds
.filter(txId => foundTransactions.findIndex(ftx => ftx.result?.tx_id === txId) < 0)
.map(txId => {
return { found: false, result: { tx_id: txId } };
});
// generating response
const resp = [...foundTransactions, ...notFoundTransactions].reduce(
(map: TransactionList, obj) => {
if (obj.result) {
map[obj.result.tx_id] = obj;
}
return map;
},
{}
);
return resp;
}
export async function searchTx(
db: DataStore,
args: GetTxArgs | GetTxWithEventsArgs
): Promise<FoundOrNot<Transaction | MempoolTransaction>> {
// First, check the happy path: the tx is mined and in the canonical chain.
const minedTx = await getTxFromDataStore(db, args);
if (minedTx.found && minedTx.result.canonical && minedTx.result.microblock_canonical) {
return minedTx;
const minedTxs = await getTxsFromDataStore(db, { ...args, txIds: [args.txId] });
const minedTx = minedTxs[0] ?? undefined;
if (minedTx && minedTx.canonical && minedTx.microblock_canonical) {
return { found: true, result: minedTx };
} else {
// Otherwise, if not mined or not canonical, check in the mempool.
const mempoolTxQuery = await getMempoolTxFromDataStore(db, args);
if (mempoolTxQuery.found) {
return mempoolTxQuery;
const mempoolTxQuery = await getMempoolTxsFromDataStore(db, { ...args, txIds: [args.txId] });
const mempoolTx = mempoolTxQuery[0] ?? undefined;
if (mempoolTx) {
return { found: true, result: mempoolTx };
}
// Fallback for a situation where the tx was only mined in a non-canonical chain, but somehow not in the mempool table.
else if (minedTx.found) {
else if (minedTx) {
logger.warn(`Tx only exists in a non-canonical chain, missing from mempool: ${args.txId}`);
return minedTx;
return { found: true, result: minedTx };
}
// Tx not found in db.
else {

View File

@@ -7,6 +7,7 @@ import {
parseTxTypeStrings,
parseDbMempoolTx,
searchTx,
searchTxs,
} from '../controllers/db-controller';
import {
waiter,
@@ -90,6 +91,27 @@ export function createTxRouter(db: DataStore): RouterWithAsync {
res.json(response);
});
router.getAsync('/multiple', async (req, res, next) => {
const txList: string[] = req.query.tx_id as string[];
const eventLimit = parseTxQueryEventsLimit(req.query['event_limit'] ?? 96);
const eventOffset = parsePagingQueryInput(req.query['event_offset'] ?? 0);
const includeUnanchored = isUnanchoredRequest(req, res, next);
const txQuery = await searchTxs(db, {
txIds: txList,
eventLimit,
eventOffset,
includeUnanchored,
});
// TODO: this validation needs fixed now that the mempool-tx and mined-tx types no longer overlap
/*
const schemaPath = require.resolve(
'@stacks/stacks-blockchain-api-types/entities/transactions/transaction.schema.json'
);
await validate(schemaPath, txQuery.result);
*/
res.json(txQuery);
});
router.getAsync('/mempool', async (req, res, next) => {
const limit = parseTxQueryLimit(req.query.limit ?? 96);
const offset = parsePagingQueryInput(req.query.offset ?? 0);

View File

@@ -598,6 +598,11 @@ export interface DataStore extends DataStoreEventEmitter {
offset: number
): Promise<{ results: DbTx[]; total: number }>;
getMempoolTxs(args: {
txIds: string[];
includeUnanchored: boolean;
includePruned?: boolean;
}): Promise<DbMempoolTx[]>;
getMempoolTx(args: {
txId: string;
includeUnanchored: boolean;
@@ -631,6 +636,18 @@ export interface DataStore extends DataStoreEventEmitter {
offset: number;
}): Promise<{ results: DbEvent[] }>;
getTxListEvents(args: {
txs: {
txId: string;
indexBlockHash: string;
}[];
limit: number;
offset: number;
}): Promise<{ results: DbEvent[] }>;
getTxListDetails(args: { txIds: string[]; includeUnanchored: boolean }): Promise<DbTx[]>; // tx_id is returned for not found case
getSmartContractList(contractIds: string[]): Promise<DbSmartContract[]>;
getSmartContract(contractId: string): Promise<FoundOrNot<DbSmartContract>>;
getSmartContractEvents(args: {

View File

@@ -332,6 +332,14 @@ export class MemoryDataStore
return Promise.resolve({ found: true, result: tx });
}
getMempoolTxs(args: {
txIds: string[];
includeUnanchored: boolean;
includePruned?: boolean;
}): Promise<DbMempoolTx[]> {
throw new Error('not yet implemented');
}
getDroppedTxs(args: {
limit: number;
offset: number;
@@ -385,6 +393,25 @@ export class MemoryDataStore
return Promise.resolve({ results, total: transactionsList.length });
}
getTxListEvents(args: {
txs: {
txId: string;
indexBlockHash: string;
}[];
limit: number;
offset: number;
}): Promise<{ results: DbEvent[] }> {
throw new Error('not implemented');
}
getTxListDetails(args: { txIds: string[]; includeUnanchored: boolean }): Promise<DbTx[]> {
throw new Error('not implemented');
}
getSmartContractList(contractIds: string[]): Promise<DbSmartContract[]> {
throw new Error('not implemented');
}
getTxEvents(args: { txId: string; indexBlockHash: string; limit: number; offset: number }) {
const stxLockEvents = [...this.stxLockEvents.values()].filter(
e => e.indexBlockHash === args.indexBlockHash && e.entry.tx_id === args.txId

View File

@@ -3327,6 +3327,63 @@ export class PgDataStore
return tx;
}
private async parseMempoolTransactions(
result: QueryResult<MempoolTxQueryResult>,
client: ClientBase,
includeUnanchored: boolean
) {
if (result.rowCount === 0) {
return [];
}
const pruned = result.rows.filter(memTx => memTx.pruned && !includeUnanchored);
if (pruned.length !== 0) {
const unanchoredBlockHeight = await this.getMaxBlockHeight(client, {
includeUnanchored: true,
});
const notPrunedBufferTxIds = pruned.map(tx => tx.tx_id);
const query = await client.query<{ tx_id: Buffer }>(
`
SELECT tx_id
FROM txs
WHERE canonical = true AND microblock_canonical = true
AND tx_id = ANY($1)
AND block_height = $2
`,
[notPrunedBufferTxIds, unanchoredBlockHeight]
);
// The tx is marked as pruned because it's in an unanchored microblock
query.rows.forEach(tran => {
const transaction = result.rows.find(
tx => bufferToHexPrefixString(tx.tx_id) === bufferToHexPrefixString(tran.tx_id)
);
if (transaction) {
transaction.pruned = false;
transaction.status = DbTxStatus.Pending;
}
});
}
return result.rows.map(transaction => this.parseMempoolTxQueryResult(transaction));
}
async getMempoolTxs(args: {
txIds: string[];
includeUnanchored: boolean;
includePruned?: boolean;
}): Promise<DbMempoolTx[]> {
return this.queryTx(async client => {
const hexTxIds = args.txIds.map(txId => hexToBuffer(txId));
const result = await client.query<MempoolTxQueryResult>(
`
SELECT ${MEMPOOL_TX_COLUMNS}
FROM mempool_txs
WHERE tx_id = ANY($1)
`,
[hexTxIds]
);
return await this.parseMempoolTransactions(result, client, args.includeUnanchored);
});
}
async getMempoolTx({
txId,
includePruned,
@@ -3373,8 +3430,8 @@ export class PgDataStore
if (result.rowCount > 1) {
throw new Error(`Multiple transactions found in mempool table for txid: ${txId}`);
}
const row = result.rows[0];
const tx = this.parseMempoolTxQueryResult(row);
const rows = await this.parseMempoolTransactions(result, client, includeUnanchored);
const tx = rows[0];
return { found: true, result: tx };
});
}
@@ -3620,6 +3677,147 @@ export class PgDataStore
});
}
getTxListEvents(args: {
txs: {
txId: string;
indexBlockHash: string;
}[];
limit: number;
offset: number;
}) {
return this.queryTx(async client => {
// preparing condition to query from
// condition = (tx_id=$1 AND index_block_hash=$2) OR (tx_id=$3 AND index_block_hash=$4)
// let condition = this.generateParameterizedWhereAndOrClause(args.txs);
if (args.txs.length === 0) return { results: [] };
let condition = '(tx_id, index_block_hash) = ANY(VALUES ';
let counter = 1;
const transactionValues = args.txs
.map(_ => {
const singleCondition = '($' + counter + '::bytea, $' + (counter + 1) + '::bytea)';
counter += 2;
return singleCondition;
})
.join(', ');
condition += transactionValues + ')';
// preparing values for condition
// conditionParams = [tx_id1, index_block_hash1, tx_id2, index_block_hash2]
const conditionParams: Buffer[] = [];
args.txs.forEach(transaction =>
conditionParams.push(hexToBuffer(transaction.txId), hexToBuffer(transaction.indexBlockHash))
);
const eventIndexStart = args.offset;
const eventIndexEnd = args.offset + args.limit - 1;
// preparing complete where clause condition
const paramEventIndexStart = args.txs.length * 2 + 1;
const paramEventIndexEnd = paramEventIndexStart + 1;
condition =
condition +
' AND microblock_canonical = true AND event_index BETWEEN $' +
paramEventIndexStart +
' AND $' +
paramEventIndexEnd;
const stxLockResults = await client.query<{
event_index: number;
tx_id: Buffer;
tx_index: number;
block_height: number;
canonical: boolean;
locked_amount: string;
unlock_height: string;
locked_address: string;
}>(
`
SELECT
event_index, tx_id, tx_index, block_height, canonical, locked_amount, unlock_height, locked_address
FROM stx_lock_events
WHERE ${condition}
`,
[...conditionParams, eventIndexStart, eventIndexEnd]
);
const stxResults = await client.query<{
event_index: number;
tx_id: Buffer;
tx_index: number;
block_height: number;
canonical: boolean;
asset_event_type_id: number;
sender?: string;
recipient?: string;
amount: string;
}>(
`
SELECT
event_index, tx_id, tx_index, block_height, canonical, asset_event_type_id, sender, recipient, amount
FROM stx_events
WHERE ${condition}
`,
[...conditionParams, eventIndexStart, eventIndexEnd]
);
const ftResults = await client.query<{
event_index: number;
tx_id: Buffer;
tx_index: number;
block_height: number;
canonical: boolean;
asset_event_type_id: number;
sender?: string;
recipient?: string;
asset_identifier: string;
amount: string;
}>(
`
SELECT
event_index, tx_id, tx_index, block_height, canonical, asset_event_type_id, sender, recipient, asset_identifier, amount
FROM ft_events
WHERE ${condition}
`,
[...conditionParams, eventIndexStart, eventIndexEnd]
);
const nftResults = await client.query<{
event_index: number;
tx_id: Buffer;
tx_index: number;
block_height: number;
canonical: boolean;
asset_event_type_id: number;
sender?: string;
recipient?: string;
asset_identifier: string;
value: Buffer;
}>(
`
SELECT
event_index, tx_id, tx_index, block_height, canonical, asset_event_type_id, sender, recipient, asset_identifier, value
FROM nft_events
WHERE ${condition}
`,
[...conditionParams, eventIndexStart, eventIndexEnd]
);
const logResults = await client.query<{
event_index: number;
tx_id: Buffer;
tx_index: number;
block_height: number;
canonical: boolean;
contract_identifier: string;
topic: string;
value: Buffer;
}>(
`
SELECT
event_index, tx_id, tx_index, block_height, canonical, contract_identifier, topic, value
FROM contract_logs
WHERE ${condition}
`,
[...conditionParams, eventIndexStart, eventIndexEnd]
);
return {
results: this.parseDbEvents(stxLockResults, stxResults, ftResults, nftResults, logResults),
};
});
}
async getTxEvents(args: { txId: string; indexBlockHash: string; limit: number; offset: number }) {
// Note: when this is used to fetch events for an unanchored microblock tx, the `indexBlockHash` is empty
// which will cause the sql queries to also match micro-orphaned tx data (resulting in duplicate event results).
@@ -3726,94 +3924,156 @@ export class PgDataStore
`,
[txIdBuffer, blockHashBuffer, eventIndexStart, eventIndexEnd]
);
const events = new Array<DbEvent>(
stxResults.rowCount +
nftResults.rowCount +
ftResults.rowCount +
logResults.rowCount +
stxLockResults.rowCount
);
let rowIndex = 0;
for (const result of stxLockResults.rows) {
const event: DbStxLockEvent = {
event_type: DbEventTypeId.StxLock,
event_index: result.event_index,
tx_id: bufferToHexPrefixString(result.tx_id),
tx_index: result.tx_index,
block_height: result.block_height,
canonical: result.canonical,
locked_amount: BigInt(result.locked_amount),
unlock_height: Number(result.unlock_height),
locked_address: result.locked_address,
};
events[rowIndex++] = event;
}
for (const result of stxResults.rows) {
const event: DbStxEvent = {
event_index: result.event_index,
tx_id: bufferToHexPrefixString(result.tx_id),
tx_index: result.tx_index,
block_height: result.block_height,
canonical: result.canonical,
asset_event_type_id: result.asset_event_type_id,
sender: result.sender,
recipient: result.recipient,
event_type: DbEventTypeId.StxAsset,
amount: BigInt(result.amount),
};
events[rowIndex++] = event;
}
for (const result of ftResults.rows) {
const event: DbFtEvent = {
event_index: result.event_index,
tx_id: bufferToHexPrefixString(result.tx_id),
tx_index: result.tx_index,
block_height: result.block_height,
canonical: result.canonical,
asset_event_type_id: result.asset_event_type_id,
sender: result.sender,
recipient: result.recipient,
asset_identifier: result.asset_identifier,
event_type: DbEventTypeId.FungibleTokenAsset,
amount: BigInt(result.amount),
};
events[rowIndex++] = event;
}
for (const result of nftResults.rows) {
const event: DbNftEvent = {
event_index: result.event_index,
tx_id: bufferToHexPrefixString(result.tx_id),
tx_index: result.tx_index,
block_height: result.block_height,
canonical: result.canonical,
asset_event_type_id: result.asset_event_type_id,
sender: result.sender,
recipient: result.recipient,
asset_identifier: result.asset_identifier,
event_type: DbEventTypeId.NonFungibleTokenAsset,
value: result.value,
};
events[rowIndex++] = event;
}
for (const result of logResults.rows) {
const event: DbSmartContractEvent = {
event_index: result.event_index,
tx_id: bufferToHexPrefixString(result.tx_id),
tx_index: result.tx_index,
block_height: result.block_height,
canonical: result.canonical,
event_type: DbEventTypeId.SmartContractLog,
contract_identifier: result.contract_identifier,
topic: result.topic,
value: result.value,
};
events[rowIndex++] = event;
}
events.sort((a, b) => a.event_index - b.event_index);
return { results: events };
return {
results: this.parseDbEvents(stxLockResults, stxResults, ftResults, nftResults, logResults),
};
});
}
parseDbEvents(
stxLockResults: QueryResult<{
event_index: number;
tx_id: Buffer;
tx_index: number;
block_height: number;
canonical: boolean;
locked_amount: string;
unlock_height: string;
locked_address: string;
}>,
stxResults: QueryResult<{
event_index: number;
tx_id: Buffer;
tx_index: number;
block_height: number;
canonical: boolean;
asset_event_type_id: number;
sender?: string | undefined;
recipient?: string | undefined;
amount: string;
}>,
ftResults: QueryResult<{
event_index: number;
tx_id: Buffer;
tx_index: number;
block_height: number;
canonical: boolean;
asset_event_type_id: number;
sender?: string | undefined;
recipient?: string | undefined;
asset_identifier: string;
amount: string;
}>,
nftResults: QueryResult<{
event_index: number;
tx_id: Buffer;
tx_index: number;
block_height: number;
canonical: boolean;
asset_event_type_id: number;
sender?: string | undefined;
recipient?: string | undefined;
asset_identifier: string;
value: Buffer;
}>,
logResults: QueryResult<{
event_index: number;
tx_id: Buffer;
tx_index: number;
block_height: number;
canonical: boolean;
contract_identifier: string;
topic: string;
value: Buffer;
}>
) {
const events = new Array<DbEvent>(
stxResults.rowCount +
nftResults.rowCount +
ftResults.rowCount +
logResults.rowCount +
stxLockResults.rowCount
);
let rowIndex = 0;
for (const result of stxLockResults.rows) {
const event: DbStxLockEvent = {
event_type: DbEventTypeId.StxLock,
event_index: result.event_index,
tx_id: bufferToHexPrefixString(result.tx_id),
tx_index: result.tx_index,
block_height: result.block_height,
canonical: result.canonical,
locked_amount: BigInt(result.locked_amount),
unlock_height: Number(result.unlock_height),
locked_address: result.locked_address,
};
events[rowIndex++] = event;
}
for (const result of stxResults.rows) {
const event: DbStxEvent = {
event_index: result.event_index,
tx_id: bufferToHexPrefixString(result.tx_id),
tx_index: result.tx_index,
block_height: result.block_height,
canonical: result.canonical,
asset_event_type_id: result.asset_event_type_id,
sender: result.sender,
recipient: result.recipient,
event_type: DbEventTypeId.StxAsset,
amount: BigInt(result.amount),
};
events[rowIndex++] = event;
}
for (const result of ftResults.rows) {
const event: DbFtEvent = {
event_index: result.event_index,
tx_id: bufferToHexPrefixString(result.tx_id),
tx_index: result.tx_index,
block_height: result.block_height,
canonical: result.canonical,
asset_event_type_id: result.asset_event_type_id,
sender: result.sender,
recipient: result.recipient,
asset_identifier: result.asset_identifier,
event_type: DbEventTypeId.FungibleTokenAsset,
amount: BigInt(result.amount),
};
events[rowIndex++] = event;
}
for (const result of nftResults.rows) {
const event: DbNftEvent = {
event_index: result.event_index,
tx_id: bufferToHexPrefixString(result.tx_id),
tx_index: result.tx_index,
block_height: result.block_height,
canonical: result.canonical,
asset_event_type_id: result.asset_event_type_id,
sender: result.sender,
recipient: result.recipient,
asset_identifier: result.asset_identifier,
event_type: DbEventTypeId.NonFungibleTokenAsset,
value: result.value,
};
events[rowIndex++] = event;
}
for (const result of logResults.rows) {
const event: DbSmartContractEvent = {
event_index: result.event_index,
tx_id: bufferToHexPrefixString(result.tx_id),
tx_index: result.tx_index,
block_height: result.block_height,
canonical: result.canonical,
event_type: DbEventTypeId.SmartContractLog,
contract_identifier: result.contract_identifier,
topic: result.topic,
value: result.value,
};
events[rowIndex++] = event;
}
events.sort((a, b) => a.event_index - b.event_index);
return events;
}
async updateStxLockEvent(client: ClientBase, tx: DbTx, event: DbStxLockEvent) {
await client.query(
`
@@ -4269,6 +4529,31 @@ export class PgDataStore
);
}
async getSmartContractList(contractIds: string[]) {
return this.query(async client => {
const result = await client.query<{
tx_id: Buffer;
canonical: boolean;
contract_id: string;
block_height: number;
source_code: string;
abi: string;
}>(
`
SELECT tx_id, canonical, contract_id, block_height, source_code, abi
FROM smart_contracts
WHERE contract_id = ANY($1)
ORDER BY abi != 'null' DESC, canonical DESC, microblock_canonical DESC, block_height DESC
`,
[contractIds]
);
if (result.rowCount === 0) {
[];
}
return result.rows.map(r => this.parseQueryResultToSmartContract(r)).map(res => res.result);
});
}
async getSmartContract(contractId: string) {
return this.query(async client => {
const result = await client.query<{
@@ -4292,18 +4577,29 @@ export class PgDataStore
return { found: false } as const;
}
const row = result.rows[0];
const smartContract: DbSmartContract = {
tx_id: bufferToHexPrefixString(row.tx_id),
canonical: row.canonical,
contract_id: row.contract_id,
block_height: row.block_height,
source_code: row.source_code,
abi: row.abi,
};
return { found: true, result: smartContract };
return this.parseQueryResultToSmartContract(row);
});
}
parseQueryResultToSmartContract(row: {
tx_id: Buffer;
canonical: boolean;
contract_id: string;
block_height: number;
source_code: string;
abi: string;
}) {
const smartContract: DbSmartContract = {
tx_id: bufferToHexPrefixString(row.tx_id),
canonical: row.canonical,
contract_id: row.contract_id,
block_height: row.block_height,
source_code: row.source_code,
abi: row.abi,
};
return { found: true, result: smartContract };
}
async getSmartContractEvents({
contractId,
limit,
@@ -5636,6 +5932,33 @@ export class PgDataStore
);
}
async getTxListDetails({
txIds,
includeUnanchored,
}: {
txIds: string[];
includeUnanchored: boolean;
}) {
return this.queryTx(async client => {
const values = txIds.map(id => hexToBuffer(id));
const maxBlockHeight = await this.getMaxBlockHeight(client, { includeUnanchored });
const result = await client.query<TxQueryResult>(
`
SELECT ${TX_COLUMNS}
FROM txs
WHERE tx_id = ANY($1) AND block_height <= $2 AND canonical = true AND microblock_canonical = true
`,
[values, maxBlockHeight]
);
if (result.rowCount === 0) {
return [];
}
return result.rows.map(row => {
return this.parseTxQueryResult(row);
});
});
}
async getConfigState(): Promise<DbConfigState> {
const queryResult = await this.pool.query(`SELECT * FROM config_state`);
const result: DbConfigState = {

View File

@@ -63,6 +63,7 @@ export async function up(pgm: MigrationBuilder): Promise<void> {
pgm.createIndex('stx_lock_events', 'tx_id');
pgm.createIndex('stx_lock_events', 'block_height');
pgm.createIndex('stx_lock_events', 'index_block_hash');
pgm.createIndex('stx_lock_events', ['index_block_hash', 'tx_id']);
pgm.createIndex('stx_lock_events', 'canonical');
pgm.createIndex('stx_lock_events', 'microblock_canonical');
pgm.createIndex('stx_lock_events', 'locked_address');

View File

@@ -700,6 +700,109 @@ describe('api tests', () => {
expect(JSON.parse(rewardResult.text)).toEqual(expectedResp1);
});
test('fetch tx list details', async () => {
const mempoolTx: DbMempoolTx = {
pruned: false,
tx_id: '0x8912000000000000000000000000000000000000000000000000000000000000',
anchor_mode: 3,
nonce: 0,
raw_tx: Buffer.from('test-raw-tx'),
type_id: DbTxTypeId.Coinbase,
status: DbTxStatus.Pending,
receipt_time: 1594307695,
coinbase_payload: Buffer.from('coinbase hi'),
post_conditions: Buffer.from([0x01, 0xf5]),
fee_rate: 1234n,
sponsored: false,
sponsor_address: undefined,
sender_address: 'sender-addr',
origin_hash_mode: 1,
};
await db.updateMempoolTxs({ mempoolTxs: [mempoolTx] });
const dbTx: DbTx = {
tx_id: '0x8911000000000000000000000000000000000000000000000000000000000000',
anchor_mode: 3,
nonce: 0,
raw_tx: Buffer.from('test-raw-tx'),
type_id: DbTxTypeId.Coinbase,
coinbase_payload: Buffer.from('coinbase hi'),
post_conditions: Buffer.from([0x01, 0xf5]),
fee_rate: 1234n,
sponsored: true,
sender_address: 'sender-addr',
sponsor_address: 'sponsor-addr',
origin_hash_mode: 1,
block_hash: '0x0123',
index_block_hash: '0x1234',
parent_block_hash: '0x5678',
block_height: 0,
burn_block_time: 39486,
parent_burn_block_time: 1626122935,
tx_index: 4,
status: DbTxStatus.Success,
raw_result: '0x0100000000000000000000000000000001', // u1
canonical: true,
microblock_canonical: true,
microblock_sequence: I32_MAX,
microblock_hash: '',
parent_index_block_hash: '',
event_count: 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,
};
const dbTx2: DbTx = {
tx_id: '0x8915000000000000000000000000000000000000000000000000000000000000',
anchor_mode: 3,
nonce: 1000,
raw_tx: Buffer.from('test-raw-tx'),
type_id: DbTxTypeId.Coinbase,
coinbase_payload: Buffer.from('coinbase hi'),
post_conditions: Buffer.from([0x01, 0xf5]),
fee_rate: 1234n,
sponsored: true,
sender_address: 'sender-addr',
sponsor_address: 'sponsor-addr',
origin_hash_mode: 1,
block_hash: '0x0123',
index_block_hash: '0x1234',
parent_block_hash: '0x5678',
block_height: 0,
burn_block_time: 39486,
parent_burn_block_time: 1626122935,
tx_index: 4,
status: DbTxStatus.Success,
raw_result: '0x0100000000000000000000000000000001', // u1
canonical: true,
microblock_canonical: true,
microblock_sequence: I32_MAX,
microblock_hash: '',
parent_index_block_hash: '',
event_count: 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,
};
await db.updateTx(client, dbTx);
await db.updateTx(client, dbTx2);
const notFoundTxId = '0x8914000000000000000000000000000000000000000000000000000000000000';
const txsListDetail = await supertest(api.server).get(
`/extended/v1/tx/multiple?tx_id=${mempoolTx.tx_id}&tx_id=${dbTx.tx_id}&tx_id=${notFoundTxId}&tx_id=${dbTx2.tx_id}`
);
const jsonRes = txsListDetail.body;
// tx comparison
expect(jsonRes[mempoolTx.tx_id].result.tx_id).toEqual(mempoolTx.tx_id);
expect(jsonRes[dbTx.tx_id].result.tx_id).toEqual(dbTx.tx_id);
// mempool tx comparison
expect(jsonRes[notFoundTxId].result.tx_id).toEqual(notFoundTxId);
// not found comparison
expect(jsonRes[dbTx2.tx_id].result.tx_id).toEqual(dbTx2.tx_id);
});
test('fetch mempool-tx', async () => {
const mempoolTx: DbMempoolTx = {
pruned: false,

View File

@@ -3844,7 +3844,7 @@ describe('postgres datastore', () => {
expect(t3.result?.canonical).toBe(true);
const sc1 = await db.getSmartContract(contract1.contract_id);
expect(sc1.result?.canonical).toBe(true);
expect(sc1.found && sc1.result?.canonical).toBe(true);
});
test('pg get raw tx', async () => {