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.
- package/CHANGELOG.md +9 -0
- package/CLAUDE.md +66 -0
- package/README.md +52 -1
- package/dist/chains/index.cjs +2 -2
- package/dist/chains/index.d.cts +2 -2
- package/dist/chains/index.d.ts +2 -2
- package/dist/chains/index.js +1 -1
- package/dist/{chains-BIe_Q0mF.d.cts → chains-B7B7UXdn.d.cts} +6 -2
- package/dist/{chains-BIe_Q0mF.d.ts → chains-B7B7UXdn.d.ts} +6 -2
- package/dist/{chunk-NO75TOQL.js → chunk-KVHGQTAI.js} +539 -4
- package/dist/{chunk-SMGWE7OH.cjs → chunk-QAAO2WJL.cjs} +540 -5
- package/dist/index-3leEwFoq.d.cts +1389 -0
- package/dist/index-BBh1NZjP.d.ts +1389 -0
- package/dist/{index-C5zeayBB.d.cts → index-BVDASTaU.d.cts} +1 -1
- package/dist/{index-BpFWfpio.d.ts → index-ucNO2REF.d.ts} +1 -1
- package/dist/index.cjs +551 -38
- package/dist/index.d.cts +17 -5
- package/dist/index.d.ts +17 -5
- package/dist/index.js +528 -15
- package/dist/types/index.cjs +2 -2
- package/dist/types/index.d.cts +2 -2
- package/dist/types/index.d.ts +2 -2
- package/dist/types/index.js +1 -1
- package/package.json +1 -1
- package/src/abi/index.ts +1 -0
- package/src/abi/staking.ts +525 -0
- package/src/chains/localnet.ts +2 -1
- package/src/chains/studionet.ts +1 -0
- package/src/chains/testnetAsimov.ts +10 -3
- package/src/client/client.ts +17 -15
- package/src/index.ts +1 -0
- package/src/staking/actions.ts +609 -0
- package/src/staking/index.ts +2 -0
- package/src/staking/utils.ts +22 -0
- package/src/types/chains.ts +6 -2
- package/src/types/clients.ts +6 -6
- package/src/types/index.ts +1 -0
- package/src/types/staking.ts +228 -0
- package/tsconfig.vitest-temp.json +41 -0
- package/dist/index-B2AY6_eD.d.ts +0 -407
- package/dist/index-BYma5s90.d.cts +0 -407
- /package/dist/{chunk-FPZNF3JH.cjs → chunk-W4V73RPN.cjs} +0 -0
- /package/dist/{chunk-47QDX7IX.js → chunk-ZHBOSLFN.js} +0 -0
package/src/client/client.ts
CHANGED
|
@@ -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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|
-
|
|
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
|
@@ -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
|
+
};
|