hive-stream 2.0.6 → 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 (123) hide show
  1. package/.claude/settings.local.json +12 -0
  2. package/.env.example +2 -2
  3. package/.travis.yml +11 -11
  4. package/AGENTS.md +35 -0
  5. package/CHANGELOG.md +166 -0
  6. package/CLAUDE.md +75 -0
  7. package/DOCUMENTATION.md +380 -0
  8. package/LICENSE +21 -21
  9. package/README.md +429 -238
  10. package/dist/actions.d.ts +41 -10
  11. package/dist/actions.js +126 -23
  12. package/dist/actions.js.map +1 -1
  13. package/dist/adapters/base.adapter.d.ts +43 -25
  14. package/dist/adapters/base.adapter.js +79 -49
  15. package/dist/adapters/base.adapter.js.map +1 -1
  16. package/dist/adapters/mongodb.adapter.d.ts +44 -37
  17. package/dist/adapters/mongodb.adapter.js +363 -158
  18. package/dist/adapters/mongodb.adapter.js.map +1 -1
  19. package/dist/adapters/postgresql.adapter.d.ts +66 -0
  20. package/dist/adapters/postgresql.adapter.js +598 -0
  21. package/dist/adapters/postgresql.adapter.js.map +1 -0
  22. package/dist/adapters/sqlite.adapter.d.ts +57 -41
  23. package/dist/adapters/sqlite.adapter.js +561 -397
  24. package/dist/adapters/sqlite.adapter.js.map +1 -1
  25. package/dist/api.d.ts +6 -6
  26. package/dist/api.js +181 -55
  27. package/dist/api.js.map +1 -1
  28. package/dist/config.d.ts +19 -16
  29. package/dist/config.js +21 -18
  30. package/dist/config.js.map +1 -1
  31. package/dist/contracts/coinflip.contract.d.ts +9 -14
  32. package/dist/contracts/coinflip.contract.js +232 -94
  33. package/dist/contracts/coinflip.contract.js.map +1 -1
  34. package/dist/contracts/contract.d.ts +3 -0
  35. package/dist/contracts/contract.js +26 -0
  36. package/dist/contracts/contract.js.map +1 -0
  37. package/dist/contracts/dice.contract.d.ts +10 -29
  38. package/dist/contracts/dice.contract.js +217 -155
  39. package/dist/contracts/dice.contract.js.map +1 -1
  40. package/dist/contracts/exchange.contract.d.ts +11 -0
  41. package/dist/contracts/exchange.contract.js +492 -0
  42. package/dist/contracts/exchange.contract.js.map +1 -0
  43. package/dist/contracts/lotto.contract.d.ts +16 -20
  44. package/dist/contracts/lotto.contract.js +238 -246
  45. package/dist/contracts/lotto.contract.js.map +1 -1
  46. package/dist/contracts/nft.contract.d.ts +28 -0
  47. package/dist/contracts/nft.contract.js +598 -0
  48. package/dist/contracts/nft.contract.js.map +1 -0
  49. package/dist/contracts/poll.contract.d.ts +4 -0
  50. package/dist/contracts/poll.contract.js +105 -0
  51. package/dist/contracts/poll.contract.js.map +1 -0
  52. package/dist/contracts/rps.contract.d.ts +9 -0
  53. package/dist/contracts/rps.contract.js +217 -0
  54. package/dist/contracts/rps.contract.js.map +1 -0
  55. package/dist/contracts/tipjar.contract.d.ts +4 -0
  56. package/dist/contracts/tipjar.contract.js +60 -0
  57. package/dist/contracts/tipjar.contract.js.map +1 -0
  58. package/dist/contracts/token.contract.d.ts +4 -0
  59. package/dist/contracts/token.contract.js +311 -0
  60. package/dist/contracts/token.contract.js.map +1 -0
  61. package/dist/exchanges/bittrex.d.ts +6 -6
  62. package/dist/exchanges/bittrex.js +34 -34
  63. package/dist/exchanges/coingecko.d.ts +11 -0
  64. package/dist/exchanges/coingecko.js +57 -0
  65. package/dist/exchanges/coingecko.js.map +1 -0
  66. package/dist/exchanges/exchange.d.ts +16 -9
  67. package/dist/exchanges/exchange.js +80 -26
  68. package/dist/exchanges/exchange.js.map +1 -1
  69. package/dist/hive-rates.d.ts +34 -9
  70. package/dist/hive-rates.js +208 -75
  71. package/dist/hive-rates.js.map +1 -1
  72. package/dist/index.d.ts +19 -11
  73. package/dist/index.js +47 -32
  74. package/dist/index.js.map +1 -1
  75. package/dist/streamer.d.ts +233 -93
  76. package/dist/streamer.js +1063 -545
  77. package/dist/streamer.js.map +1 -1
  78. package/dist/test.d.ts +1 -1
  79. package/dist/test.js +24 -25
  80. package/dist/test.js.map +1 -1
  81. package/dist/types/hive-stream.d.ts +106 -6
  82. package/dist/types/hive-stream.js +2 -2
  83. package/dist/types/rates.d.ts +47 -0
  84. package/dist/types/rates.js +29 -0
  85. package/dist/types/rates.js.map +1 -0
  86. package/dist/utils.d.ts +334 -27
  87. package/dist/utils.js +960 -261
  88. package/dist/utils.js.map +1 -1
  89. package/ecosystem.config.js +17 -17
  90. package/examples/contracts/README.md +8 -0
  91. package/examples/contracts/exchange.ts +38 -0
  92. package/examples/contracts/poll.ts +21 -0
  93. package/examples/contracts/rps.ts +19 -0
  94. package/examples/contracts/tipjar.ts +19 -0
  95. package/jest.config.js +8 -8
  96. package/package.json +54 -48
  97. package/test-contract-block.md +18 -18
  98. package/tests/actions.spec.ts +252 -0
  99. package/tests/adapters/actions-persistence.spec.ts +144 -0
  100. package/tests/adapters/postgresql.adapter.spec.ts +127 -0
  101. package/tests/adapters/sqlite.adapter.spec.ts +180 -42
  102. package/tests/contracts/coinflip.contract.spec.ts +94 -132
  103. package/tests/contracts/dice.contract.spec.ts +87 -160
  104. package/tests/contracts/entrants.json +728 -728
  105. package/tests/contracts/exchange.contract.spec.ts +84 -0
  106. package/tests/contracts/lotto.contract.spec.ts +59 -324
  107. package/tests/contracts/nft.contract.spec.ts +948 -0
  108. package/tests/contracts/token.contract.spec.ts +90 -0
  109. package/tests/exchanges/coingecko.exchange.spec.ts +169 -0
  110. package/tests/exchanges/exchange.base.spec.ts +246 -0
  111. package/tests/helpers/mock-adapter.ts +214 -0
  112. package/tests/helpers/mock-fetch.ts +165 -0
  113. package/tests/hive-chain-features.spec.ts +238 -0
  114. package/tests/hive-rates.spec.ts +443 -0
  115. package/tests/integration/hive-rates.integration.spec.ts +35 -0
  116. package/tests/setup.ts +29 -18
  117. package/tests/streamer-actions.spec.ts +274 -0
  118. package/tests/streamer.spec.ts +342 -152
  119. package/tests/types/rates.spec.ts +216 -0
  120. package/tests/utils.spec.ts +113 -95
  121. package/tsconfig.build.json +3 -22
  122. package/tslint.json +20 -20
  123. package/wallaby.js +26 -26
package/dist/streamer.js CHANGED
@@ -1,546 +1,1064 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.Streamer = void 0;
7
- const api_1 = require("./api");
8
- const sqlite_adapter_1 = require("./adapters/sqlite.adapter");
9
- const utils_1 = require("@hiveio/dhive/lib/utils");
10
- const actions_1 = require("./actions");
11
- const dhive_1 = require("@hiveio/dhive");
12
- const utils_2 = require("./utils");
13
- const config_1 = require("./config");
14
- const moment_1 = __importDefault(require("moment"));
15
- const sscjs_1 = __importDefault(require("sscjs"));
16
- class Streamer {
17
- customJsonSubscriptions = [];
18
- customJsonIdSubscriptions = [];
19
- customJsonHiveEngineSubscriptions = [];
20
- commentSubscriptions = [];
21
- postSubscriptions = [];
22
- transferSubscriptions = [];
23
- attempts = 0;
24
- config = config_1.Config;
25
- client;
26
- hive;
27
- username;
28
- postingKey;
29
- activeKey;
30
- blockNumberTimeout = null;
31
- latestBlockTimer = null;
32
- lastBlockNumber = 0;
33
- blockId;
34
- previousBlockId;
35
- transactionId;
36
- blockTime;
37
- latestBlockchainTime;
38
- disableAllProcessing = false;
39
- contracts = [];
40
- adapter;
41
- actions = [];
42
- utils = utils_2.Utils;
43
- constructor(userConfig = {}) {
44
- this.config = Object.assign(config_1.Config, userConfig);
45
- this.lastBlockNumber = this.config.LAST_BLOCK_NUMBER;
46
- this.username = this.config.USERNAME;
47
- this.postingKey = this.config.POSTING_KEY;
48
- this.activeKey = this.config.ACTIVE_KEY;
49
- this.hive = new sscjs_1.default(this.config.HIVE_ENGINE_API);
50
- this.client = new dhive_1.Client(this.config.API_NODES);
51
- this.registerAdapter(new sqlite_adapter_1.SqliteAdapter());
52
- if (process?.env?.NODE_ENV !== 'test') {
53
- new api_1.Api(this);
54
- }
55
- }
56
- registerAdapter(adapter) {
57
- this.adapter = adapter;
58
- if (this?.adapter?.create) {
59
- this.adapter.create();
60
- }
61
- }
62
- getAdapter() {
63
- return this.adapter;
64
- }
65
- /**
66
- * Register a new action
67
- *
68
- */
69
- async registerAction(action) {
70
- const loadedActions = await this.adapter.loadActions();
71
- for (const a of loadedActions) {
72
- const exists = this.actions.find(i => i.id === a.id);
73
- if (!exists) {
74
- this.actions.push(new actions_1.TimeAction(a.timeValue, a.id, a.contractName, a.contractMethod, a.date));
75
- }
76
- }
77
- const exists = this.actions.find(a => a.id === action.id);
78
- if (!exists) {
79
- this.actions.push(action);
80
- }
81
- }
82
- /**
83
- * Resets a specific action time value
84
- *
85
- * @param id
86
- *
87
- */
88
- resetAction(id) {
89
- const action = this.actions.find(i => i.id === id);
90
- if (action) {
91
- action.reset();
92
- }
93
- }
94
- registerContract(name, contract) {
95
- // Store an instance of the streamer
96
- contract['_instance'] = this;
97
- // Call the contract create lifecycle method if it exists
98
- if (contract && typeof contract['create'] !== 'undefined') {
99
- contract.create();
100
- }
101
- const storedReference = { name, contract };
102
- // Push the contract reference to be called later on
103
- this.contracts.push(storedReference);
104
- return this;
105
- }
106
- unregisterContract(name) {
107
- // Find the registered contract by it's ID
108
- const contractIndex = this.contracts.findIndex(c => c.name === name);
109
- if (contractIndex >= 0) {
110
- // Get the contract itself
111
- const contract = this.contracts.find(c => c.name === name);
112
- // Call the contract destroy lifecycle method if it exists
113
- if (contract && typeof contract.contract['destroy'] !== 'undefined') {
114
- contract.contract.destroy();
115
- }
116
- // Remove the contract
117
- this.contracts.splice(contractIndex, 1);
118
- }
119
- }
120
- /**
121
- * setConfig
122
- *
123
- * Allows specific configuration settings to be overridden
124
- *
125
- * @param config
126
- */
127
- setConfig(config) {
128
- Object.assign(this.config, config);
129
- // Set keys and username incase they have changed
130
- this.username = this.config.USERNAME;
131
- this.postingKey = this.config.POSTING_KEY;
132
- this.activeKey = this.config.ACTIVE_KEY;
133
- return this;
134
- }
135
- /**
136
- * Start
137
- *
138
- * Starts the streamer bot to get blocks from the Hive API
139
- *
140
- */
141
- async start() {
142
- if (this.config.DEBUG_MODE) {
143
- console.log('Starting to stream the Hive blockchain');
144
- }
145
- this.disableAllProcessing = false;
146
- const state = await this.adapter.loadState();
147
- if (this.config.DEBUG_MODE) {
148
- console.log(`Restoring state from file`);
149
- }
150
- if (!this.config.LAST_BLOCK_NUMBER && state?.lastBlockNumber) {
151
- if (state.lastBlockNumber) {
152
- this.lastBlockNumber = state.lastBlockNumber;
153
- }
154
- }
155
- // Kicks off the blockchain streaming and operation parsing
156
- this.getBlock();
157
- this.latestBlockTimer = setInterval(() => { this.getLatestBlock(); }, this.config.BLOCK_CHECK_INTERVAL);
158
- return this;
159
- }
160
- /**
161
- * Stop
162
- *
163
- * Stops the streamer from running
164
- */
165
- async stop() {
166
- this.disableAllProcessing = true;
167
- if (this.blockNumberTimeout) {
168
- clearTimeout(this.blockNumberTimeout);
169
- }
170
- if (this.latestBlockTimer) {
171
- clearInterval(this.latestBlockTimer);
172
- }
173
- if (this?.adapter?.destroy) {
174
- this.adapter.destroy();
175
- }
176
- await (0, utils_1.sleep)(800);
177
- }
178
- async getLatestBlock() {
179
- const props = await this.client.database.getDynamicGlobalProperties();
180
- if (props) {
181
- this.latestBlockchainTime = new Date(`${props.time}Z`);
182
- }
183
- }
184
- async getBlock() {
185
- try {
186
- // Load global properties from the Hive API
187
- const props = await this.client.database.getDynamicGlobalProperties();
188
- // We have no props, so try loading them again.
189
- if (!props && !this.disableAllProcessing) {
190
- this.blockNumberTimeout = setTimeout(() => {
191
- this.getBlock();
192
- }, this.config.BLOCK_CHECK_INTERVAL);
193
- return;
194
- }
195
- // If the block number we've got is zero
196
- // set it to the last irreversible block number
197
- if (this.lastBlockNumber === 0) {
198
- this.lastBlockNumber = props.head_block_number - 1;
199
- }
200
- if (this.config.DEBUG_MODE) {
201
- console.log(`Head block number: `, props.head_block_number);
202
- console.log(`Last block number: `, this.lastBlockNumber);
203
- }
204
- const BLOCKS_BEHIND = parseInt(this.config.BLOCKS_BEHIND_WARNING, 10);
205
- if (!this.disableAllProcessing) {
206
- await this.loadBlock(this.lastBlockNumber + 1);
207
- }
208
- // We are more than 25 blocks behind, uh oh, we gotta catch up
209
- if (props.head_block_number >= (this.lastBlockNumber + BLOCKS_BEHIND) && this.config.DEBUG_MODE) {
210
- console.log(`We are more than ${BLOCKS_BEHIND} blocks behind ${props.head_block_number}, ${(this.lastBlockNumber + BLOCKS_BEHIND)}`);
211
- if (!this.disableAllProcessing) {
212
- this.getBlock();
213
- return;
214
- }
215
- }
216
- // Storing timeout allows us to clear it, as this just calls itself
217
- if (!this.disableAllProcessing) {
218
- this.blockNumberTimeout = setTimeout(() => { this.getBlock(); }, this.config.BLOCK_CHECK_INTERVAL);
219
- }
220
- }
221
- catch (e) {
222
- const message = e.message.toLowerCase();
223
- console.error(message);
224
- }
225
- }
226
- // Takes the block from Hive and allows us to work with it
227
- async loadBlock(blockNumber) {
228
- // Load the block itself from the Hive API
229
- const block = await this.client.database.getBlock(blockNumber);
230
- // The block doesn't exist, wait and try again
231
- if (!block) {
232
- await utils_2.Utils.sleep(this.config.BLOCK_CHECK_INTERVAL);
233
- return;
234
- }
235
- // Get the block date and time
236
- const blockTime = new Date(`${block.timestamp}Z`);
237
- if (this.lastBlockNumber !== blockNumber) {
238
- this.processActions();
239
- }
240
- this.blockId = block.block_id;
241
- this.previousBlockId = block.previous;
242
- this.transactionId = block.transaction_ids[1];
243
- this.blockTime = blockTime;
244
- if (this.adapter?.processBlock) {
245
- this.adapter.processBlock(block);
246
- }
247
- // Loop over all transactions in the block
248
- for (const [i, transaction] of Object.entries(block.transactions)) {
249
- // Loop over operations in the block
250
- for (const [opIndex, op] of Object.entries(transaction.operations)) {
251
- // For every operation, process it
252
- await this.processOperation(op, blockNumber, block.block_id, block.previous, block.transaction_ids[i], blockTime);
253
- }
254
- }
255
- this.lastBlockNumber = blockNumber;
256
- this.saveStateToDisk();
257
- }
258
- processOperation(op, blockNumber, blockId, prevBlockId, trxId, blockTime) {
259
- if (this.adapter?.processOperation) {
260
- this.adapter.processOperation(op, blockNumber, blockId, prevBlockId, trxId, blockTime);
261
- }
262
- // Operation is a "comment" which could either be a post or comment
263
- if (op[0] === 'comment') {
264
- // This is a post
265
- if (op[1].parent_author === '') {
266
- this.postSubscriptions.forEach(sub => {
267
- sub.callback(op[1], blockNumber, blockId, prevBlockId, trxId, blockTime);
268
- });
269
- // This is a comment
270
- }
271
- else {
272
- this.commentSubscriptions.forEach(sub => {
273
- sub.callback(op[1], blockNumber, blockId, prevBlockId, trxId, blockTime);
274
- });
275
- }
276
- }
277
- // This is a transfer
278
- if (op[0] === 'transfer') {
279
- const sender = op[1]?.from;
280
- const amount = op[1]?.amount;
281
- const json = utils_2.Utils.jsonParse(op[1].memo);
282
- if (json?.[this.config.PAYLOAD_IDENTIFIER] && json?.[this.config.PAYLOAD_IDENTIFIER]?.id === this.config.JSON_ID) {
283
- // Pull out details of contract
284
- const { name, action, payload } = json[this.config.PAYLOAD_IDENTIFIER];
285
- // Do we have a contract that matches the name in the payload?
286
- const contract = this.contracts.find(c => c.name === name);
287
- if (contract) {
288
- if (this?.adapter?.processTransfer) {
289
- this.adapter.processTransfer(op[1], { name, action, payload }, { sender, amount });
290
- }
291
- if (contract?.contract?.updateBlockInfo) {
292
- contract.contract.updateBlockInfo(blockNumber, blockId, prevBlockId, trxId);
293
- }
294
- if (contract?.contract[action]) {
295
- contract.contract[action](payload, { sender, amount });
296
- }
297
- }
298
- }
299
- this.transferSubscriptions.forEach(sub => {
300
- if (sub.account === op[1].to) {
301
- sub.callback(op[1], blockNumber, blockId, prevBlockId, trxId, blockTime);
302
- }
303
- });
304
- }
305
- // This is a custom JSON operation
306
- if (op[0] === 'custom_json') {
307
- let isSignedWithActiveKey = false;
308
- let sender;
309
- const id = op[1]?.id;
310
- if (op[1]?.required_auths?.length > 0) {
311
- sender = op[1].required_auths[0];
312
- isSignedWithActiveKey = true;
313
- }
314
- else if (op[1]?.required_posting_auths?.length > 0) {
315
- sender = op[1].required_posting_auths[0];
316
- isSignedWithActiveKey = false;
317
- }
318
- const json = utils_2.Utils.jsonParse(op[1].json);
319
- if (json && json?.[this.config.PAYLOAD_IDENTIFIER] && id === this.config.JSON_ID) {
320
- // Pull out details of contract
321
- const { name, action, payload } = json[this.config.PAYLOAD_IDENTIFIER];
322
- // Do we have a contract that matches the name in the payload?
323
- const contract = this.contracts.find(c => c.name === name);
324
- if (contract) {
325
- this.adapter.processCustomJson(op[1], { name, action, payload }, { sender, isSignedWithActiveKey });
326
- if (contract?.contract?.updateBlockInfo) {
327
- contract.contract.updateBlockInfo(blockNumber, blockId, prevBlockId, trxId);
328
- }
329
- if (contract?.contract[action]) {
330
- contract.contract[action](payload, { sender, isSignedWithActiveKey }, id);
331
- }
332
- }
333
- }
334
- this.customJsonSubscriptions.forEach(sub => {
335
- sub.callback(op[1], { sender, isSignedWithActiveKey }, blockNumber, blockId, prevBlockId, trxId, blockTime);
336
- });
337
- this.customJsonIdSubscriptions.forEach(sub => {
338
- const byId = this.customJsonIdSubscriptions.find(s => s.id === op[1].id);
339
- if (byId) {
340
- sub.callback(op[1], { sender, isSignedWithActiveKey }, blockNumber, blockId, prevBlockId, trxId, blockTime);
341
- }
342
- });
343
- utils_2.Utils.asyncForEach(this.customJsonHiveEngineSubscriptions, async (sub) => {
344
- let isSignedWithActiveKey = null;
345
- let sender;
346
- if (op[1].required_auths.length > 0) {
347
- sender = op[1].required_auths[0];
348
- isSignedWithActiveKey = true;
349
- }
350
- else {
351
- sender = op[1].required_posting_auths[0];
352
- isSignedWithActiveKey = false;
353
- }
354
- const id = op[1].id;
355
- const json = utils_2.Utils.jsonParse(op[1].json);
356
- // Hive Engine JSON operation
357
- if (id === this.config.HIVE_ENGINE_ID) {
358
- const { contractName, contractAction, contractPayload } = json;
359
- try {
360
- // Attempt to get the transaction from Hive Engine itself
361
- const txInfo = await this.hive.getTransactionInfo(trxId);
362
- const logs = txInfo && txInfo.logs ? utils_2.Utils.jsonParse(txInfo.logs) : null;
363
- // Do we have a valid transaction and are there no errors? It's a real transaction
364
- if (txInfo && logs && typeof logs.errors === 'undefined') {
365
- sub.callback(contractName, contractAction, contractPayload, sender, op[1], blockNumber, blockId, prevBlockId, trxId, blockTime);
366
- }
367
- }
368
- catch (e) {
369
- console.error(e);
370
- return;
371
- }
372
- }
373
- });
374
- }
375
- }
376
- processActions() {
377
- const blockDate = moment_1.default.utc(this.latestBlockchainTime);
378
- for (const action of this.actions) {
379
- const date = moment_1.default.utc(action.date);
380
- const frequency = action.timeValue;
381
- const contract = this.contracts.find(c => c.name === action.contractName);
382
- // Contract doesn't exist or action doesn't exist, carry on
383
- if (!contract || !contract?.contract?.[action.contractMethod]) {
384
- continue;
385
- }
386
- let difference = 0;
387
- switch (frequency) {
388
- case '3s':
389
- case 'block':
390
- difference = date.diff(blockDate, 's');
391
- // 3 seconds or more has passed
392
- if (difference >= 3) {
393
- contract.contract[action.contractMethod](action.payload);
394
- action.reset();
395
- }
396
- break;
397
- case '10s':
398
- difference = blockDate.diff(date, 's');
399
- // 10 seconds or more has passed
400
- if (difference >= 10) {
401
- contract.contract[action.contractMethod](action.payload);
402
- action.reset();
403
- }
404
- break;
405
- case '30s':
406
- difference = blockDate.diff(date, 's');
407
- // 30 seconds or more has passed
408
- if (difference >= 30) {
409
- contract.contract[action.contractMethod](action.payload);
410
- action.reset();
411
- }
412
- break;
413
- case '1m':
414
- case 'minute':
415
- difference = blockDate.diff(date, 'm');
416
- // One minute has passed
417
- if (difference >= 1) {
418
- contract.contract[action.contractMethod](action.payload);
419
- action.reset();
420
- }
421
- break;
422
- case '15m':
423
- case 'quarter':
424
- difference = blockDate.diff(date, 'm');
425
- // 15 minutes has passed
426
- if (difference >= 15) {
427
- contract.contract[action.contractMethod](action.payload);
428
- action.reset();
429
- }
430
- break;
431
- case '30m':
432
- case 'halfhour':
433
- difference = blockDate.diff(date, 'm');
434
- // 30 minutes has passed
435
- if (difference >= 30) {
436
- contract.contract[action.contractMethod](action.payload);
437
- action.reset();
438
- }
439
- break;
440
- case 'hourly':
441
- case '1h':
442
- difference = blockDate.diff(date, 'h');
443
- // One our or more has passed
444
- if (difference >= 1) {
445
- contract.contract[action.contractMethod](action.payload);
446
- action.reset();
447
- }
448
- break;
449
- case '12h':
450
- case 'halfday':
451
- difference = blockDate.diff(date, 'h');
452
- // Twelve hours or more has passed
453
- if (difference >= 12) {
454
- contract.contract[action.contractMethod](action.payload);
455
- action.reset();
456
- }
457
- break;
458
- case '24h':
459
- case 'day':
460
- difference = blockDate.diff(date, 'd');
461
- // One day (24 hours) has passed
462
- if (difference >= 1) {
463
- contract.contract[action.contractMethod](action.payload);
464
- action.reset();
465
- }
466
- break;
467
- case 'week':
468
- difference = blockDate.diff(date, 'w');
469
- // One week has passed
470
- if (difference >= 1) {
471
- contract.contract[action.contractMethod](action.payload);
472
- action.reset();
473
- }
474
- break;
475
- }
476
- }
477
- }
478
- async saveStateToDisk() {
479
- if (this.adapter?.saveState) {
480
- this.adapter.saveState({ lastBlockNumber: this.lastBlockNumber, actions: this.actions });
481
- }
482
- }
483
- saveToHiveApi(from, data) {
484
- return utils_2.Utils.transferHiveTokens(this.client, this.config, from, 'hiveapi', '0.001', 'HIVE', data);
485
- }
486
- getAccountTransfers(account, from = -1, limit = 100) {
487
- return utils_2.Utils.getAccountTransfers(this.client, account, from, limit);
488
- }
489
- transferHiveTokens(from, to, amount, symbol, memo = '') {
490
- return utils_2.Utils.transferHiveTokens(this.client, this.config, from, to, amount, symbol, memo);
491
- }
492
- transferHiveTokensMultiple(from, accounts = [], amount = '0', symbol, memo = '') {
493
- return utils_2.Utils.transferHiveTokensMultiple(this.client, this.config, from, accounts, amount, symbol, memo);
494
- }
495
- transferHiveEngineTokens(from, to, symbol, quantity, memo = '') {
496
- return utils_2.Utils.transferHiveEngineTokens(this.client, this.config, from, to, symbol, quantity, memo);
497
- }
498
- transferHiveEngineTokensMultiple(from, accounts = [], symbol, memo = '', amount = '0') {
499
- return utils_2.Utils.transferHiveEngineTokensMultiple(this.client, this.config, from, accounts, symbol, memo, amount);
500
- }
501
- issueHiveEngineTokens(from, to, symbol, quantity, memo = '') {
502
- return utils_2.Utils.issueHiveEngineTokens(this.client, this.config, from, to, symbol, quantity, memo);
503
- }
504
- issueHiveEngineTokensMultiple(from, accounts = [], symbol, memo = '', amount = '0') {
505
- return utils_2.Utils.issueHiveEngineTokensMultiple(this.client, this.config, from, accounts, symbol, memo, amount);
506
- }
507
- upvote(votePercentage = '100.0', username, permlink) {
508
- return utils_2.Utils.upvote(this.client, this.config, this.username, votePercentage, username, permlink);
509
- }
510
- downvote(votePercentage = '100.0', username, permlink) {
511
- return utils_2.Utils.downvote(this.client, this.config, this.username, votePercentage, username, permlink);
512
- }
513
- getTransaction(blockNumber, transactionId) {
514
- return utils_2.Utils.getTransaction(this.client, blockNumber, transactionId);
515
- }
516
- verifyTransfer(transaction, from, to, amount) {
517
- return utils_2.Utils.verifyTransfer(transaction, from, to, amount);
518
- }
519
- onComment(callback) {
520
- this.commentSubscriptions.push({
521
- callback
522
- });
523
- }
524
- onPost(callback) {
525
- this.postSubscriptions.push({
526
- callback
527
- });
528
- }
529
- onTransfer(account, callback) {
530
- this.transferSubscriptions.push({
531
- account,
532
- callback
533
- });
534
- }
535
- onCustomJson(callback) {
536
- this.customJsonSubscriptions.push({ callback });
537
- }
538
- onCustomJsonId(callback, id) {
539
- this.customJsonIdSubscriptions.push({ callback, id });
540
- }
541
- onHiveEngine(callback) {
542
- this.customJsonHiveEngineSubscriptions.push({ callback });
543
- }
544
- }
545
- exports.Streamer = Streamer;
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.Streamer = void 0;
7
+ const api_1 = require("./api");
8
+ const sqlite_adapter_1 = require("./adapters/sqlite.adapter");
9
+ const utils_1 = require("@hiveio/dhive/lib/utils");
10
+ const actions_1 = require("./actions");
11
+ const dhive_1 = require("@hiveio/dhive");
12
+ const utils_2 = require("./utils");
13
+ const config_1 = require("./config");
14
+ const sscjs_1 = __importDefault(require("sscjs"));
15
+ class Streamer {
16
+ customJsonSubscriptions = [];
17
+ customJsonIdSubscriptions = [];
18
+ customJsonHiveEngineSubscriptions = [];
19
+ commentSubscriptions = [];
20
+ postSubscriptions = [];
21
+ transferSubscriptions = [];
22
+ escrowSubscriptions = [];
23
+ attempts = 0;
24
+ config = config_1.Config;
25
+ client;
26
+ hive;
27
+ username;
28
+ postingKey;
29
+ activeKey;
30
+ blockNumberTimeout = null;
31
+ latestBlockTimer = null;
32
+ lastBlockNumber = 0;
33
+ headBlockNumber = 0;
34
+ isPollingBlock = false;
35
+ isCatchingUp = false;
36
+ blockId;
37
+ previousBlockId;
38
+ transactionId;
39
+ blockTime;
40
+ latestBlockchainTime;
41
+ disableAllProcessing = false;
42
+ contracts = [];
43
+ adapter;
44
+ actions = [];
45
+ // Performance optimization properties
46
+ lastStateSave = Date.now();
47
+ stateSaveInterval = 5000; // Save state every 5 seconds instead of every block
48
+ blockProcessingQueue = [];
49
+ isProcessingQueue = false;
50
+ // Memory management
51
+ maxSubscriptions = 1000;
52
+ subscriptionCleanupInterval = null;
53
+ // Action processing optimization
54
+ actionFrequencyMap = new Map([
55
+ ['3s', 3], ['block', 3], ['10s', 10], ['30s', 30],
56
+ ['1m', 60], ['5m', 300], ['minute', 60], ['15m', 900], ['quarter', 900],
57
+ ['30m', 1800], ['halfhour', 1800], ['hourly', 3600], ['1h', 3600],
58
+ ['12h', 43200], ['halfday', 43200], ['24h', 86400], ['day', 86400], ['daily', 86400],
59
+ ['week', 604800], ['weekly', 604800]
60
+ ]);
61
+ contractCache = new Map();
62
+ // Data caching for performance
63
+ blockCache = new Map();
64
+ transactionCache = new Map();
65
+ accountCache = new Map();
66
+ cacheTimeout = 300000; // 5 minutes
67
+ maxCacheSize = 1000;
68
+ utils = utils_2.Utils;
69
+ constructor(userConfig = {}) {
70
+ this.config = Object.assign(config_1.Config, userConfig);
71
+ this.lastBlockNumber = this.config.LAST_BLOCK_NUMBER;
72
+ this.username = this.config.USERNAME;
73
+ this.postingKey = this.config.POSTING_KEY;
74
+ this.activeKey = this.config.ACTIVE_KEY;
75
+ this.hive = new sscjs_1.default(this.config.HIVE_ENGINE_API);
76
+ this.client = new dhive_1.Client(this.config.API_NODES);
77
+ if (process?.env?.NODE_ENV !== 'test') {
78
+ this._initializeAdapter(new sqlite_adapter_1.SqliteAdapter());
79
+ new api_1.Api(this);
80
+ }
81
+ // Start subscription cleanup interval
82
+ this.subscriptionCleanupInterval = setInterval(() => {
83
+ this.cleanupSubscriptions();
84
+ }, 60000); // Cleanup every minute
85
+ }
86
+ _initializeAdapter(adapter) {
87
+ this.adapter = adapter;
88
+ if (this?.adapter?.create) {
89
+ this.adapter.create();
90
+ }
91
+ }
92
+ async registerAdapter(adapter) {
93
+ if (this.adapter && this.adapter.destroy) {
94
+ try {
95
+ await this.adapter.destroy();
96
+ }
97
+ catch (error) {
98
+ console.warn('[Streamer] Error destroying existing adapter:', error);
99
+ }
100
+ }
101
+ this.adapter = adapter;
102
+ if (this?.adapter?.create) {
103
+ await this.adapter.create();
104
+ }
105
+ }
106
+ getAdapter() {
107
+ return this.adapter;
108
+ }
109
+ /**
110
+ * Register a new action with improved validation and persistence
111
+ */
112
+ async registerAction(action) {
113
+ if (!action || !(action instanceof actions_1.TimeAction)) {
114
+ throw new Error('Invalid action: must be an instance of TimeAction');
115
+ }
116
+ const loadedActions = await this.adapter.loadActions();
117
+ for (const a of loadedActions) {
118
+ const exists = this.actions.find(i => i.id === a.id);
119
+ if (!exists) {
120
+ try {
121
+ const restoredAction = actions_1.TimeAction.fromJSON(a);
122
+ this.actions.push(restoredAction);
123
+ }
124
+ catch (error) {
125
+ console.warn(`[Streamer] Failed to restore action ${a.id}:`, error);
126
+ }
127
+ }
128
+ }
129
+ const exists = this.actions.find(a => a.id === action.id);
130
+ if (!exists) {
131
+ this.validateActionContract(action);
132
+ this.actions.push(action);
133
+ await this.saveActionsToDisk();
134
+ if (this.config.DEBUG_MODE) {
135
+ console.log(`[Streamer] Registered time-based action: ${action.id} (${action.timeValue})`);
136
+ }
137
+ }
138
+ else {
139
+ if (this.config.DEBUG_MODE) {
140
+ console.warn(`[Streamer] Action with ID ${action.id} already exists, skipping registration`);
141
+ }
142
+ }
143
+ }
144
+ /**
145
+ * Validate that the contract and method exist for the action
146
+ */
147
+ validateActionContract(action) {
148
+ const contract = this.contractCache.get(action.contractName) ||
149
+ this.contracts.find(c => c.name === action.contractName);
150
+ if (!contract) {
151
+ throw new Error(`Contract '${action.contractName}' not found for action '${action.id}'`);
152
+ }
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}'`);
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);
167
+ }
168
+ /**
169
+ * Remove an action by ID
170
+ */
171
+ async removeAction(actionId) {
172
+ const index = this.actions.findIndex(a => a.id === actionId);
173
+ if (index >= 0) {
174
+ const removedAction = this.actions.splice(index, 1)[0];
175
+ await this.saveActionsToDisk();
176
+ if (this.config.DEBUG_MODE) {
177
+ console.log(`[Streamer] Removed time-based action: ${actionId}`);
178
+ }
179
+ return true;
180
+ }
181
+ return false;
182
+ }
183
+ /**
184
+ * Get all registered actions
185
+ */
186
+ getActions() {
187
+ return [...this.actions];
188
+ }
189
+ /**
190
+ * Get action by ID
191
+ */
192
+ getAction(actionId) {
193
+ return this.actions.find(a => a.id === actionId);
194
+ }
195
+ /**
196
+ * Enable/disable an action
197
+ */
198
+ async setActionEnabled(actionId, enabled) {
199
+ const action = this.actions.find(a => a.id === actionId);
200
+ if (action) {
201
+ if (enabled) {
202
+ action.enable();
203
+ }
204
+ else {
205
+ action.disable();
206
+ }
207
+ await this.saveActionsToDisk();
208
+ if (this.config.DEBUG_MODE) {
209
+ console.log(`[Streamer] Action ${actionId} ${enabled ? 'enabled' : 'disabled'}`);
210
+ }
211
+ return true;
212
+ }
213
+ return false;
214
+ }
215
+ /**
216
+ * Save actions to disk asynchronously
217
+ */
218
+ async saveActionsToDisk() {
219
+ try {
220
+ if (this.adapter?.saveState) {
221
+ await this.adapter.saveState({
222
+ lastBlockNumber: this.lastBlockNumber,
223
+ actions: this.actions.map(a => a.toJSON())
224
+ });
225
+ }
226
+ }
227
+ catch (error) {
228
+ if (error?.code !== 'SQLITE_MISUSE') {
229
+ console.error('[Streamer] Failed to save actions to disk:', error);
230
+ }
231
+ }
232
+ }
233
+ /**
234
+ * Resets a specific action time value
235
+ */
236
+ async resetAction(id) {
237
+ const action = this.actions.find(i => i.id === id);
238
+ if (action) {
239
+ action.reset();
240
+ await this.saveActionsToDisk();
241
+ if (this.config.DEBUG_MODE) {
242
+ console.log(`[Streamer] Reset action: ${id}`);
243
+ }
244
+ return true;
245
+ }
246
+ return false;
247
+ }
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);
271
+ }
272
+ async unregisterContract(name) {
273
+ const contractIndex = this.contracts.findIndex(c => c.name === name);
274
+ if (contractIndex >= 0) {
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
+ });
282
+ }
283
+ this.contracts.splice(contractIndex, 1);
284
+ this.contractCache.delete(name);
285
+ }
286
+ }
287
+ /**
288
+ * setConfig
289
+ *
290
+ * Allows specific configuration settings to be overridden
291
+ *
292
+ * @param config
293
+ */
294
+ setConfig(config) {
295
+ Object.assign(this.config, config);
296
+ // Set keys and username incase they have changed
297
+ this.username = this.config.USERNAME;
298
+ this.postingKey = this.config.POSTING_KEY;
299
+ this.activeKey = this.config.ACTIVE_KEY;
300
+ return this;
301
+ }
302
+ /**
303
+ * Start
304
+ *
305
+ * Starts the streamer bot to get blocks from the Hive API
306
+ *
307
+ */
308
+ async start() {
309
+ if (this.config.DEBUG_MODE) {
310
+ console.log('Starting to stream the Hive blockchain');
311
+ }
312
+ this.disableAllProcessing = false;
313
+ const state = await this.adapter.loadState();
314
+ if (this.config.DEBUG_MODE) {
315
+ console.log(`Restoring state from file`);
316
+ }
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;
322
+ }
323
+ // Kicks off the blockchain streaming and operation parsing
324
+ this.getBlock();
325
+ this.latestBlockTimer = setInterval(() => { this.getLatestBlock(); }, this.config.BLOCK_CHECK_INTERVAL);
326
+ return this;
327
+ }
328
+ /**
329
+ * Stop
330
+ *
331
+ * Stops the streamer from running
332
+ */
333
+ async stop() {
334
+ this.disableAllProcessing = true;
335
+ if (this.blockNumberTimeout) {
336
+ clearTimeout(this.blockNumberTimeout);
337
+ }
338
+ if (this.latestBlockTimer) {
339
+ clearInterval(this.latestBlockTimer);
340
+ }
341
+ if (this.subscriptionCleanupInterval) {
342
+ clearInterval(this.subscriptionCleanupInterval);
343
+ }
344
+ if (this?.adapter?.destroy) {
345
+ await this.adapter.destroy();
346
+ }
347
+ await (0, utils_1.sleep)(800);
348
+ }
349
+ async getLatestBlock() {
350
+ try {
351
+ const props = await this.client.database.getDynamicGlobalProperties();
352
+ if (props) {
353
+ this.latestBlockchainTime = new Date(`${props.time}Z`);
354
+ }
355
+ }
356
+ catch (error) {
357
+ console.error('[Streamer] Error getting latest block:', error);
358
+ // Continue with cached time if available
359
+ }
360
+ }
361
+ async getBlock() {
362
+ if (this.isPollingBlock) {
363
+ return;
364
+ }
365
+ this.isPollingBlock = true;
366
+ let nextDelay = this.config.BLOCK_CHECK_INTERVAL;
367
+ try {
368
+ // Load global properties from the Hive API
369
+ const props = await this.client.database.getDynamicGlobalProperties();
370
+ // We have no props, so try loading them again.
371
+ if (!props) {
372
+ return;
373
+ }
374
+ this.headBlockNumber = props.head_block_number;
375
+ // If the block number we've got is zero set it to the latest head block
376
+ if (this.lastBlockNumber === 0) {
377
+ this.lastBlockNumber = props.head_block_number - 1;
378
+ }
379
+ if (this.config.DEBUG_MODE) {
380
+ console.log(`Head block number: `, props.head_block_number);
381
+ console.log(`Last block number: `, this.lastBlockNumber);
382
+ }
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
+ }
390
+ if (!this.disableAllProcessing) {
391
+ for (let i = 0; i < blocksToProcess; i++) {
392
+ await this.loadBlock(this.lastBlockNumber + 1);
393
+ }
394
+ }
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);
399
+ }
400
+ }
401
+ catch (e) {
402
+ const error = e instanceof Error ? e : new Error(String(e));
403
+ console.error(`[Streamer] Block processing error: ${error.message}`, {
404
+ stack: error.stack,
405
+ blockNumber: this.lastBlockNumber + 1
406
+ });
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
413
+ if (!this.disableAllProcessing) {
414
+ this.blockNumberTimeout = setTimeout(() => { this.getBlock(); }, nextDelay);
415
+ }
416
+ }
417
+ }
418
+ // Takes the block from Hive and allows us to work with it
419
+ async loadBlock(blockNumber) {
420
+ // Check cache first
421
+ let block = this.blockCache.get(blockNumber);
422
+ if (!block) {
423
+ // Load the block itself from the Hive API
424
+ block = await this.client.database.getBlock(blockNumber);
425
+ // Cache the block for potential reuse
426
+ if (block) {
427
+ this.blockCache.set(blockNumber, block);
428
+ // Cleanup old cache entries
429
+ if (this.blockCache.size > this.maxCacheSize) {
430
+ const oldestKey = this.blockCache.keys().next().value;
431
+ this.blockCache.delete(oldestKey);
432
+ }
433
+ }
434
+ }
435
+ // The block doesn't exist, wait and try again
436
+ if (!block) {
437
+ await utils_2.Utils.sleep(this.config.BLOCK_CHECK_INTERVAL);
438
+ return;
439
+ }
440
+ // Get the block date and time
441
+ const blockTime = new Date(`${block.timestamp}Z`);
442
+ if (this.lastBlockNumber !== blockNumber) {
443
+ this.processActions().catch(error => {
444
+ console.error('[Streamer] Error processing actions:', error);
445
+ });
446
+ }
447
+ this.blockId = block.block_id;
448
+ this.previousBlockId = block.previous;
449
+ this.transactionId = block.transaction_ids[1];
450
+ this.blockTime = blockTime;
451
+ if (this.adapter?.processBlock) {
452
+ this.adapter.processBlock(block);
453
+ }
454
+ // Process transactions with improved concurrency
455
+ const transactions = block.transactions;
456
+ const transactionIds = block.transaction_ids;
457
+ // Create operation processing promises for better concurrency
458
+ const operationPromises = [];
459
+ for (let i = 0; i < transactions.length; i++) {
460
+ const transaction = transactions[i];
461
+ const operations = transaction.operations;
462
+ // Process operations in batch for better performance
463
+ for (let opIndex = 0; opIndex < operations.length; opIndex++) {
464
+ const op = operations[opIndex];
465
+ // Create promise for each operation (but don't await yet)
466
+ const operationPromise = this.processOperation(op, blockNumber, block.block_id, block.previous, transactionIds[i], blockTime).catch(error => {
467
+ console.error('[Streamer] Operation processing error:', error, {
468
+ blockNumber,
469
+ transactionIndex: i,
470
+ operationIndex: opIndex
471
+ });
472
+ });
473
+ operationPromises.push(operationPromise);
474
+ // Process in batches to avoid overwhelming the system
475
+ if (operationPromises.length >= 50) {
476
+ await Promise.all(operationPromises);
477
+ operationPromises.length = 0; // Clear array
478
+ }
479
+ }
480
+ }
481
+ // Process any remaining operations
482
+ if (operationPromises.length > 0) {
483
+ await Promise.all(operationPromises);
484
+ }
485
+ this.lastBlockNumber = blockNumber;
486
+ this.saveStateThrottled();
487
+ }
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
+ };
498
+ if (this.adapter?.processOperation) {
499
+ await this.adapter.processOperation(op, blockNumber, blockId, prevBlockId, trxId, blockTime);
500
+ }
501
+ // Operation is a "comment" which could either be a post or comment
502
+ if (operationType === 'comment') {
503
+ // This is a post
504
+ if (operationData.parent_author === '') {
505
+ this.postSubscriptions.forEach(sub => {
506
+ sub.callback(operationData, blockNumber, blockId, prevBlockId, trxId, blockTime);
507
+ });
508
+ // This is a comment
509
+ }
510
+ else {
511
+ this.commentSubscriptions.forEach(sub => {
512
+ sub.callback(operationData, blockNumber, blockId, prevBlockId, trxId, blockTime);
513
+ });
514
+ }
515
+ }
516
+ // This is a transfer
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
+ });
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);
548
+ }
549
+ this.transferSubscriptions.forEach(sub => {
550
+ if (sub.account === operationData.to) {
551
+ sub.callback(operationData, blockNumber, blockId, prevBlockId, trxId, blockTime);
552
+ }
553
+ });
554
+ }
555
+ // This is a custom JSON operation
556
+ if (operationType === 'custom_json') {
557
+ let isSignedWithActiveKey = false;
558
+ let sender;
559
+ const id = operationData?.id;
560
+ if (operationData?.required_auths?.length > 0) {
561
+ sender = operationData.required_auths[0];
562
+ isSignedWithActiveKey = true;
563
+ }
564
+ else if (operationData?.required_posting_auths?.length > 0) {
565
+ sender = operationData.required_posting_auths[0];
566
+ isSignedWithActiveKey = false;
567
+ }
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
+ });
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);
593
+ }
594
+ this.customJsonSubscriptions.forEach(sub => {
595
+ sub.callback(operationData, { sender, isSignedWithActiveKey }, blockNumber, blockId, prevBlockId, trxId, blockTime);
596
+ });
597
+ this.customJsonIdSubscriptions.forEach(sub => {
598
+ if (sub.id === operationData.id) {
599
+ sub.callback(operationData, { sender, isSignedWithActiveKey }, blockNumber, blockId, prevBlockId, trxId, blockTime);
600
+ }
601
+ });
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
+ }
613
+ }
614
+ catch (e) {
615
+ console.error(e);
616
+ }
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
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);
659
+ }
660
+ });
661
+ }
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
+ }
775
+ async processActions() {
776
+ if (!this.latestBlockchainTime || this.actions.length === 0) {
777
+ return;
778
+ }
779
+ const currentTime = this.latestBlockchainTime.getTime();
780
+ const executedActions = [];
781
+ // Process actions in batch with optimized time calculations
782
+ for (let i = 0; i < this.actions.length; i++) {
783
+ const action = this.actions[i];
784
+ // Skip disabled actions or actions that have reached max executions
785
+ if (!action.enabled || action.hasReachedMaxExecutions()) {
786
+ continue;
787
+ }
788
+ // Get contract from cache or find and cache it
789
+ let contract = this.contractCache.get(action.contractName);
790
+ if (!contract) {
791
+ contract = this.contracts.find(c => c.name === action.contractName);
792
+ if (contract) {
793
+ this.contractCache.set(action.contractName, contract);
794
+ }
795
+ }
796
+ // Contract doesn't exist or method doesn't exist, log warning and skip
797
+ if (!contract) {
798
+ console.warn(`[Streamer] Contract '${action.contractName}' not found for action '${action.id}'`);
799
+ continue;
800
+ }
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}'`);
808
+ continue;
809
+ }
810
+ // Get frequency in seconds from optimized map
811
+ const frequencySeconds = this.actionFrequencyMap.get(action.timeValue);
812
+ if (!frequencySeconds) {
813
+ console.warn(`[Streamer] Invalid time value '${action.timeValue}' for action '${action.id}'`);
814
+ continue;
815
+ }
816
+ // Optimized time difference calculation using timestamps
817
+ const actionTime = action.date.getTime();
818
+ const differenceSeconds = (currentTime - actionTime) / 1000;
819
+ // Check if enough time has passed
820
+ if (differenceSeconds >= frequencySeconds) {
821
+ try {
822
+ // Execute the action with error isolation
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);
829
+ // Reset the action timer and increment execution count
830
+ action.reset();
831
+ action.incrementExecutionCount();
832
+ executedActions.push(action.id);
833
+ if (this.config.DEBUG_MODE) {
834
+ console.log(`[Streamer] Executed action: ${action.id} (execution #${action.executionCount})`);
835
+ }
836
+ }
837
+ catch (error) {
838
+ const err = error instanceof Error ? error : new Error(String(error));
839
+ console.error(`[Streamer] Action execution error for ${action.contractName}.${action.contractAction}:`, {
840
+ actionId: action.id,
841
+ error: err.message,
842
+ stack: err.stack,
843
+ payload: action.payload
844
+ });
845
+ // Optionally disable action after repeated failures
846
+ // This could be configurable in the future
847
+ }
848
+ }
849
+ }
850
+ // Save state if any actions were executed
851
+ if (executedActions.length > 0) {
852
+ await this.saveActionsToDisk();
853
+ }
854
+ // Clean up disabled or completed actions periodically
855
+ this.cleanupActions();
856
+ }
857
+ /**
858
+ * Clean up completed or disabled actions to prevent memory leaks
859
+ */
860
+ cleanupActions() {
861
+ const beforeCount = this.actions.length;
862
+ // Remove actions that have reached their max executions
863
+ this.actions = this.actions.filter(action => {
864
+ if (action.hasReachedMaxExecutions()) {
865
+ if (this.config.DEBUG_MODE) {
866
+ console.log(`[Streamer] Removing completed action: ${action.id} (${action.executionCount}/${action.maxExecutions} executions)`);
867
+ }
868
+ return false;
869
+ }
870
+ return true;
871
+ });
872
+ const afterCount = this.actions.length;
873
+ if (beforeCount !== afterCount) {
874
+ // Save state if we removed any actions
875
+ this.saveActionsToDisk().catch(error => {
876
+ console.error('[Streamer] Failed to save state after action cleanup:', error);
877
+ });
878
+ }
879
+ }
880
+ async saveStateToDisk() {
881
+ if (this.adapter?.saveState) {
882
+ await this.adapter.saveState({ lastBlockNumber: this.lastBlockNumber, actions: this.actions });
883
+ }
884
+ }
885
+ // Throttled state saving for performance
886
+ saveStateThrottled() {
887
+ const now = Date.now();
888
+ if (now - this.lastStateSave > this.stateSaveInterval) {
889
+ this.lastStateSave = now;
890
+ // Save state asynchronously without blocking block processing
891
+ this.saveStateToDisk().catch(error => {
892
+ console.error('[Streamer] State save error:', error);
893
+ });
894
+ }
895
+ }
896
+ saveToHiveApi(from, data) {
897
+ return utils_2.Utils.transferHiveTokens(this.client, this.config, from, 'hiveapi', '0.001', 'HIVE', data);
898
+ }
899
+ getAccountTransfers(account, from = -1, limit = 100) {
900
+ return utils_2.Utils.getAccountTransfers(this.client, account, from, limit);
901
+ }
902
+ transferHiveTokens(from, to, amount, symbol, memo = '') {
903
+ return utils_2.Utils.transferHiveTokens(this.client, this.config, from, to, amount, symbol, memo);
904
+ }
905
+ transferHiveTokensMultiple(from, accounts = [], amount = '0', symbol, memo = '') {
906
+ return utils_2.Utils.transferHiveTokensMultiple(this.client, this.config, from, accounts, amount, symbol, memo);
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
+ }
944
+ transferHiveEngineTokens(from, to, symbol, quantity, memo = '') {
945
+ return utils_2.Utils.transferHiveEngineTokens(this.client, this.config, from, to, quantity, symbol, memo);
946
+ }
947
+ transferHiveEngineTokensMultiple(from, accounts = [], symbol, memo = '', amount = '0') {
948
+ return utils_2.Utils.transferHiveEngineTokensMultiple(this.client, this.config, from, accounts, symbol, memo, amount);
949
+ }
950
+ issueHiveEngineTokens(from, to, symbol, quantity, memo = '') {
951
+ return utils_2.Utils.issueHiveEngineTokens(this.client, this.config, from, to, symbol, quantity, memo);
952
+ }
953
+ issueHiveEngineTokensMultiple(from, accounts = [], symbol, memo = '', amount = '0') {
954
+ return utils_2.Utils.issueHiveEngineTokensMultiple(this.client, this.config, from, accounts, symbol, memo, amount);
955
+ }
956
+ upvote(votePercentage = '100.0', username, permlink) {
957
+ return utils_2.Utils.upvote(this.client, this.config, this.username, votePercentage, username, permlink);
958
+ }
959
+ downvote(votePercentage = '100.0', username, permlink) {
960
+ return utils_2.Utils.downvote(this.client, this.config, this.username, votePercentage, username, permlink);
961
+ }
962
+ getTransaction(blockNumber, transactionId) {
963
+ return utils_2.Utils.getTransaction(this.client, blockNumber, transactionId);
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
+ }
976
+ verifyTransfer(transaction, from, to, amount) {
977
+ return utils_2.Utils.verifyTransfer(transaction, from, to, amount);
978
+ }
979
+ onComment(callback) {
980
+ this.commentSubscriptions.push({
981
+ callback
982
+ });
983
+ }
984
+ onPost(callback) {
985
+ this.postSubscriptions.push({
986
+ callback
987
+ });
988
+ }
989
+ onTransfer(account, callback) {
990
+ this.transferSubscriptions.push({
991
+ account,
992
+ callback
993
+ });
994
+ }
995
+ onCustomJson(callback) {
996
+ this.customJsonSubscriptions.push({ callback });
997
+ }
998
+ onCustomJsonId(callback, id) {
999
+ this.customJsonIdSubscriptions.push({ callback, id });
1000
+ }
1001
+ onHiveEngine(callback) {
1002
+ this.customJsonHiveEngineSubscriptions.push({ callback });
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
+ }
1016
+ // Memory management: cleanup subscriptions
1017
+ cleanupSubscriptions() {
1018
+ // Limit subscription arrays to prevent memory leaks
1019
+ if (this.customJsonSubscriptions.length > this.maxSubscriptions) {
1020
+ this.customJsonSubscriptions = this.customJsonSubscriptions.slice(-this.maxSubscriptions);
1021
+ console.warn(`[Streamer] Trimmed customJsonSubscriptions to ${this.maxSubscriptions} items`);
1022
+ }
1023
+ if (this.customJsonIdSubscriptions.length > this.maxSubscriptions) {
1024
+ this.customJsonIdSubscriptions = this.customJsonIdSubscriptions.slice(-this.maxSubscriptions);
1025
+ console.warn(`[Streamer] Trimmed customJsonIdSubscriptions to ${this.maxSubscriptions} items`);
1026
+ }
1027
+ if (this.customJsonHiveEngineSubscriptions.length > this.maxSubscriptions) {
1028
+ this.customJsonHiveEngineSubscriptions = this.customJsonHiveEngineSubscriptions.slice(-this.maxSubscriptions);
1029
+ console.warn(`[Streamer] Trimmed customJsonHiveEngineSubscriptions to ${this.maxSubscriptions} items`);
1030
+ }
1031
+ if (this.commentSubscriptions.length > this.maxSubscriptions) {
1032
+ this.commentSubscriptions = this.commentSubscriptions.slice(-this.maxSubscriptions);
1033
+ console.warn(`[Streamer] Trimmed commentSubscriptions to ${this.maxSubscriptions} items`);
1034
+ }
1035
+ if (this.postSubscriptions.length > this.maxSubscriptions) {
1036
+ this.postSubscriptions = this.postSubscriptions.slice(-this.maxSubscriptions);
1037
+ console.warn(`[Streamer] Trimmed postSubscriptions to ${this.maxSubscriptions} items`);
1038
+ }
1039
+ if (this.transferSubscriptions.length > this.maxSubscriptions) {
1040
+ this.transferSubscriptions = this.transferSubscriptions.slice(-this.maxSubscriptions);
1041
+ console.warn(`[Streamer] Trimmed transferSubscriptions to ${this.maxSubscriptions} items`);
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
+ }
1047
+ }
1048
+ // Add method to remove specific subscriptions
1049
+ removeTransferSubscription(account) {
1050
+ this.transferSubscriptions = this.transferSubscriptions.filter(sub => sub.account !== account);
1051
+ }
1052
+ removeCustomJsonIdSubscription(id) {
1053
+ this.customJsonIdSubscriptions = this.customJsonIdSubscriptions.filter(sub => sub.id !== id);
1054
+ }
1055
+ removeEscrowSubscriptions(type) {
1056
+ if (!type) {
1057
+ this.escrowSubscriptions = [];
1058
+ return;
1059
+ }
1060
+ this.escrowSubscriptions = this.escrowSubscriptions.filter(sub => sub.type !== type);
1061
+ }
1062
+ }
1063
+ exports.Streamer = Streamer;
546
1064
  //# sourceMappingURL=streamer.js.map