Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
181 changes: 181 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -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/<methodName>.ts`:
```typescript
export async function myMethodHandler(request: RPCRequest): Promise<RPCResponse | RPCError> {
// 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
2 changes: 1 addition & 1 deletion nodemon.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"watch": ["src"],
"watch": ["src", "config.json"],
"ext": ".ts,.js",
"ignore": [],
"exec": "npx ts-node ./src/index.ts"
Expand Down
34 changes: 33 additions & 1 deletion src/rpc/calls/ethCall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import {
decodeEVMCalldataWithAddressConversion,
encodeStarknetData,
getFunctionSelectorFromCalldata,
requiresTokenMetadataException,
handleTokenMetadataException,
} from '../../utils/calldata'
import {
ConvertableType,
Expand Down Expand Up @@ -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 =
Expand Down Expand Up @@ -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 <RPCError>{
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',
Expand All @@ -224,7 +256,7 @@ export async function ethCallHandler(
error: snResponse,
}
}

const starknetFunctionEthereumOutputTypes: Array<CairoNamedConvertableType> =
getEthereumOutputsCairoNamed(
starknetFunction.snFunction,
Expand Down
91 changes: 85 additions & 6 deletions src/utils/calldata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand All @@ -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')
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
Loading