mirror of
https://github.com/alexgo-io/stacks-blockchain-api.git
synced 2026-01-12 22:43:34 +08:00
feat: endpoint for list of transactions #647
This commit is contained in:
24
docs/entities/transactions/transaction-found.schema.json
Normal file
24
docs/entities/transactions/transaction-found.schema.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
15
docs/entities/transactions/transaction-list.schema.json
Normal file
15
docs/entities/transactions/transaction-list.schema.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
24
docs/entities/transactions/transaction-not-found.schema.json
Normal file
24
docs/entities/transactions/transaction-not-found.schema.json
Normal 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"]
|
||||
}
|
||||
@@ -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
22
docs/generated.d.ts
vendored
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user