|
| 1 | +#!/usr/bin/env tsx |
| 2 | +/** |
| 3 | + * Send greeting from Sepolia to Solana via Executor using ON-CHAIN QUOTE |
| 4 | + * |
| 5 | + * This uses the updated HelloWormholeOnChainQuote contract which supports |
| 6 | + * Solana destinations via overloaded sendGreeting + quoteGreeting (with msgValue). |
| 7 | + * |
| 8 | + * Flow: |
| 9 | + * 1. quoteGreeting(targetChain, gasLimit, msgValue, quoter) — get on-chain quote |
| 10 | + * 2. sendGreeting(greeting, targetChain, gasLimit, msgValue, totalCost, quoter) — publish + relay |
| 11 | + * |
| 12 | + * Usage: |
| 13 | + * npx tsx e2e/sendToSolanaOnChainQuote.ts "Hello from Sepolia (on-chain quote)!" |
| 14 | + */ |
| 15 | + |
| 16 | +import { ethers } from 'ethers'; |
| 17 | +import * as dotenv from 'dotenv'; |
| 18 | +import { fileURLToPath } from 'url'; |
| 19 | +import { dirname, join } from 'path'; |
| 20 | +import { pollForExecutorStatus } from './executor.js'; |
| 21 | + |
| 22 | +const __filename = fileURLToPath(import.meta.url); |
| 23 | +const __dirname = dirname(__filename); |
| 24 | +dotenv.config({ path: join(__dirname, '.env') }); |
| 25 | + |
| 26 | +// Configuration |
| 27 | +const SEPOLIA_RPC = process.env.SEPOLIA_RPC_URL || 'https://ethereum-sepolia-rpc.publicnode.com'; |
| 28 | +const PRIVATE_KEY = process.env.PRIVATE_KEY_SEPOLIA!; |
| 29 | + |
| 30 | +// HelloWormholeOnChainQuote contract with Solana peer support. |
| 31 | +// Deploy with: forge script script/HelloWormholeOnChainQuote.s.sol --rpc-url $SEPOLIA_RPC_URL --broadcast |
| 32 | +// Then register Solana peer: forge script script/SetupSolanaPeerOnChainQuote.s.sol --rpc-url $SEPOLIA_RPC_URL --broadcast |
| 33 | +const HELLO_WORMHOLE_OC = process.env.HELLO_WORMHOLE_OC_SEPOLIA_CROSSVM!; |
| 34 | +if (!HELLO_WORMHOLE_OC) { |
| 35 | + throw new Error('HELLO_WORMHOLE_OC_SEPOLIA_CROSSVM not set. Deploy the contract and set this in e2e/.env'); |
| 36 | +} |
| 37 | + |
| 38 | +// Wormhole chain IDs — full reference: https://docs.wormhole.com/products/reference/chain-ids/ |
| 39 | +const CHAIN_ID_SOLANA = 1; |
| 40 | + |
| 41 | +// On-chain quoter address (operated by Wormhole) |
| 42 | +const QUOTER_ADDRESS = process.env.QUOTER_ADDRESS |
| 43 | + || '0x5241c9276698439fef2780dbab76fec90b633fbd'; |
| 44 | + |
| 45 | +// Solana-specific: msgValue in LAMPORTS for rent + priority fees on the Solana side. |
| 46 | +// This is forwarded by the Executor when calling receive_greeting. Adjust upward |
| 47 | +// if delivery fails with insufficient funds for account creation. |
| 48 | +const SOLANA_MSG_VALUE_LAMPORTS = 15_000_000n; // ~0.015 SOL |
| 49 | + |
| 50 | +// Gas limit for Solana execution (compute units) |
| 51 | +const GAS_LIMIT = 500_000n; |
| 52 | + |
| 53 | +const ABI = [ |
| 54 | + 'function quoteGreeting(uint16 targetChain, uint128 gasLimit, uint128 msgValue, address quoterAddress) external view returns (uint256 totalCost)', |
| 55 | + 'function sendGreeting(string calldata greeting, uint16 targetChain, uint128 gasLimit, uint128 msgValue, uint256 totalCost, address quoterAddress) external payable returns (uint64 sequence)', |
| 56 | + 'function peers(uint16 chainId) external view returns (bytes32)', |
| 57 | + 'event GreetingSent(string greeting, uint16 targetChain, uint64 sequence)', |
| 58 | +]; |
| 59 | + |
| 60 | +async function main() { |
| 61 | + const greeting = process.argv[2] || 'Hello from Sepolia (on-chain quote)!'; |
| 62 | + |
| 63 | + console.log('Sending Greeting: Sepolia -> Solana (On-Chain Quote)\n'); |
| 64 | + console.log(`Message: "${greeting}"`); |
| 65 | + |
| 66 | + const provider = new ethers.JsonRpcProvider(SEPOLIA_RPC); |
| 67 | + const wallet = new ethers.Wallet(PRIVATE_KEY, provider); |
| 68 | + console.log(`Wallet: ${wallet.address}`); |
| 69 | + |
| 70 | + const balance = await provider.getBalance(wallet.address); |
| 71 | + console.log(`Balance: ${ethers.formatEther(balance)} ETH`); |
| 72 | + |
| 73 | + const contract = new ethers.Contract(HELLO_WORMHOLE_OC, ABI, wallet); |
| 74 | + |
| 75 | + // Verify peer is set |
| 76 | + const peer = await contract.peers(CHAIN_ID_SOLANA); |
| 77 | + if (peer === ethers.ZeroHash) { |
| 78 | + throw new Error('No Solana peer registered on contract. Run SetupSolanaPeer first.'); |
| 79 | + } |
| 80 | + console.log(`\nSolana peer: ${peer}`); |
| 81 | + |
| 82 | + // Step 1: Get on-chain quote |
| 83 | + console.log('\nGetting on-chain quote...'); |
| 84 | + console.log(` Gas limit: ${GAS_LIMIT}`); |
| 85 | + console.log(` msgValue: ${SOLANA_MSG_VALUE_LAMPORTS} lamports (${Number(SOLANA_MSG_VALUE_LAMPORTS) / 1e9} SOL)`); |
| 86 | + console.log(` Quoter: ${QUOTER_ADDRESS}`); |
| 87 | + |
| 88 | + const totalCost = await contract.quoteGreeting( |
| 89 | + CHAIN_ID_SOLANA, |
| 90 | + GAS_LIMIT, |
| 91 | + SOLANA_MSG_VALUE_LAMPORTS, |
| 92 | + QUOTER_ADDRESS, |
| 93 | + ); |
| 94 | + |
| 95 | + console.log(` Total cost: ${ethers.formatEther(totalCost)} ETH (${totalCost} wei)`); |
| 96 | + |
| 97 | + if (balance < totalCost) { |
| 98 | + throw new Error(`Insufficient balance: have ${ethers.formatEther(balance)}, need ${ethers.formatEther(totalCost)}`); |
| 99 | + } |
| 100 | + |
| 101 | + // Step 2: Send greeting |
| 102 | + console.log('\nSending greeting with on-chain quote...'); |
| 103 | + const tx = await contract.sendGreeting( |
| 104 | + greeting, |
| 105 | + CHAIN_ID_SOLANA, |
| 106 | + GAS_LIMIT, |
| 107 | + SOLANA_MSG_VALUE_LAMPORTS, |
| 108 | + totalCost, |
| 109 | + QUOTER_ADDRESS, |
| 110 | + { value: totalCost }, |
| 111 | + ); |
| 112 | + |
| 113 | + console.log(`TX Hash: ${tx.hash}`); |
| 114 | + console.log(`Explorer: https://sepolia.etherscan.io/tx/${tx.hash}`); |
| 115 | + |
| 116 | + console.log('\nWaiting for confirmation...'); |
| 117 | + const receipt = await tx.wait(); |
| 118 | + console.log(`Confirmed in block ${receipt.blockNumber}`); |
| 119 | + |
| 120 | + // Parse GreetingSent event (filter by contract address, then decode) |
| 121 | + const iface = new ethers.Interface(ABI); |
| 122 | + const sentEvent = receipt.logs |
| 123 | + .filter((log: any) => log.address.toLowerCase() === HELLO_WORMHOLE_OC.toLowerCase()) |
| 124 | + .map((log: any) => iface.parseLog({ topics: log.topics as string[], data: log.data })) |
| 125 | + .find((event: any) => event?.name === 'GreetingSent'); |
| 126 | + |
| 127 | + if (sentEvent) { |
| 128 | + console.log(`\nGreetingSent event:`); |
| 129 | + console.log(` Message: ${sentEvent.args[0]}`); |
| 130 | + console.log(` Target Chain: ${sentEvent.args[1]}`); |
| 131 | + console.log(` Sequence: ${sentEvent.args[2]}`); |
| 132 | + } |
| 133 | + |
| 134 | + // Poll executor status |
| 135 | + const executorStatus = await pollForExecutorStatus( |
| 136 | + 'Sepolia', |
| 137 | + receipt.hash, |
| 138 | + 'Testnet', |
| 139 | + 120000, |
| 140 | + ); |
| 141 | + |
| 142 | + if (Array.isArray(executorStatus)) { |
| 143 | + const relay = executorStatus[0]; |
| 144 | + if (relay?.status === 'submitted' && relay.txs?.length) { |
| 145 | + console.log('\nSUCCESS! Message delivered to Solana!'); |
| 146 | + console.log(` Solana TX: ${relay.txs[0].txHash}`); |
| 147 | + console.log(` https://explorer.solana.com/tx/${relay.txs[0].txHash}?cluster=devnet`); |
| 148 | + } else if (relay?.status === 'error' || relay?.status === 'aborted') { |
| 149 | + console.log(`\nRelay failed: ${relay.failureCause || relay.error || relay.status}`); |
| 150 | + } else if (relay?.status === 'underpaid') { |
| 151 | + console.log('\nRelay underpaid. Try increasing GAS_LIMIT or SOLANA_MSG_VALUE_LAMPORTS.'); |
| 152 | + } else { |
| 153 | + console.log(`\nRelay status: ${relay?.status || 'unknown'}`); |
| 154 | + } |
| 155 | + } else { |
| 156 | + console.log('\nExecutor delivery not confirmed within timeout.'); |
| 157 | + } |
| 158 | + |
| 159 | + console.log('\n' + '-'.repeat(60)); |
| 160 | + console.log('Links:'); |
| 161 | + console.log(` Sepolia TX: https://sepolia.etherscan.io/tx/${receipt.hash}`); |
| 162 | + console.log(` Wormholescan: https://wormholescan.io/#/tx/${receipt.hash}?network=Testnet`); |
| 163 | + console.log(` Executor: https://wormholelabs-xyz.github.io/executor-explorer/#/tx/${receipt.hash}?endpoint=https%3A%2F%2Fexecutor-testnet.labsapis.com&env=Testnet`); |
| 164 | +} |
| 165 | + |
| 166 | +main().catch((error) => { |
| 167 | + console.error('\nError:', error.message || error); |
| 168 | + process.exit(1); |
| 169 | +}); |
0 commit comments