Skip to content

Conversation

@randygrok
Copy link
Contributor

@randygrok randygrok commented Jan 28, 2026

Description

Closes: #109

@claude
Copy link

claude bot commented Jan 28, 2026

Claude finished @randygrok's task —— View job


Code Review Completed ✅

I've thoroughly reviewed this client implementation for EvNode transactions. Here's my comprehensive feedback:

  • Code quality and best practices
  • Potential bugs or issues
  • Performance considerations
  • Security concerns
  • Test coverage

Summary

This is a well-structured TypeScript client library for EvNode transactions with good Viem integration. The implementation is comprehensive but has several important issues that should be addressed, particularly around gas estimation, error handling, and potential security concerns.

🔴 Critical Issues

1. Gas Estimation Incomplete - clients/src/index.ts:198-211

The estimateIntrinsicGas function has multiple issues:

  • Missing access list gas: Doesn't account for EIP-2930 access list costs (2400 gas per address + 1900 gas per storage key)
  • Incorrect per-call cost: Charges 21000 gas per call, but calls within a batch don't each cost 21000 gas
  • Used incorrectly: Line 592 doesn't pass the accessList parameter to the function

Recommendation:

export function estimateIntrinsicGas(calls: Call[], accessList?: AccessList): bigint {
  let gas = 21000n; // base transaction cost
  
  for (const call of calls) {
    if (call.to === null) gas += 32000n; // CREATE costs extra
    
    // Data gas cost
    for (const byte of hexToBytes(call.data)) {
      gas += byte === 0 ? 4n : 16n;
    }
  }
  
  // Access list gas cost
  if (accessList) {
    for (const item of accessList) {
      gas += 2400n; // per address
      gas += BigInt(item.storageKeys.length) * 1900n; // per storage key
    }
  }
  
  return gas;
}

2. Dangerous Error Handling - clients/src/index.ts:620-628

The fetchMaxPriorityFee function swallows all errors, which can hide network issues, RPC misconfigurations, or other problems that developers should know about.

Recommendation:

async function fetchMaxPriorityFee(client: Client): Promise<bigint> {
  try {
    const result = await client.request({ method: 'eth_maxPriorityFeePerGas' });
    if (!isHex(result)) throw new Error('eth_maxPriorityFeePerGas returned non-hex');
    return hexToBigIntSafe(result);
  } catch (err) {
    // Only fallback for method not supported errors
    if (err instanceof Error && (err.message.includes('not found') || err.message.includes('not supported'))) {
      return 0n;
    }
    throw err; // Re-throw other errors
  }
}

3. Deprecated RPC Method Warning - clients/src/index.ts:374-392

The hashSignerFromRpcClient function uses eth_sign, which has inconsistent behavior across Ethereum clients and is deprecated. Some nodes prefix the hash with EIP-191, leading to invalid signatures.

Recommendation: Add prominent documentation warning about node compatibility requirements.

🟡 Moderate Issues

4. Suboptimal Gas Price Logic - clients/src/index.ts:591

Using eth_gasPrice for maxFeePerGas can lead to overpaying on EIP-1559 chains. Better approach:

const maxFeePerGas = overrides.maxFeePerGas ?? 
  (await fetchBaseFeePerGas(client)) + maxPriorityFeePerGas;

5. Code Duplication - Type Definitions

The codebase has inline anonymous types that duplicate EvnodeSendArgs and EvnodeIntentArgs. Consider creating specific extended interfaces:

type EvnodeSendArgsWithExecutor = Omit<EvnodeSendArgs, 'executor'> & {
  executor: HashSigner;
};

6. Validation Logic - clients/src/index.ts:213-223

The validation only checks for CREATE calls not being first, but could benefit from additional checks:

  • Gas limit minimum validation
  • Call data size limits
  • Value overflow checks

🔵 Minor Issues

7. Error Messages

Some error messages could be more descriptive:

  • Line 106: "Invalid EvNode transaction type" could specify expected vs actual
  • Line 387: Could include the actual returned value type

8. Type Safety

Consider stricter typing for RLP values and stronger validation of decoded data.

✅ Positive Aspects

Strong Architecture

  • Clean separation between core transaction logic and client wrapper
  • Good use of TypeScript interfaces and type safety
  • Proper RLP encoding/decoding implementation
  • Well-structured sponsorship flow

Comprehensive Functionality

  • Full transaction lifecycle support (build, sign, send)
  • Both sponsored and unsponsored transaction support
  • Good error handling patterns (where implemented correctly)
  • Flexible client configuration

Good Documentation

  • Clear interface definitions
  • Helpful examples covering different use cases
  • Reasonable function naming and organization

🧪 Test Coverage Assessment

Good Coverage Areas:

  • Basic transaction flows (unsponsored and sponsored)
  • Batch transaction handling
  • Real-world integration testing with balance verification

Missing Coverage:

  • RLP encoding/decoding edge cases
  • Error handling paths
  • Access list functionality
  • Gas estimation accuracy
  • Signature recovery functions
  • Invalid transaction validation

Recommendations:

  • Add unit tests for encoding/decoding functions
  • Test error conditions and edge cases
  • Add tests for access list gas estimation
  • Test signature validation edge cases

🔒 Security Analysis

Secure Practices:

  • Proper signature handling and validation
  • Correct hash computation for different signature domains
  • Input validation for transaction parameters

Areas of Concern:

  • Over-broad error catching could hide security issues
  • Gas estimation inaccuracy could lead to failed transactions
  • Reliance on eth_sign introduces node compatibility issues

📋 Recommendations Priority

High Priority:

  1. Fix gas estimation for access lists and multiple calls
  2. Improve error handling in fetchMaxPriorityFee
  3. Add warnings about eth_sign compatibility

Medium Priority:

  1. Improve gas price calculation for EIP-1559
  2. Reduce code duplication with proper type definitions
  3. Add comprehensive unit tests

Low Priority:

  1. Enhance error messages
  2. Strengthen input validation
  3. Consider alternative to eth_sign

Overall, this is a solid foundation for EvNode transaction handling, but the gas estimation and error handling issues should be addressed before production use.

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @randygrok, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request establishes the foundational client-side logic for a new EvNode transaction system. It defines the structure and behavior of these specialized transactions, enabling developers to programmatically construct, sign with multiple parties (executor and sponsor), and dispatch them to the network. The implementation focuses on robust data handling, signature management, and integration with existing blockchain interaction libraries to streamline the development of applications utilizing this new transaction type.

Highlights

  • New EvNode Transaction Type: Introduces a custom EvNode transaction type (0x76) with specific structures for calls, access lists, and dual signatures (executor and optional fee payer/sponsor).
  • Comprehensive Client Library: Provides a full client implementation (createEvnodeClient) for building, signing, and sending these custom transactions, including functions for creating sponsorable intents and handling sponsor signatures.
  • RLP Encoding and Decoding: Implements robust RLP encoding and decoding mechanisms for EvNode transactions, ensuring proper serialization and deserialization for on-chain submission and off-chain processing.
  • Viem Integration: Seamlessly integrates with the viem library, leveraging its utilities for address recovery, hash computation, and RPC interactions, and registers the EvNode transaction type with viem's transaction serializer.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a Viem client for a custom EvNode transaction type. The implementation is comprehensive, covering transaction encoding/decoding, signing, and client-side actions. My review focuses on improving robustness, correctness of gas estimations, and maintainability. I've identified a few areas for improvement, including more accurate gas calculations, safer error handling in RPC calls, and addressing potential issues with the eth_sign method. I've also suggested some minor refactoring to reduce code duplication.

Comment on lines 405 to 423
export function hashSignerFromRpcClient(
client: Client,
address: Address,
): HashSigner {
return {
address,
signHash: async (hash) => {
// eth_sign is expected to sign raw bytes (no EIP-191 prefix).
const signature = await client.request({
method: 'eth_sign',
params: [address, hash],
});
if (!isHex(signature)) {
throw new Error('eth_sign returned non-hex signature');
}
return signature;
},
};
}
Copy link
Contributor

Choose a reason for hiding this comment

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

high

The eth_sign RPC method has inconsistent behavior across different Ethereum node implementations and is generally considered deprecated. Some nodes (like Geth) will sign the raw hash as expected here, but others (like Parity/OpenEthereum) will prefix the hash according to EIP-191, which would lead to invalid signatures for this use case. This can cause hard-to-debug issues for users of your client depending on which RPC provider they use.

It's highly recommended to add a prominent warning in the function's documentation about this limitation and the specific node behavior it relies on.

Comment on lines +608 to +616
async function fetchMaxPriorityFee(client: Client): Promise<bigint> {
try {
const result = await client.request({ method: 'eth_maxPriorityFeePerGas' });
if (!isHex(result)) throw new Error('eth_maxPriorityFeePerGas returned non-hex');
return hexToBigIntSafe(result);
} catch {
return 0n;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

high

The broad catch block here will swallow any error from the eth_maxPriorityFeePerGas call and default to 0n. This can hide underlying problems like network connectivity issues or RPC misconfigurations, leading to transactions being sent with a potentially undesirable maxPriorityFeePerGas. It's safer to only catch specific errors that indicate the method is not supported (like on a pre-EIP-1559 chain) and re-throw others.

  try {
    const result = await client.request({ method: 'eth_maxPriorityFeePerGas' });
    if (!isHex(result)) throw new Error('eth_maxPriorityFeePerGas returned non-hex');
    return hexToBigIntSafe(result);
  } catch (err) {
    // Only fallback to 0n if the method is not supported.
    // A robust implementation would check for a specific RPC error code (e.g., -32601).
    if (err instanceof Error && (err.message.includes('not found') || err.message.includes('not supported'))) {
      return 0n;
    }
    throw err;
  }

Comment on lines 205 to 217
export function estimateIntrinsicGas(calls: Call[]): bigint {
let gas = 21000n;

for (const call of calls) {
if (call.to === null) gas += 32000n;

for (const byte of hexToBytes(call.data)) {
gas += byte === 0 ? 4n : 16n;
}
}

return gas;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

The current implementation of estimateIntrinsicGas does not account for the gas costs associated with an accessList (per EIP-2930). This will lead to an underestimation of the intrinsic gas for transactions using access lists, potentially causing them to fail. The function should be updated to accept an accessList and include its cost in the calculation.

export function estimateIntrinsicGas(calls: Call[], accessList?: AccessList): bigint {
  let gas = 21000n;

  for (const call of calls) {
    if (call.to === null) gas += 32000n;

    for (const byte of hexToBytes(call.data)) {
      gas += byte === 0 ? 4n : 16n;
    }
  }

  if (accessList) {
    const ACCESS_LIST_ADDRESS_COST = 2400n;
    const ACCESS_LIST_STORAGE_KEY_COST = 1900n;
    gas += BigInt(accessList.length) * ACCESS_LIST_ADDRESS_COST;
    for (const item of accessList) {
      gas += BigInt(item.storageKeys.length) * ACCESS_LIST_STORAGE_KEY_COST;
    }
  }

  return gas;
}

Comment on lines 233 to 242
async sendEvNodeTransaction(args: {
calls: Call[];
executor: HashSigner;
chainId?: bigint;
nonce?: bigint;
maxFeePerGas?: bigint;
maxPriorityFeePerGas?: bigint;
gasLimit?: bigint;
accessList?: AccessList;
}): Promise<Hex> {
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

The args parameter uses an inline anonymous type that is very similar to the EvnodeSendArgs interface defined earlier. This creates code duplication and can make maintenance harder.

To improve this, consider defining a specific type for these arguments that extends EvnodeSendArgs but makes executor a required property. This would make the code more DRY and easier to reason about.

Comment on lines 273 to 282
async createSponsorableIntent(args: {
calls: Call[];
executor: HashSigner;
chainId?: bigint;
nonce?: bigint;
maxFeePerGas?: bigint;
maxPriorityFeePerGas?: bigint;
gasLimit?: bigint;
accessList?: AccessList;
}): Promise<SponsorableIntent> {
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

Similar to sendEvNodeTransaction, this function uses an inline anonymous type for its args parameter which duplicates most of EvnodeIntentArgs. Reusing and extending the existing interface would make the code more maintainable and reduce redundancy.

const nonce = overrides.nonce ?? (await fetchNonce(client, address));
const maxPriorityFeePerGas =
overrides.maxPriorityFeePerGas ?? (await fetchMaxPriorityFee(client));
const maxFeePerGas = overrides.maxFeePerGas ?? (await fetchGasPrice(client));
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

Defaulting maxFeePerGas to the result of eth_gasPrice can be suboptimal for EIP-1559 transactions, as it might lead to overpaying for gas. A more conventional approach is to calculate maxFeePerGas as baseFeePerGas + maxPriorityFeePerGas. This would require fetching the baseFeePerGas from the latest block.

const maxPriorityFeePerGas =
overrides.maxPriorityFeePerGas ?? (await fetchMaxPriorityFee(client));
const maxFeePerGas = overrides.maxFeePerGas ?? (await fetchGasPrice(client));
const gasLimit = overrides.gasLimit ?? estimateIntrinsicGas(calls);
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

To fix the underestimation of intrinsic gas, you should pass the accessList to estimateIntrinsicGas here, assuming you've updated the function as per my other comment.

  const gasLimit = overrides.gasLimit ?? estimateIntrinsicGas(calls, accessList);

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.

[FEATURE] Implement Viem-based custom client for ADR-0003: Typed Transaction 0x76 Sponsorship

3 participants