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