@wireio/stake 0.3.1 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/lib/stake.browser.js +12887 -10017
  2. package/lib/stake.browser.js.map +1 -1
  3. package/lib/stake.d.ts +3305 -1364
  4. package/lib/stake.js +16298 -13436
  5. package/lib/stake.js.map +1 -1
  6. package/lib/stake.m.js +12887 -10017
  7. package/lib/stake.m.js.map +1 -1
  8. package/package.json +3 -1
  9. package/src/assets/solana/idl/liqsol_core.json +2327 -887
  10. package/src/assets/solana/idl/liqsol_token.json +1 -1
  11. package/src/assets/solana/idl/transfer_hook.json +192 -0
  12. package/src/assets/solana/idl/validator_leaderboard.json +147 -4
  13. package/src/assets/solana/types/liqsol_core.ts +2327 -887
  14. package/src/assets/solana/types/liqsol_token.ts +1 -1
  15. package/src/assets/solana/types/transfer_hook.ts +198 -0
  16. package/src/assets/solana/types/validator_leaderboard.ts +147 -4
  17. package/src/networks/ethereum/clients/{deposit.client.ts → convert.client.ts} +36 -4
  18. package/src/networks/ethereum/clients/opp.client.ts +390 -0
  19. package/src/networks/ethereum/clients/pretoken.client.ts +88 -49
  20. package/src/networks/ethereum/clients/receipt.client.ts +129 -0
  21. package/src/networks/ethereum/clients/stake.client.ts +1 -148
  22. package/src/networks/ethereum/contract.ts +7 -4
  23. package/src/networks/ethereum/ethereum.ts +44 -70
  24. package/src/networks/ethereum/types.ts +1 -0
  25. package/src/networks/ethereum/utils.ts +1 -1
  26. package/src/networks/solana/clients/deposit.client.ts +154 -8
  27. package/src/networks/solana/clients/distribution.client.ts +72 -291
  28. package/src/networks/solana/clients/leaderboard.client.ts +59 -14
  29. package/src/networks/solana/clients/outpost.client.ts +188 -359
  30. package/src/networks/solana/clients/token.client.ts +85 -100
  31. package/src/networks/solana/constants.ts +155 -64
  32. package/src/networks/solana/solana.ts +273 -154
  33. package/src/networks/solana/types.ts +532 -71
  34. package/src/networks/solana/utils.ts +68 -51
  35. package/src/types.ts +161 -17
  36. package/src/networks/ethereum/clients/liq.client.ts +0 -47
@@ -1,38 +1,33 @@
1
- import { AnchorProvider, Program } from '@coral-xyz/anchor';
2
- import {
3
- ParsedAccountData,
4
- PublicKey,
5
- SYSVAR_INSTRUCTIONS_PUBKEY,
6
- Transaction,
7
- SystemProgram,
8
- } from '@solana/web3.js';
9
- import {
10
- TOKEN_2022_PROGRAM_ID,
11
- getAssociatedTokenAddress,
12
- } from '@solana/spl-token';
1
+ import { AnchorProvider, Program, BN } from '@coral-xyz/anchor';
2
+ import { PublicKey } from '@solana/web3.js';
13
3
 
14
4
  import { SolanaProgramService } from '../program';
15
5
  import type { LiqsolCore } from '../../../assets/solana/types/liqsol_core';
16
6
  import {
17
- deriveBucketAuthorityPda,
18
7
  deriveDistributionStatePda,
19
8
  deriveLiqsolMintPda,
20
- derivePayRateHistoryPda,
21
9
  deriveUserRecordPda,
22
10
  } from '../constants';
23
- import type { CorrectRegisterBuildResult, DistributionState, MismatchCandidate, ParsedAccountInfo, UserRecord } from '../types';
11
+ import type { DistributionState, DistributionUserRecord } from '../types';
12
+ import { getAssociatedTokenAddressSync, TOKEN_2022_PROGRAM_ID } from '@solana/spl-token';
24
13
 
25
14
  /**
26
15
  * 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
16
+ * program in the *new* shares-only model.
17
+ *
18
+ * Responsibilities:
19
+ * - Read DistributionState (global index/share pool)
20
+ * - Read per-user UserRecord (shares)
21
+ * - Provide simple helpers for querying user share positions
31
22
  *
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
23
+ * There is no longer any notion of:
24
+ * - tracked vs actual balances
25
+ * - mismatch candidates
26
+ * - updateUser / correct-&-register flow
27
+ *
28
+ * On-chain truth:
29
+ * - User balances live in their liqSOL ATA.
30
+ * - DistributionState + UserRecord track *shares* for yield.
36
31
  */
37
32
  export class DistributionClient {
38
33
  private program: Program<LiqsolCore>;
@@ -42,15 +37,18 @@ export class DistributionClient {
42
37
  this.program = svc.getProgram('liqsolCore');
43
38
  }
44
39
 
45
- get connection() { return this.provider.connection; }
40
+ get connection() {
41
+ return this.provider.connection;
42
+ }
46
43
 
47
44
  /**
48
45
  * Fetch the global distribution state account.
46
+ *
47
+ * IDL account name: "distributionState"
49
48
  */
50
49
  async getDistributionState(): Promise<DistributionState | null> {
51
50
  const pda = deriveDistributionStatePda();
52
51
  try {
53
- // IDL account name: "distributionState"
54
52
  return await this.program.account.distributionState.fetch(pda);
55
53
  } catch {
56
54
  return null;
@@ -59,294 +57,77 @@ export class DistributionClient {
59
57
 
60
58
  /**
61
59
  * Fetch a user's distribution userRecord (or null if missing).
62
- */
63
- async getUserRecord(user: PublicKey): Promise<UserRecord | null> {
64
- const pda = deriveUserRecordPda(user);
65
- try {
66
- return await this.program.account.userRecord.fetchNullable(pda);
67
- } catch {
68
- return null;
69
- }
70
- }
71
-
72
- /**
73
- * Helper: get actual liqSOL balances for all token holders.
74
60
  *
75
- * Returns a map: owner (base58) -> actual balance (BigInt)
61
+ * IDL account name: "userRecord"
76
62
  */
77
- private async getActualBalancesByOwner(): Promise<Map<string, bigint>> {
63
+ async getUserRecord(
64
+ ownerOrAta: PublicKey,
65
+ ): Promise<DistributionUserRecord | null> {
78
66
  const liqsolMint = deriveLiqsolMintPda();
79
- const mintStr = liqsolMint.toBase58();
80
-
81
- const accounts = await this.connection.getParsedProgramAccounts(
82
- TOKEN_2022_PROGRAM_ID,
83
- {
84
- filters: [
85
- // SPL token layout: mint at offset 0
86
- { memcmp: { offset: 0, bytes: mintStr } },
87
- ],
88
- commitment: 'confirmed',
89
- },
90
- );
91
-
92
- const byOwner = new Map<string, bigint>();
93
-
94
- for (const acct of accounts) {
95
- const data = acct.account.data as ParsedAccountData;
96
- const parsed = data.parsed;
97
- if (!parsed || parsed.type !== 'account') continue;
98
-
99
- const info: ParsedAccountInfo = parsed.info;
100
- const ownerStr = info.owner;
101
- const amountStr = info.tokenAmount.amount;
102
- const amount = BigInt(amountStr);
103
67
 
104
- const prev = byOwner.get(ownerStr) ?? BigInt(0);
105
- byOwner.set(ownerStr, prev + amount);
68
+ // 1) Try treating ownerOrAta as a WALLET: derive its liqSOL ATA, then userRecord.
69
+ try {
70
+ const ata = getAssociatedTokenAddressSync(
71
+ liqsolMint,
72
+ ownerOrAta,
73
+ false,
74
+ TOKEN_2022_PROGRAM_ID,
75
+ );
76
+ const pdaFromWallet = deriveUserRecordPda(ata);
77
+ const recFromWallet =
78
+ await this.program.account.userRecord.fetchNullable(pdaFromWallet);
79
+ if (recFromWallet) {
80
+ return recFromWallet;
81
+ }
82
+ } catch {
83
+ // fall through
106
84
  }
107
85
 
108
- return byOwner;
109
- }
110
-
111
- /**
112
- * Helper: get tracked balances from all userRecord accounts,
113
- * keyed by *actual wallet owner*, not the userRecord PDA.
114
- *
115
- * userRecord struct:
116
- * - userAta: pubkey
117
- * - trackedBalance: u64
118
- * - claimBalance: u64
119
- * - lastClaimTimestamp: i64
120
- * - bump: u8
121
- */
122
- private async getTrackedBalances(): Promise<
123
- Map<string, { owner: PublicKey; tracked: bigint }>
124
- > {
125
- const records = await this.program.account.userRecord.all();
126
- const map = new Map<string, { owner: PublicKey; tracked: bigint }>();
127
-
128
- for (const r of records) {
129
- const ur = r.account as UserRecord;
130
- const userAta = ur.userAta as PublicKey;
131
-
132
- // Resolve the *wallet* that owns this ATA
133
- const ataInfo = await this.connection.getParsedAccountInfo(userAta);
134
- const parsedData = ataInfo.value?.data as ParsedAccountData | undefined;
135
- if (!parsedData) continue;
136
-
137
- const parsed = parsedData.parsed;
138
- if (!parsed || (parsed as any).type !== 'account') continue;
139
-
140
- const info: ParsedAccountInfo = parsed.info;
141
- const ownerStr = info.owner;
142
- const owner = new PublicKey(ownerStr);
143
-
144
- const tracked = BigInt(ur.trackedBalance.toString());
145
- map.set(ownerStr, { owner, tracked });
86
+ // 2) Fallback: treat ownerOrAta as already being the TOKEN ACCOUNT.
87
+ try {
88
+ const pdaFromAta = deriveUserRecordPda(ownerOrAta);
89
+ return await this.program.account.userRecord.fetchNullable(pdaFromAta);
90
+ } catch {
91
+ return null;
146
92
  }
147
-
148
- return map;
149
93
  }
150
94
 
151
95
  /**
152
- * Discover all mismatch candidates where tracked > actual.
96
+ * Convenience: get the user's current distribution shares.
153
97
  *
154
- * - actual balances are derived from token accounts for the liqSOL mint
155
- * - tracked balances come from Distribution.userRecord
98
+ * Returns BN(0) if the user has no userRecord yet.
156
99
  */
157
- private async deriveMismatchCandidates(): Promise<MismatchCandidate[]> {
158
- const [actualByOwner, trackedByOwner] = await Promise.all([
159
- this.getActualBalancesByOwner(),
160
- this.getTrackedBalances(),
161
- ]);
162
-
163
- const out: MismatchCandidate[] = [];
164
-
165
- for (const [ownerStr, { owner, tracked }] of trackedByOwner.entries()) {
166
- const actual = actualByOwner.get(ownerStr) ?? BigInt(0);
167
- const delta = tracked - actual;
168
- if (delta > BigInt(0)) {
169
- out.push({ owner, actual, tracked, delta });
170
- }
171
- }
172
-
173
- // Largest discrepancy first
174
- out.sort((a, b) => (b.delta > a.delta ? 1 : b.delta < a.delta ? -1 : 0));
175
- return out;
176
- }
177
-
178
- /**
179
- * Canonical helper to build an `updateUser` instruction for a given user,
180
- * matching the standalone update-user.ts script.
181
- */
182
- private async buildUpdateUserIx(targetUser: PublicKey) {
183
- const walletPk = this.provider.wallet.publicKey;
184
-
185
- const distributionStatePDA = deriveDistributionStatePda();
186
- const userRecordPDA = deriveUserRecordPda(targetUser);
187
- const liqsolMintPDA = deriveLiqsolMintPda();
188
- const payRateHistoryPDA = derivePayRateHistoryPda();
189
- const bucketAuthorityPDA = deriveBucketAuthorityPda();
190
-
191
- const userAta = await getAssociatedTokenAddress(
192
- liqsolMintPDA,
193
- targetUser,
194
- false,
195
- TOKEN_2022_PROGRAM_ID,
196
- );
197
-
198
- const bucketTokenAccount = await getAssociatedTokenAddress(
199
- liqsolMintPDA,
200
- bucketAuthorityPDA,
201
- true, // allowOwnerOffCurve
202
- TOKEN_2022_PROGRAM_ID,
203
- );
204
-
205
- return (this.program.methods as any)
206
- .updateUser()
207
- .accounts({
208
- user: targetUser,
209
- userAta,
210
- userRecord: userRecordPDA,
211
- authority: walletPk,
212
- payer: walletPk,
213
- distributionState: distributionStatePDA,
214
- liqsolMint: liqsolMintPDA,
215
- instructionsSysvar: SYSVAR_INSTRUCTIONS_PUBKEY,
216
- tokenProgram: TOKEN_2022_PROGRAM_ID,
217
- bucketAuthority: bucketAuthorityPDA,
218
- bucketTokenAccount,
219
- payRateHistory: payRateHistoryPDA,
220
- systemProgram: SystemProgram.programId,
221
- })
222
- .instruction();
100
+ async getUserShares(user: PublicKey): Promise<BN> {
101
+ const record = await this.getUserRecord(user);
102
+ return record ? record.shares : new BN(0);
223
103
  }
224
104
 
225
105
  /**
226
- * Build the "correct & register" transaction.
227
- *
228
- * - Fetches DistributionState + all userRecords + token holders
229
- * - Computes caller's untracked amount (actual - tracked)
230
- * - If DistributionState.availableBalance already covers that, we only
231
- * send updateUser(caller).
232
- * - Otherwise we select top mismatch candidates until their freed deltas
233
- * cover the shortfall, then build updateUser(target) for each,
234
- * followed by updateUser(caller).
106
+ * Convenience: get both the user's shares and totalShares, plus a
107
+ * floating-point ratio (userShares / totalShares).
235
108
  *
236
- * NOTE:
237
- * - This no longer uses a separate updateSpecificUser; the single
238
- * updateUser entrypoint accepts any `user` as long as authority/payer
239
- * are valid, per your script.
109
+ * NOTE: ratio is a JS number and may lose precision for very large values,
110
+ * but is fine for display / percentage UIs.
240
111
  */
241
- async buildCorrectRegisterTx(opts: {
242
- amount?: bigint;
243
- maxCandidates?: number;
244
- } = {}): Promise<CorrectRegisterBuildResult> {
245
- const walletPk = this.provider.wallet.publicKey;
246
- const callerStr = walletPk.toBase58();
247
-
248
- const [distState, actualByOwner, trackedByOwner] = await Promise.all([
112
+ async getUserSharePosition(user: PublicKey): Promise<{
113
+ shares: BN;
114
+ totalShares: BN;
115
+ ratio: number;
116
+ }> {
117
+ const [dist, userShares] = await Promise.all([
249
118
  this.getDistributionState(),
250
- this.getActualBalancesByOwner(),
251
- this.getTrackedBalances(),
119
+ this.getUserShares(user),
252
120
  ]);
253
121
 
254
- if (!distState) {
255
- return {
256
- needToRegister: false,
257
- canSucceed: false,
258
- reason: 'DistributionState not initialized',
259
- transaction: undefined,
260
- plan: { deficit: BigInt(0), willFree: BigInt(0), selected: [] },
261
- };
262
- }
263
-
264
- const availableBalance = BigInt(distState.availableBalance.toString());
265
-
266
- const trackedEntry =
267
- trackedByOwner.get(callerStr) ?? {
268
- owner: walletPk,
269
- tracked: BigInt(0),
270
- };
271
-
272
- const actual = actualByOwner.get(callerStr) ?? BigInt(0);
273
- const tracked = trackedEntry.tracked;
274
- const untracked = actual - tracked;
275
-
276
- // Nothing to register
277
- if (untracked <= BigInt(0)) {
278
- return {
279
- needToRegister: false,
280
- canSucceed: true,
281
- reason: 'No untracked liqSOL to register',
282
- transaction: undefined,
283
- plan: { deficit: BigInt(0), willFree: BigInt(0), selected: [] },
284
- };
285
- }
286
-
287
- // Optional user-specified cap on how much to register.
288
- const targetRegister =
289
- opts.amount && opts.amount > BigInt(0) ? opts.amount : untracked;
290
-
291
- // Simple case: availableBalance already covers what we want to register
292
- if (availableBalance >= targetRegister) {
293
- const tx = new Transaction();
294
- const ix = await this.buildUpdateUserIx(walletPk); // caller only
295
- tx.add(ix);
296
-
297
- return {
298
- needToRegister: true,
299
- canSucceed: true,
300
- transaction: tx,
301
- plan: { deficit: BigInt(0), willFree: BigInt(0), selected: [] },
302
- };
303
- }
304
-
305
- // Need to free up more availableBalance by correcting others.
306
- const deficit = targetRegister - availableBalance;
307
-
308
- const allCandidates = await this.deriveMismatchCandidates();
309
- const maxCandidates = opts.maxCandidates ?? 10;
310
-
311
- const selected: MismatchCandidate[] = [];
312
- let willFree = BigInt(0);
313
-
314
- for (const c of allCandidates) {
315
- if (c.owner.equals(walletPk)) continue; // don't self-correct here
316
- selected.push(c);
317
- willFree += c.delta;
318
- if (willFree >= deficit || selected.length >= maxCandidates) break;
319
- }
320
-
321
- if (willFree < deficit) {
322
- return {
323
- needToRegister: true,
324
- canSucceed: false,
325
- reason: 'Not enough mismatched candidates to cover deficit',
326
- transaction: undefined,
327
- plan: { deficit, willFree, selected },
328
- };
329
- }
330
-
331
- const tx = new Transaction();
332
-
333
- // 1) Correct selected mismatched users.
334
- for (const c of selected) {
335
- const ix = await this.buildUpdateUserIx(c.owner);
336
- tx.add(ix);
337
- }
122
+ const totalShares = dist ? dist.totalShares : new BN(0);
338
123
 
339
- // 2) Register caller (updateUser(caller)).
340
- {
341
- const ix = await this.buildUpdateUserIx(walletPk);
342
- tx.add(ix);
124
+ let ratio = 0;
125
+ if (!totalShares.isZero()) {
126
+ // use toNumber() for UI-friendly ratio; if you need exact math,
127
+ // do it with BN or bigint at the call site instead.
128
+ ratio = userShares.toNumber() / totalShares.toNumber();
343
129
  }
344
130
 
345
- return {
346
- needToRegister: true,
347
- canSucceed: true,
348
- transaction: tx,
349
- plan: { deficit, willFree, selected },
350
- };
131
+ return { shares: userShares, totalShares, ratio };
351
132
  }
352
133
  }
@@ -7,6 +7,7 @@ import {
7
7
  deriveLeaderboardStatePda,
8
8
  deriveValidatorRecordPda,
9
9
  } from '../constants';
10
+ import { LeaderboardState, ValidatorRecord } from '../types';
10
11
 
11
12
  /**
12
13
  * Simple read client for the validator_leaderboard program.
@@ -21,7 +22,7 @@ export class LeaderboardClient {
21
22
  this.program = svc.getProgram('validatorLeaderboard');
22
23
  }
23
24
 
24
- async getState(): Promise<any | null> {
25
+ async getState(): Promise<LeaderboardState | null> {
25
26
  const pda = deriveLeaderboardStatePda();
26
27
  try {
27
28
  // Assumes account name "leaderboardState"
@@ -31,7 +32,7 @@ export class LeaderboardClient {
31
32
  }
32
33
  }
33
34
 
34
- async getValidatorRecord(voteAccount: PublicKey): Promise<any | null> {
35
+ async getValidatorRecord(voteAccount: PublicKey): Promise<ValidatorRecord | null> {
35
36
  const pda = deriveValidatorRecordPda(voteAccount);
36
37
  try {
37
38
  // Assumes account name "validatorRecord"
@@ -42,18 +43,62 @@ export class LeaderboardClient {
42
43
  }
43
44
 
44
45
  /**
45
- * Convenience helper to fetch and sort top validators by score.
46
- * Assumes `validatorRecord` has a numeric `score` field in the IDL.
46
+ * Convenience helper to fetch and return top validators ordered
47
+ * by the on-chain leaderboard scores.
48
+ *
49
+ * Uses:
50
+ * - `leaderboardState.scores`
51
+ * - `leaderboardState.sortedIndices`
52
+ * - `validatorRecord.registryIndex`
53
+ *
54
+ * Fallback: if leaderboardState can't be fetched, we sort by
55
+ * `creditsObserved` descending.
47
56
  */
48
- async getTopValidators(limit = 20): Promise<any[]> {
49
- const records = await (this.program.account as any).validatorRecord.all();
50
- const sorted = (records as Array<{ publicKey: PublicKey; account: any }>).sort(
51
- (a, b) => {
52
- const sa = BigInt(a.account.score?.toString?.() ?? '0');
53
- const sb = BigInt(b.account.score?.toString?.() ?? '0');
54
- return sb > sa ? 1 : sb < sa ? -1 : 0;
55
- },
56
- );
57
- return sorted.slice(0, limit);
57
+ async getTopValidators(limit = 20): Promise<ValidatorRecord[]> {
58
+ // Try to use the leaderboard state first
59
+ try {
60
+ const leaderboardPda = deriveLeaderboardStatePda();
61
+ const leaderboard: LeaderboardState
62
+ = await this.program.account.leaderboardState.fetch(leaderboardPda);
63
+
64
+ const all = await this.program.account.validatorRecord.all();
65
+ const byIndex = new Map<number, ValidatorRecord>();
66
+
67
+ for (const r of all) {
68
+ const vr = r.account;
69
+ byIndex.set(vr.registryIndex, vr);
70
+ }
71
+
72
+ const max = Math.min(
73
+ limit,
74
+ leaderboard.numValidators,
75
+ leaderboard.sortedIndices.length,
76
+ );
77
+
78
+ const top: ValidatorRecord[] = [];
79
+ for (let i = 0; i < max; i++) {
80
+ const idx = leaderboard.sortedIndices[i];
81
+ const rec = byIndex.get(idx);
82
+ if (rec) {
83
+ top.push(rec);
84
+ }
85
+ }
86
+
87
+ return top;
88
+ } catch (e) {
89
+ // Fallback: no leaderboardState available, sort by creditsObserved.
90
+ const all = await this.program.account.validatorRecord.all();
91
+
92
+ all.sort((a, b) => {
93
+ const va = a.account;
94
+ const vb = b.account;
95
+ const ca = va.creditsObserved;
96
+ const cb = vb.creditsObserved;
97
+ // BN.cmp: -1 if a < b, 0 if equal, 1 if a > b
98
+ return cb.cmp(ca);
99
+ });
100
+
101
+ return all.slice(0, limit).map(r => r.account);
102
+ }
58
103
  }
59
104
  }