hive-stream 3.0.0 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/AGENTS.md +35 -0
  2. package/DOCUMENTATION.md +380 -0
  3. package/README.md +113 -22
  4. package/dist/actions.d.ts +3 -3
  5. package/dist/actions.js +7 -7
  6. package/dist/actions.js.map +1 -1
  7. package/dist/adapters/base.adapter.d.ts +19 -1
  8. package/dist/adapters/base.adapter.js +16 -0
  9. package/dist/adapters/base.adapter.js.map +1 -1
  10. package/dist/adapters/mongodb.adapter.d.ts +5 -11
  11. package/dist/adapters/mongodb.adapter.js +10 -10
  12. package/dist/adapters/mongodb.adapter.js.map +1 -1
  13. package/dist/adapters/postgresql.adapter.d.ts +17 -0
  14. package/dist/adapters/postgresql.adapter.js +99 -8
  15. package/dist/adapters/postgresql.adapter.js.map +1 -1
  16. package/dist/adapters/sqlite.adapter.d.ts +17 -0
  17. package/dist/adapters/sqlite.adapter.js +99 -8
  18. package/dist/adapters/sqlite.adapter.js.map +1 -1
  19. package/dist/api.js +86 -0
  20. package/dist/api.js.map +1 -1
  21. package/dist/config.d.ts +3 -0
  22. package/dist/config.js +6 -3
  23. package/dist/config.js.map +1 -1
  24. package/dist/contracts/coinflip.contract.d.ts +8 -26
  25. package/dist/contracts/coinflip.contract.js +123 -144
  26. package/dist/contracts/coinflip.contract.js.map +1 -1
  27. package/dist/contracts/contract.d.ts +3 -0
  28. package/dist/contracts/contract.js +26 -0
  29. package/dist/contracts/contract.js.map +1 -0
  30. package/dist/contracts/dice.contract.d.ts +9 -36
  31. package/dist/contracts/dice.contract.js +135 -200
  32. package/dist/contracts/dice.contract.js.map +1 -1
  33. package/dist/contracts/exchange.contract.d.ts +11 -0
  34. package/dist/contracts/exchange.contract.js +492 -0
  35. package/dist/contracts/exchange.contract.js.map +1 -0
  36. package/dist/contracts/lotto.contract.d.ts +15 -19
  37. package/dist/contracts/lotto.contract.js +154 -162
  38. package/dist/contracts/lotto.contract.js.map +1 -1
  39. package/dist/contracts/nft.contract.d.ts +4 -0
  40. package/dist/contracts/nft.contract.js +65 -0
  41. package/dist/contracts/nft.contract.js.map +1 -1
  42. package/dist/contracts/poll.contract.d.ts +4 -0
  43. package/dist/contracts/poll.contract.js +105 -0
  44. package/dist/contracts/poll.contract.js.map +1 -0
  45. package/dist/contracts/rps.contract.d.ts +9 -0
  46. package/dist/contracts/rps.contract.js +217 -0
  47. package/dist/contracts/rps.contract.js.map +1 -0
  48. package/dist/contracts/tipjar.contract.d.ts +4 -0
  49. package/dist/contracts/tipjar.contract.js +60 -0
  50. package/dist/contracts/tipjar.contract.js.map +1 -0
  51. package/dist/contracts/token.contract.d.ts +3 -17
  52. package/dist/contracts/token.contract.js +128 -80
  53. package/dist/contracts/token.contract.js.map +1 -1
  54. package/dist/exchanges/coingecko.d.ts +7 -1
  55. package/dist/exchanges/coingecko.js +38 -21
  56. package/dist/exchanges/coingecko.js.map +1 -1
  57. package/dist/exchanges/exchange.d.ts +15 -8
  58. package/dist/exchanges/exchange.js +65 -11
  59. package/dist/exchanges/exchange.js.map +1 -1
  60. package/dist/hive-rates.d.ts +29 -4
  61. package/dist/hive-rates.js +179 -92
  62. package/dist/hive-rates.js.map +1 -1
  63. package/dist/index.d.ts +10 -3
  64. package/dist/index.js +18 -4
  65. package/dist/index.js.map +1 -1
  66. package/dist/streamer.d.ts +101 -8
  67. package/dist/streamer.js +410 -140
  68. package/dist/streamer.js.map +1 -1
  69. package/dist/test.js +11 -12
  70. package/dist/test.js.map +1 -1
  71. package/dist/types/hive-stream.d.ts +85 -14
  72. package/dist/types/rates.d.ts +47 -0
  73. package/dist/types/rates.js +29 -0
  74. package/dist/types/rates.js.map +1 -0
  75. package/dist/utils.d.ts +318 -11
  76. package/dist/utils.js +804 -115
  77. package/dist/utils.js.map +1 -1
  78. package/examples/contracts/README.md +8 -0
  79. package/examples/contracts/exchange.ts +38 -0
  80. package/examples/contracts/poll.ts +21 -0
  81. package/examples/contracts/rps.ts +19 -0
  82. package/examples/contracts/tipjar.ts +19 -0
  83. package/package.json +20 -19
  84. package/tests/actions.spec.ts +7 -7
  85. package/tests/adapters/actions-persistence.spec.ts +4 -4
  86. package/tests/adapters/sqlite.adapter.spec.ts +2 -2
  87. package/tests/contracts/coinflip.contract.spec.ts +26 -154
  88. package/tests/contracts/dice.contract.spec.ts +24 -140
  89. package/tests/contracts/exchange.contract.spec.ts +84 -0
  90. package/tests/contracts/lotto.contract.spec.ts +30 -295
  91. package/tests/contracts/token.contract.spec.ts +72 -316
  92. package/tests/exchanges/coingecko.exchange.spec.ts +169 -0
  93. package/tests/exchanges/exchange.base.spec.ts +246 -0
  94. package/tests/helpers/mock-fetch.ts +165 -0
  95. package/tests/hive-chain-features.spec.ts +238 -0
  96. package/tests/hive-rates.spec.ts +443 -0
  97. package/tests/integration/hive-rates.integration.spec.ts +35 -0
  98. package/tests/streamer-actions.spec.ts +29 -18
  99. package/tests/streamer.spec.ts +142 -49
  100. package/tests/types/rates.spec.ts +216 -0
  101. package/tests/utils.spec.ts +27 -6
package/dist/streamer.js CHANGED
@@ -19,6 +19,7 @@ class Streamer {
19
19
  commentSubscriptions = [];
20
20
  postSubscriptions = [];
21
21
  transferSubscriptions = [];
22
+ escrowSubscriptions = [];
22
23
  attempts = 0;
23
24
  config = config_1.Config;
24
25
  client;
@@ -29,6 +30,9 @@ class Streamer {
29
30
  blockNumberTimeout = null;
30
31
  latestBlockTimer = null;
31
32
  lastBlockNumber = 0;
33
+ headBlockNumber = 0;
34
+ isPollingBlock = false;
35
+ isCatchingUp = false;
32
36
  blockId;
33
37
  previousBlockId;
34
38
  transactionId;
@@ -146,9 +150,20 @@ class Streamer {
146
150
  if (!contract) {
147
151
  throw new Error(`Contract '${action.contractName}' not found for action '${action.id}'`);
148
152
  }
149
- if (!contract.contract[action.contractMethod] || typeof contract.contract[action.contractMethod] !== 'function') {
150
- throw new Error(`Method '${action.contractMethod}' not found in contract '${action.contractName}' for action '${action.id}'`);
153
+ const actionDefinition = contract.actions?.[action.contractAction];
154
+ if (!actionDefinition || typeof actionDefinition.handler !== 'function') {
155
+ throw new Error(`Action '${action.contractAction}' not found in contract '${action.contractName}' for action '${action.id}'`);
151
156
  }
157
+ if (!this.isActionTriggerAllowed(actionDefinition, 'time')) {
158
+ throw new Error(`Action '${action.contractAction}' does not allow time triggers for action '${action.id}'`);
159
+ }
160
+ }
161
+ isActionTriggerAllowed(actionDefinition, trigger) {
162
+ const configured = actionDefinition?.trigger;
163
+ const triggers = configured
164
+ ? (Array.isArray(configured) ? configured : [configured])
165
+ : ['custom_json'];
166
+ return triggers.includes(trigger);
152
167
  }
153
168
  /**
154
169
  * Remove an action by ID
@@ -230,33 +245,42 @@ class Streamer {
230
245
  }
231
246
  return false;
232
247
  }
233
- registerContract(name, contract) {
234
- // Store an instance of the streamer
235
- contract['_instance'] = this;
236
- // Call the contract create lifecycle method if it exists
237
- if (contract && typeof contract['create'] !== 'undefined') {
238
- contract.create();
239
- }
240
- const storedReference = { name, contract };
241
- // Push the contract reference to be called later on
242
- this.contracts.push(storedReference);
243
- // Cache the contract for faster lookups
244
- this.contractCache.set(name, storedReference);
245
- return this;
248
+ async registerContract(contract) {
249
+ if (!contract || typeof contract !== 'object') {
250
+ throw new Error('Contract must be a valid definition object');
251
+ }
252
+ if (!contract.name || typeof contract.name !== 'string') {
253
+ throw new Error('Contract name must be a non-empty string');
254
+ }
255
+ if (this.contractCache.has(contract.name)) {
256
+ throw new Error(`Contract '${contract.name}' is already registered`);
257
+ }
258
+ if (!contract.actions || typeof contract.actions !== 'object') {
259
+ throw new Error(`Contract '${contract.name}' must define actions`);
260
+ }
261
+ const lifecycleContext = {
262
+ streamer: this,
263
+ adapter: this.adapter,
264
+ config: this.config
265
+ };
266
+ if (contract.hooks?.create) {
267
+ await contract.hooks.create(lifecycleContext);
268
+ }
269
+ this.contracts.push(contract);
270
+ this.contractCache.set(contract.name, contract);
246
271
  }
247
- unregisterContract(name) {
248
- // Find the registered contract by it's ID
272
+ async unregisterContract(name) {
249
273
  const contractIndex = this.contracts.findIndex(c => c.name === name);
250
274
  if (contractIndex >= 0) {
251
- // Get the contract itself
252
- const contract = this.contracts.find(c => c.name === name);
253
- // Call the contract destroy lifecycle method if it exists
254
- if (contract && typeof contract.contract['destroy'] !== 'undefined') {
255
- contract.contract.destroy();
275
+ const contract = this.contracts[contractIndex];
276
+ if (contract?.hooks?.destroy) {
277
+ await contract.hooks.destroy({
278
+ streamer: this,
279
+ adapter: this.adapter,
280
+ config: this.config
281
+ });
256
282
  }
257
- // Remove the contract
258
283
  this.contracts.splice(contractIndex, 1);
259
- // Remove from cache
260
284
  this.contractCache.delete(name);
261
285
  }
262
286
  }
@@ -290,10 +314,11 @@ class Streamer {
290
314
  if (this.config.DEBUG_MODE) {
291
315
  console.log(`Restoring state from file`);
292
316
  }
293
- if (!this.config.LAST_BLOCK_NUMBER && state?.lastBlockNumber) {
294
- if (state.lastBlockNumber) {
295
- this.lastBlockNumber = state.lastBlockNumber;
296
- }
317
+ if (this.config.RESUME_FROM_STATE && state?.lastBlockNumber) {
318
+ this.lastBlockNumber = state.lastBlockNumber;
319
+ }
320
+ else if (this.config.LAST_BLOCK_NUMBER) {
321
+ this.lastBlockNumber = this.config.LAST_BLOCK_NUMBER;
297
322
  }
298
323
  // Kicks off the blockchain streaming and operation parsing
299
324
  this.getBlock();
@@ -317,7 +342,7 @@ class Streamer {
317
342
  clearInterval(this.subscriptionCleanupInterval);
318
343
  }
319
344
  if (this?.adapter?.destroy) {
320
- this.adapter.destroy();
345
+ await this.adapter.destroy();
321
346
  }
322
347
  await (0, utils_1.sleep)(800);
323
348
  }
@@ -334,18 +359,20 @@ class Streamer {
334
359
  }
335
360
  }
336
361
  async getBlock() {
362
+ if (this.isPollingBlock) {
363
+ return;
364
+ }
365
+ this.isPollingBlock = true;
366
+ let nextDelay = this.config.BLOCK_CHECK_INTERVAL;
337
367
  try {
338
368
  // Load global properties from the Hive API
339
369
  const props = await this.client.database.getDynamicGlobalProperties();
340
370
  // We have no props, so try loading them again.
341
- if (!props && !this.disableAllProcessing) {
342
- this.blockNumberTimeout = setTimeout(() => {
343
- this.getBlock();
344
- }, this.config.BLOCK_CHECK_INTERVAL);
371
+ if (!props) {
345
372
  return;
346
373
  }
347
- // If the block number we've got is zero
348
- // set it to the last irreversible block number
374
+ this.headBlockNumber = props.head_block_number;
375
+ // If the block number we've got is zero set it to the latest head block
349
376
  if (this.lastBlockNumber === 0) {
350
377
  this.lastBlockNumber = props.head_block_number - 1;
351
378
  }
@@ -354,20 +381,21 @@ class Streamer {
354
381
  console.log(`Last block number: `, this.lastBlockNumber);
355
382
  }
356
383
  const BLOCKS_BEHIND = parseInt(this.config.BLOCKS_BEHIND_WARNING, 10);
384
+ const maxBatchSize = Math.max(1, this.config.CATCH_UP_BATCH_SIZE || 1);
385
+ const blocksBehind = Math.max(0, props.head_block_number - this.lastBlockNumber);
386
+ const blocksToProcess = Math.min(blocksBehind, maxBatchSize);
387
+ if (blocksBehind >= BLOCKS_BEHIND && this.config.DEBUG_MODE) {
388
+ console.log(`[Streamer] ${blocksBehind} blocks behind head (${props.head_block_number}). Catching up...`);
389
+ }
357
390
  if (!this.disableAllProcessing) {
358
- await this.loadBlock(this.lastBlockNumber + 1);
359
- }
360
- // We are more than 25 blocks behind, uh oh, we gotta catch up
361
- if (props.head_block_number >= (this.lastBlockNumber + BLOCKS_BEHIND) && this.config.DEBUG_MODE) {
362
- console.log(`We are more than ${BLOCKS_BEHIND} blocks behind ${props.head_block_number}, ${(this.lastBlockNumber + BLOCKS_BEHIND)}`);
363
- if (!this.disableAllProcessing) {
364
- this.getBlock();
365
- return;
391
+ for (let i = 0; i < blocksToProcess; i++) {
392
+ await this.loadBlock(this.lastBlockNumber + 1);
366
393
  }
367
394
  }
368
- // Storing timeout allows us to clear it, as this just calls itself
369
- if (!this.disableAllProcessing) {
370
- this.blockNumberTimeout = setTimeout(() => { this.getBlock(); }, this.config.BLOCK_CHECK_INTERVAL);
395
+ const remainingBehind = Math.max(0, props.head_block_number - this.lastBlockNumber);
396
+ this.isCatchingUp = remainingBehind > 0;
397
+ if (remainingBehind > 0) {
398
+ nextDelay = Math.max(0, this.config.CATCH_UP_DELAY_MS);
371
399
  }
372
400
  }
373
401
  catch (e) {
@@ -377,8 +405,13 @@ class Streamer {
377
405
  blockNumber: this.lastBlockNumber + 1
378
406
  });
379
407
  // Retry after a longer delay on error
408
+ nextDelay = this.config.BLOCK_CHECK_INTERVAL * 2;
409
+ }
410
+ finally {
411
+ this.isPollingBlock = false;
412
+ // Storing timeout allows us to clear it, as this just calls itself
380
413
  if (!this.disableAllProcessing) {
381
- this.blockNumberTimeout = setTimeout(() => { this.getBlock(); }, this.config.BLOCK_CHECK_INTERVAL * 2);
414
+ this.blockNumberTimeout = setTimeout(() => { this.getBlock(); }, nextDelay);
382
415
  }
383
416
  }
384
417
  }
@@ -453,123 +486,292 @@ class Streamer {
453
486
  this.saveStateThrottled();
454
487
  }
455
488
  async processOperation(op, blockNumber, blockId, prevBlockId, trxId, blockTime) {
489
+ const operationType = op[0];
490
+ const operationData = op[1];
491
+ const operationMetadata = {
492
+ blockNumber,
493
+ blockId,
494
+ previousBlockId: prevBlockId,
495
+ transactionId: trxId,
496
+ blockTime
497
+ };
456
498
  if (this.adapter?.processOperation) {
457
- this.adapter.processOperation(op, blockNumber, blockId, prevBlockId, trxId, blockTime);
499
+ await this.adapter.processOperation(op, blockNumber, blockId, prevBlockId, trxId, blockTime);
458
500
  }
459
501
  // Operation is a "comment" which could either be a post or comment
460
- if (op[0] === 'comment') {
502
+ if (operationType === 'comment') {
461
503
  // This is a post
462
- if (op[1].parent_author === '') {
504
+ if (operationData.parent_author === '') {
463
505
  this.postSubscriptions.forEach(sub => {
464
- sub.callback(op[1], blockNumber, blockId, prevBlockId, trxId, blockTime);
506
+ sub.callback(operationData, blockNumber, blockId, prevBlockId, trxId, blockTime);
465
507
  });
466
508
  // This is a comment
467
509
  }
468
510
  else {
469
511
  this.commentSubscriptions.forEach(sub => {
470
- sub.callback(op[1], blockNumber, blockId, prevBlockId, trxId, blockTime);
512
+ sub.callback(operationData, blockNumber, blockId, prevBlockId, trxId, blockTime);
471
513
  });
472
514
  }
473
515
  }
474
516
  // This is a transfer
475
- if (op[0] === 'transfer') {
476
- const sender = op[1]?.from;
477
- const amount = op[1]?.amount;
478
- const json = utils_2.Utils.jsonParse(op[1].memo);
479
- if (json?.[this.config.PAYLOAD_IDENTIFIER] && json?.[this.config.PAYLOAD_IDENTIFIER]?.id === this.config.JSON_ID) {
480
- // Pull out details of contract
481
- const { name, action, payload } = json[this.config.PAYLOAD_IDENTIFIER];
482
- // Do we have a contract that matches the name in the payload?
483
- const contract = this.contracts.find(c => c.name === name);
484
- if (contract) {
485
- if (this?.adapter?.processTransfer) {
486
- this.adapter.processTransfer(op[1], { name, action, payload }, { sender, amount });
487
- }
488
- if (contract?.contract?.updateBlockInfo) {
489
- contract.contract.updateBlockInfo(blockNumber, blockId, prevBlockId, trxId);
490
- }
491
- if (contract?.contract[action]) {
492
- contract.contract[action](payload, { sender, amount });
493
- }
517
+ if (operationType === 'transfer') {
518
+ const sender = operationData?.from;
519
+ const rawAmount = operationData?.amount;
520
+ const amountParts = typeof rawAmount === 'string' ? rawAmount.split(' ') : [];
521
+ const transferInfo = {
522
+ from: sender,
523
+ to: operationData?.to,
524
+ rawAmount: rawAmount || '',
525
+ amount: amountParts[0] || '',
526
+ asset: amountParts[1] || '',
527
+ memo: operationData?.memo
528
+ };
529
+ const json = utils_2.Utils.jsonParse(operationData.memo);
530
+ const payload = this.normalizeContractPayload(json?.[this.config.PAYLOAD_IDENTIFIER]);
531
+ if (payload) {
532
+ if (this?.adapter?.processTransfer) {
533
+ await this.adapter.processTransfer(operationData, payload, {
534
+ sender: sender || '',
535
+ amount: rawAmount || '',
536
+ ...operationMetadata
537
+ });
494
538
  }
539
+ const context = this.buildContractContext('transfer', blockNumber, blockId, prevBlockId, trxId, blockTime, {
540
+ sender,
541
+ transfer: transferInfo,
542
+ operation: {
543
+ type: operationType,
544
+ data: operationData
545
+ }
546
+ });
547
+ await this.dispatchContractAction(payload, context);
495
548
  }
496
549
  this.transferSubscriptions.forEach(sub => {
497
- if (sub.account === op[1].to) {
498
- sub.callback(op[1], blockNumber, blockId, prevBlockId, trxId, blockTime);
550
+ if (sub.account === operationData.to) {
551
+ sub.callback(operationData, blockNumber, blockId, prevBlockId, trxId, blockTime);
499
552
  }
500
553
  });
501
554
  }
502
555
  // This is a custom JSON operation
503
- if (op[0] === 'custom_json') {
556
+ if (operationType === 'custom_json') {
504
557
  let isSignedWithActiveKey = false;
505
558
  let sender;
506
- const id = op[1]?.id;
507
- if (op[1]?.required_auths?.length > 0) {
508
- sender = op[1].required_auths[0];
559
+ const id = operationData?.id;
560
+ if (operationData?.required_auths?.length > 0) {
561
+ sender = operationData.required_auths[0];
509
562
  isSignedWithActiveKey = true;
510
563
  }
511
- else if (op[1]?.required_posting_auths?.length > 0) {
512
- sender = op[1].required_posting_auths[0];
564
+ else if (operationData?.required_posting_auths?.length > 0) {
565
+ sender = operationData.required_posting_auths[0];
513
566
  isSignedWithActiveKey = false;
514
567
  }
515
- const json = utils_2.Utils.jsonParse(op[1].json);
516
- if (json && json?.[this.config.PAYLOAD_IDENTIFIER] && id === this.config.JSON_ID) {
517
- // Pull out details of contract
518
- const { name, action, payload } = json[this.config.PAYLOAD_IDENTIFIER];
519
- // Do we have a contract that matches the name in the payload?
520
- const contract = this.contracts.find(c => c.name === name);
521
- if (contract) {
522
- this.adapter.processCustomJson(op[1], { name, action, payload }, { sender, isSignedWithActiveKey });
523
- if (contract?.contract?.updateBlockInfo) {
524
- contract.contract.updateBlockInfo(blockNumber, blockId, prevBlockId, trxId);
525
- }
526
- if (contract?.contract[action]) {
527
- contract.contract[action](payload, { sender, isSignedWithActiveKey }, id);
528
- }
568
+ const json = utils_2.Utils.jsonParse(operationData.json);
569
+ const payload = id === this.config.JSON_ID
570
+ ? this.normalizeContractPayload(json?.[this.config.PAYLOAD_IDENTIFIER])
571
+ : null;
572
+ if (payload) {
573
+ if (this?.adapter?.processCustomJson) {
574
+ await this.adapter.processCustomJson(operationData, payload, {
575
+ sender: sender || '',
576
+ isSignedWithActiveKey,
577
+ ...operationMetadata
578
+ });
529
579
  }
580
+ const context = this.buildContractContext('custom_json', blockNumber, blockId, prevBlockId, trxId, blockTime, {
581
+ sender,
582
+ customJson: {
583
+ id,
584
+ json,
585
+ isSignedWithActiveKey
586
+ },
587
+ operation: {
588
+ type: operationType,
589
+ data: operationData
590
+ }
591
+ });
592
+ await this.dispatchContractAction(payload, context);
530
593
  }
531
594
  this.customJsonSubscriptions.forEach(sub => {
532
- sub.callback(op[1], { sender, isSignedWithActiveKey }, blockNumber, blockId, prevBlockId, trxId, blockTime);
595
+ sub.callback(operationData, { sender, isSignedWithActiveKey }, blockNumber, blockId, prevBlockId, trxId, blockTime);
533
596
  });
534
597
  this.customJsonIdSubscriptions.forEach(sub => {
535
- const byId = this.customJsonIdSubscriptions.find(s => s.id === op[1].id);
536
- if (byId) {
537
- sub.callback(op[1], { sender, isSignedWithActiveKey }, blockNumber, blockId, prevBlockId, trxId, blockTime);
598
+ if (sub.id === operationData.id) {
599
+ sub.callback(operationData, { sender, isSignedWithActiveKey }, blockNumber, blockId, prevBlockId, trxId, blockTime);
538
600
  }
539
601
  });
540
- utils_2.Utils.asyncForEach(this.customJsonHiveEngineSubscriptions, async (sub) => {
541
- let isSignedWithActiveKey = null;
542
- let sender;
543
- if (op[1].required_auths.length > 0) {
544
- sender = op[1].required_auths[0];
545
- isSignedWithActiveKey = true;
602
+ if (id === this.config.HIVE_ENGINE_ID && this.customJsonHiveEngineSubscriptions.length > 0) {
603
+ const enginePayload = json || {};
604
+ const { contractName, contractAction, contractPayload } = enginePayload;
605
+ try {
606
+ const txInfo = await this.hive.getTransactionInfo(trxId);
607
+ const logs = txInfo && txInfo.logs ? utils_2.Utils.jsonParse(txInfo.logs) : null;
608
+ if (txInfo && logs && typeof logs.errors === 'undefined') {
609
+ await Promise.all(this.customJsonHiveEngineSubscriptions.map(async (sub) => {
610
+ sub.callback(contractName, contractAction, contractPayload, sender, operationData, blockNumber, blockId, prevBlockId, trxId, blockTime);
611
+ }));
612
+ }
546
613
  }
547
- else {
548
- sender = op[1].required_posting_auths[0];
549
- isSignedWithActiveKey = false;
614
+ catch (e) {
615
+ console.error(e);
550
616
  }
551
- const id = op[1].id;
552
- const json = utils_2.Utils.jsonParse(op[1].json);
553
- // Hive Engine JSON operation
554
- if (id === this.config.HIVE_ENGINE_ID) {
555
- const { contractName, contractAction, contractPayload } = json;
556
- try {
557
- // Attempt to get the transaction from Hive Engine itself
558
- const txInfo = await this.hive.getTransactionInfo(trxId);
559
- const logs = txInfo && txInfo.logs ? utils_2.Utils.jsonParse(txInfo.logs) : null;
560
- // Do we have a valid transaction and are there no errors? It's a real transaction
561
- if (txInfo && logs && typeof logs.errors === 'undefined') {
562
- sub.callback(contractName, contractAction, contractPayload, sender, op[1], blockNumber, blockId, prevBlockId, trxId, blockTime);
563
- }
564
- }
565
- catch (e) {
566
- console.error(e);
567
- return;
617
+ }
618
+ }
619
+ // Recurrent transfers carry payloads in memo, similar to transfer.
620
+ if (operationType === 'recurrent_transfer') {
621
+ const sender = operationData?.from;
622
+ const json = utils_2.Utils.jsonParse(operationData?.memo);
623
+ const payload = this.normalizeContractPayload(json?.[this.config.PAYLOAD_IDENTIFIER]);
624
+ if (payload) {
625
+ const context = this.buildContractContext('recurrent_transfer', blockNumber, blockId, prevBlockId, trxId, blockTime, {
626
+ sender,
627
+ operation: {
628
+ type: operationType,
629
+ data: operationData
568
630
  }
631
+ });
632
+ await this.dispatchContractAction(payload, context);
633
+ }
634
+ }
635
+ if (this.isEscrowOperationType(operationType)) {
636
+ const escrow = this.buildEscrowDetails(operationType, operationData);
637
+ const sender = operationData?.from;
638
+ if (this.adapter?.processEscrow) {
639
+ await this.adapter.processEscrow(operationType, operationData, operationMetadata);
640
+ }
641
+ if (operationType === 'escrow_transfer') {
642
+ const jsonMeta = utils_2.Utils.jsonParse(operationData?.json_meta);
643
+ const payload = this.normalizeContractPayload(jsonMeta?.[this.config.PAYLOAD_IDENTIFIER]);
644
+ if (payload) {
645
+ const context = this.buildContractContext('escrow_transfer', blockNumber, blockId, prevBlockId, trxId, blockTime, {
646
+ sender,
647
+ escrow,
648
+ operation: {
649
+ type: operationType,
650
+ data: operationData
651
+ }
652
+ });
653
+ await this.dispatchContractAction(payload, context);
654
+ }
655
+ }
656
+ this.escrowSubscriptions.forEach(sub => {
657
+ if (sub.type === operationType) {
658
+ sub.callback(operationData, blockNumber, blockId, prevBlockId, trxId, blockTime);
569
659
  }
570
660
  });
571
661
  }
572
662
  }
663
+ normalizeContractPayload(payload) {
664
+ if (!payload || typeof payload !== 'object') {
665
+ return null;
666
+ }
667
+ const contract = typeof payload.contract === 'string'
668
+ ? payload.contract
669
+ : (typeof payload.name === 'string' ? payload.name : null);
670
+ const action = typeof payload.action === 'string' ? payload.action : null;
671
+ if (!contract || !action) {
672
+ return null;
673
+ }
674
+ if (!payload.contract && payload.name && this.config.DEBUG_MODE) {
675
+ console.warn('[Streamer] Legacy contract payload detected (name/action). Please migrate to { contract, action, payload }.');
676
+ }
677
+ return {
678
+ contract,
679
+ action,
680
+ payload: payload.payload ?? {},
681
+ meta: payload.meta ?? payload.metadata
682
+ };
683
+ }
684
+ isEscrowOperationType(operationType) {
685
+ return operationType === 'escrow_transfer'
686
+ || operationType === 'escrow_approve'
687
+ || operationType === 'escrow_dispute'
688
+ || operationType === 'escrow_release';
689
+ }
690
+ buildEscrowDetails(operationType, operation) {
691
+ return {
692
+ type: operationType,
693
+ from: operation?.from,
694
+ to: operation?.to,
695
+ agent: operation?.agent,
696
+ escrowId: operation?.escrow_id,
697
+ who: operation?.who,
698
+ receiver: operation?.receiver,
699
+ hiveAmount: operation?.hive_amount,
700
+ hbdAmount: operation?.hbd_amount,
701
+ fee: operation?.fee,
702
+ ratificationDeadline: operation?.ratification_deadline,
703
+ expiration: operation?.escrow_expiration,
704
+ approved: typeof operation?.approve === 'boolean' ? operation.approve : undefined
705
+ };
706
+ }
707
+ buildContractContext(trigger, blockNumber, blockId, previousBlockId, transactionId, blockTime, details) {
708
+ return {
709
+ trigger,
710
+ streamer: this,
711
+ adapter: this.adapter,
712
+ config: this.config,
713
+ block: {
714
+ number: blockNumber,
715
+ id: blockId,
716
+ previousId: previousBlockId,
717
+ time: blockTime
718
+ },
719
+ transaction: {
720
+ id: transactionId
721
+ },
722
+ sender: details.sender,
723
+ transfer: details.transfer,
724
+ customJson: details.customJson,
725
+ escrow: details.escrow,
726
+ operation: details.operation
727
+ };
728
+ }
729
+ async dispatchContractAction(payload, context) {
730
+ const contract = this.contractCache.get(payload.contract) ||
731
+ this.contracts.find(c => c.name === payload.contract);
732
+ if (!contract) {
733
+ console.warn(`[Streamer] Contract '${payload.contract}' not found for action '${payload.action}'`);
734
+ return;
735
+ }
736
+ if (contract && !this.contractCache.has(payload.contract)) {
737
+ this.contractCache.set(payload.contract, contract);
738
+ }
739
+ const actionDefinition = contract.actions?.[payload.action];
740
+ if (!actionDefinition || typeof actionDefinition.handler !== 'function') {
741
+ console.warn(`[Streamer] Action '${payload.action}' not found in contract '${payload.contract}'`);
742
+ return;
743
+ }
744
+ if (!this.isActionTriggerAllowed(actionDefinition, context.trigger)) {
745
+ console.warn(`[Streamer] Action '${payload.action}' does not allow trigger '${context.trigger}'`);
746
+ return;
747
+ }
748
+ if (actionDefinition.requiresActiveKey &&
749
+ context.trigger === 'custom_json' &&
750
+ !context.customJson?.isSignedWithActiveKey) {
751
+ console.warn(`[Streamer] Action '${payload.action}' requires active key signature`);
752
+ return;
753
+ }
754
+ let actionPayload = payload.payload ?? {};
755
+ if (actionDefinition.schema) {
756
+ const result = actionDefinition.schema.safeParse(actionPayload);
757
+ if (!result.success) {
758
+ console.warn(`[Streamer] Invalid payload for ${payload.contract}.${payload.action}`, {
759
+ errors: result.error?.errors
760
+ });
761
+ return;
762
+ }
763
+ actionPayload = result.data;
764
+ }
765
+ try {
766
+ await actionDefinition.handler(actionPayload, context);
767
+ }
768
+ catch (error) {
769
+ console.error(`[Streamer] Contract action error for ${payload.contract}.${payload.action}:`, error);
770
+ if (context.trigger === 'time') {
771
+ throw error;
772
+ }
773
+ }
774
+ }
573
775
  async processActions() {
574
776
  if (!this.latestBlockchainTime || this.actions.length === 0) {
575
777
  return;
@@ -596,8 +798,13 @@ class Streamer {
596
798
  console.warn(`[Streamer] Contract '${action.contractName}' not found for action '${action.id}'`);
597
799
  continue;
598
800
  }
599
- if (!contract?.contract?.[action.contractMethod] || typeof contract.contract[action.contractMethod] !== 'function') {
600
- console.warn(`[Streamer] Method '${action.contractMethod}' not found in contract '${action.contractName}' for action '${action.id}'`);
801
+ const actionDefinition = contract.actions?.[action.contractAction];
802
+ if (!actionDefinition || typeof actionDefinition.handler !== 'function') {
803
+ console.warn(`[Streamer] Action '${action.contractAction}' not found in contract '${action.contractName}' for action '${action.id}'`);
804
+ continue;
805
+ }
806
+ if (!this.isActionTriggerAllowed(actionDefinition, 'time')) {
807
+ console.warn(`[Streamer] Action '${action.contractAction}' does not allow time triggers for action '${action.id}'`);
601
808
  continue;
602
809
  }
603
810
  // Get frequency in seconds from optimized map
@@ -613,7 +820,12 @@ class Streamer {
613
820
  if (differenceSeconds >= frequencySeconds) {
614
821
  try {
615
822
  // Execute the action with error isolation
616
- await this.executeAction(action, contract);
823
+ const context = this.buildContractContext('time', this.lastBlockNumber, this.blockId, this.previousBlockId, action.id, this.latestBlockchainTime || new Date(), {});
824
+ await this.dispatchContractAction({
825
+ contract: action.contractName,
826
+ action: action.contractAction,
827
+ payload: action.payload || {}
828
+ }, context);
617
829
  // Reset the action timer and increment execution count
618
830
  action.reset();
619
831
  action.incrementExecutionCount();
@@ -624,7 +836,7 @@ class Streamer {
624
836
  }
625
837
  catch (error) {
626
838
  const err = error instanceof Error ? error : new Error(String(error));
627
- console.error(`[Streamer] Action execution error for ${action.contractName}.${action.contractMethod}:`, {
839
+ console.error(`[Streamer] Action execution error for ${action.contractName}.${action.contractAction}:`, {
628
840
  actionId: action.id,
629
841
  error: err.message,
630
842
  stack: err.stack,
@@ -642,18 +854,6 @@ class Streamer {
642
854
  // Clean up disabled or completed actions periodically
643
855
  this.cleanupActions();
644
856
  }
645
- /**
646
- * Execute a single action with proper isolation
647
- */
648
- async executeAction(action, contract) {
649
- const method = contract.contract[action.contractMethod];
650
- if (method.constructor.name === 'AsyncFunction') {
651
- await method.call(contract.contract, action.payload);
652
- }
653
- else {
654
- method.call(contract.contract, action.payload);
655
- }
656
- }
657
857
  /**
658
858
  * Clean up completed or disabled actions to prevent memory leaks
659
859
  */
@@ -705,8 +905,44 @@ class Streamer {
705
905
  transferHiveTokensMultiple(from, accounts = [], amount = '0', symbol, memo = '') {
706
906
  return utils_2.Utils.transferHiveTokensMultiple(this.client, this.config, from, accounts, amount, symbol, memo);
707
907
  }
908
+ broadcastOperations(operations, signingKeys) {
909
+ return utils_2.Utils.broadcastOperations(this.client, operations, signingKeys || this.config.ACTIVE_KEY);
910
+ }
911
+ broadcastMultiSigOperations(operations, signingKeys) {
912
+ return utils_2.Utils.broadcastMultiSigOperations(this.client, operations, signingKeys);
913
+ }
914
+ createAuthority(keyAuths = [], accountAuths = [], weightThreshold = 1) {
915
+ return utils_2.Utils.createAuthority(keyAuths, accountAuths, weightThreshold);
916
+ }
917
+ updateAccountAuthorities(account, authorityUpdate, signingKeys) {
918
+ return utils_2.Utils.updateAccountAuthorities(this.client, this.config, account, authorityUpdate, signingKeys);
919
+ }
920
+ escrowTransfer(options, signingKeys) {
921
+ return utils_2.Utils.escrowTransfer(this.client, this.config, options, signingKeys);
922
+ }
923
+ escrowApprove(options, signingKeys) {
924
+ return utils_2.Utils.escrowApprove(this.client, this.config, options, signingKeys);
925
+ }
926
+ escrowDispute(options, signingKeys) {
927
+ return utils_2.Utils.escrowDispute(this.client, this.config, options, signingKeys);
928
+ }
929
+ escrowRelease(options, signingKeys) {
930
+ return utils_2.Utils.escrowRelease(this.client, this.config, options, signingKeys);
931
+ }
932
+ recurrentTransfer(options, signingKeys) {
933
+ return utils_2.Utils.recurrentTransfer(this.client, this.config, options, signingKeys);
934
+ }
935
+ createProposal(options, signingKeys) {
936
+ return utils_2.Utils.createProposal(this.client, this.config, options, signingKeys);
937
+ }
938
+ updateProposalVotes(options, signingKeys) {
939
+ return utils_2.Utils.updateProposalVotes(this.client, this.config, options, signingKeys);
940
+ }
941
+ removeProposals(options, signingKeys) {
942
+ return utils_2.Utils.removeProposals(this.client, this.config, options, signingKeys);
943
+ }
708
944
  transferHiveEngineTokens(from, to, symbol, quantity, memo = '') {
709
- return utils_2.Utils.transferHiveEngineTokens(this.client, this.config, from, to, symbol, quantity, memo);
945
+ return utils_2.Utils.transferHiveEngineTokens(this.client, this.config, from, to, quantity, symbol, memo);
710
946
  }
711
947
  transferHiveEngineTokensMultiple(from, accounts = [], symbol, memo = '', amount = '0') {
712
948
  return utils_2.Utils.transferHiveEngineTokensMultiple(this.client, this.config, from, accounts, symbol, memo, amount);
@@ -726,6 +962,17 @@ class Streamer {
726
962
  getTransaction(blockNumber, transactionId) {
727
963
  return utils_2.Utils.getTransaction(this.client, blockNumber, transactionId);
728
964
  }
965
+ getStatus() {
966
+ return {
967
+ lastBlockNumber: this.lastBlockNumber,
968
+ headBlockNumber: this.headBlockNumber,
969
+ blocksBehind: this.headBlockNumber
970
+ ? Math.max(0, this.headBlockNumber - this.lastBlockNumber)
971
+ : 0,
972
+ latestBlockchainTime: this.latestBlockchainTime,
973
+ isCatchingUp: this.isCatchingUp
974
+ };
975
+ }
729
976
  verifyTransfer(transaction, from, to, amount) {
730
977
  return utils_2.Utils.verifyTransfer(transaction, from, to, amount);
731
978
  }
@@ -754,6 +1001,18 @@ class Streamer {
754
1001
  onHiveEngine(callback) {
755
1002
  this.customJsonHiveEngineSubscriptions.push({ callback });
756
1003
  }
1004
+ onEscrowTransfer(callback) {
1005
+ this.escrowSubscriptions.push({ type: 'escrow_transfer', callback });
1006
+ }
1007
+ onEscrowApprove(callback) {
1008
+ this.escrowSubscriptions.push({ type: 'escrow_approve', callback });
1009
+ }
1010
+ onEscrowDispute(callback) {
1011
+ this.escrowSubscriptions.push({ type: 'escrow_dispute', callback });
1012
+ }
1013
+ onEscrowRelease(callback) {
1014
+ this.escrowSubscriptions.push({ type: 'escrow_release', callback });
1015
+ }
757
1016
  // Memory management: cleanup subscriptions
758
1017
  cleanupSubscriptions() {
759
1018
  // Limit subscription arrays to prevent memory leaks
@@ -781,6 +1040,10 @@ class Streamer {
781
1040
  this.transferSubscriptions = this.transferSubscriptions.slice(-this.maxSubscriptions);
782
1041
  console.warn(`[Streamer] Trimmed transferSubscriptions to ${this.maxSubscriptions} items`);
783
1042
  }
1043
+ if (this.escrowSubscriptions.length > this.maxSubscriptions) {
1044
+ this.escrowSubscriptions = this.escrowSubscriptions.slice(-this.maxSubscriptions);
1045
+ console.warn(`[Streamer] Trimmed escrowSubscriptions to ${this.maxSubscriptions} items`);
1046
+ }
784
1047
  }
785
1048
  // Add method to remove specific subscriptions
786
1049
  removeTransferSubscription(account) {
@@ -789,6 +1052,13 @@ class Streamer {
789
1052
  removeCustomJsonIdSubscription(id) {
790
1053
  this.customJsonIdSubscriptions = this.customJsonIdSubscriptions.filter(sub => sub.id !== id);
791
1054
  }
1055
+ removeEscrowSubscriptions(type) {
1056
+ if (!type) {
1057
+ this.escrowSubscriptions = [];
1058
+ return;
1059
+ }
1060
+ this.escrowSubscriptions = this.escrowSubscriptions.filter(sub => sub.type !== type);
1061
+ }
792
1062
  }
793
1063
  exports.Streamer = Streamer;
794
1064
  //# sourceMappingURL=streamer.js.map