Created
January 27, 2026 15:00
-
-
Save MaksimDrozd/620023dbea322faae336eade4af51454 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env ts-node | |
| /** | |
| * Complete CrossCurve bridge flow script | |
| * | |
| * This script demonstrates the full flow from CrossCurve API configuration | |
| * to building calldata, creating user operations, and sending them to the chain. | |
| * | |
| * Swap: Native ETH from Base mainnet to Native ETH on Arbitrum mainnet | |
| * | |
| * Usage: | |
| * ts-node scripts/crosscurve-full-flow.ts | |
| * | |
| * Required environment variables or config: | |
| * - OWNER_PRIVATE_KEY: Private key of the AA wallet owner (EOA) | |
| * - AA_ADDRESS: Smart Account (Kernel) address | |
| * - BUNDLER_RPC_ARBITRUM: Bundler RPC URL for Arbitrum (if using bundler) | |
| * - ENTRYPOINT_OWNER_PRIVATE_KEY: Private key for EntryPoint owner (if not using bundler) | |
| */ | |
| import axios from 'axios'; | |
| import { BigNumber, constants, ethers } from 'ethers'; | |
| // ============================================================================ | |
| // CONFIGURATION SECTION | |
| // ============================================================================ | |
| const CONFIG = { | |
| // CrossCurve API Configuration | |
| crossCurve: { | |
| baseUrl: process.env.CROSSCURVE_BASE_URL || 'https://pusher-cdp.x.ubtk.dev', | |
| apiKey: | |
| process.env.CROSSCURVE_API_KEY || 'test-sdk-test-sdk-test-sdk-standard', | |
| endpoints: { | |
| routing: '/routing/scan', | |
| txCreate: '/tx/create', | |
| }, | |
| defaultSlippage: 0.5, | |
| }, | |
| // Chain Configuration | |
| chains: { | |
| base: { | |
| chainId: 8453, // Base mainnet | |
| rpc: | |
| process.env.BASE_RPC || | |
| 'https://go.getblock.io/e5bafd5ef9d546ef85521b5b3a09f27a', | |
| nativeToken: constants.AddressZero, // Native ETH | |
| nativeSymbol: 'ETH', | |
| nativeDecimals: 18, | |
| }, | |
| arbitrum: { | |
| chainId: 42161, // Arbitrum One mainnet | |
| rpc: | |
| process.env.ARBITRUM_RPC || | |
| 'https://go.getblock.io/3eb2408d7595404cac0fdc18823f09c3', | |
| nativeToken: constants.AddressZero, // Native ETH | |
| nativeSymbol: 'ETH', | |
| nativeDecimals: 18, | |
| }, | |
| optimism: { | |
| chainId: 10, // Optimism mainnet | |
| rpc: | |
| process.env.OPTIMISM_RPC || | |
| 'https://go.getblock.io/c515386aaa4944d58fb10697b46819b5', | |
| nativeToken: constants.AddressZero, // Native ETH | |
| nativeSymbol: 'ETH', | |
| nativeDecimals: 18, | |
| }, | |
| }, | |
| // Account Abstraction Configuration | |
| aa: { | |
| // TODO: Set your AA address here or via env | |
| aaAddress: | |
| process.env.AA_ADDRESS || '0x176f9d5cd42303f9Bb0a1DfEE5A36fE5Eb2F1A9f', // Smart Account (Kernel) address | |
| // TODO: Set owner private key here or via env | |
| ownerPrivateKey: | |
| process.env.OWNER_PRIVATE_KEY || | |
| '0xe4e5602ceaae714dadbd3d46b9c51f843ab265d5f53ab66ec20e30d46737870d', // EOA private key that owns the AA | |
| // Entry Point Configuration | |
| entryPointAddress: | |
| process.env.ENTRYPOINT_ADDRESS || | |
| '0x0000000071727De22E5E9d8BAf0edAc6f37da032', // ERC-4337 EntryPoint v0.7 | |
| // Nonce key (random number for nonce management) | |
| nonceKey: Math.floor(Math.random() * Number.MAX_SAFE_INTEGER), | |
| }, | |
| // Swap Configuration | |
| swap: { | |
| amountIn: process.env.SWAP_AMOUNT || '0.0001', // Amount in ETH (will be converted to wei) | |
| tokenIn: constants.AddressZero, // Native ETH | |
| tokenOut: constants.AddressZero, // Native ETH | |
| slippage: 0.5, // 0.5% | |
| }, | |
| // Gas Configuration (from fee.constants.ts) | |
| gas: { | |
| callGasLimit: '0x1E8480', // 2000000 (increased for complex bridge operations) | |
| verificationGasLimit: '0x249f0', // 150000 | |
| preVerificationGas: '0x30d40', // 200000 | |
| }, | |
| // EntryPoint Configuration | |
| entryPoint: { | |
| // TODO: Set private key for sending handleOps transaction | |
| // This should be a wallet that has ETH to pay for gas | |
| ownerPrivateKey: process.env.ENTRYPOINT_OWNER_PRIVATE_KEY, | |
| }, | |
| } as const; | |
| // ============================================================================ | |
| // TYPE DEFINITIONS | |
| // ============================================================================ | |
| interface CrossChainSwapConfig { | |
| chainIdIn: number; | |
| chainIdOut: number; | |
| tokenIn: string; | |
| tokenOut: string; | |
| amountIn: string; | |
| slippage: number; | |
| } | |
| interface RoutingScanBody { | |
| from: string; | |
| params: { | |
| chainIdIn: number; | |
| chainIdOut: number; | |
| tokenIn: string; | |
| tokenOut: string; | |
| amountIn: string; | |
| }; | |
| slippage: number; | |
| } | |
| interface RoutingScanResult { | |
| route: Array<{ | |
| type: string; | |
| limits?: { min: string; max: string }; | |
| }>; | |
| deliveryFee?: { amount: string }; | |
| signature?: string; | |
| [key: string]: any; | |
| } | |
| interface CreateTxParams { | |
| from: string; | |
| recipient: string; | |
| routing: RoutingScanResult & { signature: string }; | |
| buildCalldata?: boolean; | |
| } | |
| interface StartPopulatedTx { | |
| to: string; | |
| abi: string; | |
| args: unknown[]; | |
| value: string; | |
| } | |
| interface SimpleTx { | |
| to: string; | |
| value: string; | |
| data: string; | |
| } | |
| type CreateTxResponse = StartPopulatedTx | SimpleTx; | |
| interface SignaturePack { | |
| executionPrice: string; | |
| deadline: string; | |
| v: string; | |
| r: string; | |
| s: string; | |
| } | |
| interface KernelCallDataParams { | |
| toAddress: string; | |
| amount: string; | |
| data: string; | |
| } | |
| interface IUserOperation { | |
| sender: string; | |
| nonce: string; | |
| callData: string; | |
| callGasLimit: string; | |
| verificationGasLimit: string; | |
| preVerificationGas: string; | |
| maxFeePerGas: string; | |
| maxPriorityFeePerGas: string; | |
| signature: string; | |
| initCode?: string; | |
| factory?: string | null; | |
| factoryData?: string; | |
| paymasterAndData?: string; | |
| } | |
| interface IPackedUserOperation { | |
| sender: string; | |
| nonce: string; | |
| initCode: string; | |
| callData: string; | |
| accountGasLimits: string; | |
| preVerificationGas: string; | |
| gasFees: string; | |
| paymasterAndData: string; | |
| signature: string; | |
| } | |
| // ============================================================================ | |
| // UTILITY FUNCTIONS | |
| // ============================================================================ | |
| function validateConfig(): void { | |
| const errors: string[] = []; | |
| if (!CONFIG.aa.aaAddress) { | |
| errors.push('AA_ADDRESS is required'); | |
| } | |
| if (!CONFIG.aa.ownerPrivateKey) { | |
| errors.push('OWNER_PRIVATE_KEY is required (for signing user operation)'); | |
| } | |
| if (!CONFIG.entryPoint.ownerPrivateKey) { | |
| errors.push( | |
| 'ENTRYPOINT_OWNER_PRIVATE_KEY is required (for sending handleOps transaction)', | |
| ); | |
| } | |
| if (errors.length > 0) { | |
| console.error('Configuration errors:'); | |
| errors.forEach((err) => console.error(` - ${err}`)); | |
| console.error( | |
| '\nPlease set the required environment variables or update CONFIG in the script.', | |
| ); | |
| process.exit(1); | |
| } | |
| } | |
| // ============================================================================ | |
| // CROSSCURVE API CLIENT | |
| // ============================================================================ | |
| class CrossCurveAPIClient { | |
| private baseUrl: string; | |
| private apiKey: string; | |
| constructor(baseUrl: string, apiKey: string) { | |
| this.baseUrl = baseUrl; | |
| this.apiKey = apiKey; | |
| } | |
| private getHeaders(): Record<string, string> { | |
| return { | |
| 'Content-Type': 'application/json', | |
| 'api-key': this.apiKey, | |
| }; | |
| } | |
| async getRoutes( | |
| config: CrossChainSwapConfig, | |
| from: string, | |
| ): Promise<RoutingScanResult[]> { | |
| console.log( | |
| `[CrossCurve] Getting routes: ${config.chainIdIn} -> ${config.chainIdOut}`, | |
| ); | |
| console.log( | |
| `[CrossCurve] Amount: ${config.amountIn}, Token: ${config.tokenIn}`, | |
| ); | |
| const requestBody: RoutingScanBody = { | |
| from, | |
| params: { | |
| chainIdIn: config.chainIdIn, | |
| chainIdOut: config.chainIdOut, | |
| tokenIn: config.tokenIn, | |
| tokenOut: config.tokenOut, | |
| amountIn: config.amountIn, | |
| }, | |
| slippage: config.slippage, | |
| }; | |
| const url = `${this.baseUrl}${CONFIG.crossCurve.endpoints.routing}`; | |
| const response = await axios.post<RoutingScanResult[]>(url, requestBody, { | |
| headers: this.getHeaders(), | |
| }); | |
| console.log(`[CrossCurve] Found ${response.data.length} routes`); | |
| return response.data; | |
| } | |
| async createTransaction( | |
| route: RoutingScanResult, | |
| fromAddress: string, | |
| buildCalldata = false, | |
| ): Promise<CreateTxResponse> { | |
| console.log( | |
| `[CrossCurve] Creating transaction, buildCalldata=${buildCalldata}`, | |
| ); | |
| if (!route.signature) { | |
| throw new Error('Route signature is required'); | |
| } | |
| const requestBody: CreateTxParams = { | |
| from: fromAddress, | |
| recipient: fromAddress, | |
| routing: route as RoutingScanResult & { signature: string }, | |
| buildCalldata, | |
| }; | |
| const url = `${this.baseUrl}${CONFIG.crossCurve.endpoints.txCreate}`; | |
| const response = await axios.post<CreateTxResponse>(url, requestBody, { | |
| headers: this.getHeaders(), | |
| }); | |
| console.log(`[CrossCurve] Transaction created: to=${response.data.to}`); | |
| return response.data; | |
| } | |
| } | |
| // ============================================================================ | |
| // CALLDATA BUILDER | |
| // ============================================================================ | |
| class CalldataBuilder { | |
| private kernelInterface: ethers.utils.Interface; | |
| constructor() { | |
| this.kernelInterface = new ethers.utils.Interface([ | |
| 'function execute(bytes32 mode, bytes executionCalldata) external payable returns (bytes[] memory)', | |
| ]); | |
| } | |
| isNativeToken(tokenAddress: string): boolean { | |
| return ( | |
| tokenAddress.toLowerCase() === constants.AddressZero.toLowerCase() || | |
| tokenAddress === constants.AddressZero | |
| ); | |
| } | |
| buildKernelCallData(params: KernelCallDataParams[]): string { | |
| const CALLTYPE_BATCH = '0x01'; | |
| const EXECTYPE_DEFAULT = '0x00'; | |
| // Encode batch execute | |
| const batchExecuteData = this.encodeBatch( | |
| params.map((param) => ({ | |
| target: param.toAddress, | |
| value: param.amount, | |
| callData: param.data || '0x', | |
| })), | |
| ); | |
| // Create exec mode: bytes32 structure | |
| // Structure: [callType (1 byte), execType (1 byte), modeSelector (4 bytes), payload (26 bytes)] = 32 bytes | |
| const callTypeBytes = ethers.utils.arrayify(CALLTYPE_BATCH); | |
| const execTypeBytes = ethers.utils.arrayify(EXECTYPE_DEFAULT); | |
| const modeSelectorBytes = ethers.utils.arrayify('0x00000000'); | |
| const payloadBytes = ethers.utils.arrayify(constants.HashZero).slice(0, 26); | |
| const execMode = ethers.utils.hexlify( | |
| ethers.utils.concat([ | |
| callTypeBytes, | |
| execTypeBytes, | |
| modeSelectorBytes, | |
| payloadBytes, | |
| ]), | |
| ); | |
| // Encode kernel execute call | |
| const kernelExecuteCallData = this.kernelInterface.encodeFunctionData( | |
| 'execute', | |
| [execMode, batchExecuteData], | |
| ); | |
| return kernelExecuteCallData; | |
| } | |
| private encodeBatch( | |
| executions: { target: string; value: string; callData: string }[], | |
| ): string { | |
| const batchAbi = ['tuple(address target, uint256 value, bytes callData)[]']; | |
| return ethers.utils.defaultAbiCoder.encode(batchAbi, [executions]); | |
| } | |
| } | |
| // ============================================================================ | |
| // USER OPERATION BUILDER | |
| // ============================================================================ | |
| class HandleOpsBuilder { | |
| private entryPointInterface: ethers.utils.Interface; | |
| constructor() { | |
| this.entryPointInterface = new ethers.utils.Interface([ | |
| 'function handleOps(tuple(address sender, uint256 nonce, bytes initCode, bytes callData, bytes32 accountGasLimits, uint256 preVerificationGas, bytes32 gasFees, bytes paymasterAndData, bytes signature)[] ops, address beneficiary)', | |
| ]); | |
| } | |
| buildHandleOpsCalldata( | |
| ops: IPackedUserOperation[], | |
| beneficiary: string, | |
| ): string { | |
| return this.entryPointInterface.encodeFunctionData('handleOps', [ | |
| ops, | |
| beneficiary, | |
| ]); | |
| } | |
| } | |
| class UserOperationBuilder { | |
| private entryPointAddress: string; | |
| private entryPointInterface: ethers.utils.Interface; | |
| constructor(entryPointAddress: string) { | |
| this.entryPointAddress = entryPointAddress; | |
| this.entryPointInterface = new ethers.utils.Interface([ | |
| 'function getNonce(address sender, uint192 key) external view returns (uint256 nonce)', | |
| ]); | |
| } | |
| async createBaseUserOp( | |
| sender: string, | |
| callData: string, | |
| rpcProvider: ethers.providers.JsonRpcProvider, | |
| nonceKey: number, | |
| maxFeePerGas: string, | |
| maxPriorityFeePerGas: string, | |
| initCode = '0x', | |
| ): Promise<IUserOperation> { | |
| const entryPointContract = new ethers.Contract( | |
| this.entryPointAddress, | |
| this.entryPointInterface, | |
| rpcProvider, | |
| ); | |
| const nonce = await entryPointContract.getNonce(sender, nonceKey); | |
| return { | |
| sender, | |
| nonce: ethers.utils.hexlify(nonce), | |
| callData, | |
| callGasLimit: CONFIG.gas.callGasLimit, | |
| verificationGasLimit: CONFIG.gas.verificationGasLimit, | |
| preVerificationGas: CONFIG.gas.preVerificationGas, | |
| maxFeePerGas, | |
| maxPriorityFeePerGas, | |
| signature: '0x', | |
| initCode, | |
| }; | |
| } | |
| packUserOp(userOp: IUserOperation, initCode: string): IPackedUserOperation { | |
| const accountGasLimits = ethers.utils.solidityPack( | |
| ['uint128', 'uint128'], | |
| [userOp.verificationGasLimit, userOp.callGasLimit], | |
| ); | |
| const gasFees = ethers.utils.solidityPack( | |
| ['uint128', 'uint128'], | |
| [userOp.maxFeePerGas, userOp.maxPriorityFeePerGas], | |
| ); | |
| return { | |
| sender: userOp.sender, | |
| nonce: userOp.nonce, | |
| initCode: initCode || '0x', | |
| callData: userOp.callData, | |
| accountGasLimits, | |
| preVerificationGas: userOp.preVerificationGas, | |
| gasFees, | |
| paymasterAndData: userOp.paymasterAndData || '0x', | |
| signature: userOp.signature, | |
| }; | |
| } | |
| calculateUserOpHash( | |
| packedUserOp: IPackedUserOperation, | |
| chainId: number, | |
| ): string { | |
| const innerHash = ethers.utils.keccak256( | |
| ethers.utils.defaultAbiCoder.encode( | |
| [ | |
| 'address', | |
| 'uint256', | |
| 'bytes32', | |
| 'bytes32', | |
| 'bytes32', | |
| 'uint256', | |
| 'bytes32', | |
| 'bytes32', | |
| ], | |
| [ | |
| packedUserOp.sender, | |
| packedUserOp.nonce, | |
| ethers.utils.keccak256(packedUserOp.initCode), | |
| ethers.utils.keccak256(packedUserOp.callData), | |
| packedUserOp.accountGasLimits, | |
| packedUserOp.preVerificationGas, | |
| packedUserOp.gasFees, | |
| ethers.utils.keccak256(packedUserOp.paymasterAndData), | |
| ], | |
| ), | |
| ); | |
| return ethers.utils.keccak256( | |
| ethers.utils.defaultAbiCoder.encode( | |
| ['bytes32', 'address', 'uint256'], | |
| [innerHash, this.entryPointAddress, BigInt(chainId)], | |
| ), | |
| ); | |
| } | |
| } | |
| // ============================================================================ | |
| // MAIN FLOW | |
| // ============================================================================ | |
| async function main() { | |
| console.log('='.repeat(80)); | |
| console.log('CrossCurve Bridge Flow: Base ETH -> Arbitrum ETH'); | |
| console.log('='.repeat(80)); | |
| console.log(); | |
| // Validate configuration | |
| validateConfig(); | |
| // Initialize clients | |
| const crossCurveClient = new CrossCurveAPIClient( | |
| CONFIG.crossCurve.baseUrl, | |
| CONFIG.crossCurve.apiKey, | |
| ); | |
| const calldataBuilder = new CalldataBuilder(); | |
| const userOpBuilder = new UserOperationBuilder(CONFIG.aa.entryPointAddress); | |
| const handleOpsBuilder = new HandleOpsBuilder(); | |
| // Step 1: Prepare swap configuration | |
| console.log('[Step 1] Preparing swap configuration...'); | |
| const amountInWei = ethers.utils.parseEther(CONFIG.swap.amountIn); | |
| const swapConfig: CrossChainSwapConfig = { | |
| chainIdIn: CONFIG.chains.base.chainId, | |
| chainIdOut: CONFIG.chains.arbitrum.chainId, | |
| tokenIn: CONFIG.swap.tokenIn, | |
| tokenOut: CONFIG.swap.tokenOut, | |
| amountIn: amountInWei.toString(), | |
| slippage: CONFIG.swap.slippage, | |
| }; | |
| console.log( | |
| ` Amount: ${CONFIG.swap.amountIn} ETH (${amountInWei.toString()} wei)`, | |
| ); | |
| console.log(` From: Base (${swapConfig.chainIdIn})`); | |
| console.log(` To: Arbitrum (${swapConfig.chainIdOut})`); | |
| console.log(); | |
| // Step 2: Get routes from CrossCurve API | |
| console.log('[Step 2] Getting routes from CrossCurve API...'); | |
| const routes = await crossCurveClient.getRoutes( | |
| swapConfig, | |
| CONFIG.aa.aaAddress, | |
| ); | |
| if (routes.length === 0) { | |
| throw new Error('No routes available'); | |
| } | |
| const bestRoute = routes[0]; | |
| console.log(` Found ${routes.length} route(s)`); | |
| console.log(' π Route Details:'); | |
| console.log( | |
| ` Delivery Fee: ${bestRoute.deliveryFee?.amount || '0'} wei (${ethers.utils.formatEther(bestRoute.deliveryFee?.amount || '0')} ETH)`, | |
| ); | |
| console.log( | |
| ` Expected Finality: ${bestRoute.expectedFinalitySeconds || 'N/A'} seconds`, | |
| ); | |
| console.log(` Slippage: ${bestRoute.slippage || 'N/A'}%`); | |
| console.log(` Price Impact: ${bestRoute.priceImpact || 'N/A'}%`); | |
| console.log( | |
| ` Amount Out: ${bestRoute.amountOut || 'N/A'} wei (${bestRoute.amountOut ? ethers.utils.formatEther(bestRoute.amountOut) : 'N/A'} ETH)`, | |
| ); | |
| console.log( | |
| ` Amount In: ${bestRoute.amountIn || 'N/A'} wei (${bestRoute.amountIn ? ethers.utils.formatEther(bestRoute.amountIn) : 'N/A'} ETH)`, | |
| ); | |
| if (bestRoute.route && Array.isArray(bestRoute.route)) { | |
| console.log(` Route Steps: ${bestRoute.route.length}`); | |
| } | |
| if (bestRoute.signature) { | |
| console.log(` Signature: ${bestRoute.signature.substring(0, 20)}...`); | |
| } | |
| console.log(); | |
| // Step 3: Create transaction via CrossCurve API | |
| console.log('[Step 3] Creating transaction via CrossCurve API...'); | |
| const transactionData = await crossCurveClient.createTransaction( | |
| bestRoute, | |
| CONFIG.aa.aaAddress, | |
| false, // buildCalldata: false - we build it ourselves | |
| ); | |
| // Validate we got ABI+args format | |
| if ( | |
| !('abi' in transactionData) || | |
| !('args' in transactionData) || | |
| !transactionData.abi || | |
| !Array.isArray(transactionData.args) | |
| ) { | |
| throw new Error('Expected ABI+args format from CrossCurve API'); | |
| } | |
| const txData = transactionData as StartPopulatedTx; | |
| console.log(' π Transaction Details:'); | |
| console.log(` Target: ${txData.to}`); | |
| console.log( | |
| ` Value: ${txData.value} wei (${ethers.utils.formatEther(txData.value)} ETH)`, | |
| ); | |
| console.log(` ABI: ${txData.abi}`); | |
| console.log(` Args Count: ${txData.args.length}`); | |
| console.log(' Args:'); | |
| txData.args.forEach((arg, index) => { | |
| if (typeof arg === 'object' && arg !== null) { | |
| console.log(` [${index}]: ${JSON.stringify(arg, null, 2)}`); | |
| } else { | |
| console.log(` [${index}]: ${arg}`); | |
| } | |
| }); | |
| console.log(); | |
| // Step 4: Extract execution price | |
| console.log('[Step 4] Extracting execution price...'); | |
| let executionPrice = BigNumber.from(0); | |
| if (bestRoute.deliveryFee?.amount) { | |
| executionPrice = BigNumber.from(bestRoute.deliveryFee.amount); | |
| } | |
| // Also try to extract from args[2] (receipt/invoice) | |
| const invoiceReceipt = txData.args[2] as | |
| | SignaturePack | |
| | { invoice?: SignaturePack }; | |
| if (invoiceReceipt) { | |
| const invoice = | |
| 'invoice' in invoiceReceipt | |
| ? invoiceReceipt.invoice | |
| : (invoiceReceipt as SignaturePack); | |
| if (invoice?.executionPrice) { | |
| executionPrice = BigNumber.from(invoice.executionPrice); | |
| } | |
| } | |
| console.log(` Execution price: ${executionPrice.toString()} wei`); | |
| console.log(); | |
| // Step 5: Build calldata | |
| console.log('[Step 5] Building kernel calldata...'); | |
| const operations: KernelCallDataParams[] = []; | |
| // For native token, we need to send value with the transaction | |
| const amountIn = BigNumber.from(swapConfig.amountIn); | |
| const correctValue = amountIn.add(executionPrice); | |
| // Encode function call from ABI+args | |
| const iface = new ethers.utils.Interface([txData.abi]); | |
| const functionName = txData.abi.match(/function (\w+)/)?.[1]; | |
| if (!functionName) { | |
| throw new Error('Failed to extract function name from ABI'); | |
| } | |
| // Process args - convert SignaturePack to tuple array format | |
| // Match the exact logic from crosscurve-calldata.service.ts | |
| const processedArgs = txData.args.map((arg: unknown, index: number) => { | |
| if (index === 2 && arg && typeof arg === 'object') { | |
| const receipt = arg as | |
| | SignaturePack | |
| | { | |
| invoice?: SignaturePack; | |
| feeShare?: string; | |
| feeShareRecipient?: string; | |
| feeToken?: string; | |
| }; | |
| if ('invoice' in receipt && receipt.invoice) { | |
| // New format: { invoice: SignaturePack, feeShare, feeShareRecipient, feeToken } | |
| // Return as array: [signature tuple array, feeShare, feeShareRecipient, feeToken] | |
| // The signature tuple should be [executionPrice, deadline, v, r, s] | |
| const invoice = receipt.invoice; | |
| const feeShare = receipt.feeShare || '0'; | |
| const feeShareRecipient = | |
| receipt.feeShareRecipient || constants.AddressZero; | |
| const feeToken = receipt.feeToken || constants.AddressZero; | |
| console.log(' π§ Processing args[2] (invoice format):'); | |
| console.log( | |
| ` Execution Price: ${invoice.executionPrice} wei (${ethers.utils.formatEther(invoice.executionPrice)} ETH)`, | |
| ); | |
| console.log( | |
| ` Deadline: ${invoice.deadline} (${new Date(Number(invoice.deadline) * 1000).toISOString()})`, | |
| ); | |
| console.log(` Signature v: ${invoice.v}`); | |
| console.log(` Signature r: ${invoice.r.substring(0, 20)}...`); | |
| console.log(` Signature s: ${invoice.s.substring(0, 20)}...`); | |
| console.log( | |
| ` Fee Share: ${feeShare} wei (${ethers.utils.formatEther(feeShare)} ETH)`, | |
| ); | |
| console.log(` Fee Share Recipient: ${feeShareRecipient}`); | |
| console.log(` Fee Token: ${feeToken}`); | |
| console.log(); | |
| return [ | |
| [ | |
| invoice.executionPrice, | |
| invoice.deadline, | |
| invoice.v, | |
| invoice.r, | |
| invoice.s, | |
| ], | |
| feeShare, | |
| feeShareRecipient, | |
| feeToken, | |
| ]; | |
| } else if ('executionPrice' in receipt) { | |
| // Old format: SignaturePack directly - return as tuple array | |
| const sig = receipt as SignaturePack; | |
| console.log(' π§ Processing args[2] (old format):'); | |
| console.log( | |
| ` Execution Price: ${sig.executionPrice} wei (${ethers.utils.formatEther(sig.executionPrice)} ETH)`, | |
| ); | |
| console.log( | |
| ` Deadline: ${sig.deadline} (${new Date(Number(sig.deadline) * 1000).toISOString()})`, | |
| ); | |
| console.log(` Signature v: ${sig.v}`); | |
| console.log(` Signature r: ${sig.r.substring(0, 20)}...`); | |
| console.log(` Signature s: ${sig.s.substring(0, 20)}...`); | |
| console.log(); | |
| return [sig.executionPrice, sig.deadline, sig.v, sig.r, sig.s]; | |
| } | |
| } | |
| return arg; | |
| }); | |
| const bridgeCallData = iface.encodeFunctionData(functionName, processedArgs); | |
| // Add bridge operation | |
| operations.push({ | |
| toAddress: txData.to, | |
| amount: ethers.utils.formatUnits(correctValue, 0), | |
| data: bridgeCallData, | |
| }); | |
| // Build kernel calldata | |
| const kernelCallData = calldataBuilder.buildKernelCallData(operations); | |
| console.log(' π¦ Calldata Summary:'); | |
| console.log(` Function: ${functionName}`); | |
| console.log(` Operations count: ${operations.length}`); | |
| operations.forEach((op, index) => { | |
| console.log(` Operation [${index}]:`); | |
| console.log(` To: ${op.toAddress}`); | |
| console.log( | |
| ` Amount: ${op.amount} wei (${ethers.utils.formatEther(op.amount)} ETH)`, | |
| ); | |
| console.log(` Data length: ${op.data.length} bytes`); | |
| }); | |
| console.log(` Kernel calldata length: ${kernelCallData.length} bytes`); | |
| console.log( | |
| ` Amount In: ${amountIn.toString()} wei (${ethers.utils.formatEther(amountIn.toString())} ETH)`, | |
| ); | |
| console.log( | |
| ` Execution Price: ${executionPrice.toString()} wei (${ethers.utils.formatEther(executionPrice.toString())} ETH)`, | |
| ); | |
| console.log( | |
| ` Total Value: ${correctValue.toString()} wei (${ethers.utils.formatEther(correctValue.toString())} ETH)`, | |
| ); | |
| console.log(); | |
| // Step 6: Create user operation | |
| console.log('[Step 6] Creating user operation...'); | |
| const rpcProvider = new ethers.providers.JsonRpcProvider( | |
| CONFIG.chains.base.rpc, | |
| ); | |
| // Get current gas prices before creating user operation | |
| const feeData = await rpcProvider.getFeeData(); | |
| const maxFeePerGas = feeData.maxFeePerGas || BigNumber.from('0x3b9aca00'); // Fallback to 1 gwei | |
| const maxPriorityFeePerGas = | |
| feeData.maxPriorityFeePerGas || BigNumber.from('0x3b9aca00'); // Fallback to 1 gwei | |
| console.log(' β½ Gas Price Details (from feeData):'); | |
| console.log( | |
| ` maxFeePerGas: ${maxFeePerGas.toString()} wei (${ethers.utils.formatUnits(maxFeePerGas, 'gwei')} gwei)`, | |
| ); | |
| console.log( | |
| ` maxPriorityFeePerGas: ${maxPriorityFeePerGas.toString()} wei (${ethers.utils.formatUnits(maxPriorityFeePerGas, 'gwei')} gwei)`, | |
| ); | |
| if (feeData.gasPrice) { | |
| console.log( | |
| ` gasPrice: ${feeData.gasPrice.toString()} wei (${ethers.utils.formatUnits(feeData.gasPrice, 'gwei')} gwei)`, | |
| ); | |
| } | |
| console.log(); | |
| // Estimate callGasLimit dynamically | |
| console.log(' β½ Estimating callGasLimit...'); | |
| let estimatedCallGasLimit: BigNumber; | |
| try { | |
| // Estimate gas for the execution call | |
| const gasEstimate = await rpcProvider.estimateGas({ | |
| from: CONFIG.aa.entryPointAddress, | |
| to: CONFIG.aa.aaAddress, | |
| data: kernelCallData, | |
| value: correctValue, | |
| }); | |
| // Add 50% buffer for safety (bridge operations can be complex) | |
| estimatedCallGasLimit = gasEstimate.mul(150).div(100); | |
| console.log( | |
| ` Estimated gas: ${gasEstimate.toString()} wei (${gasEstimate.toString()})`, | |
| ); | |
| console.log( | |
| ` With 50% buffer: ${estimatedCallGasLimit.toString()} wei (${estimatedCallGasLimit.toString()})`, | |
| ); | |
| } catch (error) { | |
| console.log( | |
| ` β οΈ Gas estimation failed, using default: ${CONFIG.gas.callGasLimit}`, | |
| ); | |
| console.log( | |
| ` Error: ${error instanceof Error ? error.message : String(error)}`, | |
| ); | |
| estimatedCallGasLimit = BigNumber.from(CONFIG.gas.callGasLimit); | |
| } | |
| console.log(); | |
| const userOp = await userOpBuilder.createBaseUserOp( | |
| CONFIG.aa.aaAddress, | |
| kernelCallData, | |
| rpcProvider, | |
| CONFIG.aa.nonceKey, | |
| maxFeePerGas.toHexString(), | |
| maxPriorityFeePerGas.toHexString(), | |
| '0x', // initCode (empty for existing account) | |
| ); | |
| // Override callGasLimit with estimated value | |
| userOp.callGasLimit = estimatedCallGasLimit.toHexString(); | |
| console.log(` Sender: ${userOp.sender}`); | |
| console.log(` Nonce: ${userOp.nonce}`); | |
| console.log(` Call data length: ${userOp.callData.length} bytes`); | |
| console.log( | |
| ` Call gas limit: ${userOp.callGasLimit} (${BigNumber.from(userOp.callGasLimit).toString()})`, | |
| ); | |
| console.log( | |
| ` Verification gas limit: ${userOp.verificationGasLimit} (${BigNumber.from(userOp.verificationGasLimit).toString()})`, | |
| ); | |
| console.log( | |
| ` Pre-verification gas: ${userOp.preVerificationGas} (${BigNumber.from(userOp.preVerificationGas).toString()})`, | |
| ); | |
| console.log(); | |
| // Step 7: Pack user operation | |
| console.log('[Step 7] Packing user operation...'); | |
| const packedUserOp = userOpBuilder.packUserOp(userOp, '0x'); // initCode (empty for existing account) | |
| console.log(` Packed user operation created`); | |
| console.log(); | |
| // Step 8: Calculate user operation hash | |
| console.log('[Step 8] Calculating user operation hash...'); | |
| const userOpHash = userOpBuilder.calculateUserOpHash( | |
| packedUserOp, | |
| CONFIG.chains.base.chainId, | |
| ); | |
| console.log(` UserOp hash: ${userOpHash}`); | |
| console.log(); | |
| // Step 9: Sign user operation | |
| console.log('[Step 9] Signing user operation...'); | |
| const ownerWallet = new ethers.Wallet(CONFIG.aa.ownerPrivateKey); | |
| const signature = await ownerWallet.signMessage( | |
| ethers.utils.arrayify(userOpHash), | |
| ); | |
| userOp.signature = signature; | |
| packedUserOp.signature = signature; | |
| console.log(` Signature: ${signature.substring(0, 20)}...`); | |
| console.log(); | |
| console.log(' π¦ Final User Operation:'); | |
| const userOpFormatted = { | |
| ...userOp, | |
| callGasLimit: `${userOp.callGasLimit} (${BigNumber.from(userOp.callGasLimit).toString()} wei)`, | |
| verificationGasLimit: `${userOp.verificationGasLimit} (${BigNumber.from(userOp.verificationGasLimit).toString()} wei)`, | |
| preVerificationGas: `${userOp.preVerificationGas} (${BigNumber.from(userOp.preVerificationGas).toString()} wei)`, | |
| maxFeePerGas: `${userOp.maxFeePerGas} (${ethers.utils.formatUnits(userOp.maxFeePerGas, 'gwei')} gwei)`, | |
| maxPriorityFeePerGas: `${userOp.maxPriorityFeePerGas} (${ethers.utils.formatUnits(userOp.maxPriorityFeePerGas, 'gwei')} gwei)`, | |
| }; | |
| console.log(JSON.stringify(userOpFormatted, null, 2)); | |
| console.log(' π¦ Final Packed User Operation:'); | |
| console.log(JSON.stringify(packedUserOp, null, 2)); | |
| console.log(); | |
| // Step 10: Send user operation via handleOps | |
| console.log('[Step 10] Sending user operation via handleOps...'); | |
| const entryPointOwnerWallet = new ethers.Wallet( | |
| CONFIG.entryPoint.ownerPrivateKey, | |
| rpcProvider, | |
| ); | |
| const beneficiary = entryPointOwnerWallet.address; | |
| console.log(` Beneficiary: ${beneficiary} (will receive gas refund)`); | |
| // Build handleOps calldata | |
| const handleOpsCalldata = handleOpsBuilder.buildHandleOpsCalldata( | |
| [packedUserOp], | |
| beneficiary, | |
| ); | |
| console.log(` HandleOps calldata length: ${handleOpsCalldata.length} bytes`); | |
| console.log(); | |
| console.log(' π€ HandleOps Transaction Details:'); | |
| console.log(` From: ${beneficiary}`); | |
| console.log(` To: ${CONFIG.aa.entryPointAddress}`); | |
| console.log(` Value: 0`); | |
| console.log(` Calldata: ${handleOpsCalldata}`); | |
| console.log(); | |
| // Estimate gas | |
| const gasEstimate = await rpcProvider.estimateGas({ | |
| to: CONFIG.aa.entryPointAddress, | |
| from: beneficiary, | |
| data: handleOpsCalldata, | |
| }); | |
| console.log(' β½ Gas Estimate:'); | |
| console.log(` Estimated gas: ${gasEstimate.toString()} wei`); | |
| const estimatedGasCost = gasEstimate.mul(maxFeePerGas); | |
| console.log( | |
| ` Estimated cost: ${estimatedGasCost.toString()} wei (${ethers.utils.formatEther(estimatedGasCost.toString())} ETH)`, | |
| ); | |
| console.log(); | |
| // Send transaction | |
| const tx = await entryPointOwnerWallet.sendTransaction({ | |
| to: CONFIG.aa.entryPointAddress, | |
| data: handleOpsCalldata, | |
| gasLimit: gasEstimate.mul(120).div(100), // Add 20% buffer | |
| maxFeePerGas, | |
| maxPriorityFeePerGas, | |
| }); | |
| console.log(` β Transaction sent!`); | |
| console.log(` Transaction hash: ${tx.hash}`); | |
| console.log(` Waiting for confirmation...`); | |
| const txReceipt = await tx.wait(); | |
| console.log(` β Transaction confirmed in block ${txReceipt.blockNumber}`); | |
| console.log(` Gas used: ${txReceipt.gasUsed.toString()}`); | |
| console.log(); | |
| console.log('='.repeat(80)); | |
| console.log('Flow completed!'); | |
| console.log('='.repeat(80)); | |
| } | |
| // Run the script | |
| main().catch((error) => { | |
| console.error('Error:', error); | |
| process.exit(1); | |
| }); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment