Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/tilt.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: start minikube
id: minikube
uses: medyagh/setup-minikube@cea33675329b799adccc9526aa5daccc26cd5052 # pinned to v0.0.19 commit
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,5 @@ USER bun
EXPOSE 3000/tcp
ENTRYPOINT [ "bun", "start" ]

FROM release as test
FROM release AS test

9 changes: 9 additions & 0 deletions Dockerfile.e2e
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
FROM forge

FROM executor

WORKDIR /usr/src/app

COPY --from=forge /app/out evm/out

ENTRYPOINT [ "bun", "test" ]
20 changes: 20 additions & 0 deletions Tiltfile
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,30 @@ docker_build(
]
)

docker_build(
ref = "forge",
context = "evm",
dockerfile = "./evm/Dockerfile",
only=["foundry.toml", "lib", "src"]
)
docker_build(
ref = "e2e",
context = ".",
dockerfile = "./Dockerfile.e2e",
only=[]
)

k8s_yaml("k8s/executor.yaml")
k8s_resource(
"executor",
port_forwards = 3000,
resource_deps = ["anvil-eth-sepolia", "anvil-base-sepolia"],
labels = ["app"],
)

k8s_yaml("k8s/e2e.yaml")
k8s_resource(
"e2e",
resource_deps=["anvil-eth-sepolia", "anvil-base-sepolia", "executor"],
labels=["tests"]
)
8 changes: 8 additions & 0 deletions evm/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
FROM ghcr.io/foundry-rs/foundry:v1.2.3@sha256:d9133dae61c19383b72695dc7eeca29d1e7a89f1f1b5fdfd8900c660b46b4303 AS forge

WORKDIR /app
COPY foundry.toml foundry.toml
COPY lib lib
COPY src src

RUN forge build
15 changes: 15 additions & 0 deletions k8s/e2e.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
kind: Job
apiVersion: batch/v1
metadata:
name: e2e
spec:
backoffLimit: 0
template:
metadata:
labels:
app: e2e
spec:
restartPolicy: Never
containers:
- name: e2e
image: e2e
156 changes: 129 additions & 27 deletions src/e2e.test.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,26 @@
import { mnemonicToAccount } from "viem/accounts";
import { ANVIL_MNEMONIC } from "./consts";
import { test } from "bun:test";
import { serializeLayout } from "@wormhole-foundation/sdk-base";
import { relayInstructionsLayout } from "@wormhole-foundation/sdk-definitions";
import axios from "axios";
import { sleep } from "bun";
import { expect, test } from "bun:test";
import {
createPublicClient,
createWalletClient,
getContract,
http,
isHex,
padHex,
toHex,
type Account,
type Chain,
type PublicClient,
type WalletClient,
} from "viem";
import { anvil, sepolia } from "viem/chains";
import { serializeLayout } from "@wormhole-foundation/sdk-base";
import {
quoteLayout,
relayInstructionsLayout,
signedQuoteLayout,
} from "@wormhole-foundation/sdk-definitions";
import axios from "axios";
import { deserialize } from "binary-layout";
import { mnemonicToAccount } from "viem/accounts";
import forgeOutput from "../evm/out/ExecutorVAAv1Integration.sol/ExecutorVAAv1Integration.json";
import { enabledChains } from "./chains";
import { ANVIL_MNEMONIC } from "./consts";
import { RelayStatus } from "./types";

const ABI = [
{
Expand Down Expand Up @@ -113,7 +116,90 @@ const ABI = [
},
] as const;

async function deployIntegrationContract(
publicClient: PublicClient,
client: WalletClient,
account: Account,
viemChain: Chain,
coreContractAddress: string,
executorAddress: string,
) {
if (!isHex(forgeOutput.bytecode.object)) {
throw new Error("invalid bytecode");
}
if (!isHex(coreContractAddress)) {
throw new Error("invalid coreContractAddress");
}
if (!isHex(executorAddress)) {
throw new Error("invalid executorAddress");
}
const hash = await client.deployContract({
account,
chain: viemChain,
abi: ABI,
bytecode: forgeOutput.bytecode.object,
args: [coreContractAddress, executorAddress, 200],
});
const receipt = await publicClient.waitForTransactionReceipt({
hash,
});
if (!isHex(receipt.contractAddress)) {
throw new Error("invalid contractAddress");
}
return receipt.contractAddress;
}

test("it performs a VAA v1 relay", async () => {
const srcChain = enabledChains[10002]!;
const dstChain = enabledChains[10004]!;
const account = mnemonicToAccount(ANVIL_MNEMONIC, { addressIndex: 0 });
if (!srcChain.viemChain || !dstChain.viemChain) {
throw new Error("invalid viem chain");
}
const srcTransport = http(srcChain.rpc);
const srcPublicClient = createPublicClient({
chain: srcChain.viemChain,
transport: srcTransport,
});
const srcClient = createWalletClient({
account,
chain: srcChain.viemChain,
transport: srcTransport,
});
const dstTransport = http(dstChain.rpc);
const dstPublicClient = createPublicClient({
chain: dstChain.viemChain,
transport: dstTransport,
});
const dstClient = createWalletClient({
account,
chain: dstChain.viemChain,
transport: dstTransport,
});
const srcContract = await deployIntegrationContract(
srcPublicClient,
srcClient,
account,
srcChain.viemChain,
srcChain.coreContractAddress,
srcChain.executorAddress,
);
console.log(`Deployed source contract: ${srcContract}`);
const dstContract = await deployIntegrationContract(
dstPublicClient,
dstClient,
account,
dstChain.viemChain,
dstChain.coreContractAddress,
dstChain.executorAddress,
);
console.log(`Deployed destination contract: ${dstContract}`);
const dstTestContract = getContract({
address: srcContract,
abi: ABI,
client: srcClient,
});
expect(await dstTestContract.read.number()).toBe(0n);
const relayInstructions = toHex(
serializeLayout(relayInstructionsLayout, {
requests: [
Expand All @@ -127,27 +213,20 @@ test("it performs a VAA v1 relay", async () => {
],
}),
);
const response = await axios.post("http://localhost:3000/v0/quote", {
const response = await axios.post("http://executor:3000/v0/quote", {
srcChain: 10002,
dstChain: 10004,
relayInstructions,
});
const transport = http("http://localhost:8545");
const account = mnemonicToAccount(ANVIL_MNEMONIC, { addressIndex: 0 });
const client = createWalletClient({
account,
chain: sepolia,
transport,
});
const testContract = getContract({
address: "0x8e98Bd10a6f4c1Ee0C4b5d9F50a18D1a7E20EaF8",
const srcTestContract = getContract({
address: srcContract,
abi: ABI,
client,
client: srcClient,
});
const tx = await testContract.write.incrementAndSend(
const hash = await srcTestContract.write.incrementAndSend(
[
10004,
padHex("0x7d77360666066967579a2235332d271587cd62dC", {
padHex(dstContract, {
dir: "left",
size: 32,
}),
Expand All @@ -160,6 +239,29 @@ test("it performs a VAA v1 relay", async () => {
{ value: BigInt(response.data.estimatedCost) },
);
console.log(
`https://wormholelabs-xyz.github.io/executor-explorer/#/chain/10002/tx/${tx}?endpoint=http%3A%2F%2Flocalhost%3A3000&env=Testnet`,
`Request execution: https://wormholelabs-xyz.github.io/executor-explorer/#/chain/10002/tx/${hash}?endpoint=http%3A%2F%2Flocalhost%3A3000&env=Testnet`,
);
});
await srcPublicClient.waitForTransactionReceipt({
hash,
});
let statusResult;
while (
!statusResult ||
statusResult.data?.[0].status === RelayStatus.Pending
) {
console.log(`Statusing tx: ${hash}`);
if (statusResult) {
await sleep(1000);
}
statusResult = await axios.post("http://executor:3000/v0/status/tx", {
chainId: srcChain.wormholeChainId,
txHash: hash,
});
if (statusResult.data.length !== 1) {
throw new Error(`unexpected status result length`);
}
}
expect(statusResult.data?.[0].status).toBe(RelayStatus.Submitted);
expect(await srcTestContract.read.number()).toBe(1n);
expect(await dstTestContract.read.number()).toBe(1n);
}, 60000);
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,5 @@
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false
},
"exclude": ["evm"]
"exclude": ["evm", "src/e2e.test.ts"]
}
Loading