@wireio/stake 0.3.0 → 0.4.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/lib/stake.browser.js +3602 -1921
- package/lib/stake.browser.js.map +1 -1
- package/lib/stake.d.ts +3265 -1358
- package/lib/stake.js +4369 -2728
- package/lib/stake.js.map +1 -1
- package/lib/stake.m.js +3602 -1921
- package/lib/stake.m.js.map +1 -1
- package/package.json +3 -1
- package/src/assets/solana/idl/liqsol_core.json +2327 -887
- package/src/assets/solana/idl/liqsol_token.json +1 -1
- package/src/assets/solana/idl/transfer_hook.json +192 -0
- package/src/assets/solana/idl/validator_leaderboard.json +147 -4
- package/src/assets/solana/types/liqsol_core.ts +2327 -887
- package/src/assets/solana/types/liqsol_token.ts +1 -1
- package/src/assets/solana/types/transfer_hook.ts +198 -0
- package/src/assets/solana/types/validator_leaderboard.ts +147 -4
- package/src/networks/ethereum/ethereum.ts +0 -5
- package/src/networks/solana/clients/deposit.client.ts +154 -8
- package/src/networks/solana/clients/distribution.client.ts +72 -291
- package/src/networks/solana/clients/leaderboard.client.ts +59 -14
- package/src/networks/solana/clients/outpost.client.ts +188 -359
- package/src/networks/solana/clients/token.client.ts +85 -100
- package/src/networks/solana/constants.ts +155 -64
- package/src/networks/solana/solana.ts +273 -153
- package/src/networks/solana/types.ts +531 -71
- package/src/networks/solana/utils.ts +66 -49
- package/src/types.ts +108 -17
|
@@ -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 {
|
|
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
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* -
|
|
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
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
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() {
|
|
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
|
-
*
|
|
61
|
+
* IDL account name: "userRecord"
|
|
76
62
|
*/
|
|
77
|
-
|
|
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
|
-
|
|
105
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
*
|
|
96
|
+
* Convenience: get the user's current distribution shares.
|
|
153
97
|
*
|
|
154
|
-
*
|
|
155
|
-
* - tracked balances come from Distribution.userRecord
|
|
98
|
+
* Returns BN(0) if the user has no userRecord yet.
|
|
156
99
|
*/
|
|
157
|
-
|
|
158
|
-
const
|
|
159
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
const
|
|
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.
|
|
251
|
-
this.getTrackedBalances(),
|
|
119
|
+
this.getUserShares(user),
|
|
252
120
|
]);
|
|
253
121
|
|
|
254
|
-
|
|
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
|
-
|
|
340
|
-
{
|
|
341
|
-
|
|
342
|
-
|
|
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<
|
|
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<
|
|
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
|
|
46
|
-
*
|
|
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<
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
}
|