@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.
- package/background/KoniTypes.d.ts +11 -0
- package/background/KoniTypes.js +3 -0
- package/cjs/background/KoniTypes.js +3 -0
- package/cjs/koni/background/handlers/Extension.js +62 -0
- package/cjs/koni/background/handlers/State.js +5 -2
- package/cjs/koni/background/handlers/Tabs.js +11 -4
- package/cjs/packageInfo.js +1 -1
- package/cjs/services/balance-service/transfer/token.js +34 -3
- package/cjs/services/chain-service/constants.js +17 -5
- package/cjs/services/chain-service/utils/index.js +13 -5
- package/cjs/services/chain-service/utils/patch.js +1 -1
- package/cjs/services/event-service/index.js +1 -0
- package/cjs/services/open-gov/handler.js +563 -0
- package/cjs/services/open-gov/index.js +273 -0
- package/cjs/services/open-gov/interface.js +28 -0
- package/cjs/services/open-gov/utils.js +66 -0
- package/cjs/services/storage-service/DatabaseService.js +19 -1
- package/cjs/services/storage-service/databases/index.js +3 -0
- package/cjs/services/storage-service/db-stores/GovLockedInfoStore.js +35 -0
- package/cjs/services/transaction-service/helpers/index.js +6 -0
- package/cjs/services/transaction-service/index.js +43 -0
- package/cjs/services/transaction-service/utils.js +3 -3
- package/cjs/utils/account/transform.js +5 -4
- package/koni/background/handlers/Extension.d.ts +4 -0
- package/koni/background/handlers/Extension.js +62 -0
- package/koni/background/handlers/State.d.ts +2 -0
- package/koni/background/handlers/State.js +5 -2
- package/koni/background/handlers/Tabs.js +11 -4
- package/package.json +31 -6
- package/packageInfo.js +1 -1
- package/services/balance-service/transfer/token.d.ts +4 -0
- package/services/balance-service/transfer/token.js +31 -1
- package/services/chain-service/constants.d.ts +9 -0
- package/services/chain-service/constants.js +14 -3
- package/services/chain-service/utils/index.js +13 -5
- package/services/chain-service/utils/patch.d.ts +1 -1
- package/services/chain-service/utils/patch.js +1 -1
- package/services/event-service/index.d.ts +1 -0
- package/services/event-service/index.js +1 -0
- package/services/event-service/types.d.ts +1 -0
- package/services/open-gov/handler.d.ts +27 -0
- package/services/open-gov/handler.js +547 -0
- package/services/open-gov/index.d.ts +45 -0
- package/services/open-gov/index.js +265 -0
- package/services/open-gov/interface.d.ts +141 -0
- package/services/open-gov/interface.js +21 -0
- package/services/open-gov/utils.d.ts +14 -0
- package/services/open-gov/utils.js +52 -0
- package/services/storage-service/DatabaseService.d.ts +7 -0
- package/services/storage-service/DatabaseService.js +19 -1
- package/services/storage-service/databases/index.d.ts +2 -0
- package/services/storage-service/databases/index.js +3 -0
- package/services/storage-service/db-stores/GovLockedInfoStore.d.ts +10 -0
- package/services/storage-service/db-stores/GovLockedInfoStore.js +27 -0
- package/services/transaction-service/helpers/index.js +6 -0
- package/services/transaction-service/index.js +43 -0
- package/services/transaction-service/utils.js +3 -3
- 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 {};
|