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

hirosystems / stacks-blockchain-api / 4134334244

pending completion
4134334244

push

github

GitHub
Merge pull request #1543 from hirosystems/chore/master-into-dev

2038 of 3151 branches covered (64.68%)

77 of 129 new or added lines in 13 files covered. (59.69%)

7158 of 9352 relevant lines covered (76.54%)

1168.49 hits per line

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

58.03
/src/event-stream/reader.ts
1
import {
38✔
2
  CoreNodeBlockMessage,
3
  CoreNodeEvent,
4
  CoreNodeEventType,
5
  CoreNodeMicroblockTxMessage,
6
  CoreNodeParsedTxMessage,
7
  CoreNodeTxMessage,
8
  isTxWithMicroblockInfo,
9
  SmartContractEvent,
10
  StxLockEvent,
11
  StxTransferEvent,
12
} from './core-node-message';
13
import {
38✔
14
  decodeClarityValue,
15
  decodeTransaction,
16
  decodeStacksAddress,
17
  ClarityTypeID,
18
  ClarityValuePrincipalStandard,
19
  ClarityValueResponse,
20
  ClarityValueTuple,
21
  ClarityValueUInt,
22
  AnchorModeID,
23
  DecodedTxResult,
24
  PostConditionModeID,
25
  PrincipalTypeID,
26
  TxPayloadTypeID,
27
  PostConditionAuthFlag,
28
  TxPublicKeyEncoding,
29
  TxSpendingConditionSingleSigHashMode,
30
  decodeClarityValueList,
31
} from 'stacks-encoding-native-js';
32
import {
33
  DbMicroblockPartial,
34
  DbPox2DelegateStxEvent,
35
  DbPox2StackStxEvent,
36
} from '../datastore/common';
37
import { NotImplementedError } from '../errors';
38✔
38
import {
38✔
39
  getEnumDescription,
40
  logger,
41
  logError,
42
  I32_MAX,
43
  bufferToHexPrefixString,
44
  hexToBuffer,
45
} from '../helpers';
46
import {
38✔
47
  TransactionVersion,
48
  ChainID,
49
  uintCV,
50
  tupleCV,
51
  bufferCV,
52
  serializeCV,
53
  noneCV,
54
  someCV,
55
  OptionalCV,
56
  TupleCV,
57
  BufferCV,
58
  SomeCV,
59
  NoneCV,
60
  UIntCV,
61
} from '@stacks/transactions';
62
import { poxAddressToTuple } from '@stacks/stacking';
38✔
63
import { c32ToB58 } from 'c32check';
38✔
64
import { decodePox2PrintEvent } from './pox2-event-parsing';
38✔
65
import { Pox2ContractIdentifer, Pox2EventName } from '../pox-helpers';
66
import { principalCV } from '@stacks/transactions/dist/clarity/types/principalCV';
38✔
67

68
export function getTxSenderAddress(tx: DecodedTxResult): string {
38✔
69
  const txSender = tx.auth.origin_condition.signer.address;
984✔
70
  return txSender;
984✔
71
}
72

73
export function getTxSponsorAddress(tx: DecodedTxResult): string | undefined {
38✔
74
  let sponsorAddress: string | undefined = undefined;
887✔
75
  if (tx.auth.type_id === PostConditionAuthFlag.Sponsored) {
887✔
76
    sponsorAddress = tx.auth.sponsor_condition.signer.address;
1✔
77
  }
78
  return sponsorAddress;
887✔
79
}
80

81
function createTransactionFromCoreBtcStxLockEvent(
82
  chainId: ChainID,
83
  event: StxLockEvent,
84
  burnBlockHeight: number,
85
  txResult: string,
86
  txId: string,
87
  stxStacksPox2Event: DbPox2StackStxEvent | undefined
88
): DecodedTxResult {
89
  const resultCv = decodeClarityValue<
2✔
90
    ClarityValueResponse<
91
      ClarityValueTuple<{
92
        'lock-amount': ClarityValueUInt;
93
        'unlock-burn-height': ClarityValueUInt;
94
        stacker: ClarityValuePrincipalStandard;
95
      }>
96
    >
97
  >(txResult);
98
  if (resultCv.type_id !== ClarityTypeID.ResponseOk) {
2!
99
    throw new Error(`Unexpected tx result Clarity type ID: ${resultCv.type_id}`);
×
100
  }
101
  const resultTuple = resultCv.value;
2✔
102
  const lockAmount = resultTuple.data['lock-amount'];
2✔
103
  const stacker = resultTuple.data['stacker'];
2✔
104
  const unlockBurnHeight = Number(resultTuple.data['unlock-burn-height'].value);
2✔
105

106
  // Number of cycles: floor((unlock-burn-height - burn-height) / reward-cycle-length)
107
  const rewardCycleLength = chainId === ChainID.Mainnet ? 2100 : 50;
2!
108
  const lockPeriod = Math.floor((unlockBurnHeight - burnBlockHeight) / rewardCycleLength);
2✔
109
  const senderAddress = decodeStacksAddress(event.stx_lock_event.locked_address);
2✔
110
  const poxAddressString =
111
    chainId === ChainID.Mainnet ? 'SP000000000000000000002Q6VF78' : 'ST000000000000000000002AMW42H';
2!
112
  const poxAddress = decodeStacksAddress(poxAddressString);
2✔
113

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

116
  // If a pox-2 event is available then use its pox_addr, otherwise fallback to the stacker address
117
  const poxAddrArg = stxStacksPox2Event?.pox_addr
2!
118
    ? poxAddressToTuple(stxStacksPox2Event.pox_addr)
119
    : poxAddressToTuple(c32ToB58(stacker.address));
120

121
  const legacyClarityVals = [
2✔
122
    uintCV(lockAmount.value), // amount-ustx
123
    poxAddrArg, // pox-addr
124
    uintCV(burnBlockHeight), // start-burn-height
125
    uintCV(lockPeriod), // lock-period
126
  ];
127
  const fnLenBuffer = Buffer.alloc(4);
2✔
128
  fnLenBuffer.writeUInt32BE(legacyClarityVals.length);
2✔
129
  const serializedClarityValues = legacyClarityVals.map(c => serializeCV(c));
8✔
130
  const rawFnArgs = bufferToHexPrefixString(
2✔
131
    Buffer.concat([fnLenBuffer, ...serializedClarityValues])
132
  );
133
  const clarityFnArgs = decodeClarityValueList(rawFnArgs);
2✔
134

135
  const tx: DecodedTxResult = {
2✔
136
    tx_id: txId,
137
    version: chainId === ChainID.Mainnet ? TransactionVersion.Mainnet : TransactionVersion.Testnet,
2!
138
    chain_id: chainId,
139
    auth: {
140
      type_id: PostConditionAuthFlag.Standard,
141
      origin_condition: {
142
        hash_mode: TxSpendingConditionSingleSigHashMode.P2PKH,
143
        signer: {
144
          address_version: senderAddress[0],
145
          address_hash_bytes: senderAddress[1],
146
          address: event.stx_lock_event.locked_address,
147
        },
148
        nonce: '0',
149
        tx_fee: '0',
150
        key_encoding: TxPublicKeyEncoding.Compressed,
151
        signature: '0x',
152
      },
153
    },
154
    anchor_mode: AnchorModeID.Any,
155
    post_condition_mode: PostConditionModeID.Allow,
156
    post_conditions: [],
157
    post_conditions_buffer: '0x0100000000',
158
    payload: {
159
      type_id: TxPayloadTypeID.ContractCall,
160
      address: poxAddressString,
161
      address_version: poxAddress[0],
162
      address_hash_bytes: poxAddress[1],
163
      contract_name: contractName,
164
      function_name: 'stack-stx',
165
      function_args: clarityFnArgs,
166
      function_args_buffer: rawFnArgs,
167
    },
168
  };
169
  return tx;
2✔
170
}
171

172
/*
173
;; Delegate to `delegate-to` the ability to stack from a given address.
174
;;  This method _does not_ lock the funds, rather, it allows the delegate
175
;;  to issue the stacking lock.
176
;; The caller specifies:
177
;;   * amount-ustx: the total amount of ustx the delegate may be allowed to lock
178
;;   * until-burn-ht: an optional burn height at which this delegation expiration
179
;;   * pox-addr: an optional address to which any rewards *must* be sent
180
(define-public (delegate-stx (amount-ustx uint)
181
                             (delegate-to principal)
182
                             (until-burn-ht (optional uint))
183
                             (pox-addr (optional { version: (buff 1),
184
                                                   hashbytes: (buff 32) })))
185
*/
186
function createTransactionFromCoreBtcDelegateStxEvent(
187
  chainId: ChainID,
188
  contractEvent: SmartContractEvent,
189
  decodedEvent: DbPox2DelegateStxEvent,
190
  txResult: string,
191
  txId: string
192
): DecodedTxResult {
NEW
193
  const resultCv = decodeClarityValue<ClarityValueResponse>(txResult);
×
NEW
194
  if (resultCv.type_id !== ClarityTypeID.ResponseOk) {
×
NEW
195
    throw new Error(`Unexpected tx result Clarity type ID: ${resultCv.type_id}`);
×
196
  }
197

NEW
198
  const senderAddress = decodeStacksAddress(decodedEvent.stacker);
×
199
  const poxContractAddressString =
NEW
200
    chainId === ChainID.Mainnet ? 'SP000000000000000000002Q6VF78' : 'ST000000000000000000002AMW42H';
×
NEW
201
  const poxContractAddress = decodeStacksAddress(poxContractAddressString);
×
NEW
202
  const contractName = contractEvent.contract_event.contract_identifier?.split('.')?.[1] ?? 'pox';
×
203

NEW
204
  let poxAddr: NoneCV | OptionalCV<TupleCV> = noneCV();
×
NEW
205
  if (decodedEvent.pox_addr) {
×
NEW
206
    poxAddr = someCV(poxAddressToTuple(decodedEvent.pox_addr));
×
207
  }
208

NEW
209
  let untilBurnHeight: NoneCV | OptionalCV<UIntCV> = noneCV();
×
NEW
210
  if (decodedEvent.data.unlock_burn_height) {
×
NEW
211
    untilBurnHeight = someCV(uintCV(decodedEvent.data.unlock_burn_height));
×
212
  }
213

NEW
214
  const legacyClarityVals = [
×
215
    uintCV(decodedEvent.data.amount_ustx), // amount-ustx
216
    principalCV(decodedEvent.data.delegate_to), // delegate-to
217
    untilBurnHeight, // until-burn-ht
218
    poxAddr, // pox-addr
219
  ];
NEW
220
  const fnLenBuffer = Buffer.alloc(4);
×
NEW
221
  fnLenBuffer.writeUInt32BE(legacyClarityVals.length);
×
NEW
222
  const serializedClarityValues = legacyClarityVals.map(c => serializeCV(c));
×
NEW
223
  const rawFnArgs = bufferToHexPrefixString(
×
224
    Buffer.concat([fnLenBuffer, ...serializedClarityValues])
225
  );
NEW
226
  const clarityFnArgs = decodeClarityValueList(rawFnArgs);
×
227

NEW
228
  const tx: DecodedTxResult = {
×
229
    tx_id: txId,
230
    version: chainId === ChainID.Mainnet ? TransactionVersion.Mainnet : TransactionVersion.Testnet,
×
231
    chain_id: chainId,
232
    auth: {
233
      type_id: PostConditionAuthFlag.Standard,
234
      origin_condition: {
235
        hash_mode: TxSpendingConditionSingleSigHashMode.P2PKH,
236
        signer: {
237
          address_version: senderAddress[0],
238
          address_hash_bytes: senderAddress[1],
239
          address: decodedEvent.stacker,
240
        },
241
        nonce: '0',
242
        tx_fee: '0',
243
        key_encoding: TxPublicKeyEncoding.Compressed,
244
        signature: '0x',
245
      },
246
    },
247
    anchor_mode: AnchorModeID.Any,
248
    post_condition_mode: PostConditionModeID.Allow,
249
    post_conditions: [],
250
    post_conditions_buffer: '0x0100000000',
251
    payload: {
252
      type_id: TxPayloadTypeID.ContractCall,
253
      address: poxContractAddressString,
254
      address_version: poxContractAddress[0],
255
      address_hash_bytes: poxContractAddress[1],
256
      contract_name: contractName,
257
      function_name: 'delegate-stx',
258
      function_args: clarityFnArgs,
259
      function_args_buffer: rawFnArgs,
260
    },
261
  };
NEW
262
  return tx;
×
263
}
264

265
function createTransactionFromCoreBtcTxEvent(
266
  chainId: ChainID,
267
  event: StxTransferEvent,
268
  txId: string
269
): DecodedTxResult {
270
  const recipientAddress = decodeStacksAddress(event.stx_transfer_event.recipient);
2✔
271
  const senderAddress = decodeStacksAddress(event.stx_transfer_event.sender);
2✔
272
  const tx: DecodedTxResult = {
2✔
273
    tx_id: txId,
274
    version: chainId === ChainID.Mainnet ? TransactionVersion.Mainnet : TransactionVersion.Testnet,
2!
275
    chain_id: chainId,
276
    auth: {
277
      type_id: PostConditionAuthFlag.Standard,
278
      origin_condition: {
279
        hash_mode: TxSpendingConditionSingleSigHashMode.P2PKH,
280
        signer: {
281
          address_version: senderAddress[0],
282
          address_hash_bytes: senderAddress[1],
283
          address: event.stx_transfer_event.sender,
284
        },
285
        nonce: '0',
286
        tx_fee: '0',
287
        key_encoding: TxPublicKeyEncoding.Compressed,
288
        signature: '0x',
289
      },
290
    },
291
    anchor_mode: AnchorModeID.Any,
292
    post_condition_mode: PostConditionModeID.Allow,
293
    post_conditions: [],
294
    post_conditions_buffer: '0x0100000000',
295
    payload: {
296
      type_id: TxPayloadTypeID.TokenTransfer,
297
      recipient: {
298
        type_id: PrincipalTypeID.Standard,
299
        address_version: recipientAddress[0],
300
        address_hash_bytes: recipientAddress[1],
301
        address: event.stx_transfer_event.recipient,
302
      },
303
      amount: BigInt(event.stx_transfer_event.amount).toString(),
304
      memo_hex: '0x',
305
    },
306
  };
307
  return tx;
2✔
308
}
309

310
export interface CoreNodeMsgBlockData {
311
  block_hash: string;
312
  index_block_hash: string;
313
  parent_index_block_hash: string;
314
  parent_block_hash: string;
315
  parent_burn_block_timestamp: number;
316
  parent_burn_block_height: number;
317
  parent_burn_block_hash: string;
318
  block_height: number;
319
  burn_block_time: number;
320
  burn_block_height: number;
321
}
322

323
export function parseMicroblocksFromTxs(args: {
38✔
324
  parentIndexBlockHash: string;
325
  txs: CoreNodeTxMessage[];
326
  parentBurnBlock: {
327
    hash: string;
328
    time: number;
329
    height: number;
330
  };
331
}): DbMicroblockPartial[] {
332
  const microblockMap = new Map<string, DbMicroblockPartial>();
420✔
333
  args.txs.forEach(tx => {
420✔
334
    if (isTxWithMicroblockInfo(tx) && !microblockMap.has(tx.microblock_hash)) {
734✔
335
      const dbMbPartial: DbMicroblockPartial = {
59✔
336
        microblock_hash: tx.microblock_hash,
337
        microblock_sequence: tx.microblock_sequence,
338
        microblock_parent_hash: tx.microblock_parent_hash,
339
        parent_index_block_hash: args.parentIndexBlockHash,
340
        parent_burn_block_height: args.parentBurnBlock.height,
341
        parent_burn_block_hash: args.parentBurnBlock.hash,
342
        parent_burn_block_time: args.parentBurnBlock.time,
343
      };
344
      microblockMap.set(tx.microblock_hash, dbMbPartial);
59✔
345
    }
346
  });
347
  const dbMicroblocks = [...microblockMap.values()].sort(
420✔
348
    (a, b) => a.microblock_sequence - b.microblock_sequence
31✔
349
  );
350
  return dbMicroblocks;
420✔
351
}
352

353
export function parseMessageTransaction(
38✔
354
  chainId: ChainID,
355
  coreTx: CoreNodeTxMessage,
356
  blockData: CoreNodeMsgBlockData,
357
  allEvents: CoreNodeEvent[]
358
): CoreNodeParsedTxMessage | null {
359
  try {
738✔
360
    let rawTx: DecodedTxResult;
361
    let txSender: string;
362
    let sponsorAddress: string | undefined = undefined;
738✔
363
    if (coreTx.raw_tx === '0x00') {
738✔
364
      const events = allEvents.filter(event => event.txid === coreTx.txid);
45✔
365
      if (events.length === 0) {
4!
366
        logger.warn(`Could not find event for process BTC tx: ${JSON.stringify(coreTx)}`);
×
367
        return null;
×
368
      }
369
      const stxTransferEvent = events.find(
4✔
370
        (e): e is StxTransferEvent => e.type === CoreNodeEventType.StxTransferEvent
4✔
371
      );
372
      const stxLockEvent = events.find(
4✔
373
        (e): e is StxLockEvent => e.type === CoreNodeEventType.StxLockEvent
4✔
374
      );
375

376
      const pox2Event = events
4✔
377
        .filter(
378
          (e): e is SmartContractEvent =>
379
            e.type === CoreNodeEventType.ContractEvent &&
4!
380
            e.contract_event.topic === 'print' &&
381
            (e.contract_event.contract_identifier === Pox2ContractIdentifer.mainnet ||
382
              e.contract_event.contract_identifier === Pox2ContractIdentifer.testnet)
383
        )
384
        .map(e => {
NEW
385
          const network = chainId === ChainID.Mainnet ? 'mainnet' : 'testnet';
×
NEW
386
          const decodedEvent = decodePox2PrintEvent(e.contract_event.raw_value, network);
×
NEW
387
          if (decodedEvent) {
×
NEW
388
            return {
×
389
              contractEvent: e,
390
              decodedEvent,
391
            };
392
          }
393
        })
NEW
394
        .find(e => !!e);
×
395

396
      if (stxTransferEvent) {
4✔
397
        rawTx = createTransactionFromCoreBtcTxEvent(chainId, stxTransferEvent, coreTx.txid);
2✔
398
        txSender = stxTransferEvent.stx_transfer_event.sender;
2✔
399
      } else if (stxLockEvent) {
2!
400
        const stxStacksPox2Event =
401
          pox2Event?.decodedEvent.name === Pox2EventName.StackStx
2!
402
            ? pox2Event.decodedEvent
403
            : undefined;
404
        rawTx = createTransactionFromCoreBtcStxLockEvent(
2✔
405
          chainId,
406
          stxLockEvent,
407
          blockData.burn_block_height,
408
          coreTx.raw_result,
409
          coreTx.txid,
410
          stxStacksPox2Event
411
        );
412
        txSender = stxLockEvent.stx_lock_event.locked_address;
2✔
NEW
413
      } else if (pox2Event && pox2Event.decodedEvent.name === Pox2EventName.DelegateStx) {
×
NEW
414
        rawTx = createTransactionFromCoreBtcDelegateStxEvent(
×
415
          chainId,
416
          pox2Event.contractEvent,
417
          pox2Event.decodedEvent,
418
          coreTx.raw_result,
419
          coreTx.txid
420
        );
NEW
421
        txSender = pox2Event.decodedEvent.stacker;
×
422
      } else {
423
        logError(
×
424
          `BTC transaction found, but no STX transfer event available to recreate transaction. TX: ${JSON.stringify(
425
            coreTx
426
          )}`
427
        );
428
        throw new Error('Unable to generate transaction from BTC tx');
×
429
      }
430
    } else {
431
      rawTx = decodeTransaction(coreTx.raw_tx.substring(2));
734✔
432
      txSender = getTxSenderAddress(rawTx);
734✔
433
      sponsorAddress = getTxSponsorAddress(rawTx);
734✔
434
    }
435
    const parsedTx: CoreNodeParsedTxMessage = {
738✔
436
      core_tx: coreTx,
437
      nonce: Number(rawTx.auth.origin_condition.nonce),
438
      raw_tx: coreTx.raw_tx,
439
      parsed_tx: rawTx,
440
      block_hash: blockData.block_hash,
441
      index_block_hash: blockData.index_block_hash,
442
      parent_index_block_hash: blockData.parent_index_block_hash,
443
      parent_block_hash: blockData.parent_block_hash,
444
      parent_burn_block_hash: blockData.parent_burn_block_hash,
445
      parent_burn_block_time: blockData.parent_burn_block_timestamp,
446
      block_height: blockData.block_height,
447
      burn_block_time: blockData.burn_block_time,
448
      microblock_sequence: coreTx.microblock_sequence ?? I32_MAX,
1,404✔
449
      microblock_hash: coreTx.microblock_hash ?? '',
1,404✔
450
      sender_address: txSender,
451
      sponsor_address: sponsorAddress,
452
    };
453
    const payload = rawTx.payload;
738✔
454
    switch (payload.type_id) {
738!
455
      case TxPayloadTypeID.Coinbase: {
456
        break;
402✔
457
      }
458
      case TxPayloadTypeID.CoinbaseToAltRecipient: {
459
        if (payload.recipient.type_id === PrincipalTypeID.Standard) {
×
460
          logger.verbose(
×
461
            `Coinbase to alt recipient, standard principal: ${payload.recipient.address}`
462
          );
463
        } else {
464
          logger.verbose(
×
465
            `Coinbase to alt recipient, contract principal: ${payload.recipient.address}.${payload.recipient.contract_name}`
466
          );
467
        }
468
        break;
×
469
      }
470
      case TxPayloadTypeID.SmartContract: {
471
        logger.verbose(
84✔
472
          `Smart contract deployed: ${parsedTx.sender_address}.${payload.contract_name}`
473
        );
474
        break;
84✔
475
      }
476
      case TxPayloadTypeID.ContractCall: {
477
        logger.verbose(
81✔
478
          `Contract call: ${payload.address}.${payload.contract_name}.${payload.function_name}`
479
        );
480
        break;
81✔
481
      }
482
      case TxPayloadTypeID.TokenTransfer: {
483
        let recipientPrincipal = payload.recipient.address;
171✔
484
        if (payload.recipient.type_id === PrincipalTypeID.Contract) {
171!
485
          recipientPrincipal += '.' + payload.recipient.contract_name;
×
486
        }
487
        logger.verbose(
171✔
488
          `Token transfer: ${payload.amount} from ${parsedTx.sender_address} to ${recipientPrincipal}`
489
        );
490
        break;
171✔
491
      }
492
      case TxPayloadTypeID.PoisonMicroblock: {
493
        logger.verbose(
×
494
          `Poison microblock: header1 ${payload.microblock_header_1}), header2: ${payload.microblock_header_2}`
495
        );
496
        break;
×
497
      }
498
      case TxPayloadTypeID.VersionedSmartContract: {
499
        logger.verbose(
×
500
          `Versioned smart contract deployed: Clarity version ${payload.clarity_version}, ${parsedTx.sender_address}.${payload.contract_name}`
501
        );
502
        break;
×
503
      }
504
      default: {
505
        throw new NotImplementedError(
×
506
          `extracting data for tx type: ${getEnumDescription(
507
            TxPayloadTypeID,
508
            rawTx.payload.type_id
509
          )}`
510
        );
511
      }
512
    }
513
    return parsedTx;
738✔
514
  } catch (error) {
515
    logError(`error parsing message transaction ${JSON.stringify(coreTx)}: ${error}`, error);
×
516
    throw error;
×
517
  }
518
}
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