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

hirosystems / stacks-blockchain-api / 3885427095

pending completion
3885427095

push

github

Matthew Little
chore!: support for Stacks 2.1

2029 of 3104 branches covered (65.37%)

7123 of 9267 relevant lines covered (76.86%)

1177.49 hits per line

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

76.87
/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
import { poxAddressToTuple } from '@stacks/stacking';
38✔
50
import { c32ToB58 } from 'c32check';
38✔
51

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

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

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

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

97
  const contractName = event.stx_lock_event.contract_identifier?.split('.')?.[1] ?? 'pox';
2✔
98

99
  const legacyClarityVals = [
2✔
100
    uintCV(lockAmount.value), // amount-ustx
101
    poxAddressToTuple(c32ToB58(stacker.address)), // pox-addr
102
    uintCV(burnBlockHeight), // start-burn-height
103
    uintCV(lockPeriod), // lock-period
104
  ];
105
  const fnLenBuffer = Buffer.alloc(4);
2✔
106
  fnLenBuffer.writeUInt32BE(legacyClarityVals.length);
2✔
107
  const serializedClarityValues = legacyClarityVals.map(c => serializeCV(c));
8✔
108
  const rawFnArgs = bufferToHexPrefixString(
2✔
109
    Buffer.concat([fnLenBuffer, ...serializedClarityValues])
110
  );
111
  const clarityFnArgs = decodeClarityValueList(rawFnArgs);
2✔
112

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

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

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

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

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