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