diff --git a/__tests__/rpcProvider.test.ts b/__tests__/rpcProvider.test.ts index a10bdcabf..c8bf0615e 100644 --- a/__tests__/rpcProvider.test.ts +++ b/__tests__/rpcProvider.test.ts @@ -18,6 +18,8 @@ import { StarknetChainId } from '../src/constants'; import { felt, uint256 } from '../src/utils/calldata/cairo'; import { toHexString } from '../src/utils/num'; import { + compiledC1v2, + compiledC1v2Casm, compiledErc20Echo, compiledL1L2, compiledOpenZeppelinAccount, @@ -109,24 +111,47 @@ describeIfRpc('RPCProvider', () => { }); describe('Test Estimate message fee', () => { - const L1_ADDRESS = '0x8359E4B0152ed5A731162D3c7B0D8D56edB165A0'; - let l1l2ContractAddress: string; + let l1l2ContractCairo0Address: string; + let l1l2ContractCairo1Address: string; beforeAll(async () => { const { deploy } = await account.declareAndDeploy({ contract: compiledL1L2, }); - l1l2ContractAddress = deploy.contract_address; + l1l2ContractCairo0Address = deploy.contract_address; + const { deploy: deploy2 } = await account.declareAndDeploy({ + contract: compiledC1v2, + casm: compiledC1v2Casm, + }); + l1l2ContractCairo1Address = deploy2.contract_address; }); - test('estimate message fee', async () => { - const estimation = await rpcProvider.estimateMessageFee({ + test('estimate message fee Cairo 0', async () => { + const L1_ADDRESS = '0x8359E4B0152ed5A731162D3c7B0D8D56edB165A0'; + const estimationCairo0 = await rpcProvider.estimateMessageFee({ from_address: L1_ADDRESS, - to_address: l1l2ContractAddress, + to_address: l1l2ContractCairo0Address, entry_point_selector: 'deposit', payload: ['556', '123'], }); - expect(estimation).toEqual( + expect(estimationCairo0).toEqual( + expect.objectContaining({ + gas_consumed: expect.anything(), + gas_price: expect.anything(), + overall_fee: expect.anything(), + }) + ); + }); + + test('estimate message fee Cairo 1', async () => { + const L1_ADDRESS = '0x8359E4B0152ed5A731162D3c7B0D8D56edB165'; // not coded in 20 bytes + const estimationCairo1 = await rpcProvider.estimateMessageFee({ + from_address: L1_ADDRESS, + to_address: l1l2ContractCairo1Address, + entry_point_selector: 'increase_bal', + payload: ['100'], + }); + expect(estimationCairo1).toEqual( expect.objectContaining({ gas_consumed: expect.anything(), gas_price: expect.anything(), diff --git a/__tests__/utils/ethSigner.test.ts b/__tests__/utils/ethSigner.test.ts index 21b420bcc..a4b39652d 100644 --- a/__tests__/utils/ethSigner.test.ts +++ b/__tests__/utils/ethSigner.test.ts @@ -13,6 +13,7 @@ import { num, stark, } from '../../src'; +import { validateAndParseEthAddress } from '../../src/utils/eth'; import { ETransactionVersion } from '../../src/types/api'; import { compiledDummy1Eth, @@ -321,4 +322,18 @@ describe('Ethereum signer', () => { ); }); }); + describe('Ethereum address', () => { + test('Eth address format', async () => { + const ethAddr = '0x8359E4B0152ed5A731162D3c7B0D8D56edB165'; // not a valid 20 bytes ETh address + expect(validateAndParseEthAddress(ethAddr)).toBe( + '0x008359e4b0152ed5a731162d3c7b0d8d56edb165' + ); + expect(validateAndParseEthAddress(BigInt(ethAddr))).toBe( + '0x008359e4b0152ed5a731162d3c7b0d8d56edb165' + ); + expect(validateAndParseEthAddress(BigInt(ethAddr).toString(10))).toBe( + '0x008359e4b0152ed5a731162d3c7b0d8d56edb165' + ); + }); + }); }); diff --git a/src/channel/rpc_0_6.ts b/src/channel/rpc_0_6.ts index c7754fbb2..f89ce7eff 100644 --- a/src/channel/rpc_0_6.ts +++ b/src/channel/rpc_0_6.ts @@ -20,6 +20,7 @@ import { import { JRPC, RPCSPEC06 as RPC } from '../types/api'; import { CallData } from '../utils/calldata'; import { isSierra } from '../utils/contract'; +import { validateAndParseEthAddress } from '../utils/eth'; import fetch from '../utils/fetchPonyfill'; import { getSelector, getSelectorFromName } from '../utils/hash'; import { stringify } from '../utils/json'; @@ -565,7 +566,7 @@ export class RpcChannel { ) { const { from_address, to_address, entry_point_selector, payload } = message; const formattedMessage = { - from_address: toHex(from_address), + from_address: validateAndParseEthAddress(from_address), to_address: toHex(to_address), entry_point_selector: getSelector(entry_point_selector), payload: getHexStringArray(payload), diff --git a/src/channel/rpc_0_7.ts b/src/channel/rpc_0_7.ts index 7e74e0fa5..3d1f40eca 100644 --- a/src/channel/rpc_0_7.ts +++ b/src/channel/rpc_0_7.ts @@ -20,6 +20,7 @@ import { import { JRPC, RPCSPEC07 as RPC } from '../types/api'; import { CallData } from '../utils/calldata'; import { isSierra } from '../utils/contract'; +import { validateAndParseEthAddress } from '../utils/eth'; import fetch from '../utils/fetchPonyfill'; import { getSelector, getSelectorFromName } from '../utils/hash'; import { stringify } from '../utils/json'; @@ -569,7 +570,7 @@ export class RpcChannel { ) { const { from_address, to_address, entry_point_selector, payload } = message; const formattedMessage = { - from_address: toHex(from_address), + from_address: validateAndParseEthAddress(from_address), to_address: toHex(to_address), entry_point_selector: getSelector(entry_point_selector), payload: getHexStringArray(payload), diff --git a/src/utils/eth.ts b/src/utils/eth.ts index f32431556..d8c2a7536 100644 --- a/src/utils/eth.ts +++ b/src/utils/eth.ts @@ -1,14 +1,37 @@ import { secp256k1 } from '@noble/curves/secp256k1'; -import { buf2hex, sanitizeHex } from './encode'; +import { addHexPrefix, buf2hex, removeHexPrefix, sanitizeHex } from './encode'; +import type { BigNumberish } from '../types'; +import { assertInRange, toHex } from './num'; +import { ZERO } from '../constants'; +import assert from './assert'; /** * Get random Ethereum private Key. * @returns an Hex string * @example + * ```typescript * const myPK: string = randomAddress() * // result = "0xf04e69ac152fba37c02929c2ae78c9a481461dda42dbc6c6e286be6eb2a8ab83" + * ``` */ export function ethRandomPrivateKey(): string { return sanitizeHex(buf2hex(secp256k1.utils.randomPrivateKey())); } + +/** + * Get a string formatted for an Ethereum address, without uppercase characters. + * @param {BigNumberish} address Address of an Ethereum account. + * @returns an Hex string coded on 20 bytes + * @example + * ```typescript + * const myEthAddress: string = validateAndParseEthAddress("0x8359E4B0152ed5A731162D3c7B0D8D56edB165") + * // result = "0x008359e4b0152ed5a731162d3c7b0d8d56edb165" + * ``` + */ +export function validateAndParseEthAddress(address: BigNumberish): string { + assertInRange(address, ZERO, 2n ** 160n - 1n, 'Ethereum Address '); + const result = addHexPrefix(removeHexPrefix(toHex(address)).padStart(40, '0')); + assert(result.match(/^(0x)?[0-9a-f]{40}$/), 'Invalid Ethereum Address Format'); + return result; +} diff --git a/www/docs/guides/L1message.md b/www/docs/guides/L1message.md index eec10460c..8e154b8c8 100644 --- a/www/docs/guides/L1message.md +++ b/www/docs/guides/L1message.md @@ -42,11 +42,11 @@ const responseEstimateMessageFee = await provider.estimateMessageFee({ from_address: L1address, to_address: L2address, entry_point_selector: 'handle_l1_mess', - payload: ['1234567890123456789', '200'], + payload: ['1234567890123456789', '200'], // without from_address }); ``` -If the fee is paid in L1, the Cairo contract at `to_Address` is automatically executed, function `entry_point_selector` (the function shall have a decorator `@l1_handler` in the Cairo code), with parameters `payload`. +If the fee is paid in L1, the Cairo contract at `to_Address` is automatically executed, function `entry_point_selector` (the function shall have a decorator `#[l1_handler]` in the Cairo code, with a first parameter called `from_address: felt252`). The payload shall not include the `from_address` parameter. ## L2 ➡️ L1 messages