Skip to content

feat(svm): svm spoke binaries integration tests #1048

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 56 commits into from
May 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
8b3ed44
chore: create skeleton for svm spoke client (#965)
james-a-morris Apr 4, 2025
6b1b65a
feat(svm): Initial SpokeUtils skeleton (#960)
pxrl Apr 8, 2025
1b6939c
fix: epic branch lint (#972)
melisaguevara Apr 8, 2025
dff7f94
improve(Address): Support new EVM address from Base58 encoding
pxrl Apr 8, 2025
60ecbce
Add svm/base16 counterpart
pxrl Apr 8, 2025
58f376c
lint
pxrl Apr 8, 2025
1a79e88
Merge remote-tracking branch 'origin/master'
pxrl Apr 9, 2025
b8aedad
Add test
pxrl Apr 9, 2025
eb9ae20
lint
pxrl Apr 9, 2025
fc4c167
Merge remote-tracking branch 'origin/pxrl/base58EVMAddress' into epic…
pxrl Apr 10, 2025
27ce8ec
chore: Unify svm w/ arch/svm (#977)
pxrl Apr 10, 2025
57d86cd
refactor(sdk): transition SVM events client to use blocks instead of …
james-a-morris Apr 14, 2025
06e400d
feat(svmSpokeUtils): get fill deadline buffer implementation (#978)
melisaguevara Apr 23, 2025
c9ee25e
improve: allow SVM events client to take in non-spoke IDLs (#982)
bmzig Apr 23, 2025
3e20d7e
improve(svmEventsClient): enable pda events querying (#989)
melisaguevara Apr 24, 2025
c5efe4a
feat: create svm spoke _update (#976)
james-a-morris Apr 28, 2025
4f9b54f
improve: add tests for SVM (#1007)
james-a-morris Apr 30, 2025
eec28b8
feat(SvmSpokePoolClient): relayFillStatus and fillStatusArray impleme…
melisaguevara May 7, 2025
549fee8
fix: svm spoke client missing rpc (#1023)
md0x May 7, 2025
5ac115a
feat(SvmSpokeUtils): implement findFillEvent (#998)
melisaguevara May 7, 2025
ef8b147
chore: reuse SVMProvider type (#1025)
melisaguevara May 8, 2025
f7f5163
feat(svm): integration tests with test validator (#1015)
md0x May 8, 2025
0146d3c
feat: Solana relayFeeCalculator and gasPriceOracle (#980)
bmzig May 8, 2025
5bb6a2f
improve: SVM getTimestampForSlot (#1026)
melisaguevara May 8, 2025
68280e9
chore: resync epic branch with master (#1029)
bmzig May 9, 2025
bfd4550
Revert "chore: resync epic branch with master (#1029)"
bmzig May 9, 2025
4c1d608
Merge branch 'master' into epic/svm-client
bmzig May 9, 2025
9b3bb77
feat: add request slow fill and fill functions and event tests (#1028)
md0x May 14, 2025
9d93433
feat(svm-eventsClient): add methods to find deposit and fills from si…
gsteenkamp89 May 14, 2025
5582642
feat(svm): add svm test node hook at root level (#1037)
md0x May 15, 2025
fdbf818
chore: Create release v4.1.63-beta.1 (#1040)
gsteenkamp89 May 15, 2025
da60fb8
feat: expose `getNativeGasCosts` in `svmQuery` (#1039)
dohaki May 15, 2025
feebf2a
chore: bump 4.1.63-beta.2 (#1041)
dohaki May 15, 2025
4041826
improve: generalize block finder interface for EVM (#1038)
bmzig May 15, 2025
4151ac4
feat(sdk): implement Solana-optimized deposit finder (#983)
james-a-morris May 15, 2025
de781d1
chore: bump to next minor version (#1045)
james-a-morris May 15, 2025
94d2ff6
Merge branch 'master' into epic/svm-client
james-a-morris May 15, 2025
e46123f
use named imports only for solana/kit (#1046)
gsteenkamp89 May 16, 2025
8426ced
feat: Mock SVMSpokePoolClient (#1010)
melisaguevara May 16, 2025
809cfba
chore(release): v4.1.63-beta.3 (#1047)
gsteenkamp89 May 16, 2025
11b39d2
feat: add tests for fill related functions of the svm spoke pool clie…
melisaguevara May 16, 2025
1b4dfb1
updates
bmzig May 16, 2025
345eb57
feat(svm): svm spoke binaries integration tests
md0x May 16, 2025
9ddeb47
refactor: remove new line
md0x May 16, 2025
f3efed9
refactor: update test name
md0x May 16, 2025
968926b
fix: lint
md0x May 16, 2025
a9f7199
fix: current time in tests
md0x May 16, 2025
15856e1
refactor: remove catch
md0x May 16, 2025
c392dac
fix: test
md0x May 16, 2025
2fc707f
refactor: simplify path
md0x May 16, 2025
cfde0c7
Merge branch 'master' into pablo/svm-binaries
md0x May 16, 2025
68b7bee
feat: get across contract version from package.json
md0x May 26, 2025
b39873d
Merge branch 'master' into pablo/svm-binaries
md0x May 26, 2025
a4d2e6a
fix: revert to 4.0.9 contracts to update in different pr
md0x May 26, 2025
51254f5
test: update testcase
md0x May 26, 2025
0a3387b
fix: test
md0x May 26, 2025
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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ dist
/artifacts
/typechain-types
.ledger
/target
*.tar.gz

# production
/build
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,14 +105,14 @@
"dependencies": {
"@across-protocol/across-token": "^1.0.0",
"@across-protocol/constants": "^3.1.67",
"@across-protocol/contracts": "^4.0.9",
"@across-protocol/contracts": "4.0.9",
"@coral-xyz/anchor": "^0.30.1",
"@eth-optimism/sdk": "^3.3.1",
"@ethersproject/bignumber": "^5.7.0",
"@pinata/sdk": "^2.1.0",
"@solana/kit": "^2.1.0",
"@solana-program/system": "^0.7.0",
"@solana-program/token-2022": "^0.4.0",
"@solana/kit": "^2.1.0",
"@solana/web3.js": "^1.31.0",
"@types/mocha": "^10.0.1",
"@uma/sdk": "^0.34.10",
Expand Down
5 changes: 3 additions & 2 deletions src/arch/svm/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,10 @@ export type EventWithData = {
program: Address;
};

export type SVMProvider = Rpc<SolanaRpcApiFromTransport<RpcTransport>>;

// Typed aggregate of JSON‑RPC and subscription clients.
export type RpcClient = {
rpc: Rpc<SolanaRpcApiFromTransport<RpcTransport>>;
rpc: SVMProvider;
rpcSubscriptions: RpcSubscriptions<SignatureNotificationsApi & SlotNotificationsApi>;
};
export type SVMProvider = Rpc<SolanaRpcApiFromTransport<RpcTransport>>;
43 changes: 42 additions & 1 deletion test/SVMSpokePoolClient.fills.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { KeyPairSigner, address } from "@solana/kit";
import { KeyPairSigner, address, fetchEncodedAccount } from "@solana/kit";
import { CHAIN_IDs } from "@across-protocol/constants";
import { SvmSpokeClient } from "@across-protocol/contracts";
import { intToU8Array32 } from "@across-protocol/contracts/dist/src/svm/web3-v1";
Expand All @@ -12,6 +12,8 @@ import {
formatRelayData,
mintTokens,
sendRequestSlowFill,
closeFillPda,
setCurrentTime,
} from "./utils/svm/utils";
import { SVM_DEFAULT_ADDRESS, findFillEvent, getRandomSvmAddress } from "../src/arch/svm";
import { SVMSpokePoolClient } from "../src/clients";
Expand Down Expand Up @@ -202,4 +204,43 @@ describe("SVMSpokePoolClient: Fills", function () {
);
expect(fillStatusAfterRequest).to.equal(FillStatus.RequestedSlowFill);
});

it("Closes the fill pda after the fill deadline has passed", async () => {
const currentSlot = await solanaClient.rpc.getSlot({ commitment: "confirmed" }).send();
const currentSlotTimestamp = await solanaClient.rpc.getBlockTime(currentSlot).send();
const fillDeadline = Number(currentSlotTimestamp) + 1;
await setCurrentTime(signer, solanaClient, Number(currentSlotTimestamp));
const newRelayData = { ...relayData, depositId: new Uint8Array(intToU8Array32(getRandomInt())), fillDeadline };
const formattedRelayData = formatRelayData(newRelayData);
await mintTokens(signer, solanaClient, mint.address, BigInt(relayData.outputAmount));
const { fillInput, relayData: fillRelayData } = await sendCreateFill(
solanaClient,
signer,
mint,
decimals,
newRelayData
);

const fillStatusAfterFill = await spokePoolClient.relayFillStatus(formattedRelayData);
expect(fillStatusAfterFill).to.equal(FillStatus.Filled);

try {
await closeFillPda(signer, solanaClient, fillInput.fillStatus);
} catch (error) {
expect(error.context.logs.some((log) => log.includes("The fill deadline has not passed!"))).to.be.true;
}

await setCurrentTime(signer, solanaClient, fillRelayData.fillDeadline + 1);

await closeFillPda(signer, solanaClient, fillInput.fillStatus);

const fillStatusAccount = await fetchEncodedAccount(solanaClient.rpc, fillInput.fillStatus, {
commitment: "confirmed",
});

expect(fillStatusAccount.exists).to.be.false;

const fillStatusWithPdaClosed = await spokePoolClient.relayFillStatus(formattedRelayData);
expect(fillStatusWithPdaClosed).to.equal(FillStatus.Filled);
});
});
15 changes: 11 additions & 4 deletions test/Solana.setup.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
import { validatorSetup, validatorTeardown } from "./utils/svm/validator.setup";
import { createDefaultSolanaClient, generateKeyPairSignerWithSol, initializeSvmSpoke } from "./utils/svm/utils";

import { airdropFactory, generateKeyPairSigner, lamports } from "@solana/kit";
// Create the signer
let signer: Awaited<ReturnType<typeof generateKeyPairSignerWithSol>>;

before(async function () {
// Generate the signer
signer = await generateKeyPairSigner();

/* Local validator spin‑up can take a few seconds */
this.timeout(60_000);
await validatorSetup();
await validatorSetup(signer.address);

const solanaClient = createDefaultSolanaClient();

// Generate the signer
signer = await generateKeyPairSignerWithSol(solanaClient);
// Airdrop SOL to the signer
await airdropFactory(solanaClient)({
recipientAddress: signer.address,
lamports: lamports(1_000_000_000n),
commitment: "confirmed",
});

// Initialize the program and get the state
await initializeSvmSpoke(signer, solanaClient, signer.address);
Expand Down
54 changes: 42 additions & 12 deletions test/utils/svm/utils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { CHAIN_IDs } from "@across-protocol/constants";
import { SvmSpokeClient } from "@across-protocol/contracts";
import { intToU8Array32 } from "@across-protocol/contracts/dist/src/svm/web3-v1";
import { RelayDataArgs } from "@across-protocol/contracts/dist/src/svm/clients/SvmSpoke";
import { CHAIN_IDs } from "@across-protocol/constants";
import { intToU8Array32 } from "@across-protocol/contracts/dist/src/svm/web3-v1";
import { getCreateAccountInstruction, SYSTEM_PROGRAM_ADDRESS } from "@solana-program/system";
import {
ASSOCIATED_TOKEN_PROGRAM_ADDRESS,
Expand Down Expand Up @@ -34,21 +34,20 @@ import {
TransactionMessageWithBlockhashLifetime,
TransactionSigner,
} from "@solana/kit";
import { hexlify, arrayify } from "ethers/lib/utils";
import { arrayify, hexlify } from "ethers/lib/utils";
import {
getAssociatedTokenAddress,
getEventAuthority,
getFillStatusPda,
getRandomSvmAddress,
getRoutePda,
getStatePda,
getTimestampForSlot,
RpcClient,
SVM_DEFAULT_ADDRESS,
SVM_SPOKE_SEED,
} from "../../../src/arch/svm";
import { EvmAddress, SvmAddress, getRandomInt, getRelayDataHash, randomAddress, BigNumber } from "../../../src/utils";
import { RelayData } from "../../../src/interfaces";
import { BigNumber, EvmAddress, getRandomInt, getRelayDataHash, randomAddress, SvmAddress } from "../../../src/utils";

/** RPC / Client */

Expand Down Expand Up @@ -336,6 +335,40 @@ export const createFill = async (
);
};

// Closes the fill PDA.
export const closeFillPda = async (signer: KeyPairSigner, solanaClient: RpcClient, fillStatusPda: Address) => {
const closeFillPdaIx = await SvmSpokeClient.getCloseFillPdaInstruction({
signer,
state: await getStatePda(SvmSpokeClient.SVM_SPOKE_PROGRAM_ADDRESS),
fillStatus: fillStatusPda,
});
return pipe(
await createDefaultTransaction(solanaClient, signer),
(tx) => appendTransactionMessageInstruction(closeFillPdaIx, tx),
(tx) => signAndSendTransaction(solanaClient, tx)
);
};

// Sets the current time for the SVM Spoke program.
export const setCurrentTime = async (signer: KeyPairSigner, solanaClient: RpcClient, newTime: number) => {
const setCurrentTimeIx = await SvmSpokeClient.getSetCurrentTimeInstruction({
signer,
state: await getStatePda(SvmSpokeClient.SVM_SPOKE_PROGRAM_ADDRESS),
newTime,
});
return pipe(
await createDefaultTransaction(solanaClient, signer),
(tx) => appendTransactionMessageInstruction(setCurrentTimeIx, tx),
(tx) => signAndSendTransaction(solanaClient, tx)
);
};

export const getCurrentTime = async (solanaClient: RpcClient) => {
const statePda = await getStatePda(SvmSpokeClient.SVM_SPOKE_PROGRAM_ADDRESS);
const state = await SvmSpokeClient.fetchState(solanaClient.rpc, statePda);
return state.data.currentTime;
};

// helper to send a fill
export const sendCreateFill = async (
solanaClient: RpcClient,
Expand All @@ -344,8 +377,7 @@ export const sendCreateFill = async (
mintDecimals: number,
overrides: Partial<RelayDataArgs> = {}
) => {
const latestSlot = await solanaClient.rpc.getSlot({ commitment: "confirmed" }).send();
const currentTime = await getTimestampForSlot(solanaClient.rpc, Number(latestSlot));
const currentTime = await getCurrentTime(solanaClient);

const relayData: SvmSpokeClient.FillRelayInput["relayData"] = {
depositor: overrides.depositor ?? address(EvmAddress.from(randomAddress()).toBase58()),
Expand Down Expand Up @@ -402,7 +434,7 @@ export const sendCreateFill = async (
};

const signature = await createFill(signer, solanaClient, fillInput, mintDecimals);
return { signature, relayData };
return { signature, relayData, fillInput };
};

// helper to send a request slow fill
Expand All @@ -412,8 +444,7 @@ export const sendRequestSlowFill = async (
overrides: Partial<RelayDataArgs> = {}
) => {
const destinationChainId = CHAIN_IDs.SOLANA;
const latestSlot = await solanaClient.rpc.getSlot({ commitment: "confirmed" }).send();
const currentTime = await getTimestampForSlot(solanaClient.rpc, Number(latestSlot));
const currentTime = await getCurrentTime(solanaClient);

const relayData: SvmSpokeClient.RequestSlowFillInstructionDataArgs["relayData"] = {
depositor: overrides.depositor ?? address(EvmAddress.from(randomAddress()).toBase58()),
Expand Down Expand Up @@ -463,8 +494,7 @@ export const sendCreateDeposit = async (
overrides: Partial<SvmSpokeClient.DepositInput> = {},
destinationChainId: number = CHAIN_IDs.MAINNET
) => {
const latestSlot = await solanaClient.rpc.getSlot({ commitment: "confirmed" }).send();
const currentTime = await solanaClient.rpc.getBlockTime(latestSlot).send();
const currentTime = await getCurrentTime(solanaClient);

const depositInput: SvmSpokeClient.DepositInput = {
depositor: signer.address,
Expand Down
75 changes: 60 additions & 15 deletions test/utils/svm/validator.setup.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,58 @@
import { SvmSpokeClient } from "@across-protocol/contracts";
import { Address } from "@solana/kit";
import { spawn } from "child_process";
import path from "node:path";
import fs from "node:fs/promises";
import { SvmSpokeClient } from "@across-protocol/contracts";
import path from "node:path";
import tar from "tar";

// Directory and file constants
const LEDGER_DIR = path.resolve(__dirname, "..", ".ledger");
const TARGET_DIR = path.resolve(__dirname, "..", "..", "..");
const SVM_SPOKE_SO_PATH = path.resolve(TARGET_DIR, "target", "deploy", "svm_spoke.so");
const BINARY_RELEASE_TAG = process.env.SVM_BINARY_RELEASE_TAG || "v4.0.11";
const BINARY_ARCHIVE_NAME = process.env.SVM_BINARY_ARCHIVE_NAME || "svm-verified-test-binaries.tar.gz";
const BINARY_DOWNLOAD_URL = `https://github.com/across-protocol/contracts/releases/download/${BINARY_RELEASE_TAG}/${BINARY_ARCHIVE_NAME}`;
const SAVED_ARCHIVE_NAME = `svm-verified-test-binaries.${BINARY_RELEASE_TAG}.tar.gz`;

/**
* Download and save the SVM spoke test binaries if not already present.
*/
async function ensureBinaries(): Promise<void> {
const archivePath = path.join(TARGET_DIR, SAVED_ARCHIVE_NAME);
try {
await fs.access(archivePath);
} catch {
const response = await fetch(BINARY_DOWNLOAD_URL);
if (!response.ok) {
throw new Error(`Failed to download binaries: ${response.status} ${response.statusText}`);
}

export const validatorSetup = async () => {
// Always start with a clean ledger
const blob = await response.blob();
await fs.writeFile(archivePath, Buffer.from(await blob.arrayBuffer()));
}

await tar.x({ file: path.join(TARGET_DIR, SAVED_ARCHIVE_NAME), cwd: TARGET_DIR });
}

/**
* Starts a local Solana test validator with the SVM spoke program loaded.
* @param upgradeAuthority - The address authorized to upgrade the program
*/
export async function validatorSetup(upgradeAuthority: Address): Promise<void> {
// Clean previous ledger state
await fs.rm(LEDGER_DIR, { recursive: true, force: true });
// Ensure target directory
await fs.mkdir(TARGET_DIR, { recursive: true });

// Prepare binaries
await ensureBinaries();

// Launch test validator
const args = [
"--clone-upgradeable-program",
"--upgradeable-program",
SvmSpokeClient.SVM_SPOKE_PROGRAM_ADDRESS,
"--url",
"https://api.mainnet-beta.solana.com",
SVM_SPOKE_SO_PATH,
upgradeAuthority,
"--ledger",
LEDGER_DIR,
"--reset",
Expand All @@ -23,24 +62,30 @@ export const validatorSetup = async () => {
stdio: ["ignore", "pipe", "pipe"],
});

// Wait until the validator is ready
await new Promise<void>((resolve, reject) => {
proc.stdout.on("data", (d: Buffer) => {
if (d.toString().includes("JSON RPC URL")) resolve();
proc.stdout.on("data", (chunk: Buffer) => {
const msg = chunk.toString();
if (msg.includes("JSON RPC URL")) {
resolve();
}
});
proc.on("error", reject);
proc.on("exit", (code) => reject(new Error(`validator exited early with code ${code}`)));
proc.on("exit", (code) => reject(new Error(`Validator exited early with code ${code}`)));
});

// expose the pid for teardown
// Store PID for teardown
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(global as any).__SOLANA_VALIDATOR_PID__ = proc.pid;
};
}

export const validatorTeardown = () => {
/**
* Stops the running Solana test validator, if any.
*/
export function validatorTeardown(): void {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const pid = (global as any).__SOLANA_VALIDATOR_PID__;
if (pid) {
process.kill(pid);
return;
}
};
}
Loading