feat: get contract code new cli command#253
Conversation
…actCode Adds CodeAction and wires `code <contractAddress>` under contracts.
WalkthroughAdds a new "contracts code" CLI subcommand and CodeAction to fetch on-chain contract code (optional --rpc), includes unit tests for action and CLI wiring, and bumps Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User
participant CLI as CLI (contracts)
participant Action as CodeAction
participant Client as RPC Client
note over CLI,Action: "contracts code <contractAddress> [--rpc]" flow
User->>CLI: contracts code <contractAddress> [--rpc <url>]
CLI->>Action: code({ contractAddress, rpc })
Action->>Client: getClient(rpc, true)
Action->>Client: initializeConsensusSmartContract()
Action->>Client: getContractCode(contractAddress)
alt success
Client-->>Action: code bytes/string
Action-->>CLI: succeedSpinner(code)
CLI-->>User: display code
else failure
Client-->>Action: error
Action-->>CLI: failSpinner(error)
CLI-->>User: show error
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested reviewers
Poem
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 💡 Knowledge Base configuration:
You can enable these sources in your CodeRabbit configuration. 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
✨ 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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (3)
src/commands/contracts/code.ts (2)
13-19: Reuse CodeOptions in the method signature to avoid type drift.- async code({ - contractAddress, - rpc, - }: { - contractAddress: string; - rpc?: string; - }): Promise<void> { + async code({ contractAddress, rpc }: { contractAddress: string } & CodeOptions): Promise<void> {
21-21: Is consensus initialization required for a read-only getContractCode?If not strictly needed, drop it to reduce latency.
src/commands/contracts/index.ts (1)
76-83: Tighten help text; optionally add output controls.If the API returns bytecode only, prefer “bytecode” to avoid implying source retrieval. Consider a future --json/--out option for large outputs.
- .description("Get the bytecode/source for a deployed contract") + .description("Get the bytecode for a deployed contract")
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (3)
package.json(1 hunks)src/commands/contracts/code.ts(1 hunks)src/commands/contracts/index.ts(2 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-08-12T22:52:10.974Z
Learnt from: epsjunior
PR: genlayerlabs/genlayer-cli#248
File: package.json:67-67
Timestamp: 2025-08-12T22:52:10.974Z
Learning: genlayer-js v0.14.0 is a valid and published version on npm as of December 2024. The package has regular releases and v0.14.0 is the current latest version.
Applied to files:
package.json
📚 Learning: 2025-07-10T23:50:34.628Z
Learnt from: epsjunior
PR: genlayerlabs/genlayer-cli#239
File: package.json:60-66
Timestamp: 2025-07-10T23:50:34.628Z
Learning: In the genlayer-cli project, dotenv is used with manual parsing via dotenv.parse() rather than automatic loading via dotenv.config(), so warnings about implicit .env.local auto-loading changes in dotenv v17 are not applicable to this project.
Applied to files:
package.json
🧬 Code graph analysis (2)
src/commands/contracts/code.ts (1)
src/lib/actions/BaseAction.ts (1)
BaseAction(14-236)
src/commands/contracts/index.ts (1)
src/commands/contracts/code.ts (2)
CodeOptions(4-6)CodeAction(8-31)
🔇 Additional comments (1)
package.json (1)
67-67: genlayer-js@^0.16.0 validated
– Version 0.16.0 is published on npm and the currentlatestdist-tag (npm view)
– Localtsc --noEmitpassed without errors, confirminginitializeConsensusSmartContractandgetContractCoderemain exposed
| import {BaseAction} from "../../lib/actions/BaseAction"; | ||
| import type {Address} from "genlayer-js/types"; |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Validate address, handle EOAs explicitly, and exit non‑zero on failures.
Prevents remote calls with bad inputs, gives clear EOA feedback, and ensures CI-friendly exit codes.
import {BaseAction} from "../../lib/actions/BaseAction";
import type {Address} from "genlayer-js/types";
+import { isAddress, getAddress } from "viem";
async code({
contractAddress,
rpc,
}: {
contractAddress: string;
rpc?: string;
}): Promise<void> {
const client = await this.getClient(rpc, true);
await client.initializeConsensusSmartContract();
- this.startSpinner(`Getting code for contract at ${contractAddress}...`);
+ const target = contractAddress.trim();
+ if (!isAddress(target)) {
+ this.failSpinner("Invalid contract address format", { contractAddress });
+ process.exitCode = 1;
+ return;
+ }
+ const normalized = getAddress(target);
+ this.startSpinner(`Getting code for contract at ${normalized}...`);
try {
- const result = await client.getContractCode(contractAddress as Address);
+ const result = await client.getContractCode(normalized as Address);
+ if (!result || result === "0x") {
+ this.failSpinner("No contract code found at address (EOA or not deployed)");
+ process.exitCode = 1;
+ return;
+ }
this.succeedSpinner("Contract code retrieved successfully", result);
} catch (error) {
this.failSpinner("Error retrieving contract code", error);
+ process.exitCode = 1;
}
}Also applies to: 13-30
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (12)
tests/commands/code.test.ts (8)
6-9: Drop unnecessary esbuild mock to reduce noiseThe
codecommand path doesn’t touch esbuild. Removing this mock tightens the test surface and avoids accidental coupling.-vi.mock("esbuild", () => ({ - buildSync: vi.fn(), -}));
14-18: Stabilize CLI tests: exitOverride + stub action method in setup
- Prevent accidental process.exit by enabling Commander’s exit override.
- Ensure
CodeAction.prototype.codeis a mock Promise to avoid flakiness and to make.parse*consistently awaitable.beforeEach(() => { - program = new Command(); - initializeContractsCommands(program); - vi.clearAllMocks(); + program = new Command(); + program.exitOverride(); // avoid process.exit in tests + initializeContractsCommands(program); + vi.clearAllMocks(); + vi.spyOn(CodeAction.prototype, "code").mockResolvedValue(undefined as any); });
24-30: Prefer parseAsync to await async actionsCommander recommends using
parseAsyncwhen handlers are async. Keeps behavior consistent if the impl later awaits I/O.-test("CodeAction.code is called with default options", async () => { - program.parse(["node", "test", "code", "0xMockedContract"]); +test("CodeAction.code is called with default options", async () => { + await program.parseAsync(["node", "test", "code", "0xMockedContract"]);
32-46: Await CLI with parseAsync and validate argumentsSame as above; also ensures the RPC flag is forwarded under async execution.
-test("CodeAction.code is called with custom RPC URL", async () => { - program.parse([ +test("CodeAction.code is called with custom RPC URL", async () => { + await program.parseAsync([ "node", "test", "code", "0xMockedContract", "--rpc", "https://custom-rpc-url.com" ]); - expect(CodeAction).toHaveBeenCalledTimes(1); + expect(CodeAction).toHaveBeenCalledTimes(1);
48-51: Await parseAsync for instantiation test as wellMinor consistency tweak.
-test("CodeAction is instantiated when the code command is executed", async () => { - program.parse(["node", "test", "code", "0xMockedContract"]); +test("CodeAction is instantiated when the code command is executed", async () => { + await program.parseAsync(["node", "test", "code", "0xMockedContract"]);
53-58: Use rejects + regex to make the unknown option assertion robustAvoid coupling to exact Commander error strings and ensure the promise is awaited.
-test("throws error for unrecognized options", async () => { - const codeCommand = program.commands.find((cmd) => cmd.name() === "code"); - codeCommand?.exitOverride(); - expect(() => program.parse(["node", "test", "code", "0xMockedContract", "--unknown"])) - .toThrowError("error: unknown option '--unknown'"); -}); +test("throws error for unrecognized options", async () => { + const codeCommand = program.commands.find((cmd) => cmd.name() === "code"); + codeCommand?.exitOverride(); + await expect( + program.parseAsync(["node", "test", "code", "0xMockedContract", "--unknown"]) + ).rejects.toThrow(/unknown option/i); +});
60-66: Mock before parsing and await parseAsyncAvoid the first un-mocked parse; assert via resolves to be explicit.
-test("CodeAction.code is called without throwing errors for valid options", async () => { - program.parse(["node", "test", "code", "0xMockedContract"]); - vi.mocked(CodeAction.prototype.code).mockResolvedValueOnce(undefined as any); - expect(() => - program.parse(["node", "test", "code", "0xMockedContract"]) - ).not.toThrow(); -}); +test("CodeAction.code is called without throwing errors for valid options", async () => { + vi.mocked(CodeAction.prototype.code).mockResolvedValueOnce(undefined as any); + await expect( + program.parseAsync(["node", "test", "code", "0xMockedContract"]) + ).resolves.toBeUndefined(); +});
67-70: Add a test for missing required argumentCatches regressions where the positional
<contractAddress>stops being required.}); - - - +test("errors when <contractAddress> is missing", async () => { + program.exitOverride(); + await expect(program.parseAsync(["node", "test", "code"])) + .rejects.toThrow(/missing required argument ['"]?contractAddress['"]?/i); +});tests/actions/code.test.ts (4)
16-27: Trim redundant stubs to keep tests focused
createAccountandlogaren’t needed sincegetAccountis stubbed. Dropping them reduces setup noise.-vi.mocked(createAccount).mockReturnValue({privateKey: mockPrivateKey} as any); ... -vi.spyOn(codeAction as any, "log").mockImplementation(() => {});
33-47: Assert spinner message for better UX coverageVerify we show the address in the progress text.
await codeAction.code({ contractAddress: "0xMockedContract", }); expect(mockClient.getContractCode).toHaveBeenCalledWith("0xMockedContract"); +expect(codeAction["startSpinner"]).toHaveBeenCalledWith( + "Getting code for contract at 0xMockedContract..." +); expect(codeAction["succeedSpinner"]).toHaveBeenCalledWith( "Contract code retrieved successfully", mockResult, );
49-55: Also assert success path isn’t triggered on errorStrengthens negative-path guarantees.
await codeAction.code({contractAddress: "0xMockedContract"}); expect(codeAction["failSpinner"]).toHaveBeenCalledWith("Error retrieving contract code", expect.any(Error)); +expect(codeAction["succeedSpinner"]).not.toHaveBeenCalled();
57-76: Optionally verify getClient args for RPC wiringSpy on
getClientto assert therpcandtrueflags are passed through.-await codeAction.code({ +const getClientSpy = vi.spyOn(codeAction as any, "getClient"); +await codeAction.code({ contractAddress: "0xMockedContract", rpc: "https://custom-rpc-url.com", }); +expect(getClientSpy).toHaveBeenCalledWith("https://custom-rpc-url.com", true); expect(createClient).toHaveBeenCalledWith( expect.objectContaining({ endpoint: "https://custom-rpc-url.com", }), );
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (3)
package.json(1 hunks)tests/actions/code.test.ts(1 hunks)tests/commands/code.test.ts(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- package.json
🧰 Additional context used
🧬 Code graph analysis (2)
tests/actions/code.test.ts (1)
src/commands/contracts/code.ts (1)
CodeAction(8-31)
tests/commands/code.test.ts (2)
src/commands/contracts/index.ts (1)
initializeContractsCommands(15-86)src/commands/contracts/code.ts (1)
CodeAction(8-31)
Summary
Adds a new CLI command to fetch a deployed contract’s bytecode/source and updates
genlayer-jsfor compatibility.Changes
genlayer code <contractAddress> [--rpc <rpcUrl>]CodeActionusinggetContractCodewith spinners and RPC supportgenlayer-jsversion📁 File Structure
🏗️ Command Architecture
code <contractAddress> [--rpc <rpcUrl>]🔧 Usage
🧪 Testing
tests/actions/code.test.tstests/commands/code.test.ts✨ Code Quality
BaseActionand existing JSON-RPC client🛡️ Backward Compatibility
Docs: Include in generated command docs when running docs generation
Summary by CodeRabbit
New Features
Tests
Chores