-
Notifications
You must be signed in to change notification settings - Fork 34
feat: staking commands #417
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
📝 WalkthroughWalkthroughAdds a new staking CLI surface with root command and subcommands (validators, delegations, delegate, undelegate, redelegate). Registers the staking command in the main program and exports it publicly. Introduces staking precompile ABI, constants, schemas, and helper API tweaks, plus multicall JSON fix. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User
participant CLI as CLI (commander)
participant Helpers as Helpers (setup/confirm)
participant Ethers as Ethers.js
participant Chain as Staking Precompile
rect rgb(238,245,255)
note right of CLI: New staking tx subcommands
User->>CLI: staking [delegate|undelegate|redelegate] --opts
CLI->>Helpers: validate options, setup tx (rpc, signer)
Helpers-->>CLI: signer, provider, address
CLI->>User: print intent & details
CLI->>Helpers: confirm (--yes or prompt)
alt confirmed
CLI->>Ethers: encode ABI call data
Ethers->>Chain: sendTransaction(to=STAKING_PRECOMPILE, data/value)
Chain-->>Ethers: receipt
Ethers-->>CLI: tx hash / receipt
CLI-->>User: output tx hash
else cancelled
CLI-->>User: exit without sending
end
end
sequenceDiagram
autonumber
actor User
participant CLI as CLI (validators/delegations)
participant Ethers as Provider+Contracts
participant Chain as Staking Precompile
participant MC as Multicall3
rect rgb(245,255,245)
note right of CLI: New query subcommands
User->>CLI: staking validators list --status/--json
CLI->>Ethers: call validators(...) with pagination
loop pages
Ethers->>Chain: validators(PageRequest, status)
Chain-->>Ethers: page of validators + nextKey
end
CLI-->>User: JSON or table
end
rect rgb(255,248,240)
User->>CLI: staking delegations list --address
CLI->>Ethers: fetch validator set (statuses, pages)
CLI->>MC: try aggregate delegation(addr, each validator) batched
alt multicall success
MC-->>CLI: results decoded
else fallback
CLI->>Chain: delegation(addr, validator) per item (bounded)
Chain-->>CLI: results
end
CLI-->>User: JSON or table
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 4
🧹 Nitpick comments (5)
packages/commands/src/staking/delegate.ts (2)
21-31: Tighten input validation for amount and validator.Prevent invalid/negative values and guide users on address formats.
Apply this diff to the schema:
- amount: z.string(), + amount: z + .string() + .refine((v) => { + try { + return ethers.parseEther(v) > 0n; + } catch { + return false; + } + }, "Amount must be a positive decimal number"), @@ - validator: z.string(), + validator: z + .string() + .refine( + (v) => + /^0x[0-9a-fA-F]{40}$/.test(v) || v.startsWith("zetavaloper1"), + "Validator must be 0x… or zetavaloper…" + ),
90-92: Clarify flag help: accept 0x or bech32.Update help text so users know both formats are supported.
- "Validator operator address (zetavaloper...)" + "Validator operator address (0x… or zetavaloper…)"packages/commands/src/staking/validators/list.ts (3)
1-7: Import Buffer explicitly to avoid ESM/bundler issues.Buffer is global in Node, but explicit import improves portability.
import { bech32 } from "bech32"; import chalk from "chalk"; import { Command, Option } from "commander"; import { ethers } from "ethers"; import ora from "ora"; import { getBorderCharacters, table } from "table"; +import { Buffer } from "buffer";
104-110: Hoist static statusMap to module scope.Avoid re-allocating per row.
- const statusMap: Record<number, string> = { - 0: "Unspecified", - 1: "Unbonded", - 2: "Unbonding", - 3: "Bonded", - }; + // at module scope: + // const statusMap: Record<number, string> = { 0: "Unspecified", 1: "Unbonded", 2: "Unbonding", 3: "Bonded" };
336-338: Unify schema validation UX with existing helper.Use validateAndParseSchema for consistent error formatting and exit behavior across commands.
- const options = validatorsListOptionsSchema.parse(rawOptions); + import { validateAndParseSchema } from "../../../../../utils"; + const options = validateAndParseSchema(rawOptions, validatorsListOptionsSchema, { exitOnError: true });
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
⛔ Files ignored due to path filters (23)
typechain-types/factories/@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy__factory.tsis excluded by!typechain-types/**typechain-types/factories/@openzeppelin/contracts/proxy/ERC1967/ERC1967Utils__factory.tsis excluded by!typechain-types/**typechain-types/factories/@openzeppelin/contracts/token/ERC20/utils/SafeERC20__factory.tsis excluded by!typechain-types/**typechain-types/factories/@openzeppelin/contracts/utils/Address__factory.tsis excluded by!typechain-types/**typechain-types/factories/@zetachain/protocol-contracts/contracts/evm/ERC20Custody__factory.tsis excluded by!typechain-types/**typechain-types/factories/@zetachain/protocol-contracts/contracts/evm/GatewayEVM__factory.tsis excluded by!typechain-types/**typechain-types/factories/@zetachain/protocol-contracts/contracts/evm/ZetaConnectorNative__factory.tsis excluded by!typechain-types/**typechain-types/factories/@zetachain/protocol-contracts/contracts/evm/ZetaConnectorNonNative__factory.tsis excluded by!typechain-types/**typechain-types/factories/@zetachain/protocol-contracts/contracts/zevm/GatewayZEVM__factory.tsis excluded by!typechain-types/**typechain-types/factories/contracts/EthZetaMock.sol/ZetaEthMock__factory.tsis excluded by!typechain-types/**typechain-types/factories/contracts/SwapHelpers.sol/SwapLibrary__factory.tsis excluded by!typechain-types/**typechain-types/factories/contracts/TestZRC20__factory.tsis excluded by!typechain-types/**typechain-types/factories/contracts/shared/MockZRC20__factory.tsis excluded by!typechain-types/**typechain-types/factories/contracts/testing/EVMSetup.t.sol/EVMSetup__factory.tsis excluded by!typechain-types/**typechain-types/factories/contracts/testing/FoundrySetup.t.sol/FoundrySetup__factory.tsis excluded by!typechain-types/**typechain-types/factories/contracts/testing/TokenSetup.t.sol/TokenSetup__factory.tsis excluded by!typechain-types/**typechain-types/factories/contracts/testing/UniswapV2SetupLib.sol/UniswapV2SetupLib__factory.tsis excluded by!typechain-types/**typechain-types/factories/contracts/testing/UniswapV3SetupLib.sol/UniswapV3SetupLib__factory.tsis excluded by!typechain-types/**typechain-types/factories/contracts/testing/ZetaSetup.t.sol/ZetaSetup__factory.tsis excluded by!typechain-types/**typechain-types/factories/contracts/testing/mock/ERC20Mock__factory.tsis excluded by!typechain-types/**typechain-types/factories/contracts/testing/mockGateway/NodeLogicMock__factory.tsis excluded by!typechain-types/**typechain-types/factories/contracts/testing/mockGateway/WrapGatewayEVM__factory.tsis excluded by!typechain-types/**typechain-types/factories/contracts/testing/mockGateway/WrapGatewayZEVM__factory.tsis excluded by!typechain-types/**
📒 Files selected for processing (16)
packages/commands/src/index.ts(1 hunks)packages/commands/src/program.ts(2 hunks)packages/commands/src/staking/delegate.ts(1 hunks)packages/commands/src/staking/delegations/index.ts(1 hunks)packages/commands/src/staking/delegations/list.ts(1 hunks)packages/commands/src/staking/delegations/show.ts(1 hunks)packages/commands/src/staking/index.ts(1 hunks)packages/commands/src/staking/redelegate.ts(1 hunks)packages/commands/src/staking/staking.json(1 hunks)packages/commands/src/staking/undelegate.ts(1 hunks)packages/commands/src/staking/validators/index.ts(1 hunks)packages/commands/src/staking/validators/list.ts(1 hunks)src/constants/addresses.ts(1 hunks)src/schemas/commands/validators.ts(1 hunks)utils/multicall3.json(1 hunks)utils/zetachain.command.helpers.ts(3 hunks)
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: fadeev
PR: zeta-chain/toolkit#346
File: packages/commands/src/ton/depositAndCall.ts:0-0
Timestamp: 2025-06-13T15:33:54.781Z
Learning: fadeev prefers responses in English.
🧬 Code graph analysis (11)
packages/commands/src/staking/delegations/show.ts (2)
src/constants/addresses.ts (1)
STAKING_PRECOMPILE(2-2)src/constants/api.ts (1)
DEFAULT_EVM_RPC_URL(3-4)
packages/commands/src/staking/delegations/index.ts (2)
packages/commands/src/staking/delegations/list.ts (1)
listCommand(412-428)packages/commands/src/staking/delegations/show.ts (1)
showCommand(57-84)
packages/commands/src/staking/index.ts (5)
packages/commands/src/staking/validators/index.ts (1)
validatorsCommand(5-8)packages/commands/src/staking/delegate.ts (1)
delegateCommand(83-85)packages/commands/src/staking/undelegate.ts (1)
undelegateCommand(82-84)packages/commands/src/staking/redelegate.ts (1)
redelegateCommand(85-87)packages/commands/src/staking/delegations/index.ts (1)
delegationsCommand(6-10)
packages/commands/src/staking/delegations/list.ts (6)
types/shared.schema.ts (2)
evmAddressSchema(9-11)rpcOrChainIdRefineRule(133-138)utils/validateAndParseSchema.ts (1)
validateAndParseSchema(28-59)utils/chains.ts (1)
getRpcUrl(3-15)src/constants/addresses.ts (2)
STAKING_PRECOMPILE(2-2)MULTICALL_ADDRESS(1-1)types/balances.types.ts (1)
MULTICALL3_ABI(54-75)src/constants/api.ts (1)
DEFAULT_EVM_RPC_URL(3-4)
packages/commands/src/staking/undelegate.ts (5)
types/shared.schema.ts (1)
namePkRefineRule(69-74)utils/zetachain.command.helpers.ts (4)
setupZetachainTransaction(64-100)SetupTxOptionsSubset(58-61)confirmZetachainTransaction(102-136)ConfirmTxOptionsSubset(62-62)src/constants/addresses.ts (1)
STAKING_PRECOMPILE(2-2)utils/handleError.ts (1)
handleError(31-45)utils/validateAndParseSchema.ts (1)
validateAndParseSchema(28-59)
packages/commands/src/staking/validators/list.ts (4)
packages/tasks/src/account.ts (1)
main(121-169)src/constants/addresses.ts (1)
STAKING_PRECOMPILE(2-2)src/constants/api.ts (1)
DEFAULT_EVM_RPC_URL(3-4)src/schemas/commands/validators.ts (1)
validatorsListOptionsSchema(3-7)
packages/commands/src/program.ts (1)
packages/commands/src/staking/index.ts (1)
stakingCommand(9-16)
utils/zetachain.command.helpers.ts (1)
utils/chains.ts (1)
getRpcUrl(3-15)
packages/commands/src/staking/delegate.ts (5)
types/shared.schema.ts (1)
namePkRefineRule(69-74)utils/zetachain.command.helpers.ts (4)
setupZetachainTransaction(64-100)SetupTxOptionsSubset(58-61)confirmZetachainTransaction(102-136)ConfirmTxOptionsSubset(62-62)src/constants/addresses.ts (1)
STAKING_PRECOMPILE(2-2)utils/handleError.ts (1)
handleError(31-45)utils/validateAndParseSchema.ts (1)
validateAndParseSchema(28-59)
packages/commands/src/staking/validators/index.ts (1)
packages/commands/src/staking/validators/list.ts (1)
listCommand(324-339)
packages/commands/src/staking/redelegate.ts (5)
types/shared.schema.ts (1)
namePkRefineRule(69-74)utils/zetachain.command.helpers.ts (4)
setupZetachainTransaction(64-100)SetupTxOptionsSubset(58-61)confirmZetachainTransaction(102-136)ConfirmTxOptionsSubset(62-62)src/constants/addresses.ts (1)
STAKING_PRECOMPILE(2-2)utils/handleError.ts (1)
handleError(31-45)utils/validateAndParseSchema.ts (1)
validateAndParseSchema(28-59)
🔇 Additional comments (11)
utils/zetachain.command.helpers.ts (2)
56-62: LGTM! Type exports follow least-privilege pattern.Exporting
BaseZetachainOptionsand introducingSetupTxOptionsSubsetandConfirmTxOptionsSubsetprovides clear type contracts for the narrowed function signatures. This improves type safety by restricting each function to only the options it actually uses.
102-136: LGTM! Signature narrowing is consistent.The function signature now accepts
ConfirmTxOptionsSubset, consistent with the subset pattern applied tosetupZetachainTransaction. No behavioral changes.packages/commands/src/index.ts (1)
7-7: LGTM! Staking module added to public API.The new export expands the public API surface to include staking commands, consistent with the PR objectives.
src/constants/addresses.ts (1)
2-2: LGTM! Staking precompile address added.The constant follows the standard pattern for precompile addresses (low address space). Used by staking command modules for transaction encoding.
utils/multicall3.json (1)
38-38: LGTM! JSON formatting corrected.Trailing formatting issue fixed. No functional changes.
packages/commands/src/program.ts (1)
11-11: LGTM! Staking command registered with toolkit.The command is imported and registered following the established pattern for other commands. No issues identified.
Also applies to: 29-29
src/schemas/commands/validators.ts (1)
1-10: LGTM! Validators list options schema is well-defined.The schema provides clear validation for the validators list command options. The required
rpcfield is reasonable since the command definition (inlist.ts) provides a default value via Commander before schema validation.packages/commands/src/staking/delegations/index.ts (1)
1-11: LGTM! Delegations command structure is standard.The parent command follows the established pattern for organizing subcommands. The structure is clear and consistent with other command modules.
packages/commands/src/staking/validators/index.ts (1)
1-9: LGTM! Validators command structure is standard.The parent command follows the same clear pattern as the delegations command. Structure is consistent and well-organized.
packages/commands/src/staking/validators/list.ts (2)
189-279: Robust pagination and status probing — nice work.Candidate probing + nextKey loop and mixed-shape decoding are solid and resilient.
117-132: No change needed: zetavaloper uses original Bech32
Confirmed that validator-operator (zetavaloper) addresses use the original Bech32 format; the currentbech32.encodeimplementation is correct.
| const data = iface.encodeFunctionData("delegate", [ | ||
| delegatorAddress, | ||
| options.validator, | ||
| amountWei, | ||
| ]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Validator address must be normalized (accept bech32 valoper and 0x).
You pass options.validator as-is. CLI examples use zetavaloper… bech32, while the precompile likely expects a 20‑byte EVM address. Normalize input to 0x… (and checksum) before encoding.
Apply this diff in-place to use a normalized address:
- const data = iface.encodeFunctionData("delegate", [
- delegatorAddress,
- options.validator,
- amountWei,
- ]);
+ const validatorHex = normalizeValidatorAddress(options.validator);
+ const data = iface.encodeFunctionData("delegate", [
+ delegatorAddress,
+ validatorHex,
+ amountWei,
+ ]);Add this helper and imports outside the range:
import { bech32 } from "bech32";
import { Buffer } from "buffer";
const normalizeValidatorAddress = (input: string): string => {
if (/^0x[0-9a-fA-F]{40}$/.test(input)) return ethers.getAddress(input);
try {
const { prefix, words } = bech32.decode(input);
if (prefix !== "zetavaloper") throw new Error("invalid bech32 prefix");
const bytes = Buffer.from(bech32.fromWords(words));
if (bytes.length !== 20) throw new Error("expected 20-byte operator address");
return ethers.getAddress("0x" + bytes.toString("hex"));
} catch {
throw new Error(
"Validator must be a 0x…20-byte address or a zetavaloper… bech32 string"
);
}
};🤖 Prompt for AI Agents
In packages/commands/src/staking/delegate.ts around lines 59 to 63,
options.validator is passed as-is but the precompile expects a 20-byte EVM
address; add a normalize helper and imports and use it when encoding. Add
imports for bech32 and Buffer and implement normalizeValidatorAddress that
accepts either 0x..40hex or zetavaloper bech32, decodes bech32 to 20 bytes,
validates length and prefix, then returns ethers.getAddress(hex) (throwing a
clear error on invalid input). Replace the current use of options.validator in
iface.encodeFunctionData("delegate", [...]) with
normalizeValidatorAddress(options.validator) so a checksummed 0x address is
encoded.
| const tx = await signer.sendTransaction({ | ||
| data, | ||
| to: STAKING_PRECOMPILE, | ||
| value: amountWei, | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Double-specifying amount (calldata + msg.value) may be wrong for this ABI. Please verify.
You encode amount as a parameter and also send value: amountWei. Depending on the staking precompile ABI, only one may be required.
Run to confirm the delegate signature and mutability:
If inputs do not include an amount (or stateMutability is payable), drop the calldata amount and rely on msg.value; otherwise, keep the calldata amount and set value: 0n.
🏁 Script executed:
#!/bin/bash
# Locate staking.json and inspect the `delegate` function signature.
set -euo pipefail
fd -a 'staking.json' | while read -r f; do
echo "File: $f"
jq -r '
.abi
| map(select(.type=="function" and .name=="delegate"))
| .[] | {name, stateMutability, inputs, outputs}
' "$f"
doneLength of output: 553
Drop value for delegate transaction
The delegate ABI is nonpayable and already takes amount as a parameter—remove value: amountWei from the transaction to prevent reverts.
🤖 Prompt for AI Agents
In packages/commands/src/staking/delegate.ts around lines 65 to 69, the
transaction is including value: amountWei which causes reverts because the
delegate ABI is nonpayable and already encodes the amount in the calldata;
remove the value field from the signer.sendTransaction call (or set value to 0)
so the transaction doesn't send ETH and leave the amount solely in the encoded
data parameter.
| const provider = new ethers.JsonRpcProvider(opts.rpc); | ||
| const contract = new ethers.Contract( | ||
| STAKING_PRECOMPILE, | ||
| STAKING_ABI, | ||
| provider | ||
| ); | ||
| const [shares, balance] = (await contract.delegation( | ||
| opts.address, | ||
| opts.validator | ||
| )) as [bigint, { amount: bigint; denom: string }]; | ||
|
|
||
| const result = { | ||
| address: opts.address, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ensure hex validator input is converted to bech32 before querying
The CLI advertises --validator accepts hex or bech32, but the call forwards hex straight into contract.delegation. The staking precompile expects a bech32 operator string, so passing hex causes the call to revert. Normalize hex addresses to zetavaloper before invoking the contract (like the list command already does) so both forms work.
+import { bech32 } from "bech32";
...
- const [shares, balance] = (await contract.delegation(
- opts.address,
- opts.validator
- )) as [bigint, { amount: bigint; denom: string }];
+ const validator =
+ /^0x[0-9a-fA-F]{40}$/.test(opts.validator)
+ ? bech32.encode(
+ "zetavaloper",
+ bech32.toWords(Buffer.from(opts.validator.slice(2), "hex"))
+ )
+ : opts.validator;
+
+ const [shares, balance] = (await contract.delegation(
+ opts.address,
+ validator
+ )) as [bigint, { amount: bigint; denom: string }];🤖 Prompt for AI Agents
packages/commands/src/staking/delegations/show.ts lines 21-33: the validator
argument may be hex but the staking precompile expects a bech32 operator string,
so normalize the input before calling contract.delegation; detect if
opts.validator is a hex address and convert it to the zetavaloper bech32 form
(reuse the same conversion/helper used in the list command) and then pass the
converted bech32 validator into contract.delegation; ensure any required utility
import is added and update tests/usage accordingly.
| // Default chain id to 7001 (ZetaChain testnet) when neither rpc nor chainId provided | ||
| const resolvedChainId = options.chainId || "7001"; | ||
| const rpc = options.rpc || getRpcUrl(parseInt(resolvedChainId)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Verify that defaulting logic aligns with schema validation.
The function now defaults chainId to "7001" when neither rpc nor chainId is provided. However, baseZetachainOptionsRefined (lines 49-54) uses rpcOrChainIdRefineRule which appears to require at least one of these fields.
Since setupZetachainTransaction accepts SetupTxOptionsSubset (which doesn't enforce the refined rule), callers may bypass the schema validation and hit the default silently. This could cause unexpected behavior if users expect an error when omitting both options.
Verify that all callers of setupZetachainTransaction validate their options through baseZetachainOptionsRefined before calling this function, or update the function to throw an error when both rpc and chainId are undefined to maintain consistency with the schema's intent.
#!/bin/bash
# Description: Find all call sites of setupZetachainTransaction and check if they validate with baseZetachainOptionsRefined
# Search for function calls
rg -n --type ts 'setupZetachainTransaction\(' -A 3 -B 10🤖 Prompt for AI Agents
In utils/zetachain.command.helpers.ts around lines 81 to 83, the code currently
defaults chainId to "7001" when both rpc and chainId are missing which can
silently bypass the refined schema rule; either (A) update all call sites to
validate their options with baseZetachainOptionsRefined before calling
setupZetachainTransaction (search for setupZetachainTransaction(...) and
insert/refactor validation using baseZetachainOptionsRefined and return/throw on
validation failure), or (B) change setupZetachainTransaction itself to enforce
the schema intent by detecting when both options.rpc and options.chainId are
undefined and throwing a clear error instead of defaulting, so callers cannot
silently bypass the refined rule.
Validator List
Delegate
Summary by CodeRabbit
New Features
Bug Fixes