@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.
- package/README.md +57 -0
- package/lib/stake.browser.js +11836 -4103
- package/lib/stake.browser.js.map +1 -1
- package/lib/stake.d.ts +374 -556
- package/lib/stake.js +12089 -4303
- package/lib/stake.js.map +1 -1
- package/lib/stake.m.js +11836 -4103
- package/lib/stake.m.js.map +1 -1
- package/package.json +1 -1
- package/src/assets/ethereum/ABI/liqEth/DepositManager.sol/DepositManager.dbg.json +4 -0
- package/src/assets/ethereum/ABI/liqEth/DepositManager.sol/DepositManager.json +1153 -0
- package/src/assets/ethereum/ABI/liqEth/LiqEthCommon.sol/IAccounting.dbg.json +4 -0
- package/src/assets/ethereum/ABI/liqEth/LiqEthCommon.sol/IAccounting.json +172 -0
- package/src/assets/ethereum/ABI/liqEth/LiqEthCommon.sol/IDepositContract.dbg.json +4 -0
- package/src/assets/ethereum/ABI/liqEth/LiqEthCommon.sol/IDepositContract.json +39 -0
- package/src/assets/ethereum/ABI/liqEth/LiqEthCommon.sol/IDepositManager.dbg.json +4 -0
- package/src/assets/ethereum/ABI/liqEth/LiqEthCommon.sol/IDepositManager.json +64 -0
- package/src/assets/ethereum/ABI/liqEth/LiqEthCommon.sol/ILiqEthBurn.dbg.json +4 -0
- package/src/assets/ethereum/ABI/liqEth/LiqEthCommon.sol/ILiqEthBurn.json +24 -0
- package/src/assets/ethereum/ABI/liqEth/LiqEthCommon.sol/ILiqEthMint.dbg.json +4 -0
- package/src/assets/ethereum/ABI/liqEth/LiqEthCommon.sol/ILiqEthMint.json +35 -0
- package/src/assets/ethereum/ABI/liqEth/LiqEthCommon.sol/IRewardsERC20.dbg.json +4 -0
- package/src/assets/ethereum/ABI/liqEth/LiqEthCommon.sol/IRewardsERC20.json +213 -0
- package/src/assets/ethereum/ABI/liqEth/LiqEthCommon.sol/IStakingModule.dbg.json +4 -0
- package/src/assets/ethereum/ABI/liqEth/LiqEthCommon.sol/IStakingModule.json +138 -0
- package/src/assets/ethereum/ABI/liqEth/LiqEthCommon.sol/IValidatorBalanceVerifier.dbg.json +4 -0
- package/src/assets/ethereum/ABI/liqEth/LiqEthCommon.sol/IValidatorBalanceVerifier.json +70 -0
- package/src/assets/ethereum/ABI/liqEth/LiqEthCommon.sol/IWithdrawalRecord.dbg.json +4 -0
- package/src/assets/ethereum/ABI/liqEth/LiqEthCommon.sol/IWithdrawalRecord.json +64 -0
- package/src/assets/ethereum/ABI/liqEth/LiqEthCommon.sol/LiqEthCommon.dbg.json +4 -0
- package/src/assets/ethereum/ABI/liqEth/LiqEthCommon.sol/LiqEthCommon.json +10 -0
- package/src/assets/ethereum/ABI/liqEth/RewardsERC20.sol/RewardsERC20.dbg.json +4 -0
- package/src/assets/ethereum/ABI/liqEth/RewardsERC20.sol/RewardsERC20.json +749 -0
- package/src/assets/ethereum/ABI/liqEth/RewardsERC20Pausable.sol/RewardsERC20Pausable.dbg.json +4 -0
- package/src/assets/ethereum/ABI/liqEth/RewardsERC20Pausable.sol/RewardsERC20Pausable.json +812 -0
- package/src/assets/ethereum/ABI/liqEth/ValidatorBalanceVerifier.sol/BeaconRoots.dbg.json +4 -0
- package/src/assets/ethereum/ABI/liqEth/ValidatorBalanceVerifier.sol/BeaconRoots.json +10 -0
- package/src/assets/ethereum/ABI/liqEth/ValidatorBalanceVerifier.sol/SSZ.dbg.json +4 -0
- package/src/assets/ethereum/ABI/liqEth/ValidatorBalanceVerifier.sol/SSZ.json +10 -0
- package/src/assets/ethereum/ABI/liqEth/ValidatorBalanceVerifier.sol/ValidatorBalanceVerifier.dbg.json +4 -0
- package/src/assets/ethereum/ABI/liqEth/ValidatorBalanceVerifier.sol/ValidatorBalanceVerifier.json +225 -0
- package/src/assets/ethereum/ABI/liqEth/Yield.sol/BeaconRoots.dbg.json +4 -0
- package/src/assets/ethereum/ABI/liqEth/Yield.sol/BeaconRoots.json +10 -0
- package/src/assets/ethereum/ABI/liqEth/Yield.sol/SSZ.dbg.json +4 -0
- package/src/assets/ethereum/ABI/liqEth/Yield.sol/SSZ.json +10 -0
- package/src/assets/ethereum/ABI/liqEth/Yield.sol/YieldOracle.dbg.json +4 -0
- package/src/assets/ethereum/ABI/liqEth/Yield.sol/YieldOracle.json +813 -0
- package/src/assets/ethereum/ABI/liqEth/accounting.sol/Accounting.dbg.json +4 -0
- package/src/assets/ethereum/ABI/liqEth/accounting.sol/Accounting.json +651 -0
- package/src/assets/ethereum/ABI/liqEth/liqEth.sol/LiqEthToken.dbg.json +4 -0
- package/src/assets/ethereum/ABI/liqEth/liqEth.sol/LiqEthToken.json +1110 -0
- package/src/assets/ethereum/ABI/liqEth/liqEthBurn.sol/LiqEthBurn.dbg.json +4 -0
- package/src/assets/ethereum/ABI/liqEth/liqEthBurn.sol/LiqEthBurn.json +391 -0
- package/src/assets/ethereum/ABI/liqEth/liqEthMint.sol/LiqEthMint.dbg.json +4 -0
- package/src/assets/ethereum/ABI/liqEth/liqEthMint.sol/LiqEthMint.json +402 -0
- package/src/assets/ethereum/ABI/liqEth/stakingModule.sol/StakingModule.dbg.json +4 -0
- package/src/assets/ethereum/ABI/liqEth/stakingModule.sol/StakingModule.json +1225 -0
- package/src/assets/ethereum/ABI/liqEth/withdrawalQueue.sol/WithdrawalQueue.dbg.json +4 -0
- package/src/assets/ethereum/ABI/liqEth/withdrawalQueue.sol/WithdrawalQueue.json +927 -0
- package/src/assets/ethereum/ABI/liqEth/withdrawalVault.sol/Uint64BE.dbg.json +4 -0
- package/src/assets/ethereum/ABI/liqEth/withdrawalVault.sol/Uint64BE.json +10 -0
- package/src/assets/ethereum/ABI/liqEth/withdrawalVault.sol/WithdrawalVault.dbg.json +4 -0
- package/src/assets/ethereum/ABI/liqEth/withdrawalVault.sol/WithdrawalVault.json +447 -0
- 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 +270 -265
- 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 +270 -265
- package/src/index.ts +1 -3
- package/src/networks/ethereum/contract.ts +101 -36
- package/src/networks/ethereum/ethereum.ts +141 -45
- package/src/networks/ethereum/types.ts +30 -2
- package/src/networks/solana/clients/deposit.client.ts +71 -109
- package/src/networks/solana/clients/distribution.client.ts +256 -383
- package/src/networks/solana/clients/leaderboard.client.ts +38 -133
- package/src/networks/solana/constants.ts +214 -130
- package/src/networks/solana/program.ts +25 -38
- package/src/networks/solana/solana.ts +120 -105
- package/src/networks/solana/types.ts +37 -47
- package/src/networks/solana/utils.ts +551 -0
- package/src/scripts/tsconfig.json +17 -0
- package/src/staker/staker.ts +10 -6
- package/src/staker/types.ts +14 -9
- package/src/assets/solana/idl/deposit.json +0 -296
- package/src/assets/solana/idl/distribution.json +0 -768
- package/src/assets/solana/idl/liq_sol_token.json +0 -298
- package/src/assets/solana/idl/mint_helper.json +0 -110
- package/src/assets/solana/idl/read_tracked_balance.json +0 -140
- package/src/assets/solana/idl/stake_controller.json +0 -2149
- package/src/assets/solana/idl/treasury.json +0 -110
- package/src/assets/solana/idl/validator_registry.json +0 -487
- package/src/assets/solana/idl/yield_oracle.json +0 -32
- package/src/assets/solana/types/deposit.ts +0 -302
- package/src/assets/solana/types/distribution.ts +0 -774
- package/src/assets/solana/types/liq_sol_token.ts +0 -304
- package/src/assets/solana/types/mint_helper.ts +0 -116
- package/src/assets/solana/types/read_tracked_balance.ts +0 -146
- package/src/assets/solana/types/stake_controller.ts +0 -2155
- package/src/assets/solana/types/stake_registry.ts +0 -441
- package/src/assets/solana/types/treasury.ts +0 -116
- package/src/assets/solana/types/validator_registry.ts +0 -493
- package/src/assets/solana/types/yield_oracle.ts +0 -38
- package/src/common/utils.ts +0 -9
|
@@ -1,481 +1,354 @@
|
|
|
1
|
-
import { AnchorProvider, Program
|
|
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
|
-
|
|
7
|
+
SystemProgram,
|
|
8
8
|
} from '@solana/web3.js';
|
|
9
9
|
import {
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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 {
|
|
28
|
-
|
|
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
|
|
38
|
+
private program: Program<LiqsolCore>;
|
|
32
39
|
|
|
33
40
|
constructor(private provider: AnchorProvider) {
|
|
34
|
-
|
|
41
|
+
const svc = new SolanaProgramService(provider);
|
|
42
|
+
this.program = svc.getProgram('liqsolCore');
|
|
35
43
|
}
|
|
36
44
|
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
50
|
+
* Fetch the global distribution state account.
|
|
48
51
|
*/
|
|
49
|
-
async getDistributionState(): Promise<
|
|
50
|
-
const
|
|
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
|
+
}
|
|
52
60
|
}
|
|
53
61
|
|
|
54
62
|
/**
|
|
55
|
-
* Fetch a user
|
|
63
|
+
* Fetch a user's distribution userRecord (or null if missing).
|
|
56
64
|
*/
|
|
57
|
-
async getUserRecord(user: PublicKey): Promise<
|
|
58
|
-
const
|
|
59
|
-
try {
|
|
60
|
-
|
|
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
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
75
|
+
* Helper: get actual liqSOL balances for all token holders.
|
|
76
|
+
*
|
|
77
|
+
* Returns a map: owner (base58) -> actual balance (BigInt)
|
|
67
78
|
*/
|
|
68
|
-
async
|
|
69
|
-
const
|
|
70
|
-
|
|
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
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
101
|
-
// ───────────────────────────────────────────────────────────────────────────────
|
|
110
|
+
return byOwner;
|
|
111
|
+
}
|
|
102
112
|
|
|
103
113
|
/**
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
*
|
|
111
|
-
*
|
|
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
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
*
|
|
126
|
-
*
|
|
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
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
|
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
|
|
154
|
-
.
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
177
|
-
*
|
|
181
|
+
* Canonical helper to build an `updateUser` instruction for a given user,
|
|
182
|
+
* matching the standalone update-user.ts script.
|
|
178
183
|
*/
|
|
179
|
-
async
|
|
180
|
-
const
|
|
181
|
-
|
|
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 =
|
|
184
|
-
|
|
185
|
-
|
|
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
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
200
|
+
const bucketTokenAccount = await getAssociatedTokenAddress(
|
|
201
|
+
liqsolMintPDA,
|
|
202
|
+
bucketAuthorityPDA,
|
|
203
|
+
true, // allowOwnerOffCurve
|
|
204
|
+
TOKEN_2022_PROGRAM_ID,
|
|
205
|
+
);
|
|
195
206
|
|
|
196
|
-
|
|
197
|
-
.
|
|
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
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
yieldOracleProgram: YIELD_ORACLE_PROGRAM_ID,
|
|
219
|
+
bucketAuthority: bucketAuthorityPDA,
|
|
220
|
+
bucketTokenAccount,
|
|
221
|
+
payRateHistory: payRateHistoryPDA,
|
|
210
222
|
systemProgram: SystemProgram.programId,
|
|
211
223
|
})
|
|
212
|
-
.
|
|
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
|
|
267
|
-
*
|
|
268
|
-
*
|
|
269
|
-
*
|
|
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
|
-
*
|
|
272
|
-
*
|
|
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
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
const
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
|
|
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
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
-
|
|
325
|
-
const
|
|
326
|
-
|
|
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
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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
|
-
//
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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
|
-
|
|
374
|
-
if (plan.deficit > ZERO) {
|
|
323
|
+
if (willFree < deficit) {
|
|
375
324
|
return {
|
|
325
|
+
needToRegister: true,
|
|
376
326
|
canSucceed: false,
|
|
377
|
-
reason:
|
|
378
|
-
|
|
379
|
-
|
|
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
|
-
|
|
390
|
-
|
|
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
|
-
|
|
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
|
+
}
|