From 9996dfb6094042203088cbad0b6e0ff67c05c69b Mon Sep 17 00:00:00 2001 From: Boreas09 Date: Wed, 22 Oct 2025 15:26:49 +0300 Subject: [PATCH] Update configuration and enhance ethCall handling - Modified nodemon.json to watch config.json for changes. - Added token metadata exception handling in ethCall.ts. - Introduced utility functions for token metadata processing in calldata.ts. - Added comprehensive documentation in CLAUDE.md for project setup and usage. --- CLAUDE.md | 181 +++++++++++++++++++++++++++++++++++++++ nodemon.json | 2 +- src/rpc/calls/ethCall.ts | 34 +++++++- src/utils/calldata.ts | 91 ++++++++++++++++++-- 4 files changed, 300 insertions(+), 8 deletions(-) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..5583570e --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,181 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Rosettanet is an Ethereum-to-Starknet RPC middleware that translates Ethereum JSON-RPC requests into Starknet RPC calls and formats responses back to Ethereum format. This enables users to interact with Starknet using existing EVM wallets (MetaMask, etc.) and libraries (ethers, web3.js). + +**Key Concept**: Rosettanet is NOT a Starknet node—it's a translation layer that requires a working Starknet node connection. + +## Essential Commands + +### Development +```bash +npm run start:dev # Start with nodemon hot reload +npm start # Direct start without hot reload +``` + +### Testing +```bash +npm test # Run all unit tests with Jest +npm run test:watch # Run tests in watch mode +npm run test:utils # Watch tests for utilities +npm run test:converters # Watch tests for converters +npm run test:e2e # Run end-to-end tests (starts Starknet devnet) +npm run test:e2e:specific # Run specific e2e tests +``` + +### Code Quality +```bash +npm run lint # Run ESLint +npm run format # Format code with Prettier +npm run clean # Clean dist directory +``` + +### Development with Logging +```bash +npm run start:console-logging # Enable console logging +npm run start:file-logging # Log to file +npm run start:dev:console-sniff # Console logging with request sniffer +npm run start:dev:file-sniff # File logging with request sniffer +npm run start:dev:console-error-sniffer # Log errors only to console +``` + +## Architecture + +### Core Components + +**[src/index.ts](src/index.ts)** - Entry point that: +1. Initializes configuration from `config.json` +2. Syncs initial block number and gas price +3. Starts background sync processes +4. Starts Express server + +**[src/server.ts](src/server.ts)** - Express server that: +- Handles JSON-RPC 2.0 requests (POST only) +- Supports both single and batch requests +- Parses `text/plain` and missing Content-Type headers +- Routes to appropriate RPC handlers + +**[src/rpc/calls.ts](src/rpc/calls.ts)** - Central RPC router: +- Maps Ethereum RPC method names to handlers (e.g., `eth_chainId`, `eth_getBalance`) +- Handles special `starknet_*` methods directly (early exit) +- Returns JSON-RPC 2.0 compliant responses/errors +- All RPC implementations are in `src/rpc/calls/*.ts` + +**[src/cache/](src/cache/)** - Background sync system: +- `blockNumber.ts` - Syncs Starknet block height every 10 seconds +- `gasPrice.ts` - Syncs gas prices every 10 seconds +- Used to provide faster responses for common queries + +### Translation Layer + +**[src/utils/rosettanet.ts](src/utils/rosettanet.ts)** - Rosettanet account operations: +- `getRosettaAccountAddress()` - Precalculates Starknet account address from Ethereum address +- `deployRosettanetAccount()` - Deploys Cairo account contract that verifies Ethereum signatures +- `getRosettanetAccountNonce()` - Fetches nonce from deployed account +- `parseRosettanetRawCalldata()` - Decodes transaction data from Cairo calldata format + +**[src/utils/converters/](src/utils/converters/)** - Data format converters: +- `integer.ts` - Converts between Ethereum and Starknet number formats (U256 ↔ Uint256) +- `abiFormatter.ts` - Transforms ABIs between formats + +**[src/utils/](src/utils/)** - Core utilities: +- `calldata.ts` - Constructs Cairo calldata from Ethereum transaction data +- `transaction.ts` - Transaction format conversions +- `signature.ts` - Signature handling and validation +- `gas.ts` - Gas estimation and resource bound calculations +- `resourceBounds.ts` - Converts gas limits to Starknet v3 resource bounds +- `starknet.ts` - Starknet-specific utilities +- `wrapper.ts` - Response formatting helpers +- `callHelper.ts` - Makes RPC calls to underlying Starknet node + +## Configuration + +**[config.json](config.json)** - Runtime configuration: +- `port`, `host` - Server settings +- `rpcUrls` - Starknet RPC endpoints (array for fallback) +- `chainId` - Ethereum-style chain ID (e.g., `0x52535453`) +- `accountClass` - Cairo account contract class hash +- `rosettanet` - Rosettanet factory contract address +- `ethAddress`, `strkAddress` - Token contract addresses +- `validateFeeEstimator` - Contract for fee estimation +- `logging` - Configure logging behavior (active, sniffer, output type, severity) + +**[config.test.json](config.test.json)** - Test environment configuration + +## Testing + +**Unit Tests** ([tests/](tests/)): +- Use Jest with ts-jest preset +- Setup file: `tests/setup.ts` +- Test utilities in `tests/utils/` +- Converters tested in `tests/utils/converters/` + +**E2E Tests** ([e2e/](e2e/)): +- Require Starknet devnet running on port 6050 +- Script `scripts/e2e_tests.sh` starts devnet, runs tests, cleans up +- Use Jest with custom config: `e2e/jest.config.ts` +- Include account registration, deployment, and transaction flow tests + +## Key Patterns + +### Adding a New RPC Method + +1. Create handler in `src/rpc/calls/.ts`: +```typescript +export async function myMethodHandler(request: RPCRequest): Promise { + // Parse params + // Call Starknet via callHelper + // Transform response to Ethereum format + // Return RPCResponse or RPCError +} +``` + +2. Register in `src/rpc/calls.ts`: +```typescript +import { myMethodHandler } from './calls/myMethod' +Methods.set('eth_myMethod', { + method: 'eth_myMethod', + handler: myMethodHandler, +}) +``` + +### Calling Starknet RPC + +Use `callStarknet()` from `src/utils/callHelper.ts`: +```typescript +const response: RPCResponse | StarknetRPCError = await callStarknet({ + jsonrpc: '2.0', + method: 'starknet_call', + params: [...], + id: 1, +}) + +if (isStarknetRPCError(response)) { + // Handle error +} +``` + +### Type Guards + +Always use type guards from `src/types/typeGuards.ts`: +- `isRPCResponse()`, `isRPCError()` - Check response types +- `isStarknetRPCError()` - Check Starknet errors +- `isSignedRawTransaction()` - Validate transaction format + +## Important Notes + +- Rosettanet uses **Cairo account contracts** that verify Ethereum signatures on Starknet +- Transaction flow: Ethereum transaction → Rosettanet calldata → Cairo account → Starknet execution +- Block numbers and gas prices are cached—use `getCachedBlockNumber()` and `getCachedGasPrice()` for performance +- Logger (`src/logger.ts`) supports console/file output and request sniffing—check severity levels (0=info, 1=warning, 2=error) +- Starknet uses resource bounds (v3 transactions) instead of simple gas limits—see `getDeploymentResourceBounds()` and `getResourceBounds()` +- Configuration is loaded once at startup via `initConfig()` and accessed via `getConfigurationProperty()` + +## Documentation + +- [docs/Infura_JSON_RPC_methods.md](docs/Infura_JSON_RPC_methods.md) - Ethereum JSON-RPC method reference +- [docs/architecture.png](docs/architecture.png) - High-level architecture diagram +- [README.md](README.md) - Project overview and contributor information diff --git a/nodemon.json b/nodemon.json index 3d850a53..51efcd54 100644 --- a/nodemon.json +++ b/nodemon.json @@ -1,5 +1,5 @@ { - "watch": ["src"], + "watch": ["src", "config.json"], "ext": ".ts,.js", "ignore": [], "exec": "npx ts-node ./src/index.ts" diff --git a/src/rpc/calls/ethCall.ts b/src/rpc/calls/ethCall.ts index adb79e75..f3ef394b 100644 --- a/src/rpc/calls/ethCall.ts +++ b/src/rpc/calls/ethCall.ts @@ -24,6 +24,8 @@ import { decodeEVMCalldataWithAddressConversion, encodeStarknetData, getFunctionSelectorFromCalldata, + requiresTokenMetadataException, + handleTokenMetadataException, } from '../../utils/calldata' import { ConvertableType, @@ -127,6 +129,8 @@ export async function ethCallHandler( result: '0x', } } + + // ETH CALL BAZEN from field bos geliyor. // to ise registered degilse result 0x donmeli const targetContractAddress: string | StarknetRPCError = @@ -210,6 +214,34 @@ export async function ethCallHandler( 'pending', // update to latest ] + // Check if this is a token metadata function that requires special handling + if (requiresTokenMetadataException(targetFunctionSelector)) { + const snResponse: RPCResponse | StarknetRPCError = await callStarknet({ + jsonrpc: request.jsonrpc, + method: 'starknet_call', + params: starknetCallParams, + id: request.id, + }) + + if (isStarknetRPCError(snResponse)) { + return { + jsonrpc: request.jsonrpc, + id: request.id, + error: snResponse, + } + } + + const exceptionResult = handleTokenMetadataException( + snResponse.result + ) + + return { + jsonrpc: request.jsonrpc, + id: request.id, + result: exceptionResult, + } + } + const snResponse: RPCResponse | StarknetRPCError = await callStarknet({ jsonrpc: request.jsonrpc, method: 'starknet_call', @@ -224,7 +256,7 @@ export async function ethCallHandler( error: snResponse, } } - + const starknetFunctionEthereumOutputTypes: Array = getEthereumOutputsCairoNamed( starknetFunction.snFunction, diff --git a/src/utils/calldata.ts b/src/utils/calldata.ts index 07d43ad7..35e25547 100644 --- a/src/utils/calldata.ts +++ b/src/utils/calldata.ts @@ -14,7 +14,7 @@ import { } from './converters/integer' import { getSnAddressWithFallback } from './wrapper' import { CairoNamedConvertableType } from './starknet' -import { addHexPrefix, removeHexZeroes } from './padding' +import { addHexPrefix, removeHexZeroes, removeHexPrefix } from './padding' import { isStarknetRPCError } from '../types/typeGuards' import { convertStringIntoChunks } from './felt' @@ -31,6 +31,85 @@ export function getFunctionSelectorFromCalldata(calldata: any): string | null { return calldata.substring(0, 10) } +/** + * Checks if the function selector is one that requires special exception handling + * 0x06fdde03 - name() function + * 0x95d89b41 - symbol() function + * @param selector The function selector to check + * @returns true if the selector requires exception handling + */ +export function requiresTokenMetadataException(selector: string | null): boolean { + if (selector === null) { + return false + } + + const NAME_SELECTOR = '0x06fdde03' + const SYMBOL_SELECTOR = '0x95d89b41' + + return selector === NAME_SELECTOR || selector === SYMBOL_SELECTOR +} + +/** + * Converts hex string to ASCII string + * @param hex Hex string to convert (with or without 0x prefix) + * @returns ASCII string + */ +function hexToAscii(hex: string): string { + const cleanHex = removeHexPrefix(hex) + let str = '' + for (let i = 0; i < cleanHex.length; i += 2) { + str += String.fromCharCode(parseInt(cleanHex.substr(i, 2), 16)) + } + return str +} + +/** + * Handles special exception for token metadata functions (name and symbol) + * Converts Starknet result format to Ethereum-compatible string format + * + * Supports two formats: + * 1. Single element array: ["0x4574686572"] - Direct hex encoded string + * 2. Three element array: ["0x0", "0x537461726b6e65742074425443", "0xd"] - Felt252 encoded string with length + * + * @param snResult Incoming result from starknet RPC for name or symbol request. + * @returns Ethereum-compatible ABI encoded string + */ +export function handleTokenMetadataException(snResult: string[]): string { + if (snResult.length === 1) { + // Format 1: Single hex string + // Remove 0x and convert hex to ASCII + const asciiValue = hexToAscii(snResult[0]) + + // Encode as Ethereum string format using AbiCoder + const encoder = new AbiCoder() + return encoder.encode(['string'], [asciiValue]) + } else if (snResult.length === 3) { + // Format 2: Three element array [padding, hex_data, length] + // The second element contains the hex data + // The third element is the expected string length + const hexData = snResult[1] + const expectedLength = parseInt(removeHexPrefix(snResult[2]), 16) + + // Convert hex to ASCII + const asciiValue = hexToAscii(hexData) + + // Verify length matches expected length + if (asciiValue.length !== expectedLength) { + throw new Error( + `String length mismatch: expected ${expectedLength}, got ${asciiValue.length}` + ) + } + + // Encode as Ethereum string format using AbiCoder + const encoder = new AbiCoder() + return encoder.encode(['string'], [asciiValue]) + } else { + throw new Error( + `Invalid snResult format for token metadata. Expected 1 or 3 elements, got ${snResult.length}` + ) + } +} + export function to128Bits(calldata: string): string[] { if (!calldata.startsWith('0x')) { throw new Error('Calldata must be a hex sting starting with 0x') @@ -63,19 +142,19 @@ export function to128Bits(calldata: string): string[] { // Returns calldata in a serialized string format export function to256Bits(calldata: string[]): string { - if(calldata.length == 0) { + if (calldata.length == 0) { return '0x' } - if(calldata.length == 1) { + if (calldata.length == 1) { return calldata[0]; // returns only selector } const selector = calldata[0]; let str = `${selector}` for (let i = 1; i < calldata.length; i++) { - const data = safeU256ToUint256([calldata[i], calldata[i+1]]); + const data = safeU256ToUint256([calldata[i], calldata[i + 1]]); str = str + data.replace('0x', '') - i +=2; + i += 2; } return str; @@ -130,7 +209,7 @@ export function mergeSlots( for (let i = 0; i < data.length; i++) { const currentType = types[typeIndex] - if(currentType.solidityType === 'uint256' && currentType.cairoType === 'core::felt252') { + if (currentType.solidityType === 'uint256' && currentType.cairoType === 'core::felt252') { encodedValues.push(data[i]) typeIndex++ continue