hive-stream 3.0.0 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +35 -0
- package/DOCUMENTATION.md +380 -0
- package/README.md +113 -22
- 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 +3 -0
- package/dist/config.js +6 -3
- 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 +10 -3
- package/dist/index.js +18 -4
- package/dist/index.js.map +1 -1
- package/dist/streamer.d.ts +101 -8
- package/dist/streamer.js +410 -140
- 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/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/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 +238 -0
- package/tests/hive-rates.spec.ts +443 -0
- package/tests/integration/hive-rates.integration.spec.ts +35 -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,6 +19,7 @@ class Streamer {
|
|
|
19
19
|
commentSubscriptions = [];
|
|
20
20
|
postSubscriptions = [];
|
|
21
21
|
transferSubscriptions = [];
|
|
22
|
+
escrowSubscriptions = [];
|
|
22
23
|
attempts = 0;
|
|
23
24
|
config = config_1.Config;
|
|
24
25
|
client;
|
|
@@ -29,6 +30,9 @@ class Streamer {
|
|
|
29
30
|
blockNumberTimeout = null;
|
|
30
31
|
latestBlockTimer = null;
|
|
31
32
|
lastBlockNumber = 0;
|
|
33
|
+
headBlockNumber = 0;
|
|
34
|
+
isPollingBlock = false;
|
|
35
|
+
isCatchingUp = false;
|
|
32
36
|
blockId;
|
|
33
37
|
previousBlockId;
|
|
34
38
|
transactionId;
|
|
@@ -146,9 +150,20 @@ class Streamer {
|
|
|
146
150
|
if (!contract) {
|
|
147
151
|
throw new Error(`Contract '${action.contractName}' not found for action '${action.id}'`);
|
|
148
152
|
}
|
|
149
|
-
|
|
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
|
}
|
|
@@ -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,292 @@ class Streamer {
|
|
|
453
486
|
this.saveStateThrottled();
|
|
454
487
|
}
|
|
455
488
|
async processOperation(op, blockNumber, blockId, prevBlockId, trxId, blockTime) {
|
|
489
|
+
const operationType = op[0];
|
|
490
|
+
const operationData = op[1];
|
|
491
|
+
const operationMetadata = {
|
|
492
|
+
blockNumber,
|
|
493
|
+
blockId,
|
|
494
|
+
previousBlockId: prevBlockId,
|
|
495
|
+
transactionId: trxId,
|
|
496
|
+
blockTime
|
|
497
|
+
};
|
|
456
498
|
if (this.adapter?.processOperation) {
|
|
457
|
-
this.adapter.processOperation(op, blockNumber, blockId, prevBlockId, trxId, blockTime);
|
|
499
|
+
await this.adapter.processOperation(op, blockNumber, blockId, prevBlockId, trxId, blockTime);
|
|
458
500
|
}
|
|
459
501
|
// Operation is a "comment" which could either be a post or comment
|
|
460
|
-
if (
|
|
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
|
+
try {
|
|
606
|
+
const txInfo = await this.hive.getTransactionInfo(trxId);
|
|
607
|
+
const logs = txInfo && txInfo.logs ? utils_2.Utils.jsonParse(txInfo.logs) : null;
|
|
608
|
+
if (txInfo && logs && typeof logs.errors === 'undefined') {
|
|
609
|
+
await Promise.all(this.customJsonHiveEngineSubscriptions.map(async (sub) => {
|
|
610
|
+
sub.callback(contractName, contractAction, contractPayload, sender, operationData, blockNumber, blockId, prevBlockId, trxId, blockTime);
|
|
611
|
+
}));
|
|
612
|
+
}
|
|
546
613
|
}
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
isSignedWithActiveKey = false;
|
|
614
|
+
catch (e) {
|
|
615
|
+
console.error(e);
|
|
550
616
|
}
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
}
|
|
565
|
-
catch (e) {
|
|
566
|
-
console.error(e);
|
|
567
|
-
return;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
// Recurrent transfers carry payloads in memo, similar to transfer.
|
|
620
|
+
if (operationType === 'recurrent_transfer') {
|
|
621
|
+
const sender = operationData?.from;
|
|
622
|
+
const json = utils_2.Utils.jsonParse(operationData?.memo);
|
|
623
|
+
const payload = this.normalizeContractPayload(json?.[this.config.PAYLOAD_IDENTIFIER]);
|
|
624
|
+
if (payload) {
|
|
625
|
+
const context = this.buildContractContext('recurrent_transfer', blockNumber, blockId, prevBlockId, trxId, blockTime, {
|
|
626
|
+
sender,
|
|
627
|
+
operation: {
|
|
628
|
+
type: operationType,
|
|
629
|
+
data: operationData
|
|
568
630
|
}
|
|
631
|
+
});
|
|
632
|
+
await this.dispatchContractAction(payload, context);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
if (this.isEscrowOperationType(operationType)) {
|
|
636
|
+
const escrow = this.buildEscrowDetails(operationType, operationData);
|
|
637
|
+
const sender = operationData?.from;
|
|
638
|
+
if (this.adapter?.processEscrow) {
|
|
639
|
+
await this.adapter.processEscrow(operationType, operationData, operationMetadata);
|
|
640
|
+
}
|
|
641
|
+
if (operationType === 'escrow_transfer') {
|
|
642
|
+
const jsonMeta = utils_2.Utils.jsonParse(operationData?.json_meta);
|
|
643
|
+
const payload = this.normalizeContractPayload(jsonMeta?.[this.config.PAYLOAD_IDENTIFIER]);
|
|
644
|
+
if (payload) {
|
|
645
|
+
const context = this.buildContractContext('escrow_transfer', blockNumber, blockId, prevBlockId, trxId, blockTime, {
|
|
646
|
+
sender,
|
|
647
|
+
escrow,
|
|
648
|
+
operation: {
|
|
649
|
+
type: operationType,
|
|
650
|
+
data: operationData
|
|
651
|
+
}
|
|
652
|
+
});
|
|
653
|
+
await this.dispatchContractAction(payload, context);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
this.escrowSubscriptions.forEach(sub => {
|
|
657
|
+
if (sub.type === operationType) {
|
|
658
|
+
sub.callback(operationData, blockNumber, blockId, prevBlockId, trxId, blockTime);
|
|
569
659
|
}
|
|
570
660
|
});
|
|
571
661
|
}
|
|
572
662
|
}
|
|
663
|
+
normalizeContractPayload(payload) {
|
|
664
|
+
if (!payload || typeof payload !== 'object') {
|
|
665
|
+
return null;
|
|
666
|
+
}
|
|
667
|
+
const contract = typeof payload.contract === 'string'
|
|
668
|
+
? payload.contract
|
|
669
|
+
: (typeof payload.name === 'string' ? payload.name : null);
|
|
670
|
+
const action = typeof payload.action === 'string' ? payload.action : null;
|
|
671
|
+
if (!contract || !action) {
|
|
672
|
+
return null;
|
|
673
|
+
}
|
|
674
|
+
if (!payload.contract && payload.name && this.config.DEBUG_MODE) {
|
|
675
|
+
console.warn('[Streamer] Legacy contract payload detected (name/action). Please migrate to { contract, action, payload }.');
|
|
676
|
+
}
|
|
677
|
+
return {
|
|
678
|
+
contract,
|
|
679
|
+
action,
|
|
680
|
+
payload: payload.payload ?? {},
|
|
681
|
+
meta: payload.meta ?? payload.metadata
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
isEscrowOperationType(operationType) {
|
|
685
|
+
return operationType === 'escrow_transfer'
|
|
686
|
+
|| operationType === 'escrow_approve'
|
|
687
|
+
|| operationType === 'escrow_dispute'
|
|
688
|
+
|| operationType === 'escrow_release';
|
|
689
|
+
}
|
|
690
|
+
buildEscrowDetails(operationType, operation) {
|
|
691
|
+
return {
|
|
692
|
+
type: operationType,
|
|
693
|
+
from: operation?.from,
|
|
694
|
+
to: operation?.to,
|
|
695
|
+
agent: operation?.agent,
|
|
696
|
+
escrowId: operation?.escrow_id,
|
|
697
|
+
who: operation?.who,
|
|
698
|
+
receiver: operation?.receiver,
|
|
699
|
+
hiveAmount: operation?.hive_amount,
|
|
700
|
+
hbdAmount: operation?.hbd_amount,
|
|
701
|
+
fee: operation?.fee,
|
|
702
|
+
ratificationDeadline: operation?.ratification_deadline,
|
|
703
|
+
expiration: operation?.escrow_expiration,
|
|
704
|
+
approved: typeof operation?.approve === 'boolean' ? operation.approve : undefined
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
buildContractContext(trigger, blockNumber, blockId, previousBlockId, transactionId, blockTime, details) {
|
|
708
|
+
return {
|
|
709
|
+
trigger,
|
|
710
|
+
streamer: this,
|
|
711
|
+
adapter: this.adapter,
|
|
712
|
+
config: this.config,
|
|
713
|
+
block: {
|
|
714
|
+
number: blockNumber,
|
|
715
|
+
id: blockId,
|
|
716
|
+
previousId: previousBlockId,
|
|
717
|
+
time: blockTime
|
|
718
|
+
},
|
|
719
|
+
transaction: {
|
|
720
|
+
id: transactionId
|
|
721
|
+
},
|
|
722
|
+
sender: details.sender,
|
|
723
|
+
transfer: details.transfer,
|
|
724
|
+
customJson: details.customJson,
|
|
725
|
+
escrow: details.escrow,
|
|
726
|
+
operation: details.operation
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
async dispatchContractAction(payload, context) {
|
|
730
|
+
const contract = this.contractCache.get(payload.contract) ||
|
|
731
|
+
this.contracts.find(c => c.name === payload.contract);
|
|
732
|
+
if (!contract) {
|
|
733
|
+
console.warn(`[Streamer] Contract '${payload.contract}' not found for action '${payload.action}'`);
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
if (contract && !this.contractCache.has(payload.contract)) {
|
|
737
|
+
this.contractCache.set(payload.contract, contract);
|
|
738
|
+
}
|
|
739
|
+
const actionDefinition = contract.actions?.[payload.action];
|
|
740
|
+
if (!actionDefinition || typeof actionDefinition.handler !== 'function') {
|
|
741
|
+
console.warn(`[Streamer] Action '${payload.action}' not found in contract '${payload.contract}'`);
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
if (!this.isActionTriggerAllowed(actionDefinition, context.trigger)) {
|
|
745
|
+
console.warn(`[Streamer] Action '${payload.action}' does not allow trigger '${context.trigger}'`);
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
if (actionDefinition.requiresActiveKey &&
|
|
749
|
+
context.trigger === 'custom_json' &&
|
|
750
|
+
!context.customJson?.isSignedWithActiveKey) {
|
|
751
|
+
console.warn(`[Streamer] Action '${payload.action}' requires active key signature`);
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
let actionPayload = payload.payload ?? {};
|
|
755
|
+
if (actionDefinition.schema) {
|
|
756
|
+
const result = actionDefinition.schema.safeParse(actionPayload);
|
|
757
|
+
if (!result.success) {
|
|
758
|
+
console.warn(`[Streamer] Invalid payload for ${payload.contract}.${payload.action}`, {
|
|
759
|
+
errors: result.error?.errors
|
|
760
|
+
});
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
actionPayload = result.data;
|
|
764
|
+
}
|
|
765
|
+
try {
|
|
766
|
+
await actionDefinition.handler(actionPayload, context);
|
|
767
|
+
}
|
|
768
|
+
catch (error) {
|
|
769
|
+
console.error(`[Streamer] Contract action error for ${payload.contract}.${payload.action}:`, error);
|
|
770
|
+
if (context.trigger === 'time') {
|
|
771
|
+
throw error;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
573
775
|
async processActions() {
|
|
574
776
|
if (!this.latestBlockchainTime || this.actions.length === 0) {
|
|
575
777
|
return;
|
|
@@ -596,8 +798,13 @@ class Streamer {
|
|
|
596
798
|
console.warn(`[Streamer] Contract '${action.contractName}' not found for action '${action.id}'`);
|
|
597
799
|
continue;
|
|
598
800
|
}
|
|
599
|
-
|
|
600
|
-
|
|
801
|
+
const actionDefinition = contract.actions?.[action.contractAction];
|
|
802
|
+
if (!actionDefinition || typeof actionDefinition.handler !== 'function') {
|
|
803
|
+
console.warn(`[Streamer] Action '${action.contractAction}' not found in contract '${action.contractName}' for action '${action.id}'`);
|
|
804
|
+
continue;
|
|
805
|
+
}
|
|
806
|
+
if (!this.isActionTriggerAllowed(actionDefinition, 'time')) {
|
|
807
|
+
console.warn(`[Streamer] Action '${action.contractAction}' does not allow time triggers for action '${action.id}'`);
|
|
601
808
|
continue;
|
|
602
809
|
}
|
|
603
810
|
// Get frequency in seconds from optimized map
|
|
@@ -613,7 +820,12 @@ class Streamer {
|
|
|
613
820
|
if (differenceSeconds >= frequencySeconds) {
|
|
614
821
|
try {
|
|
615
822
|
// Execute the action with error isolation
|
|
616
|
-
|
|
823
|
+
const context = this.buildContractContext('time', this.lastBlockNumber, this.blockId, this.previousBlockId, action.id, this.latestBlockchainTime || new Date(), {});
|
|
824
|
+
await this.dispatchContractAction({
|
|
825
|
+
contract: action.contractName,
|
|
826
|
+
action: action.contractAction,
|
|
827
|
+
payload: action.payload || {}
|
|
828
|
+
}, context);
|
|
617
829
|
// Reset the action timer and increment execution count
|
|
618
830
|
action.reset();
|
|
619
831
|
action.incrementExecutionCount();
|
|
@@ -624,7 +836,7 @@ class Streamer {
|
|
|
624
836
|
}
|
|
625
837
|
catch (error) {
|
|
626
838
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
627
|
-
console.error(`[Streamer] Action execution error for ${action.contractName}.${action.
|
|
839
|
+
console.error(`[Streamer] Action execution error for ${action.contractName}.${action.contractAction}:`, {
|
|
628
840
|
actionId: action.id,
|
|
629
841
|
error: err.message,
|
|
630
842
|
stack: err.stack,
|
|
@@ -642,18 +854,6 @@ class Streamer {
|
|
|
642
854
|
// Clean up disabled or completed actions periodically
|
|
643
855
|
this.cleanupActions();
|
|
644
856
|
}
|
|
645
|
-
/**
|
|
646
|
-
* Execute a single action with proper isolation
|
|
647
|
-
*/
|
|
648
|
-
async executeAction(action, contract) {
|
|
649
|
-
const method = contract.contract[action.contractMethod];
|
|
650
|
-
if (method.constructor.name === 'AsyncFunction') {
|
|
651
|
-
await method.call(contract.contract, action.payload);
|
|
652
|
-
}
|
|
653
|
-
else {
|
|
654
|
-
method.call(contract.contract, action.payload);
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
857
|
/**
|
|
658
858
|
* Clean up completed or disabled actions to prevent memory leaks
|
|
659
859
|
*/
|
|
@@ -705,8 +905,44 @@ class Streamer {
|
|
|
705
905
|
transferHiveTokensMultiple(from, accounts = [], amount = '0', symbol, memo = '') {
|
|
706
906
|
return utils_2.Utils.transferHiveTokensMultiple(this.client, this.config, from, accounts, amount, symbol, memo);
|
|
707
907
|
}
|
|
908
|
+
broadcastOperations(operations, signingKeys) {
|
|
909
|
+
return utils_2.Utils.broadcastOperations(this.client, operations, signingKeys || this.config.ACTIVE_KEY);
|
|
910
|
+
}
|
|
911
|
+
broadcastMultiSigOperations(operations, signingKeys) {
|
|
912
|
+
return utils_2.Utils.broadcastMultiSigOperations(this.client, operations, signingKeys);
|
|
913
|
+
}
|
|
914
|
+
createAuthority(keyAuths = [], accountAuths = [], weightThreshold = 1) {
|
|
915
|
+
return utils_2.Utils.createAuthority(keyAuths, accountAuths, weightThreshold);
|
|
916
|
+
}
|
|
917
|
+
updateAccountAuthorities(account, authorityUpdate, signingKeys) {
|
|
918
|
+
return utils_2.Utils.updateAccountAuthorities(this.client, this.config, account, authorityUpdate, signingKeys);
|
|
919
|
+
}
|
|
920
|
+
escrowTransfer(options, signingKeys) {
|
|
921
|
+
return utils_2.Utils.escrowTransfer(this.client, this.config, options, signingKeys);
|
|
922
|
+
}
|
|
923
|
+
escrowApprove(options, signingKeys) {
|
|
924
|
+
return utils_2.Utils.escrowApprove(this.client, this.config, options, signingKeys);
|
|
925
|
+
}
|
|
926
|
+
escrowDispute(options, signingKeys) {
|
|
927
|
+
return utils_2.Utils.escrowDispute(this.client, this.config, options, signingKeys);
|
|
928
|
+
}
|
|
929
|
+
escrowRelease(options, signingKeys) {
|
|
930
|
+
return utils_2.Utils.escrowRelease(this.client, this.config, options, signingKeys);
|
|
931
|
+
}
|
|
932
|
+
recurrentTransfer(options, signingKeys) {
|
|
933
|
+
return utils_2.Utils.recurrentTransfer(this.client, this.config, options, signingKeys);
|
|
934
|
+
}
|
|
935
|
+
createProposal(options, signingKeys) {
|
|
936
|
+
return utils_2.Utils.createProposal(this.client, this.config, options, signingKeys);
|
|
937
|
+
}
|
|
938
|
+
updateProposalVotes(options, signingKeys) {
|
|
939
|
+
return utils_2.Utils.updateProposalVotes(this.client, this.config, options, signingKeys);
|
|
940
|
+
}
|
|
941
|
+
removeProposals(options, signingKeys) {
|
|
942
|
+
return utils_2.Utils.removeProposals(this.client, this.config, options, signingKeys);
|
|
943
|
+
}
|
|
708
944
|
transferHiveEngineTokens(from, to, symbol, quantity, memo = '') {
|
|
709
|
-
return utils_2.Utils.transferHiveEngineTokens(this.client, this.config, from, to,
|
|
945
|
+
return utils_2.Utils.transferHiveEngineTokens(this.client, this.config, from, to, quantity, symbol, memo);
|
|
710
946
|
}
|
|
711
947
|
transferHiveEngineTokensMultiple(from, accounts = [], symbol, memo = '', amount = '0') {
|
|
712
948
|
return utils_2.Utils.transferHiveEngineTokensMultiple(this.client, this.config, from, accounts, symbol, memo, amount);
|
|
@@ -726,6 +962,17 @@ class Streamer {
|
|
|
726
962
|
getTransaction(blockNumber, transactionId) {
|
|
727
963
|
return utils_2.Utils.getTransaction(this.client, blockNumber, transactionId);
|
|
728
964
|
}
|
|
965
|
+
getStatus() {
|
|
966
|
+
return {
|
|
967
|
+
lastBlockNumber: this.lastBlockNumber,
|
|
968
|
+
headBlockNumber: this.headBlockNumber,
|
|
969
|
+
blocksBehind: this.headBlockNumber
|
|
970
|
+
? Math.max(0, this.headBlockNumber - this.lastBlockNumber)
|
|
971
|
+
: 0,
|
|
972
|
+
latestBlockchainTime: this.latestBlockchainTime,
|
|
973
|
+
isCatchingUp: this.isCatchingUp
|
|
974
|
+
};
|
|
975
|
+
}
|
|
729
976
|
verifyTransfer(transaction, from, to, amount) {
|
|
730
977
|
return utils_2.Utils.verifyTransfer(transaction, from, to, amount);
|
|
731
978
|
}
|
|
@@ -754,6 +1001,18 @@ class Streamer {
|
|
|
754
1001
|
onHiveEngine(callback) {
|
|
755
1002
|
this.customJsonHiveEngineSubscriptions.push({ callback });
|
|
756
1003
|
}
|
|
1004
|
+
onEscrowTransfer(callback) {
|
|
1005
|
+
this.escrowSubscriptions.push({ type: 'escrow_transfer', callback });
|
|
1006
|
+
}
|
|
1007
|
+
onEscrowApprove(callback) {
|
|
1008
|
+
this.escrowSubscriptions.push({ type: 'escrow_approve', callback });
|
|
1009
|
+
}
|
|
1010
|
+
onEscrowDispute(callback) {
|
|
1011
|
+
this.escrowSubscriptions.push({ type: 'escrow_dispute', callback });
|
|
1012
|
+
}
|
|
1013
|
+
onEscrowRelease(callback) {
|
|
1014
|
+
this.escrowSubscriptions.push({ type: 'escrow_release', callback });
|
|
1015
|
+
}
|
|
757
1016
|
// Memory management: cleanup subscriptions
|
|
758
1017
|
cleanupSubscriptions() {
|
|
759
1018
|
// Limit subscription arrays to prevent memory leaks
|
|
@@ -781,6 +1040,10 @@ class Streamer {
|
|
|
781
1040
|
this.transferSubscriptions = this.transferSubscriptions.slice(-this.maxSubscriptions);
|
|
782
1041
|
console.warn(`[Streamer] Trimmed transferSubscriptions to ${this.maxSubscriptions} items`);
|
|
783
1042
|
}
|
|
1043
|
+
if (this.escrowSubscriptions.length > this.maxSubscriptions) {
|
|
1044
|
+
this.escrowSubscriptions = this.escrowSubscriptions.slice(-this.maxSubscriptions);
|
|
1045
|
+
console.warn(`[Streamer] Trimmed escrowSubscriptions to ${this.maxSubscriptions} items`);
|
|
1046
|
+
}
|
|
784
1047
|
}
|
|
785
1048
|
// Add method to remove specific subscriptions
|
|
786
1049
|
removeTransferSubscription(account) {
|
|
@@ -789,6 +1052,13 @@ class Streamer {
|
|
|
789
1052
|
removeCustomJsonIdSubscription(id) {
|
|
790
1053
|
this.customJsonIdSubscriptions = this.customJsonIdSubscriptions.filter(sub => sub.id !== id);
|
|
791
1054
|
}
|
|
1055
|
+
removeEscrowSubscriptions(type) {
|
|
1056
|
+
if (!type) {
|
|
1057
|
+
this.escrowSubscriptions = [];
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1060
|
+
this.escrowSubscriptions = this.escrowSubscriptions.filter(sub => sub.type !== type);
|
|
1061
|
+
}
|
|
792
1062
|
}
|
|
793
1063
|
exports.Streamer = Streamer;
|
|
794
1064
|
//# sourceMappingURL=streamer.js.map
|