@wireio/stake 0.0.5 → 0.1.0
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 +203 -13
- package/lib/stake.browser.js +2803 -3331
- package/lib/stake.browser.js.map +1 -1
- package/lib/stake.d.ts +377 -6260
- package/lib/stake.js +2940 -3478
- package/lib/stake.js.map +1 -1
- package/lib/stake.m.js +2803 -3331
- package/lib/stake.m.js.map +1 -1
- package/package.json +2 -2
- package/src/assets/solana/idl/deposit.json +46 -10
- package/src/assets/solana/idl/distribution.json +40 -8
- package/src/assets/solana/idl/liq_sol_token.json +25 -2
- package/src/assets/solana/idl/mint_helper.json +110 -0
- package/src/assets/solana/idl/read_tracked_balance.json +140 -0
- package/src/assets/solana/idl/stake_controller.json +1141 -780
- package/src/assets/solana/idl/treasury.json +1 -227
- package/src/assets/solana/idl/validator_leaderboard.json +88 -47
- package/src/assets/solana/idl/validator_registry.json +115 -46
- package/src/assets/solana/idl/yield_oracle.json +1 -1
- package/src/assets/solana/types/deposit.ts +46 -10
- package/src/assets/solana/types/distribution.ts +40 -8
- package/src/assets/solana/types/liq_sol_token.ts +25 -2
- package/src/assets/solana/types/mint_helper.ts +116 -0
- package/src/assets/solana/types/read_tracked_balance.ts +146 -0
- package/src/assets/solana/types/stake_controller.ts +1141 -780
- package/src/assets/solana/types/treasury.ts +1 -227
- package/src/assets/solana/types/validator_leaderboard.ts +88 -47
- package/src/assets/solana/types/validator_registry.ts +115 -46
- package/src/assets/solana/types/yield_oracle.ts +1 -1
- package/src/index.ts +3 -4
- package/src/networks/ethereum/ethereum.ts +2 -2
- package/src/networks/solana/clients/deposit.client.ts +71 -80
- package/src/networks/solana/clients/distribution.client.ts +392 -141
- package/src/networks/solana/clients/leaderboard.client.ts +82 -107
- package/src/networks/solana/constants.ts +141 -56
- package/src/networks/solana/program.ts +36 -89
- package/src/networks/solana/solana.ts +173 -36
- package/src/networks/solana/types.ts +57 -0
- package/src/scripts/fetch-artifacts.sh +24 -0
- package/src/staker/staker.ts +32 -28
- package/src/staker/types.ts +25 -21
- package/src/assets/solana/idl/stake_registry.json +0 -435
- package/src/networks/solana/utils.ts +0 -122
- /package/src/{utils.ts → common/utils.ts} +0 -0
|
@@ -1,93 +1,134 @@
|
|
|
1
|
-
|
|
2
|
-
import { AnchorProvider, Program, BN, web3 } from '@coral-xyz/anchor';
|
|
1
|
+
import { AnchorProvider, Program, BN } from '@coral-xyz/anchor';
|
|
3
2
|
import {
|
|
4
3
|
PublicKey,
|
|
4
|
+
SystemProgram,
|
|
5
|
+
SYSVAR_INSTRUCTIONS_PUBKEY,
|
|
5
6
|
Transaction,
|
|
6
7
|
TransactionInstruction,
|
|
7
|
-
SystemProgram,
|
|
8
|
-
VersionedTransaction,
|
|
9
8
|
} from '@solana/web3.js';
|
|
10
9
|
import {
|
|
11
|
-
|
|
12
|
-
ASSOCIATED_TOKEN_PROGRAM_ID,
|
|
10
|
+
getAccount,
|
|
13
11
|
getAssociatedTokenAddressSync,
|
|
14
12
|
} from '@solana/spl-token';
|
|
13
|
+
|
|
14
|
+
import { SolanaProgramService } from '../program';
|
|
15
15
|
import {
|
|
16
|
-
|
|
16
|
+
TOKEN_2022_PROGRAM_ID,
|
|
17
|
+
ASSOCIATED_TOKEN_PROGRAM_ID,
|
|
17
18
|
STAKE_CONTROLLER_PROGRAM_ID,
|
|
18
|
-
DistributionIDL,
|
|
19
|
-
Distribution,
|
|
20
19
|
YIELD_ORACLE_PROGRAM_ID,
|
|
21
|
-
LIQSOL_TOKEN_PROGRAM_ID,
|
|
22
|
-
} from '../constants';
|
|
23
|
-
import {
|
|
24
20
|
deriveDistributionStatePDA,
|
|
25
21
|
deriveUserRecordPDA,
|
|
26
|
-
|
|
27
|
-
|
|
22
|
+
deriveStakeControllerStatePDA,
|
|
23
|
+
deriveBucketAuthorityPDA,
|
|
24
|
+
derivePayRateHistoryPDA,
|
|
25
|
+
deriveStakeControllerAuthorityPDA,
|
|
26
|
+
} from '../constants';
|
|
27
|
+
import { Distribution } from '../../../assets/solana/types/distribution';
|
|
28
|
+
import { MismatchCandidate, CorrectAndRegisterBuild, CorrectionPlan } from '../types';
|
|
28
29
|
|
|
29
30
|
export class DistributionClient {
|
|
30
|
-
|
|
31
|
+
private programs: SolanaProgramService;
|
|
31
32
|
|
|
32
|
-
|
|
33
|
+
constructor(private provider: AnchorProvider) {
|
|
34
|
+
this.programs = new SolanaProgramService(provider);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Anchor Program<Distribution> (address comes from SolanaProgramService) */
|
|
33
38
|
private get program(): Program<Distribution> {
|
|
34
|
-
|
|
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
|
+
return this.programs.getProgram('distribution') as Program<Distribution>;
|
|
39
40
|
}
|
|
40
41
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
// ───────────────────────────────────────────────────────────────────────────────
|
|
43
|
+
// BASIC READS
|
|
44
|
+
// ───────────────────────────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Fetch Distribution global state.
|
|
48
|
+
*/
|
|
49
|
+
async getDistributionState(): Promise<any> {
|
|
50
|
+
const [pda] = deriveDistributionStatePDA();
|
|
51
|
+
return this.program.account.distributionState.fetch(pda);
|
|
44
52
|
}
|
|
45
53
|
|
|
46
|
-
/**
|
|
47
|
-
|
|
48
|
-
|
|
54
|
+
/**
|
|
55
|
+
* Fetch a user’s UserRecord or null if it doesn’t exist yet.
|
|
56
|
+
*/
|
|
57
|
+
async getUserRecord(user: PublicKey): Promise<any | null> {
|
|
58
|
+
const [pda] = deriveUserRecordPDA(user);
|
|
59
|
+
try { return await this.program.account.userRecord.fetch(pda); }
|
|
60
|
+
catch { return null; }
|
|
49
61
|
}
|
|
50
62
|
|
|
51
|
-
/**
|
|
52
|
-
|
|
53
|
-
|
|
63
|
+
/**
|
|
64
|
+
* Read protocol-tracked balance for a user (from userRecord).
|
|
65
|
+
* Returns (amount, decimals).
|
|
66
|
+
* @default decimals=9
|
|
67
|
+
*/
|
|
68
|
+
async getTrackedBalance(user: PublicKey): Promise<{ amount: bigint; decimals: number }> {
|
|
69
|
+
const rec = await this.getUserRecord(user);
|
|
70
|
+
if (!rec) return { amount: ZERO, decimals: 9 };
|
|
71
|
+
const amountStr: string = rec.trackedBalance?.toString?.() ?? '0';
|
|
72
|
+
return { amount: BigInt(amountStr), decimals: 9 };
|
|
54
73
|
}
|
|
55
74
|
|
|
56
|
-
/**
|
|
57
|
-
|
|
75
|
+
/**
|
|
76
|
+
* Read *actual* liqSOL token balance (ATA) for a user.
|
|
77
|
+
* If user has no ATA yet, returns 0.
|
|
78
|
+
* @returns { amount, decimals, ata }
|
|
79
|
+
*/
|
|
80
|
+
async getActualBalance(user: PublicKey): Promise<{ amount: bigint; decimals: number; ata: PublicKey }> {
|
|
81
|
+
const state = await this.getDistributionState();
|
|
82
|
+
const liqsolMint: PublicKey = state.liqsolMint;
|
|
83
|
+
const ata = getAssociatedTokenAddressSync(
|
|
84
|
+
liqsolMint,
|
|
85
|
+
user,
|
|
86
|
+
false,
|
|
87
|
+
TOKEN_2022_PROGRAM_ID,
|
|
88
|
+
ASSOCIATED_TOKEN_PROGRAM_ID
|
|
89
|
+
);
|
|
58
90
|
try {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
91
|
+
const acc = await getAccount(this.provider.connection, ata, 'confirmed', TOKEN_2022_PROGRAM_ID);
|
|
92
|
+
// spl-token returns bigint amount
|
|
93
|
+
return { amount: acc.amount as unknown as bigint, decimals: 9, ata };
|
|
62
94
|
} catch {
|
|
63
|
-
return
|
|
95
|
+
return { amount: ZERO, decimals: 9, ata };
|
|
64
96
|
}
|
|
65
97
|
}
|
|
66
98
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const ix = await this.program.methods
|
|
71
|
-
.initialize()
|
|
72
|
-
.accounts({
|
|
73
|
-
authority: user,
|
|
74
|
-
distributionState: statePda,
|
|
75
|
-
systemProgram: SystemProgram.programId,
|
|
76
|
-
rent: web3.SYSVAR_RENT_PUBKEY,
|
|
77
|
-
} as any)
|
|
78
|
-
.instruction();
|
|
99
|
+
// ───────────────────────────────────────────────────────────────────────────────
|
|
100
|
+
// STATE-CHANGING ACTIONS (single-user)
|
|
101
|
+
// ───────────────────────────────────────────────────────────────────────────────
|
|
79
102
|
|
|
80
|
-
|
|
103
|
+
/**
|
|
104
|
+
* Register / Update a user’s record.
|
|
105
|
+
* - If updating someone else, pass their pubkey as `targetUser` and let the connected wallet be `authority`.
|
|
106
|
+
* - Optionally pass an explicit `authorityUser` if you need to override (must be the connected wallet).
|
|
107
|
+
* Returns the tx signature.
|
|
108
|
+
*/
|
|
109
|
+
/**
|
|
110
|
+
* Register / Update a user’s record with the connected wallet as authority.
|
|
111
|
+
* Keeps a single source of truth via `prepareUpdateUser()`.
|
|
112
|
+
*/
|
|
113
|
+
async updateUser(targetUser: PublicKey): Promise<string> {
|
|
114
|
+
const { builder } = await this.prepareUpdateUser(targetUser);
|
|
115
|
+
return builder.rpc();
|
|
81
116
|
}
|
|
82
117
|
|
|
83
|
-
/** Build
|
|
84
|
-
async
|
|
85
|
-
const
|
|
86
|
-
|
|
118
|
+
/** Build-only variant (returns the IX) so you can batch. */
|
|
119
|
+
async buildUpdateUserIx(targetUser: PublicKey): Promise<TransactionInstruction> {
|
|
120
|
+
const { builder } = await this.prepareUpdateUser(targetUser);
|
|
121
|
+
return builder.instruction();
|
|
122
|
+
}
|
|
87
123
|
|
|
88
|
-
|
|
89
|
-
|
|
124
|
+
/**
|
|
125
|
+
* Claim rewards for a user.
|
|
126
|
+
* @returns tx signature
|
|
127
|
+
*/
|
|
128
|
+
async claimRewards(user: PublicKey): Promise<string> {
|
|
129
|
+
const state = await this.getDistributionState();
|
|
90
130
|
const liqsolMint: PublicKey = state.liqsolMint;
|
|
131
|
+
|
|
91
132
|
const userAta = getAssociatedTokenAddressSync(
|
|
92
133
|
liqsolMint,
|
|
93
134
|
user,
|
|
@@ -96,38 +137,49 @@ export class DistributionClient {
|
|
|
96
137
|
ASSOCIATED_TOKEN_PROGRAM_ID
|
|
97
138
|
);
|
|
98
139
|
|
|
99
|
-
const [
|
|
100
|
-
|
|
101
|
-
|
|
140
|
+
const [userRecordPDA] = deriveUserRecordPDA(user);
|
|
141
|
+
const [distributionStatePDA] = deriveDistributionStatePDA();
|
|
142
|
+
const [bucketAuthority] = deriveBucketAuthorityPDA();
|
|
143
|
+
const [payRateHistory] = derivePayRateHistoryPDA();
|
|
144
|
+
|
|
145
|
+
const bucketTokenAccount = getAssociatedTokenAddressSync(
|
|
146
|
+
liqsolMint,
|
|
147
|
+
bucketAuthority,
|
|
148
|
+
true,
|
|
149
|
+
TOKEN_2022_PROGRAM_ID,
|
|
150
|
+
ASSOCIATED_TOKEN_PROGRAM_ID
|
|
102
151
|
);
|
|
103
152
|
|
|
104
|
-
const
|
|
105
|
-
.
|
|
153
|
+
const sig = await this.program.methods
|
|
154
|
+
.claimRewards()
|
|
106
155
|
.accounts({
|
|
107
156
|
user,
|
|
157
|
+
// @ts-ignore
|
|
108
158
|
userAta,
|
|
109
|
-
userRecord:
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
159
|
+
userRecord: userRecordPDA,
|
|
160
|
+
distributionState: distributionStatePDA,
|
|
161
|
+
liqsolMint,
|
|
162
|
+
stakeControllerProgram: STAKE_CONTROLLER_PROGRAM_ID,
|
|
163
|
+
bucketAuthority,
|
|
164
|
+
bucketTokenAccount,
|
|
165
|
+
payRateHistory,
|
|
114
166
|
tokenProgram: TOKEN_2022_PROGRAM_ID,
|
|
115
|
-
|
|
167
|
+
associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
|
|
116
168
|
systemProgram: SystemProgram.programId,
|
|
117
|
-
}
|
|
118
|
-
.
|
|
169
|
+
})
|
|
170
|
+
.rpc();
|
|
119
171
|
|
|
120
|
-
return
|
|
172
|
+
return sig;
|
|
121
173
|
}
|
|
122
174
|
|
|
123
|
-
/**
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const state: any = await this.getState();
|
|
175
|
+
/**
|
|
176
|
+
* Withdraw liqSOL (amount in base units; e.g. 1e9 = 1 liqSOL).
|
|
177
|
+
* @returns tx signature
|
|
178
|
+
*/
|
|
179
|
+
async withdraw(user: PublicKey, amount: bigint | number): Promise<string> {
|
|
180
|
+
const state = await this.getDistributionState();
|
|
130
181
|
const liqsolMint: PublicKey = state.liqsolMint;
|
|
182
|
+
|
|
131
183
|
const userAta = getAssociatedTokenAddressSync(
|
|
132
184
|
liqsolMint,
|
|
133
185
|
user,
|
|
@@ -136,95 +188,294 @@ export class DistributionClient {
|
|
|
136
188
|
ASSOCIATED_TOKEN_PROGRAM_ID
|
|
137
189
|
);
|
|
138
190
|
|
|
139
|
-
const [
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
);
|
|
143
|
-
const [controllerAuthPda] = PublicKey.findProgramAddressSync(
|
|
144
|
-
[Buffer.from('stake_authority')],
|
|
145
|
-
STAKE_CONTROLLER_PROGRAM_ID
|
|
146
|
-
);
|
|
191
|
+
const [userRecordPDA] = deriveUserRecordPDA(user);
|
|
192
|
+
const [distributionStatePDA] = deriveDistributionStatePDA();
|
|
193
|
+
const [stakeControllerState] = deriveStakeControllerStatePDA();
|
|
194
|
+
const [controllerAuthority] = deriveStakeControllerAuthorityPDA();
|
|
147
195
|
|
|
148
|
-
const
|
|
149
|
-
.withdraw(new BN(amount))
|
|
196
|
+
const sig = await this.program.methods
|
|
197
|
+
.withdraw(new BN(amount.toString()))
|
|
150
198
|
.accounts({
|
|
151
199
|
user,
|
|
200
|
+
// @ts-ignore
|
|
152
201
|
userAta,
|
|
153
|
-
userRecord:
|
|
154
|
-
distributionState:
|
|
202
|
+
userRecord: userRecordPDA,
|
|
203
|
+
distributionState: distributionStatePDA,
|
|
155
204
|
liqsolMint,
|
|
156
205
|
tokenProgram: TOKEN_2022_PROGRAM_ID,
|
|
157
206
|
stakeControllerProgram: STAKE_CONTROLLER_PROGRAM_ID,
|
|
158
|
-
stakeControllerState
|
|
159
|
-
controllerAuthority
|
|
207
|
+
stakeControllerState,
|
|
208
|
+
controllerAuthority,
|
|
160
209
|
yieldOracleProgram: YIELD_ORACLE_PROGRAM_ID,
|
|
161
210
|
systemProgram: SystemProgram.programId,
|
|
162
|
-
}
|
|
163
|
-
.
|
|
211
|
+
})
|
|
212
|
+
.rpc();
|
|
164
213
|
|
|
165
|
-
return
|
|
214
|
+
return sig;
|
|
166
215
|
}
|
|
167
216
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
217
|
+
// ───────────────────────────────────────────────────────────────────────────────
|
|
218
|
+
// INSTRUCTION BUILDERS (for bundling into 1 tx)
|
|
219
|
+
// ───────────────────────────────────────────────────────────────────────────────
|
|
220
|
+
|
|
221
|
+
/** Single prep path used by both send & build. */
|
|
222
|
+
private async prepareUpdateUser(targetUser: PublicKey) {
|
|
223
|
+
const authority = this.provider.wallet?.publicKey;
|
|
224
|
+
if (!authority) throw new Error('Wallet not connected');
|
|
172
225
|
|
|
173
|
-
|
|
174
|
-
const state: any = await this.getState();
|
|
226
|
+
const state = await this.getDistributionState();
|
|
175
227
|
const liqsolMint: PublicKey = state.liqsolMint;
|
|
176
|
-
|
|
228
|
+
|
|
229
|
+
const targetUserAta = getAssociatedTokenAddressSync(
|
|
177
230
|
liqsolMint,
|
|
178
|
-
|
|
231
|
+
targetUser,
|
|
179
232
|
false,
|
|
180
233
|
TOKEN_2022_PROGRAM_ID,
|
|
181
234
|
ASSOCIATED_TOKEN_PROGRAM_ID
|
|
182
235
|
);
|
|
183
|
-
const [mintAuthPda] = deriveLiqsolMintAuthorityPDA();
|
|
184
236
|
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
237
|
+
const [targetUserRecordPDA] = deriveUserRecordPDA(targetUser);
|
|
238
|
+
const [distributionStatePDA] = deriveDistributionStatePDA();
|
|
239
|
+
const [payRateHistory] = derivePayRateHistoryPDA();
|
|
240
|
+
|
|
241
|
+
const accounts = {
|
|
242
|
+
user: targetUser,
|
|
243
|
+
userAta: targetUserAta,
|
|
244
|
+
// @ts-ignore (account name casing differences)
|
|
245
|
+
userRecord: targetUserRecordPDA,
|
|
246
|
+
authority,
|
|
247
|
+
payer: authority,
|
|
248
|
+
distributionState: distributionStatePDA,
|
|
249
|
+
liqsolMint,
|
|
250
|
+
instructionsSysvar: SYSVAR_INSTRUCTIONS_PUBKEY,
|
|
251
|
+
tokenProgram: TOKEN_2022_PROGRAM_ID,
|
|
252
|
+
stakeControllerProgram: STAKE_CONTROLLER_PROGRAM_ID,
|
|
253
|
+
payRateHistory,
|
|
254
|
+
systemProgram: SystemProgram.programId,
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const builder = this.program.methods.updateUser().accounts(accounts as any);
|
|
258
|
+
return { builder, accounts };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ───────────────────────────────────────────────────────────────────────────────
|
|
262
|
+
// ONE-SHOT BUILDER: correct 0..N others, then register self
|
|
263
|
+
// ───────────────────────────────────────────────────────────────────────────────
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Build a single transaction that:
|
|
267
|
+
* 1) If self mismatch < 0 → just correct self (single updateUser).
|
|
268
|
+
* 2) Else (mismatch > 0) → correct top candidates (if needed) to free “available” balance
|
|
269
|
+
* and then register self (updateUser).
|
|
270
|
+
*
|
|
271
|
+
* It refuses to build if it can’t free enough to cover the full positive untracked amount
|
|
272
|
+
* (since updateUser has no partial-amount arg).
|
|
273
|
+
*/
|
|
274
|
+
async buildCorrectRegisterTx(opts?: {
|
|
275
|
+
/** optional override of computed mismatch; may be positive (register) or negative (self-correct) */
|
|
276
|
+
amount?: bigint;
|
|
277
|
+
/** optionally pass pre-fetched candidates to avoid re-reading on UI flows */
|
|
278
|
+
preloadCandidates?: MismatchCandidate[];
|
|
279
|
+
}): Promise<CorrectAndRegisterBuild> {
|
|
280
|
+
const self = this.provider.wallet?.publicKey;
|
|
281
|
+
if (!self) throw new Error('Wallet not connected');
|
|
282
|
+
|
|
283
|
+
const state = await this.getDistributionState();
|
|
284
|
+
const liqsolMint: PublicKey = state.liqsolMint;
|
|
285
|
+
const availableBefore = BigInt(state.availableBalance.toString());
|
|
286
|
+
|
|
287
|
+
// self balances
|
|
288
|
+
const [selfUserRecordPda] = deriveUserRecordPDA(self);
|
|
289
|
+
const { amount: selfTracked } = await this.getTrackedBalance(self);
|
|
290
|
+
const { amount: selfActual, ata: selfUserAta } = await this.getActualBalance(self);
|
|
291
|
+
|
|
292
|
+
// True on-chain mismatch (can be negative or positive)
|
|
293
|
+
const computedMismatch = selfActual - selfTracked; // + = need to register, - = need to correct self
|
|
294
|
+
|
|
295
|
+
// Allow caller to suggest an amount (mainly for planning); clamp to real mismatch
|
|
296
|
+
let desired = opts?.amount ?? computedMismatch;
|
|
297
|
+
if (computedMismatch >= ZERO) {
|
|
298
|
+
// clamp down to computedMismatch
|
|
299
|
+
if (desired < ZERO) desired = ZERO;
|
|
300
|
+
if (desired > computedMismatch) desired = computedMismatch;
|
|
301
|
+
} else {
|
|
302
|
+
// negative side: clamp up to computedMismatch (remember computedMismatch is negative)
|
|
303
|
+
if (desired > ZERO) desired = ZERO;
|
|
304
|
+
if (desired < computedMismatch) desired = computedMismatch;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// CASE A: Negative mismatch → just correct self
|
|
308
|
+
if (desired < ZERO) {
|
|
309
|
+
console.log('Building Correct transaction for self with amount:', desired);
|
|
310
|
+
|
|
311
|
+
const tx = new Transaction().add(await this.buildUpdateUserIx(self));
|
|
312
|
+
return {
|
|
313
|
+
canSucceed: true,
|
|
314
|
+
transaction: tx,
|
|
192
315
|
liqsolMint,
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
316
|
+
needToRegister: desired, // negative indicates self-correction
|
|
317
|
+
availableBefore,
|
|
318
|
+
candidates: [],
|
|
319
|
+
plan: { selected: [], willFree: ZERO, deficit: ZERO },
|
|
320
|
+
accounts: { selfUserRecordPda, selfUserAta },
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Reuse the existing “positive mismatch” path
|
|
325
|
+
const needToRegister = desired; // >= 0
|
|
326
|
+
if (needToRegister === ZERO) {
|
|
327
|
+
return {
|
|
328
|
+
canSucceed: false,
|
|
329
|
+
reason: 'No mismatch to resolve.',
|
|
330
|
+
liqsolMint,
|
|
331
|
+
needToRegister,
|
|
332
|
+
availableBefore,
|
|
333
|
+
candidates: [],
|
|
334
|
+
plan: { selected: [], willFree: ZERO, deficit: ZERO },
|
|
335
|
+
accounts: { selfUserRecordPda, selfUserAta },
|
|
336
|
+
};
|
|
337
|
+
}
|
|
202
338
|
|
|
203
|
-
|
|
339
|
+
// Already enough available → just register self
|
|
340
|
+
if (availableBefore >= needToRegister) {
|
|
341
|
+
console.log('Available balance', availableBefore, 'is enough to register self:', needToRegister);
|
|
342
|
+
const registerIx = await this.buildUpdateUserIx(self);
|
|
343
|
+
const tx = new Transaction().add(registerIx);
|
|
344
|
+
return {
|
|
345
|
+
canSucceed: true,
|
|
346
|
+
transaction: tx,
|
|
347
|
+
liqsolMint,
|
|
348
|
+
needToRegister,
|
|
349
|
+
availableBefore,
|
|
350
|
+
candidates: [],
|
|
351
|
+
plan: { selected: [], willFree: ZERO, deficit: ZERO },
|
|
352
|
+
accounts: { selfUserRecordPda, selfUserAta },
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Need to free more
|
|
357
|
+
const missing = needToRegister - availableBefore;
|
|
358
|
+
const candidates = opts?.preloadCandidates ?? (await this.fetchMismatchCandidates());
|
|
359
|
+
|
|
360
|
+
if (!candidates.length) {
|
|
361
|
+
return {
|
|
362
|
+
canSucceed: false,
|
|
363
|
+
reason: 'No candidates to free available balance.',
|
|
364
|
+
liqsolMint,
|
|
365
|
+
needToRegister,
|
|
366
|
+
availableBefore,
|
|
367
|
+
candidates: [],
|
|
368
|
+
plan: { selected: [], willFree: ZERO, deficit: missing },
|
|
369
|
+
accounts: { selfUserRecordPda, selfUserAta },
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const plan = this.chooseCandidatesFor(missing, candidates);
|
|
374
|
+
if (plan.deficit > ZERO) {
|
|
375
|
+
return {
|
|
376
|
+
canSucceed: false,
|
|
377
|
+
reason: `Insufficient freeable balance (need ${missing}, can free ${plan.willFree}).`,
|
|
378
|
+
liqsolMint,
|
|
379
|
+
needToRegister,
|
|
380
|
+
availableBefore,
|
|
381
|
+
candidates,
|
|
382
|
+
plan,
|
|
383
|
+
accounts: { selfUserRecordPda, selfUserAta },
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Build: correct N others, then register self
|
|
388
|
+
const tx = new Transaction();
|
|
389
|
+
for (const c of plan.selected) {
|
|
390
|
+
tx.add(await this.buildUpdateUserIx(c.owner));
|
|
391
|
+
}
|
|
392
|
+
tx.add(await this.buildUpdateUserIx(self));
|
|
393
|
+
|
|
394
|
+
return {
|
|
395
|
+
canSucceed: true,
|
|
396
|
+
transaction: tx,
|
|
397
|
+
liqsolMint,
|
|
398
|
+
needToRegister,
|
|
399
|
+
availableBefore,
|
|
400
|
+
candidates,
|
|
401
|
+
plan,
|
|
402
|
+
accounts: { selfUserRecordPda, selfUserAta },
|
|
403
|
+
};
|
|
204
404
|
}
|
|
205
405
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
406
|
+
|
|
407
|
+
// ───────────────────────────────────────────────────────────────────────────────
|
|
408
|
+
// MISMATCH DISCOVERY
|
|
409
|
+
// ───────────────────────────────────────────────────────────────────────────────
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Fetch all distribution user records and turn them into mismatch candidates:
|
|
413
|
+
* rows where `tracked > actual` (delta > 0). Sorted by largest delta first.
|
|
414
|
+
*
|
|
415
|
+
* NOTE: This reads each user’s token account (`getAccount`) to recover
|
|
416
|
+
* the owner and actual balance — simpler and reliable for Token-2022 ATAs.
|
|
417
|
+
*/
|
|
418
|
+
async fetchMismatchCandidates(): Promise<MismatchCandidate[]> {
|
|
419
|
+
const state = await this.getDistributionState();
|
|
420
|
+
const liqsolMint: PublicKey = state.liqsolMint;
|
|
421
|
+
|
|
422
|
+
const records = await this.program.account.userRecord.all();
|
|
423
|
+
if (!records.length) return [];
|
|
424
|
+
|
|
425
|
+
const out: MismatchCandidate[] = [];
|
|
426
|
+
|
|
427
|
+
// For each userRecord, read its token account to recover owner + actual
|
|
428
|
+
// (small N is fine; for very large N you can batch or shard).
|
|
429
|
+
for (const rec of records) {
|
|
430
|
+
const userRecordPda = rec.publicKey;
|
|
431
|
+
const userAta: PublicKey = rec.account.userAta;
|
|
432
|
+
const tracked = BigInt(rec.account.trackedBalance.toString());
|
|
433
|
+
|
|
434
|
+
let actual = ZERO;
|
|
435
|
+
let owner: PublicKey | null = null;
|
|
436
|
+
|
|
437
|
+
try {
|
|
438
|
+
const acc = await getAccount(this.provider.connection, userAta, 'confirmed', TOKEN_2022_PROGRAM_ID);
|
|
439
|
+
actual = acc.amount as unknown as bigint;
|
|
440
|
+
owner = acc.owner as unknown as PublicKey;
|
|
441
|
+
} catch {
|
|
442
|
+
// missing/closed ATA → actual = 0
|
|
443
|
+
owner = null;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (!owner) continue;
|
|
447
|
+
|
|
448
|
+
const delta = tracked - actual;
|
|
449
|
+
if (delta > ZERO) {
|
|
450
|
+
// we only care about freeable deltas
|
|
451
|
+
out.push({ owner, userRecordPda, userAta, tracked, actual, delta });
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Largest first
|
|
456
|
+
out.sort((a, b) => (a.delta === b.delta ? 0 : a.delta > b.delta ? -1 : 1));
|
|
457
|
+
return out;
|
|
215
458
|
}
|
|
216
459
|
|
|
217
|
-
/**
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
460
|
+
/**
|
|
461
|
+
* Given a required amount to free (in base units), choose the
|
|
462
|
+
* smallest prefix of candidates (already sorted desc) that can cover it.
|
|
463
|
+
*/
|
|
464
|
+
chooseCandidatesFor(required: bigint, candidates: MismatchCandidate[]): CorrectionPlan {
|
|
465
|
+
if (required <= ZERO) return { selected: [], willFree: ZERO, deficit: ZERO };
|
|
466
|
+
|
|
467
|
+
let willFree = ZERO;
|
|
468
|
+
const selected: MismatchCandidate[] = [];
|
|
469
|
+
|
|
470
|
+
for (const c of candidates) {
|
|
471
|
+
if (willFree >= required) break;
|
|
472
|
+
selected.push(c);
|
|
473
|
+
willFree += c.delta;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const deficit = willFree >= required ? ZERO : required - willFree;
|
|
477
|
+
return { selected, willFree, deficit };
|
|
229
478
|
}
|
|
230
|
-
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const ZERO = BigInt(0);
|