hive-stream 2.0.5 → 3.0.0

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 (89) hide show
  1. package/.claude/settings.local.json +12 -0
  2. package/.env.example +2 -2
  3. package/.travis.yml +11 -11
  4. package/CHANGELOG.md +166 -0
  5. package/CLAUDE.md +75 -0
  6. package/LICENSE +21 -21
  7. package/README.md +338 -238
  8. package/dist/actions.d.ts +41 -10
  9. package/dist/actions.js +126 -23
  10. package/dist/actions.js.map +1 -1
  11. package/dist/adapters/base.adapter.d.ts +25 -25
  12. package/dist/adapters/base.adapter.js +63 -49
  13. package/dist/adapters/base.adapter.js.map +1 -1
  14. package/dist/adapters/mongodb.adapter.d.ts +50 -37
  15. package/dist/adapters/mongodb.adapter.js +363 -158
  16. package/dist/adapters/mongodb.adapter.js.map +1 -1
  17. package/dist/adapters/postgresql.adapter.d.ts +49 -0
  18. package/dist/adapters/postgresql.adapter.js +507 -0
  19. package/dist/adapters/postgresql.adapter.js.map +1 -0
  20. package/dist/adapters/sqlite.adapter.d.ts +40 -41
  21. package/dist/adapters/sqlite.adapter.js +470 -397
  22. package/dist/adapters/sqlite.adapter.js.map +1 -1
  23. package/dist/api.d.ts +6 -6
  24. package/dist/api.js +95 -55
  25. package/dist/api.js.map +1 -1
  26. package/dist/config.d.ts +16 -16
  27. package/dist/config.js +18 -18
  28. package/dist/config.js.map +1 -1
  29. package/dist/contracts/coinflip.contract.d.ts +27 -14
  30. package/dist/contracts/coinflip.contract.js +253 -94
  31. package/dist/contracts/coinflip.contract.js.map +1 -1
  32. package/dist/contracts/dice.contract.d.ts +37 -29
  33. package/dist/contracts/dice.contract.js +282 -155
  34. package/dist/contracts/dice.contract.js.map +1 -1
  35. package/dist/contracts/lotto.contract.d.ts +20 -20
  36. package/dist/contracts/lotto.contract.js +246 -246
  37. package/dist/contracts/nft.contract.d.ts +24 -0
  38. package/dist/contracts/nft.contract.js +533 -0
  39. package/dist/contracts/nft.contract.js.map +1 -0
  40. package/dist/contracts/token.contract.d.ts +18 -0
  41. package/dist/contracts/token.contract.js +263 -0
  42. package/dist/contracts/token.contract.js.map +1 -0
  43. package/dist/exchanges/bittrex.d.ts +6 -6
  44. package/dist/exchanges/bittrex.js +34 -34
  45. package/dist/exchanges/coingecko.d.ts +5 -0
  46. package/dist/exchanges/coingecko.js +40 -0
  47. package/dist/exchanges/coingecko.js.map +1 -0
  48. package/dist/exchanges/exchange.d.ts +9 -9
  49. package/dist/exchanges/exchange.js +26 -26
  50. package/dist/hive-rates.d.ts +9 -9
  51. package/dist/hive-rates.js +121 -75
  52. package/dist/hive-rates.js.map +1 -1
  53. package/dist/index.d.ts +12 -11
  54. package/dist/index.js +33 -32
  55. package/dist/index.js.map +1 -1
  56. package/dist/streamer.d.ts +140 -93
  57. package/dist/streamer.js +793 -545
  58. package/dist/streamer.js.map +1 -1
  59. package/dist/test.d.ts +1 -1
  60. package/dist/test.js +25 -25
  61. package/dist/test.js.map +1 -1
  62. package/dist/types/hive-stream.d.ts +35 -6
  63. package/dist/types/hive-stream.js +2 -2
  64. package/dist/utils.d.ts +27 -27
  65. package/dist/utils.js +271 -261
  66. package/dist/utils.js.map +1 -1
  67. package/ecosystem.config.js +17 -17
  68. package/jest.config.js +8 -8
  69. package/package.json +53 -48
  70. package/test-contract-block.md +18 -18
  71. package/tests/actions.spec.ts +252 -0
  72. package/tests/adapters/actions-persistence.spec.ts +144 -0
  73. package/tests/adapters/postgresql.adapter.spec.ts +127 -0
  74. package/tests/adapters/sqlite.adapter.spec.ts +180 -42
  75. package/tests/contracts/coinflip.contract.spec.ts +221 -131
  76. package/tests/contracts/dice.contract.spec.ts +202 -159
  77. package/tests/contracts/entrants.json +728 -728
  78. package/tests/contracts/lotto.contract.spec.ts +323 -323
  79. package/tests/contracts/nft.contract.spec.ts +948 -0
  80. package/tests/contracts/token.contract.spec.ts +334 -0
  81. package/tests/helpers/mock-adapter.ts +214 -0
  82. package/tests/setup.ts +29 -18
  83. package/tests/streamer-actions.spec.ts +263 -0
  84. package/tests/streamer.spec.ts +248 -151
  85. package/tests/utils.spec.ts +91 -94
  86. package/tsconfig.build.json +3 -22
  87. package/tslint.json +20 -20
  88. package/wallaby.js +26 -26
  89. package/.env +0 -3
package/dist/streamer.js CHANGED
@@ -1,546 +1,794 @@
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
+ attempts = 0;
23
+ config = config_1.Config;
24
+ client;
25
+ hive;
26
+ username;
27
+ postingKey;
28
+ activeKey;
29
+ blockNumberTimeout = null;
30
+ latestBlockTimer = null;
31
+ lastBlockNumber = 0;
32
+ blockId;
33
+ previousBlockId;
34
+ transactionId;
35
+ blockTime;
36
+ latestBlockchainTime;
37
+ disableAllProcessing = false;
38
+ contracts = [];
39
+ adapter;
40
+ actions = [];
41
+ // Performance optimization properties
42
+ lastStateSave = Date.now();
43
+ stateSaveInterval = 5000; // Save state every 5 seconds instead of every block
44
+ blockProcessingQueue = [];
45
+ isProcessingQueue = false;
46
+ // Memory management
47
+ maxSubscriptions = 1000;
48
+ subscriptionCleanupInterval = null;
49
+ // Action processing optimization
50
+ actionFrequencyMap = new Map([
51
+ ['3s', 3], ['block', 3], ['10s', 10], ['30s', 30],
52
+ ['1m', 60], ['5m', 300], ['minute', 60], ['15m', 900], ['quarter', 900],
53
+ ['30m', 1800], ['halfhour', 1800], ['hourly', 3600], ['1h', 3600],
54
+ ['12h', 43200], ['halfday', 43200], ['24h', 86400], ['day', 86400], ['daily', 86400],
55
+ ['week', 604800], ['weekly', 604800]
56
+ ]);
57
+ contractCache = new Map();
58
+ // Data caching for performance
59
+ blockCache = new Map();
60
+ transactionCache = new Map();
61
+ accountCache = new Map();
62
+ cacheTimeout = 300000; // 5 minutes
63
+ maxCacheSize = 1000;
64
+ utils = utils_2.Utils;
65
+ constructor(userConfig = {}) {
66
+ this.config = Object.assign(config_1.Config, userConfig);
67
+ this.lastBlockNumber = this.config.LAST_BLOCK_NUMBER;
68
+ this.username = this.config.USERNAME;
69
+ this.postingKey = this.config.POSTING_KEY;
70
+ this.activeKey = this.config.ACTIVE_KEY;
71
+ this.hive = new sscjs_1.default(this.config.HIVE_ENGINE_API);
72
+ this.client = new dhive_1.Client(this.config.API_NODES);
73
+ if (process?.env?.NODE_ENV !== 'test') {
74
+ this._initializeAdapter(new sqlite_adapter_1.SqliteAdapter());
75
+ new api_1.Api(this);
76
+ }
77
+ // Start subscription cleanup interval
78
+ this.subscriptionCleanupInterval = setInterval(() => {
79
+ this.cleanupSubscriptions();
80
+ }, 60000); // Cleanup every minute
81
+ }
82
+ _initializeAdapter(adapter) {
83
+ this.adapter = adapter;
84
+ if (this?.adapter?.create) {
85
+ this.adapter.create();
86
+ }
87
+ }
88
+ async registerAdapter(adapter) {
89
+ if (this.adapter && this.adapter.destroy) {
90
+ try {
91
+ await this.adapter.destroy();
92
+ }
93
+ catch (error) {
94
+ console.warn('[Streamer] Error destroying existing adapter:', error);
95
+ }
96
+ }
97
+ this.adapter = adapter;
98
+ if (this?.adapter?.create) {
99
+ await this.adapter.create();
100
+ }
101
+ }
102
+ getAdapter() {
103
+ return this.adapter;
104
+ }
105
+ /**
106
+ * Register a new action with improved validation and persistence
107
+ */
108
+ async registerAction(action) {
109
+ if (!action || !(action instanceof actions_1.TimeAction)) {
110
+ throw new Error('Invalid action: must be an instance of TimeAction');
111
+ }
112
+ const loadedActions = await this.adapter.loadActions();
113
+ for (const a of loadedActions) {
114
+ const exists = this.actions.find(i => i.id === a.id);
115
+ if (!exists) {
116
+ try {
117
+ const restoredAction = actions_1.TimeAction.fromJSON(a);
118
+ this.actions.push(restoredAction);
119
+ }
120
+ catch (error) {
121
+ console.warn(`[Streamer] Failed to restore action ${a.id}:`, error);
122
+ }
123
+ }
124
+ }
125
+ const exists = this.actions.find(a => a.id === action.id);
126
+ if (!exists) {
127
+ this.validateActionContract(action);
128
+ this.actions.push(action);
129
+ await this.saveActionsToDisk();
130
+ if (this.config.DEBUG_MODE) {
131
+ console.log(`[Streamer] Registered time-based action: ${action.id} (${action.timeValue})`);
132
+ }
133
+ }
134
+ else {
135
+ if (this.config.DEBUG_MODE) {
136
+ console.warn(`[Streamer] Action with ID ${action.id} already exists, skipping registration`);
137
+ }
138
+ }
139
+ }
140
+ /**
141
+ * Validate that the contract and method exist for the action
142
+ */
143
+ validateActionContract(action) {
144
+ const contract = this.contractCache.get(action.contractName) ||
145
+ this.contracts.find(c => c.name === action.contractName);
146
+ if (!contract) {
147
+ throw new Error(`Contract '${action.contractName}' not found for action '${action.id}'`);
148
+ }
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}'`);
151
+ }
152
+ }
153
+ /**
154
+ * Remove an action by ID
155
+ */
156
+ async removeAction(actionId) {
157
+ const index = this.actions.findIndex(a => a.id === actionId);
158
+ if (index >= 0) {
159
+ const removedAction = this.actions.splice(index, 1)[0];
160
+ await this.saveActionsToDisk();
161
+ if (this.config.DEBUG_MODE) {
162
+ console.log(`[Streamer] Removed time-based action: ${actionId}`);
163
+ }
164
+ return true;
165
+ }
166
+ return false;
167
+ }
168
+ /**
169
+ * Get all registered actions
170
+ */
171
+ getActions() {
172
+ return [...this.actions];
173
+ }
174
+ /**
175
+ * Get action by ID
176
+ */
177
+ getAction(actionId) {
178
+ return this.actions.find(a => a.id === actionId);
179
+ }
180
+ /**
181
+ * Enable/disable an action
182
+ */
183
+ async setActionEnabled(actionId, enabled) {
184
+ const action = this.actions.find(a => a.id === actionId);
185
+ if (action) {
186
+ if (enabled) {
187
+ action.enable();
188
+ }
189
+ else {
190
+ action.disable();
191
+ }
192
+ await this.saveActionsToDisk();
193
+ if (this.config.DEBUG_MODE) {
194
+ console.log(`[Streamer] Action ${actionId} ${enabled ? 'enabled' : 'disabled'}`);
195
+ }
196
+ return true;
197
+ }
198
+ return false;
199
+ }
200
+ /**
201
+ * Save actions to disk asynchronously
202
+ */
203
+ async saveActionsToDisk() {
204
+ try {
205
+ if (this.adapter?.saveState) {
206
+ await this.adapter.saveState({
207
+ lastBlockNumber: this.lastBlockNumber,
208
+ actions: this.actions.map(a => a.toJSON())
209
+ });
210
+ }
211
+ }
212
+ catch (error) {
213
+ if (error?.code !== 'SQLITE_MISUSE') {
214
+ console.error('[Streamer] Failed to save actions to disk:', error);
215
+ }
216
+ }
217
+ }
218
+ /**
219
+ * Resets a specific action time value
220
+ */
221
+ async resetAction(id) {
222
+ const action = this.actions.find(i => i.id === id);
223
+ if (action) {
224
+ action.reset();
225
+ await this.saveActionsToDisk();
226
+ if (this.config.DEBUG_MODE) {
227
+ console.log(`[Streamer] Reset action: ${id}`);
228
+ }
229
+ return true;
230
+ }
231
+ return false;
232
+ }
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;
246
+ }
247
+ unregisterContract(name) {
248
+ // Find the registered contract by it's ID
249
+ const contractIndex = this.contracts.findIndex(c => c.name === name);
250
+ 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();
256
+ }
257
+ // Remove the contract
258
+ this.contracts.splice(contractIndex, 1);
259
+ // Remove from cache
260
+ this.contractCache.delete(name);
261
+ }
262
+ }
263
+ /**
264
+ * setConfig
265
+ *
266
+ * Allows specific configuration settings to be overridden
267
+ *
268
+ * @param config
269
+ */
270
+ setConfig(config) {
271
+ Object.assign(this.config, config);
272
+ // Set keys and username incase they have changed
273
+ this.username = this.config.USERNAME;
274
+ this.postingKey = this.config.POSTING_KEY;
275
+ this.activeKey = this.config.ACTIVE_KEY;
276
+ return this;
277
+ }
278
+ /**
279
+ * Start
280
+ *
281
+ * Starts the streamer bot to get blocks from the Hive API
282
+ *
283
+ */
284
+ async start() {
285
+ if (this.config.DEBUG_MODE) {
286
+ console.log('Starting to stream the Hive blockchain');
287
+ }
288
+ this.disableAllProcessing = false;
289
+ const state = await this.adapter.loadState();
290
+ if (this.config.DEBUG_MODE) {
291
+ console.log(`Restoring state from file`);
292
+ }
293
+ if (!this.config.LAST_BLOCK_NUMBER && state?.lastBlockNumber) {
294
+ if (state.lastBlockNumber) {
295
+ this.lastBlockNumber = state.lastBlockNumber;
296
+ }
297
+ }
298
+ // Kicks off the blockchain streaming and operation parsing
299
+ this.getBlock();
300
+ this.latestBlockTimer = setInterval(() => { this.getLatestBlock(); }, this.config.BLOCK_CHECK_INTERVAL);
301
+ return this;
302
+ }
303
+ /**
304
+ * Stop
305
+ *
306
+ * Stops the streamer from running
307
+ */
308
+ async stop() {
309
+ this.disableAllProcessing = true;
310
+ if (this.blockNumberTimeout) {
311
+ clearTimeout(this.blockNumberTimeout);
312
+ }
313
+ if (this.latestBlockTimer) {
314
+ clearInterval(this.latestBlockTimer);
315
+ }
316
+ if (this.subscriptionCleanupInterval) {
317
+ clearInterval(this.subscriptionCleanupInterval);
318
+ }
319
+ if (this?.adapter?.destroy) {
320
+ this.adapter.destroy();
321
+ }
322
+ await (0, utils_1.sleep)(800);
323
+ }
324
+ async getLatestBlock() {
325
+ try {
326
+ const props = await this.client.database.getDynamicGlobalProperties();
327
+ if (props) {
328
+ this.latestBlockchainTime = new Date(`${props.time}Z`);
329
+ }
330
+ }
331
+ catch (error) {
332
+ console.error('[Streamer] Error getting latest block:', error);
333
+ // Continue with cached time if available
334
+ }
335
+ }
336
+ async getBlock() {
337
+ try {
338
+ // Load global properties from the Hive API
339
+ const props = await this.client.database.getDynamicGlobalProperties();
340
+ // 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);
345
+ return;
346
+ }
347
+ // If the block number we've got is zero
348
+ // set it to the last irreversible block number
349
+ if (this.lastBlockNumber === 0) {
350
+ this.lastBlockNumber = props.head_block_number - 1;
351
+ }
352
+ if (this.config.DEBUG_MODE) {
353
+ console.log(`Head block number: `, props.head_block_number);
354
+ console.log(`Last block number: `, this.lastBlockNumber);
355
+ }
356
+ const BLOCKS_BEHIND = parseInt(this.config.BLOCKS_BEHIND_WARNING, 10);
357
+ 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;
366
+ }
367
+ }
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);
371
+ }
372
+ }
373
+ catch (e) {
374
+ const error = e instanceof Error ? e : new Error(String(e));
375
+ console.error(`[Streamer] Block processing error: ${error.message}`, {
376
+ stack: error.stack,
377
+ blockNumber: this.lastBlockNumber + 1
378
+ });
379
+ // Retry after a longer delay on error
380
+ if (!this.disableAllProcessing) {
381
+ this.blockNumberTimeout = setTimeout(() => { this.getBlock(); }, this.config.BLOCK_CHECK_INTERVAL * 2);
382
+ }
383
+ }
384
+ }
385
+ // Takes the block from Hive and allows us to work with it
386
+ async loadBlock(blockNumber) {
387
+ // Check cache first
388
+ let block = this.blockCache.get(blockNumber);
389
+ if (!block) {
390
+ // Load the block itself from the Hive API
391
+ block = await this.client.database.getBlock(blockNumber);
392
+ // Cache the block for potential reuse
393
+ if (block) {
394
+ this.blockCache.set(blockNumber, block);
395
+ // Cleanup old cache entries
396
+ if (this.blockCache.size > this.maxCacheSize) {
397
+ const oldestKey = this.blockCache.keys().next().value;
398
+ this.blockCache.delete(oldestKey);
399
+ }
400
+ }
401
+ }
402
+ // The block doesn't exist, wait and try again
403
+ if (!block) {
404
+ await utils_2.Utils.sleep(this.config.BLOCK_CHECK_INTERVAL);
405
+ return;
406
+ }
407
+ // Get the block date and time
408
+ const blockTime = new Date(`${block.timestamp}Z`);
409
+ if (this.lastBlockNumber !== blockNumber) {
410
+ this.processActions().catch(error => {
411
+ console.error('[Streamer] Error processing actions:', error);
412
+ });
413
+ }
414
+ this.blockId = block.block_id;
415
+ this.previousBlockId = block.previous;
416
+ this.transactionId = block.transaction_ids[1];
417
+ this.blockTime = blockTime;
418
+ if (this.adapter?.processBlock) {
419
+ this.adapter.processBlock(block);
420
+ }
421
+ // Process transactions with improved concurrency
422
+ const transactions = block.transactions;
423
+ const transactionIds = block.transaction_ids;
424
+ // Create operation processing promises for better concurrency
425
+ const operationPromises = [];
426
+ for (let i = 0; i < transactions.length; i++) {
427
+ const transaction = transactions[i];
428
+ const operations = transaction.operations;
429
+ // Process operations in batch for better performance
430
+ for (let opIndex = 0; opIndex < operations.length; opIndex++) {
431
+ const op = operations[opIndex];
432
+ // Create promise for each operation (but don't await yet)
433
+ const operationPromise = this.processOperation(op, blockNumber, block.block_id, block.previous, transactionIds[i], blockTime).catch(error => {
434
+ console.error('[Streamer] Operation processing error:', error, {
435
+ blockNumber,
436
+ transactionIndex: i,
437
+ operationIndex: opIndex
438
+ });
439
+ });
440
+ operationPromises.push(operationPromise);
441
+ // Process in batches to avoid overwhelming the system
442
+ if (operationPromises.length >= 50) {
443
+ await Promise.all(operationPromises);
444
+ operationPromises.length = 0; // Clear array
445
+ }
446
+ }
447
+ }
448
+ // Process any remaining operations
449
+ if (operationPromises.length > 0) {
450
+ await Promise.all(operationPromises);
451
+ }
452
+ this.lastBlockNumber = blockNumber;
453
+ this.saveStateThrottled();
454
+ }
455
+ async processOperation(op, blockNumber, blockId, prevBlockId, trxId, blockTime) {
456
+ if (this.adapter?.processOperation) {
457
+ this.adapter.processOperation(op, blockNumber, blockId, prevBlockId, trxId, blockTime);
458
+ }
459
+ // Operation is a "comment" which could either be a post or comment
460
+ if (op[0] === 'comment') {
461
+ // This is a post
462
+ if (op[1].parent_author === '') {
463
+ this.postSubscriptions.forEach(sub => {
464
+ sub.callback(op[1], blockNumber, blockId, prevBlockId, trxId, blockTime);
465
+ });
466
+ // This is a comment
467
+ }
468
+ else {
469
+ this.commentSubscriptions.forEach(sub => {
470
+ sub.callback(op[1], blockNumber, blockId, prevBlockId, trxId, blockTime);
471
+ });
472
+ }
473
+ }
474
+ // 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
+ }
494
+ }
495
+ }
496
+ this.transferSubscriptions.forEach(sub => {
497
+ if (sub.account === op[1].to) {
498
+ sub.callback(op[1], blockNumber, blockId, prevBlockId, trxId, blockTime);
499
+ }
500
+ });
501
+ }
502
+ // This is a custom JSON operation
503
+ if (op[0] === 'custom_json') {
504
+ let isSignedWithActiveKey = false;
505
+ let sender;
506
+ const id = op[1]?.id;
507
+ if (op[1]?.required_auths?.length > 0) {
508
+ sender = op[1].required_auths[0];
509
+ isSignedWithActiveKey = true;
510
+ }
511
+ else if (op[1]?.required_posting_auths?.length > 0) {
512
+ sender = op[1].required_posting_auths[0];
513
+ isSignedWithActiveKey = false;
514
+ }
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
+ }
529
+ }
530
+ }
531
+ this.customJsonSubscriptions.forEach(sub => {
532
+ sub.callback(op[1], { sender, isSignedWithActiveKey }, blockNumber, blockId, prevBlockId, trxId, blockTime);
533
+ });
534
+ 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);
538
+ }
539
+ });
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;
546
+ }
547
+ else {
548
+ sender = op[1].required_posting_auths[0];
549
+ isSignedWithActiveKey = false;
550
+ }
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;
568
+ }
569
+ }
570
+ });
571
+ }
572
+ }
573
+ async processActions() {
574
+ if (!this.latestBlockchainTime || this.actions.length === 0) {
575
+ return;
576
+ }
577
+ const currentTime = this.latestBlockchainTime.getTime();
578
+ const executedActions = [];
579
+ // Process actions in batch with optimized time calculations
580
+ for (let i = 0; i < this.actions.length; i++) {
581
+ const action = this.actions[i];
582
+ // Skip disabled actions or actions that have reached max executions
583
+ if (!action.enabled || action.hasReachedMaxExecutions()) {
584
+ continue;
585
+ }
586
+ // Get contract from cache or find and cache it
587
+ let contract = this.contractCache.get(action.contractName);
588
+ if (!contract) {
589
+ contract = this.contracts.find(c => c.name === action.contractName);
590
+ if (contract) {
591
+ this.contractCache.set(action.contractName, contract);
592
+ }
593
+ }
594
+ // Contract doesn't exist or method doesn't exist, log warning and skip
595
+ if (!contract) {
596
+ console.warn(`[Streamer] Contract '${action.contractName}' not found for action '${action.id}'`);
597
+ continue;
598
+ }
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}'`);
601
+ continue;
602
+ }
603
+ // Get frequency in seconds from optimized map
604
+ const frequencySeconds = this.actionFrequencyMap.get(action.timeValue);
605
+ if (!frequencySeconds) {
606
+ console.warn(`[Streamer] Invalid time value '${action.timeValue}' for action '${action.id}'`);
607
+ continue;
608
+ }
609
+ // Optimized time difference calculation using timestamps
610
+ const actionTime = action.date.getTime();
611
+ const differenceSeconds = (currentTime - actionTime) / 1000;
612
+ // Check if enough time has passed
613
+ if (differenceSeconds >= frequencySeconds) {
614
+ try {
615
+ // Execute the action with error isolation
616
+ await this.executeAction(action, contract);
617
+ // Reset the action timer and increment execution count
618
+ action.reset();
619
+ action.incrementExecutionCount();
620
+ executedActions.push(action.id);
621
+ if (this.config.DEBUG_MODE) {
622
+ console.log(`[Streamer] Executed action: ${action.id} (execution #${action.executionCount})`);
623
+ }
624
+ }
625
+ catch (error) {
626
+ const err = error instanceof Error ? error : new Error(String(error));
627
+ console.error(`[Streamer] Action execution error for ${action.contractName}.${action.contractMethod}:`, {
628
+ actionId: action.id,
629
+ error: err.message,
630
+ stack: err.stack,
631
+ payload: action.payload
632
+ });
633
+ // Optionally disable action after repeated failures
634
+ // This could be configurable in the future
635
+ }
636
+ }
637
+ }
638
+ // Save state if any actions were executed
639
+ if (executedActions.length > 0) {
640
+ await this.saveActionsToDisk();
641
+ }
642
+ // Clean up disabled or completed actions periodically
643
+ this.cleanupActions();
644
+ }
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
+ /**
658
+ * Clean up completed or disabled actions to prevent memory leaks
659
+ */
660
+ cleanupActions() {
661
+ const beforeCount = this.actions.length;
662
+ // Remove actions that have reached their max executions
663
+ this.actions = this.actions.filter(action => {
664
+ if (action.hasReachedMaxExecutions()) {
665
+ if (this.config.DEBUG_MODE) {
666
+ console.log(`[Streamer] Removing completed action: ${action.id} (${action.executionCount}/${action.maxExecutions} executions)`);
667
+ }
668
+ return false;
669
+ }
670
+ return true;
671
+ });
672
+ const afterCount = this.actions.length;
673
+ if (beforeCount !== afterCount) {
674
+ // Save state if we removed any actions
675
+ this.saveActionsToDisk().catch(error => {
676
+ console.error('[Streamer] Failed to save state after action cleanup:', error);
677
+ });
678
+ }
679
+ }
680
+ async saveStateToDisk() {
681
+ if (this.adapter?.saveState) {
682
+ await this.adapter.saveState({ lastBlockNumber: this.lastBlockNumber, actions: this.actions });
683
+ }
684
+ }
685
+ // Throttled state saving for performance
686
+ saveStateThrottled() {
687
+ const now = Date.now();
688
+ if (now - this.lastStateSave > this.stateSaveInterval) {
689
+ this.lastStateSave = now;
690
+ // Save state asynchronously without blocking block processing
691
+ this.saveStateToDisk().catch(error => {
692
+ console.error('[Streamer] State save error:', error);
693
+ });
694
+ }
695
+ }
696
+ saveToHiveApi(from, data) {
697
+ return utils_2.Utils.transferHiveTokens(this.client, this.config, from, 'hiveapi', '0.001', 'HIVE', data);
698
+ }
699
+ getAccountTransfers(account, from = -1, limit = 100) {
700
+ return utils_2.Utils.getAccountTransfers(this.client, account, from, limit);
701
+ }
702
+ transferHiveTokens(from, to, amount, symbol, memo = '') {
703
+ return utils_2.Utils.transferHiveTokens(this.client, this.config, from, to, amount, symbol, memo);
704
+ }
705
+ transferHiveTokensMultiple(from, accounts = [], amount = '0', symbol, memo = '') {
706
+ return utils_2.Utils.transferHiveTokensMultiple(this.client, this.config, from, accounts, amount, symbol, memo);
707
+ }
708
+ transferHiveEngineTokens(from, to, symbol, quantity, memo = '') {
709
+ return utils_2.Utils.transferHiveEngineTokens(this.client, this.config, from, to, symbol, quantity, memo);
710
+ }
711
+ transferHiveEngineTokensMultiple(from, accounts = [], symbol, memo = '', amount = '0') {
712
+ return utils_2.Utils.transferHiveEngineTokensMultiple(this.client, this.config, from, accounts, symbol, memo, amount);
713
+ }
714
+ issueHiveEngineTokens(from, to, symbol, quantity, memo = '') {
715
+ return utils_2.Utils.issueHiveEngineTokens(this.client, this.config, from, to, symbol, quantity, memo);
716
+ }
717
+ issueHiveEngineTokensMultiple(from, accounts = [], symbol, memo = '', amount = '0') {
718
+ return utils_2.Utils.issueHiveEngineTokensMultiple(this.client, this.config, from, accounts, symbol, memo, amount);
719
+ }
720
+ upvote(votePercentage = '100.0', username, permlink) {
721
+ return utils_2.Utils.upvote(this.client, this.config, this.username, votePercentage, username, permlink);
722
+ }
723
+ downvote(votePercentage = '100.0', username, permlink) {
724
+ return utils_2.Utils.downvote(this.client, this.config, this.username, votePercentage, username, permlink);
725
+ }
726
+ getTransaction(blockNumber, transactionId) {
727
+ return utils_2.Utils.getTransaction(this.client, blockNumber, transactionId);
728
+ }
729
+ verifyTransfer(transaction, from, to, amount) {
730
+ return utils_2.Utils.verifyTransfer(transaction, from, to, amount);
731
+ }
732
+ onComment(callback) {
733
+ this.commentSubscriptions.push({
734
+ callback
735
+ });
736
+ }
737
+ onPost(callback) {
738
+ this.postSubscriptions.push({
739
+ callback
740
+ });
741
+ }
742
+ onTransfer(account, callback) {
743
+ this.transferSubscriptions.push({
744
+ account,
745
+ callback
746
+ });
747
+ }
748
+ onCustomJson(callback) {
749
+ this.customJsonSubscriptions.push({ callback });
750
+ }
751
+ onCustomJsonId(callback, id) {
752
+ this.customJsonIdSubscriptions.push({ callback, id });
753
+ }
754
+ onHiveEngine(callback) {
755
+ this.customJsonHiveEngineSubscriptions.push({ callback });
756
+ }
757
+ // Memory management: cleanup subscriptions
758
+ cleanupSubscriptions() {
759
+ // Limit subscription arrays to prevent memory leaks
760
+ if (this.customJsonSubscriptions.length > this.maxSubscriptions) {
761
+ this.customJsonSubscriptions = this.customJsonSubscriptions.slice(-this.maxSubscriptions);
762
+ console.warn(`[Streamer] Trimmed customJsonSubscriptions to ${this.maxSubscriptions} items`);
763
+ }
764
+ if (this.customJsonIdSubscriptions.length > this.maxSubscriptions) {
765
+ this.customJsonIdSubscriptions = this.customJsonIdSubscriptions.slice(-this.maxSubscriptions);
766
+ console.warn(`[Streamer] Trimmed customJsonIdSubscriptions to ${this.maxSubscriptions} items`);
767
+ }
768
+ if (this.customJsonHiveEngineSubscriptions.length > this.maxSubscriptions) {
769
+ this.customJsonHiveEngineSubscriptions = this.customJsonHiveEngineSubscriptions.slice(-this.maxSubscriptions);
770
+ console.warn(`[Streamer] Trimmed customJsonHiveEngineSubscriptions to ${this.maxSubscriptions} items`);
771
+ }
772
+ if (this.commentSubscriptions.length > this.maxSubscriptions) {
773
+ this.commentSubscriptions = this.commentSubscriptions.slice(-this.maxSubscriptions);
774
+ console.warn(`[Streamer] Trimmed commentSubscriptions to ${this.maxSubscriptions} items`);
775
+ }
776
+ if (this.postSubscriptions.length > this.maxSubscriptions) {
777
+ this.postSubscriptions = this.postSubscriptions.slice(-this.maxSubscriptions);
778
+ console.warn(`[Streamer] Trimmed postSubscriptions to ${this.maxSubscriptions} items`);
779
+ }
780
+ if (this.transferSubscriptions.length > this.maxSubscriptions) {
781
+ this.transferSubscriptions = this.transferSubscriptions.slice(-this.maxSubscriptions);
782
+ console.warn(`[Streamer] Trimmed transferSubscriptions to ${this.maxSubscriptions} items`);
783
+ }
784
+ }
785
+ // Add method to remove specific subscriptions
786
+ removeTransferSubscription(account) {
787
+ this.transferSubscriptions = this.transferSubscriptions.filter(sub => sub.account !== account);
788
+ }
789
+ removeCustomJsonIdSubscription(id) {
790
+ this.customJsonIdSubscriptions = this.customJsonIdSubscriptions.filter(sub => sub.id !== id);
791
+ }
792
+ }
793
+ exports.Streamer = Streamer;
546
794
  //# sourceMappingURL=streamer.js.map