Skip to content

Commit 1d82d4e

Browse files
authored
Merge pull request #3 from wormhole-foundation/feat/solana-cross-vm
feat: add evm <> solana cross-VM messaging support
2 parents 14981a6 + 44c3df2 commit 1d82d4e

13 files changed

Lines changed: 706 additions & 50 deletions

.github/workflows/test.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,18 @@ jobs:
2121
persist-credentials: false
2222
submodules: recursive
2323

24+
- name: Setup Node.js
25+
uses: actions/setup-node@v4
26+
with:
27+
node-version: 22
28+
cache: npm
29+
30+
- name: Install npm dependencies
31+
run: npm ci
32+
33+
- name: TypeScript check
34+
run: npx tsc --noEmit
35+
2436
- name: Install Foundry
2537
uses: foundry-rs/foundry-toolchain@v1
2638

README.md

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
Cross-chain messaging with Wormhole Executor, demonstrating both **off-chain** and **on-chain** quote methods.
44

5+
This repo covers two use cases:
6+
- **EVM ↔ EVM** — both off-chain and on-chain quotes, Sepolia ↔ Base Sepolia
7+
- **EVM ↔ Solana** — off-chain quotes only (see [Cross-VM section](#cross-vm-evm--solana))
8+
59
> **License:** Code provided "AS IS", without warranties. Audit before mainnet deployment.
610
711
## Contracts
@@ -26,6 +30,9 @@ function quoteGreeting(uint16 targetChain, uint128 gasLimit, address quoterAddre
2630
function sendGreeting(..., address quoterAddress) external payable;
2731
```
2832

33+
> **Note:** On-chain quotes currently support EVM destination chains only.
34+
> For EVM → Solana, use `HelloWormhole` (off-chain signed quotes) instead.
35+
2936
## Deployed Contracts (Testnet)
3037

3138
| Chain | HelloWormhole (Off-chain) | HelloWormholeOnChainQuote |
@@ -139,6 +146,77 @@ npx tsx testOnChainQuote.ts # On-chain quote E2E
139146
npx tsx test.ts # Off-chain quote E2E
140147
```
141148

149+
---
150+
151+
## Cross-VM: EVM ↔ Solana
152+
153+
Uses `HelloWormhole` (off-chain quotes) — on-chain quotes do not yet support Solana destinations.
154+
155+
See the [Solana demo repo](https://github.com/wormhole-foundation/demo-hello-executor-solana) for the Solana-side implementation.
156+
157+
### Peer Registration
158+
159+
For EVM ↔ Solana, peer registration on the EVM side requires **two separate addresses** because the Executor uses `peers[chainId]` as a routing address (must be an executable program), while incoming VAAs carry the **emitter PDA** as their source:
160+
161+
```solidity
162+
// 1. Program ID → executor routing (must be an executable account on Solana)
163+
hello.setPeer(CHAIN_ID_SOLANA, solanaProgramIdBytes32);
164+
165+
// 2. Emitter PDA → VAA verification (PDA(["emitter"], programId))
166+
hello.setVaaEmitter(CHAIN_ID_SOLANA, solanaEmitterPdaBytes32);
167+
```
168+
169+
Run `script/SetupSolanaPeer.s.sol` to register both in one step. For EVM↔EVM, only `setPeer()` is needed.
170+
171+
Derive both Solana addresses from your program ID:
172+
```typescript
173+
const programId = new PublicKey("7eiTqf1b1dNwpzn27qEr4eGSWnuon2fJTbnTuWcFifZG");
174+
const [emitterPda] = PublicKey.findProgramAddressSync([Buffer.from("emitter")], programId);
175+
176+
const programIdBytes32 = '0x' + Buffer.from(programId.toBytes()).toString('hex');
177+
const emitterPdaBytes32 = '0x' + Buffer.from(emitterPda.toBytes()).toString('hex');
178+
```
179+
180+
### Sending to Solana
181+
182+
Use `sendGreetingWithMsgValue` with `msgValue` in **lamports** to cover rent and fees on Solana:
183+
184+
```solidity
185+
// msgValue covers rent + fees on Solana (~0.015 SOL = 15_000_000 lamports)
186+
sequence = sendGreetingWithMsgValue(
187+
greeting,
188+
1, // Wormhole chain ID for Solana
189+
500000, // compute units (not gas)
190+
15_000_000, // msgValue in lamports
191+
totalCost,
192+
signedQuote
193+
);
194+
```
195+
196+
> **Message size limit:** The Solana receiver enforces a **512-byte** max on greeting messages.
197+
> Messages longer than 512 bytes will revert on Sepolia with `PayloadTooLargeForSolana`.
198+
199+
### Deployment
200+
201+
```bash
202+
# Deploy HelloWormhole (cross-VM variant)
203+
forge script script/HelloWormhole.s.sol --rpc-url $SEPOLIA_RPC_URL --broadcast --verify
204+
205+
# Register Solana as a peer (both program ID and emitter PDA)
206+
forge script script/SetupSolanaPeer.s.sol --rpc-url $SEPOLIA_RPC_URL --broadcast
207+
```
208+
209+
### E2E Testing
210+
211+
```bash
212+
cd e2e
213+
cp .env.example .env # Fill in HELLO_WORMHOLE_SEPOLIA_CROSSVM and Solana peer addresses
214+
npx tsx sendToSolana.ts # EVM → Solana
215+
# For Solana → EVM, run sendToSepolia.ts from the Solana demo repo
216+
```
217+
218+
---
219+
142220
## Key Concepts
143221

144222
| Concept | Description |
@@ -152,3 +230,4 @@ npx tsx test.ts # Off-chain quote E2E
152230
- [Wormhole Docs](https://wormhole.com/docs)
153231
- [Solidity SDK](https://github.com/wormhole-foundation/wormhole-solidity-sdk)
154232
- [Executor Documentation](https://wormhole.com/docs/protocol/infrastructure/relayer/#executor)
233+
- [Solana demo repo](https://github.com/wormhole-foundation/demo-hello-executor-solana)

e2e/.env.example

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ PRIVATE_KEY_BASE_SEPOLIA=0x01234...
77
# BASE_SEPOLIA_RPC_URL=https://base-sepolia-rpc.publicnode.com
88

99
# Pre-deployed HelloWormhole contract addresses (off-chain quotes version)
10-
# Deploy these using forge script or manually, then add addresses here
1110
HELLO_WORMHOLE_SEPOLIA=0x8f6E15d9A4d0abCe4814c7d86D5B741A91bDCC04
1211
HELLO_WORMHOLE_BASE_SEPOLIA=0xdF781F7473a1A7C20C1e5fC5f427Fa712dafB698
1312

@@ -24,3 +23,17 @@ QUOTER_ADDRESS=0x5241c9276698439fef2780dbab76fec90b633fbd
2423
# - CoreBridge addresses (via SDK chain configs)
2524
# - Executor addresses (via SDK automatic relay)
2625
# - Default RPC URLs (can be overridden above)
26+
27+
# =============================================================================
28+
# EVM ↔ Solana (Cross-VM) — additional setup required
29+
# Wormhole chain IDs reference: https://wormhole.com/docs/products/reference/chain-ids/
30+
# =============================================================================
31+
32+
# Deploy with: forge script script/HelloWormhole.s.sol --rpc-url $SEPOLIA_RPC_URL --broadcast
33+
# For mainnet: use a mainnet RPC and update CHAIN_ID_SEPOLIA in sendToSolana.ts
34+
HELLO_WORMHOLE_SEPOLIA_CROSSVM=0x...
35+
36+
# Solana peer addresses — see script/SetupSolanaPeer.s.sol for how to derive these
37+
# Update if you deployed your own Solana program (defaults point to the demo program)
38+
SOLANA_PROGRAM_ID_BYTES32=0x62cf7e5a219d24a831e51b2c2417fa898920b930fd1c6947f3a4fc8feec1020f
39+
SOLANA_EMITTER_PDA_BYTES32=0x58235d29729e44920df367836a92ab77fcee36b7a27b03304cd699f5eb0efae5

e2e/executor.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,63 @@ export async function getExecutorCapabilities(
4040
return (await response.json()) as Record<number, ExecutorCapabilities>;
4141
}
4242

43+
/**
44+
* Parse the signed quote bytes to extract cost parameters
45+
*
46+
* EQ01 Quote Layout (from SDK signedQuoteLayout):
47+
* - [0-4] prefix (EQ01)
48+
* - [4-24] quoterAddress (20 bytes)
49+
* - [24-56] payeeAddress (32 bytes)
50+
* - [56-58] srcChain (uint16 BE)
51+
* - [58-60] dstChain (uint16 BE)
52+
* - [60-68] expiryTime (uint64 BE)
53+
* - [68-76] baseFee (uint64 BE)
54+
* - [76-84] dstGasPrice (uint64 BE)
55+
* - [84-92] srcPrice (uint64 BE)
56+
* - [92-100] dstPrice (uint64 BE)
57+
* - [100-165] signature (65 bytes)
58+
*/
59+
export function parseSignedQuote(signedQuoteHex: string): {
60+
baseFee: bigint;
61+
dstGasPrice: bigint;
62+
srcPrice: bigint;
63+
dstPrice: bigint;
64+
} {
65+
// Remove 0x prefix if present
66+
const hex = signedQuoteHex.startsWith('0x') ? signedQuoteHex.slice(2) : signedQuoteHex;
67+
const bytes = Buffer.from(hex, 'hex');
68+
69+
// Verify prefix
70+
const prefix = bytes.slice(0, 4).toString('ascii');
71+
if (prefix !== 'EQ01') {
72+
throw new Error(`Invalid quote prefix: ${prefix}, expected EQ01`);
73+
}
74+
75+
// Parse big-endian uint64 values
76+
const baseFee = bytes.readBigUInt64BE(68);
77+
const dstGasPrice = bytes.readBigUInt64BE(76);
78+
const srcPrice = bytes.readBigUInt64BE(84);
79+
const dstPrice = bytes.readBigUInt64BE(92);
80+
81+
return { baseFee, dstGasPrice, srcPrice, dstPrice };
82+
}
83+
84+
/**
85+
* Calculate the estimated cost from quote parameters
86+
* Formula: baseFee + (gasLimit * dstGasPrice * srcPrice / dstPrice)
87+
*/
88+
export function calculateEstimatedCost(
89+
quote: { baseFee: bigint; dstGasPrice: bigint; srcPrice: bigint; dstPrice: bigint },
90+
gasLimit: bigint
91+
): bigint {
92+
if (quote.dstPrice === 0n) {
93+
throw new Error('Invalid quote: dstPrice is 0');
94+
}
95+
96+
const relayCost = (gasLimit * quote.dstGasPrice * quote.srcPrice) / quote.dstPrice;
97+
return quote.baseFee + relayCost;
98+
}
99+
43100
/**
44101
* Get a quote from the Executor API using SDK's fetchQuote function
45102
*

0 commit comments

Comments
 (0)