hive-stream 3.0.3 → 3.0.4

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/dist/streamer.js CHANGED
@@ -9,7 +9,9 @@ const sqlite_adapter_1 = require("./adapters/sqlite.adapter");
9
9
  const utils_1 = require("@hiveio/dhive/lib/utils");
10
10
  const actions_1 = require("./actions");
11
11
  const dhive_1 = require("@hiveio/dhive");
12
+ const bignumber_js_1 = __importDefault(require("bignumber.js"));
12
13
  const utils_2 = require("./utils");
14
+ const builders_1 = require("./builders");
13
15
  const config_1 = require("./config");
14
16
  const hive_provider_1 = require("./providers/hive-provider");
15
17
  const sscjs_1 = __importDefault(require("sscjs"));
@@ -73,6 +75,39 @@ class Streamer {
73
75
  cacheTimeout = 300000; // 5 minutes
74
76
  maxCacheSize = 1000;
75
77
  utils = utils_2.Utils;
78
+ money = {
79
+ parseAssetAmount: (rawAmount) => utils_2.Utils.parseAssetAmount(rawAmount),
80
+ formatAmount: (amount, precision) => utils_2.Utils.formatAmount(amount, precision),
81
+ formatAssetAmount: (amount, symbol, precision) => utils_2.Utils.formatAssetAmount(amount, symbol, precision),
82
+ calculatePercentageAmount: (amount, percentage, precision) => utils_2.Utils.calculatePercentageAmount(amount, percentage, precision),
83
+ calculateBasisPointsAmount: (amount, basisPoints, precision) => utils_2.Utils.calculateBasisPointsAmount(amount, basisPoints, precision),
84
+ splitAmountByBasisPoints: (amount, basisPoints, precision) => utils_2.Utils.splitAmountByBasisPoints(amount, basisPoints, precision),
85
+ splitAmountByPercentage: (amount, percentages, precision) => utils_2.Utils.splitAmountByPercentage(amount, percentages, precision),
86
+ splitAmountByWeights: (amount, weights, precision) => utils_2.Utils.splitAmountByWeights(amount, weights, precision),
87
+ };
88
+ flows = {
89
+ incomingTransfers: (account) => new builders_1.IncomingTransfersBuilder(this, account),
90
+ autoBurnIncomingTransfers: (options = {}) => this.autoBurnIncomingTransfers(options),
91
+ autoForwardIncomingTransfers: (options) => this.autoForwardIncomingTransfers(options),
92
+ autoRefundIncomingTransfers: (options = {}) => this.autoRefundIncomingTransfers(options),
93
+ autoSplitIncomingTransfers: (options) => this.autoSplitIncomingTransfers(options),
94
+ autoRouteIncomingTransfers: (options) => this.autoRouteIncomingTransfers(options),
95
+ planIncomingTransferRoutes: (transfer, options) => this.planIncomingTransferRoutes(transfer, options),
96
+ };
97
+ ops = {
98
+ transfer: () => new builders_1.HiveTransferBuilder(this),
99
+ burn: () => new builders_1.HiveBurnBuilder(this),
100
+ escrowTransfer: () => new builders_1.HiveEscrowTransferBuilder(this),
101
+ recurrentTransfer: () => new builders_1.HiveRecurrentTransferBuilder(this),
102
+ createProposal: () => new builders_1.HiveProposalBuilder(this),
103
+ transferEngine: () => new builders_1.HiveEngineTokenTransferBuilder(this),
104
+ burnEngine: () => new builders_1.HiveEngineTokenBurnBuilder(this),
105
+ issueEngine: () => new builders_1.HiveEngineTokenIssueBuilder(this),
106
+ voteProposals: () => new builders_1.HiveProposalVotesBuilder(this),
107
+ removeProposals: () => new builders_1.HiveRemoveProposalsBuilder(this),
108
+ upvote: () => new builders_1.HiveVoteBuilder(this, 'upvote'),
109
+ downvote: () => new builders_1.HiveVoteBuilder(this, 'downvote'),
110
+ };
76
111
  constructor(userConfig = {}) {
77
112
  this.config = (0, config_1.createConfig)(userConfig);
78
113
  this.lastBlockNumber = this.config.LAST_BLOCK_NUMBER;
@@ -116,6 +151,295 @@ class Streamer {
116
151
  config: this.config
117
152
  };
118
153
  }
154
+ createTransferEvent(op, blockNumber, blockId, prevBlockId, trxId, blockTime) {
155
+ const rawAmount = String(op?.amount || '');
156
+ let amount = '';
157
+ let asset = '';
158
+ try {
159
+ const parsed = utils_2.Utils.parseAssetAmount(rawAmount);
160
+ amount = parsed.amount;
161
+ asset = parsed.asset;
162
+ }
163
+ catch (error) {
164
+ amount = '';
165
+ asset = '';
166
+ }
167
+ return {
168
+ op,
169
+ transfer: {
170
+ from: op?.from || '',
171
+ to: op?.to || '',
172
+ rawAmount,
173
+ amount,
174
+ asset,
175
+ memo: op?.memo
176
+ },
177
+ block: {
178
+ number: blockNumber,
179
+ id: blockId,
180
+ previousId: prevBlockId,
181
+ time: blockTime
182
+ },
183
+ transaction: {
184
+ id: trxId
185
+ }
186
+ };
187
+ }
188
+ normalizeTransferPreviewInput(transfer) {
189
+ if (typeof transfer === 'object' && transfer !== null && 'transfer' in transfer && 'transaction' in transfer && 'block' in transfer) {
190
+ return transfer;
191
+ }
192
+ const op = typeof transfer === 'string'
193
+ ? { amount: transfer }
194
+ : transfer || {};
195
+ return this.createTransferEvent(op, 0, '', '', '', new Date(0));
196
+ }
197
+ resolveAccount(account) {
198
+ const resolved = account || this.username || this.config.USERNAME;
199
+ if (!resolved) {
200
+ throw new Error('Account is required');
201
+ }
202
+ return resolved;
203
+ }
204
+ async dedupeHas(store, key) {
205
+ return Boolean(await store.has(key));
206
+ }
207
+ async dedupeAdd(store, key) {
208
+ await store.add(key);
209
+ }
210
+ normalizeDedupeStore(dedupeStore) {
211
+ const store = dedupeStore || new Set();
212
+ if (store instanceof Set) {
213
+ return {
214
+ has: (key) => store.has(key),
215
+ add: (key) => {
216
+ store.add(key);
217
+ }
218
+ };
219
+ }
220
+ return store;
221
+ }
222
+ isZeroAmountFlowError(error) {
223
+ return error instanceof Error
224
+ && (error.message === 'Burn amount must be greater than zero' || error.message === 'Route amount must be greater than zero');
225
+ }
226
+ resolveFlowBasisPoints(options, context, allowUnspecified = true) {
227
+ const percentage = options.percentage ?? options.percent;
228
+ const basisPoints = options.basisPoints;
229
+ if (percentage !== undefined && basisPoints !== undefined) {
230
+ throw new Error(`${context} accepts either percentage or basisPoints, not both`);
231
+ }
232
+ if (percentage === undefined && basisPoints === undefined) {
233
+ if (allowUnspecified) {
234
+ return null;
235
+ }
236
+ throw new Error(`${context} requires percentage or basisPoints`);
237
+ }
238
+ if (basisPoints !== undefined) {
239
+ if (!Number.isInteger(basisPoints) || basisPoints < 0 || basisPoints > 10000) {
240
+ throw new Error(`${context} basisPoints must be an integer between 0 and 10000`);
241
+ }
242
+ return basisPoints;
243
+ }
244
+ const percentageValue = new bignumber_js_1.default(percentage);
245
+ if (percentageValue.isNaN() || !percentageValue.isFinite()) {
246
+ throw new Error(`${context} percentage must be a valid number`);
247
+ }
248
+ if (percentageValue.lt(0) || percentageValue.gt(100)) {
249
+ throw new Error(`${context} percentage must be between 0 and 100`);
250
+ }
251
+ const bps = percentageValue.multipliedBy(100);
252
+ if (!bps.isInteger()) {
253
+ throw new Error(`${context} percentage supports up to 2 decimal places; use basisPoints for finer control`);
254
+ }
255
+ return bps.toNumber();
256
+ }
257
+ resolveFlowMode(route) {
258
+ return route.mode === 'onTop' ? 'onTop' : 'base';
259
+ }
260
+ normalizeBaseRouteAllocations(routes) {
261
+ const allocations = routes.map((route, index) => this.resolveFlowBasisPoints(route, `Route ${index + 1}`, true));
262
+ const unspecified = allocations.filter((allocation) => allocation === null).length;
263
+ const explicitTotal = allocations.reduce((sum, allocation) => sum + (allocation || 0), 0);
264
+ if (unspecified > 1) {
265
+ throw new Error('Only one flow route can omit percentage or basisPoints');
266
+ }
267
+ if (explicitTotal > 10000) {
268
+ throw new Error('Flow route allocations cannot exceed 10000 basis points');
269
+ }
270
+ if (unspecified === 0 && explicitTotal !== 10000) {
271
+ throw new Error('Flow route allocations must total 100%');
272
+ }
273
+ if (unspecified === 1) {
274
+ const remainder = 10000 - explicitTotal;
275
+ return allocations.map((allocation) => allocation === null ? remainder : allocation);
276
+ }
277
+ return allocations;
278
+ }
279
+ normalizeOnTopRouteAllocations(routes) {
280
+ const allocations = routes.map((route, index) => this.resolveFlowBasisPoints(route, `On-top route ${index + 1}`, false));
281
+ const explicitTotal = allocations.reduce((sum, allocation) => sum + allocation, 0);
282
+ if (explicitTotal <= 0) {
283
+ throw new Error('On-top flow route allocations must be greater than zero');
284
+ }
285
+ return allocations;
286
+ }
287
+ resolvePlannedRouteMemo(event, route, index, defaultMemo) {
288
+ if (typeof route.memo === 'function') {
289
+ return route.memo(event);
290
+ }
291
+ if (typeof route.memo === 'string') {
292
+ return route.memo;
293
+ }
294
+ if (typeof defaultMemo === 'function') {
295
+ return defaultMemo(event, route, index);
296
+ }
297
+ return defaultMemo || '';
298
+ }
299
+ resolveGroupSplitStrategy(route) {
300
+ if (route.split) {
301
+ return route.split;
302
+ }
303
+ return route.group.some((recipient) => recipient.weight !== undefined) ? 'weighted' : 'equal';
304
+ }
305
+ resolveGroupWeights(route, routeIndex) {
306
+ if (!Array.isArray(route.group) || route.group.length === 0) {
307
+ throw new Error(`Route ${routeIndex + 1} group must include at least one account`);
308
+ }
309
+ if (this.resolveGroupSplitStrategy(route) === 'equal') {
310
+ return route.group.map(() => 1);
311
+ }
312
+ return route.group.map((recipient, groupIndex) => {
313
+ if (recipient.weight === undefined) {
314
+ return 1;
315
+ }
316
+ const weight = new bignumber_js_1.default(recipient.weight);
317
+ if (weight.isNaN() || !weight.isFinite() || weight.lte(0)) {
318
+ throw new Error(`Route ${routeIndex + 1} recipient ${groupIndex + 1} weight must be greater than zero`);
319
+ }
320
+ return weight.toString();
321
+ });
322
+ }
323
+ expandPlannedRoute(event, route, routeIndex, amount, asset, memo) {
324
+ const mode = this.resolveFlowMode(route);
325
+ if (route.type === 'burn') {
326
+ return [{
327
+ type: 'burn',
328
+ mode,
329
+ amount,
330
+ asset,
331
+ memo,
332
+ routeIndex
333
+ }];
334
+ }
335
+ if ('group' in route) {
336
+ const weights = this.resolveGroupWeights(route, routeIndex);
337
+ const amounts = utils_2.Utils.splitAmountByWeights(amount, weights);
338
+ return route.group.map((recipient, groupIndex) => {
339
+ const destination = typeof recipient.account === 'function' ? recipient.account(event) : recipient.account;
340
+ if (typeof destination !== 'string' || destination.trim().length === 0) {
341
+ throw new Error(`Route ${routeIndex + 1} recipient ${groupIndex + 1} destination account is required`);
342
+ }
343
+ return {
344
+ type: 'transfer',
345
+ mode,
346
+ amount: amounts[groupIndex],
347
+ asset,
348
+ memo,
349
+ to: destination.trim(),
350
+ routeIndex,
351
+ groupIndex
352
+ };
353
+ });
354
+ }
355
+ const destination = typeof route.to === 'function' ? route.to(event) : route.to;
356
+ if (typeof destination !== 'string' || destination.trim().length === 0) {
357
+ throw new Error(`Route ${routeIndex + 1} destination account is required`);
358
+ }
359
+ return [{
360
+ type: 'transfer',
361
+ mode,
362
+ amount,
363
+ asset,
364
+ memo,
365
+ to: destination.trim(),
366
+ routeIndex
367
+ }];
368
+ }
369
+ sumPlannedRouteAmounts(plan) {
370
+ return plan.reduce((sum, route) => sum.plus(route.amount), new bignumber_js_1.default(0)).toFixed(3);
371
+ }
372
+ buildPlannedIncomingTransferRoutes(event, routes, allowedSymbols, defaultMemo) {
373
+ if (!Array.isArray(routes) || routes.length === 0) {
374
+ throw new Error('At least one flow route is required');
375
+ }
376
+ const parsed = utils_2.Utils.parseAssetAmount(event.transfer.rawAmount);
377
+ if (Array.isArray(allowedSymbols) && allowedSymbols.length > 0 && !allowedSymbols.includes(parsed.asset)) {
378
+ throw new Error(`Asset '${parsed.asset}' is not allowed for this flow`);
379
+ }
380
+ const baseRoutes = routes.filter((route) => this.resolveFlowMode(route) === 'base');
381
+ const onTopRoutes = routes.filter((route) => this.resolveFlowMode(route) === 'onTop');
382
+ if (baseRoutes.length === 0) {
383
+ throw new Error('At least one base flow route is required');
384
+ }
385
+ const baseAllocations = this.normalizeBaseRouteAllocations(baseRoutes);
386
+ let baseAmount = parsed.amount;
387
+ let onTopRouteAmounts = [];
388
+ if (onTopRoutes.length > 0) {
389
+ const onTopAllocations = this.normalizeOnTopRouteAllocations(onTopRoutes);
390
+ const pools = utils_2.Utils.splitAmountByWeights(parsed.value, [10000, ...onTopAllocations]);
391
+ baseAmount = pools[0];
392
+ onTopRouteAmounts = pools.slice(1);
393
+ }
394
+ const baseRouteAmounts = utils_2.Utils.splitAmountByBasisPoints(baseAmount, baseAllocations);
395
+ const routesPlan = routes.flatMap((route, index) => {
396
+ const memo = this.resolvePlannedRouteMemo(event, route, index, defaultMemo);
397
+ const amount = this.resolveFlowMode(route) === 'base'
398
+ ? baseRouteAmounts.shift()
399
+ : onTopRouteAmounts.shift();
400
+ return this.expandPlannedRoute(event, route, index, amount, parsed.asset, memo);
401
+ });
402
+ const onTopPlannedRoutes = routesPlan.filter((route) => route.mode === 'onTop');
403
+ return {
404
+ incomingAmount: utils_2.Utils.formatAssetAmount(parsed.value, parsed.asset),
405
+ asset: parsed.asset,
406
+ baseAmount,
407
+ onTopAmount: this.sumPlannedRouteAmounts(onTopPlannedRoutes),
408
+ routes: routesPlan
409
+ };
410
+ }
411
+ async executePlannedFlowRoutes(from, plan, ignoreZeroAmount) {
412
+ const results = [];
413
+ for (const route of plan) {
414
+ if (route.amount === '0.000') {
415
+ if (ignoreZeroAmount) {
416
+ continue;
417
+ }
418
+ throw new Error('Route amount must be greater than zero');
419
+ }
420
+ if (route.type === 'burn') {
421
+ results.push(await this.burnHiveTokens(from, route.amount, route.asset, route.memo));
422
+ continue;
423
+ }
424
+ results.push(await this.transferHiveTokens(from, route.to, route.amount, route.asset, route.memo));
425
+ }
426
+ return results;
427
+ }
428
+ calculateSingleFlowAmount(transfer, basisPoints, allowedSymbols, errorContext) {
429
+ const rawAmount = typeof transfer === 'string' ? transfer : transfer?.amount || '';
430
+ const parsed = utils_2.Utils.parseAssetAmount(rawAmount);
431
+ if (Array.isArray(allowedSymbols) && allowedSymbols.length > 0 && !allowedSymbols.includes(parsed.asset)) {
432
+ throw new Error(`Asset '${parsed.asset}' is not allowed for ${errorContext}`);
433
+ }
434
+ const amount = basisPoints === null ? parsed.amount : utils_2.Utils.calculateBasisPointsAmount(parsed.value, basisPoints);
435
+ if (amount === '0.000') {
436
+ throw new Error('Route amount must be greater than zero');
437
+ }
438
+ return {
439
+ amount,
440
+ asset: parsed.asset
441
+ };
442
+ }
119
443
  async initializeContract(contract) {
120
444
  if (this.initializedContracts.has(contract.name)) {
121
445
  return;
@@ -364,11 +688,16 @@ class Streamer {
364
688
  * @param config
365
689
  */
366
690
  setConfig(config) {
367
- const normalized = (0, config_1.normalizeConfigInput)(config);
368
- const shouldRecreateClient = normalized.API_NODES !== undefined;
369
- const shouldRecreateHiveEngine = normalized.HIVE_ENGINE_API !== undefined;
370
- const shouldSyncApiServer = normalized.API_ENABLED !== undefined || normalized.API_PORT !== undefined;
371
- Object.assign(this.config, normalized);
691
+ const normalizedInput = (0, config_1.normalizeConfigInput)(config);
692
+ const nextConfig = (0, config_1.createConfig)({
693
+ ...this.config,
694
+ ...normalizedInput,
695
+ env: config.env,
696
+ });
697
+ const shouldRecreateClient = JSON.stringify(this.config.API_NODES) !== JSON.stringify(nextConfig.API_NODES);
698
+ const shouldRecreateHiveEngine = this.config.HIVE_ENGINE_API !== nextConfig.HIVE_ENGINE_API;
699
+ const shouldSyncApiServer = this.config.API_ENABLED !== nextConfig.API_ENABLED || this.config.API_PORT !== nextConfig.API_PORT;
700
+ Object.assign(this.config, nextConfig);
372
701
  // Set keys and username incase they have changed
373
702
  this.username = this.config.USERNAME;
374
703
  this.postingKey = this.config.POSTING_KEY;
@@ -639,13 +968,23 @@ class Streamer {
639
968
  if (operationType === 'transfer') {
640
969
  const sender = operationData?.from;
641
970
  const rawAmount = operationData?.amount;
642
- const amountParts = typeof rawAmount === 'string' ? rawAmount.split(' ') : [];
971
+ let amount = '';
972
+ let asset = '';
973
+ try {
974
+ const parsedAmount = utils_2.Utils.parseAssetAmount(String(rawAmount || ''));
975
+ amount = parsedAmount.amount;
976
+ asset = parsedAmount.asset;
977
+ }
978
+ catch (error) {
979
+ amount = '';
980
+ asset = '';
981
+ }
643
982
  const transferInfo = {
644
983
  from: sender,
645
984
  to: operationData?.to,
646
985
  rawAmount: rawAmount || '',
647
- amount: amountParts[0] || '',
648
- asset: amountParts[1] || '',
986
+ amount,
987
+ asset,
649
988
  memo: operationData?.memo
650
989
  };
651
990
  const json = utils_2.Utils.jsonParse(operationData.memo);
@@ -668,11 +1007,9 @@ class Streamer {
668
1007
  });
669
1008
  await this.dispatchContractAction(payload, context);
670
1009
  }
671
- this.transferSubscriptions.forEach(sub => {
672
- if (sub.account === operationData.to) {
673
- sub.callback(operationData, blockNumber, blockId, prevBlockId, trxId, blockTime);
674
- }
675
- });
1010
+ await Promise.all(this.transferSubscriptions
1011
+ .filter((sub) => sub.account === operationData.to)
1012
+ .map((sub) => Promise.resolve(sub.callback(operationData, blockNumber, blockId, prevBlockId, trxId, blockTime))));
676
1013
  }
677
1014
  // This is a custom JSON operation
678
1015
  if (operationType === 'custom_json') {
@@ -1029,6 +1366,229 @@ class Streamer {
1029
1366
  transferHiveTokens(from, to, amount, symbol, memo = '') {
1030
1367
  return utils_2.Utils.transferHiveTokens(this.client, this.config, from, to, amount, symbol, memo);
1031
1368
  }
1369
+ burnHiveTokens(from, amount, symbol, memo = '') {
1370
+ return utils_2.Utils.burnHiveTokens(this.client, this.config, from, amount, symbol, memo);
1371
+ }
1372
+ burnTransferPortion(from, transfer, basisPoints, memo = '', allowedSymbols = ['HIVE', 'HBD']) {
1373
+ const rawAmount = typeof transfer === 'string' ? transfer : transfer?.amount || '';
1374
+ const parsed = utils_2.Utils.parseAssetAmount(rawAmount);
1375
+ if (Array.isArray(allowedSymbols) && allowedSymbols.length > 0 && !allowedSymbols.includes(parsed.asset)) {
1376
+ throw new Error(`Asset '${parsed.asset}' is not allowed for burn`);
1377
+ }
1378
+ const burnAmount = utils_2.Utils.calculateBasisPointsAmount(parsed.value, basisPoints);
1379
+ if (burnAmount === '0.000') {
1380
+ throw new Error('Burn amount must be greater than zero');
1381
+ }
1382
+ return this.burnHiveTokens(from, burnAmount, parsed.asset, memo);
1383
+ }
1384
+ burnTransferPercentage(from, transfer, percentage, memo = '', allowedSymbols = ['HIVE', 'HBD']) {
1385
+ const rawAmount = typeof transfer === 'string' ? transfer : transfer?.amount || '';
1386
+ const parsed = utils_2.Utils.parseAssetAmount(rawAmount);
1387
+ if (Array.isArray(allowedSymbols) && allowedSymbols.length > 0 && !allowedSymbols.includes(parsed.asset)) {
1388
+ throw new Error(`Asset '${parsed.asset}' is not allowed for burn`);
1389
+ }
1390
+ const burnAmount = utils_2.Utils.calculatePercentageAmount(parsed.value, percentage);
1391
+ if (burnAmount === '0.000') {
1392
+ throw new Error('Burn amount must be greater than zero');
1393
+ }
1394
+ return this.burnHiveTokens(from, burnAmount, parsed.asset, memo);
1395
+ }
1396
+ autoBurnIncomingTransfers(options = {}) {
1397
+ const basisPoints = this.resolveFlowBasisPoints(options, 'autoBurnIncomingTransfers', false);
1398
+ const account = this.resolveAccount(options.account);
1399
+ const normalizedStore = this.normalizeDedupeStore(options.dedupeStore);
1400
+ const allowedSymbols = options.allowedSymbols || ['HIVE', 'HBD'];
1401
+ const ignoreZeroAmount = options.ignoreZeroAmount !== false;
1402
+ const callback = async (op, blockNumber, blockId, prevBlockId, trxId, blockTime) => {
1403
+ const event = this.createTransferEvent(op, blockNumber, blockId, prevBlockId, trxId, blockTime);
1404
+ try {
1405
+ if (await this.dedupeHas(normalizedStore, trxId)) {
1406
+ return;
1407
+ }
1408
+ const memo = typeof options.memo === 'function' ? options.memo(event) : options.memo || '';
1409
+ const amount = this.calculateSingleFlowAmount(op, basisPoints, allowedSymbols, 'burn');
1410
+ const result = await this.burnHiveTokens(account, amount.amount, amount.asset, memo);
1411
+ await this.dedupeAdd(normalizedStore, trxId);
1412
+ if (options.onBurned) {
1413
+ await options.onBurned(result, event);
1414
+ }
1415
+ }
1416
+ catch (error) {
1417
+ if (ignoreZeroAmount && this.isZeroAmountFlowError(error)) {
1418
+ return;
1419
+ }
1420
+ if (options.onError) {
1421
+ await options.onError(error, event);
1422
+ return;
1423
+ }
1424
+ throw error;
1425
+ }
1426
+ };
1427
+ this.onTransfer(account, callback);
1428
+ return {
1429
+ account,
1430
+ stop: () => {
1431
+ this.removeTransferSubscription(account, callback);
1432
+ }
1433
+ };
1434
+ }
1435
+ autoForwardIncomingTransfers(options) {
1436
+ if (!options || typeof options.to !== 'string' || options.to.trim().length === 0) {
1437
+ throw new Error('autoForwardIncomingTransfers requires a destination account');
1438
+ }
1439
+ const account = this.resolveAccount(options.account);
1440
+ const normalizedStore = this.normalizeDedupeStore(options.dedupeStore);
1441
+ const allowedSymbols = options.allowedSymbols || ['HIVE', 'HBD'];
1442
+ const ignoreZeroAmount = options.ignoreZeroAmount !== false;
1443
+ const basisPoints = this.resolveFlowBasisPoints(options, 'autoForwardIncomingTransfers', true);
1444
+ const destination = options.to.trim();
1445
+ const callback = async (op, blockNumber, blockId, prevBlockId, trxId, blockTime) => {
1446
+ const event = this.createTransferEvent(op, blockNumber, blockId, prevBlockId, trxId, blockTime);
1447
+ try {
1448
+ if (await this.dedupeHas(normalizedStore, trxId)) {
1449
+ return;
1450
+ }
1451
+ const memo = typeof options.memo === 'function' ? options.memo(event) : options.memo || '';
1452
+ const amount = this.calculateSingleFlowAmount(op, basisPoints, allowedSymbols, 'forward');
1453
+ const result = await this.transferHiveTokens(account, destination, amount.amount, amount.asset, memo);
1454
+ await this.dedupeAdd(normalizedStore, trxId);
1455
+ if (options.onForwarded) {
1456
+ await options.onForwarded(result, event);
1457
+ }
1458
+ }
1459
+ catch (error) {
1460
+ if (ignoreZeroAmount && this.isZeroAmountFlowError(error)) {
1461
+ return;
1462
+ }
1463
+ if (options.onError) {
1464
+ await options.onError(error, event);
1465
+ return;
1466
+ }
1467
+ throw error;
1468
+ }
1469
+ };
1470
+ this.onTransfer(account, callback);
1471
+ return {
1472
+ account,
1473
+ stop: () => {
1474
+ this.removeTransferSubscription(account, callback);
1475
+ }
1476
+ };
1477
+ }
1478
+ autoRefundIncomingTransfers(options = {}) {
1479
+ const account = this.resolveAccount(options.account);
1480
+ const normalizedStore = this.normalizeDedupeStore(options.dedupeStore);
1481
+ const allowedSymbols = options.allowedSymbols || ['HIVE', 'HBD'];
1482
+ const ignoreZeroAmount = options.ignoreZeroAmount !== false;
1483
+ const basisPoints = this.resolveFlowBasisPoints(options, 'autoRefundIncomingTransfers', true);
1484
+ const callback = async (op, blockNumber, blockId, prevBlockId, trxId, blockTime) => {
1485
+ const event = this.createTransferEvent(op, blockNumber, blockId, prevBlockId, trxId, blockTime);
1486
+ try {
1487
+ if (await this.dedupeHas(normalizedStore, trxId)) {
1488
+ return;
1489
+ }
1490
+ const memo = typeof options.memo === 'function' ? options.memo(event) : options.memo || '';
1491
+ const amount = this.calculateSingleFlowAmount(op, basisPoints, allowedSymbols, 'refund');
1492
+ const result = await this.transferHiveTokens(account, event.transfer.from, amount.amount, amount.asset, memo);
1493
+ await this.dedupeAdd(normalizedStore, trxId);
1494
+ if (options.onRefunded) {
1495
+ await options.onRefunded(result, event);
1496
+ }
1497
+ }
1498
+ catch (error) {
1499
+ if (ignoreZeroAmount && this.isZeroAmountFlowError(error)) {
1500
+ return;
1501
+ }
1502
+ if (options.onError) {
1503
+ await options.onError(error, event);
1504
+ return;
1505
+ }
1506
+ throw error;
1507
+ }
1508
+ };
1509
+ this.onTransfer(account, callback);
1510
+ return {
1511
+ account,
1512
+ stop: () => {
1513
+ this.removeTransferSubscription(account, callback);
1514
+ }
1515
+ };
1516
+ }
1517
+ autoSplitIncomingTransfers(options) {
1518
+ if (!options || !Array.isArray(options.recipients) || options.recipients.length === 0) {
1519
+ throw new Error('autoSplitIncomingTransfers requires at least one recipient');
1520
+ }
1521
+ const defaultMemo = options.memo;
1522
+ return this.autoRouteIncomingTransfers({
1523
+ account: options.account,
1524
+ routes: options.recipients.map((recipient, index) => ({
1525
+ to: recipient.account,
1526
+ percentage: recipient.percentage,
1527
+ percent: recipient.percent,
1528
+ basisPoints: recipient.basisPoints,
1529
+ memo: recipient.memo || (typeof defaultMemo === 'function'
1530
+ ? (event) => defaultMemo(event, { account: recipient.account }, index)
1531
+ : defaultMemo)
1532
+ })),
1533
+ allowedSymbols: options.allowedSymbols,
1534
+ dedupeStore: options.dedupeStore,
1535
+ ignoreZeroAmount: options.ignoreZeroAmount,
1536
+ onRouted: async (results, event, plan) => {
1537
+ if (options.onSplit) {
1538
+ await options.onSplit(results, event, plan);
1539
+ }
1540
+ },
1541
+ onError: options.onError
1542
+ });
1543
+ }
1544
+ planIncomingTransferRoutes(transfer, options) {
1545
+ if (!options || !Array.isArray(options.routes) || options.routes.length === 0) {
1546
+ throw new Error('planIncomingTransferRoutes requires at least one route');
1547
+ }
1548
+ const event = this.normalizeTransferPreviewInput(transfer);
1549
+ const allowedSymbols = options.allowedSymbols || ['HIVE', 'HBD'];
1550
+ return this.buildPlannedIncomingTransferRoutes(event, options.routes, allowedSymbols, options.memo);
1551
+ }
1552
+ autoRouteIncomingTransfers(options) {
1553
+ if (!options || !Array.isArray(options.routes) || options.routes.length === 0) {
1554
+ throw new Error('autoRouteIncomingTransfers requires at least one route');
1555
+ }
1556
+ const account = this.resolveAccount(options.account);
1557
+ const normalizedStore = this.normalizeDedupeStore(options.dedupeStore);
1558
+ const allowedSymbols = options.allowedSymbols || ['HIVE', 'HBD'];
1559
+ const ignoreZeroAmount = options.ignoreZeroAmount !== false;
1560
+ const callback = async (op, blockNumber, blockId, prevBlockId, trxId, blockTime) => {
1561
+ const event = this.createTransferEvent(op, blockNumber, blockId, prevBlockId, trxId, blockTime);
1562
+ try {
1563
+ if (await this.dedupeHas(normalizedStore, trxId)) {
1564
+ return;
1565
+ }
1566
+ const plan = this.buildPlannedIncomingTransferRoutes(event, options.routes, allowedSymbols, options.memo);
1567
+ const results = await this.executePlannedFlowRoutes(account, plan.routes, ignoreZeroAmount);
1568
+ await this.dedupeAdd(normalizedStore, trxId);
1569
+ if (options.onRouted) {
1570
+ await options.onRouted(results, event, plan.routes);
1571
+ }
1572
+ }
1573
+ catch (error) {
1574
+ if (ignoreZeroAmount && this.isZeroAmountFlowError(error)) {
1575
+ return;
1576
+ }
1577
+ if (options.onError) {
1578
+ await options.onError(error, event);
1579
+ return;
1580
+ }
1581
+ throw error;
1582
+ }
1583
+ };
1584
+ this.onTransfer(account, callback);
1585
+ return {
1586
+ account,
1587
+ stop: () => {
1588
+ this.removeTransferSubscription(account, callback);
1589
+ }
1590
+ };
1591
+ }
1032
1592
  transferHiveTokensMultiple(from, accounts = [], amount = '0', symbol, memo = '') {
1033
1593
  return utils_2.Utils.transferHiveTokensMultiple(this.client, this.config, from, accounts, amount, symbol, memo);
1034
1594
  }
@@ -1071,6 +1631,9 @@ class Streamer {
1071
1631
  transferHiveEngineTokens(from, to, symbol, quantity, memo = '') {
1072
1632
  return utils_2.Utils.transferHiveEngineTokens(this.client, this.config, from, to, quantity, symbol, memo);
1073
1633
  }
1634
+ burnHiveEngineTokens(from, symbol, quantity, memo = '') {
1635
+ return utils_2.Utils.burnHiveEngineTokens(this.client, this.config, from, symbol, quantity, memo);
1636
+ }
1074
1637
  transferHiveEngineTokensMultiple(from, accounts = [], symbol, memo = '', amount = '0') {
1075
1638
  return utils_2.Utils.transferHiveEngineTokensMultiple(this.client, this.config, from, accounts, symbol, memo, amount);
1076
1639
  }
@@ -1173,8 +1736,16 @@ class Streamer {
1173
1736
  }
1174
1737
  }
1175
1738
  // Add method to remove specific subscriptions
1176
- removeTransferSubscription(account) {
1177
- this.transferSubscriptions = this.transferSubscriptions.filter(sub => sub.account !== account);
1739
+ removeTransferSubscription(account, callback) {
1740
+ this.transferSubscriptions = this.transferSubscriptions.filter((sub) => {
1741
+ if (sub.account !== account) {
1742
+ return true;
1743
+ }
1744
+ if (!callback) {
1745
+ return false;
1746
+ }
1747
+ return sub.callback !== callback;
1748
+ });
1178
1749
  }
1179
1750
  removeCustomJsonIdSubscription(id) {
1180
1751
  this.customJsonIdSubscriptions = this.customJsonIdSubscriptions.filter(sub => sub.id !== id);