@wireio/stake 0.0.6 → 0.1.1
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/README.md +260 -13
- package/lib/stake.browser.js +4861 -4218
- package/lib/stake.browser.js.map +1 -1
- package/lib/stake.d.ts +434 -6484
- package/lib/stake.js +5059 -4371
- package/lib/stake.js.map +1 -1
- package/lib/stake.m.js +4861 -4218
- package/lib/stake.m.js.map +1 -1
- package/package.json +2 -2
- package/src/assets/solana/idl/liqsol_core.json +4239 -0
- package/src/assets/solana/idl/liqsol_token.json +183 -0
- package/src/assets/solana/idl/validator_leaderboard.json +296 -250
- package/src/assets/solana/types/liqsol_core.ts +4245 -0
- package/src/assets/solana/types/liqsol_token.ts +189 -0
- package/src/assets/solana/types/validator_leaderboard.ts +296 -250
- package/src/index.ts +2 -5
- package/src/networks/ethereum/contract.ts +138 -36
- package/src/networks/ethereum/ethereum.ts +167 -38
- package/src/networks/ethereum/types.ts +32 -1
- package/src/networks/solana/clients/deposit.client.ts +92 -139
- package/src/networks/solana/clients/distribution.client.ts +302 -178
- package/src/networks/solana/clients/leaderboard.client.ts +40 -160
- package/src/networks/solana/constants.ts +238 -69
- package/src/networks/solana/program.ts +27 -93
- package/src/networks/solana/solana.ts +181 -36
- package/src/networks/solana/types.ts +47 -0
- package/src/networks/solana/utils.ts +522 -93
- package/src/scripts/fetch-artifacts.sh +24 -0
- package/src/scripts/tsconfig.json +17 -0
- package/src/staker/staker.ts +35 -30
- package/src/staker/types.ts +25 -22
- package/src/assets/solana/idl/deposit.json +0 -260
- package/src/assets/solana/idl/distribution.json +0 -736
- package/src/assets/solana/idl/liq_sol_token.json +0 -275
- package/src/assets/solana/idl/stake_controller.json +0 -1788
- package/src/assets/solana/idl/stake_registry.json +0 -435
- package/src/assets/solana/idl/treasury.json +0 -336
- package/src/assets/solana/idl/validator_registry.json +0 -418
- package/src/assets/solana/idl/yield_oracle.json +0 -32
- package/src/assets/solana/types/deposit.ts +0 -266
- package/src/assets/solana/types/distribution.ts +0 -742
- package/src/assets/solana/types/liq_sol_token.ts +0 -281
- package/src/assets/solana/types/stake_controller.ts +0 -1794
- package/src/assets/solana/types/stake_registry.ts +0 -441
- package/src/assets/solana/types/treasury.ts +0 -342
- package/src/assets/solana/types/validator_registry.ts +0 -424
- package/src/assets/solana/types/yield_oracle.ts +0 -38
- package/src/utils.ts +0 -9
|
@@ -1,230 +1,354 @@
|
|
|
1
|
-
|
|
2
|
-
import { AnchorProvider, Program, BN, web3 } from '@coral-xyz/anchor';
|
|
1
|
+
import { AnchorProvider, Program } from '@coral-xyz/anchor';
|
|
3
2
|
import {
|
|
3
|
+
ParsedAccountData,
|
|
4
4
|
PublicKey,
|
|
5
|
+
SYSVAR_INSTRUCTIONS_PUBKEY,
|
|
5
6
|
Transaction,
|
|
6
|
-
TransactionInstruction,
|
|
7
7
|
SystemProgram,
|
|
8
|
-
VersionedTransaction,
|
|
9
8
|
} from '@solana/web3.js';
|
|
10
9
|
import {
|
|
11
10
|
TOKEN_2022_PROGRAM_ID,
|
|
12
|
-
|
|
13
|
-
getAssociatedTokenAddressSync,
|
|
11
|
+
getAssociatedTokenAddress,
|
|
14
12
|
} from '@solana/spl-token';
|
|
13
|
+
|
|
14
|
+
import { SolanaProgramService } from '../program';
|
|
15
|
+
import type { LiqsolCore } from '../../../assets/solana/types/liqsol_core';
|
|
15
16
|
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
LIQSOL_TOKEN_PROGRAM_ID,
|
|
17
|
+
deriveBucketAuthorityPda,
|
|
18
|
+
deriveDistributionStatePda,
|
|
19
|
+
deriveLiqsolMintPda,
|
|
20
|
+
derivePayRateHistoryPda,
|
|
21
|
+
deriveUserRecordPda,
|
|
22
22
|
} from '../constants';
|
|
23
|
-
import {
|
|
24
|
-
deriveDistributionStatePDA,
|
|
25
|
-
deriveUserRecordPDA,
|
|
26
|
-
deriveLiqsolMintAuthorityPDA,
|
|
27
|
-
} from '../utils';
|
|
23
|
+
import type { CorrectRegisterBuildResult, DistributionState, MismatchCandidate, ParsedAccountInfo, UserRecord } from '../types';
|
|
28
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Distribution client – wraps the distribution portion of the liqsol_core
|
|
27
|
+
* program. Responsible for:
|
|
28
|
+
* - Reading DistributionState + UserRecord
|
|
29
|
+
* - Computing mismatch candidates (tracked > actual)
|
|
30
|
+
* - Building a "correct then register" transaction for the caller
|
|
31
|
+
*
|
|
32
|
+
* Aligned with the on-chain `update_user` script:
|
|
33
|
+
* - Single `updateUser()` entrypoint that:
|
|
34
|
+
* * Can create userRecord if missing
|
|
35
|
+
* * Reconciles tracked vs actual using user, userAta, bucket, pay-rate
|
|
36
|
+
*/
|
|
29
37
|
export class DistributionClient {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
/** Wrapped Anchor Program for Distribution */
|
|
33
|
-
private get program(): Program<Distribution> {
|
|
34
|
-
const idlWithAddress = {
|
|
35
|
-
...JSON.parse(JSON.stringify(DistributionIDL)),
|
|
36
|
-
address: DISTRIBUTION_PROGRAM_ID.toString(),
|
|
37
|
-
};
|
|
38
|
-
return new Program(idlWithAddress as any, this.provider) as Program<Distribution>;
|
|
39
|
-
}
|
|
38
|
+
private program: Program<LiqsolCore>;
|
|
40
39
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
constructor(private provider: AnchorProvider) {
|
|
41
|
+
const svc = new SolanaProgramService(provider);
|
|
42
|
+
this.program = svc.getProgram('liqsolCore');
|
|
44
43
|
}
|
|
45
44
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
return deriveUserRecordPDA(user)[0];
|
|
45
|
+
get connection() {
|
|
46
|
+
return this.provider.connection;
|
|
49
47
|
}
|
|
50
48
|
|
|
51
|
-
/**
|
|
52
|
-
|
|
53
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Fetch the global distribution state account.
|
|
51
|
+
*/
|
|
52
|
+
async getDistributionState(): Promise<DistributionState | null> {
|
|
53
|
+
const pda = deriveDistributionStatePda();
|
|
54
|
+
try {
|
|
55
|
+
// IDL account name: "distributionState"
|
|
56
|
+
return await this.program.account.distributionState.fetch(pda);
|
|
57
|
+
} catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
54
60
|
}
|
|
55
61
|
|
|
56
|
-
/**
|
|
57
|
-
|
|
62
|
+
/**
|
|
63
|
+
* Fetch a user's distribution userRecord (or null if missing).
|
|
64
|
+
*/
|
|
65
|
+
async getUserRecord(user: PublicKey): Promise<UserRecord | null> {
|
|
66
|
+
const pda = deriveUserRecordPda(user);
|
|
58
67
|
try {
|
|
59
|
-
return await this.program.account.userRecord.
|
|
60
|
-
this.deriveUserRecordPDA(user)
|
|
61
|
-
);
|
|
68
|
+
return await this.program.account.userRecord.fetchNullable(pda);
|
|
62
69
|
} catch {
|
|
63
70
|
return null;
|
|
64
71
|
}
|
|
65
72
|
}
|
|
66
73
|
|
|
67
|
-
/**
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
systemProgram: SystemProgram.programId,
|
|
76
|
-
rent: web3.SYSVAR_RENT_PUBKEY,
|
|
77
|
-
} as any)
|
|
78
|
-
.instruction();
|
|
79
|
-
|
|
80
|
-
return new Transaction().add(ix);
|
|
81
|
-
}
|
|
74
|
+
/**
|
|
75
|
+
* Helper: get actual liqSOL balances for all token holders.
|
|
76
|
+
*
|
|
77
|
+
* Returns a map: owner (base58) -> actual balance (BigInt)
|
|
78
|
+
*/
|
|
79
|
+
private async getActualBalancesByOwner(): Promise<Map<string, bigint>> {
|
|
80
|
+
const liqsolMint = deriveLiqsolMintPda();
|
|
81
|
+
const mintStr = liqsolMint.toBase58();
|
|
82
82
|
|
|
83
|
-
|
|
84
|
-
async buildUpdateUserTransaction(user: PublicKey): Promise<Transaction> {
|
|
85
|
-
const statePda = this.deriveStatePDA();
|
|
86
|
-
const userPda = this.deriveUserRecordPDA(user);
|
|
87
|
-
|
|
88
|
-
// Fetch state to get the liqSOL mint address
|
|
89
|
-
const state: any = await this.getState();
|
|
90
|
-
const liqsolMint: PublicKey = state.liqsolMint;
|
|
91
|
-
const userAta = getAssociatedTokenAddressSync(
|
|
92
|
-
liqsolMint,
|
|
93
|
-
user,
|
|
94
|
-
false,
|
|
83
|
+
const accounts = await this.connection.getParsedProgramAccounts(
|
|
95
84
|
TOKEN_2022_PROGRAM_ID,
|
|
96
|
-
|
|
85
|
+
{
|
|
86
|
+
filters: [
|
|
87
|
+
// SPL token layout: mint at offset 0
|
|
88
|
+
{ memcmp: { offset: 0, bytes: mintStr } },
|
|
89
|
+
],
|
|
90
|
+
commitment: 'confirmed',
|
|
91
|
+
},
|
|
97
92
|
);
|
|
98
93
|
|
|
99
|
-
const
|
|
100
|
-
[Buffer.from('stake_controller')],
|
|
101
|
-
STAKE_CONTROLLER_PROGRAM_ID
|
|
102
|
-
);
|
|
94
|
+
const byOwner = new Map<string, bigint>();
|
|
103
95
|
|
|
104
|
-
const
|
|
105
|
-
.
|
|
106
|
-
.
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
} as any)
|
|
118
|
-
.instruction();
|
|
96
|
+
for (const acct of accounts) {
|
|
97
|
+
const data = acct.account.data as ParsedAccountData;
|
|
98
|
+
const parsed = data.parsed;
|
|
99
|
+
if (!parsed || parsed.type !== 'account') continue;
|
|
100
|
+
|
|
101
|
+
const info: ParsedAccountInfo = parsed.info;
|
|
102
|
+
const ownerStr = info.owner;
|
|
103
|
+
const amountStr = info.tokenAmount.amount;
|
|
104
|
+
const amount = BigInt(amountStr);
|
|
105
|
+
|
|
106
|
+
const prev = byOwner.get(ownerStr) ?? BigInt(0);
|
|
107
|
+
byOwner.set(ownerStr, prev + amount);
|
|
108
|
+
}
|
|
119
109
|
|
|
120
|
-
return
|
|
110
|
+
return byOwner;
|
|
121
111
|
}
|
|
122
112
|
|
|
123
|
-
/**
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
);
|
|
113
|
+
/**
|
|
114
|
+
* Helper: get tracked balances from all userRecord accounts,
|
|
115
|
+
* keyed by *actual wallet owner*, not the userRecord PDA.
|
|
116
|
+
*
|
|
117
|
+
* userRecord struct:
|
|
118
|
+
* - userAta: pubkey
|
|
119
|
+
* - trackedBalance: u64
|
|
120
|
+
* - claimBalance: u64
|
|
121
|
+
* - lastClaimTimestamp: i64
|
|
122
|
+
* - bump: u8
|
|
123
|
+
*/
|
|
124
|
+
private async getTrackedBalances(): Promise<
|
|
125
|
+
Map<string, { owner: PublicKey; tracked: bigint }>
|
|
126
|
+
> {
|
|
127
|
+
const records = await this.program.account.userRecord.all();
|
|
128
|
+
const map = new Map<string, { owner: PublicKey; tracked: bigint }>();
|
|
138
129
|
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
);
|
|
143
|
-
const [controllerAuthPda] = PublicKey.findProgramAddressSync(
|
|
144
|
-
[Buffer.from('stake_authority')],
|
|
145
|
-
STAKE_CONTROLLER_PROGRAM_ID
|
|
146
|
-
);
|
|
130
|
+
for (const r of records) {
|
|
131
|
+
const ur = r.account as UserRecord;
|
|
132
|
+
const userAta = ur.userAta as PublicKey;
|
|
147
133
|
|
|
148
|
-
|
|
149
|
-
.
|
|
150
|
-
.
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
.instruction();
|
|
134
|
+
// Resolve the *wallet* that owns this ATA
|
|
135
|
+
const ataInfo = await this.connection.getParsedAccountInfo(userAta);
|
|
136
|
+
const parsedData = ataInfo.value?.data as ParsedAccountData | undefined;
|
|
137
|
+
if (!parsedData) continue;
|
|
138
|
+
|
|
139
|
+
const parsed = parsedData.parsed;
|
|
140
|
+
if (!parsed || (parsed as any).type !== 'account') continue;
|
|
141
|
+
|
|
142
|
+
const info: ParsedAccountInfo = parsed.info;
|
|
143
|
+
const ownerStr = info.owner;
|
|
144
|
+
const owner = new PublicKey(ownerStr);
|
|
145
|
+
|
|
146
|
+
const tracked = BigInt(ur.trackedBalance.toString());
|
|
147
|
+
map.set(ownerStr, { owner, tracked });
|
|
148
|
+
}
|
|
164
149
|
|
|
165
|
-
return
|
|
150
|
+
return map;
|
|
166
151
|
}
|
|
167
152
|
|
|
168
|
-
/**
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
153
|
+
/**
|
|
154
|
+
* Discover all mismatch candidates where tracked > actual.
|
|
155
|
+
*
|
|
156
|
+
* - actual balances are derived from token accounts for the liqSOL mint
|
|
157
|
+
* - tracked balances come from Distribution.userRecord
|
|
158
|
+
*/
|
|
159
|
+
private async deriveMismatchCandidates(): Promise<MismatchCandidate[]> {
|
|
160
|
+
const [actualByOwner, trackedByOwner] = await Promise.all([
|
|
161
|
+
this.getActualBalancesByOwner(),
|
|
162
|
+
this.getTrackedBalances(),
|
|
163
|
+
]);
|
|
164
|
+
|
|
165
|
+
const out: MismatchCandidate[] = [];
|
|
166
|
+
|
|
167
|
+
for (const [ownerStr, { owner, tracked }] of trackedByOwner.entries()) {
|
|
168
|
+
const actual = actualByOwner.get(ownerStr) ?? BigInt(0);
|
|
169
|
+
const delta = tracked - actual;
|
|
170
|
+
if (delta > BigInt(0)) {
|
|
171
|
+
out.push({ owner, actual, tracked, delta });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Largest discrepancy first
|
|
176
|
+
out.sort((a, b) => (b.delta > a.delta ? 1 : b.delta < a.delta ? -1 : 0));
|
|
177
|
+
return out;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Canonical helper to build an `updateUser` instruction for a given user,
|
|
182
|
+
* matching the standalone update-user.ts script.
|
|
183
|
+
*/
|
|
184
|
+
private async buildUpdateUserIx(targetUser: PublicKey) {
|
|
185
|
+
const walletPk = this.provider.wallet.publicKey;
|
|
186
|
+
|
|
187
|
+
const distributionStatePDA = deriveDistributionStatePda();
|
|
188
|
+
const userRecordPDA = deriveUserRecordPda(targetUser);
|
|
189
|
+
const liqsolMintPDA = deriveLiqsolMintPda();
|
|
190
|
+
const payRateHistoryPDA = derivePayRateHistoryPda();
|
|
191
|
+
const bucketAuthorityPDA = deriveBucketAuthorityPda();
|
|
192
|
+
|
|
193
|
+
const userAta = await getAssociatedTokenAddress(
|
|
194
|
+
liqsolMintPDA,
|
|
195
|
+
targetUser,
|
|
179
196
|
false,
|
|
180
197
|
TOKEN_2022_PROGRAM_ID,
|
|
181
|
-
ASSOCIATED_TOKEN_PROGRAM_ID
|
|
182
198
|
);
|
|
183
|
-
const [mintAuthPda] = deriveLiqsolMintAuthorityPDA();
|
|
184
199
|
|
|
185
|
-
const
|
|
186
|
-
|
|
200
|
+
const bucketTokenAccount = await getAssociatedTokenAddress(
|
|
201
|
+
liqsolMintPDA,
|
|
202
|
+
bucketAuthorityPDA,
|
|
203
|
+
true, // allowOwnerOffCurve
|
|
204
|
+
TOKEN_2022_PROGRAM_ID,
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
return (this.program.methods as any)
|
|
208
|
+
.updateUser()
|
|
187
209
|
.accounts({
|
|
188
|
-
user,
|
|
210
|
+
user: targetUser,
|
|
189
211
|
userAta,
|
|
190
|
-
userRecord:
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
instructionsSysvar:
|
|
196
|
-
yieldOracleProgram: YIELD_ORACLE_PROGRAM_ID,
|
|
212
|
+
userRecord: userRecordPDA,
|
|
213
|
+
authority: walletPk,
|
|
214
|
+
payer: walletPk,
|
|
215
|
+
distributionState: distributionStatePDA,
|
|
216
|
+
liqsolMint: liqsolMintPDA,
|
|
217
|
+
instructionsSysvar: SYSVAR_INSTRUCTIONS_PUBKEY,
|
|
197
218
|
tokenProgram: TOKEN_2022_PROGRAM_ID,
|
|
198
|
-
|
|
219
|
+
bucketAuthority: bucketAuthorityPDA,
|
|
220
|
+
bucketTokenAccount,
|
|
221
|
+
payRateHistory: payRateHistoryPDA,
|
|
199
222
|
systemProgram: SystemProgram.programId,
|
|
200
|
-
}
|
|
223
|
+
})
|
|
201
224
|
.instruction();
|
|
202
|
-
|
|
203
|
-
return new Transaction().add(ix);
|
|
204
225
|
}
|
|
205
226
|
|
|
206
|
-
/**
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
227
|
+
/**
|
|
228
|
+
* Build the "correct & register" transaction.
|
|
229
|
+
*
|
|
230
|
+
* - Fetches DistributionState + all userRecords + token holders
|
|
231
|
+
* - Computes caller's untracked amount (actual - tracked)
|
|
232
|
+
* - If DistributionState.availableBalance already covers that, we only
|
|
233
|
+
* send updateUser(caller).
|
|
234
|
+
* - Otherwise we select top mismatch candidates until their freed deltas
|
|
235
|
+
* cover the shortfall, then build updateUser(target) for each,
|
|
236
|
+
* followed by updateUser(caller).
|
|
237
|
+
*
|
|
238
|
+
* NOTE:
|
|
239
|
+
* - This no longer uses a separate updateSpecificUser; the single
|
|
240
|
+
* updateUser entrypoint accepts any `user` as long as authority/payer
|
|
241
|
+
* are valid, per your script.
|
|
242
|
+
*/
|
|
243
|
+
async buildCorrectRegisterTx(opts: {
|
|
244
|
+
amount?: bigint;
|
|
245
|
+
maxCandidates?: number;
|
|
246
|
+
} = {}): Promise<CorrectRegisterBuildResult> {
|
|
247
|
+
const walletPk = this.provider.wallet.publicKey;
|
|
248
|
+
const callerStr = walletPk.toBase58();
|
|
249
|
+
|
|
250
|
+
const [distState, actualByOwner, trackedByOwner] = await Promise.all([
|
|
251
|
+
this.getDistributionState(),
|
|
252
|
+
this.getActualBalancesByOwner(),
|
|
253
|
+
this.getTrackedBalances(),
|
|
254
|
+
]);
|
|
255
|
+
|
|
256
|
+
if (!distState) {
|
|
257
|
+
return {
|
|
258
|
+
needToRegister: false,
|
|
259
|
+
canSucceed: false,
|
|
260
|
+
reason: 'DistributionState not initialized',
|
|
261
|
+
transaction: undefined,
|
|
262
|
+
plan: { deficit: BigInt(0), willFree: BigInt(0), selected: [] },
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const availableBalance = BigInt(distState.availableBalance.toString());
|
|
267
|
+
|
|
268
|
+
const trackedEntry =
|
|
269
|
+
trackedByOwner.get(callerStr) ?? {
|
|
270
|
+
owner: walletPk,
|
|
271
|
+
tracked: BigInt(0),
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
const actual = actualByOwner.get(callerStr) ?? BigInt(0);
|
|
275
|
+
const tracked = trackedEntry.tracked;
|
|
276
|
+
const untracked = actual - tracked;
|
|
216
277
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
278
|
+
// Nothing to register
|
|
279
|
+
if (untracked <= BigInt(0)) {
|
|
280
|
+
return {
|
|
281
|
+
needToRegister: false,
|
|
282
|
+
canSucceed: true,
|
|
283
|
+
reason: 'No untracked liqSOL to register',
|
|
284
|
+
transaction: undefined,
|
|
285
|
+
plan: { deficit: BigInt(0), willFree: BigInt(0), selected: [] },
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Optional user-specified cap on how much to register.
|
|
290
|
+
const targetRegister =
|
|
291
|
+
opts.amount && opts.amount > BigInt(0) ? opts.amount : untracked;
|
|
292
|
+
|
|
293
|
+
// Simple case: availableBalance already covers what we want to register
|
|
294
|
+
if (availableBalance >= targetRegister) {
|
|
295
|
+
const tx = new Transaction();
|
|
296
|
+
const ix = await this.buildUpdateUserIx(walletPk); // caller only
|
|
297
|
+
tx.add(ix);
|
|
298
|
+
|
|
299
|
+
return {
|
|
300
|
+
needToRegister: true,
|
|
301
|
+
canSucceed: true,
|
|
302
|
+
transaction: tx,
|
|
303
|
+
plan: { deficit: BigInt(0), willFree: BigInt(0), selected: [] },
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Need to free up more availableBalance by correcting others.
|
|
308
|
+
const deficit = targetRegister - availableBalance;
|
|
309
|
+
|
|
310
|
+
const allCandidates = await this.deriveMismatchCandidates();
|
|
311
|
+
const maxCandidates = opts.maxCandidates ?? 10;
|
|
312
|
+
|
|
313
|
+
const selected: MismatchCandidate[] = [];
|
|
314
|
+
let willFree = BigInt(0);
|
|
315
|
+
|
|
316
|
+
for (const c of allCandidates) {
|
|
317
|
+
if (c.owner.equals(walletPk)) continue; // don't self-correct here
|
|
318
|
+
selected.push(c);
|
|
319
|
+
willFree += c.delta;
|
|
320
|
+
if (willFree >= deficit || selected.length >= maxCandidates) break;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (willFree < deficit) {
|
|
324
|
+
return {
|
|
325
|
+
needToRegister: true,
|
|
326
|
+
canSucceed: false,
|
|
327
|
+
reason: 'Not enough mismatched candidates to cover deficit',
|
|
328
|
+
transaction: undefined,
|
|
329
|
+
plan: { deficit, willFree, selected },
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const tx = new Transaction();
|
|
334
|
+
|
|
335
|
+
// 1) Correct selected mismatched users.
|
|
336
|
+
for (const c of selected) {
|
|
337
|
+
const ix = await this.buildUpdateUserIx(c.owner);
|
|
338
|
+
tx.add(ix);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// 2) Register caller (updateUser(caller)).
|
|
342
|
+
{
|
|
343
|
+
const ix = await this.buildUpdateUserIx(walletPk);
|
|
344
|
+
tx.add(ix);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return {
|
|
348
|
+
needToRegister: true,
|
|
349
|
+
canSucceed: true,
|
|
350
|
+
transaction: tx,
|
|
351
|
+
plan: { deficit, willFree, selected },
|
|
352
|
+
};
|
|
229
353
|
}
|
|
230
354
|
}
|