mirror of
https://github.com/alexgo-io/stacks-blockchain-api.git
synced 2026-05-11 10:14:21 +08:00
864 lines
26 KiB
TypeScript
864 lines
26 KiB
TypeScript
import {
|
|
abiFunctionToString,
|
|
BufferReader,
|
|
ClarityAbi,
|
|
cvToString,
|
|
deserializeCV,
|
|
getCVTypeString,
|
|
getTypeString,
|
|
serializeCV,
|
|
} from '@stacks/transactions';
|
|
|
|
import {
|
|
Block,
|
|
ContractCallTransaction,
|
|
MempoolTransaction,
|
|
MempoolTransactionStatus,
|
|
RosettaBlock,
|
|
RosettaParentBlockIdentifier,
|
|
RosettaTransaction,
|
|
SmartContractTransaction,
|
|
Transaction,
|
|
TransactionAnchorModeType,
|
|
TransactionEvent,
|
|
TransactionEventFungibleAsset,
|
|
TransactionEventNonFungibleAsset,
|
|
TransactionEventSmartContractLog,
|
|
TransactionEventStxAsset,
|
|
TransactionEventStxLock,
|
|
TransactionStatus,
|
|
TransactionType,
|
|
} from '@stacks/stacks-blockchain-api-types';
|
|
|
|
import {
|
|
DataStore,
|
|
DbAssetEventTypeId,
|
|
DbBlock,
|
|
DbEvent,
|
|
DbEventTypeId,
|
|
DbMempoolTx,
|
|
DbMicroblock,
|
|
DbTx,
|
|
DbTxStatus,
|
|
DbTxTypeId,
|
|
} from '../../datastore/common';
|
|
import {
|
|
assertNotNullish as unwrapOptional,
|
|
bufferToHexPrefixString,
|
|
ElementType,
|
|
FoundOrNot,
|
|
hexToBuffer,
|
|
logger,
|
|
unixEpochToIso,
|
|
} from '../../helpers';
|
|
import { readClarityValueArray, readTransactionPostConditions } from '../../p2p/tx';
|
|
import { serializePostCondition, serializePostConditionMode } from '../serializers/post-conditions';
|
|
import { getMinerOperations, getOperations, processEvents } from '../../rosetta-helpers';
|
|
|
|
export function parseTxTypeStrings(values: string[]): TransactionType[] {
|
|
return values.map(v => {
|
|
switch (v) {
|
|
case 'contract_call':
|
|
case 'smart_contract':
|
|
case 'token_transfer':
|
|
case 'coinbase':
|
|
case 'poison_microblock':
|
|
return v;
|
|
default:
|
|
throw new Error(`Unexpected tx type: ${JSON.stringify(v)}`);
|
|
}
|
|
});
|
|
}
|
|
|
|
export function getTxTypeString(typeId: DbTxTypeId): Transaction['tx_type'] {
|
|
switch (typeId) {
|
|
case DbTxTypeId.TokenTransfer:
|
|
return 'token_transfer';
|
|
case DbTxTypeId.SmartContract:
|
|
return 'smart_contract';
|
|
case DbTxTypeId.ContractCall:
|
|
return 'contract_call';
|
|
case DbTxTypeId.PoisonMicroblock:
|
|
return 'poison_microblock';
|
|
case DbTxTypeId.Coinbase:
|
|
return 'coinbase';
|
|
default:
|
|
throw new Error(`Unexpected DbTxTypeId: ${typeId}`);
|
|
}
|
|
}
|
|
|
|
export function getTxAnchorModeString(anchorMode: number): TransactionAnchorModeType {
|
|
switch (anchorMode) {
|
|
case 0x01:
|
|
return 'on_chain_only';
|
|
case 0x02:
|
|
return 'off_chain_only';
|
|
case 0x03:
|
|
return 'any';
|
|
default:
|
|
throw new Error(`Unexpected anchor mode value ${anchorMode}`);
|
|
}
|
|
}
|
|
|
|
export function getTxTypeId(typeString: Transaction['tx_type']): DbTxTypeId {
|
|
switch (typeString) {
|
|
case 'token_transfer':
|
|
return DbTxTypeId.TokenTransfer;
|
|
case 'smart_contract':
|
|
return DbTxTypeId.SmartContract;
|
|
case 'contract_call':
|
|
return DbTxTypeId.ContractCall;
|
|
case 'poison_microblock':
|
|
return DbTxTypeId.PoisonMicroblock;
|
|
case 'coinbase':
|
|
return DbTxTypeId.Coinbase;
|
|
default:
|
|
throw new Error(`Unexpected tx type string: ${typeString}`);
|
|
}
|
|
}
|
|
|
|
export function getTxStatusString(
|
|
txStatus: DbTxStatus
|
|
): TransactionStatus | MempoolTransactionStatus {
|
|
switch (txStatus) {
|
|
case DbTxStatus.Pending:
|
|
return 'pending';
|
|
case DbTxStatus.Success:
|
|
return 'success';
|
|
case DbTxStatus.AbortByResponse:
|
|
return 'abort_by_response';
|
|
case DbTxStatus.AbortByPostCondition:
|
|
return 'abort_by_post_condition';
|
|
case DbTxStatus.DroppedReplaceByFee:
|
|
return 'dropped_replace_by_fee';
|
|
case DbTxStatus.DroppedReplaceAcrossFork:
|
|
return 'dropped_replace_across_fork';
|
|
case DbTxStatus.DroppedTooExpensive:
|
|
return 'dropped_too_expensive';
|
|
case DbTxStatus.DroppedStaleGarbageCollect:
|
|
return 'dropped_stale_garbage_collect';
|
|
default:
|
|
throw new Error(`Unexpected DbTxStatus: ${txStatus}`);
|
|
}
|
|
}
|
|
|
|
export function getTxStatus(txStatus: DbTxStatus | string): string {
|
|
if (txStatus == '') {
|
|
return '';
|
|
} else {
|
|
return getTxStatusString(txStatus as DbTxStatus);
|
|
}
|
|
}
|
|
|
|
type HasEventTransaction = SmartContractTransaction | ContractCallTransaction;
|
|
|
|
export function getEventTypeString(
|
|
eventTypeId: DbEventTypeId
|
|
): ElementType<Exclude<HasEventTransaction['events'], undefined>>['event_type'] {
|
|
switch (eventTypeId) {
|
|
case DbEventTypeId.SmartContractLog:
|
|
return 'smart_contract_log';
|
|
case DbEventTypeId.StxAsset:
|
|
return 'stx_asset';
|
|
case DbEventTypeId.FungibleTokenAsset:
|
|
return 'fungible_token_asset';
|
|
case DbEventTypeId.NonFungibleTokenAsset:
|
|
return 'non_fungible_token_asset';
|
|
case DbEventTypeId.StxLock:
|
|
return 'stx_lock';
|
|
default:
|
|
throw new Error(`Unexpected DbEventTypeId: ${eventTypeId}`);
|
|
}
|
|
}
|
|
|
|
export function getAssetEventTypeString(
|
|
assetEventTypeId: DbAssetEventTypeId
|
|
): 'transfer' | 'mint' | 'burn' {
|
|
switch (assetEventTypeId) {
|
|
case DbAssetEventTypeId.Transfer:
|
|
return 'transfer';
|
|
case DbAssetEventTypeId.Mint:
|
|
return 'mint';
|
|
case DbAssetEventTypeId.Burn:
|
|
return 'burn';
|
|
default:
|
|
throw new Error(`Unexpected DbAssetEventTypeId: ${assetEventTypeId}`);
|
|
}
|
|
}
|
|
|
|
export function parseDbEvent(dbEvent: DbEvent): TransactionEvent {
|
|
switch (dbEvent.event_type) {
|
|
case DbEventTypeId.SmartContractLog: {
|
|
const valueBuffer = dbEvent.value;
|
|
const valueHex = bufferToHexPrefixString(valueBuffer);
|
|
const valueRepr = cvToString(deserializeCV(valueBuffer));
|
|
const event: TransactionEventSmartContractLog = {
|
|
event_index: dbEvent.event_index,
|
|
event_type: 'smart_contract_log',
|
|
contract_log: {
|
|
contract_id: dbEvent.contract_identifier,
|
|
topic: dbEvent.topic,
|
|
value: { hex: valueHex, repr: valueRepr },
|
|
},
|
|
};
|
|
return event;
|
|
}
|
|
case DbEventTypeId.StxLock: {
|
|
const event: TransactionEventStxLock = {
|
|
event_index: dbEvent.event_index,
|
|
event_type: 'stx_lock',
|
|
stx_lock_event: {
|
|
locked_amount: dbEvent.locked_amount.toString(10),
|
|
unlock_height: Number(dbEvent.unlock_height),
|
|
locked_address: dbEvent.locked_address,
|
|
},
|
|
};
|
|
return event;
|
|
}
|
|
case DbEventTypeId.StxAsset: {
|
|
const event: TransactionEventStxAsset = {
|
|
event_index: dbEvent.event_index,
|
|
event_type: 'stx_asset',
|
|
asset: {
|
|
asset_event_type: getAssetEventTypeString(dbEvent.asset_event_type_id),
|
|
sender: dbEvent.sender || '',
|
|
recipient: dbEvent.recipient || '',
|
|
amount: dbEvent.amount.toString(10),
|
|
},
|
|
};
|
|
return event;
|
|
}
|
|
case DbEventTypeId.FungibleTokenAsset: {
|
|
const event: TransactionEventFungibleAsset = {
|
|
event_index: dbEvent.event_index,
|
|
event_type: 'fungible_token_asset',
|
|
asset: {
|
|
asset_event_type: getAssetEventTypeString(dbEvent.asset_event_type_id),
|
|
asset_id: dbEvent.asset_identifier,
|
|
sender: dbEvent.sender || '',
|
|
recipient: dbEvent.recipient || '',
|
|
amount: dbEvent.amount.toString(10),
|
|
},
|
|
};
|
|
return event;
|
|
}
|
|
case DbEventTypeId.NonFungibleTokenAsset: {
|
|
const valueBuffer = dbEvent.value;
|
|
const valueHex = bufferToHexPrefixString(valueBuffer);
|
|
const valueRepr = cvToString(deserializeCV(valueBuffer));
|
|
const event: TransactionEventNonFungibleAsset = {
|
|
event_index: dbEvent.event_index,
|
|
event_type: 'non_fungible_token_asset',
|
|
asset: {
|
|
asset_event_type: getAssetEventTypeString(dbEvent.asset_event_type_id),
|
|
asset_id: dbEvent.asset_identifier,
|
|
sender: dbEvent.sender || '',
|
|
recipient: dbEvent.recipient || '',
|
|
value: {
|
|
hex: valueHex,
|
|
repr: valueRepr,
|
|
},
|
|
},
|
|
};
|
|
return event;
|
|
}
|
|
default:
|
|
throw new Error(`Unexpected event_type in: ${JSON.stringify(dbEvent)}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fetch block from datastore by blockHash or blockHeight (index)
|
|
* If both blockHeight and blockHash are provided, blockHeight is used.
|
|
* If neither argument is present, the most recent block is returned.
|
|
* @param db -- datastore
|
|
* @param fetchTransactions -- return block transactions
|
|
* @param blockHash -- hexadecimal hash string
|
|
* @param blockHeight -- number
|
|
*/
|
|
export async function getRosettaBlockFromDataStore(
|
|
db: DataStore,
|
|
fetchTransactions: boolean,
|
|
blockHash?: string,
|
|
blockHeight?: number
|
|
): Promise<FoundOrNot<RosettaBlock>> {
|
|
let query;
|
|
if (blockHash) {
|
|
query = db.getBlock({ hash: blockHash });
|
|
} else if (blockHeight && blockHeight > 0) {
|
|
query = db.getBlock({ height: blockHeight });
|
|
} else {
|
|
query = db.getCurrentBlock();
|
|
}
|
|
const blockQuery = await query;
|
|
|
|
if (!blockQuery.found) {
|
|
return { found: false };
|
|
}
|
|
const dbBlock = blockQuery.result;
|
|
let blockTxs = {} as FoundOrNot<RosettaTransaction[]>;
|
|
blockTxs.found = false;
|
|
if (fetchTransactions) {
|
|
blockTxs = await getRosettaBlockTransactionsFromDataStore(
|
|
dbBlock.block_hash,
|
|
dbBlock.index_block_hash,
|
|
db
|
|
);
|
|
}
|
|
|
|
const parentBlockHash = dbBlock.parent_block_hash;
|
|
let parent_block_identifier: RosettaParentBlockIdentifier;
|
|
|
|
if (dbBlock.block_height <= 1) {
|
|
// case for genesis block
|
|
parent_block_identifier = {
|
|
index: dbBlock.block_height,
|
|
hash: dbBlock.block_hash,
|
|
};
|
|
} else {
|
|
const parentBlockQuery = await db.getBlock({ hash: parentBlockHash });
|
|
if (parentBlockQuery.found) {
|
|
const parentBlock = parentBlockQuery.result;
|
|
parent_block_identifier = {
|
|
index: parentBlock.block_height,
|
|
hash: parentBlock.block_hash,
|
|
};
|
|
} else {
|
|
return { found: false };
|
|
}
|
|
}
|
|
|
|
const apiBlock: RosettaBlock = {
|
|
block_identifier: { index: dbBlock.block_height, hash: dbBlock.block_hash },
|
|
parent_block_identifier,
|
|
timestamp: dbBlock.burn_block_time * 1000,
|
|
transactions: blockTxs.found ? blockTxs.result : [],
|
|
};
|
|
return { found: true, result: apiBlock };
|
|
}
|
|
|
|
export async function getBlockFromDataStore({
|
|
blockIdentifer,
|
|
db,
|
|
}: {
|
|
blockIdentifer: { hash: string } | { height: number };
|
|
db: DataStore;
|
|
}): Promise<FoundOrNot<Block>> {
|
|
const blockQuery = await db.getBlockWithMetadata(blockIdentifer, {
|
|
txs: true,
|
|
microblocks: true,
|
|
});
|
|
if (!blockQuery.found) {
|
|
return { found: false };
|
|
}
|
|
const result = blockQuery.result;
|
|
const apiBlock = parseDbBlock(
|
|
result.block,
|
|
result.txs.map(tx => tx.tx_id),
|
|
result.microblocks.accepted.map(mb => mb.microblock_hash),
|
|
result.microblocks.streamed.map(mb => mb.microblock_hash)
|
|
);
|
|
return { found: true, result: apiBlock };
|
|
}
|
|
|
|
export function parseDbBlock(
|
|
dbBlock: DbBlock,
|
|
txIds: string[],
|
|
microblocksAccepted: string[],
|
|
microblocksStreamed: string[]
|
|
): Block {
|
|
const apiBlock: Block = {
|
|
canonical: dbBlock.canonical,
|
|
height: dbBlock.block_height,
|
|
hash: dbBlock.block_hash,
|
|
parent_block_hash: dbBlock.parent_block_hash,
|
|
burn_block_time: dbBlock.burn_block_time,
|
|
burn_block_time_iso: unixEpochToIso(dbBlock.burn_block_time),
|
|
burn_block_hash: dbBlock.burn_block_hash,
|
|
burn_block_height: dbBlock.burn_block_height,
|
|
miner_txid: dbBlock.miner_txid,
|
|
parent_microblock_hash: dbBlock.parent_microblock_hash,
|
|
parent_microblock_sequence: dbBlock.parent_microblock_sequence,
|
|
txs: [...txIds],
|
|
microblocks_accepted: [...microblocksAccepted],
|
|
microblocks_streamed: [...microblocksStreamed],
|
|
};
|
|
return apiBlock;
|
|
}
|
|
|
|
export async function getRosettaBlockTransactionsFromDataStore(
|
|
blockHash: string,
|
|
indexBlockHash: string,
|
|
db: DataStore
|
|
): Promise<FoundOrNot<RosettaTransaction[]>> {
|
|
const blockQuery = await db.getBlock({ hash: blockHash });
|
|
if (!blockQuery.found) {
|
|
return { found: false };
|
|
}
|
|
|
|
const txsQuery = await db.getBlockTxsRows(blockHash);
|
|
const minerRewards = await db.getMinerRewards({
|
|
blockHeight: blockQuery.result.block_height,
|
|
});
|
|
|
|
if (!txsQuery.found) {
|
|
return { found: false };
|
|
}
|
|
|
|
const transactions: RosettaTransaction[] = [];
|
|
|
|
for (const tx of txsQuery.result) {
|
|
let events: DbEvent[] = [];
|
|
if (blockQuery.result.block_height > 1) {
|
|
// only return events of blocks at height greater than 1
|
|
const eventsQuery = await db.getTxEvents({
|
|
txId: tx.tx_id,
|
|
indexBlockHash: indexBlockHash,
|
|
limit: 5000,
|
|
offset: 0,
|
|
});
|
|
events = eventsQuery.results;
|
|
}
|
|
|
|
const operations = getOperations(tx, minerRewards, events);
|
|
|
|
transactions.push({
|
|
transaction_identifier: { hash: tx.tx_id },
|
|
operations: operations,
|
|
});
|
|
}
|
|
|
|
return { found: true, result: transactions };
|
|
}
|
|
|
|
export async function getRosettaTransactionFromDataStore(
|
|
txId: string,
|
|
db: DataStore
|
|
): Promise<FoundOrNot<RosettaTransaction>> {
|
|
const txQuery = await db.getTx(txId);
|
|
if (!txQuery.found) {
|
|
return { found: false };
|
|
}
|
|
const operations = getOperations(txQuery.result);
|
|
const result = {
|
|
transaction_identifier: { hash: txId },
|
|
operations: operations,
|
|
};
|
|
return { found: true, result: result };
|
|
}
|
|
|
|
export function parseDbMempoolTx(dbTx: DbMempoolTx): MempoolTransaction {
|
|
const apiTx: Partial<MempoolTransaction> = {
|
|
tx_id: dbTx.tx_id,
|
|
tx_status: getTxStatusString(dbTx.status) as 'pending',
|
|
tx_type: getTxTypeString(dbTx.type_id),
|
|
receipt_time: dbTx.receipt_time,
|
|
receipt_time_iso: unixEpochToIso(dbTx.receipt_time),
|
|
|
|
nonce: dbTx.nonce,
|
|
fee_rate: dbTx.fee_rate.toString(10),
|
|
sender_address: dbTx.sender_address,
|
|
sponsored: dbTx.sponsored,
|
|
sponsor_address: dbTx.sponsor_address,
|
|
|
|
post_condition_mode: serializePostConditionMode(dbTx.post_conditions.readUInt8(0)),
|
|
};
|
|
|
|
switch (apiTx.tx_type) {
|
|
case 'token_transfer': {
|
|
apiTx.token_transfer = {
|
|
recipient_address: unwrapOptional(
|
|
dbTx.token_transfer_recipient_address,
|
|
() => 'Unexpected nullish token_transfer_recipient_address'
|
|
),
|
|
amount: unwrapOptional(
|
|
dbTx.token_transfer_amount,
|
|
() => 'Unexpected nullish token_transfer_amount'
|
|
).toString(10),
|
|
memo: bufferToHexPrefixString(
|
|
unwrapOptional(dbTx.token_transfer_memo, () => 'Unexpected nullish token_transfer_memo')
|
|
),
|
|
};
|
|
break;
|
|
}
|
|
case 'smart_contract': {
|
|
const postConditions = readTransactionPostConditions(
|
|
BufferReader.fromBuffer(dbTx.post_conditions.slice(1))
|
|
);
|
|
apiTx.post_conditions = postConditions.map(pc => serializePostCondition(pc));
|
|
apiTx.smart_contract = {
|
|
contract_id: unwrapOptional(
|
|
dbTx.smart_contract_contract_id,
|
|
() => 'Unexpected nullish smart_contract_contract_id'
|
|
),
|
|
source_code: unwrapOptional(
|
|
dbTx.smart_contract_source_code,
|
|
() => 'Unexpected nullish smart_contract_source_code'
|
|
),
|
|
};
|
|
break;
|
|
}
|
|
case 'contract_call': {
|
|
const postConditions = readTransactionPostConditions(
|
|
BufferReader.fromBuffer(dbTx.post_conditions.slice(1))
|
|
);
|
|
const contractId = unwrapOptional(
|
|
dbTx.contract_call_contract_id,
|
|
() => 'Unexpected nullish contract_call_contract_id'
|
|
);
|
|
const functionName = unwrapOptional(
|
|
dbTx.contract_call_function_name,
|
|
() => 'Unexpected nullish contract_call_function_name'
|
|
);
|
|
apiTx.post_conditions = postConditions.map(pc => serializePostCondition(pc));
|
|
apiTx.contract_call = { contract_id: contractId, function_name: functionName };
|
|
break;
|
|
}
|
|
case 'poison_microblock': {
|
|
apiTx.poison_microblock = {
|
|
microblock_header_1: bufferToHexPrefixString(
|
|
unwrapOptional(dbTx.poison_microblock_header_1)
|
|
),
|
|
microblock_header_2: bufferToHexPrefixString(
|
|
unwrapOptional(dbTx.poison_microblock_header_2)
|
|
),
|
|
};
|
|
break;
|
|
}
|
|
case 'coinbase': {
|
|
apiTx.coinbase_payload = {
|
|
data: bufferToHexPrefixString(
|
|
unwrapOptional(dbTx.coinbase_payload, () => 'Unexpected nullish coinbase_payload')
|
|
),
|
|
};
|
|
break;
|
|
}
|
|
default:
|
|
throw new Error(`Unexpected DbTxTypeId: ${dbTx.type_id}`);
|
|
}
|
|
return apiTx as MempoolTransaction;
|
|
}
|
|
|
|
export interface GetTxArgs {
|
|
txId: string;
|
|
}
|
|
|
|
export interface GetTxWithEventsArgs extends GetTxArgs {
|
|
eventLimit: number;
|
|
eventOffset: number;
|
|
}
|
|
|
|
export function parseDbTx(dbTx: DbTx): Transaction {
|
|
const tx: Partial<Transaction> = {
|
|
tx_id: dbTx.tx_id,
|
|
tx_type: getTxTypeString(dbTx.type_id),
|
|
|
|
nonce: dbTx.nonce,
|
|
fee_rate: dbTx.fee_rate.toString(10),
|
|
sender_address: dbTx.sender_address,
|
|
sponsored: dbTx.sponsored,
|
|
sponsor_address: dbTx.sponsor_address,
|
|
|
|
post_condition_mode: serializePostConditionMode(dbTx.post_conditions.readUInt8(0)),
|
|
|
|
tx_status: getTxStatusString(dbTx.status) as TransactionStatus,
|
|
|
|
block_hash: dbTx.block_hash,
|
|
block_height: dbTx.block_height,
|
|
burn_block_time: dbTx.burn_block_time,
|
|
burn_block_time_iso: unixEpochToIso(dbTx.burn_block_time),
|
|
canonical: dbTx.canonical,
|
|
tx_index: dbTx.tx_index,
|
|
event_count: dbTx.event_count,
|
|
};
|
|
if (dbTx.raw_result) {
|
|
tx.tx_result = {
|
|
hex: dbTx.raw_result,
|
|
repr: cvToString(deserializeCV(hexToBuffer(dbTx.raw_result))),
|
|
};
|
|
}
|
|
switch (tx.tx_type) {
|
|
case 'token_transfer': {
|
|
tx.token_transfer = {
|
|
recipient_address: unwrapOptional(
|
|
dbTx.token_transfer_recipient_address,
|
|
() => 'Unexpected nullish token_transfer_recipient_address'
|
|
),
|
|
amount: unwrapOptional(
|
|
dbTx.token_transfer_amount,
|
|
() => 'Unexpected nullish token_transfer_amount'
|
|
).toString(10),
|
|
memo: bufferToHexPrefixString(
|
|
unwrapOptional(dbTx.token_transfer_memo, () => 'Unexpected nullish token_transfer_memo')
|
|
),
|
|
};
|
|
break;
|
|
}
|
|
case 'smart_contract': {
|
|
const postConditions = readTransactionPostConditions(
|
|
BufferReader.fromBuffer(dbTx.post_conditions.slice(1))
|
|
);
|
|
tx.post_conditions = postConditions.map(pc => serializePostCondition(pc));
|
|
tx.smart_contract = {
|
|
contract_id: unwrapOptional(
|
|
dbTx.smart_contract_contract_id,
|
|
() => 'Unexpected nullish smart_contract_contract_id'
|
|
),
|
|
source_code: unwrapOptional(
|
|
dbTx.smart_contract_source_code,
|
|
() => 'Unexpected nullish smart_contract_source_code'
|
|
),
|
|
};
|
|
break;
|
|
}
|
|
case 'contract_call': {
|
|
const postConditions = readTransactionPostConditions(
|
|
BufferReader.fromBuffer(dbTx.post_conditions.slice(1))
|
|
);
|
|
const contractId = unwrapOptional(
|
|
dbTx.contract_call_contract_id,
|
|
() => 'Unexpected nullish contract_call_contract_id'
|
|
);
|
|
const functionName = unwrapOptional(
|
|
dbTx.contract_call_function_name,
|
|
() => 'Unexpected nullish contract_call_function_name'
|
|
);
|
|
tx.post_conditions = postConditions.map(pc => serializePostCondition(pc));
|
|
tx.contract_call = {
|
|
contract_id: contractId,
|
|
function_name: functionName,
|
|
function_signature: '',
|
|
};
|
|
if (dbTx.contract_call_function_args) {
|
|
tx.contract_call.function_args = readClarityValueArray(
|
|
dbTx.contract_call_function_args
|
|
).map(c => {
|
|
return {
|
|
hex: bufferToHexPrefixString(serializeCV(c)),
|
|
repr: cvToString(c),
|
|
name: '',
|
|
type: getCVTypeString(c),
|
|
};
|
|
});
|
|
}
|
|
break;
|
|
}
|
|
case 'poison_microblock': {
|
|
tx.poison_microblock = {
|
|
microblock_header_1: bufferToHexPrefixString(
|
|
unwrapOptional(dbTx.poison_microblock_header_1)
|
|
),
|
|
microblock_header_2: bufferToHexPrefixString(
|
|
unwrapOptional(dbTx.poison_microblock_header_2)
|
|
),
|
|
};
|
|
break;
|
|
}
|
|
case 'coinbase': {
|
|
tx.coinbase_payload = {
|
|
data: bufferToHexPrefixString(
|
|
unwrapOptional(dbTx.coinbase_payload, () => 'Unexpected nullish coinbase_payload')
|
|
),
|
|
};
|
|
break;
|
|
}
|
|
default:
|
|
throw new Error(`Unexpected DbTxTypeId: ${dbTx.type_id}`);
|
|
}
|
|
return tx as Transaction;
|
|
}
|
|
|
|
export async function getTxFromDataStore(
|
|
db: DataStore,
|
|
args: GetTxArgs | GetTxWithEventsArgs
|
|
): Promise<FoundOrNot<Transaction>> {
|
|
let dbTx: DbTx | DbMempoolTx;
|
|
let dbTxEvents: DbEvent[] = [];
|
|
let eventCount = 0;
|
|
|
|
const txQuery = await db.getTx(args.txId);
|
|
const mempoolTxQuery = await db.getMempoolTx({ txId: args.txId, includePruned: true });
|
|
// First, check the happy path: the tx is mined and in the canonical chain.
|
|
if (txQuery.found && txQuery.result.canonical) {
|
|
dbTx = txQuery.result;
|
|
eventCount = dbTx.event_count;
|
|
}
|
|
|
|
// Otherwise, if not mined or not canonical, check in the mempool.
|
|
else if (mempoolTxQuery.found) {
|
|
dbTx = mempoolTxQuery.result;
|
|
}
|
|
// Fallback for a situation where the tx was only mined in a non-canonical chain, but somehow not in the mempool table.
|
|
else if (txQuery.found) {
|
|
logger.warn(`Tx only exists in a non-canonical chain, missing from mempool: ${args.txId}`);
|
|
dbTx = txQuery.result;
|
|
eventCount = dbTx.event_count;
|
|
}
|
|
// Tx not found in db.
|
|
else {
|
|
return { found: false };
|
|
}
|
|
|
|
// if tx is included in a block
|
|
if ('tx_index' in dbTx) {
|
|
// if tx events are requested
|
|
if ('eventLimit' in args) {
|
|
const eventsQuery = await db.getTxEvents({
|
|
txId: args.txId,
|
|
indexBlockHash: dbTx.index_block_hash,
|
|
limit: args.eventLimit,
|
|
offset: args.eventOffset,
|
|
});
|
|
dbTxEvents = eventsQuery.results;
|
|
}
|
|
}
|
|
|
|
const apiTx: Partial<Transaction | MempoolTransaction> = {
|
|
tx_id: dbTx.tx_id,
|
|
tx_type: getTxTypeString(dbTx.type_id),
|
|
|
|
nonce: dbTx.nonce,
|
|
fee_rate: dbTx.fee_rate.toString(10),
|
|
sender_address: dbTx.sender_address,
|
|
sponsored: dbTx.sponsored,
|
|
sponsor_address: dbTx.sponsor_address,
|
|
|
|
post_condition_mode: serializePostConditionMode(dbTx.post_conditions.readUInt8(0)),
|
|
anchor_mode: getTxAnchorModeString(dbTx.anchor_mode),
|
|
};
|
|
|
|
(apiTx as Transaction | MempoolTransaction).tx_status = getTxStatusString(dbTx.status);
|
|
|
|
// If not a mempool transaction then block info is available
|
|
if ('tx_index' in dbTx) {
|
|
const tx = apiTx as Transaction;
|
|
tx.block_hash = dbTx.block_hash;
|
|
tx.block_height = dbTx.block_height;
|
|
tx.burn_block_time = dbTx.burn_block_time;
|
|
tx.burn_block_time_iso = unixEpochToIso(dbTx.burn_block_time);
|
|
tx.canonical = dbTx.canonical;
|
|
tx.tx_index = dbTx.tx_index;
|
|
|
|
if (dbTx.raw_result) {
|
|
tx.tx_result = {
|
|
hex: dbTx.raw_result,
|
|
repr: cvToString(deserializeCV(hexToBuffer(dbTx.raw_result))),
|
|
};
|
|
}
|
|
} else if ('receipt_time' in dbTx) {
|
|
const tx = apiTx as MempoolTransaction;
|
|
tx.receipt_time = dbTx.receipt_time;
|
|
tx.receipt_time_iso = unixEpochToIso(dbTx.receipt_time);
|
|
} else {
|
|
throw new Error(`Unexpected transaction object type. Expected a mined TX or a mempool TX`);
|
|
}
|
|
|
|
switch (apiTx.tx_type) {
|
|
case 'token_transfer': {
|
|
apiTx.token_transfer = {
|
|
recipient_address: unwrapOptional(
|
|
dbTx.token_transfer_recipient_address,
|
|
() => 'Unexpected nullish token_transfer_recipient_address'
|
|
),
|
|
amount: unwrapOptional(
|
|
dbTx.token_transfer_amount,
|
|
() => 'Unexpected nullish token_transfer_amount'
|
|
).toString(10),
|
|
memo: bufferToHexPrefixString(
|
|
unwrapOptional(dbTx.token_transfer_memo, () => 'Unexpected nullish token_transfer_memo')
|
|
),
|
|
};
|
|
break;
|
|
}
|
|
case 'smart_contract': {
|
|
const postConditions = readTransactionPostConditions(
|
|
BufferReader.fromBuffer(dbTx.post_conditions.slice(1))
|
|
);
|
|
apiTx.post_conditions = postConditions.map(pc => serializePostCondition(pc));
|
|
apiTx.smart_contract = {
|
|
contract_id: unwrapOptional(
|
|
dbTx.smart_contract_contract_id,
|
|
() => 'Unexpected nullish smart_contract_contract_id'
|
|
),
|
|
source_code: unwrapOptional(
|
|
dbTx.smart_contract_source_code,
|
|
() => 'Unexpected nullish smart_contract_source_code'
|
|
),
|
|
};
|
|
break;
|
|
}
|
|
case 'contract_call': {
|
|
const postConditions = readTransactionPostConditions(
|
|
BufferReader.fromBuffer(dbTx.post_conditions.slice(1))
|
|
);
|
|
const contractId = unwrapOptional(
|
|
dbTx.contract_call_contract_id,
|
|
() => 'Unexpected nullish contract_call_contract_id'
|
|
);
|
|
const functionName = unwrapOptional(
|
|
dbTx.contract_call_function_name,
|
|
() => 'Unexpected nullish contract_call_function_name'
|
|
);
|
|
apiTx.post_conditions = postConditions.map(pc => serializePostCondition(pc));
|
|
const contract = await db.getSmartContract(contractId);
|
|
if (!contract.found) {
|
|
throw new Error(`Failed to lookup smart contract by ID ${contractId}`);
|
|
}
|
|
const contractAbi: ClarityAbi = JSON.parse(contract.result.abi);
|
|
const functionAbi = contractAbi.functions.find(fn => fn.name === functionName);
|
|
if (!functionAbi) {
|
|
throw new Error(`Could not find function name "${functionName}" in ABI for ${contractId}`);
|
|
}
|
|
apiTx.contract_call = {
|
|
contract_id: contractId,
|
|
function_name: functionName,
|
|
function_signature: abiFunctionToString(functionAbi),
|
|
};
|
|
if (dbTx.contract_call_function_args) {
|
|
let fnArgIndex = 0;
|
|
apiTx.contract_call.function_args = readClarityValueArray(
|
|
dbTx.contract_call_function_args
|
|
).map(c => {
|
|
const functionArgAbi = functionAbi.args[fnArgIndex++];
|
|
return {
|
|
hex: bufferToHexPrefixString(serializeCV(c)),
|
|
repr: cvToString(c),
|
|
name: functionArgAbi.name,
|
|
type: getTypeString(functionArgAbi.type),
|
|
};
|
|
});
|
|
}
|
|
break;
|
|
}
|
|
case 'poison_microblock': {
|
|
apiTx.poison_microblock = {
|
|
microblock_header_1: bufferToHexPrefixString(
|
|
unwrapOptional(dbTx.poison_microblock_header_1)
|
|
),
|
|
microblock_header_2: bufferToHexPrefixString(
|
|
unwrapOptional(dbTx.poison_microblock_header_2)
|
|
),
|
|
};
|
|
break;
|
|
}
|
|
case 'coinbase': {
|
|
apiTx.coinbase_payload = {
|
|
data: bufferToHexPrefixString(
|
|
unwrapOptional(dbTx.coinbase_payload, () => 'Unexpected nullish coinbase_payload')
|
|
),
|
|
};
|
|
break;
|
|
}
|
|
default:
|
|
throw new Error(`Unexpected DbTxTypeId: ${dbTx.type_id}`);
|
|
}
|
|
|
|
(apiTx as Transaction).events = dbTxEvents.map(event => parseDbEvent(event));
|
|
(apiTx as Transaction).event_count = eventCount;
|
|
|
|
return {
|
|
found: true,
|
|
result: apiTx as Transaction,
|
|
};
|
|
}
|