@subwallet/extension-base 1.3.68-1 → 1.3.70-2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/background/KoniTypes.d.ts +11 -0
  2. package/background/KoniTypes.js +3 -0
  3. package/cjs/background/KoniTypes.js +3 -0
  4. package/cjs/koni/background/handlers/Extension.js +62 -0
  5. package/cjs/koni/background/handlers/State.js +5 -2
  6. package/cjs/koni/background/handlers/Tabs.js +11 -4
  7. package/cjs/packageInfo.js +1 -1
  8. package/cjs/services/balance-service/transfer/token.js +34 -3
  9. package/cjs/services/chain-service/constants.js +17 -5
  10. package/cjs/services/chain-service/utils/index.js +13 -5
  11. package/cjs/services/chain-service/utils/patch.js +1 -1
  12. package/cjs/services/event-service/index.js +1 -0
  13. package/cjs/services/open-gov/handler.js +563 -0
  14. package/cjs/services/open-gov/index.js +273 -0
  15. package/cjs/services/open-gov/interface.js +28 -0
  16. package/cjs/services/open-gov/utils.js +66 -0
  17. package/cjs/services/storage-service/DatabaseService.js +19 -1
  18. package/cjs/services/storage-service/databases/index.js +3 -0
  19. package/cjs/services/storage-service/db-stores/GovLockedInfoStore.js +35 -0
  20. package/cjs/services/transaction-service/helpers/index.js +6 -0
  21. package/cjs/services/transaction-service/index.js +43 -0
  22. package/cjs/services/transaction-service/utils.js +3 -3
  23. package/cjs/utils/account/transform.js +5 -4
  24. package/koni/background/handlers/Extension.d.ts +4 -0
  25. package/koni/background/handlers/Extension.js +62 -0
  26. package/koni/background/handlers/State.d.ts +2 -0
  27. package/koni/background/handlers/State.js +5 -2
  28. package/koni/background/handlers/Tabs.js +11 -4
  29. package/package.json +31 -6
  30. package/packageInfo.js +1 -1
  31. package/services/balance-service/transfer/token.d.ts +4 -0
  32. package/services/balance-service/transfer/token.js +31 -1
  33. package/services/chain-service/constants.d.ts +9 -0
  34. package/services/chain-service/constants.js +14 -3
  35. package/services/chain-service/utils/index.js +13 -5
  36. package/services/chain-service/utils/patch.d.ts +1 -1
  37. package/services/chain-service/utils/patch.js +1 -1
  38. package/services/event-service/index.d.ts +1 -0
  39. package/services/event-service/index.js +1 -0
  40. package/services/event-service/types.d.ts +1 -0
  41. package/services/open-gov/handler.d.ts +27 -0
  42. package/services/open-gov/handler.js +547 -0
  43. package/services/open-gov/index.d.ts +45 -0
  44. package/services/open-gov/index.js +265 -0
  45. package/services/open-gov/interface.d.ts +141 -0
  46. package/services/open-gov/interface.js +21 -0
  47. package/services/open-gov/utils.d.ts +14 -0
  48. package/services/open-gov/utils.js +52 -0
  49. package/services/storage-service/DatabaseService.d.ts +7 -0
  50. package/services/storage-service/DatabaseService.js +19 -1
  51. package/services/storage-service/databases/index.d.ts +2 -0
  52. package/services/storage-service/databases/index.js +3 -0
  53. package/services/storage-service/db-stores/GovLockedInfoStore.d.ts +10 -0
  54. package/services/storage-service/db-stores/GovLockedInfoStore.js +27 -0
  55. package/services/transaction-service/helpers/index.js +6 -0
  56. package/services/transaction-service/index.js +43 -0
  57. package/services/transaction-service/utils.js +3 -3
  58. package/utils/account/transform.js +5 -4
@@ -0,0 +1,563 @@
1
+ "use strict";
2
+
3
+ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4
+ Object.defineProperty(exports, "__esModule", {
5
+ value: true
6
+ });
7
+ exports.default = void 0;
8
+ var _TransactionError = require("@subwallet/extension-base/background/errors/TransactionError");
9
+ var _types = require("@subwallet/extension-base/types");
10
+ var _number = require("@subwallet/extension-base/utils/number");
11
+ var _bignumber = _interopRequireDefault(require("bignumber.js"));
12
+ var _rxjs = require("rxjs");
13
+ var _constants = require("../chain-service/constants");
14
+ var _utils = require("../chain-service/utils");
15
+ var _interface = require("./interface");
16
+ var _utils2 = require("./utils");
17
+ // Copyright 2019-2022 @subwallet/extension-base
18
+ // SPDX-License-Identifier: Apache-2.0
19
+
20
+ class BaseOpenGovHandler {
21
+ constructor(state, chain) {
22
+ this.state = state;
23
+ this.chain = chain;
24
+ }
25
+ get substrateApi() {
26
+ return this.state.getSubstrateApi(this.chain);
27
+ }
28
+ get chainInfo() {
29
+ return this.state.getChainInfo(this.chain);
30
+ }
31
+ get nativeToken() {
32
+ return this.state.getNativeTokenInfo(this.chain);
33
+ }
34
+ lockPeriod(days) {
35
+ var _EXPECTED_BLOCK_TIME$;
36
+ const blockTime = (_EXPECTED_BLOCK_TIME$ = _constants._EXPECTED_BLOCK_TIME[this.chain]) !== null && _EXPECTED_BLOCK_TIME$ !== void 0 ? _EXPECTED_BLOCK_TIME$ : 6;
37
+ const baseLockedPeriod = 24 * 60 * 60 * days;
38
+ return baseLockedPeriod / blockTime;
39
+ }
40
+ refToTrackMap = new Map();
41
+
42
+ /* Referendum related actions */
43
+
44
+ async handleVote(request) {
45
+ const earlyError = await this.earlyValidateVoting(request);
46
+ if (earlyError) {
47
+ return Promise.reject(earlyError);
48
+ }
49
+ switch (request.type) {
50
+ case _interface.GovVoteType.AYE:
51
+ case _interface.GovVoteType.NAY:
52
+ return this.handleStandardVote(request);
53
+ case _interface.GovVoteType.SPLIT:
54
+ return this.handleSplitVote(request);
55
+ case _interface.GovVoteType.ABSTAIN:
56
+ return this.handleSplitAbstainVote(request);
57
+ default:
58
+ throw new _TransactionError.TransactionError(_types.BasicTxErrorType.INVALID_PARAMS, 'Unsupported vote type');
59
+ }
60
+ }
61
+ async handleStandardVote(request) {
62
+ const substrateApi = await this.substrateApi.isReady;
63
+ const earlyError = await this.validateConvictionAndBalance(request.address, request.amount, request.conviction);
64
+ if (earlyError) {
65
+ return Promise.reject(earlyError);
66
+ }
67
+ const extrinsic = substrateApi.api.tx.convictionVoting.vote(request.referendumIndex, {
68
+ Standard: {
69
+ vote: {
70
+ aye: request.type === _interface.GovVoteType.AYE,
71
+ conviction: _utils2.numberToConviction[request.conviction]
72
+ },
73
+ balance: request.amount
74
+ }
75
+ });
76
+ return extrinsic;
77
+ }
78
+ async handleSplitVote(request) {
79
+ const substrateApi = await this.substrateApi.isReady;
80
+ const earlyError = await this.validateSplitAbstainAmount(request.address, false, request.ayeAmount, request.nayAmount);
81
+ if (earlyError) {
82
+ return Promise.reject(earlyError);
83
+ }
84
+ const extrinsic = substrateApi.api.tx.convictionVoting.vote(request.referendumIndex, {
85
+ Split: {
86
+ aye: request.ayeAmount,
87
+ nay: request.nayAmount
88
+ }
89
+ });
90
+ return extrinsic;
91
+ }
92
+ async handleSplitAbstainVote(request) {
93
+ const substrateApi = await this.substrateApi.isReady;
94
+ const earlyError = await this.validateSplitAbstainAmount(request.address, true, request.ayeAmount, request.nayAmount, request.abstainAmount);
95
+ if (earlyError) {
96
+ return Promise.reject(earlyError);
97
+ }
98
+ const extrinsic = substrateApi.api.tx.convictionVoting.vote(request.referendumIndex, {
99
+ SplitAbstain: {
100
+ aye: request.ayeAmount,
101
+ nay: request.nayAmount,
102
+ abstain: request.abstainAmount
103
+ }
104
+ });
105
+ return extrinsic;
106
+ }
107
+ async handleRemoveVote(request) {
108
+ const substrateApi = await this.substrateApi.isReady;
109
+ const extrinsic = substrateApi.api.tx.convictionVoting.removeVote(request.trackId, request.referendumIndex);
110
+ return extrinsic;
111
+ }
112
+ async handleUnlockVote(request) {
113
+ const substrateApi = await this.substrateApi.isReady;
114
+ const {
115
+ address,
116
+ referendumIds,
117
+ trackIds
118
+ } = request;
119
+ const extrinsics = [];
120
+
121
+ // 1. Unlock all refs
122
+ for (const refIndex of referendumIds !== null && referendumIds !== void 0 ? referendumIds : []) {
123
+ const trackId = this.refToTrackMap.get(refIndex);
124
+ extrinsics.push(substrateApi.api.tx.convictionVoting.removeVote(trackId !== null && trackId !== void 0 ? trackId : null, refIndex));
125
+ }
126
+
127
+ // 2. Unlock all tracks
128
+ for (const trackId of trackIds !== null && trackIds !== void 0 ? trackIds : []) {
129
+ extrinsics.push(substrateApi.api.tx.convictionVoting.unlock(trackId, address));
130
+ }
131
+
132
+ // 3. Decide whether to batch or not
133
+ if (extrinsics.length === 1) {
134
+ return extrinsics[0];
135
+ }
136
+ if (extrinsics.length === 0) {
137
+ return Promise.reject(new _TransactionError.TransactionError(_types.BasicTxErrorType.INVALID_PARAMS));
138
+ }
139
+ return substrateApi.api.tx.utility.batchAll(extrinsics);
140
+ }
141
+
142
+ /* Validate OpengGov Action */
143
+
144
+ async earlyValidateVoting(request) {
145
+ var _locked$delegating;
146
+ const substrateApi = await this.substrateApi.isReady;
147
+ const {
148
+ address,
149
+ trackId
150
+ } = request;
151
+ const locked = (await substrateApi.api.query.convictionVoting.votingFor(address, trackId)).toPrimitive();
152
+ if (!locked) {
153
+ return null;
154
+ }
155
+ if (locked !== null && locked !== void 0 && (_locked$delegating = locked.delegating) !== null && _locked$delegating !== void 0 && _locked$delegating.balance && new _bignumber.default(locked.delegating.balance).gt(0)) {
156
+ return new _TransactionError.TransactionError(_types.BasicTxErrorType.INVALID_PARAMS, `Already delegating on track ${trackId}`);
157
+ }
158
+ return null;
159
+ }
160
+ async validateConvictionAndBalance(address, balance, conviction) {
161
+ if (!balance) {
162
+ return new _TransactionError.TransactionError(_types.BasicTxErrorType.INVALID_PARAMS, 'Amount is required');
163
+ }
164
+ if (conviction < 0 || conviction > 6) {
165
+ return new _TransactionError.TransactionError(_types.BasicTxErrorType.INVALID_PARAMS, 'Invalid conviction');
166
+ }
167
+ const totalBalance = await this.state.balanceService.getTotalBalance(address, this.chain);
168
+ const bnBalance = new _bignumber.default(balance);
169
+ const substrateApi = await this.substrateApi.isReady;
170
+ let estimatedFee = new _bignumber.default(0);
171
+ try {
172
+ const dummyTx = substrateApi.api.tx.convictionVoting.vote(0, {
173
+ Standard: {
174
+ vote: {
175
+ aye: true,
176
+ conviction
177
+ },
178
+ balance: bnBalance.toString()
179
+ }
180
+ });
181
+ const paymentInfo = await dummyTx.paymentInfo(address);
182
+ estimatedFee = new _bignumber.default(paymentInfo.partialFee.toString());
183
+ } catch (e) {
184
+ console.warn('Cannot estimate fee, fallback to default', e);
185
+ const decimals = Number((0, _utils._getAssetDecimals)(this.nativeToken));
186
+ estimatedFee = new _bignumber.default(0.001).multipliedBy(new _bignumber.default(10).pow(decimals)); // fallback 0.001
187
+ }
188
+
189
+ const availableBalance = new _bignumber.default(totalBalance.value).minus(estimatedFee);
190
+ if (availableBalance.lte(0)) {
191
+ return new _TransactionError.TransactionError(_types.BasicTxErrorType.NOT_ENOUGH_BALANCE, "You don't have enough tokens to proceed");
192
+ }
193
+ if (bnBalance.gt(availableBalance)) {
194
+ return new _TransactionError.TransactionError(_types.BasicTxErrorType.NOT_ENOUGH_BALANCE, `Amount must be equal or less than ${(0, _number.formatNumber)(availableBalance, (0, _utils._getAssetDecimals)(this.nativeToken))}`);
195
+ }
196
+ return null;
197
+ }
198
+ async validateSplitAbstainAmount(address) {
199
+ let isSplitAbstain = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
200
+ let aye = arguments.length > 2 ? arguments[2] : undefined;
201
+ let nay = arguments.length > 3 ? arguments[3] : undefined;
202
+ let abstain = arguments.length > 4 ? arguments[4] : undefined;
203
+ if (!nay || !aye) {
204
+ return new _TransactionError.TransactionError(_types.BasicTxErrorType.INVALID_PARAMS, 'Amount is required');
205
+ }
206
+ const values = [new _bignumber.default(aye), new _bignumber.default(nay), new _bignumber.default(abstain !== null && abstain !== void 0 ? abstain : '0')];
207
+ const total = values.reduce((acc, val) => acc.plus(val), new _bignumber.default(0));
208
+ const totalBalance = await this.state.balanceService.getTotalBalance(address, this.chain);
209
+ const substrateApi = await this.substrateApi.isReady;
210
+ let dummyTx;
211
+ try {
212
+ dummyTx = substrateApi.api.tx.convictionVoting.vote(1,
213
+ // dummy referendum id
214
+ isSplitAbstain ? {
215
+ SplitAbstain: {
216
+ aye: 1,
217
+ nay: 1,
218
+ abstain: 1
219
+ }
220
+ } : {
221
+ Split: {
222
+ aye: 1,
223
+ nay: 1
224
+ }
225
+ });
226
+ } catch (e) {
227
+ console.warn('Cannot build dummy tx for fee estimation', e);
228
+ }
229
+ let estimatedFee = new _bignumber.default(0);
230
+ if (dummyTx) {
231
+ try {
232
+ const paymentInfo = await dummyTx.paymentInfo(address);
233
+ estimatedFee = new _bignumber.default(paymentInfo.partialFee.toString());
234
+ } catch (e) {
235
+ console.warn('Cannot get payment info, fallback to default fee', e);
236
+ estimatedFee = new _bignumber.default(0.001 * 10 ** (0, _utils._getAssetDecimals)(this.nativeToken)); // fallback 0.001
237
+ }
238
+ }
239
+
240
+ const availableBalance = new _bignumber.default(totalBalance.value).minus(estimatedFee);
241
+ if (availableBalance.lte(0)) {
242
+ return new _TransactionError.TransactionError(_types.BasicTxErrorType.NOT_ENOUGH_BALANCE, "You don't have enough tokens to proceed");
243
+ }
244
+ if (total.gt(availableBalance)) {
245
+ return new _TransactionError.TransactionError(_types.BasicTxErrorType.NOT_ENOUGH_BALANCE, `Amount must be equal or less than ${(0, _number.formatNumber)(availableBalance, (0, _utils._getAssetDecimals)(this.nativeToken))}`);
246
+ }
247
+ return null;
248
+ }
249
+
250
+ /* Lock info */
251
+ async subscribeGovLockedInfo(addresses, cb) {
252
+ const substrateApi = await this.substrateApi.isReady;
253
+ const streams = addresses.map(addr => {
254
+ return (0, _rxjs.combineLatest)([substrateApi.api.query.convictionVoting.votingFor.entries(addr), substrateApi.api.query.convictionVoting.classLocksFor(addr)]).pipe((0, _rxjs.mergeMap)(async _ref => {
255
+ let [votingEntries, classLocks] = _ref;
256
+ let totalDelegated = new _bignumber.default(0);
257
+ let totalVoted = new _bignumber.default(0);
258
+ const tracks = [];
259
+ const trackBalances = new Map();
260
+ const trackStates = new Map();
261
+ const trackVotedAmounts = new Map();
262
+ const unlockingReferenda = [];
263
+ const unlockableReferenda = new Set();
264
+ const trackVotes = new Map();
265
+ const trackPriorBlocks = new Map();
266
+ let totalLocked = new _bignumber.default(0);
267
+
268
+ // --- Collect locked balances per track ---
269
+ const classLocksArray = classLocks.toPrimitive();
270
+ for (const [trackId, balance] of classLocksArray) {
271
+ const bnBalance = new _bignumber.default(balance);
272
+ trackBalances.set(trackId, bnBalance);
273
+ totalLocked = _bignumber.default.max(totalLocked, bnBalance);
274
+ }
275
+ let currentBlock;
276
+ if (_utils2.MIGRATED_CHAINS.includes(this.chain) && substrateApi.api.query.parachainSystem && substrateApi.api.query.parachainSystem.lastRelayChainBlockNumber) {
277
+ const blockRootsRaw = await substrateApi.api.query.parachainSystem.lastRelayChainBlockNumber();
278
+ const blockRoots = blockRootsRaw === null || blockRootsRaw === void 0 ? void 0 : blockRootsRaw.toPrimitive();
279
+ if (blockRoots) {
280
+ currentBlock = new _bignumber.default(blockRoots);
281
+ } else {
282
+ const currentBlockInfo = await substrateApi.api.rpc.chain.getHeader();
283
+ currentBlock = new _bignumber.default(currentBlockInfo.toPrimitive().number);
284
+ }
285
+ } else {
286
+ // fallback
287
+ const currentBlockInfo = await substrateApi.api.rpc.chain.getHeader();
288
+ currentBlock = new _bignumber.default(currentBlockInfo.toPrimitive().number);
289
+ }
290
+
291
+ // --- Handle each voting entry per track ---
292
+ for (const [key, voting] of votingEntries) {
293
+ const trackId = key.args[1].toPrimitive();
294
+ const v = voting.toPrimitive();
295
+ if (v.delegating) {
296
+ // Track is delegating → store delegation info
297
+ trackStates.set(trackId, 'delegating');
298
+ const {
299
+ balance,
300
+ conviction,
301
+ target
302
+ } = v.delegating;
303
+ const delegation = {
304
+ balance: balance.toString(),
305
+ target,
306
+ conviction
307
+ };
308
+ tracks.push({
309
+ trackId,
310
+ delegation
311
+ });
312
+ } else if (v.casting) {
313
+ trackStates.set(trackId, 'casting');
314
+ const priorBlock = new _bignumber.default(v.casting.prior[0]);
315
+ const priorBalance = new _bignumber.default(v.casting.prior[1]);
316
+ if (!currentBlock.gte(priorBlock)) {
317
+ var _EXPECTED_BLOCK_TIME$2;
318
+ // --- Still locked → estimate unlock timestamp ---
319
+ const blockTimeSec = (_EXPECTED_BLOCK_TIME$2 = _constants._EXPECTED_BLOCK_TIME[this.chain]) !== null && _EXPECTED_BLOCK_TIME$2 !== void 0 ? _EXPECTED_BLOCK_TIME$2 : 6;
320
+ const remainingBlocks = priorBlock.minus(currentBlock);
321
+ const timestamp = Date.now() + remainingBlocks.multipliedBy(blockTimeSec * 1000).toNumber();
322
+ unlockingReferenda.push({
323
+ id: `track_prior_${trackId}`,
324
+ balance: priorBalance.toFixed(),
325
+ timestamp
326
+ });
327
+ }
328
+
329
+ // --- Parse votes and check if referenda are finished ---
330
+ const {
331
+ unlockingReferenda: trackUnlocking,
332
+ votes
333
+ } = await this.parseVotesAndCheckFinished(v.casting.votes || [], unlockableReferenda, currentBlock.toNumber(), substrateApi);
334
+ unlockingReferenda.push(...trackUnlocking);
335
+ trackVotes.set(trackId, votes);
336
+ for (const vote of votes) {
337
+ this.refToTrackMap.set(vote.referendumIndex.toString(), trackId);
338
+ }
339
+
340
+ // --- Calculate total voted amount per track ---
341
+ const totalCast = votes.reduce((sum, vote) => {
342
+ return sum.plus(new _bignumber.default(vote.ayeAmount || '0')).plus(new _bignumber.default(vote.nayAmount || '0')).plus(new _bignumber.default(vote.abstainAmount || '0'));
343
+ }, new _bignumber.default(0));
344
+ trackVotedAmounts.set(trackId, totalCast);
345
+ if (v.casting.prior && new _bignumber.default(v.casting.prior[0]).gt(0)) {
346
+ trackPriorBlocks.set(trackId, new _bignumber.default(v.casting.prior[0]));
347
+ }
348
+ tracks.push({
349
+ trackId,
350
+ votes: votes.length > 0 ? votes : undefined
351
+ });
352
+ }
353
+ }
354
+
355
+ // --- Compute unlockable amounts across all tracks ---
356
+ const {
357
+ totalUnlockable,
358
+ unlockableTrackIds
359
+ } = this.calculateUnlockAmounts(trackBalances, trackStates, unlockableReferenda, trackVotes, trackPriorBlocks, currentBlock);
360
+
361
+ // --- Determine total delegated and voted locked balances ---
362
+ for (const [trackId, lockedBalance] of trackBalances) {
363
+ const state = trackStates.get(trackId) || 'empty';
364
+ if (state === 'delegating') {
365
+ totalDelegated = _bignumber.default.max(totalDelegated, lockedBalance);
366
+ } else if (state === 'casting') {
367
+ const votedAmount = trackVotedAmounts.get(trackId) || new _bignumber.default(0);
368
+ if (votedAmount.gt(0)) {
369
+ totalVoted = _bignumber.default.max(totalVoted, lockedBalance);
370
+ }
371
+ }
372
+ }
373
+ const result = {
374
+ chain: this.chain,
375
+ address: addr,
376
+ summary: {
377
+ delegated: totalDelegated.toString(),
378
+ voted: totalVoted.toString(),
379
+ totalLocked: totalLocked.toString(),
380
+ unlocking: {
381
+ unlockingReferenda
382
+ },
383
+ unlockable: {
384
+ balance: totalUnlockable.toFixed(),
385
+ trackIds: unlockableTrackIds,
386
+ unlockableReferenda: Array.from(unlockableReferenda).sort((a, b) => Number(a) - Number(b))
387
+ }
388
+ },
389
+ tracks
390
+ };
391
+ return result;
392
+ }));
393
+ });
394
+ const sub = (0, _rxjs.merge)(...streams).subscribe(cb);
395
+ return () => sub.unsubscribe();
396
+ }
397
+ async parseVotesAndCheckFinished(votesData, unlockableReferenda, currentBlockNumber, substrateApi) {
398
+ if (!votesData || votesData.length === 0) {
399
+ return {
400
+ votes: [],
401
+ unlockingReferenda: []
402
+ };
403
+ }
404
+ const votes = [];
405
+ const unlockingReferenda = [];
406
+
407
+ // --- Parse all vote types: standard / split / splitAbstain and normalize data
408
+ for (const [refIndex, vote] of votesData) {
409
+ if ('standard' in vote) {
410
+ const isAye = vote.standard.vote.aye === true;
411
+ votes.push({
412
+ referendumIndex: refIndex,
413
+ type: isAye ? _interface.GovVoteType.AYE : _interface.GovVoteType.NAY,
414
+ conviction: vote.standard.vote.conviction,
415
+ ayeAmount: isAye ? vote.standard.balance : '0',
416
+ nayAmount: !isAye ? vote.standard.balance : '0'
417
+ });
418
+ } else if ('split' in vote) {
419
+ votes.push({
420
+ referendumIndex: refIndex,
421
+ type: _interface.GovVoteType.SPLIT,
422
+ conviction: _interface.Conviction.None,
423
+ ayeAmount: vote.split.aye,
424
+ nayAmount: vote.split.nay
425
+ });
426
+ } else if ('splitAbstain' in vote) {
427
+ votes.push({
428
+ referendumIndex: refIndex,
429
+ type: _interface.GovVoteType.ABSTAIN,
430
+ conviction: _interface.Conviction.None,
431
+ ayeAmount: vote.splitAbstain.aye,
432
+ nayAmount: vote.splitAbstain.nay,
433
+ abstainAmount: vote.splitAbstain.abstain
434
+ });
435
+ }
436
+ }
437
+ const refIndexes = votes.map(v => v.referendumIndex);
438
+ const referendumInfos = await substrateApi.api.query.referenda.referendumInfoFor.multi(refIndexes);
439
+ referendumInfos.forEach((info, i) => {
440
+ if (info.isSome) {
441
+ const referendum = info.unwrap();
442
+ const refIndex = refIndexes[i];
443
+ const voteDetail = votes[i];
444
+ if (referendum.isKilled || referendum.isTimedOut || referendum.isCancelled) {
445
+ unlockableReferenda.add(refIndex.toString());
446
+ return;
447
+ }
448
+ if (!referendum.isOngoing) {
449
+ const referendumInfo = referendum.toJSON();
450
+
451
+ // 0x conviction (no lock) → unlock immediately
452
+ if (voteDetail.conviction === _interface.Conviction.None) {
453
+ unlockableReferenda.add(refIndex.toString());
454
+ return;
455
+ }
456
+
457
+ // --- Determine unlock block based on conviction ---
458
+ const statusKey = Object.keys(referendumInfo)[0];
459
+ const statusVal = referendumInfo[statusKey];
460
+ const endBlock = statusVal[0];
461
+ if (endBlock) {
462
+ const days = (0, _utils2.getConvictionDays)(this.chain, voteDetail.conviction);
463
+ const lockBlocks = this.lockPeriod(days);
464
+ const unlockBlock = new _bignumber.default(endBlock).plus(lockBlocks);
465
+ const canUnlock = new _bignumber.default(currentBlockNumber).gte(unlockBlock);
466
+
467
+ // Referendum ended → check if vote side allows unlock
468
+ const shouldUnlock = referendum.isApproved ? voteDetail.type === _interface.GovVoteType.NAY || canUnlock : voteDetail.type === _interface.GovVoteType.AYE || canUnlock;
469
+ if (shouldUnlock) {
470
+ unlockableReferenda.add(refIndex.toString());
471
+ } else {
472
+ var _EXPECTED_BLOCK_TIME$3;
473
+ // Can't unlock → calculate remaining lock time
474
+ const balance = new _bignumber.default(voteDetail.ayeAmount || '0').plus(new _bignumber.default(voteDetail.nayAmount || '0')).plus(new _bignumber.default(voteDetail.abstainAmount || '0'));
475
+ const blockTimeSec = (_EXPECTED_BLOCK_TIME$3 = _constants._EXPECTED_BLOCK_TIME[this.chain]) !== null && _EXPECTED_BLOCK_TIME$3 !== void 0 ? _EXPECTED_BLOCK_TIME$3 : 6;
476
+ const remainingBlocks = unlockBlock.minus(currentBlockNumber);
477
+ const timestamp = Date.now() + remainingBlocks.multipliedBy(blockTimeSec * 1000).toNumber();
478
+ unlockingReferenda.push({
479
+ id: refIndex.toString(),
480
+ balance: balance.toFixed(),
481
+ timestamp: timestamp
482
+ });
483
+ }
484
+ }
485
+ }
486
+ }
487
+ });
488
+ return {
489
+ votes,
490
+ unlockingReferenda
491
+ };
492
+ }
493
+ calculateUnlockAmounts(trackBalances, trackStates, unlockableReferenda, trackVotes, trackPriorBlocks, currentBlockNumber) {
494
+ const unlockableTrackIds = [];
495
+
496
+ // Determine which tracks are unlockable:
497
+ // - all votes finished
498
+ // - prior block passed
499
+ // - state is empty
500
+ // Calculate total unlockable amount = max(unlockable balance) - highest still locked balance
501
+ for (const [trackId, balance] of trackBalances) {
502
+ const state = trackStates.get(trackId) || 'empty';
503
+ const votes = trackVotes.get(trackId) || [];
504
+ const priorBlock = trackPriorBlocks.get(trackId) || new _bignumber.default(0);
505
+ if (state === 'casting') {
506
+ const allVotesUnlockable = votes.length === 0 || votes.every(vote => unlockableReferenda.has(vote.referendumIndex.toString()));
507
+ const activeVoteAmount = votes.filter(vote => !unlockableReferenda.has(vote.referendumIndex)).reduce((sum, vote) => {
508
+ return sum.plus(new _bignumber.default(vote.amount || '0')).plus(new _bignumber.default(vote.ayeAmount || '0')).plus(new _bignumber.default(vote.nayAmount || '0')).plus(new _bignumber.default(vote.abstainAmount || '0'));
509
+ }, new _bignumber.default(0));
510
+ if (allVotesUnlockable) {
511
+ if (priorBlock.eq(0) || currentBlockNumber.gte(priorBlock)) {
512
+ unlockableTrackIds.push(trackId);
513
+ }
514
+ } else if (activeVoteAmount.lt(balance)) {
515
+ if (priorBlock.eq(0) || currentBlockNumber.gte(priorBlock)) {
516
+ unlockableTrackIds.push(trackId);
517
+ }
518
+ }
519
+ } else if (state === 'empty') {
520
+ unlockableTrackIds.push(trackId);
521
+ }
522
+ }
523
+ const actualTrackBalances = new Map();
524
+ for (const [trackId, balance] of trackBalances) {
525
+ const state = trackStates.get(trackId) || 'empty';
526
+ const votes = trackVotes.get(trackId) || [];
527
+ if (state === 'casting') {
528
+ const activeVoteAmount = votes.filter(vote => !unlockableReferenda.has(vote.referendumIndex)).reduce((sum, vote) => {
529
+ return sum.plus(new _bignumber.default(vote.amount || '0')).plus(new _bignumber.default(vote.ayeAmount || '0')).plus(new _bignumber.default(vote.nayAmount || '0')).plus(new _bignumber.default(vote.abstainAmount || '0'));
530
+ }, new _bignumber.default(0));
531
+ if (activeVoteAmount.lt(balance)) {
532
+ actualTrackBalances.set(trackId, balance.minus(activeVoteAmount));
533
+ } else {
534
+ actualTrackBalances.set(trackId, balance);
535
+ }
536
+ } else {
537
+ actualTrackBalances.set(trackId, balance);
538
+ }
539
+ }
540
+
541
+ // Sort by actual unlockable balances
542
+ const sortedBalances = Array.from(actualTrackBalances.entries()).sort((a, b) => b[1].comparedTo(a[1]));
543
+ let totalUnlockable = new _bignumber.default(0);
544
+ if (unlockableTrackIds.length > 0) {
545
+ const unlockableBalances = unlockableTrackIds.map(trackId => actualTrackBalances.get(trackId) || new _bignumber.default(0)).sort((a, b) => b.comparedTo(a));
546
+ const maxUnlockableBalance = unlockableBalances[0];
547
+ const lockedTracks = sortedBalances.filter(_ref2 => {
548
+ let [trackId] = _ref2;
549
+ return !unlockableTrackIds.includes(trackId);
550
+ });
551
+ const worstLockedBalance = lockedTracks.length > 0 ? lockedTracks[0][1] : new _bignumber.default(0);
552
+ totalUnlockable = maxUnlockableBalance.minus(worstLockedBalance);
553
+ if (totalUnlockable.lt(0)) {
554
+ totalUnlockable = new _bignumber.default(0);
555
+ }
556
+ }
557
+ return {
558
+ unlockableTrackIds,
559
+ totalUnlockable
560
+ };
561
+ }
562
+ }
563
+ exports.default = BaseOpenGovHandler;