• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

hirosystems / stacks-blockchain-api / 3749978634

pending completion
3749978634

push

github

GitHub
feat: [Stacks 2.1] Support new "block 0" boot events (#1476)

2014 of 3089 branches covered (65.2%)

13 of 13 new or added lines in 1 file covered. (100.0%)

886 existing lines in 37 files now uncovered.

7070 of 9217 relevant lines covered (76.71%)

970.65 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

75.2
/src/event-stream/reader.ts
1
import {
38✔
2
  CoreNodeBlockMessage,
3
  CoreNodeEvent,
4
  CoreNodeEventType,
5
  CoreNodeMicroblockTxMessage,
6
  CoreNodeParsedTxMessage,
7
  CoreNodeTxMessage,
8
  isTxWithMicroblockInfo,
9
  StxLockEvent,
10
  StxTransferEvent,
11
} from './core-node-message';
12
import {
38✔
13
  decodeClarityValue,
14
  decodeTransaction,
15
  decodeStacksAddress,
16
  ClarityTypeID,
17
  ClarityValuePrincipalStandard,
18
  ClarityValueResponse,
19
  ClarityValueTuple,
20
  ClarityValueUInt,
21
  AnchorModeID,
22
  DecodedTxResult,
23
  PostConditionModeID,
24
  PrincipalTypeID,
25
  TxPayloadTypeID,
26
  PostConditionAuthFlag,
27
  TxPublicKeyEncoding,
28
  TxSpendingConditionSingleSigHashMode,
29
  decodeClarityValueList,
30
} from 'stacks-encoding-native-js';
31
import { DbMicroblockPartial } from '../datastore/common';
32
import { NotImplementedError } from '../errors';
38✔
33
import {
38✔
34
  getEnumDescription,
35
  logger,
36
  logError,
37
  I32_MAX,
38
  bufferToHexPrefixString,
39
  hexToBuffer,
40
} from '../helpers';
41
import {
38✔
42
  TransactionVersion,
43
  ChainID,
44
  uintCV,
45
  tupleCV,
46
  bufferCV,
47
  serializeCV,
48
} from '@stacks/transactions';
49

50
export function getTxSenderAddress(tx: DecodedTxResult): string {
38✔
51
  const txSender = tx.auth.origin_condition.signer.address;
958✔
52
  return txSender;
958✔
53
}
54

55
export function getTxSponsorAddress(tx: DecodedTxResult): string | undefined {
38✔
56
  let sponsorAddress: string | undefined = undefined;
873✔
57
  if (tx.auth.type_id === PostConditionAuthFlag.Sponsored) {
873✔
58
    sponsorAddress = tx.auth.sponsor_condition.signer.address;
1✔
59
  }
60
  return sponsorAddress;
873✔
61
}
62

63
function createTransactionFromCoreBtcStxLockEvent(
64
  chainId: ChainID,
65
  event: StxLockEvent,
66
  burnBlockHeight: number,
67
  txResult: string,
68
  txId: string
69
): DecodedTxResult {
70
  const resultCv = decodeClarityValue<
2✔
71
    ClarityValueResponse<
72
      ClarityValueTuple<{
73
        'lock-amount': ClarityValueUInt;
74
        'unlock-burn-height': ClarityValueUInt;
75
        stacker: ClarityValuePrincipalStandard;
76
      }>
77
    >
78
  >(txResult);
79
  if (resultCv.type_id !== ClarityTypeID.ResponseOk) {
2!
80
    throw new Error(`Unexpected tx result Clarity type ID: ${resultCv.type_id}`);
×
81
  }
82
  const resultTuple = resultCv.value;
2✔
83
  const lockAmount = resultTuple.data['lock-amount'];
2✔
84
  const stacker = resultTuple.data['stacker'];
2✔
85
  const unlockBurnHeight = Number(resultTuple.data['unlock-burn-height'].value);
2✔
86

87
  // Number of cycles: floor((unlock-burn-height - burn-height) / reward-cycle-length)
88
  const rewardCycleLength = chainId === ChainID.Mainnet ? 2100 : 50;
2!
89
  const lockPeriod = Math.floor((unlockBurnHeight - burnBlockHeight) / rewardCycleLength);
2✔
90
  const senderAddress = decodeStacksAddress(event.stx_lock_event.locked_address);
2✔
91
  const poxAddressString =
92
    chainId === ChainID.Mainnet ? 'SP000000000000000000002Q6VF78' : 'ST000000000000000000002AMW42H';
2!
93
  const poxAddress = decodeStacksAddress(poxAddressString);
2✔
94

95
  const legacyClarityVals = [
2✔
96
    uintCV(lockAmount.value),
97
    tupleCV({
98
      hashbytes: bufferCV(hexToBuffer(stacker.address_hash_bytes)),
99
      version: bufferCV(Buffer.from([stacker.address_version])),
100
    }),
101
    uintCV(burnBlockHeight), // start-burn-height
102
    uintCV(lockPeriod), // lock-period
103
  ];
104
  const fnLenBuffer = Buffer.alloc(4);
2✔
105
  fnLenBuffer.writeUInt32BE(legacyClarityVals.length);
2✔
106
  const serializedClarityValues = legacyClarityVals.map(c => serializeCV(c));
8✔
107
  const rawFnArgs = bufferToHexPrefixString(
2✔
108
    Buffer.concat([fnLenBuffer, ...serializedClarityValues])
109
  );
110
  const clarityFnArgs = decodeClarityValueList(rawFnArgs);
2✔
111

112
  const tx: DecodedTxResult = {
2✔
113
    tx_id: txId,
114
    version: chainId === ChainID.Mainnet ? TransactionVersion.Mainnet : TransactionVersion.Testnet,
2!
115
    chain_id: chainId,
116
    auth: {
117
      type_id: PostConditionAuthFlag.Standard,
118
      origin_condition: {
119
        hash_mode: TxSpendingConditionSingleSigHashMode.P2PKH,
120
        signer: {
121
          address_version: senderAddress[0],
122
          address_hash_bytes: senderAddress[1],
123
          address: event.stx_lock_event.locked_address,
124
        },
125
        nonce: '0',
126
        tx_fee: '0',
127
        key_encoding: TxPublicKeyEncoding.Compressed,
128
        signature: '0x',
129
      },
130
    },
131
    anchor_mode: AnchorModeID.Any,
132
    post_condition_mode: PostConditionModeID.Allow,
133
    post_conditions: [],
134
    post_conditions_buffer: '0x0100000000',
135
    payload: {
136
      type_id: TxPayloadTypeID.ContractCall,
137
      address: poxAddressString,
138
      address_version: poxAddress[0],
139
      address_hash_bytes: poxAddress[1],
140
      contract_name: 'pox',
141
      function_name: 'stack-stx',
142
      function_args: clarityFnArgs,
143
      function_args_buffer: rawFnArgs,
144
    },
145
  };
146
  return tx;
2✔
147
}
148

149
function createTransactionFromCoreBtcTxEvent(
150
  chainId: ChainID,
151
  event: StxTransferEvent,
152
  txId: string
153
): DecodedTxResult {
154
  const recipientAddress = decodeStacksAddress(event.stx_transfer_event.recipient);
2✔
155
  const senderAddress = decodeStacksAddress(event.stx_transfer_event.sender);
2✔
156
  const tx: DecodedTxResult = {
2✔
157
    tx_id: txId,
158
    version: chainId === ChainID.Mainnet ? TransactionVersion.Mainnet : TransactionVersion.Testnet,
2!
159
    chain_id: chainId,
160
    auth: {
161
      type_id: PostConditionAuthFlag.Standard,
162
      origin_condition: {
163
        hash_mode: TxSpendingConditionSingleSigHashMode.P2PKH,
164
        signer: {
165
          address_version: senderAddress[0],
166
          address_hash_bytes: senderAddress[1],
167
          address: event.stx_transfer_event.sender,
168
        },
169
        nonce: '0',
170
        tx_fee: '0',
171
        key_encoding: TxPublicKeyEncoding.Compressed,
172
        signature: '0x',
173
      },
174
    },
175
    anchor_mode: AnchorModeID.Any,
176
    post_condition_mode: PostConditionModeID.Allow,
177
    post_conditions: [],
178
    post_conditions_buffer: '0x0100000000',
179
    payload: {
180
      type_id: TxPayloadTypeID.TokenTransfer,
181
      recipient: {
182
        type_id: PrincipalTypeID.Standard,
183
        address_version: recipientAddress[0],
184
        address_hash_bytes: recipientAddress[1],
185
        address: event.stx_transfer_event.recipient,
186
      },
187
      amount: BigInt(event.stx_transfer_event.amount).toString(),
188
      memo_hex: '0x',
189
    },
190
  };
191
  return tx;
2✔
192
}
193

194
export interface CoreNodeMsgBlockData {
195
  block_hash: string;
196
  index_block_hash: string;
197
  parent_index_block_hash: string;
198
  parent_block_hash: string;
199
  parent_burn_block_timestamp: number;
200
  parent_burn_block_height: number;
201
  parent_burn_block_hash: string;
202
  block_height: number;
203
  burn_block_time: number;
204
  burn_block_height: number;
205
}
206

207
export function parseMicroblocksFromTxs(args: {
38✔
208
  parentIndexBlockHash: string;
209
  txs: CoreNodeTxMessage[];
210
  parentBurnBlock: {
211
    hash: string;
212
    time: number;
213
    height: number;
214
  };
215
}): DbMicroblockPartial[] {
216
  const microblockMap = new Map<string, DbMicroblockPartial>();
412✔
217
  args.txs.forEach(tx => {
412✔
218
    if (isTxWithMicroblockInfo(tx) && !microblockMap.has(tx.microblock_hash)) {
714✔
219
      const dbMbPartial: DbMicroblockPartial = {
59✔
220
        microblock_hash: tx.microblock_hash,
221
        microblock_sequence: tx.microblock_sequence,
222
        microblock_parent_hash: tx.microblock_parent_hash,
223
        parent_index_block_hash: args.parentIndexBlockHash,
224
        parent_burn_block_height: args.parentBurnBlock.height,
225
        parent_burn_block_hash: args.parentBurnBlock.hash,
226
        parent_burn_block_time: args.parentBurnBlock.time,
227
      };
228
      microblockMap.set(tx.microblock_hash, dbMbPartial);
59✔
229
    }
230
  });
231
  const dbMicroblocks = [...microblockMap.values()].sort(
412✔
232
    (a, b) => a.microblock_sequence - b.microblock_sequence
31✔
233
  );
234
  return dbMicroblocks;
412✔
235
}
236

237
export function parseMessageTransaction(
38✔
238
  chainId: ChainID,
239
  coreTx: CoreNodeTxMessage,
240
  blockData: CoreNodeMsgBlockData,
241
  allEvents: CoreNodeEvent[]
242
): CoreNodeParsedTxMessage | null {
243
  try {
718✔
244
    let rawTx: DecodedTxResult;
245
    let txSender: string;
246
    let sponsorAddress: string | undefined = undefined;
718✔
247
    if (coreTx.raw_tx === '0x00') {
718✔
248
      const event = allEvents.find(event => event.txid === coreTx.txid);
10✔
249
      if (!event) {
4!
250
        logger.warn(`Could not find event for process BTC tx: ${JSON.stringify(coreTx)}`);
×
251
        return null;
×
252
      }
253
      if (event.type === CoreNodeEventType.StxTransferEvent) {
4✔
254
        rawTx = createTransactionFromCoreBtcTxEvent(chainId, event, coreTx.txid);
2✔
255
        txSender = event.stx_transfer_event.sender;
2✔
256
      } else if (event.type === CoreNodeEventType.StxLockEvent) {
2!
257
        rawTx = createTransactionFromCoreBtcStxLockEvent(
2✔
258
          chainId,
259
          event,
260
          blockData.burn_block_height,
261
          coreTx.raw_result,
262
          coreTx.txid
263
        );
264
        txSender = event.stx_lock_event.locked_address;
2✔
265
      } else {
266
        logError(
×
267
          `BTC transaction found, but no STX transfer event available to recreate transaction. TX: ${JSON.stringify(
268
            coreTx
269
          )}`
270
        );
271
        throw new Error('Unable to generate transaction from BTC tx');
×
272
      }
273
    } else {
274
      rawTx = decodeTransaction(coreTx.raw_tx.substring(2));
714✔
275
      txSender = getTxSenderAddress(rawTx);
714✔
276
      sponsorAddress = getTxSponsorAddress(rawTx);
714✔
277
    }
278
    const parsedTx: CoreNodeParsedTxMessage = {
718✔
279
      core_tx: coreTx,
280
      nonce: Number(rawTx.auth.origin_condition.nonce),
281
      raw_tx: coreTx.raw_tx,
282
      parsed_tx: rawTx,
283
      block_hash: blockData.block_hash,
284
      index_block_hash: blockData.index_block_hash,
285
      parent_index_block_hash: blockData.parent_index_block_hash,
286
      parent_block_hash: blockData.parent_block_hash,
287
      parent_burn_block_hash: blockData.parent_burn_block_hash,
288
      parent_burn_block_time: blockData.parent_burn_block_timestamp,
289
      block_height: blockData.block_height,
290
      burn_block_time: blockData.burn_block_time,
291
      microblock_sequence: coreTx.microblock_sequence ?? I32_MAX,
1,364✔
292
      microblock_hash: coreTx.microblock_hash ?? '',
1,364✔
293
      sender_address: txSender,
294
      sponsor_address: sponsorAddress,
295
    };
296
    const payload = rawTx.payload;
718✔
297
    switch (payload.type_id) {
718!
298
      case TxPayloadTypeID.Coinbase: {
299
        break;
394✔
300
      }
301
      case TxPayloadTypeID.CoinbaseToAltRecipient: {
302
        if (payload.recipient.type_id === PrincipalTypeID.Standard) {
×
303
          logger.verbose(
×
304
            `Coinbase to alt recipient, standard principal: ${payload.recipient.address}`
305
          );
306
        } else {
307
          logger.verbose(
×
308
            `Coinbase to alt recipient, contract principal: ${payload.recipient.address}.${payload.recipient.contract_name}`
309
          );
310
        }
311
        break;
×
312
      }
313
      case TxPayloadTypeID.SmartContract: {
314
        logger.verbose(
72✔
315
          `Smart contract deployed: ${parsedTx.sender_address}.${payload.contract_name}`
316
        );
317
        break;
72✔
318
      }
319
      case TxPayloadTypeID.ContractCall: {
320
        logger.verbose(
81✔
321
          `Contract call: ${payload.address}.${payload.contract_name}.${payload.function_name}`
322
        );
323
        break;
81✔
324
      }
325
      case TxPayloadTypeID.TokenTransfer: {
326
        let recipientPrincipal = payload.recipient.address;
171✔
327
        if (payload.recipient.type_id === PrincipalTypeID.Contract) {
171!
328
          recipientPrincipal += '.' + payload.recipient.contract_name;
×
329
        }
330
        logger.verbose(
171✔
331
          `Token transfer: ${payload.amount} from ${parsedTx.sender_address} to ${recipientPrincipal}`
332
        );
333
        break;
171✔
334
      }
335
      case TxPayloadTypeID.PoisonMicroblock: {
336
        logger.verbose(
×
337
          `Poison microblock: header1 ${payload.microblock_header_1}), header2: ${payload.microblock_header_2}`
338
        );
339
        break;
×
340
      }
341
      case TxPayloadTypeID.VersionedSmartContract: {
342
        logger.verbose(
×
343
          `Versioned smart contract deployed: Clarity version ${payload.clarity_version}, ${parsedTx.sender_address}.${payload.contract_name}`
344
        );
UNCOV
345
        break;
×
346
      }
347
      default: {
UNCOV
348
        throw new NotImplementedError(
×
349
          `extracting data for tx type: ${getEnumDescription(
350
            TxPayloadTypeID,
351
            rawTx.payload.type_id
352
          )}`
353
        );
354
      }
355
    }
356
    return parsedTx;
718✔
357
  } catch (error) {
UNCOV
358
    logError(`error parsing message transaction ${JSON.stringify(coreTx)}: ${error}`, error);
×
UNCOV
359
    throw error;
×
360
  }
361
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc