Skip to content

Commit fa35751

Browse files
committed
fix(staking): validator-deposit/exit work through the CLI
The `staking validator-deposit` and `staking validator-exit` keystore paths called viem's `walletClient.writeContract` directly. viem then negotiates its own fee/tx-type against the GenLayer consensus RPC, which has no EIP-1559 fee support, so the write fails. Every other staking command (and both `vesting validator` commands) instead go through the SDK's staking client, whose `executeWrite` pins `type: "legacy"` and does manual nonce/gas + sign + sendRawTransaction. Route both commands through `client.validatorDeposit` / `client.validatorExit`, which forward to the ValidatorWallet's own functions (preserving msg.sender == ValidatorWallet on re-entry into Staking) over the correct legacy-tx path. The genlayer-e2e cli-driver hard-skips these two with a "SDK bug" note; that was a misdiagnosis — the SDK actions exist and are correct, the CLI just wasn't using them. Add keystore-path tests asserting both commands call the SDK client and never touch getViemClients.
1 parent 4b26cc6 commit fa35751

3 files changed

Lines changed: 116 additions & 27 deletions

File tree

‎src/commands/staking/validatorDeposit.ts‎

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,25 +24,28 @@ export class ValidatorDepositAction extends StakingAction {
2424
const amount = this.parseAmount(options.amount);
2525
const validatorWallet = options.validator as Address;
2626

27-
const {walletClient, publicClient} = await this.getViemClients(options);
27+
// Route through the SDK's staking action rather than a raw viem
28+
// writeContract. The SDK's executeWrite pins `type: "legacy"` and does
29+
// manual nonce/gas + sign + sendRawTransaction, which the GenLayer
30+
// consensus RPC requires (it has no EIP-1559 fee support, so viem's
31+
// default fee/tx-type negotiation fails). The action forwards to the
32+
// ValidatorWallet's own `validatorDeposit`, preserving msg.sender ==
33+
// ValidatorWallet when it re-enters Staking.
34+
const client = await this.getStakingClient(options);
2835

2936
this.setSpinnerText(`Depositing ${this.formatAmount(amount)} to validator ${validatorWallet}...`);
3037

31-
const hash = await walletClient.writeContract({
32-
address: validatorWallet,
33-
abi: abi.VALIDATOR_WALLET_ABI,
34-
functionName: "validatorDeposit",
35-
value: amount,
38+
const result = await client.validatorDeposit({
39+
validator: validatorWallet,
40+
amount,
3641
});
3742

38-
const receipt = await publicClient.waitForTransactionReceipt({hash});
39-
4043
const output = {
41-
transactionHash: receipt.transactionHash,
44+
transactionHash: result.transactionHash,
4245
validator: validatorWallet,
4346
amount: this.formatAmount(amount),
44-
blockNumber: receipt.blockNumber.toString(),
45-
gasUsed: receipt.gasUsed.toString(),
47+
blockNumber: result.blockNumber.toString(),
48+
gasUsed: result.gasUsed.toString(),
4649
};
4750

4851
this.succeedSpinner("Deposit successful!", output);

‎src/commands/staking/validatorExit.ts‎

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -31,30 +31,33 @@ export class ValidatorExitAction extends StakingAction {
3131
}
3232

3333
const validatorWallet = options.validator as Address;
34-
const {walletClient, publicClient} = await this.getViemClients(options);
34+
35+
// Route through the SDK's staking action rather than a raw viem
36+
// writeContract. The SDK's executeWrite pins `type: "legacy"` and does
37+
// manual nonce/gas + sign + sendRawTransaction, which the GenLayer
38+
// consensus RPC requires (it has no EIP-1559 fee support, so viem's
39+
// default fee/tx-type negotiation fails). The action forwards to the
40+
// ValidatorWallet's own `validatorExit`, preserving msg.sender ==
41+
// ValidatorWallet when it re-enters Staking.
42+
const client = await this.getStakingClient(options);
3543

3644
this.setSpinnerText(`Exiting validator ${validatorWallet} with ${shares} shares...`);
3745

38-
const hash = await walletClient.writeContract({
39-
address: validatorWallet,
40-
abi: abi.VALIDATOR_WALLET_ABI,
41-
functionName: "validatorExit",
42-
args: [shares],
46+
const result = await client.validatorExit({
47+
validator: validatorWallet,
48+
shares,
4349
});
4450

45-
const receipt = await publicClient.waitForTransactionReceipt({hash});
46-
4751
// Check epoch to determine note
48-
const readClient = await this.getReadOnlyStakingClient(options);
49-
const epochInfo = await readClient.getEpochInfo();
52+
const epochInfo = await client.getEpochInfo();
5053
const isEpochZero = epochInfo.currentEpoch === 0n;
5154

5255
const output = {
53-
transactionHash: receipt.transactionHash,
56+
transactionHash: result.transactionHash,
5457
validator: validatorWallet,
5558
sharesWithdrawn: shares.toString(),
56-
blockNumber: receipt.blockNumber.toString(),
57-
gasUsed: receipt.gasUsed.toString(),
59+
blockNumber: result.blockNumber.toString(),
60+
gasUsed: result.gasUsed.toString(),
5861
note: isEpochZero
5962
? "Epoch 0: Withdrawal claimable immediately"
6063
: "Withdrawal will be claimable after the unbonding period",

‎tests/actions/staking.test.ts‎

Lines changed: 86 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -150,9 +150,92 @@ describe("ValidatorJoinAction", () => {
150150
});
151151
});
152152

153-
// ValidatorDepositAction, ValidatorExitAction, ValidatorClaimAction tests
154-
// are covered by command-level tests. These actions now use viem directly
155-
// to call ValidatorWallet contracts and require complex viem mocking.
153+
// ValidatorDepositAction / ValidatorExitAction: keystore path goes through the
154+
// SDK staking client (client.validatorDeposit / client.validatorExit), matching
155+
// every other staking command. Previously these two used raw viem
156+
// writeContract, which fails on the GenLayer consensus RPC (no EIP-1559 fee
157+
// support) — see fix in validatorDeposit.ts / validatorExit.ts.
158+
describe("ValidatorDepositAction", () => {
159+
let action: ValidatorDepositAction;
160+
161+
beforeEach(() => {
162+
vi.clearAllMocks();
163+
action = new ValidatorDepositAction();
164+
setupActionMocks(action);
165+
mockClient.validatorDeposit.mockResolvedValue(mockTxResult);
166+
});
167+
168+
afterEach(() => {
169+
vi.restoreAllMocks();
170+
});
171+
172+
test("deposits to validator via the SDK client (not raw viem)", async () => {
173+
const getViemSpy = vi.spyOn(action as any, "getViemClients");
174+
175+
await action.execute({validator: "0xValidatorWallet", amount: "10gen", stakingAddress: "0xStaking"});
176+
177+
expect(mockClient.validatorDeposit).toHaveBeenCalledWith({
178+
validator: "0xValidatorWallet",
179+
amount: expect.any(BigInt),
180+
});
181+
expect(getViemSpy).not.toHaveBeenCalled();
182+
expect(action["succeedSpinner"]).toHaveBeenCalledWith("Deposit successful!", expect.any(Object));
183+
});
184+
185+
test("handles errors", async () => {
186+
mockClient.validatorDeposit.mockRejectedValue(new Error("deposit failed"));
187+
188+
await action.execute({validator: "0xValidatorWallet", amount: "10gen", stakingAddress: "0xStaking"});
189+
190+
expect(action["failSpinner"]).toHaveBeenCalledWith("Failed to make deposit", "deposit failed");
191+
});
192+
});
193+
194+
describe("ValidatorExitAction", () => {
195+
let action: ValidatorExitAction;
196+
197+
beforeEach(() => {
198+
vi.clearAllMocks();
199+
action = new ValidatorExitAction();
200+
setupActionMocks(action);
201+
mockClient.validatorExit.mockResolvedValue(mockTxResult);
202+
mockClient.getEpochInfo.mockResolvedValue(mockEpochInfo);
203+
});
204+
205+
afterEach(() => {
206+
vi.restoreAllMocks();
207+
});
208+
209+
test("exits validator via the SDK client (not raw viem)", async () => {
210+
const getViemSpy = vi.spyOn(action as any, "getViemClients");
211+
212+
await action.execute({validator: "0xValidatorWallet", shares: "50", stakingAddress: "0xStaking"});
213+
214+
expect(mockClient.validatorExit).toHaveBeenCalledWith({
215+
validator: "0xValidatorWallet",
216+
shares: 50n,
217+
});
218+
expect(getViemSpy).not.toHaveBeenCalled();
219+
expect(action["succeedSpinner"]).toHaveBeenCalledWith("Exit initiated successfully!", expect.any(Object));
220+
});
221+
222+
test("rejects a non-positive shares value before calling the client", async () => {
223+
await action.execute({validator: "0xValidatorWallet", shares: "0", stakingAddress: "0xStaking"});
224+
225+
expect(mockClient.validatorExit).not.toHaveBeenCalled();
226+
expect(action["failSpinner"]).toHaveBeenCalledWith(
227+
'Invalid shares value: "0". Must be a positive whole number.',
228+
);
229+
});
230+
231+
test("handles errors", async () => {
232+
mockClient.validatorExit.mockRejectedValue(new Error("exit failed"));
233+
234+
await action.execute({validator: "0xValidatorWallet", shares: "50", stakingAddress: "0xStaking"});
235+
236+
expect(action["failSpinner"]).toHaveBeenCalledWith("Failed to exit", "exit failed");
237+
});
238+
});
156239

157240
describe("DelegatorJoinAction", () => {
158241
let action: DelegatorJoinAction;

0 commit comments

Comments
 (0)