genlayer-js 0.18.4 → 0.18.6

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 (43) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/CLAUDE.md +66 -0
  3. package/README.md +52 -1
  4. package/dist/chains/index.cjs +2 -2
  5. package/dist/chains/index.d.cts +2 -2
  6. package/dist/chains/index.d.ts +2 -2
  7. package/dist/chains/index.js +1 -1
  8. package/dist/{chains-BIe_Q0mF.d.cts → chains-B7B7UXdn.d.cts} +6 -2
  9. package/dist/{chains-BIe_Q0mF.d.ts → chains-B7B7UXdn.d.ts} +6 -2
  10. package/dist/{chunk-NO75TOQL.js → chunk-KVHGQTAI.js} +539 -4
  11. package/dist/{chunk-SMGWE7OH.cjs → chunk-QAAO2WJL.cjs} +540 -5
  12. package/dist/index-3leEwFoq.d.cts +1389 -0
  13. package/dist/index-BBh1NZjP.d.ts +1389 -0
  14. package/dist/{index-C5zeayBB.d.cts → index-BVDASTaU.d.cts} +1 -1
  15. package/dist/{index-BpFWfpio.d.ts → index-ucNO2REF.d.ts} +1 -1
  16. package/dist/index.cjs +551 -38
  17. package/dist/index.d.cts +17 -5
  18. package/dist/index.d.ts +17 -5
  19. package/dist/index.js +528 -15
  20. package/dist/types/index.cjs +2 -2
  21. package/dist/types/index.d.cts +2 -2
  22. package/dist/types/index.d.ts +2 -2
  23. package/dist/types/index.js +1 -1
  24. package/package.json +1 -1
  25. package/src/abi/index.ts +1 -0
  26. package/src/abi/staking.ts +525 -0
  27. package/src/chains/localnet.ts +2 -1
  28. package/src/chains/studionet.ts +1 -0
  29. package/src/chains/testnetAsimov.ts +10 -3
  30. package/src/client/client.ts +17 -15
  31. package/src/index.ts +1 -0
  32. package/src/staking/actions.ts +609 -0
  33. package/src/staking/index.ts +2 -0
  34. package/src/staking/utils.ts +22 -0
  35. package/src/types/chains.ts +6 -2
  36. package/src/types/clients.ts +6 -6
  37. package/src/types/index.ts +1 -0
  38. package/src/types/staking.ts +228 -0
  39. package/tsconfig.vitest-temp.json +41 -0
  40. package/dist/index-B2AY6_eD.d.ts +0 -407
  41. package/dist/index-BYma5s90.d.cts +0 -407
  42. /package/dist/{chunk-FPZNF3JH.cjs → chunk-W4V73RPN.cjs} +0 -0
  43. /package/dist/{chunk-47QDX7IX.js → chunk-ZHBOSLFN.js} +0 -0
@@ -14,6 +14,7 @@ import {accountActions} from "../accounts/actions";
14
14
  import {contractActions} from "../contracts/actions";
15
15
  import {receiptActions, transactionActions} from "../transactions/actions";
16
16
  import {walletActions as genlayerWalletActions} from "../wallet/actions";
17
+ import {stakingActions} from "../staking/actions";
17
18
  import {GenLayerClient, GenLayerChain} from "@/types";
18
19
  import {chainActions} from "@/chains/actions";
19
20
  import {localnet} from "@/chains";
@@ -38,17 +39,18 @@ const getCustomTransportConfig = (config: ClientConfig) => {
38
39
  return {
39
40
  async request({method, params = []}: {method: string; params: any[]}) {
40
41
  if (method.startsWith("eth_") && isAddress) {
41
- try {
42
- const provider = config.provider || window.ethereum;
43
- if (!provider) {
44
- throw new Error('No wallet provider available');
42
+ const provider = config.provider || (typeof window !== "undefined" ? window.ethereum : undefined);
43
+ if (provider) {
44
+ try {
45
+ return await provider.request({method, params});
46
+ } catch (err) {
47
+ console.warn(`Error using provider for method ${method}:`, err);
48
+ throw err;
45
49
  }
46
- return await provider.request({method, params});
47
- } catch (err) {
48
- console.warn(`Error using provider for method ${method}:`, err);
49
- throw err;
50
50
  }
51
- } else {
51
+ }
52
+
53
+ {
52
54
  if (!config.chain) {
53
55
  throw new Error("Chain is not set");
54
56
  }
@@ -100,13 +102,11 @@ export const createClient = (config: ClientConfig = {chain: localnet}): GenLayer
100
102
  ...(config.account ? {account: config.account} : {}),
101
103
  });
102
104
 
103
- // First extend with basic actions
104
105
  const clientWithBasicActions = baseClient
105
106
  .extend(publicActions)
106
107
  .extend(walletActions)
107
108
  .extend(client => accountActions(client as unknown as GenLayerClient<GenLayerChain>));
108
109
 
109
- // First add transaction actions, then contract actions that depend on them
110
110
  const clientWithTransactionActions = {
111
111
  ...clientWithBasicActions,
112
112
  ...transactionActions(clientWithBasicActions as unknown as GenLayerClient<GenLayerChain>, publicClient),
@@ -114,19 +114,21 @@ export const createClient = (config: ClientConfig = {chain: localnet}): GenLayer
114
114
  ...genlayerWalletActions(clientWithBasicActions as unknown as GenLayerClient<GenLayerChain>),
115
115
  } as unknown as GenLayerClient<GenLayerChain>;
116
116
 
117
- // Then add contract actions that can now access transaction actions
118
117
  const clientWithAllActions = {
119
118
  ...clientWithTransactionActions,
120
119
  ...contractActions(clientWithTransactionActions as unknown as GenLayerClient<GenLayerChain>, publicClient),
121
120
  } as unknown as GenLayerClient<GenLayerChain>;
122
121
 
123
- // Add transaction actions last, after all other actions are in place
124
- const finalClient = {
122
+ const clientWithReceiptActions = {
125
123
  ...clientWithAllActions,
126
124
  ...receiptActions(clientWithAllActions as unknown as GenLayerClient<GenLayerChain>, publicClient),
127
125
  } as unknown as GenLayerClient<GenLayerChain>;
128
126
 
129
- // Initialize in the background
127
+ const finalClient = {
128
+ ...clientWithReceiptActions,
129
+ ...stakingActions(clientWithReceiptActions as unknown as GenLayerClient<GenLayerChain>, publicClient),
130
+ } as unknown as GenLayerClient<GenLayerChain>;
131
+
130
132
  finalClient.initializeConsensusSmartContract().catch(error => {
131
133
  console.error("Failed to initialize consensus smart contract:", error);
132
134
  });
package/src/index.ts CHANGED
@@ -9,3 +9,4 @@ export {
9
9
  } from "./transactions/decoders";
10
10
  export * as chains from "./chains";
11
11
  export * as abi from "./abi";
12
+ export {parseStakingAmount, formatStakingAmount} from "./staking";
@@ -0,0 +1,609 @@
1
+ import {getContract, decodeEventLog, PublicClient, Client, Transport, Chain, Account, Address as ViemAddress, GetContractReturnType, toHex, encodeFunctionData, BaseError, ContractFunctionRevertedError} from "viem";
2
+ import {GenLayerClient, GenLayerChain, Address} from "@/types";
3
+ import {STAKING_ABI, VALIDATOR_WALLET_ABI} from "@/abi/staking";
4
+ import {parseStakingAmount, formatStakingAmount} from "./utils";
5
+ import {
6
+ ValidatorInfo,
7
+ ValidatorIdentity,
8
+ BannedValidatorInfo,
9
+ StakeInfo,
10
+ EpochInfo,
11
+ StakingTransactionResult,
12
+ ValidatorJoinResult,
13
+ DelegatorJoinResult,
14
+ ValidatorJoinOptions,
15
+ ValidatorDepositOptions,
16
+ ValidatorExitOptions,
17
+ ValidatorClaimOptions,
18
+ ValidatorPrimeOptions,
19
+ SetOperatorOptions,
20
+ SetIdentityOptions,
21
+ DelegatorJoinOptions,
22
+ DelegatorExitOptions,
23
+ DelegatorClaimOptions,
24
+ StakingContract,
25
+ PendingDeposit,
26
+ PendingWithdrawal,
27
+ } from "@/types/staking";
28
+
29
+ type ReadOnlyStakingContract = GetContractReturnType<typeof STAKING_ABI, PublicClient, ViemAddress>;
30
+ type WalletClientWithAccount = Client<Transport, Chain, Account>;
31
+
32
+ const FALLBACK_GAS = 1000000n;
33
+ const GAS_BUFFER_MULTIPLIER = 2n;
34
+
35
+ function extractRevertReason(err: unknown): string {
36
+ if (err instanceof BaseError) {
37
+ const revertError = err.walk((e) => e instanceof ContractFunctionRevertedError);
38
+ if (revertError instanceof ContractFunctionRevertedError) {
39
+ return revertError.data?.errorName || revertError.reason || "Unknown reason";
40
+ }
41
+ if (err.shortMessage) return err.shortMessage;
42
+ }
43
+ if (err instanceof Error) return err.message;
44
+ return "Unknown reason";
45
+ }
46
+
47
+ export const stakingActions = (
48
+ client: GenLayerClient<GenLayerChain>,
49
+ publicClient: PublicClient,
50
+ ) => {
51
+ const executeWrite = async (options: {
52
+ to: ViemAddress;
53
+ data: `0x${string}`;
54
+ value?: bigint;
55
+ gas?: bigint;
56
+ }): Promise<StakingTransactionResult> => {
57
+ if (!client.account) {
58
+ throw new Error("Account is required for write operations. Initialize client with a wallet account.");
59
+ }
60
+ const account = client.account;
61
+
62
+ try {
63
+ await publicClient.call({
64
+ account,
65
+ to: options.to,
66
+ data: options.data,
67
+ value: options.value,
68
+ });
69
+ } catch (err: unknown) {
70
+ const revertReason = extractRevertReason(err);
71
+ throw new Error(`Transaction would revert: ${revertReason}`);
72
+ }
73
+
74
+ let gasLimit = options.gas;
75
+ if (!gasLimit) {
76
+ try {
77
+ const estimated = await publicClient.estimateGas({
78
+ account,
79
+ to: options.to,
80
+ data: options.data,
81
+ value: options.value,
82
+ });
83
+ gasLimit = estimated * GAS_BUFFER_MULTIPLIER;
84
+ } catch {
85
+ gasLimit = FALLBACK_GAS;
86
+ }
87
+ }
88
+
89
+ const nonce = await publicClient.getTransactionCount({address: account.address as ViemAddress});
90
+
91
+ const txRequest = await publicClient.prepareTransactionRequest({
92
+ account,
93
+ to: options.to,
94
+ data: options.data,
95
+ value: options.value,
96
+ type: "legacy",
97
+ nonce,
98
+ gas: gasLimit,
99
+ chain: client.chain,
100
+ });
101
+
102
+ const signTransaction = account.signTransaction;
103
+ if (!signTransaction) {
104
+ throw new Error("Account does not support signing transactions");
105
+ }
106
+ const serializedTx = await signTransaction(txRequest as Parameters<typeof signTransaction>[0]);
107
+ const hash = await publicClient.sendRawTransaction({serializedTransaction: serializedTx});
108
+ const receipt = await publicClient.waitForTransactionReceipt({hash});
109
+
110
+ if (receipt.status === "reverted") {
111
+ let revertReason = "Unknown reason";
112
+ try {
113
+ await publicClient.call({
114
+ account,
115
+ to: options.to,
116
+ data: options.data,
117
+ value: options.value,
118
+ blockNumber: receipt.blockNumber,
119
+ });
120
+ const gasUsed = receipt.gasUsed;
121
+ if (gasUsed >= gasLimit - 1000n) {
122
+ revertReason = `Out of gas (used ${gasUsed}, limit ${gasLimit})`;
123
+ } else {
124
+ revertReason = `Unknown (simulation passes but tx reverts). Gas: ${gasUsed}/${gasLimit}`;
125
+ }
126
+ } catch (err: unknown) {
127
+ revertReason = extractRevertReason(err);
128
+ }
129
+ throw new Error(`Transaction reverted: ${revertReason} (tx: ${hash})`);
130
+ }
131
+
132
+ return {
133
+ transactionHash: receipt.transactionHash,
134
+ blockNumber: receipt.blockNumber,
135
+ gasUsed: receipt.gasUsed,
136
+ };
137
+ };
138
+
139
+ const getStakingAddress = (): ViemAddress => {
140
+ const stakingConfig = client.chain.stakingContract;
141
+ if (!stakingConfig?.address || stakingConfig.address === "0x0000000000000000000000000000000000000000") {
142
+ throw new Error("Staking is not supported on studio-based networks. Use testnet-asimov for staking operations.");
143
+ }
144
+ return stakingConfig.address as ViemAddress;
145
+ };
146
+
147
+ const getStakingContract = (): StakingContract => {
148
+ const address = getStakingAddress();
149
+ return getContract({
150
+ address,
151
+ abi: STAKING_ABI,
152
+ client: {public: publicClient, wallet: client as unknown as WalletClientWithAccount},
153
+ });
154
+ };
155
+
156
+ const getReadOnlyStakingContract = (): ReadOnlyStakingContract => {
157
+ const address = getStakingAddress();
158
+ return getContract({
159
+ address,
160
+ abi: STAKING_ABI,
161
+ client: publicClient,
162
+ });
163
+ };
164
+
165
+ return {
166
+ validatorJoin: async (options: ValidatorJoinOptions): Promise<ValidatorJoinResult> => {
167
+ const amount = parseStakingAmount(options.amount);
168
+ const stakingAddress = getStakingAddress();
169
+
170
+ const data = options.operator
171
+ ? encodeFunctionData({
172
+ abi: STAKING_ABI,
173
+ functionName: "validatorJoin",
174
+ args: [options.operator as ViemAddress],
175
+ })
176
+ : encodeFunctionData({
177
+ abi: STAKING_ABI,
178
+ functionName: "validatorJoin",
179
+ });
180
+
181
+ const result = await executeWrite({to: stakingAddress, data, value: amount});
182
+ const receipt = await publicClient.getTransactionReceipt({hash: result.transactionHash});
183
+
184
+ let validatorWallet: Address | undefined;
185
+ let eventFound = false;
186
+ for (const log of receipt.logs) {
187
+ try {
188
+ const decoded = decodeEventLog({abi: STAKING_ABI, data: log.data, topics: log.topics});
189
+ if (decoded.eventName === "ValidatorJoin") {
190
+ validatorWallet = (decoded.args as {validator: Address}).validator;
191
+ eventFound = true;
192
+ break;
193
+ }
194
+ } catch {
195
+ // Not a ValidatorJoin event - continue searching
196
+ }
197
+ }
198
+
199
+ if (!eventFound) {
200
+ throw new Error(
201
+ `ValidatorJoin event not found in transaction ${result.transactionHash}. ` +
202
+ `Transaction succeeded but validator wallet address could not be determined.`,
203
+ );
204
+ }
205
+
206
+ return {
207
+ transactionHash: receipt.transactionHash,
208
+ blockNumber: receipt.blockNumber,
209
+ gasUsed: receipt.gasUsed,
210
+ validatorWallet: validatorWallet!,
211
+ operator: options.operator || (client.account!.address as Address),
212
+ amount: formatStakingAmount(amount),
213
+ amountRaw: amount,
214
+ };
215
+ },
216
+
217
+ validatorDeposit: async (options: ValidatorDepositOptions): Promise<StakingTransactionResult> => {
218
+ const amount = parseStakingAmount(options.amount);
219
+ const data = encodeFunctionData({
220
+ abi: STAKING_ABI,
221
+ functionName: "validatorDeposit",
222
+ });
223
+ return executeWrite({to: getStakingAddress(), data, value: amount});
224
+ },
225
+
226
+ validatorExit: async (options: ValidatorExitOptions): Promise<StakingTransactionResult> => {
227
+ const shares = typeof options.shares === "string" ? BigInt(options.shares) : options.shares;
228
+ const data = encodeFunctionData({
229
+ abi: STAKING_ABI,
230
+ functionName: "validatorExit",
231
+ args: [shares],
232
+ });
233
+ return executeWrite({to: getStakingAddress(), data});
234
+ },
235
+
236
+ validatorClaim: async (options?: ValidatorClaimOptions): Promise<StakingTransactionResult & {claimedAmount: bigint}> => {
237
+ if (!options?.validator && !client.account) {
238
+ throw new Error("Either provide validator address or initialize client with an account");
239
+ }
240
+ const validatorAddress = options?.validator || (client.account!.address as Address);
241
+ const data = encodeFunctionData({
242
+ abi: STAKING_ABI,
243
+ functionName: "validatorClaim",
244
+ args: [validatorAddress as ViemAddress],
245
+ });
246
+ const result = await executeWrite({to: getStakingAddress(), data});
247
+ // TODO: Parse ClaimAmount from logs if needed
248
+ return {...result, claimedAmount: 0n};
249
+ },
250
+
251
+ validatorPrime: async (options: ValidatorPrimeOptions): Promise<StakingTransactionResult> => {
252
+ const data = encodeFunctionData({
253
+ abi: STAKING_ABI,
254
+ functionName: "validatorPrime",
255
+ args: [options.validator as ViemAddress],
256
+ });
257
+ return executeWrite({to: getStakingAddress(), data});
258
+ },
259
+
260
+ setOperator: async (options: SetOperatorOptions): Promise<StakingTransactionResult> => {
261
+ const data = encodeFunctionData({
262
+ abi: VALIDATOR_WALLET_ABI,
263
+ functionName: "setOperator",
264
+ args: [options.operator as ViemAddress],
265
+ });
266
+ return executeWrite({to: options.validator as ViemAddress, data});
267
+ },
268
+
269
+ setIdentity: async (options: SetIdentityOptions): Promise<StakingTransactionResult> => {
270
+ let extraCidBytes: `0x${string}` = "0x";
271
+ if (options.extraCid) {
272
+ if (options.extraCid.startsWith("0x")) {
273
+ extraCidBytes = options.extraCid as `0x${string}`;
274
+ } else {
275
+ extraCidBytes = toHex(new TextEncoder().encode(options.extraCid));
276
+ }
277
+ }
278
+ const data = encodeFunctionData({
279
+ abi: VALIDATOR_WALLET_ABI,
280
+ functionName: "setIdentity",
281
+ args: [
282
+ options.moniker,
283
+ options.logoUri || "",
284
+ options.website || "",
285
+ options.description || "",
286
+ options.email || "",
287
+ options.twitter || "",
288
+ options.telegram || "",
289
+ options.github || "",
290
+ extraCidBytes,
291
+ ],
292
+ });
293
+ return executeWrite({to: options.validator as ViemAddress, data});
294
+ },
295
+
296
+ delegatorJoin: async (options: DelegatorJoinOptions): Promise<DelegatorJoinResult> => {
297
+ const amount = parseStakingAmount(options.amount);
298
+ const data = encodeFunctionData({
299
+ abi: STAKING_ABI,
300
+ functionName: "delegatorJoin",
301
+ args: [options.validator as ViemAddress],
302
+ });
303
+ const result = await executeWrite({to: getStakingAddress(), data, value: amount});
304
+
305
+ return {
306
+ ...result,
307
+ validator: options.validator,
308
+ delegator: client.account!.address as Address,
309
+ amount: formatStakingAmount(amount),
310
+ amountRaw: amount,
311
+ };
312
+ },
313
+
314
+ delegatorExit: async (options: DelegatorExitOptions): Promise<StakingTransactionResult> => {
315
+ const shares = typeof options.shares === "string" ? BigInt(options.shares) : options.shares;
316
+ const data = encodeFunctionData({
317
+ abi: STAKING_ABI,
318
+ functionName: "delegatorExit",
319
+ args: [options.validator as ViemAddress, shares],
320
+ });
321
+ return executeWrite({to: getStakingAddress(), data});
322
+ },
323
+
324
+ delegatorClaim: async (options: DelegatorClaimOptions): Promise<StakingTransactionResult> => {
325
+ if (!options.delegator && !client.account) {
326
+ throw new Error("Either provide delegator address or initialize client with an account");
327
+ }
328
+ const delegatorAddress = options.delegator || (client.account!.address as Address);
329
+ const data = encodeFunctionData({
330
+ abi: STAKING_ABI,
331
+ functionName: "delegatorClaim",
332
+ args: [delegatorAddress as ViemAddress, options.validator as ViemAddress],
333
+ });
334
+ return executeWrite({to: getStakingAddress(), data});
335
+ },
336
+
337
+ isValidator: async (address: Address): Promise<boolean> => {
338
+ const contract = getReadOnlyStakingContract();
339
+ return contract.read.isValidator([address as ViemAddress]) as Promise<boolean>;
340
+ },
341
+
342
+ getValidatorInfo: async (validator: Address): Promise<ValidatorInfo> => {
343
+ const contract = getReadOnlyStakingContract();
344
+
345
+ const isVal = await contract.read.isValidator([validator as ViemAddress]);
346
+ if (!isVal) {
347
+ throw new Error(`Address ${validator} is not a validator`);
348
+ }
349
+
350
+ // Get validator wallet contract for owner/operator/identity
351
+ const walletContract = getContract({
352
+ address: validator as ViemAddress,
353
+ abi: VALIDATOR_WALLET_ABI,
354
+ client: publicClient,
355
+ });
356
+
357
+ // Fetch all data in parallel
358
+ const [view, owner, operator, identityRaw, currentEpoch] = await Promise.all([
359
+ contract.read.validatorView([validator as ViemAddress]) as Promise<any>,
360
+ walletContract.read.owner() as Promise<Address>,
361
+ walletContract.read.operator() as Promise<Address>,
362
+ walletContract.read.getIdentity().catch(() => null) as Promise<any>,
363
+ contract.read.epoch() as Promise<bigint>,
364
+ ]);
365
+
366
+ // Parse identity if available
367
+ let identity: ValidatorIdentity | undefined;
368
+ if (identityRaw && identityRaw.moniker) {
369
+ identity = {
370
+ moniker: identityRaw.moniker,
371
+ logoUri: identityRaw.logoUri,
372
+ website: identityRaw.website,
373
+ description: identityRaw.description,
374
+ email: identityRaw.email,
375
+ twitter: identityRaw.twitter,
376
+ telegram: identityRaw.telegram,
377
+ github: identityRaw.github,
378
+ extraCid: identityRaw.extraCid ? toHex(identityRaw.extraCid) : "",
379
+ };
380
+ }
381
+
382
+ // Validator needs priming if ePrimed < currentEpoch - 1
383
+ const needsPriming = currentEpoch > 0n && view.ePrimed < currentEpoch - 1n;
384
+
385
+ // Fetch pending self-stake deposits
386
+ const depositLen = (await contract.read.validatorDepositLen([validator as ViemAddress])) as bigint;
387
+ const pendingDeposits: PendingDeposit[] = [];
388
+
389
+ for (let i = 0n; i < depositLen; i++) {
390
+ const [epoch, commit] = (await contract.read.validatorDeposit([validator as ViemAddress, i])) as [
391
+ bigint,
392
+ {input: bigint; output: bigint; epoch: bigint; linkToNextCommit: bigint},
393
+ ];
394
+ pendingDeposits.push({
395
+ epoch,
396
+ stake: formatStakingAmount(commit.input),
397
+ stakeRaw: commit.input,
398
+ shares: commit.output,
399
+ });
400
+ }
401
+
402
+ // Fetch pending self-stake withdrawals
403
+ const withdrawalLen = (await contract.read.validatorWithdrawalLen([validator as ViemAddress])) as bigint;
404
+ const pendingWithdrawals: PendingWithdrawal[] = [];
405
+
406
+ for (let i = 0n; i < withdrawalLen; i++) {
407
+ const [epoch, commit] = (await contract.read.validatorWithdrawal([validator as ViemAddress, i])) as [
408
+ bigint,
409
+ {input: bigint; output: bigint; epoch: bigint; linkToNextCommit: bigint},
410
+ ];
411
+ pendingWithdrawals.push({
412
+ epoch,
413
+ shares: commit.input,
414
+ stake: formatStakingAmount(commit.output),
415
+ stakeRaw: commit.output,
416
+ });
417
+ }
418
+
419
+ return {
420
+ address: validator,
421
+ owner,
422
+ operator,
423
+ vStake: formatStakingAmount(view.vStake),
424
+ vStakeRaw: view.vStake,
425
+ vShares: view.vShares,
426
+ dStake: formatStakingAmount(view.dStake),
427
+ dStakeRaw: view.dStake,
428
+ dShares: view.dShares,
429
+ vDeposit: formatStakingAmount(view.vDeposit),
430
+ vDepositRaw: view.vDeposit,
431
+ vWithdrawal: formatStakingAmount(view.vWithdrawal),
432
+ vWithdrawalRaw: view.vWithdrawal,
433
+ ePrimed: view.ePrimed,
434
+ live: view.live,
435
+ banned: view.eBanned > 0n,
436
+ bannedEpoch: view.eBanned > 0n ? view.eBanned : undefined,
437
+ needsPriming,
438
+ identity,
439
+ pendingDeposits,
440
+ pendingWithdrawals,
441
+ };
442
+ },
443
+
444
+ getStakeInfo: async (delegator: Address, validator: Address): Promise<StakeInfo> => {
445
+ const contract = getReadOnlyStakingContract();
446
+
447
+ const shares = (await contract.read.sharesOf([delegator as ViemAddress, validator as ViemAddress])) as bigint;
448
+ // stakeOf divides by shares, so it fails with division by zero if no shares yet
449
+ let stake = 0n;
450
+ if (shares > 0n) {
451
+ stake = (await contract.read.stakeOf([delegator as ViemAddress, validator as ViemAddress])) as bigint;
452
+ }
453
+
454
+ // Fetch pending delegator deposits
455
+ const depositLen = (await contract.read.delegatorDepositLen([
456
+ delegator as ViemAddress,
457
+ validator as ViemAddress,
458
+ ])) as bigint;
459
+ const pendingDeposits: PendingDeposit[] = [];
460
+
461
+ for (let i = 0n; i < depositLen; i++) {
462
+ const [claim, commit] = (await contract.read.delegatorDeposit([
463
+ delegator as ViemAddress,
464
+ validator as ViemAddress,
465
+ i,
466
+ ])) as [
467
+ {quantity: bigint; commit: bigint},
468
+ {input: bigint; output: bigint; epoch: bigint; linkToNextCommit: bigint},
469
+ ];
470
+ pendingDeposits.push({
471
+ epoch: commit.epoch,
472
+ stake: formatStakingAmount(commit.input),
473
+ stakeRaw: commit.input,
474
+ shares: claim.quantity,
475
+ });
476
+ }
477
+
478
+ // Fetch pending delegator withdrawals
479
+ const withdrawalLen = (await contract.read.delegatorWithdrawalLen([
480
+ delegator as ViemAddress,
481
+ validator as ViemAddress,
482
+ ])) as bigint;
483
+ const pendingWithdrawals: PendingWithdrawal[] = [];
484
+
485
+ for (let i = 0n; i < withdrawalLen; i++) {
486
+ const [claim, commit] = (await contract.read.delegatorWithdrawal([
487
+ delegator as ViemAddress,
488
+ validator as ViemAddress,
489
+ i,
490
+ ])) as [
491
+ {quantity: bigint; commit: bigint},
492
+ {input: bigint; output: bigint; epoch: bigint; linkToNextCommit: bigint},
493
+ ];
494
+ pendingWithdrawals.push({
495
+ epoch: commit.epoch,
496
+ shares: claim.quantity,
497
+ stake: formatStakingAmount(commit.output),
498
+ stakeRaw: commit.output,
499
+ });
500
+ }
501
+
502
+ return {
503
+ delegator,
504
+ validator,
505
+ shares,
506
+ stake: formatStakingAmount(stake),
507
+ stakeRaw: stake,
508
+ pendingDeposits,
509
+ pendingWithdrawals,
510
+ };
511
+ },
512
+
513
+ getEpochInfo: async (): Promise<EpochInfo> => {
514
+ const contract = getReadOnlyStakingContract();
515
+
516
+ const [
517
+ epoch,
518
+ validatorMinStake,
519
+ delegatorMinStake,
520
+ activeCount,
521
+ epochMinDuration,
522
+ epochZeroMinDuration,
523
+ epochOdd,
524
+ epochEven,
525
+ ] = await Promise.all([
526
+ contract.read.epoch() as Promise<bigint>,
527
+ contract.read.validatorMinStake() as Promise<bigint>,
528
+ contract.read.delegatorMinStake() as Promise<bigint>,
529
+ contract.read.activeValidatorsCount() as Promise<bigint>,
530
+ contract.read.epochMinDuration() as Promise<bigint>,
531
+ contract.read.epochZeroMinDuration() as Promise<bigint>,
532
+ contract.read.epochOdd() as Promise<any>,
533
+ contract.read.epochEven() as Promise<any>,
534
+ ]);
535
+
536
+ // Current epoch data (even epochs use epochEven, odd use epochOdd)
537
+ const currentEpochData = epoch % 2n === 0n ? epochEven : epochOdd;
538
+ const currentEpochStart = new Date(Number(currentEpochData.start) * 1000);
539
+ const currentEpochEnd = currentEpochData.end > 0n ? new Date(Number(currentEpochData.end) * 1000) : null;
540
+
541
+ // Estimate next epoch: current start + min duration (if epoch hasn't ended)
542
+ // Epoch 0 uses epochZeroMinDuration, all other epochs use epochMinDuration
543
+ let nextEpochEstimate: Date | null = null;
544
+ if (!currentEpochEnd) {
545
+ const duration = epoch === 0n ? epochZeroMinDuration : epochMinDuration;
546
+ const estimatedEndMs = Number(currentEpochData.start + duration) * 1000;
547
+ nextEpochEstimate = new Date(estimatedEndMs);
548
+ }
549
+
550
+ return {
551
+ currentEpoch: epoch,
552
+ validatorMinStake: formatStakingAmount(validatorMinStake),
553
+ validatorMinStakeRaw: validatorMinStake,
554
+ delegatorMinStake: formatStakingAmount(delegatorMinStake),
555
+ delegatorMinStakeRaw: delegatorMinStake,
556
+ activeValidatorsCount: activeCount,
557
+ epochMinDuration,
558
+ currentEpochStart,
559
+ currentEpochEnd,
560
+ nextEpochEstimate,
561
+ inflation: formatStakingAmount(currentEpochData.inflation),
562
+ inflationRaw: currentEpochData.inflation,
563
+ totalWeight: currentEpochData.weight,
564
+ totalClaimed: formatStakingAmount(currentEpochData.claimed),
565
+ totalClaimedRaw: currentEpochData.claimed,
566
+ };
567
+ },
568
+
569
+ getActiveValidators: async (): Promise<Address[]> => {
570
+ const contract = getReadOnlyStakingContract();
571
+ const validators = (await contract.read.activeValidators()) as Address[];
572
+ return validators.filter(v => v !== "0x0000000000000000000000000000000000000000");
573
+ },
574
+
575
+ getActiveValidatorsCount: async (): Promise<bigint> => {
576
+ const contract = getReadOnlyStakingContract();
577
+ return contract.read.activeValidatorsCount() as Promise<bigint>;
578
+ },
579
+
580
+ getQuarantinedValidators: async (): Promise<Address[]> => {
581
+ const contract = getReadOnlyStakingContract();
582
+ return contract.read.getQuarantinedValidators() as Promise<Address[]>;
583
+ },
584
+
585
+ getBannedValidators: async (startIndex = 0n, size = 100n): Promise<BannedValidatorInfo[]> => {
586
+ const contract = getReadOnlyStakingContract();
587
+ const result = (await contract.read.getAllBannedValidators([startIndex, size])) as any[];
588
+ return result.map((v: any) => ({
589
+ validator: v.validator as Address,
590
+ untilEpoch: v.untilEpochBanned,
591
+ permanentlyBanned: v.permanentlyBanned,
592
+ }));
593
+ },
594
+
595
+ getQuarantinedValidatorsDetailed: async (startIndex = 0n, size = 100n): Promise<BannedValidatorInfo[]> => {
596
+ const contract = getReadOnlyStakingContract();
597
+ const result = (await contract.read.getAllQuarantinedValidators([startIndex, size])) as any[];
598
+ return result.map((v: any) => ({
599
+ validator: v.validator as Address,
600
+ untilEpoch: v.untilEpochBanned,
601
+ permanentlyBanned: v.permanentlyBanned,
602
+ }));
603
+ },
604
+
605
+ getStakingContract,
606
+ parseStakingAmount,
607
+ formatStakingAmount,
608
+ };
609
+ };
@@ -0,0 +1,2 @@
1
+ export {stakingActions} from "./actions";
2
+ export {parseStakingAmount, formatStakingAmount} from "./utils";