@wireio/stake 0.1.0 → 0.1.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 (104) hide show
  1. package/README.md +57 -0
  2. package/lib/stake.browser.js +11836 -4103
  3. package/lib/stake.browser.js.map +1 -1
  4. package/lib/stake.d.ts +374 -556
  5. package/lib/stake.js +12089 -4303
  6. package/lib/stake.js.map +1 -1
  7. package/lib/stake.m.js +11836 -4103
  8. package/lib/stake.m.js.map +1 -1
  9. package/package.json +1 -1
  10. package/src/assets/ethereum/ABI/liqEth/DepositManager.sol/DepositManager.dbg.json +4 -0
  11. package/src/assets/ethereum/ABI/liqEth/DepositManager.sol/DepositManager.json +1153 -0
  12. package/src/assets/ethereum/ABI/liqEth/LiqEthCommon.sol/IAccounting.dbg.json +4 -0
  13. package/src/assets/ethereum/ABI/liqEth/LiqEthCommon.sol/IAccounting.json +172 -0
  14. package/src/assets/ethereum/ABI/liqEth/LiqEthCommon.sol/IDepositContract.dbg.json +4 -0
  15. package/src/assets/ethereum/ABI/liqEth/LiqEthCommon.sol/IDepositContract.json +39 -0
  16. package/src/assets/ethereum/ABI/liqEth/LiqEthCommon.sol/IDepositManager.dbg.json +4 -0
  17. package/src/assets/ethereum/ABI/liqEth/LiqEthCommon.sol/IDepositManager.json +64 -0
  18. package/src/assets/ethereum/ABI/liqEth/LiqEthCommon.sol/ILiqEthBurn.dbg.json +4 -0
  19. package/src/assets/ethereum/ABI/liqEth/LiqEthCommon.sol/ILiqEthBurn.json +24 -0
  20. package/src/assets/ethereum/ABI/liqEth/LiqEthCommon.sol/ILiqEthMint.dbg.json +4 -0
  21. package/src/assets/ethereum/ABI/liqEth/LiqEthCommon.sol/ILiqEthMint.json +35 -0
  22. package/src/assets/ethereum/ABI/liqEth/LiqEthCommon.sol/IRewardsERC20.dbg.json +4 -0
  23. package/src/assets/ethereum/ABI/liqEth/LiqEthCommon.sol/IRewardsERC20.json +213 -0
  24. package/src/assets/ethereum/ABI/liqEth/LiqEthCommon.sol/IStakingModule.dbg.json +4 -0
  25. package/src/assets/ethereum/ABI/liqEth/LiqEthCommon.sol/IStakingModule.json +138 -0
  26. package/src/assets/ethereum/ABI/liqEth/LiqEthCommon.sol/IValidatorBalanceVerifier.dbg.json +4 -0
  27. package/src/assets/ethereum/ABI/liqEth/LiqEthCommon.sol/IValidatorBalanceVerifier.json +70 -0
  28. package/src/assets/ethereum/ABI/liqEth/LiqEthCommon.sol/IWithdrawalRecord.dbg.json +4 -0
  29. package/src/assets/ethereum/ABI/liqEth/LiqEthCommon.sol/IWithdrawalRecord.json +64 -0
  30. package/src/assets/ethereum/ABI/liqEth/LiqEthCommon.sol/LiqEthCommon.dbg.json +4 -0
  31. package/src/assets/ethereum/ABI/liqEth/LiqEthCommon.sol/LiqEthCommon.json +10 -0
  32. package/src/assets/ethereum/ABI/liqEth/RewardsERC20.sol/RewardsERC20.dbg.json +4 -0
  33. package/src/assets/ethereum/ABI/liqEth/RewardsERC20.sol/RewardsERC20.json +749 -0
  34. package/src/assets/ethereum/ABI/liqEth/RewardsERC20Pausable.sol/RewardsERC20Pausable.dbg.json +4 -0
  35. package/src/assets/ethereum/ABI/liqEth/RewardsERC20Pausable.sol/RewardsERC20Pausable.json +812 -0
  36. package/src/assets/ethereum/ABI/liqEth/ValidatorBalanceVerifier.sol/BeaconRoots.dbg.json +4 -0
  37. package/src/assets/ethereum/ABI/liqEth/ValidatorBalanceVerifier.sol/BeaconRoots.json +10 -0
  38. package/src/assets/ethereum/ABI/liqEth/ValidatorBalanceVerifier.sol/SSZ.dbg.json +4 -0
  39. package/src/assets/ethereum/ABI/liqEth/ValidatorBalanceVerifier.sol/SSZ.json +10 -0
  40. package/src/assets/ethereum/ABI/liqEth/ValidatorBalanceVerifier.sol/ValidatorBalanceVerifier.dbg.json +4 -0
  41. package/src/assets/ethereum/ABI/liqEth/ValidatorBalanceVerifier.sol/ValidatorBalanceVerifier.json +225 -0
  42. package/src/assets/ethereum/ABI/liqEth/Yield.sol/BeaconRoots.dbg.json +4 -0
  43. package/src/assets/ethereum/ABI/liqEth/Yield.sol/BeaconRoots.json +10 -0
  44. package/src/assets/ethereum/ABI/liqEth/Yield.sol/SSZ.dbg.json +4 -0
  45. package/src/assets/ethereum/ABI/liqEth/Yield.sol/SSZ.json +10 -0
  46. package/src/assets/ethereum/ABI/liqEth/Yield.sol/YieldOracle.dbg.json +4 -0
  47. package/src/assets/ethereum/ABI/liqEth/Yield.sol/YieldOracle.json +813 -0
  48. package/src/assets/ethereum/ABI/liqEth/accounting.sol/Accounting.dbg.json +4 -0
  49. package/src/assets/ethereum/ABI/liqEth/accounting.sol/Accounting.json +651 -0
  50. package/src/assets/ethereum/ABI/liqEth/liqEth.sol/LiqEthToken.dbg.json +4 -0
  51. package/src/assets/ethereum/ABI/liqEth/liqEth.sol/LiqEthToken.json +1110 -0
  52. package/src/assets/ethereum/ABI/liqEth/liqEthBurn.sol/LiqEthBurn.dbg.json +4 -0
  53. package/src/assets/ethereum/ABI/liqEth/liqEthBurn.sol/LiqEthBurn.json +391 -0
  54. package/src/assets/ethereum/ABI/liqEth/liqEthMint.sol/LiqEthMint.dbg.json +4 -0
  55. package/src/assets/ethereum/ABI/liqEth/liqEthMint.sol/LiqEthMint.json +402 -0
  56. package/src/assets/ethereum/ABI/liqEth/stakingModule.sol/StakingModule.dbg.json +4 -0
  57. package/src/assets/ethereum/ABI/liqEth/stakingModule.sol/StakingModule.json +1225 -0
  58. package/src/assets/ethereum/ABI/liqEth/withdrawalQueue.sol/WithdrawalQueue.dbg.json +4 -0
  59. package/src/assets/ethereum/ABI/liqEth/withdrawalQueue.sol/WithdrawalQueue.json +927 -0
  60. package/src/assets/ethereum/ABI/liqEth/withdrawalVault.sol/Uint64BE.dbg.json +4 -0
  61. package/src/assets/ethereum/ABI/liqEth/withdrawalVault.sol/Uint64BE.json +10 -0
  62. package/src/assets/ethereum/ABI/liqEth/withdrawalVault.sol/WithdrawalVault.dbg.json +4 -0
  63. package/src/assets/ethereum/ABI/liqEth/withdrawalVault.sol/WithdrawalVault.json +447 -0
  64. package/src/assets/solana/idl/liqsol_core.json +4239 -0
  65. package/src/assets/solana/idl/liqsol_token.json +183 -0
  66. package/src/assets/solana/idl/validator_leaderboard.json +270 -265
  67. package/src/assets/solana/types/liqsol_core.ts +4245 -0
  68. package/src/assets/solana/types/liqsol_token.ts +189 -0
  69. package/src/assets/solana/types/validator_leaderboard.ts +270 -265
  70. package/src/index.ts +1 -3
  71. package/src/networks/ethereum/contract.ts +101 -36
  72. package/src/networks/ethereum/ethereum.ts +141 -45
  73. package/src/networks/ethereum/types.ts +30 -2
  74. package/src/networks/solana/clients/deposit.client.ts +71 -109
  75. package/src/networks/solana/clients/distribution.client.ts +256 -383
  76. package/src/networks/solana/clients/leaderboard.client.ts +38 -133
  77. package/src/networks/solana/constants.ts +214 -130
  78. package/src/networks/solana/program.ts +25 -38
  79. package/src/networks/solana/solana.ts +120 -105
  80. package/src/networks/solana/types.ts +37 -47
  81. package/src/networks/solana/utils.ts +551 -0
  82. package/src/scripts/tsconfig.json +17 -0
  83. package/src/staker/staker.ts +10 -6
  84. package/src/staker/types.ts +14 -9
  85. package/src/assets/solana/idl/deposit.json +0 -296
  86. package/src/assets/solana/idl/distribution.json +0 -768
  87. package/src/assets/solana/idl/liq_sol_token.json +0 -298
  88. package/src/assets/solana/idl/mint_helper.json +0 -110
  89. package/src/assets/solana/idl/read_tracked_balance.json +0 -140
  90. package/src/assets/solana/idl/stake_controller.json +0 -2149
  91. package/src/assets/solana/idl/treasury.json +0 -110
  92. package/src/assets/solana/idl/validator_registry.json +0 -487
  93. package/src/assets/solana/idl/yield_oracle.json +0 -32
  94. package/src/assets/solana/types/deposit.ts +0 -302
  95. package/src/assets/solana/types/distribution.ts +0 -774
  96. package/src/assets/solana/types/liq_sol_token.ts +0 -304
  97. package/src/assets/solana/types/mint_helper.ts +0 -116
  98. package/src/assets/solana/types/read_tracked_balance.ts +0 -146
  99. package/src/assets/solana/types/stake_controller.ts +0 -2155
  100. package/src/assets/solana/types/stake_registry.ts +0 -441
  101. package/src/assets/solana/types/treasury.ts +0 -116
  102. package/src/assets/solana/types/validator_registry.ts +0 -493
  103. package/src/assets/solana/types/yield_oracle.ts +0 -38
  104. package/src/common/utils.ts +0 -9
@@ -1,481 +1,354 @@
1
- import { AnchorProvider, Program, BN } from '@coral-xyz/anchor';
1
+ import { AnchorProvider, Program } from '@coral-xyz/anchor';
2
2
  import {
3
+ ParsedAccountData,
3
4
  PublicKey,
4
- SystemProgram,
5
5
  SYSVAR_INSTRUCTIONS_PUBKEY,
6
6
  Transaction,
7
- TransactionInstruction,
7
+ SystemProgram,
8
8
  } from '@solana/web3.js';
9
9
  import {
10
- getAccount,
11
- getAssociatedTokenAddressSync,
10
+ TOKEN_2022_PROGRAM_ID,
11
+ getAssociatedTokenAddress,
12
12
  } from '@solana/spl-token';
13
13
 
14
14
  import { SolanaProgramService } from '../program';
15
+ import type { LiqsolCore } from '../../../assets/solana/types/liqsol_core';
15
16
  import {
16
- TOKEN_2022_PROGRAM_ID,
17
- ASSOCIATED_TOKEN_PROGRAM_ID,
18
- STAKE_CONTROLLER_PROGRAM_ID,
19
- YIELD_ORACLE_PROGRAM_ID,
20
- deriveDistributionStatePDA,
21
- deriveUserRecordPDA,
22
- deriveStakeControllerStatePDA,
23
- deriveBucketAuthorityPDA,
24
- derivePayRateHistoryPDA,
25
- deriveStakeControllerAuthorityPDA,
17
+ deriveBucketAuthorityPda,
18
+ deriveDistributionStatePda,
19
+ deriveLiqsolMintPda,
20
+ derivePayRateHistoryPda,
21
+ deriveUserRecordPda,
26
22
  } from '../constants';
27
- import { Distribution } from '../../../assets/solana/types/distribution';
28
- import { MismatchCandidate, CorrectAndRegisterBuild, CorrectionPlan } from '../types';
29
-
23
+ import type { CorrectRegisterBuildResult, DistributionState, MismatchCandidate, ParsedAccountInfo, UserRecord } from '../types';
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
+ */
30
37
  export class DistributionClient {
31
- private programs: SolanaProgramService;
38
+ private program: Program<LiqsolCore>;
32
39
 
33
40
  constructor(private provider: AnchorProvider) {
34
- this.programs = new SolanaProgramService(provider);
41
+ const svc = new SolanaProgramService(provider);
42
+ this.program = svc.getProgram('liqsolCore');
35
43
  }
36
44
 
37
- /** Anchor Program<Distribution> (address comes from SolanaProgramService) */
38
- private get program(): Program<Distribution> {
39
- return this.programs.getProgram('distribution') as Program<Distribution>;
45
+ get connection() {
46
+ return this.provider.connection;
40
47
  }
41
48
 
42
- // ───────────────────────────────────────────────────────────────────────────────
43
- // BASIC READS
44
- // ───────────────────────────────────────────────────────────────────────────────
45
-
46
49
  /**
47
- * Fetch Distribution global state.
50
+ * Fetch the global distribution state account.
48
51
  */
49
- async getDistributionState(): Promise<any> {
50
- const [pda] = deriveDistributionStatePDA();
51
- return this.program.account.distributionState.fetch(pda);
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
+ }
52
60
  }
53
61
 
54
62
  /**
55
- * Fetch a users UserRecord or null if it doesn’t exist yet.
63
+ * Fetch a user's distribution userRecord (or null if missing).
56
64
  */
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; }
65
+ async getUserRecord(user: PublicKey): Promise<UserRecord | null> {
66
+ const pda = deriveUserRecordPda(user);
67
+ try {
68
+ return await this.program.account.userRecord.fetchNullable(pda);
69
+ } catch {
70
+ return null;
71
+ }
61
72
  }
62
73
 
63
74
  /**
64
- * Read protocol-tracked balance for a user (from userRecord).
65
- * Returns (amount, decimals).
66
- * @default decimals=9
75
+ * Helper: get actual liqSOL balances for all token holders.
76
+ *
77
+ * Returns a map: owner (base58) -> actual balance (BigInt)
67
78
  */
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 };
73
- }
79
+ private async getActualBalancesByOwner(): Promise<Map<string, bigint>> {
80
+ const liqsolMint = deriveLiqsolMintPda();
81
+ const mintStr = liqsolMint.toBase58();
74
82
 
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,
83
+ const accounts = await this.connection.getParsedProgramAccounts(
87
84
  TOKEN_2022_PROGRAM_ID,
88
- ASSOCIATED_TOKEN_PROGRAM_ID
85
+ {
86
+ filters: [
87
+ // SPL token layout: mint at offset 0
88
+ { memcmp: { offset: 0, bytes: mintStr } },
89
+ ],
90
+ commitment: 'confirmed',
91
+ },
89
92
  );
90
- try {
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 };
94
- } catch {
95
- return { amount: ZERO, decimals: 9, ata };
93
+
94
+ const byOwner = new Map<string, bigint>();
95
+
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);
96
108
  }
97
- }
98
109
 
99
- // ───────────────────────────────────────────────────────────────────────────────
100
- // STATE-CHANGING ACTIONS (single-user)
101
- // ───────────────────────────────────────────────────────────────────────────────
110
+ return byOwner;
111
+ }
102
112
 
103
113
  /**
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()`.
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
112
123
  */
113
- async updateUser(targetUser: PublicKey): Promise<string> {
114
- const { builder } = await this.prepareUpdateUser(targetUser);
115
- return builder.rpc();
116
- }
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 }>();
129
+
130
+ for (const r of records) {
131
+ const ur = r.account as UserRecord;
132
+ const userAta = ur.userAta as PublicKey;
133
+
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;
117
141
 
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();
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
+ }
149
+
150
+ return map;
122
151
  }
123
152
 
124
153
  /**
125
- * Claim rewards for a user.
126
- * @returns tx signature
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
127
158
  */
128
- async claimRewards(user: PublicKey): Promise<string> {
129
- const state = await this.getDistributionState();
130
- const liqsolMint: PublicKey = state.liqsolMint;
131
-
132
- const userAta = getAssociatedTokenAddressSync(
133
- liqsolMint,
134
- user,
135
- false,
136
- TOKEN_2022_PROGRAM_ID,
137
- ASSOCIATED_TOKEN_PROGRAM_ID
138
- );
139
-
140
- const [userRecordPDA] = deriveUserRecordPDA(user);
141
- const [distributionStatePDA] = deriveDistributionStatePDA();
142
- const [bucketAuthority] = deriveBucketAuthorityPDA();
143
- const [payRateHistory] = derivePayRateHistoryPDA();
159
+ private async deriveMismatchCandidates(): Promise<MismatchCandidate[]> {
160
+ const [actualByOwner, trackedByOwner] = await Promise.all([
161
+ this.getActualBalancesByOwner(),
162
+ this.getTrackedBalances(),
163
+ ]);
144
164
 
145
- const bucketTokenAccount = getAssociatedTokenAddressSync(
146
- liqsolMint,
147
- bucketAuthority,
148
- true,
149
- TOKEN_2022_PROGRAM_ID,
150
- ASSOCIATED_TOKEN_PROGRAM_ID
151
- );
165
+ const out: MismatchCandidate[] = [];
152
166
 
153
- const sig = await this.program.methods
154
- .claimRewards()
155
- .accounts({
156
- user,
157
- // @ts-ignore
158
- userAta,
159
- userRecord: userRecordPDA,
160
- distributionState: distributionStatePDA,
161
- liqsolMint,
162
- stakeControllerProgram: STAKE_CONTROLLER_PROGRAM_ID,
163
- bucketAuthority,
164
- bucketTokenAccount,
165
- payRateHistory,
166
- tokenProgram: TOKEN_2022_PROGRAM_ID,
167
- associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
168
- systemProgram: SystemProgram.programId,
169
- })
170
- .rpc();
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
+ }
171
174
 
172
- return sig;
175
+ // Largest discrepancy first
176
+ out.sort((a, b) => (b.delta > a.delta ? 1 : b.delta < a.delta ? -1 : 0));
177
+ return out;
173
178
  }
174
179
 
175
180
  /**
176
- * Withdraw liqSOL (amount in base units; e.g. 1e9 = 1 liqSOL).
177
- * @returns tx signature
181
+ * Canonical helper to build an `updateUser` instruction for a given user,
182
+ * matching the standalone update-user.ts script.
178
183
  */
179
- async withdraw(user: PublicKey, amount: bigint | number): Promise<string> {
180
- const state = await this.getDistributionState();
181
- const liqsolMint: PublicKey = state.liqsolMint;
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();
182
192
 
183
- const userAta = getAssociatedTokenAddressSync(
184
- liqsolMint,
185
- user,
193
+ const userAta = await getAssociatedTokenAddress(
194
+ liqsolMintPDA,
195
+ targetUser,
186
196
  false,
187
197
  TOKEN_2022_PROGRAM_ID,
188
- ASSOCIATED_TOKEN_PROGRAM_ID
189
198
  );
190
199
 
191
- const [userRecordPDA] = deriveUserRecordPDA(user);
192
- const [distributionStatePDA] = deriveDistributionStatePDA();
193
- const [stakeControllerState] = deriveStakeControllerStatePDA();
194
- const [controllerAuthority] = deriveStakeControllerAuthorityPDA();
200
+ const bucketTokenAccount = await getAssociatedTokenAddress(
201
+ liqsolMintPDA,
202
+ bucketAuthorityPDA,
203
+ true, // allowOwnerOffCurve
204
+ TOKEN_2022_PROGRAM_ID,
205
+ );
195
206
 
196
- const sig = await this.program.methods
197
- .withdraw(new BN(amount.toString()))
207
+ return (this.program.methods as any)
208
+ .updateUser()
198
209
  .accounts({
199
- user,
200
- // @ts-ignore
210
+ user: targetUser,
201
211
  userAta,
202
212
  userRecord: userRecordPDA,
213
+ authority: walletPk,
214
+ payer: walletPk,
203
215
  distributionState: distributionStatePDA,
204
- liqsolMint,
216
+ liqsolMint: liqsolMintPDA,
217
+ instructionsSysvar: SYSVAR_INSTRUCTIONS_PUBKEY,
205
218
  tokenProgram: TOKEN_2022_PROGRAM_ID,
206
- stakeControllerProgram: STAKE_CONTROLLER_PROGRAM_ID,
207
- stakeControllerState,
208
- controllerAuthority,
209
- yieldOracleProgram: YIELD_ORACLE_PROGRAM_ID,
219
+ bucketAuthority: bucketAuthorityPDA,
220
+ bucketTokenAccount,
221
+ payRateHistory: payRateHistoryPDA,
210
222
  systemProgram: SystemProgram.programId,
211
223
  })
212
- .rpc();
213
-
214
- return sig;
215
- }
216
-
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');
225
-
226
- const state = await this.getDistributionState();
227
- const liqsolMint: PublicKey = state.liqsolMint;
228
-
229
- const targetUserAta = getAssociatedTokenAddressSync(
230
- liqsolMint,
231
- targetUser,
232
- false,
233
- TOKEN_2022_PROGRAM_ID,
234
- ASSOCIATED_TOKEN_PROGRAM_ID
235
- );
236
-
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 };
224
+ .instruction();
259
225
  }
260
226
 
261
- // ───────────────────────────────────────────────────────────────────────────────
262
- // ONE-SHOT BUILDER: correct 0..N others, then register self
263
- // ───────────────────────────────────────────────────────────────────────────────
264
-
265
227
  /**
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).
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).
270
237
  *
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).
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.
273
242
  */
274
- async buildCorrectRegisterTx(opts?: {
275
- /** optional override of computed mismatch; may be positive (register) or negative (self-correct) */
243
+ async buildCorrectRegisterTx(opts: {
276
244
  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;
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
+ };
305
264
  }
306
265
 
307
- // CASE A: Negative mismatch → just correct self
308
- if (desired < ZERO) {
309
- console.log('Building Correct transaction for self with amount:', desired);
266
+ const availableBalance = BigInt(distState.availableBalance.toString());
310
267
 
311
- const tx = new Transaction().add(await this.buildUpdateUserIx(self));
312
- return {
313
- canSucceed: true,
314
- transaction: tx,
315
- liqsolMint,
316
- needToRegister: desired, // negative indicates self-correction
317
- availableBefore,
318
- candidates: [],
319
- plan: { selected: [], willFree: ZERO, deficit: ZERO },
320
- accounts: { selfUserRecordPda, selfUserAta },
268
+ const trackedEntry =
269
+ trackedByOwner.get(callerStr) ?? {
270
+ owner: walletPk,
271
+ tracked: BigInt(0),
321
272
  };
322
- }
323
273
 
324
- // Reuse the existing “positive mismatch” path
325
- const needToRegister = desired; // >= 0
326
- if (needToRegister === ZERO) {
274
+ const actual = actualByOwner.get(callerStr) ?? BigInt(0);
275
+ const tracked = trackedEntry.tracked;
276
+ const untracked = actual - tracked;
277
+
278
+ // Nothing to register
279
+ if (untracked <= BigInt(0)) {
327
280
  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 },
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: [] },
336
286
  };
337
287
  }
338
288
 
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);
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
+
344
299
  return {
300
+ needToRegister: true,
345
301
  canSucceed: true,
346
302
  transaction: tx,
347
- liqsolMint,
348
- needToRegister,
349
- availableBefore,
350
- candidates: [],
351
- plan: { selected: [], willFree: ZERO, deficit: ZERO },
352
- accounts: { selfUserRecordPda, selfUserAta },
303
+ plan: { deficit: BigInt(0), willFree: BigInt(0), selected: [] },
353
304
  };
354
305
  }
355
306
 
356
- // Need to free more
357
- const missing = needToRegister - availableBefore;
358
- const candidates = opts?.preloadCandidates ?? (await this.fetchMismatchCandidates());
307
+ // Need to free up more availableBalance by correcting others.
308
+ const deficit = targetRegister - availableBalance;
359
309
 
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
- };
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;
371
321
  }
372
322
 
373
- const plan = this.chooseCandidatesFor(missing, candidates);
374
- if (plan.deficit > ZERO) {
323
+ if (willFree < deficit) {
375
324
  return {
325
+ needToRegister: true,
376
326
  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 },
327
+ reason: 'Not enough mismatched candidates to cover deficit',
328
+ transaction: undefined,
329
+ plan: { deficit, willFree, selected },
384
330
  };
385
331
  }
386
332
 
387
- // Build: correct N others, then register self
388
333
  const tx = new Transaction();
389
- for (const c of plan.selected) {
390
- tx.add(await this.buildUpdateUserIx(c.owner));
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);
391
345
  }
392
- tx.add(await this.buildUpdateUserIx(self));
393
346
 
394
347
  return {
348
+ needToRegister: true,
395
349
  canSucceed: true,
396
350
  transaction: tx,
397
- liqsolMint,
398
- needToRegister,
399
- availableBefore,
400
- candidates,
401
- plan,
402
- accounts: { selfUserRecordPda, selfUserAta },
351
+ plan: { deficit, willFree, selected },
403
352
  };
404
353
  }
405
-
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;
458
- }
459
-
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 };
478
- }
479
- }
480
-
481
- const ZERO = BigInt(0);
354
+ }