hive-stream 3.0.0 → 3.0.2

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 (107) hide show
  1. package/AGENTS.md +35 -0
  2. package/DOCUMENTATION.md +396 -0
  3. package/README.md +160 -39
  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 +26 -0
  22. package/dist/config.js +76 -4
  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 +11 -3
  64. package/dist/index.js +19 -4
  65. package/dist/index.js.map +1 -1
  66. package/dist/metadata.d.ts +63 -0
  67. package/dist/metadata.js +407 -0
  68. package/dist/metadata.js.map +1 -0
  69. package/dist/streamer.d.ts +104 -11
  70. package/dist/streamer.js +415 -143
  71. package/dist/streamer.js.map +1 -1
  72. package/dist/test.js +11 -12
  73. package/dist/test.js.map +1 -1
  74. package/dist/types/hive-stream.d.ts +85 -14
  75. package/dist/types/rates.d.ts +47 -0
  76. package/dist/types/rates.js +29 -0
  77. package/dist/types/rates.js.map +1 -0
  78. package/dist/utils.d.ts +318 -11
  79. package/dist/utils.js +804 -115
  80. package/dist/utils.js.map +1 -1
  81. package/examples/contracts/README.md +8 -0
  82. package/examples/contracts/exchange.ts +38 -0
  83. package/examples/contracts/poll.ts +21 -0
  84. package/examples/contracts/rps.ts +19 -0
  85. package/examples/contracts/tipjar.ts +19 -0
  86. package/package.json +20 -19
  87. package/test-contract-block.md +3 -3
  88. package/tests/actions.spec.ts +7 -7
  89. package/tests/adapters/actions-persistence.spec.ts +4 -4
  90. package/tests/adapters/sqlite.adapter.spec.ts +2 -2
  91. package/tests/config-input.spec.ts +90 -0
  92. package/tests/contracts/coinflip.contract.spec.ts +26 -154
  93. package/tests/contracts/dice.contract.spec.ts +24 -140
  94. package/tests/contracts/exchange.contract.spec.ts +84 -0
  95. package/tests/contracts/lotto.contract.spec.ts +30 -295
  96. package/tests/contracts/token.contract.spec.ts +72 -316
  97. package/tests/exchanges/coingecko.exchange.spec.ts +169 -0
  98. package/tests/exchanges/exchange.base.spec.ts +246 -0
  99. package/tests/helpers/mock-fetch.ts +165 -0
  100. package/tests/hive-chain-features.spec.ts +319 -0
  101. package/tests/hive-rates.spec.ts +443 -0
  102. package/tests/integration/hive-rates.integration.spec.ts +35 -0
  103. package/tests/metadata.spec.ts +63 -0
  104. package/tests/streamer-actions.spec.ts +29 -18
  105. package/tests/streamer.spec.ts +142 -49
  106. package/tests/types/rates.spec.ts +216 -0
  107. package/tests/utils.spec.ts +27 -6
package/dist/streamer.js CHANGED
@@ -19,8 +19,9 @@ class Streamer {
19
19
  commentSubscriptions = [];
20
20
  postSubscriptions = [];
21
21
  transferSubscriptions = [];
22
+ escrowSubscriptions = [];
22
23
  attempts = 0;
23
- config = config_1.Config;
24
+ config = (0, config_1.createConfig)();
24
25
  client;
25
26
  hive;
26
27
  username;
@@ -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;
@@ -63,7 +67,7 @@ class Streamer {
63
67
  maxCacheSize = 1000;
64
68
  utils = utils_2.Utils;
65
69
  constructor(userConfig = {}) {
66
- this.config = Object.assign(config_1.Config, userConfig);
70
+ this.config = (0, config_1.createConfig)(userConfig);
67
71
  this.lastBlockNumber = this.config.LAST_BLOCK_NUMBER;
68
72
  this.username = this.config.USERNAME;
69
73
  this.postingKey = this.config.POSTING_KEY;
@@ -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
  }
@@ -268,7 +292,7 @@ class Streamer {
268
292
  * @param config
269
293
  */
270
294
  setConfig(config) {
271
- Object.assign(this.config, config);
295
+ Object.assign(this.config, (0, config_1.normalizeConfigInput)(config));
272
296
  // Set keys and username incase they have changed
273
297
  this.username = this.config.USERNAME;
274
298
  this.postingKey = this.config.POSTING_KEY;
@@ -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,294 @@ 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
+ let hasVerificationErrors = false;
606
+ try {
607
+ const txInfo = await this.hive.getTransactionInfo(trxId);
608
+ const logs = txInfo && txInfo.logs ? utils_2.Utils.jsonParse(txInfo.logs) : null;
609
+ hasVerificationErrors = Boolean(txInfo && logs && typeof logs.errors !== 'undefined');
546
610
  }
547
- else {
548
- sender = op[1].required_posting_auths[0];
549
- isSignedWithActiveKey = false;
611
+ catch (e) {
612
+ console.error(e);
550
613
  }
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;
614
+ if (!hasVerificationErrors) {
615
+ await Promise.all(this.customJsonHiveEngineSubscriptions.map(async (sub) => {
616
+ sub.callback(contractName, contractAction, contractPayload, sender, operationData, blockNumber, blockId, prevBlockId, trxId, blockTime);
617
+ }));
618
+ }
619
+ }
620
+ }
621
+ // Recurrent transfers carry payloads in memo, similar to transfer.
622
+ if (operationType === 'recurrent_transfer') {
623
+ const sender = operationData?.from;
624
+ const json = utils_2.Utils.jsonParse(operationData?.memo);
625
+ const payload = this.normalizeContractPayload(json?.[this.config.PAYLOAD_IDENTIFIER]);
626
+ if (payload) {
627
+ const context = this.buildContractContext('recurrent_transfer', blockNumber, blockId, prevBlockId, trxId, blockTime, {
628
+ sender,
629
+ operation: {
630
+ type: operationType,
631
+ data: operationData
568
632
  }
633
+ });
634
+ await this.dispatchContractAction(payload, context);
635
+ }
636
+ }
637
+ if (this.isEscrowOperationType(operationType)) {
638
+ const escrow = this.buildEscrowDetails(operationType, operationData);
639
+ const sender = operationData?.from;
640
+ if (this.adapter?.processEscrow) {
641
+ await this.adapter.processEscrow(operationType, operationData, operationMetadata);
642
+ }
643
+ if (operationType === 'escrow_transfer') {
644
+ const jsonMeta = utils_2.Utils.jsonParse(operationData?.json_meta);
645
+ const payload = this.normalizeContractPayload(jsonMeta?.[this.config.PAYLOAD_IDENTIFIER]);
646
+ if (payload) {
647
+ const context = this.buildContractContext('escrow_transfer', blockNumber, blockId, prevBlockId, trxId, blockTime, {
648
+ sender,
649
+ escrow,
650
+ operation: {
651
+ type: operationType,
652
+ data: operationData
653
+ }
654
+ });
655
+ await this.dispatchContractAction(payload, context);
656
+ }
657
+ }
658
+ this.escrowSubscriptions.forEach(sub => {
659
+ if (sub.type === operationType) {
660
+ sub.callback(operationData, blockNumber, blockId, prevBlockId, trxId, blockTime);
569
661
  }
570
662
  });
571
663
  }
572
664
  }
665
+ normalizeContractPayload(payload) {
666
+ if (!payload || typeof payload !== 'object') {
667
+ return null;
668
+ }
669
+ const contract = typeof payload.contract === 'string'
670
+ ? payload.contract
671
+ : (typeof payload.name === 'string' ? payload.name : null);
672
+ const action = typeof payload.action === 'string' ? payload.action : null;
673
+ if (!contract || !action) {
674
+ return null;
675
+ }
676
+ if (!payload.contract && payload.name && this.config.DEBUG_MODE) {
677
+ console.warn('[Streamer] Legacy contract payload detected (name/action). Please migrate to { contract, action, payload }.');
678
+ }
679
+ return {
680
+ contract,
681
+ action,
682
+ payload: payload.payload ?? {},
683
+ meta: payload.meta ?? payload.metadata
684
+ };
685
+ }
686
+ isEscrowOperationType(operationType) {
687
+ return operationType === 'escrow_transfer'
688
+ || operationType === 'escrow_approve'
689
+ || operationType === 'escrow_dispute'
690
+ || operationType === 'escrow_release';
691
+ }
692
+ buildEscrowDetails(operationType, operation) {
693
+ return {
694
+ type: operationType,
695
+ from: operation?.from,
696
+ to: operation?.to,
697
+ agent: operation?.agent,
698
+ escrowId: operation?.escrow_id,
699
+ who: operation?.who,
700
+ receiver: operation?.receiver,
701
+ hiveAmount: operation?.hive_amount,
702
+ hbdAmount: operation?.hbd_amount,
703
+ fee: operation?.fee,
704
+ ratificationDeadline: operation?.ratification_deadline,
705
+ expiration: operation?.escrow_expiration,
706
+ approved: typeof operation?.approve === 'boolean' ? operation.approve : undefined
707
+ };
708
+ }
709
+ buildContractContext(trigger, blockNumber, blockId, previousBlockId, transactionId, blockTime, details) {
710
+ return {
711
+ trigger,
712
+ streamer: this,
713
+ adapter: this.adapter,
714
+ config: this.config,
715
+ block: {
716
+ number: blockNumber,
717
+ id: blockId,
718
+ previousId: previousBlockId,
719
+ time: blockTime
720
+ },
721
+ transaction: {
722
+ id: transactionId
723
+ },
724
+ sender: details.sender,
725
+ transfer: details.transfer,
726
+ customJson: details.customJson,
727
+ escrow: details.escrow,
728
+ operation: details.operation
729
+ };
730
+ }
731
+ async dispatchContractAction(payload, context) {
732
+ const contract = this.contractCache.get(payload.contract) ||
733
+ this.contracts.find(c => c.name === payload.contract);
734
+ if (!contract) {
735
+ console.warn(`[Streamer] Contract '${payload.contract}' not found for action '${payload.action}'`);
736
+ return;
737
+ }
738
+ if (contract && !this.contractCache.has(payload.contract)) {
739
+ this.contractCache.set(payload.contract, contract);
740
+ }
741
+ const actionDefinition = contract.actions?.[payload.action];
742
+ if (!actionDefinition || typeof actionDefinition.handler !== 'function') {
743
+ console.warn(`[Streamer] Action '${payload.action}' not found in contract '${payload.contract}'`);
744
+ return;
745
+ }
746
+ if (!this.isActionTriggerAllowed(actionDefinition, context.trigger)) {
747
+ console.warn(`[Streamer] Action '${payload.action}' does not allow trigger '${context.trigger}'`);
748
+ return;
749
+ }
750
+ if (actionDefinition.requiresActiveKey &&
751
+ context.trigger === 'custom_json' &&
752
+ !context.customJson?.isSignedWithActiveKey) {
753
+ console.warn(`[Streamer] Action '${payload.action}' requires active key signature`);
754
+ return;
755
+ }
756
+ let actionPayload = payload.payload ?? {};
757
+ if (actionDefinition.schema) {
758
+ const result = actionDefinition.schema.safeParse(actionPayload);
759
+ if (!result.success) {
760
+ console.warn(`[Streamer] Invalid payload for ${payload.contract}.${payload.action}`, {
761
+ errors: result.error?.errors
762
+ });
763
+ return;
764
+ }
765
+ actionPayload = result.data;
766
+ }
767
+ try {
768
+ await actionDefinition.handler(actionPayload, context);
769
+ }
770
+ catch (error) {
771
+ console.error(`[Streamer] Contract action error for ${payload.contract}.${payload.action}:`, error);
772
+ if (context.trigger === 'time') {
773
+ throw error;
774
+ }
775
+ }
776
+ }
573
777
  async processActions() {
574
778
  if (!this.latestBlockchainTime || this.actions.length === 0) {
575
779
  return;
@@ -596,8 +800,13 @@ class Streamer {
596
800
  console.warn(`[Streamer] Contract '${action.contractName}' not found for action '${action.id}'`);
597
801
  continue;
598
802
  }
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}'`);
803
+ const actionDefinition = contract.actions?.[action.contractAction];
804
+ if (!actionDefinition || typeof actionDefinition.handler !== 'function') {
805
+ console.warn(`[Streamer] Action '${action.contractAction}' not found in contract '${action.contractName}' for action '${action.id}'`);
806
+ continue;
807
+ }
808
+ if (!this.isActionTriggerAllowed(actionDefinition, 'time')) {
809
+ console.warn(`[Streamer] Action '${action.contractAction}' does not allow time triggers for action '${action.id}'`);
601
810
  continue;
602
811
  }
603
812
  // Get frequency in seconds from optimized map
@@ -613,7 +822,12 @@ class Streamer {
613
822
  if (differenceSeconds >= frequencySeconds) {
614
823
  try {
615
824
  // Execute the action with error isolation
616
- await this.executeAction(action, contract);
825
+ const context = this.buildContractContext('time', this.lastBlockNumber, this.blockId, this.previousBlockId, action.id, this.latestBlockchainTime || new Date(), {});
826
+ await this.dispatchContractAction({
827
+ contract: action.contractName,
828
+ action: action.contractAction,
829
+ payload: action.payload || {}
830
+ }, context);
617
831
  // Reset the action timer and increment execution count
618
832
  action.reset();
619
833
  action.incrementExecutionCount();
@@ -624,7 +838,7 @@ class Streamer {
624
838
  }
625
839
  catch (error) {
626
840
  const err = error instanceof Error ? error : new Error(String(error));
627
- console.error(`[Streamer] Action execution error for ${action.contractName}.${action.contractMethod}:`, {
841
+ console.error(`[Streamer] Action execution error for ${action.contractName}.${action.contractAction}:`, {
628
842
  actionId: action.id,
629
843
  error: err.message,
630
844
  stack: err.stack,
@@ -642,18 +856,6 @@ class Streamer {
642
856
  // Clean up disabled or completed actions periodically
643
857
  this.cleanupActions();
644
858
  }
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
859
  /**
658
860
  * Clean up completed or disabled actions to prevent memory leaks
659
861
  */
@@ -705,8 +907,44 @@ class Streamer {
705
907
  transferHiveTokensMultiple(from, accounts = [], amount = '0', symbol, memo = '') {
706
908
  return utils_2.Utils.transferHiveTokensMultiple(this.client, this.config, from, accounts, amount, symbol, memo);
707
909
  }
910
+ broadcastOperations(operations, signingKeys) {
911
+ return utils_2.Utils.broadcastOperations(this.client, operations, signingKeys || this.config.ACTIVE_KEY);
912
+ }
913
+ broadcastMultiSigOperations(operations, signingKeys) {
914
+ return utils_2.Utils.broadcastMultiSigOperations(this.client, operations, signingKeys);
915
+ }
916
+ createAuthority(keyAuths = [], accountAuths = [], weightThreshold = 1) {
917
+ return utils_2.Utils.createAuthority(keyAuths, accountAuths, weightThreshold);
918
+ }
919
+ updateAccountAuthorities(account, authorityUpdate, signingKeys) {
920
+ return utils_2.Utils.updateAccountAuthorities(this.client, this.config, account, authorityUpdate, signingKeys);
921
+ }
922
+ escrowTransfer(options, signingKeys) {
923
+ return utils_2.Utils.escrowTransfer(this.client, this.config, options, signingKeys);
924
+ }
925
+ escrowApprove(options, signingKeys) {
926
+ return utils_2.Utils.escrowApprove(this.client, this.config, options, signingKeys);
927
+ }
928
+ escrowDispute(options, signingKeys) {
929
+ return utils_2.Utils.escrowDispute(this.client, this.config, options, signingKeys);
930
+ }
931
+ escrowRelease(options, signingKeys) {
932
+ return utils_2.Utils.escrowRelease(this.client, this.config, options, signingKeys);
933
+ }
934
+ recurrentTransfer(options, signingKeys) {
935
+ return utils_2.Utils.recurrentTransfer(this.client, this.config, options, signingKeys);
936
+ }
937
+ createProposal(options, signingKeys) {
938
+ return utils_2.Utils.createProposal(this.client, this.config, options, signingKeys);
939
+ }
940
+ updateProposalVotes(options, signingKeys) {
941
+ return utils_2.Utils.updateProposalVotes(this.client, this.config, options, signingKeys);
942
+ }
943
+ removeProposals(options, signingKeys) {
944
+ return utils_2.Utils.removeProposals(this.client, this.config, options, signingKeys);
945
+ }
708
946
  transferHiveEngineTokens(from, to, symbol, quantity, memo = '') {
709
- return utils_2.Utils.transferHiveEngineTokens(this.client, this.config, from, to, symbol, quantity, memo);
947
+ return utils_2.Utils.transferHiveEngineTokens(this.client, this.config, from, to, quantity, symbol, memo);
710
948
  }
711
949
  transferHiveEngineTokensMultiple(from, accounts = [], symbol, memo = '', amount = '0') {
712
950
  return utils_2.Utils.transferHiveEngineTokensMultiple(this.client, this.config, from, accounts, symbol, memo, amount);
@@ -726,6 +964,17 @@ class Streamer {
726
964
  getTransaction(blockNumber, transactionId) {
727
965
  return utils_2.Utils.getTransaction(this.client, blockNumber, transactionId);
728
966
  }
967
+ getStatus() {
968
+ return {
969
+ lastBlockNumber: this.lastBlockNumber,
970
+ headBlockNumber: this.headBlockNumber,
971
+ blocksBehind: this.headBlockNumber
972
+ ? Math.max(0, this.headBlockNumber - this.lastBlockNumber)
973
+ : 0,
974
+ latestBlockchainTime: this.latestBlockchainTime,
975
+ isCatchingUp: this.isCatchingUp
976
+ };
977
+ }
729
978
  verifyTransfer(transaction, from, to, amount) {
730
979
  return utils_2.Utils.verifyTransfer(transaction, from, to, amount);
731
980
  }
@@ -754,6 +1003,18 @@ class Streamer {
754
1003
  onHiveEngine(callback) {
755
1004
  this.customJsonHiveEngineSubscriptions.push({ callback });
756
1005
  }
1006
+ onEscrowTransfer(callback) {
1007
+ this.escrowSubscriptions.push({ type: 'escrow_transfer', callback });
1008
+ }
1009
+ onEscrowApprove(callback) {
1010
+ this.escrowSubscriptions.push({ type: 'escrow_approve', callback });
1011
+ }
1012
+ onEscrowDispute(callback) {
1013
+ this.escrowSubscriptions.push({ type: 'escrow_dispute', callback });
1014
+ }
1015
+ onEscrowRelease(callback) {
1016
+ this.escrowSubscriptions.push({ type: 'escrow_release', callback });
1017
+ }
757
1018
  // Memory management: cleanup subscriptions
758
1019
  cleanupSubscriptions() {
759
1020
  // Limit subscription arrays to prevent memory leaks
@@ -781,6 +1042,10 @@ class Streamer {
781
1042
  this.transferSubscriptions = this.transferSubscriptions.slice(-this.maxSubscriptions);
782
1043
  console.warn(`[Streamer] Trimmed transferSubscriptions to ${this.maxSubscriptions} items`);
783
1044
  }
1045
+ if (this.escrowSubscriptions.length > this.maxSubscriptions) {
1046
+ this.escrowSubscriptions = this.escrowSubscriptions.slice(-this.maxSubscriptions);
1047
+ console.warn(`[Streamer] Trimmed escrowSubscriptions to ${this.maxSubscriptions} items`);
1048
+ }
784
1049
  }
785
1050
  // Add method to remove specific subscriptions
786
1051
  removeTransferSubscription(account) {
@@ -789,6 +1054,13 @@ class Streamer {
789
1054
  removeCustomJsonIdSubscription(id) {
790
1055
  this.customJsonIdSubscriptions = this.customJsonIdSubscriptions.filter(sub => sub.id !== id);
791
1056
  }
1057
+ removeEscrowSubscriptions(type) {
1058
+ if (!type) {
1059
+ this.escrowSubscriptions = [];
1060
+ return;
1061
+ }
1062
+ this.escrowSubscriptions = this.escrowSubscriptions.filter(sub => sub.type !== type);
1063
+ }
792
1064
  }
793
1065
  exports.Streamer = Streamer;
794
1066
  //# sourceMappingURL=streamer.js.map