|
| 1 | +/** |
| 2 | + * 03-crosschain-native-transfer-meeclient.ts |
| 3 | + * |
| 4 | + * Cross-chain native ETH transfer using MEE Client. |
| 5 | + * Sends ETH on Base AND Optimism in a single Supertransaction. |
| 6 | + * |
| 7 | + * NEW FEATURE: |
| 8 | + * - Demonstrates MEE Client's cross-chain capabilities |
| 9 | + * - Multiple instructions for different chains |
| 10 | + * - Single quote/signature for entire cross-chain bundle |
| 11 | + * - Automatic gas sponsorship across all chains |
| 12 | + */ |
| 13 | + |
| 14 | +import { ethers } from "ethers"; |
| 15 | +import * as fs from "fs"; |
| 16 | +import * as path from "path"; |
| 17 | +import { createPublicClient, http, parseEther, type Hex } from "viem"; |
| 18 | +import { privateKeyToAccount } from "viem/accounts"; |
| 19 | +import { base, optimism } from "viem/chains"; |
| 20 | +import { createMeeClient, toMultichainNexusAccount, getMEEVersion, MEEVersion } from "@biconomy/abstractjs"; |
| 21 | + |
| 22 | +async function crosschainNativeTransferMeeClient() { |
| 23 | + console.log("🧪 Cross-Chain Native ETH Transfer - MEE Client\n"); |
| 24 | + console.log("=".repeat(80)); |
| 25 | + console.log("📋 Chains: Base Mainnet (8453) + Optimism Mainnet (10)"); |
| 26 | + console.log("📋 API: createMeeClient() + toMultichainNexusAccount()"); |
| 27 | + console.log("📋 Sponsorship: Biconomy hosted (all chains)\n"); |
| 28 | + console.log("=".repeat(80)); |
| 29 | + |
| 30 | + // ============================================================================ |
| 31 | + // WALLET INFO |
| 32 | + // ============================================================================ |
| 33 | + |
| 34 | + const walletAddress = "0x846A51Ac27990D255Eaa0a732A9411F21cAF91b6" as `0x${string}`; |
| 35 | + const ownerAddress = "0xeDC117090236293afEBb179260e8B9dd5bffe4dC" as `0x${string}`; |
| 36 | + |
| 37 | + console.log("\n📋 Migrated Wallet:"); |
| 38 | + console.log("-".repeat(80)); |
| 39 | + console.log(` Address: ${walletAddress}`); |
| 40 | + console.log(` Owner: ${ownerAddress}`); |
| 41 | + console.log(` Type: Passport → Nexus (migrated)\n`); |
| 42 | + console.log("=".repeat(80)); |
| 43 | + |
| 44 | + // ============================================================================ |
| 45 | + // SETUP SIGNER |
| 46 | + // ============================================================================ |
| 47 | + |
| 48 | + console.log("\n⚙️ Setting up signer..."); |
| 49 | + |
| 50 | + const privateKeyRaw = process.env.MIGRATION_TEST_OWNER_PK; |
| 51 | + |
| 52 | + if (!privateKeyRaw) { |
| 53 | + throw new Error("MIGRATION_TEST_OWNER_PK not found in .env"); |
| 54 | + } |
| 55 | + |
| 56 | + const privateKey = privateKeyRaw.startsWith("0x") ? privateKeyRaw : `0x${privateKeyRaw}`; |
| 57 | + const viemAccount = privateKeyToAccount(privateKey as Hex); |
| 58 | + |
| 59 | + console.log(` Owner EOA: ${viemAccount.address}`); |
| 60 | + |
| 61 | + if (viemAccount.address.toLowerCase() !== ownerAddress.toLowerCase()) { |
| 62 | + throw new Error( |
| 63 | + `Signer mismatch! Expected ${ownerAddress}, got ${viemAccount.address}` |
| 64 | + ); |
| 65 | + } |
| 66 | + |
| 67 | + console.log(" ✅ Signer configured\n"); |
| 68 | + console.log("=".repeat(80)); |
| 69 | + |
| 70 | + // ============================================================================ |
| 71 | + // SETUP PUBLIC CLIENTS FOR BOTH CHAINS |
| 72 | + // ============================================================================ |
| 73 | + |
| 74 | + console.log("\n🔗 Setting up public clients..."); |
| 75 | + |
| 76 | + const baseRpcUrl = process.env.BASE_MAINNET_ENDPOINT || "https://mainnet.base.org"; |
| 77 | + const optimismRpcUrl = process.env.OPTIMISM_MAINNET_ENDPOINT || "https://mainnet.optimism.io"; |
| 78 | + |
| 79 | + const basePublicClient = createPublicClient({ |
| 80 | + chain: base, |
| 81 | + transport: http(baseRpcUrl), |
| 82 | + }); |
| 83 | + |
| 84 | + const optimismPublicClient = createPublicClient({ |
| 85 | + chain: optimism, |
| 86 | + transport: http(optimismRpcUrl), |
| 87 | + }); |
| 88 | + |
| 89 | + console.log(" ✅ Base client configured"); |
| 90 | + console.log(" ✅ Optimism client configured\n"); |
| 91 | + console.log("=".repeat(80)); |
| 92 | + |
| 93 | + // ============================================================================ |
| 94 | + // CHECK WALLET BALANCES ON BOTH CHAINS |
| 95 | + // ============================================================================ |
| 96 | + |
| 97 | + console.log("\n💰 Checking Wallet Balances..."); |
| 98 | + |
| 99 | + const baseBalance = await basePublicClient.getBalance({ |
| 100 | + address: walletAddress, |
| 101 | + }); |
| 102 | + |
| 103 | + const optimismBalance = await optimismPublicClient.getBalance({ |
| 104 | + address: walletAddress, |
| 105 | + }); |
| 106 | + |
| 107 | + console.log(` Base: ${ethers.utils.formatEther(baseBalance.toString())} ETH`); |
| 108 | + console.log(` Optimism: ${ethers.utils.formatEther(optimismBalance.toString())} ETH\n`); |
| 109 | + |
| 110 | + if (baseBalance < parseEther("0.00002")) { |
| 111 | + throw new Error("Insufficient balance on Base"); |
| 112 | + } |
| 113 | + |
| 114 | + if (optimismBalance < parseEther("0.00002")) { |
| 115 | + throw new Error("Insufficient balance on Optimism"); |
| 116 | + } |
| 117 | + |
| 118 | + console.log("=".repeat(80)); |
| 119 | + |
| 120 | + // ============================================================================ |
| 121 | + // CREATE MEE CLIENT WITH MULTICHAIN SUPPORT |
| 122 | + // ============================================================================ |
| 123 | + |
| 124 | + console.log("\n🚀 Creating Multichain MEE Client..."); |
| 125 | + |
| 126 | + const apiKey = process.env.SUPERTX_API_KEY; |
| 127 | + if (!apiKey) { |
| 128 | + throw new Error("SUPERTX_API_KEY not found in .env"); |
| 129 | + } |
| 130 | + |
| 131 | + console.log(` API Key: ${apiKey.substring(0, 10)}...${apiKey.substring(apiKey.length - 4)}`); |
| 132 | + console.log(` Chains: Base (${base.id}) + Optimism (${optimism.id})`); |
| 133 | + console.log(` Account: ${walletAddress}\n`); |
| 134 | + |
| 135 | + try { |
| 136 | + // Create Multichain Nexus account with both chains |
| 137 | + const nexusAccount = await toMultichainNexusAccount({ |
| 138 | + signer: viemAccount, |
| 139 | + chainConfigurations: [ |
| 140 | + { |
| 141 | + chain: base, |
| 142 | + transport: http(baseRpcUrl), |
| 143 | + version: getMEEVersion(MEEVersion.V2_1_0), |
| 144 | + }, |
| 145 | + { |
| 146 | + chain: optimism, |
| 147 | + transport: http(optimismRpcUrl), |
| 148 | + version: getMEEVersion(MEEVersion.V2_1_0), |
| 149 | + }, |
| 150 | + ], |
| 151 | + accountAddress: walletAddress, |
| 152 | + }); |
| 153 | + |
| 154 | + console.log(" ✅ Multichain Nexus Account created (Base + Optimism)"); |
| 155 | + |
| 156 | + // Create MEE Client |
| 157 | + const meeClient = await createMeeClient({ |
| 158 | + account: nexusAccount, |
| 159 | + apiKey: apiKey, |
| 160 | + }); |
| 161 | + |
| 162 | + console.log(" ✅ MEE Client created\n"); |
| 163 | + console.log("=".repeat(80)); |
| 164 | + |
| 165 | + // ======================================================================== |
| 166 | + // SEND CROSS-CHAIN NATIVE ETH TRANSFER |
| 167 | + // ======================================================================== |
| 168 | + |
| 169 | + console.log("\n🚀 Sending Cross-Chain Native ETH Transfer..."); |
| 170 | + |
| 171 | + const testAmount = parseEther("0.00001"); |
| 172 | + const recipientAddress = "0xF04fF8e30816858dc4ec5436d3e148D1B9D84b6B" as `0x${string}`; // Native Nexus wallet |
| 173 | + |
| 174 | + console.log(` From: ${walletAddress}`); |
| 175 | + console.log(` To: ${recipientAddress}`); |
| 176 | + console.log(` Amount: ${ethers.utils.formatEther(testAmount.toString())} ETH (per chain)`); |
| 177 | + console.log(` Chains: Base + Optimism`); |
| 178 | + console.log(` Sponsored: YES (Biconomy)\n`); |
| 179 | + |
| 180 | + console.log(" 📋 Building cross-chain quote with gas sponsorship..."); |
| 181 | + |
| 182 | + const quote = await meeClient.getQuote({ |
| 183 | + sponsorship: true, |
| 184 | + instructions: [ |
| 185 | + // Instruction 1: Base Mainnet |
| 186 | + { |
| 187 | + calls: [ |
| 188 | + { |
| 189 | + to: recipientAddress, |
| 190 | + value: testAmount, |
| 191 | + }, |
| 192 | + ], |
| 193 | + chainId: base.id, |
| 194 | + }, |
| 195 | + // Instruction 2: Optimism Mainnet |
| 196 | + { |
| 197 | + calls: [ |
| 198 | + { |
| 199 | + to: recipientAddress, |
| 200 | + value: testAmount, |
| 201 | + }, |
| 202 | + ], |
| 203 | + chainId: optimism.id, |
| 204 | + }, |
| 205 | + ], |
| 206 | + }); |
| 207 | + |
| 208 | + console.log(" ✅ Cross-chain quote received!\n"); |
| 209 | + |
| 210 | + console.log(" 🖊️ Signing quote..."); |
| 211 | + const signedQuote = await meeClient.signQuote({ quote }); |
| 212 | + console.log(" ✅ Quote signed!\n"); |
| 213 | + |
| 214 | + console.log(" 📤 Executing signed quote (cross-chain)..."); |
| 215 | + const result = await meeClient.executeSignedQuote({ signedQuote }); |
| 216 | + const hash = result.hash; |
| 217 | + |
| 218 | + console.log(` ✅ Cross-chain transaction submitted!`); |
| 219 | + console.log(` Supertransaction Hash: ${hash}\n`); |
| 220 | + |
| 221 | + console.log(" ⏳ Waiting for confirmation on BOTH chains...\n"); |
| 222 | + |
| 223 | + const receipt = await meeClient.waitForSupertransactionReceipt({ hash: hash as `0x${string}` }); |
| 224 | + |
| 225 | + const isSuccess = receipt.transactionStatus === "MINED_SUCCESS" || |
| 226 | + (receipt.userOps && receipt.userOps.every((op: any) => |
| 227 | + op.executionStatus === "MINED_SUCCESS" |
| 228 | + )); |
| 229 | + |
| 230 | + console.log(` Status: ${isSuccess ? "✅ SUCCESS" : "❌ FAILED"}`); |
| 231 | + console.log(` Transaction Status: ${receipt.transactionStatus || "N/A"}\n`); |
| 232 | + |
| 233 | + // Show UserOp details (one per chain) |
| 234 | + if (receipt.userOps && receipt.userOps.length > 0) { |
| 235 | + console.log(` 📊 UserOps executed: ${receipt.userOps.length} (across ${receipt.userOps.length} chains)`); |
| 236 | + receipt.userOps.forEach((op: any, idx: number) => { |
| 237 | + const chainName = idx === 0 ? "Base" : "Optimism"; |
| 238 | + console.log(` UserOp #${idx + 1} (${chainName}):`); |
| 239 | + console.log(` Execution Status: ${op.executionStatus || "N/A"}`); |
| 240 | + console.log(` TX Hash: ${op.executionData || "N/A"}`); |
| 241 | + }); |
| 242 | + console.log(); |
| 243 | + } |
| 244 | + |
| 245 | + // Show explorer links (one per chain) |
| 246 | + if (receipt.explorerLinks && receipt.explorerLinks.length > 0) { |
| 247 | + console.log(` 🔗 Explorer Links (${receipt.explorerLinks.length} chains):`); |
| 248 | + receipt.explorerLinks.forEach((link: string, idx: number) => { |
| 249 | + const chainName = link.includes("basescan") ? "Base" : "Optimism"; |
| 250 | + console.log(` [${chainName}] ${link}`); |
| 251 | + }); |
| 252 | + console.log(); |
| 253 | + } |
| 254 | + |
| 255 | + console.log("=".repeat(80)); |
| 256 | + |
| 257 | + // ======================================================================== |
| 258 | + // VERIFY BALANCES ON BOTH CHAINS |
| 259 | + // ======================================================================== |
| 260 | + |
| 261 | + console.log("\n🔍 Verifying Balances..."); |
| 262 | + |
| 263 | + const newBaseBalance = await basePublicClient.getBalance({ |
| 264 | + address: walletAddress, |
| 265 | + }); |
| 266 | + |
| 267 | + const newOptimismBalance = await optimismPublicClient.getBalance({ |
| 268 | + address: walletAddress, |
| 269 | + }); |
| 270 | + |
| 271 | + console.log(` Base Before: ${ethers.utils.formatEther(baseBalance.toString())} ETH`); |
| 272 | + console.log(` Base After: ${ethers.utils.formatEther(newBaseBalance.toString())} ETH`); |
| 273 | + console.log(` Optimism Before: ${ethers.utils.formatEther(optimismBalance.toString())} ETH`); |
| 274 | + console.log(` Optimism After: ${ethers.utils.formatEther(newOptimismBalance.toString())} ETH\n`); |
| 275 | + |
| 276 | + console.log("=".repeat(80)); |
| 277 | + |
| 278 | + // ======================================================================== |
| 279 | + // SAVE RESULT |
| 280 | + // ======================================================================== |
| 281 | + |
| 282 | + const resultData = { |
| 283 | + timestamp: new Date().toISOString(), |
| 284 | + type: "cross-chain-native-transfer", |
| 285 | + wallet: walletAddress, |
| 286 | + owner: ownerAddress, |
| 287 | + supertxHash: hash, |
| 288 | + success: isSuccess, |
| 289 | + transactionStatus: receipt.transactionStatus, |
| 290 | + userOpsCount: receipt.userOps?.length || 0, |
| 291 | + chains: [ |
| 292 | + { |
| 293 | + name: "base", |
| 294 | + chainId: base.id, |
| 295 | + blockchainTxHash: receipt.receipts?.[0]?.transactionHash || null, |
| 296 | + balanceBefore: baseBalance.toString(), |
| 297 | + balanceAfter: newBaseBalance.toString(), |
| 298 | + }, |
| 299 | + { |
| 300 | + name: "optimism", |
| 301 | + chainId: optimism.id, |
| 302 | + blockchainTxHash: receipt.receipts?.[1]?.transactionHash || null, |
| 303 | + balanceBefore: optimismBalance.toString(), |
| 304 | + balanceAfter: newOptimismBalance.toString(), |
| 305 | + }, |
| 306 | + ], |
| 307 | + explorerLinks: receipt.explorerLinks || [], |
| 308 | + sponsored: true, |
| 309 | + testAmount: testAmount.toString(), |
| 310 | + recipient: recipientAddress, |
| 311 | + }; |
| 312 | + |
| 313 | + const resultPath = path.join(__dirname, "03-result.json"); |
| 314 | + fs.writeFileSync(resultPath, JSON.stringify(resultData, null, 2)); |
| 315 | + |
| 316 | + console.log(`\n📄 Result saved to: ${resultPath}\n`); |
| 317 | + |
| 318 | + console.log("=".repeat(80)); |
| 319 | + if (isSuccess) { |
| 320 | + console.log("\n🎉 CROSS-CHAIN NATIVE TRANSFER: SUCCESS!\n"); |
| 321 | + console.log("✅ MEE Client cross-chain works perfectly!"); |
| 322 | + console.log("✅ Gas sponsorship on BOTH chains"); |
| 323 | + console.log("✅ UserOps confirmed on Base AND Optimism"); |
| 324 | + console.log(`✅ Supertransaction: ${hash}\n`); |
| 325 | + } else { |
| 326 | + console.log("\n⚠️ CROSS-CHAIN NATIVE TRANSFER: NEEDS INVESTIGATION\n"); |
| 327 | + } |
| 328 | + console.log("=".repeat(80)); |
| 329 | + |
| 330 | + } catch (error: any) { |
| 331 | + console.log("\n❌ Test Failed!"); |
| 332 | + console.log(` Error: ${error.message}\n`); |
| 333 | + console.log("=".repeat(80)); |
| 334 | + throw error; |
| 335 | + } |
| 336 | +} |
| 337 | + |
| 338 | +crosschainNativeTransferMeeClient() |
| 339 | + .then(() => process.exit(0)) |
| 340 | + .catch((error) => { |
| 341 | + console.error("\n❌ Test Failed!"); |
| 342 | + console.error(` Error: ${error.message}\n`); |
| 343 | + process.exit(1); |
| 344 | + }); |
| 345 | + |
0 commit comments