Skip to content

Commit 9d01c20

Browse files
authored
Only load relevant rosetta transaction (#1072)
* refactor: loaded relevant tx only * test: added test for coinbase tx * refactor: added testMinerRewards * style: refactored code according to comments
1 parent cacf9a6 commit 9d01c20

File tree

3 files changed

+244
-40
lines changed

3 files changed

+244
-40
lines changed

src/api/controllers/db-controller.ts

Lines changed: 88 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ import {
5959
DbSmartContract,
6060
DbSearchResultWithMetadata,
6161
BaseTx,
62+
DbMinerReward,
63+
StxUnlockEvent,
6264
} from '../../datastore/common';
6365
import {
6466
unwrapOptional,
@@ -480,6 +482,71 @@ function parseDbBlock(
480482
return apiBlock;
481483
}
482484

485+
async function parseRosettaTxDetail(opts: {
486+
block_height: number;
487+
indexBlockHash: string;
488+
tx: DbTx;
489+
db: DataStore;
490+
minerRewards: DbMinerReward[];
491+
unlockingEvents: StxUnlockEvent[];
492+
}): Promise<RosettaTransaction> {
493+
let events: DbEvent[] = [];
494+
if (opts.block_height > 1) {
495+
// only return events of blocks at height greater than 1
496+
const eventsQuery = await opts.db.getTxEvents({
497+
txId: opts.tx.tx_id,
498+
indexBlockHash: opts.indexBlockHash,
499+
limit: 5000,
500+
offset: 0,
501+
});
502+
events = eventsQuery.results;
503+
}
504+
const operations = await getOperations(
505+
opts.tx,
506+
opts.db,
507+
opts.minerRewards,
508+
events,
509+
opts.unlockingEvents
510+
);
511+
const txMemo = parseTransactionMemo(opts.tx);
512+
const rosettaTx: RosettaTransaction = {
513+
transaction_identifier: { hash: opts.tx.tx_id },
514+
operations: operations,
515+
};
516+
if (txMemo) {
517+
rosettaTx.metadata = {
518+
memo: txMemo,
519+
};
520+
}
521+
return rosettaTx;
522+
}
523+
524+
async function getRosettaBlockTxFromDataStore(opts: {
525+
tx: DbTx;
526+
block: DbBlock;
527+
db: DataStore;
528+
}): Promise<FoundOrNot<RosettaTransaction>> {
529+
let minerRewards: DbMinerReward[] = [],
530+
unlockingEvents: StxUnlockEvent[] = [];
531+
532+
if (opts.tx.type_id === DbTxTypeId.Coinbase) {
533+
minerRewards = await opts.db.getMinersRewardsAtHeight({
534+
blockHeight: opts.block.block_height,
535+
});
536+
unlockingEvents = await opts.db.getUnlockedAddressesAtBlock(opts.block);
537+
}
538+
539+
const rosettaTx = await parseRosettaTxDetail({
540+
block_height: opts.block.block_height,
541+
indexBlockHash: opts.tx.index_block_hash,
542+
tx: opts.tx,
543+
db: opts.db,
544+
minerRewards,
545+
unlockingEvents,
546+
});
547+
return { found: true, result: rosettaTx };
548+
}
549+
483550
async function getRosettaBlockTransactionsFromDataStore(opts: {
484551
blockHash: string;
485552
indexBlockHash: string;
@@ -504,28 +571,14 @@ async function getRosettaBlockTransactionsFromDataStore(opts: {
504571
const transactions: RosettaTransaction[] = [];
505572

506573
for (const tx of txsQuery.result) {
507-
let events: DbEvent[] = [];
508-
if (blockQuery.result.block_height > 1) {
509-
// only return events of blocks at height greater than 1
510-
const eventsQuery = await opts.db.getTxEvents({
511-
txId: tx.tx_id,
512-
indexBlockHash: opts.indexBlockHash,
513-
limit: 5000,
514-
offset: 0,
515-
});
516-
events = eventsQuery.results;
517-
}
518-
const operations = await getOperations(tx, opts.db, minerRewards, events, unlockingEvents);
519-
const txMemo = parseTransactionMemo(tx);
520-
const rosettaTx: RosettaTransaction = {
521-
transaction_identifier: { hash: tx.tx_id },
522-
operations: operations,
523-
};
524-
if (txMemo) {
525-
rosettaTx.metadata = {
526-
memo: txMemo,
527-
};
528-
}
574+
const rosettaTx = await parseRosettaTxDetail({
575+
block_height: blockQuery.result.block_height,
576+
indexBlockHash: opts.indexBlockHash,
577+
tx,
578+
db: opts.db,
579+
minerRewards,
580+
unlockingEvents,
581+
});
529582
transactions.push(rosettaTx);
530583
}
531584

@@ -540,30 +593,27 @@ export async function getRosettaTransactionFromDataStore(
540593
if (!txQuery.found) {
541594
return { found: false };
542595
}
543-
const blockOperations = await getRosettaBlockTransactionsFromDataStore({
544-
blockHash: txQuery.result.block_hash,
545-
indexBlockHash: txQuery.result.index_block_hash,
546-
db,
547-
});
548-
if (!blockOperations.found) {
596+
597+
const blockQuery = await db.getBlock({ hash: txQuery.result.block_hash });
598+
if (!blockQuery.found) {
549599
throw new Error(
550600
`Could not find block for tx: ${txId}, block_hash: ${txQuery.result.block_hash}, index_block_hash: ${txQuery.result.index_block_hash}`
551601
);
552602
}
553-
const rosettaTx = blockOperations.result.find(
554-
op => op.transaction_identifier.hash === txQuery.result.tx_id
555-
);
556-
if (!rosettaTx) {
603+
604+
const rosettaTx = await getRosettaBlockTxFromDataStore({
605+
tx: txQuery.result,
606+
block: blockQuery.result,
607+
db,
608+
});
609+
610+
if (!rosettaTx.found) {
557611
throw new Error(
558612
`Rosetta block missing operations for tx: ${txId}, block_hash: ${txQuery.result.block_hash}, index_block_hash: ${txQuery.result.index_block_hash}`
559613
);
560614
}
561-
const result: RosettaTransaction = {
562-
transaction_identifier: rosettaTx.transaction_identifier,
563-
operations: rosettaTx.operations,
564-
metadata: rosettaTx.metadata,
565-
};
566-
return { found: true, result };
615+
616+
return rosettaTx;
567617
}
568618

569619
interface GetTxArgs {

src/test-utils/test-builders.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
DbFtEvent,
1717
DbMempoolTx,
1818
DbMicroblockPartial,
19+
DbMinerReward,
1920
DbNftEvent,
2021
DbSmartContract,
2122
DbSmartContractEvent,
@@ -66,6 +67,11 @@ const CONTRACT_ABI = {
6667
fungible_tokens: [],
6768
non_fungible_tokens: [],
6869
};
70+
const MINER_RECIPIENT = 'testAddr2';
71+
const COINBASE_AMOUNT = 15_000_000_000_000n;
72+
const TX_FEES_ANCHORED = 1_000_000_000_000n;
73+
const TX_FEES_STREAMED_CONFIRMED = 2_000_000_000_000n;
74+
const TX_FEES_STREAMED_PRODUCED = 3_000_000_000_000n;
6975

7076
interface TestBlockArgs {
7177
block_height?: number;
@@ -411,6 +417,39 @@ function testSmartContractEvent(args?: TestSmartContractEventArgs): DbSmartContr
411417
};
412418
}
413419

420+
interface TestMinerRewardArgs {
421+
block_hash?: string;
422+
index_block_hash?: string;
423+
from_index_block_hash?: string;
424+
mature_block_height?: number;
425+
canonical?: boolean;
426+
recipient?: string;
427+
coinbase_amount?: bigint;
428+
tx_fees_anchored?: bigint;
429+
tx_fees_streamed_confirmed?: bigint;
430+
tx_fees_streamed_produced?: bigint;
431+
}
432+
433+
/**
434+
* Generate a test miner reward
435+
* @param args - Optional miner reward data
436+
* @returns `DbMinerReward`
437+
*/
438+
function testMinerReward(args?: TestMinerRewardArgs): DbMinerReward {
439+
return {
440+
block_hash: args?.block_hash ?? BLOCK_HASH,
441+
index_block_hash: args?.index_block_hash ?? INDEX_BLOCK_HASH,
442+
from_index_block_hash: args?.from_index_block_hash ?? INDEX_BLOCK_HASH,
443+
mature_block_height: args?.mature_block_height ?? BLOCK_HEIGHT,
444+
canonical: args?.canonical ?? true,
445+
recipient: args?.recipient ?? MINER_RECIPIENT,
446+
coinbase_amount: args?.coinbase_amount ?? COINBASE_AMOUNT,
447+
tx_fees_anchored: args?.tx_fees_anchored ?? TX_FEES_ANCHORED,
448+
tx_fees_streamed_confirmed: args?.tx_fees_streamed_confirmed ?? TX_FEES_STREAMED_CONFIRMED,
449+
tx_fees_streamed_produced: args?.tx_fees_streamed_produced ?? TX_FEES_STREAMED_PRODUCED,
450+
};
451+
}
452+
414453
/**
415454
* Builder that creates a test block with any number of transactions and events so populating
416455
* the DB for testing becomes easier.
@@ -506,6 +545,16 @@ export class TestBlockBuilder {
506545
return this;
507546
}
508547

548+
addMinerReward(args?: TestMinerRewardArgs): TestBlockBuilder {
549+
const defaultArgs: TestMinerRewardArgs = {
550+
mature_block_height: this.block.block_height,
551+
block_hash: this.block.block_hash,
552+
index_block_hash: this.block.index_block_hash,
553+
};
554+
this.data.minerRewards.push(testMinerReward({ ...defaultArgs, ...args }));
555+
return this;
556+
}
557+
509558
build(): DataStoreBlockUpdateData {
510559
return this.data;
511560
}

src/tests-rosetta/api.ts

Lines changed: 107 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { ApiServer, startApiServer } from '../api/init';
55
import * as supertest from 'supertest';
66
import { startEventServer } from '../event-stream/event-server';
77
import { Server } from 'net';
8-
import { DbBlock, DbTx, DbMempoolTx, DbTxStatus, DbTxTypeId } from '../datastore/common';
8+
import { DbBlock, DbTx, DbMempoolTx, DbTxStatus, DbTxTypeId, DataStoreBlockUpdateData, DbMinerReward } from '../datastore/common';
99
import * as assert from 'assert';
1010
import {
1111
AnchorMode,
@@ -31,7 +31,7 @@ import {
3131
} from '@stacks/transactions';
3232
import * as BN from 'bn.js';
3333
import { StacksCoreRpcClient } from '../core-rpc/client';
34-
import { bufferToHexPrefixString, timeout } from '../helpers';
34+
import { bufferToHexPrefixString, I32_MAX, timeout } from '../helpers';
3535
import {
3636
RosettaConstructionCombineRequest,
3737
RosettaConstructionCombineResponse,
@@ -74,6 +74,7 @@ import {
7474
} from '../rosetta-helpers';
7575
import { makeSigHashPreSign, MessageSignature } from '@stacks/transactions';
7676
import { decodeBtcAddress } from '@stacks/stacking';
77+
import { TestBlockBuilder } from '../test-utils/test-builders';
7778

7879

7980
describe('Rosetta API', () => {
@@ -3378,6 +3379,110 @@ describe('Rosetta API', () => {
33783379
);
33793380
})
33803381

3382+
test('block/transaction coinbase', async () => {
3383+
const dbBlock = await db.getCurrentBlock();
3384+
const blockRes = dbBlock.result ?? undefined;
3385+
const block = {
3386+
parent_index_block_hash: blockRes ? blockRes.index_block_hash : '0x00',
3387+
parent_block_hash: blockRes ? blockRes.block_hash : "0x00",
3388+
parent_microblock_hash: '',
3389+
parent_microblock_sequence: 0,
3390+
block_height: blockRes ? blockRes.block_height + 1 : 1,
3391+
};
3392+
3393+
const tx = {
3394+
tx_id: '0x1234',
3395+
tx_index: 4,
3396+
anchor_mode: AnchorMode.Any,
3397+
type_id: DbTxTypeId.Coinbase,
3398+
raw_result: '0x0100000000000000000000000000000001', // u1
3399+
canonical: true,
3400+
microblock_canonical: true,
3401+
microblock_sequence: I32_MAX,
3402+
microblock_hash: '',
3403+
parent_index_block_hash: block.parent_microblock_hash,
3404+
parent_block_hash: block.parent_index_block_hash,
3405+
post_conditions: Buffer.from([0x01, 0xf5]),
3406+
fee_rate: 1234n,
3407+
sender_address: 'sender-addr',
3408+
};
3409+
3410+
const dbMinerReward1 = {
3411+
canonical: true,
3412+
};
3413+
3414+
const dbMinerReward2 = {
3415+
canonical: true,
3416+
recipient: 'testAddr2',
3417+
};
3418+
3419+
const data = new TestBlockBuilder(block)
3420+
.addTx(tx)
3421+
.addMinerReward(dbMinerReward1)
3422+
.addMinerReward(dbMinerReward2)
3423+
.build();
3424+
3425+
await db.update(data);
3426+
const query1 = await supertest(api.server)
3427+
.post(`/rosetta/v1/block/transaction`)
3428+
.send({
3429+
network_identifier: { blockchain: 'stacks', network: 'testnet' },
3430+
block_identifier: { index: block.block_height, hash: data.block.block_hash },
3431+
transaction_identifier: { hash: tx.tx_id },
3432+
});
3433+
expect(JSON.parse(query1.text)).toEqual({
3434+
transaction_identifier: {
3435+
hash: tx.tx_id
3436+
},
3437+
operations: [
3438+
{
3439+
operation_identifier: {
3440+
index: 0
3441+
},
3442+
type: "coinbase",
3443+
status: "success",
3444+
account: {
3445+
address: tx.sender_address
3446+
}
3447+
},
3448+
{
3449+
operation_identifier: {
3450+
index: 1
3451+
},
3452+
status: "success",
3453+
type: "miner_reward",
3454+
account: {
3455+
address: dbMinerReward2.recipient,
3456+
},
3457+
amount: {
3458+
value: "21000000000000",
3459+
currency: {
3460+
decimals: 6,
3461+
symbol: "STX"
3462+
}
3463+
}
3464+
},
3465+
{
3466+
operation_identifier: {
3467+
index: 2
3468+
},
3469+
status: "success",
3470+
type: "miner_reward",
3471+
account: {
3472+
address: 'testAddr2',
3473+
},
3474+
amount: {
3475+
value: "21000000000000",
3476+
currency: {
3477+
decimals: 6,
3478+
symbol: "STX"
3479+
}
3480+
}
3481+
}
3482+
]
3483+
})
3484+
});
3485+
33813486
/* rosetta construction end */
33823487

33833488
afterAll(async () => {

0 commit comments

Comments
 (0)