hive-stream 3.0.2 → 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/DOCUMENTATION.md +50 -2
- package/README.md +282 -4
- package/dist/adapters/base.adapter.d.ts +5 -0
- package/dist/adapters/base.adapter.js +9 -0
- package/dist/adapters/base.adapter.js.map +1 -1
- package/dist/adapters/mongodb.adapter.d.ts +6 -6
- package/dist/adapters/mongodb.adapter.js +36 -21
- package/dist/adapters/mongodb.adapter.js.map +1 -1
- package/dist/adapters/postgresql.adapter.d.ts +7 -0
- package/dist/adapters/postgresql.adapter.js +46 -19
- package/dist/adapters/postgresql.adapter.js.map +1 -1
- package/dist/adapters/sqlite.adapter.d.ts +4 -0
- package/dist/adapters/sqlite.adapter.js +10 -0
- package/dist/adapters/sqlite.adapter.js.map +1 -1
- package/dist/api.d.ts +13 -3
- package/dist/api.js +96 -62
- package/dist/api.js.map +1 -1
- package/dist/builders.d.ts +176 -0
- package/dist/builders.js +727 -0
- package/dist/builders.js.map +1 -0
- package/dist/config.d.ts +16 -1
- package/dist/config.js +95 -3
- package/dist/config.js.map +1 -1
- package/dist/contracts/auctionhouse.contract.d.ts +4 -0
- package/dist/contracts/auctionhouse.contract.js +234 -0
- package/dist/contracts/auctionhouse.contract.js.map +1 -0
- package/dist/contracts/booking.contract.d.ts +4 -0
- package/dist/contracts/booking.contract.js +225 -0
- package/dist/contracts/booking.contract.js.map +1 -0
- package/dist/contracts/bountyboard.contract.d.ts +4 -0
- package/dist/contracts/bountyboard.contract.js +233 -0
- package/dist/contracts/bountyboard.contract.js.map +1 -0
- package/dist/contracts/bundlemarketplace.contract.d.ts +4 -0
- package/dist/contracts/bundlemarketplace.contract.js +195 -0
- package/dist/contracts/bundlemarketplace.contract.js.map +1 -0
- package/dist/contracts/charitymatch.contract.d.ts +4 -0
- package/dist/contracts/charitymatch.contract.js +172 -0
- package/dist/contracts/charitymatch.contract.js.map +1 -0
- package/dist/contracts/coinflip.contract.js +25 -22
- package/dist/contracts/coinflip.contract.js.map +1 -1
- package/dist/contracts/crowdfund.contract.d.ts +4 -0
- package/dist/contracts/crowdfund.contract.js +290 -0
- package/dist/contracts/crowdfund.contract.js.map +1 -0
- package/dist/contracts/dcabot.contract.d.ts +4 -0
- package/dist/contracts/dcabot.contract.js +217 -0
- package/dist/contracts/dcabot.contract.js.map +1 -0
- package/dist/contracts/dice.contract.js +25 -22
- package/dist/contracts/dice.contract.js.map +1 -1
- package/dist/contracts/domainregistry.contract.d.ts +4 -0
- package/dist/contracts/domainregistry.contract.js +232 -0
- package/dist/contracts/domainregistry.contract.js.map +1 -0
- package/dist/contracts/exchange.contract.js +209 -168
- package/dist/contracts/exchange.contract.js.map +1 -1
- package/dist/contracts/fanclub.contract.d.ts +4 -0
- package/dist/contracts/fanclub.contract.js +193 -0
- package/dist/contracts/fanclub.contract.js.map +1 -0
- package/dist/contracts/giftcard.contract.d.ts +4 -0
- package/dist/contracts/giftcard.contract.js +158 -0
- package/dist/contracts/giftcard.contract.js.map +1 -0
- package/dist/contracts/grantrounds.contract.d.ts +4 -0
- package/dist/contracts/grantrounds.contract.js +265 -0
- package/dist/contracts/grantrounds.contract.js.map +1 -0
- package/dist/contracts/groupbuy.contract.d.ts +4 -0
- package/dist/contracts/groupbuy.contract.js +198 -0
- package/dist/contracts/groupbuy.contract.js.map +1 -0
- package/dist/contracts/helpers.d.ts +66 -0
- package/dist/contracts/helpers.js +166 -0
- package/dist/contracts/helpers.js.map +1 -0
- package/dist/contracts/insurancepool.contract.d.ts +4 -0
- package/dist/contracts/insurancepool.contract.js +281 -0
- package/dist/contracts/insurancepool.contract.js.map +1 -0
- package/dist/contracts/invoice.contract.d.ts +4 -0
- package/dist/contracts/invoice.contract.js +193 -0
- package/dist/contracts/invoice.contract.js.map +1 -0
- package/dist/contracts/launchpad.contract.d.ts +4 -0
- package/dist/contracts/launchpad.contract.js +225 -0
- package/dist/contracts/launchpad.contract.js.map +1 -0
- package/dist/contracts/lotto.contract.js +53 -37
- package/dist/contracts/lotto.contract.js.map +1 -1
- package/dist/contracts/multisigtreasury.contract.d.ts +4 -0
- package/dist/contracts/multisigtreasury.contract.js +245 -0
- package/dist/contracts/multisigtreasury.contract.js.map +1 -0
- package/dist/contracts/nft.contract.d.ts +1 -0
- package/dist/contracts/nft.contract.js +234 -195
- package/dist/contracts/nft.contract.js.map +1 -1
- package/dist/contracts/oraclebounty.contract.d.ts +4 -0
- package/dist/contracts/oraclebounty.contract.js +250 -0
- package/dist/contracts/oraclebounty.contract.js.map +1 -0
- package/dist/contracts/payroll.contract.d.ts +4 -0
- package/dist/contracts/payroll.contract.js +232 -0
- package/dist/contracts/payroll.contract.js.map +1 -0
- package/dist/contracts/paywall.contract.d.ts +4 -0
- package/dist/contracts/paywall.contract.js +185 -0
- package/dist/contracts/paywall.contract.js.map +1 -0
- package/dist/contracts/poll.contract.js +2 -0
- package/dist/contracts/poll.contract.js.map +1 -1
- package/dist/contracts/predictionmarket.contract.d.ts +4 -0
- package/dist/contracts/predictionmarket.contract.js +213 -0
- package/dist/contracts/predictionmarket.contract.js.map +1 -0
- package/dist/contracts/proposaltimelock.contract.d.ts +4 -0
- package/dist/contracts/proposaltimelock.contract.js +250 -0
- package/dist/contracts/proposaltimelock.contract.js.map +1 -0
- package/dist/contracts/questpass.contract.d.ts +4 -0
- package/dist/contracts/questpass.contract.js +214 -0
- package/dist/contracts/questpass.contract.js.map +1 -0
- package/dist/contracts/referral.contract.d.ts +4 -0
- package/dist/contracts/referral.contract.js +238 -0
- package/dist/contracts/referral.contract.js.map +1 -0
- package/dist/contracts/rental.contract.d.ts +4 -0
- package/dist/contracts/rental.contract.js +221 -0
- package/dist/contracts/rental.contract.js.map +1 -0
- package/dist/contracts/revenuesplit.contract.d.ts +4 -0
- package/dist/contracts/revenuesplit.contract.js +211 -0
- package/dist/contracts/revenuesplit.contract.js.map +1 -0
- package/dist/contracts/rps.contract.js +48 -20
- package/dist/contracts/rps.contract.js.map +1 -1
- package/dist/contracts/savings.contract.d.ts +4 -0
- package/dist/contracts/savings.contract.js +208 -0
- package/dist/contracts/savings.contract.js.map +1 -0
- package/dist/contracts/subscription.contract.d.ts +4 -0
- package/dist/contracts/subscription.contract.js +241 -0
- package/dist/contracts/subscription.contract.js.map +1 -0
- package/dist/contracts/sweepstakes.contract.d.ts +4 -0
- package/dist/contracts/sweepstakes.contract.js +209 -0
- package/dist/contracts/sweepstakes.contract.js.map +1 -0
- package/dist/contracts/ticketing.contract.d.ts +4 -0
- package/dist/contracts/ticketing.contract.js +185 -0
- package/dist/contracts/ticketing.contract.js.map +1 -0
- package/dist/contracts/tipjar.contract.js +2 -0
- package/dist/contracts/tipjar.contract.js.map +1 -1
- package/dist/contracts/token.contract.js +135 -125
- package/dist/contracts/token.contract.js.map +1 -1
- package/dist/index.d.ts +40 -0
- package/dist/index.js +72 -1
- package/dist/index.js.map +1 -1
- package/dist/metadata.d.ts +20 -0
- package/dist/metadata.js +320 -1
- package/dist/metadata.js.map +1 -1
- package/dist/providers/block-provider.d.ts +22 -0
- package/dist/providers/block-provider.js +3 -0
- package/dist/providers/block-provider.js.map +1 -0
- package/dist/providers/haf-client.d.ts +30 -0
- package/dist/providers/haf-client.js +119 -0
- package/dist/providers/haf-client.js.map +1 -0
- package/dist/providers/haf-provider.d.ts +49 -0
- package/dist/providers/haf-provider.js +256 -0
- package/dist/providers/haf-provider.js.map +1 -0
- package/dist/providers/hive-provider.d.ts +13 -0
- package/dist/providers/hive-provider.js +25 -0
- package/dist/providers/hive-provider.js.map +1 -0
- package/dist/providers/index.d.ts +4 -0
- package/dist/providers/index.js +21 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/streamer.d.ts +65 -4
- package/dist/streamer.js +768 -72
- package/dist/streamer.js.map +1 -1
- package/dist/types/hive-stream.d.ts +317 -0
- package/dist/utils.d.ts +33 -0
- package/dist/utils.js +198 -2
- package/dist/utils.js.map +1 -1
- package/package.json +16 -1
- package/.claude/settings.local.json +0 -12
- package/.env.example +0 -3
- package/.travis.yml +0 -11
- package/AGENTS.md +0 -35
- package/CLAUDE.md +0 -75
- package/ecosystem.config.js +0 -17
- package/examples/contracts/README.md +0 -8
- package/examples/contracts/exchange.ts +0 -38
- package/examples/contracts/poll.ts +0 -21
- package/examples/contracts/rps.ts +0 -19
- package/examples/contracts/tipjar.ts +0 -19
- package/jest.config.js +0 -9
- package/test-contract-block.md +0 -19
- package/tests/actions.spec.ts +0 -252
- package/tests/adapters/actions-persistence.spec.ts +0 -144
- package/tests/adapters/postgresql.adapter.spec.ts +0 -127
- package/tests/adapters/sqlite.adapter.spec.ts +0 -181
- package/tests/config-input.spec.ts +0 -90
- package/tests/contracts/coinflip.contract.spec.ts +0 -94
- package/tests/contracts/dice.contract.spec.ts +0 -87
- package/tests/contracts/entrants.json +0 -729
- package/tests/contracts/exchange.contract.spec.ts +0 -84
- package/tests/contracts/lotto.contract.spec.ts +0 -59
- package/tests/contracts/nft.contract.spec.ts +0 -948
- package/tests/contracts/token.contract.spec.ts +0 -90
- package/tests/exchanges/coingecko.exchange.spec.ts +0 -169
- package/tests/exchanges/exchange.base.spec.ts +0 -246
- package/tests/helpers/mock-adapter.ts +0 -214
- package/tests/helpers/mock-fetch.ts +0 -165
- package/tests/hive-chain-features.spec.ts +0 -319
- package/tests/hive-rates.spec.ts +0 -443
- package/tests/integration/hive-rates.integration.spec.ts +0 -35
- package/tests/metadata.spec.ts +0 -63
- package/tests/setup.ts +0 -30
- package/tests/streamer-actions.spec.ts +0 -274
- package/tests/streamer.spec.ts +0 -342
- package/tests/types/rates.spec.ts +0 -216
- package/tests/utils.spec.ts +0 -113
- package/tsconfig.build.json +0 -4
- package/tslint.json +0 -21
- package/wallaby.js +0 -26
package/dist/streamer.js
CHANGED
|
@@ -9,8 +9,11 @@ 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");
|
|
16
|
+
const hive_provider_1 = require("./providers/hive-provider");
|
|
14
17
|
const sscjs_1 = __importDefault(require("sscjs"));
|
|
15
18
|
class Streamer {
|
|
16
19
|
customJsonSubscriptions = [];
|
|
@@ -39,8 +42,14 @@ class Streamer {
|
|
|
39
42
|
blockTime;
|
|
40
43
|
latestBlockchainTime;
|
|
41
44
|
disableAllProcessing = false;
|
|
45
|
+
isStarted = false;
|
|
42
46
|
contracts = [];
|
|
47
|
+
blockProvider;
|
|
43
48
|
adapter;
|
|
49
|
+
adapterInitializationPromise = null;
|
|
50
|
+
adapterInitialized = false;
|
|
51
|
+
initializedContracts = new Set();
|
|
52
|
+
apiServer = null;
|
|
44
53
|
actions = [];
|
|
45
54
|
// Performance optimization properties
|
|
46
55
|
lastStateSave = Date.now();
|
|
@@ -66,6 +75,39 @@ class Streamer {
|
|
|
66
75
|
cacheTimeout = 300000; // 5 minutes
|
|
67
76
|
maxCacheSize = 1000;
|
|
68
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
|
+
};
|
|
69
111
|
constructor(userConfig = {}) {
|
|
70
112
|
this.config = (0, config_1.createConfig)(userConfig);
|
|
71
113
|
this.lastBlockNumber = this.config.LAST_BLOCK_NUMBER;
|
|
@@ -74,23 +116,388 @@ class Streamer {
|
|
|
74
116
|
this.activeKey = this.config.ACTIVE_KEY;
|
|
75
117
|
this.hive = new sscjs_1.default(this.config.HIVE_ENGINE_API);
|
|
76
118
|
this.client = new dhive_1.Client(this.config.API_NODES);
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
119
|
+
this.adapter = new sqlite_adapter_1.SqliteAdapter();
|
|
120
|
+
if (userConfig.blockProvider) {
|
|
121
|
+
this.blockProvider = userConfig.blockProvider;
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
this.blockProvider = new hive_provider_1.HiveProvider({ apiNodes: this.config.API_NODES });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
async ensureAdapterReady() {
|
|
128
|
+
if (this.adapterInitialized) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (!this.adapter) {
|
|
132
|
+
throw new Error('No adapter registered');
|
|
133
|
+
}
|
|
134
|
+
if (!this.adapterInitializationPromise) {
|
|
135
|
+
this.adapterInitializationPromise = Promise.resolve(this.adapter.create?.())
|
|
136
|
+
.then(() => {
|
|
137
|
+
this.adapterInitialized = true;
|
|
138
|
+
})
|
|
139
|
+
.catch((error) => {
|
|
140
|
+
this.adapterInitializationPromise = null;
|
|
141
|
+
this.adapterInitialized = false;
|
|
142
|
+
throw error;
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
await this.adapterInitializationPromise;
|
|
146
|
+
}
|
|
147
|
+
getLifecycleContext() {
|
|
148
|
+
return {
|
|
149
|
+
streamer: this,
|
|
150
|
+
adapter: this.adapter,
|
|
151
|
+
config: this.config
|
|
152
|
+
};
|
|
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
|
+
}
|
|
443
|
+
async initializeContract(contract) {
|
|
444
|
+
if (this.initializedContracts.has(contract.name)) {
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
await this.ensureAdapterReady();
|
|
448
|
+
if (contract.hooks?.create) {
|
|
449
|
+
await contract.hooks.create(this.getLifecycleContext());
|
|
450
|
+
}
|
|
451
|
+
this.initializedContracts.add(contract.name);
|
|
452
|
+
}
|
|
453
|
+
async initializeContracts() {
|
|
454
|
+
for (const contract of this.contracts) {
|
|
455
|
+
await this.initializeContract(contract);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
async destroyContractLifecycle(contract) {
|
|
459
|
+
if (!this.initializedContracts.has(contract.name)) {
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
if (contract.hooks?.destroy) {
|
|
463
|
+
await contract.hooks.destroy(this.getLifecycleContext());
|
|
464
|
+
}
|
|
465
|
+
this.initializedContracts.delete(contract.name);
|
|
466
|
+
}
|
|
467
|
+
async destroyContracts() {
|
|
468
|
+
for (const contract of [...this.contracts]) {
|
|
469
|
+
await this.destroyContractLifecycle(contract);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
startSubscriptionCleanupInterval() {
|
|
473
|
+
if (this.subscriptionCleanupInterval) {
|
|
474
|
+
return;
|
|
80
475
|
}
|
|
81
|
-
// Start subscription cleanup interval
|
|
82
476
|
this.subscriptionCleanupInterval = setInterval(() => {
|
|
83
477
|
this.cleanupSubscriptions();
|
|
84
|
-
}, 60000);
|
|
478
|
+
}, 60000);
|
|
85
479
|
}
|
|
86
|
-
|
|
87
|
-
this.
|
|
88
|
-
|
|
89
|
-
this.
|
|
480
|
+
clearRuntimeTimers() {
|
|
481
|
+
if (this.blockNumberTimeout) {
|
|
482
|
+
clearTimeout(this.blockNumberTimeout);
|
|
483
|
+
this.blockNumberTimeout = null;
|
|
484
|
+
}
|
|
485
|
+
if (this.latestBlockTimer) {
|
|
486
|
+
clearInterval(this.latestBlockTimer);
|
|
487
|
+
this.latestBlockTimer = null;
|
|
90
488
|
}
|
|
489
|
+
if (this.subscriptionCleanupInterval) {
|
|
490
|
+
clearInterval(this.subscriptionCleanupInterval);
|
|
491
|
+
this.subscriptionCleanupInterval = null;
|
|
492
|
+
}
|
|
493
|
+
this.isPollingBlock = false;
|
|
91
494
|
}
|
|
92
495
|
async registerAdapter(adapter) {
|
|
93
|
-
if (
|
|
496
|
+
if (!adapter) {
|
|
497
|
+
throw new Error('Adapter must be provided');
|
|
498
|
+
}
|
|
499
|
+
await this.destroyContracts();
|
|
500
|
+
if (this.adapterInitialized && this.adapter?.destroy) {
|
|
94
501
|
try {
|
|
95
502
|
await this.adapter.destroy();
|
|
96
503
|
}
|
|
@@ -99,9 +506,10 @@ class Streamer {
|
|
|
99
506
|
}
|
|
100
507
|
}
|
|
101
508
|
this.adapter = adapter;
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
509
|
+
this.adapterInitialized = false;
|
|
510
|
+
this.adapterInitializationPromise = null;
|
|
511
|
+
await this.ensureAdapterReady();
|
|
512
|
+
await this.initializeContracts();
|
|
105
513
|
}
|
|
106
514
|
getAdapter() {
|
|
107
515
|
return this.adapter;
|
|
@@ -113,6 +521,7 @@ class Streamer {
|
|
|
113
521
|
if (!action || !(action instanceof actions_1.TimeAction)) {
|
|
114
522
|
throw new Error('Invalid action: must be an instance of TimeAction');
|
|
115
523
|
}
|
|
524
|
+
await this.ensureAdapterReady();
|
|
116
525
|
const loadedActions = await this.adapter.loadActions();
|
|
117
526
|
for (const a of loadedActions) {
|
|
118
527
|
const exists = this.actions.find(i => i.id === a.id);
|
|
@@ -217,7 +626,7 @@ class Streamer {
|
|
|
217
626
|
*/
|
|
218
627
|
async saveActionsToDisk() {
|
|
219
628
|
try {
|
|
220
|
-
if (this.adapter?.saveState) {
|
|
629
|
+
if (this.adapterInitialized && this.adapter?.saveState) {
|
|
221
630
|
await this.adapter.saveState({
|
|
222
631
|
lastBlockNumber: this.lastBlockNumber,
|
|
223
632
|
actions: this.actions.map(a => a.toJSON())
|
|
@@ -258,14 +667,7 @@ class Streamer {
|
|
|
258
667
|
if (!contract.actions || typeof contract.actions !== 'object') {
|
|
259
668
|
throw new Error(`Contract '${contract.name}' must define actions`);
|
|
260
669
|
}
|
|
261
|
-
|
|
262
|
-
streamer: this,
|
|
263
|
-
adapter: this.adapter,
|
|
264
|
-
config: this.config
|
|
265
|
-
};
|
|
266
|
-
if (contract.hooks?.create) {
|
|
267
|
-
await contract.hooks.create(lifecycleContext);
|
|
268
|
-
}
|
|
670
|
+
await this.initializeContract(contract);
|
|
269
671
|
this.contracts.push(contract);
|
|
270
672
|
this.contractCache.set(contract.name, contract);
|
|
271
673
|
}
|
|
@@ -273,13 +675,7 @@ class Streamer {
|
|
|
273
675
|
const contractIndex = this.contracts.findIndex(c => c.name === name);
|
|
274
676
|
if (contractIndex >= 0) {
|
|
275
677
|
const contract = this.contracts[contractIndex];
|
|
276
|
-
|
|
277
|
-
await contract.hooks.destroy({
|
|
278
|
-
streamer: this,
|
|
279
|
-
adapter: this.adapter,
|
|
280
|
-
config: this.config
|
|
281
|
-
});
|
|
282
|
-
}
|
|
678
|
+
await this.destroyContractLifecycle(contract);
|
|
283
679
|
this.contracts.splice(contractIndex, 1);
|
|
284
680
|
this.contractCache.delete(name);
|
|
285
681
|
}
|
|
@@ -292,11 +688,37 @@ class Streamer {
|
|
|
292
688
|
* @param config
|
|
293
689
|
*/
|
|
294
690
|
setConfig(config) {
|
|
295
|
-
|
|
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);
|
|
296
701
|
// Set keys and username incase they have changed
|
|
297
702
|
this.username = this.config.USERNAME;
|
|
298
703
|
this.postingKey = this.config.POSTING_KEY;
|
|
299
704
|
this.activeKey = this.config.ACTIVE_KEY;
|
|
705
|
+
if (shouldRecreateClient) {
|
|
706
|
+
this.client = new dhive_1.Client(this.config.API_NODES);
|
|
707
|
+
if (this.blockProvider instanceof hive_provider_1.HiveProvider) {
|
|
708
|
+
this.blockProvider.updateClient(this.config.API_NODES);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
if (shouldRecreateHiveEngine) {
|
|
712
|
+
this.hive = new sscjs_1.default(this.config.HIVE_ENGINE_API);
|
|
713
|
+
}
|
|
714
|
+
if (shouldSyncApiServer && (this.apiServer || this.isStarted)) {
|
|
715
|
+
const syncApiServer = this.config.API_ENABLED
|
|
716
|
+
? this.startApiServer(this.config.API_PORT)
|
|
717
|
+
: this.stopApiServer();
|
|
718
|
+
syncApiServer.catch((error) => {
|
|
719
|
+
console.error('[Streamer] Failed to sync API server after config update:', error);
|
|
720
|
+
});
|
|
721
|
+
}
|
|
300
722
|
return this;
|
|
301
723
|
}
|
|
302
724
|
/**
|
|
@@ -306,10 +728,17 @@ class Streamer {
|
|
|
306
728
|
*
|
|
307
729
|
*/
|
|
308
730
|
async start() {
|
|
731
|
+
if (this.isStarted) {
|
|
732
|
+
return this;
|
|
733
|
+
}
|
|
309
734
|
if (this.config.DEBUG_MODE) {
|
|
310
735
|
console.log('Starting to stream the Hive blockchain');
|
|
311
736
|
}
|
|
312
737
|
this.disableAllProcessing = false;
|
|
738
|
+
await this.ensureAdapterReady();
|
|
739
|
+
await this.blockProvider.create?.();
|
|
740
|
+
await this.initializeContracts();
|
|
741
|
+
this.startSubscriptionCleanupInterval();
|
|
313
742
|
const state = await this.adapter.loadState();
|
|
314
743
|
if (this.config.DEBUG_MODE) {
|
|
315
744
|
console.log(`Restoring state from file`);
|
|
@@ -320,9 +749,13 @@ class Streamer {
|
|
|
320
749
|
else if (this.config.LAST_BLOCK_NUMBER) {
|
|
321
750
|
this.lastBlockNumber = this.config.LAST_BLOCK_NUMBER;
|
|
322
751
|
}
|
|
752
|
+
if (this.config.API_ENABLED) {
|
|
753
|
+
await this.startApiServer(this.config.API_PORT);
|
|
754
|
+
}
|
|
323
755
|
// Kicks off the blockchain streaming and operation parsing
|
|
324
756
|
this.getBlock();
|
|
325
757
|
this.latestBlockTimer = setInterval(() => { this.getLatestBlock(); }, this.config.BLOCK_CHECK_INTERVAL);
|
|
758
|
+
this.isStarted = true;
|
|
326
759
|
return this;
|
|
327
760
|
}
|
|
328
761
|
/**
|
|
@@ -332,23 +765,52 @@ class Streamer {
|
|
|
332
765
|
*/
|
|
333
766
|
async stop() {
|
|
334
767
|
this.disableAllProcessing = true;
|
|
335
|
-
|
|
336
|
-
|
|
768
|
+
this.isStarted = false;
|
|
769
|
+
this.clearRuntimeTimers();
|
|
770
|
+
await this.stopApiServer();
|
|
771
|
+
await this.destroyContracts();
|
|
772
|
+
if (this.adapterInitialized && this?.adapter?.destroy) {
|
|
773
|
+
await this.adapter.destroy();
|
|
337
774
|
}
|
|
338
|
-
|
|
339
|
-
|
|
775
|
+
await this.blockProvider.destroy?.();
|
|
776
|
+
this.adapterInitialized = false;
|
|
777
|
+
this.adapterInitializationPromise = null;
|
|
778
|
+
await (0, utils_1.sleep)(25);
|
|
779
|
+
}
|
|
780
|
+
async startApiServer(port = this.config.API_PORT) {
|
|
781
|
+
await this.ensureAdapterReady();
|
|
782
|
+
if (this.apiServer?.server && this.apiServer.port === port) {
|
|
783
|
+
return this.apiServer;
|
|
340
784
|
}
|
|
341
|
-
if (this.
|
|
342
|
-
|
|
785
|
+
if (this.apiServer) {
|
|
786
|
+
await this.stopApiServer();
|
|
343
787
|
}
|
|
344
|
-
|
|
345
|
-
|
|
788
|
+
this.apiServer = new api_1.Api(this, { port });
|
|
789
|
+
await this.apiServer.start();
|
|
790
|
+
return this.apiServer;
|
|
791
|
+
}
|
|
792
|
+
async stopApiServer() {
|
|
793
|
+
if (!this.apiServer) {
|
|
794
|
+
return;
|
|
346
795
|
}
|
|
347
|
-
|
|
796
|
+
const apiServer = this.apiServer;
|
|
797
|
+
this.apiServer = null;
|
|
798
|
+
await apiServer.stop();
|
|
799
|
+
}
|
|
800
|
+
getApiServer() {
|
|
801
|
+
return this.apiServer;
|
|
802
|
+
}
|
|
803
|
+
async registerBlockProvider(provider) {
|
|
804
|
+
await this.blockProvider.destroy?.();
|
|
805
|
+
this.blockProvider = provider;
|
|
806
|
+
await this.blockProvider.create?.();
|
|
807
|
+
}
|
|
808
|
+
getBlockProvider() {
|
|
809
|
+
return this.blockProvider;
|
|
348
810
|
}
|
|
349
811
|
async getLatestBlock() {
|
|
350
812
|
try {
|
|
351
|
-
const props = await this.
|
|
813
|
+
const props = await this.blockProvider.getDynamicGlobalProperties();
|
|
352
814
|
if (props) {
|
|
353
815
|
this.latestBlockchainTime = new Date(`${props.time}Z`);
|
|
354
816
|
}
|
|
@@ -365,8 +827,8 @@ class Streamer {
|
|
|
365
827
|
this.isPollingBlock = true;
|
|
366
828
|
let nextDelay = this.config.BLOCK_CHECK_INTERVAL;
|
|
367
829
|
try {
|
|
368
|
-
// Load global properties from the
|
|
369
|
-
const props = await this.
|
|
830
|
+
// Load global properties from the block provider
|
|
831
|
+
const props = await this.blockProvider.getDynamicGlobalProperties();
|
|
370
832
|
// We have no props, so try loading them again.
|
|
371
833
|
if (!props) {
|
|
372
834
|
return;
|
|
@@ -420,8 +882,8 @@ class Streamer {
|
|
|
420
882
|
// Check cache first
|
|
421
883
|
let block = this.blockCache.get(blockNumber);
|
|
422
884
|
if (!block) {
|
|
423
|
-
// Load the block
|
|
424
|
-
block = await this.
|
|
885
|
+
// Load the block from the active block provider
|
|
886
|
+
block = await this.blockProvider.getBlock(blockNumber);
|
|
425
887
|
// Cache the block for potential reuse
|
|
426
888
|
if (block) {
|
|
427
889
|
this.blockCache.set(blockNumber, block);
|
|
@@ -451,37 +913,26 @@ class Streamer {
|
|
|
451
913
|
if (this.adapter?.processBlock) {
|
|
452
914
|
this.adapter.processBlock(block);
|
|
453
915
|
}
|
|
454
|
-
//
|
|
916
|
+
// Hive operations are order-sensitive, so process them sequentially.
|
|
455
917
|
const transactions = block.transactions;
|
|
456
918
|
const transactionIds = block.transaction_ids;
|
|
457
|
-
// Create operation processing promises for better concurrency
|
|
458
|
-
const operationPromises = [];
|
|
459
919
|
for (let i = 0; i < transactions.length; i++) {
|
|
460
920
|
const transaction = transactions[i];
|
|
461
921
|
const operations = transaction.operations;
|
|
462
|
-
// Process operations in batch for better performance
|
|
463
922
|
for (let opIndex = 0; opIndex < operations.length; opIndex++) {
|
|
464
923
|
const op = operations[opIndex];
|
|
465
|
-
|
|
466
|
-
|
|
924
|
+
try {
|
|
925
|
+
await this.processOperation(op, blockNumber, block.block_id, block.previous, transactionIds[i], blockTime);
|
|
926
|
+
}
|
|
927
|
+
catch (error) {
|
|
467
928
|
console.error('[Streamer] Operation processing error:', error, {
|
|
468
929
|
blockNumber,
|
|
469
930
|
transactionIndex: i,
|
|
470
931
|
operationIndex: opIndex
|
|
471
932
|
});
|
|
472
|
-
});
|
|
473
|
-
operationPromises.push(operationPromise);
|
|
474
|
-
// Process in batches to avoid overwhelming the system
|
|
475
|
-
if (operationPromises.length >= 50) {
|
|
476
|
-
await Promise.all(operationPromises);
|
|
477
|
-
operationPromises.length = 0; // Clear array
|
|
478
933
|
}
|
|
479
934
|
}
|
|
480
935
|
}
|
|
481
|
-
// Process any remaining operations
|
|
482
|
-
if (operationPromises.length > 0) {
|
|
483
|
-
await Promise.all(operationPromises);
|
|
484
|
-
}
|
|
485
936
|
this.lastBlockNumber = blockNumber;
|
|
486
937
|
this.saveStateThrottled();
|
|
487
938
|
}
|
|
@@ -517,13 +968,23 @@ class Streamer {
|
|
|
517
968
|
if (operationType === 'transfer') {
|
|
518
969
|
const sender = operationData?.from;
|
|
519
970
|
const rawAmount = operationData?.amount;
|
|
520
|
-
|
|
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
|
+
}
|
|
521
982
|
const transferInfo = {
|
|
522
983
|
from: sender,
|
|
523
984
|
to: operationData?.to,
|
|
524
985
|
rawAmount: rawAmount || '',
|
|
525
|
-
amount
|
|
526
|
-
asset
|
|
986
|
+
amount,
|
|
987
|
+
asset,
|
|
527
988
|
memo: operationData?.memo
|
|
528
989
|
};
|
|
529
990
|
const json = utils_2.Utils.jsonParse(operationData.memo);
|
|
@@ -546,11 +1007,9 @@ class Streamer {
|
|
|
546
1007
|
});
|
|
547
1008
|
await this.dispatchContractAction(payload, context);
|
|
548
1009
|
}
|
|
549
|
-
this.transferSubscriptions
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
}
|
|
553
|
-
});
|
|
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))));
|
|
554
1013
|
}
|
|
555
1014
|
// This is a custom JSON operation
|
|
556
1015
|
if (operationType === 'custom_json') {
|
|
@@ -880,8 +1339,11 @@ class Streamer {
|
|
|
880
1339
|
}
|
|
881
1340
|
}
|
|
882
1341
|
async saveStateToDisk() {
|
|
883
|
-
if (this.adapter?.saveState) {
|
|
884
|
-
await this.adapter.saveState({
|
|
1342
|
+
if (this.adapterInitialized && this.adapter?.saveState) {
|
|
1343
|
+
await this.adapter.saveState({
|
|
1344
|
+
lastBlockNumber: this.lastBlockNumber,
|
|
1345
|
+
actions: this.actions.map(action => action.toJSON())
|
|
1346
|
+
});
|
|
885
1347
|
}
|
|
886
1348
|
}
|
|
887
1349
|
// Throttled state saving for performance
|
|
@@ -904,6 +1366,229 @@ class Streamer {
|
|
|
904
1366
|
transferHiveTokens(from, to, amount, symbol, memo = '') {
|
|
905
1367
|
return utils_2.Utils.transferHiveTokens(this.client, this.config, from, to, amount, symbol, memo);
|
|
906
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
|
+
}
|
|
907
1592
|
transferHiveTokensMultiple(from, accounts = [], amount = '0', symbol, memo = '') {
|
|
908
1593
|
return utils_2.Utils.transferHiveTokensMultiple(this.client, this.config, from, accounts, amount, symbol, memo);
|
|
909
1594
|
}
|
|
@@ -946,6 +1631,9 @@ class Streamer {
|
|
|
946
1631
|
transferHiveEngineTokens(from, to, symbol, quantity, memo = '') {
|
|
947
1632
|
return utils_2.Utils.transferHiveEngineTokens(this.client, this.config, from, to, quantity, symbol, memo);
|
|
948
1633
|
}
|
|
1634
|
+
burnHiveEngineTokens(from, symbol, quantity, memo = '') {
|
|
1635
|
+
return utils_2.Utils.burnHiveEngineTokens(this.client, this.config, from, symbol, quantity, memo);
|
|
1636
|
+
}
|
|
949
1637
|
transferHiveEngineTokensMultiple(from, accounts = [], symbol, memo = '', amount = '0') {
|
|
950
1638
|
return utils_2.Utils.transferHiveEngineTokensMultiple(this.client, this.config, from, accounts, symbol, memo, amount);
|
|
951
1639
|
}
|
|
@@ -1048,8 +1736,16 @@ class Streamer {
|
|
|
1048
1736
|
}
|
|
1049
1737
|
}
|
|
1050
1738
|
// Add method to remove specific subscriptions
|
|
1051
|
-
removeTransferSubscription(account) {
|
|
1052
|
-
this.transferSubscriptions = this.transferSubscriptions.filter(sub =>
|
|
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
|
+
});
|
|
1053
1749
|
}
|
|
1054
1750
|
removeCustomJsonIdSubscription(id) {
|
|
1055
1751
|
this.customJsonIdSubscriptions = this.customJsonIdSubscriptions.filter(sub => sub.id !== id);
|