Skip to content

feat: get contract code new cli command#253

Merged
epsjunior merged 6 commits into
mainfrom
dxp-621-get-contract-code-new-cli-command
Sep 3, 2025
Merged

feat: get contract code new cli command#253
epsjunior merged 6 commits into
mainfrom
dxp-621-get-contract-code-new-cli-command

Conversation

@epsjunior

@epsjunior epsjunior commented Sep 3, 2025

Copy link
Copy Markdown
Contributor

Summary

Adds a new CLI command to fetch a deployed contract’s bytecode/source and updates genlayer-js for compatibility.

Changes

  • New command: genlayer code <contractAddress> [--rpc <rpcUrl>]
  • Implementation:
    • Introduced CodeAction using getContractCode with spinners and RPC support
    • Registers command at the CLI root via the contracts initializer
  • Dependencies: Bumped genlayer-js version

📁 File Structure

src/
└── commands/
    └── contracts/
        ├── code.ts         # New CodeAction
        └── index.ts        # Registers `code` command

🏗️ Command Architecture

  • Command: code <contractAddress> [--rpc <rpcUrl>]

🔧 Usage

# Get code (default RPC)
genlayer code 0xabc...

# Get code with custom RPC
genlayer code 0xabc... --rpc http://localhost:5173

🧪 Testing

  • Added action tests: tests/actions/code.test.ts
  • Added command tests: tests/commands/code.test.ts

✨ Code Quality

  • Pattern: Uses BaseAction and existing JSON-RPC client
  • UX: Start/succeed/fail spinners; clear messaging

🛡️ Backward Compatibility

  • Non-breaking: adds a new command; existing commands unaffected

Docs: Include in generated command docs when running docs generation

Summary by CodeRabbit

  • New Features

    • Added a "contracts code" CLI command to fetch a deployed contract's code by address.
    • Supports an optional --rpc flag to target a specific endpoint.
    • Displays progress feedback and outputs the retrieved code on success.
  • Tests

    • Added tests covering success, error handling, custom RPC usage, command wiring, and option validation.
  • Chores

    • Bumped a core blockchain client dependency.

@coderabbitai

coderabbitai Bot commented Sep 3, 2025

Copy link
Copy Markdown

Walkthrough

Adds 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 genlayer-js dependency to ^0.16.0.

Changes

Cohort / File(s) Summary
Dependency bump
package.json
Upgrade: genlayer-js 0.15.1 → ^0.16.0.
Contracts code action & CLI
src/commands/contracts/code.ts, src/commands/contracts/index.ts
Add CodeAction and CodeOptions (rpc?: string); register contracts code <contractAddress> [--rpc] command that calls CodeAction.code.
Unit tests (action)
tests/actions/code.test.ts
Add tests for success, failure, custom RPC usage, and consensus smart contract initialization; mocks for client and UI spinners.
Unit tests (CLI)
tests/commands/code.test.ts
Add tests for CLI wiring: action instantiation, option forwarding (--rpc), unknown option errors, and successful invocation.

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
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • cristiam86
  • danielrc888

Poem

I twitch my nose and hop to find,
A contract's bytes upon the chain,
I spin a loader, fetch and mind,
With RPC I trace the lane.
🐇 Thump-thump — code found, I cheer again.


📜 Recent 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.

📥 Commits

Reviewing files that changed from the base of the PR and between 18f53d6 and 50d9411.

📒 Files selected for processing (1)
  • src/commands/contracts/index.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/commands/contracts/index.ts
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch dxp-621-get-contract-code-new-cli-command

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.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

📥 Commits

Reviewing files that changed from the base of the PR and between 7c3586b and 0bb4dd0.

⛔ Files ignored due to path filters (1)
  • package-lock.json is 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 current latest dist-tag (npm view)
– Local tsc --noEmit passed without errors, confirming initializeConsensusSmartContract and getContractCode remain exposed

Comment on lines +1 to +2
import {BaseAction} from "../../lib/actions/BaseAction";
import type {Address} from "genlayer-js/types";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

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

Comment thread src/commands/contracts/index.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (12)
tests/commands/code.test.ts (8)

6-9: Drop unnecessary esbuild mock to reduce noise

The code command 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.code is 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 actions

Commander recommends using parseAsync when 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 arguments

Same 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 well

Minor 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 robust

Avoid 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 parseAsync

Avoid 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 argument

Catches 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

createAccount and log aren’t needed since getAccount is 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 coverage

Verify 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 error

Strengthens 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 wiring

Spy on getClient to assert the rpc and true flags 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.

📥 Commits

Reviewing files that changed from the base of the PR and between 0bb4dd0 and 18f53d6.

⛔ Files ignored due to path filters (1)
  • package-lock.json is 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)

Comment thread tests/actions/code.test.ts
@epsjunior epsjunior merged commit d6ea30d into main Sep 3, 2025
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants