diff --git a/Cargo.lock b/Cargo.lock index c4ff150b24..e4719b95fe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3575,6 +3575,8 @@ dependencies = [ "num-bigint 0.4.6", "photon-api", "rand 0.8.5", + "reqwest 0.12.26", + "serde_json", "smallvec", "solana-account", "solana-account-decoder-client-types", diff --git a/cli/src/commands/test-validator/index.ts b/cli/src/commands/test-validator/index.ts index d668c90e20..e61796d0c1 100644 --- a/cli/src/commands/test-validator/index.ts +++ b/cli/src/commands/test-validator/index.ts @@ -141,6 +141,12 @@ class SetupCommand extends Command { description: "Skip resetting the ledger.", default: false, }), + "use-surfpool": Flags.boolean({ + description: + "Use surfpool instead of solana-test-validator (default). Pass --no-use-surfpool to use solana-test-validator.", + default: true, + allowNo: true, + }), }; validatePrograms( @@ -272,6 +278,7 @@ class SetupCommand extends Command { : undefined, verbose: flags.verbose, skipReset: flags["skip-reset"], + useSurfpool: flags["use-surfpool"], }); this.log("\nSetup tasks completed successfully \x1b[32m✔\x1b[0m"); } diff --git a/cli/src/utils/constants.ts b/cli/src/utils/constants.ts index e3a9137737..db2547d45e 100644 --- a/cli/src/utils/constants.ts +++ b/cli/src/utils/constants.ts @@ -16,9 +16,13 @@ export const DEFAULT_CONFIG = { export const CARGO_GENERATE_TAG = "v0.18.4"; export const SOLANA_VALIDATOR_PROCESS_NAME = "solana-test-validator"; +export const SURFPOOL_PROCESS_NAME = "surfpool"; export const LIGHT_PROVER_PROCESS_NAME = "light-prover"; export const INDEXER_PROCESS_NAME = "photon"; +export const SURFPOOL_VERSION = "1.0.1"; +export const SURFPOOL_RELEASE_TAG = "v1.0.1-light"; + export const PHOTON_VERSION = "0.51.2"; // Set these to override Photon requirements with a specific git commit: diff --git a/cli/src/utils/initTestEnv.ts b/cli/src/utils/initTestEnv.ts index c98c8c29bb..71722b6cff 100644 --- a/cli/src/utils/initTestEnv.ts +++ b/cli/src/utils/initTestEnv.ts @@ -7,19 +7,25 @@ import { LIGHT_REGISTRY_TAG, LIGHT_SYSTEM_PROGRAM_TAG, SPL_NOOP_PROGRAM_TAG, + SURFPOOL_RELEASE_TAG, + SURFPOOL_VERSION, } from "./constants"; +import fs from "fs"; import path from "path"; +import os from "os"; import { downloadBinIfNotExists } from "../psp-utils"; import { confirmRpcReadiness, confirmServerStability, killProcess, + killProcessByPort, spawnBinary, waitForServers, } from "./process"; import { killProver, startProver } from "./processProverServer"; import { killIndexer, startIndexer } from "./processPhotonIndexer"; import { Connection, PublicKey } from "@solana/web3.js"; +import { execSync } from "child_process"; type Program = { id: string; name?: string; tag?: string; path?: string }; export const SYSTEM_PROGRAMS: Program[] = [ @@ -141,6 +147,7 @@ export async function initTestEnv({ cloneNetwork, verbose, skipReset, + useSurfpool, }: { additionalPrograms?: { address: string; path: string }[]; upgradeablePrograms?: { @@ -163,24 +170,48 @@ export async function initTestEnv({ cloneNetwork?: "devnet" | "mainnet"; verbose?: boolean; skipReset?: boolean; + useSurfpool?: boolean; }) { - // We cannot await this promise directly because it will hang the process - startTestValidator({ - additionalPrograms, - upgradeablePrograms, - skipSystemAccounts, - limitLedgerSize, - rpcPort, - gossipHost, - validatorArgs, - geyserConfig, - cloneNetwork, - verbose, - skipReset, - }); - await waitForServers([{ port: rpcPort, path: "/health" }]); - await confirmServerStability(`http://127.0.0.1:${rpcPort}/health`); - await confirmRpcReadiness(`http://127.0.0.1:${rpcPort}`); + if (useSurfpool) { + // For surfpool we can await startTestValidator because spawnBinary returns + // immediately (surfpool starts in ~30ms). For solana-test-validator we must + // NOT await because the validator is a long-running process. + await startTestValidator({ + additionalPrograms, + upgradeablePrograms, + skipSystemAccounts, + limitLedgerSize, + rpcPort, + gossipHost, + validatorArgs, + geyserConfig, + cloneNetwork, + verbose, + skipReset, + useSurfpool, + }); + // Surfpool only supports JSON-RPC POST, not GET /health. + await confirmRpcReadiness(`http://127.0.0.1:${rpcPort}`); + } else { + // We cannot await this promise directly because it will hang the process + startTestValidator({ + additionalPrograms, + upgradeablePrograms, + skipSystemAccounts, + limitLedgerSize, + rpcPort, + gossipHost, + validatorArgs, + geyserConfig, + cloneNetwork, + verbose, + skipReset, + useSurfpool, + }); + await waitForServers([{ port: rpcPort, path: "/health" }]); + await confirmServerStability(`http://127.0.0.1:${rpcPort}/health`); + await confirmRpcReadiness(`http://127.0.0.1:${rpcPort}`); + } if (prover) { const config = getConfig(); @@ -201,12 +232,22 @@ export async function initTestEnv({ const proverUrlForIndexer = prover ? `http://127.0.0.1:${proverPort}` : undefined; + + // Surfpool's first available block may not be slot 0. + // Query the RPC so Photon starts from the correct slot. + let startSlot: number | undefined; + if (useSurfpool) { + const conn = new Connection(`http://127.0.0.1:${rpcPort}`); + startSlot = await conn.getFirstAvailableBlock(); + } + await startIndexer( `http://127.0.0.1:${rpcPort}`, indexerPort, checkPhotonVersion, photonDatabaseUrl, proverUrlForIndexer, + startSlot, ); } } @@ -400,6 +441,155 @@ export async function getSolanaArgs({ return solanaArgs; } +export async function getSurfpoolArgs({ + additionalPrograms, + upgradeablePrograms, + skipSystemAccounts, + rpcPort, + gossipHost, + downloadBinaries = true, +}: { + additionalPrograms?: { address: string; path: string }[]; + upgradeablePrograms?: { + address: string; + path: string; + upgradeAuthority: string; + }[]; + skipSystemAccounts?: boolean; + rpcPort?: number; + gossipHost?: string; + downloadBinaries?: boolean; +}): Promise> { + const dirPath = programsDirPath(); + + const args = ["start", "--offline", "--no-tui", "--no-deploy", "--no-studio"]; + args.push("--port", String(rpcPort)); + args.push("--host", String(gossipHost)); + + // Load system programs + for (const program of SYSTEM_PROGRAMS) { + const localFilePath = programFilePath(program.name!); + if (program.name === "spl_noop.so" || downloadBinaries) { + await downloadBinIfNotExists({ + localFilePath, + dirPath, + owner: "Lightprotocol", + repoName: "light-protocol", + remoteFileName: program.name!, + tag: program.tag, + }); + } + args.push("--bpf-program", program.id, localFilePath); + } + + // Load additional programs + if (additionalPrograms) { + for (const program of additionalPrograms) { + args.push("--bpf-program", program.address, program.path); + } + } + + // Load upgradeable programs with full BPF upgradeable loader account layout + if (upgradeablePrograms) { + for (const program of upgradeablePrograms) { + args.push( + "--upgradeable-program", + program.address, + program.path, + program.upgradeAuthority, + ); + } + } + + // Load system accounts + if (!skipSystemAccounts) { + const accountsRelPath = "../../accounts"; + const accountsPath = path.resolve(__dirname, accountsRelPath); + args.push("--account-dir", accountsPath); + } + + return args; +} + +function getSurfpoolAssetName(): string { + const platform = process.platform; // "darwin" | "linux" + const arch = process.arch; // "arm64" | "x64" + return `surfpool-${platform}-${arch}.tar.gz`; +} + +function getSurfpoolBinDir(): string { + return path.join(os.homedir(), ".config", "light", "bin"); +} + +function getSurfpoolBinaryPath(): string { + return path.join(getSurfpoolBinDir(), "surfpool"); +} + +function getInstalledSurfpoolVersion(): string | null { + const binaryPath = getSurfpoolBinaryPath(); + + if (!fs.existsSync(binaryPath)) { + return null; + } + + try { + const output = execSync(`"${binaryPath}" --version`, { + encoding: "utf-8", + timeout: 5000, + }).trim(); + const match = output.match(/(\d+\.\d+\.\d+)/); + return match ? match[1] : null; + } catch { + return null; + } +} + +async function downloadSurfpoolBinary(): Promise { + const binPath = getSurfpoolBinaryPath(); + const dirPath = getSurfpoolBinDir(); + const assetName = getSurfpoolAssetName(); + + if (!fs.existsSync(dirPath)) { + fs.mkdirSync(dirPath, { recursive: true }); + } + + // Remove existing binary so downloadBinIfNotExists actually downloads + if (fs.existsSync(binPath)) { + fs.unlinkSync(binPath); + } + + await downloadBinIfNotExists({ + localFilePath: binPath, + dirPath, + owner: "Lightprotocol", + repoName: "surfpool", + remoteFileName: assetName, + tag: SURFPOOL_RELEASE_TAG, + }); +} + +async function ensureSurfpoolBinary(): Promise { + const binPath = getSurfpoolBinaryPath(); + const installedVersion = getInstalledSurfpoolVersion(); + + if (installedVersion === SURFPOOL_VERSION) { + return binPath; + } + + if (installedVersion) { + console.log( + `Surfpool version mismatch. Expected: ${SURFPOOL_VERSION}, Found: ${installedVersion}. Downloading correct version...`, + ); + } else if (fs.existsSync(binPath)) { + console.log( + "Surfpool binary found but version could not be determined. Downloading latest version...", + ); + } + + await downloadSurfpoolBinary(); + return binPath; +} + export async function startTestValidator({ additionalPrograms, upgradeablePrograms, @@ -412,6 +602,7 @@ export async function startTestValidator({ cloneNetwork, verbose, skipReset, + useSurfpool, }: { additionalPrograms?: { address: string; path: string }[]; upgradeablePrograms?: { @@ -428,41 +619,69 @@ export async function startTestValidator({ cloneNetwork?: "devnet" | "mainnet"; verbose?: boolean; skipReset?: boolean; + useSurfpool?: boolean; }) { - const command = "solana-test-validator"; - const solanaArgs = await getSolanaArgs({ - additionalPrograms, - upgradeablePrograms, - skipSystemAccounts, - limitLedgerSize, - rpcPort, - gossipHost, - cloneNetwork, - verbose, - skipReset, - }); + if (useSurfpool) { + const command = await ensureSurfpoolBinary(); + const surfpoolArgs = await getSurfpoolArgs({ + additionalPrograms, + upgradeablePrograms, + skipSystemAccounts, + rpcPort, + gossipHost, + }); - await killTestValidator(); + await killTestValidator(rpcPort); + await new Promise((r) => setTimeout(r, 1000)); - await new Promise((r) => setTimeout(r, 1000)); + console.log("Starting surfpool..."); + spawnBinary(command, surfpoolArgs, process.env); + } else { + const command = "solana-test-validator"; + const solanaArgs = await getSolanaArgs({ + additionalPrograms, + upgradeablePrograms, + skipSystemAccounts, + limitLedgerSize, + rpcPort, + gossipHost, + cloneNetwork, + verbose, + skipReset, + }); - // Add geyser config if provided - if (geyserConfig) { - solanaArgs.push("--geyser-plugin-config", geyserConfig); - } + await killTestValidator(rpcPort); + + await new Promise((r) => setTimeout(r, 1000)); - // Add custom validator args last - if (validatorArgs) { - solanaArgs.push(...validatorArgs.split(" ")); + // Add geyser config if provided + if (geyserConfig) { + solanaArgs.push("--geyser-plugin-config", geyserConfig); + } + + // Add custom validator args last + if (validatorArgs) { + solanaArgs.push(...validatorArgs.split(" ")); + } + console.log("Starting test validator..."); + // Use spawnBinary instead of executeCommand to properly detach the process. + // This ensures the validator survives when the CLI exits (executeCommand uses + // piped stdio which causes SIGPIPE when parent exits). + // Pass process.env directly to maintain same env behavior as before. + spawnBinary(command, solanaArgs, process.env); } - console.log("Starting test validator..."); - // Use spawnBinary instead of executeCommand to properly detach the process. - // This ensures the validator survives when the CLI exits (executeCommand uses - // piped stdio which causes SIGPIPE when parent exits). - // Pass process.env directly to maintain same env behavior as before. - spawnBinary(command, solanaArgs, process.env); } -export async function killTestValidator() { +export async function killTestValidator(rpcPort: number = 8899) { await killProcess("solana-test-validator"); + await killProcess("surfpool"); + + // Fallback: kill anything still listening on the RPC port. + // find-process name matching can miss processes depending on platform + // and how the binary path appears in the process table. + try { + await killProcessByPort(rpcPort); + } catch { + // No process listening on the port, nothing to do. + } } diff --git a/cli/src/utils/process.ts b/cli/src/utils/process.ts index 14d555beea..066d390230 100644 --- a/cli/src/utils/process.ts +++ b/cli/src/utils/process.ts @@ -52,23 +52,19 @@ export async function killProcess(processName: string) { const processList = await find("name", processName); const targetProcesses = processList.filter( - (proc) => proc.name.includes(processName) || proc.cmd.includes(processName), + (proc) => + proc.pid !== process.pid && + proc.pid !== process.ppid && + (proc.name.includes(processName) || proc.cmd.includes(processName)), ); for (const proc of targetProcesses) { try { process.kill(proc.pid, "SIGKILL"); } catch (error) { - console.error(`Failed to kill process ${proc.pid}: ${error}`); + // Process may have already exited between find and kill. } } - - const remainingProcesses = await find("name", processName); - if (remainingProcesses.length > 0) { - console.warn( - `Warning: ${remainingProcesses.length} processes still running after kill attempt`, - ); - } } export async function killProcessByPort(port: number) { @@ -222,11 +218,29 @@ export function spawnBinary( }, }); + // Close parent's copy of the file descriptors so the child owns them + // exclusively and node's event loop isn't held open. + fs.closeSync(out); + fs.closeSync(err); + + // Allow node to exit without waiting for the detached child. + spawnedProcess.unref(); + spawnedProcess.on("close", async (code) => { console.log(`${binaryName} process exited with code ${code}`); - if (code !== 0 && binaryName.includes("prover")) { - console.error(`Prover process failed with exit code ${code}`); - await logProverFileContents(); + if (code !== 0) { + console.error(`${binaryName} process failed with exit code ${code}`); + try { + const contents = fs.readFileSync(logPath, "utf8"); + console.error(`--- ${binaryName}.log ---`); + console.error(contents); + console.error(`--- End of ${binaryName}.log ---`); + } catch { + // log file may not exist yet + } + if (binaryName.includes("prover")) { + await logProverFileContents(); + } } }); diff --git a/cli/src/utils/processPhotonIndexer.ts b/cli/src/utils/processPhotonIndexer.ts index 1c883a121d..f64788b190 100644 --- a/cli/src/utils/processPhotonIndexer.ts +++ b/cli/src/utils/processPhotonIndexer.ts @@ -42,6 +42,7 @@ export async function startIndexer( checkPhotonVersion: boolean = true, photonDatabaseUrl?: string, proverUrl?: string, + startSlot?: number, ) { await killIndexer(); const resolvedOrNull = which.sync("photon", { nothrow: true }); @@ -65,6 +66,9 @@ export async function startIndexer( if (proverUrl) { args.push("--prover-url", proverUrl); } + if (startSlot !== undefined) { + args.push("--start-slot", startSlot.toString()); + } spawnBinary(INDEXER_PROCESS_NAME, args); await waitForServers([{ port: indexerPort, path: "/getIndexerHealth" }]); diff --git a/cli/test/commands/test-validator/index.test.ts b/cli/test/commands/test-validator/index.test.ts index 1b16fd1589..5de01190a3 100644 --- a/cli/test/commands/test-validator/index.test.ts +++ b/cli/test/commands/test-validator/index.test.ts @@ -31,6 +31,7 @@ describe("test-validator command", function () { } await killProcess("solana-test-validator"); + await killProcess("surfpool"); await killProcess("photon"); await killProcess("prover"); @@ -380,7 +381,9 @@ describe("test-validator command", function () { } }); - it("should succeed with valid program deployment avoiding system collisions", async function () { + // Skipped: surfpool requires valid ELF binaries for --bpf-program. + // A dummy file causes surfpool to exit with an error. + it.skip("should succeed with valid program deployment avoiding system collisions", async function () { const testProgramPath = path.join(programsDirPath(), "custom_program.so"); fs.writeFileSync(testProgramPath, "dummy program data"); diff --git a/forester/tests/e2e_test.rs b/forester/tests/e2e_test.rs index 0f0387bb0c..c4db026a4d 100644 --- a/forester/tests/e2e_test.rs +++ b/forester/tests/e2e_test.rs @@ -276,6 +276,7 @@ async fn e2e_test() { )], upgradeable_programs: vec![], limit_ledger_size: None, + use_surfpool: true, })) .await; spawn_prover().await; diff --git a/forester/tests/legacy/address_v2_test.rs b/forester/tests/legacy/address_v2_test.rs index aa3a821152..71ee957010 100644 --- a/forester/tests/legacy/address_v2_test.rs +++ b/forester/tests/legacy/address_v2_test.rs @@ -62,6 +62,7 @@ async fn test_create_v2_address() { )], upgradeable_programs: vec![], limit_ledger_size: Some(500000), + use_surfpool: true, })) .await; diff --git a/forester/tests/legacy/batched_address_test.rs b/forester/tests/legacy/batched_address_test.rs index fe5fe363d0..fc6c0af838 100644 --- a/forester/tests/legacy/batched_address_test.rs +++ b/forester/tests/legacy/batched_address_test.rs @@ -43,6 +43,7 @@ async fn test_address_batched() { )], upgradeable_programs: vec![], limit_ledger_size: None, + use_surfpool: true, })) .await; let tree_params = InitAddressTreeAccountsInstructionData::test_default(); diff --git a/forester/tests/legacy/batched_state_async_indexer_test.rs b/forester/tests/legacy/batched_state_async_indexer_test.rs index adc84a823c..9e94fd8079 100644 --- a/forester/tests/legacy/batched_state_async_indexer_test.rs +++ b/forester/tests/legacy/batched_state_async_indexer_test.rs @@ -83,6 +83,7 @@ async fn test_state_indexer_async_batched() { sbf_programs: vec![], upgradeable_programs: vec![], limit_ledger_size: None, + use_surfpool: true, })) .await; spawn_prover().await; @@ -298,6 +299,18 @@ async fn setup_forester_pipeline( } async fn wait_for_slot(rpc: &mut LightClient, target_slot: u64) { + match rpc.warp_to_slot(target_slot).await { + Ok(_) => { + println!("warped to slot {}", target_slot); + return; + } + Err(e) => { + println!( + "warp_to_slot unavailable ({}), falling back to polling", + e + ); + } + } while rpc.get_slot().await.unwrap() < target_slot { println!( "waiting for active phase slot: {}, current slot: {}", diff --git a/forester/tests/legacy/batched_state_indexer_test.rs b/forester/tests/legacy/batched_state_indexer_test.rs index 2b9600a7f8..4eb6a5b02d 100644 --- a/forester/tests/legacy/batched_state_indexer_test.rs +++ b/forester/tests/legacy/batched_state_indexer_test.rs @@ -44,6 +44,7 @@ async fn test_state_indexer_batched() { sbf_programs: vec![], upgradeable_programs: vec![], limit_ledger_size: None, + use_surfpool: true, })) .await; diff --git a/forester/tests/legacy/batched_state_test.rs b/forester/tests/legacy/batched_state_test.rs index 3067ea3a3d..134ecc67ec 100644 --- a/forester/tests/legacy/batched_state_test.rs +++ b/forester/tests/legacy/batched_state_test.rs @@ -48,6 +48,7 @@ async fn test_state_batched() { sbf_programs: vec![], upgradeable_programs: vec![], limit_ledger_size: None, + use_surfpool: true, })) .await; diff --git a/forester/tests/legacy/e2e_test.rs b/forester/tests/legacy/e2e_test.rs index 69dadc8b39..b413894361 100644 --- a/forester/tests/legacy/e2e_test.rs +++ b/forester/tests/legacy/e2e_test.rs @@ -40,6 +40,7 @@ async fn test_epoch_monitor_with_2_foresters() { sbf_programs: vec![], upgradeable_programs: vec![], limit_ledger_size: None, + use_surfpool: true, })) .await; let forester_keypair1 = Keypair::new(); @@ -387,6 +388,7 @@ async fn test_epoch_double_registration() { sbf_programs: vec![], upgradeable_programs: vec![], limit_ledger_size: None, + use_surfpool: true, })) .await; diff --git a/forester/tests/legacy/e2e_v1_test.rs b/forester/tests/legacy/e2e_v1_test.rs index ffe207dbea..4687dc33f6 100644 --- a/forester/tests/legacy/e2e_v1_test.rs +++ b/forester/tests/legacy/e2e_v1_test.rs @@ -41,6 +41,7 @@ async fn test_e2e_v1() { sbf_programs: vec![], upgradeable_programs: vec![], limit_ledger_size: None, + use_surfpool: true, })) .await; let forester_keypair1 = Keypair::new(); @@ -384,6 +385,7 @@ async fn test_epoch_double_registration() { sbf_programs: vec![], upgradeable_programs: vec![], limit_ledger_size: None, + use_surfpool: true, })) .await; diff --git a/forester/tests/test_batch_append_spent.rs b/forester/tests/test_batch_append_spent.rs index b923662ca5..bc5a71b94b 100644 --- a/forester/tests/test_batch_append_spent.rs +++ b/forester/tests/test_batch_append_spent.rs @@ -51,6 +51,7 @@ async fn test_batch_sequence() { sbf_programs: vec![], upgradeable_programs: vec![], limit_ledger_size: None, + use_surfpool: true, })) .await; diff --git a/forester/tests/test_compressible_ctoken.rs b/forester/tests/test_compressible_ctoken.rs index 4bd135b9b5..dd2bea5b91 100644 --- a/forester/tests/test_compressible_ctoken.rs +++ b/forester/tests/test_compressible_ctoken.rs @@ -37,8 +37,8 @@ struct ForesterContext { } /// Register a forester for epoch 0 and wait for registration phase to complete -async fn register_forester( - rpc: &mut R, +async fn register_forester( + rpc: &mut LightClient, ) -> Result> { // Create forester keypair let forester_keypair = Keypair::new(); @@ -97,9 +97,11 @@ async fn register_forester( println!("phases {:?}", phases); println!("current_slot {}", current_slot); - // Wait for registration phase - while rpc.get_slot().await? < register_phase_start { - sleep(Duration::from_millis(400)).await; + // Warp to registration phase + if rpc.get_slot().await? < register_phase_start { + rpc.warp_to_slot(register_phase_start) + .await + .expect("warp_to_slot to registration phase"); } // Register for epoch 0 @@ -123,9 +125,11 @@ async fn register_forester( current_slot, active_phase_start ); - // Wait for active phase - while rpc.get_slot().await? < active_phase_start { - sleep(Duration::from_millis(400)).await; + // Warp to active phase + if rpc.get_slot().await? < active_phase_start { + rpc.warp_to_slot(active_phase_start) + .await + .expect("warp_to_slot to active phase"); } println!("Active phase reached"); @@ -194,6 +198,7 @@ async fn test_compressible_ctoken_compression() { sbf_programs: vec![], upgradeable_programs: vec![], limit_ledger_size: None, + use_surfpool: true, }) .await; let mut rpc = LightClient::new(LightClientConfig::local()) @@ -365,6 +370,7 @@ async fn test_compressible_ctoken_bootstrap() { sbf_programs: vec![], upgradeable_programs: vec![], limit_ledger_size: None, + use_surfpool: true, }) .await; diff --git a/forester/tests/test_compressible_mint.rs b/forester/tests/test_compressible_mint.rs index 9ace18f774..cb16391cc1 100644 --- a/forester/tests/test_compressible_mint.rs +++ b/forester/tests/test_compressible_mint.rs @@ -106,6 +106,7 @@ async fn test_compressible_mint_bootstrap() { sbf_programs: vec![], upgradeable_programs: vec![], limit_ledger_size: None, + use_surfpool: true, }) .await; @@ -261,6 +262,7 @@ async fn test_compressible_mint_compression() { sbf_programs: vec![], upgradeable_programs: vec![], limit_ledger_size: None, + use_surfpool: true, }) .await; @@ -436,6 +438,7 @@ async fn test_compressible_mint_subscription() { sbf_programs: vec![], upgradeable_programs: vec![], limit_ledger_size: None, + use_surfpool: true, }) .await; diff --git a/forester/tests/test_compressible_pda.rs b/forester/tests/test_compressible_pda.rs index 5e32ab4e67..d9a2030589 100644 --- a/forester/tests/test_compressible_pda.rs +++ b/forester/tests/test_compressible_pda.rs @@ -58,8 +58,8 @@ struct ForesterContext { } /// Register a forester for epoch 0 and wait for registration phase to complete -async fn register_forester( - rpc: &mut R, +async fn register_forester( + rpc: &mut LightClient, ) -> Result> { let forester_keypair = Keypair::new(); let forester_pubkey = forester_keypair.pubkey(); @@ -169,8 +169,10 @@ async fn register_forester( ) }; - while rpc.get_slot().await? < register_phase_start { - sleep(Duration::from_millis(400)).await; + if rpc.get_slot().await? < register_phase_start { + rpc.warp_to_slot(register_phase_start) + .await + .expect("warp_to_slot to registration phase"); } // Register for the target epoch @@ -191,8 +193,10 @@ async fn register_forester( println!("Registered for epoch {}", target_epoch); - while rpc.get_slot().await? < active_phase_start { - sleep(Duration::from_millis(400)).await; + if rpc.get_slot().await? < active_phase_start { + rpc.warp_to_slot(active_phase_start) + .await + .expect("warp_to_slot to active phase"); } println!("Active phase reached for epoch {}", target_epoch); @@ -266,6 +270,7 @@ async fn test_compressible_pda_bootstrap() { payer_pubkey_string(), )], limit_ledger_size: None, + use_surfpool: true, }) .await; @@ -456,6 +461,7 @@ async fn test_compressible_pda_compression() { payer_pubkey_string(), )], limit_ledger_size: None, + use_surfpool: true, }) .await; @@ -690,6 +696,7 @@ async fn test_compressible_pda_subscription() { payer_pubkey_string(), )], limit_ledger_size: None, + use_surfpool: true, }) .await; diff --git a/forester/tests/test_utils.rs b/forester/tests/test_utils.rs index 1ae2f159d7..c77d417a64 100644 --- a/forester/tests/test_utils.rs +++ b/forester/tests/test_utils.rs @@ -362,6 +362,17 @@ pub async fn get_next_active_phase_with_time( #[allow(dead_code)] pub async fn wait_for_slot(rpc: &mut LightClient, target_slot: u64) { + // Try surfpool's instant time-travel first; fall back to polling if not + // running against surfpool. + match rpc.warp_to_slot(target_slot).await { + Ok(_) => { + println!("warped to slot {}", target_slot); + return; + } + Err(e) => { + println!("warp_to_slot unavailable ({}), falling back to polling", e); + } + } while rpc.get_slot().await.unwrap() < target_slot { println!( "waiting for active phase slot: {}, current slot: {}", diff --git a/js/compressed-token/tests/e2e/approve-and-mint-to.test.ts b/js/compressed-token/tests/e2e/approve-and-mint-to.test.ts index fe52f5d701..7da24e7db2 100644 --- a/js/compressed-token/tests/e2e/approve-and-mint-to.test.ts +++ b/js/compressed-token/tests/e2e/approve-and-mint-to.test.ts @@ -14,12 +14,11 @@ import { dedupeSigner, newAccountWithLamports, sendAndConfirmTx, - getTestRpc, + createRpc, defaultTestStateTreeAccounts, TreeInfo, selectStateTreeInfo, } from '@lightprotocol/stateless.js'; -import { WasmFactory } from '@lightprotocol/hasher.rs'; import BN from 'bn.js'; import { getTokenPoolInfos, @@ -75,8 +74,7 @@ describe('approveAndMintTo', () => { let stateTreeInfo: TreeInfo; beforeAll(async () => { - const lightWasm = await WasmFactory.getInstance(); - rpc = await getTestRpc(lightWasm); + rpc = createRpc(); payer = await newAccountWithLamports(rpc); bob = Keypair.generate().publicKey; mintAuthority = Keypair.generate(); diff --git a/js/compressed-token/tests/e2e/compress-spl-token-account.test.ts b/js/compressed-token/tests/e2e/compress-spl-token-account.test.ts index 34ac9a55ce..f6fc5c66a5 100644 --- a/js/compressed-token/tests/e2e/compress-spl-token-account.test.ts +++ b/js/compressed-token/tests/e2e/compress-spl-token-account.test.ts @@ -5,7 +5,7 @@ import { bn, defaultTestStateTreeAccounts, newAccountWithLamports, - getTestRpc, + createRpc, TreeInfo, selectStateTreeInfo, } from '@lightprotocol/stateless.js'; @@ -20,7 +20,6 @@ import { mintToChecked, TOKEN_2022_PROGRAM_ID, } from '@solana/spl-token'; -import { WasmFactory } from '@lightprotocol/hasher.rs'; import { getTokenPoolInfos, selectTokenPoolInfo, @@ -40,8 +39,7 @@ describe('compressSplTokenAccount', () => { let tokenPoolInfo: TokenPoolInfo; beforeAll(async () => { - const lightWasm = await WasmFactory.getInstance(); - rpc = await getTestRpc(lightWasm); + rpc = createRpc(); payer = await newAccountWithLamports(rpc, 1e9); mintAuthority = Keypair.generate(); diff --git a/js/compressed-token/tests/e2e/compress.test.ts b/js/compressed-token/tests/e2e/compress.test.ts index e72c6ec640..736d8868a2 100644 --- a/js/compressed-token/tests/e2e/compress.test.ts +++ b/js/compressed-token/tests/e2e/compress.test.ts @@ -14,7 +14,7 @@ import { dedupeSigner, buildAndSignTx, sendAndConfirmTx, - getTestRpc, + createRpc, TreeInfo, selectStateTreeInfo, } from '@lightprotocol/stateless.js'; @@ -30,7 +30,6 @@ import { TOKEN_2022_PROGRAM_ID, } from '@solana/spl-token'; import { CompressedTokenProgram } from '../../src/program'; -import { WasmFactory } from '@lightprotocol/hasher.rs'; import { getTokenPoolInfos, selectTokenPoolInfo, @@ -109,8 +108,7 @@ describe('compress', () => { ); beforeAll(async () => { - const lightWasm = await WasmFactory.getInstance(); - rpc = await getTestRpc(lightWasm); + rpc = createRpc(); payer = await newAccountWithLamports(rpc, 1e9); mintAuthority = Keypair.generate(); diff --git a/js/compressed-token/tests/e2e/compressible-load.test.ts b/js/compressed-token/tests/e2e/compressible-load.test.ts index c44e96bfd6..cde591c3b6 100644 --- a/js/compressed-token/tests/e2e/compressible-load.test.ts +++ b/js/compressed-token/tests/e2e/compressible-load.test.ts @@ -4,7 +4,7 @@ import { Rpc, bn, newAccountWithLamports, - getTestRpc, + createRpc, selectStateTreeInfo, TreeInfo, MerkleContext, @@ -12,7 +12,6 @@ import { featureFlags, CTOKEN_PROGRAM_ID, } from '@lightprotocol/stateless.js'; -import { WasmFactory } from '@lightprotocol/hasher.rs'; import { createMint, mintTo } from '../../src/actions'; import { getTokenPoolInfos, @@ -43,8 +42,7 @@ describe('compressible-load', () => { let tokenPoolInfos: TokenPoolInfo[]; beforeAll(async () => { - const lightWasm = await WasmFactory.getInstance(); - rpc = await getTestRpc(lightWasm); + rpc = createRpc(); payer = await newAccountWithLamports(rpc, 10e9); mintAuthority = Keypair.generate(); const mintKeypair = Keypair.generate(); diff --git a/js/compressed-token/tests/e2e/create-mint.test.ts b/js/compressed-token/tests/e2e/create-mint.test.ts index c43e46af96..e419326885 100644 --- a/js/compressed-token/tests/e2e/create-mint.test.ts +++ b/js/compressed-token/tests/e2e/create-mint.test.ts @@ -6,9 +6,8 @@ import { createMint } from '../../src/actions'; import { Rpc, newAccountWithLamports, - getTestRpc, + createRpc, } from '@lightprotocol/stateless.js'; -import { WasmFactory } from '@lightprotocol/hasher.rs'; /** * Asserts that createMint() creates a new spl mint account + the respective @@ -52,8 +51,7 @@ describe('createMint (SPL)', () => { let mintAuthority: Keypair; beforeAll(async () => { - const lightWasm = await WasmFactory.getInstance(); - rpc = await getTestRpc(lightWasm); + rpc = createRpc(); payer = await newAccountWithLamports(rpc, 1e9); }); diff --git a/js/compressed-token/tests/e2e/create-token-pool.test.ts b/js/compressed-token/tests/e2e/create-token-pool.test.ts index a0ff075550..66f3658529 100644 --- a/js/compressed-token/tests/e2e/create-token-pool.test.ts +++ b/js/compressed-token/tests/e2e/create-token-pool.test.ts @@ -15,9 +15,8 @@ import { dedupeSigner, newAccountWithLamports, sendAndConfirmTx, - getTestRpc, + createRpc, } from '@lightprotocol/stateless.js'; -import { WasmFactory } from '@lightprotocol/hasher.rs'; import { TOKEN_2022_PROGRAM_ID } from '@solana/spl-token'; import { getTokenPoolInfos } from '../../src/utils'; @@ -104,8 +103,7 @@ describe('createTokenPool', () => { let mintAuthority: Keypair; beforeAll(async () => { - const lightWasm = await WasmFactory.getInstance(); - rpc = await getTestRpc(lightWasm); + rpc = createRpc(); payer = await newAccountWithLamports(rpc); mintAuthority = Keypair.generate(); mintKeypair = Keypair.generate(); diff --git a/js/compressed-token/tests/e2e/decompress-delegated.test.ts b/js/compressed-token/tests/e2e/decompress-delegated.test.ts index f1b62f65e2..caa5cdb2b5 100644 --- a/js/compressed-token/tests/e2e/decompress-delegated.test.ts +++ b/js/compressed-token/tests/e2e/decompress-delegated.test.ts @@ -5,12 +5,11 @@ import { Rpc, bn, newAccountWithLamports, - getTestRpc, + createRpc, TreeInfo, selectStateTreeInfo, ParsedTokenAccount, } from '@lightprotocol/stateless.js'; -import { WasmFactory } from '@lightprotocol/hasher.rs'; import { createMint, mintTo, @@ -107,8 +106,7 @@ describe('decompressDelegated', () => { let tokenPoolInfos: TokenPoolInfo[]; beforeAll(async () => { - const lightWasm = await WasmFactory.getInstance(); - rpc = await getTestRpc(lightWasm); + rpc = createRpc(); payer = await newAccountWithLamports(rpc, 1e9); bob = await newAccountWithLamports(rpc, 1e9); charlie = await newAccountWithLamports(rpc, 1e9); diff --git a/js/compressed-token/tests/e2e/decompress.test.ts b/js/compressed-token/tests/e2e/decompress.test.ts index b3ec1400ca..309de98a55 100644 --- a/js/compressed-token/tests/e2e/decompress.test.ts +++ b/js/compressed-token/tests/e2e/decompress.test.ts @@ -7,11 +7,10 @@ import { bn, defaultTestStateTreeAccounts, newAccountWithLamports, - getTestRpc, + createRpc, selectStateTreeInfo, TreeInfo, } from '@lightprotocol/stateless.js'; -import { WasmFactory } from '@lightprotocol/hasher.rs'; import { createMint, mintTo, decompress } from '../../src/actions'; import { createAssociatedTokenAccount } from '@solana/spl-token'; import { @@ -77,8 +76,7 @@ describe('decompress', () => { let tokenPoolInfos: TokenPoolInfo[]; beforeAll(async () => { - const lightWasm = await WasmFactory.getInstance(); - rpc = await getTestRpc(lightWasm); + rpc = createRpc(); payer = await newAccountWithLamports(rpc, 1e9); bob = await newAccountWithLamports(rpc, 1e9); charlie = await newAccountWithLamports(rpc, 1e9); @@ -119,8 +117,7 @@ describe('decompress', () => { const LOOP = 10; it(`should decompress from bob -> charlieAta ${LOOP} times`, async () => { - const lightWasm = await WasmFactory.getInstance(); - rpc = await getTestRpc(lightWasm); + rpc = createRpc(); for (let i = 0; i < LOOP; i++) { const recipientAtaBalanceBefore = await rpc.getTokenAccountBalance(charlieAta); diff --git a/js/compressed-token/tests/e2e/decompress2.test.ts b/js/compressed-token/tests/e2e/decompress2.test.ts index 57a5b7b161..7b2b71b6b7 100644 --- a/js/compressed-token/tests/e2e/decompress2.test.ts +++ b/js/compressed-token/tests/e2e/decompress2.test.ts @@ -4,13 +4,12 @@ import { Rpc, bn, newAccountWithLamports, - getTestRpc, + createRpc, selectStateTreeInfo, TreeInfo, VERSION, featureFlags, } from '@lightprotocol/stateless.js'; -import { WasmFactory } from '@lightprotocol/hasher.rs'; import { getAssociatedTokenAddress, TOKEN_PROGRAM_ID } from '@solana/spl-token'; import { createMint, mintTo } from '../../src/actions'; import { @@ -36,8 +35,7 @@ describe('decompressInterface', () => { let tokenPoolInfos: TokenPoolInfo[]; beforeAll(async () => { - const lightWasm = await WasmFactory.getInstance(); - rpc = await getTestRpc(lightWasm); + rpc = createRpc(); payer = await newAccountWithLamports(rpc, 10e9); mintAuthority = Keypair.generate(); const mintKeypair = Keypair.generate(); diff --git a/js/compressed-token/tests/e2e/delegate.test.ts b/js/compressed-token/tests/e2e/delegate.test.ts index 7505b16bc0..723487a7a3 100644 --- a/js/compressed-token/tests/e2e/delegate.test.ts +++ b/js/compressed-token/tests/e2e/delegate.test.ts @@ -5,12 +5,11 @@ import { Rpc, bn, newAccountWithLamports, - getTestRpc, + createRpc, TreeInfo, selectStateTreeInfo, ParsedTokenAccount, } from '@lightprotocol/stateless.js'; -import { WasmFactory } from '@lightprotocol/hasher.rs'; import { createMint, mintTo, @@ -113,8 +112,7 @@ describe('delegate', () => { let tokenPoolInfo: TokenPoolInfo; beforeAll(async () => { - const lightWasm = await WasmFactory.getInstance(); - rpc = await getTestRpc(lightWasm); + rpc = createRpc(); payer = await newAccountWithLamports(rpc, 1e9); mintAuthority = Keypair.generate(); const mintKeypair = Keypair.generate(); diff --git a/js/compressed-token/tests/e2e/get-account-interface.test.ts b/js/compressed-token/tests/e2e/get-account-interface.test.ts index e7e97876bb..6a3961363c 100644 --- a/js/compressed-token/tests/e2e/get-account-interface.test.ts +++ b/js/compressed-token/tests/e2e/get-account-interface.test.ts @@ -4,14 +4,13 @@ import { Rpc, bn, newAccountWithLamports, - getTestRpc, + createRpc, selectStateTreeInfo, TreeInfo, VERSION, featureFlags, CTOKEN_PROGRAM_ID, } from '@lightprotocol/stateless.js'; -import { WasmFactory } from '@lightprotocol/hasher.rs'; import { createMint as createSplMint, getOrCreateAssociatedTokenAccount, @@ -60,8 +59,7 @@ describe('get-account-interface', () => { let t22MintAuthority: Keypair; beforeAll(async () => { - const lightWasm = await WasmFactory.getInstance(); - rpc = await getTestRpc(lightWasm); + rpc = createRpc(); payer = await newAccountWithLamports(rpc, 10e9); mintAuthority = Keypair.generate(); stateTreeInfo = selectStateTreeInfo(await rpc.getStateTreeInfos()); diff --git a/js/compressed-token/tests/e2e/merge-token-accounts.test.ts b/js/compressed-token/tests/e2e/merge-token-accounts.test.ts index 06338f9e40..e39c36bfeb 100644 --- a/js/compressed-token/tests/e2e/merge-token-accounts.test.ts +++ b/js/compressed-token/tests/e2e/merge-token-accounts.test.ts @@ -5,11 +5,10 @@ import { bn, defaultTestStateTreeAccounts, newAccountWithLamports, - getTestRpc, + createRpc, TreeInfo, selectStateTreeInfo, } from '@lightprotocol/stateless.js'; -import { WasmFactory } from '@lightprotocol/hasher.rs'; import { createMint, mintTo, mergeTokenAccounts } from '../../src/actions'; @@ -22,8 +21,7 @@ describe('mergeTokenAccounts', () => { let stateTreeInfo: TreeInfo; beforeAll(async () => { - const lightWasm = await WasmFactory.getInstance(); - rpc = await getTestRpc(lightWasm); + rpc = createRpc(); payer = await newAccountWithLamports(rpc, 1e9); mintAuthority = Keypair.generate(); const mintKeypair = Keypair.generate(); diff --git a/js/compressed-token/tests/e2e/mint-to-interface.test.ts b/js/compressed-token/tests/e2e/mint-to-interface.test.ts index 697a09cea2..55810b05ba 100644 --- a/js/compressed-token/tests/e2e/mint-to-interface.test.ts +++ b/js/compressed-token/tests/e2e/mint-to-interface.test.ts @@ -4,7 +4,6 @@ import { Rpc, newAccountWithLamports, createRpc, - getTestRpc, VERSION, featureFlags, CTOKEN_PROGRAM_ID, @@ -15,7 +14,6 @@ import { TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID, } from '@solana/spl-token'; -import { WasmFactory } from '@lightprotocol/hasher.rs'; import { createMintInterface } from '../../src/v3/actions/create-mint-interface'; import { mintToInterface } from '../../src/v3/actions/mint-to-interface'; import { createMint } from '../../src/actions/create-mint'; @@ -34,8 +32,7 @@ describe('mintToInterface - SPL Mints', () => { let mintAuthority: Keypair; beforeAll(async () => { - const lightWasm = await WasmFactory.getInstance(); - rpc = await getTestRpc(lightWasm); + rpc = createRpc(); payer = await newAccountWithLamports(rpc, 10e9); mintAuthority = Keypair.generate(); @@ -341,8 +338,7 @@ describe('mintToInterface - Token-2022 Mints', () => { let mintAuthority: Keypair; beforeAll(async () => { - const lightWasm = await WasmFactory.getInstance(); - rpc = await getTestRpc(lightWasm); + rpc = createRpc(); payer = await newAccountWithLamports(rpc, 10e9); mintAuthority = Keypair.generate(); diff --git a/js/compressed-token/tests/e2e/mint-to.test.ts b/js/compressed-token/tests/e2e/mint-to.test.ts index 57e7b80483..ab7718a4e2 100644 --- a/js/compressed-token/tests/e2e/mint-to.test.ts +++ b/js/compressed-token/tests/e2e/mint-to.test.ts @@ -19,13 +19,12 @@ import { sendAndConfirmTx, buildAndSignTx, dedupeSigner, - getTestRpc, + createRpc, TreeInfo, selectStateTreeInfo, } from '@lightprotocol/stateless.js'; import { CompressedTokenProgram } from '../../src/program'; -import { WasmFactory } from '@lightprotocol/hasher.rs'; import { getTokenPoolInfos, selectTokenPoolInfo, @@ -71,8 +70,7 @@ describe('mintTo', () => { let tokenPoolInfo: TokenPoolInfo; beforeAll(async () => { - const lightWasm = await WasmFactory.getInstance(); - rpc = await getTestRpc(lightWasm); + rpc = createRpc(); payer = await newAccountWithLamports(rpc); bob = getTestKeypair(); mintAuthority = payer as Keypair; diff --git a/js/compressed-token/tests/e2e/multi-pool.test.ts b/js/compressed-token/tests/e2e/multi-pool.test.ts index e25a52538e..1185de63e6 100644 --- a/js/compressed-token/tests/e2e/multi-pool.test.ts +++ b/js/compressed-token/tests/e2e/multi-pool.test.ts @@ -21,10 +21,9 @@ import { dedupeSigner, newAccountWithLamports, sendAndConfirmTx, - getTestRpc, + createRpc, selectStateTreeInfo, } from '@lightprotocol/stateless.js'; -import { WasmFactory } from '@lightprotocol/hasher.rs'; import { TOKEN_2022_PROGRAM_ID } from '@solana/spl-token'; import { getTokenPoolInfos, @@ -81,8 +80,7 @@ describe('multi-pool', () => { let charlieAta: PublicKey; beforeAll(async () => { - const lightWasm = await WasmFactory.getInstance(); - rpc = await getTestRpc(lightWasm); + rpc = createRpc(); payer = await newAccountWithLamports(rpc); mintAuthority = Keypair.generate(); mintKeypair = Keypair.generate(); diff --git a/js/compressed-token/tests/e2e/payment-flows.test.ts b/js/compressed-token/tests/e2e/payment-flows.test.ts index 5f962c95b8..3d3525b595 100644 --- a/js/compressed-token/tests/e2e/payment-flows.test.ts +++ b/js/compressed-token/tests/e2e/payment-flows.test.ts @@ -15,7 +15,7 @@ import { Rpc, bn, newAccountWithLamports, - getTestRpc, + createRpc, selectStateTreeInfo, TreeInfo, VERSION, @@ -24,7 +24,6 @@ import { buildAndSignTx, sendAndConfirmTx, } from '@lightprotocol/stateless.js'; -import { WasmFactory } from '@lightprotocol/hasher.rs'; import { createMint, mintTo } from '../../src/actions'; import { getTokenPoolInfos, @@ -55,8 +54,7 @@ describe('Payment Flows', () => { let tokenPoolInfos: TokenPoolInfo[]; beforeAll(async () => { - const lightWasm = await WasmFactory.getInstance(); - rpc = await getTestRpc(lightWasm); + rpc = createRpc(); payer = await newAccountWithLamports(rpc, 10e9); mintAuthority = Keypair.generate(); const mintKeypair = Keypair.generate(); diff --git a/js/compressed-token/tests/e2e/rpc-token-interop.test.ts b/js/compressed-token/tests/e2e/rpc-token-interop.test.ts index 6bdcdfc7c1..4242b684ba 100644 --- a/js/compressed-token/tests/e2e/rpc-token-interop.test.ts +++ b/js/compressed-token/tests/e2e/rpc-token-interop.test.ts @@ -5,12 +5,10 @@ import { newAccountWithLamports, bn, createRpc, - getTestRpc, defaultTestStateTreeAccounts, TreeInfo, selectStateTreeInfo, } from '@lightprotocol/stateless.js'; -import { WasmFactory } from '@lightprotocol/hasher.rs'; import { createMint, mintTo, transfer } from '../../src/actions'; import { getTokenPoolInfos, @@ -32,9 +30,8 @@ describe('rpc-interop token', () => { let tokenPoolInfo: TokenPoolInfo; beforeAll(async () => { - const lightWasm = await WasmFactory.getInstance(); rpc = createRpc(); - testRpc = await getTestRpc(lightWasm); + testRpc = createRpc(); payer = await newAccountWithLamports(rpc); bob = await newAccountWithLamports(rpc); charlie = await newAccountWithLamports(rpc); diff --git a/js/compressed-token/tests/e2e/transfer-delegated.test.ts b/js/compressed-token/tests/e2e/transfer-delegated.test.ts index be2d66d2da..f1b637fc4e 100644 --- a/js/compressed-token/tests/e2e/transfer-delegated.test.ts +++ b/js/compressed-token/tests/e2e/transfer-delegated.test.ts @@ -5,12 +5,11 @@ import { Rpc, bn, newAccountWithLamports, - getTestRpc, + createRpc, TreeInfo, selectStateTreeInfo, ParsedTokenAccount, } from '@lightprotocol/stateless.js'; -import { WasmFactory } from '@lightprotocol/hasher.rs'; import { createMint, mintTo, @@ -176,8 +175,7 @@ describe('transferDelegated', () => { let tokenPoolInfo: TokenPoolInfo; beforeAll(async () => { - const lightWasm = await WasmFactory.getInstance(); - rpc = await getTestRpc(lightWasm); + rpc = createRpc(); payer = await newAccountWithLamports(rpc, 1e9); bob = await newAccountWithLamports(rpc, 1e9); mintAuthority = Keypair.generate(); diff --git a/js/compressed-token/tests/e2e/transfer-interface.test.ts b/js/compressed-token/tests/e2e/transfer-interface.test.ts index c50edfa5e0..07385932f0 100644 --- a/js/compressed-token/tests/e2e/transfer-interface.test.ts +++ b/js/compressed-token/tests/e2e/transfer-interface.test.ts @@ -4,14 +4,13 @@ import { Rpc, bn, newAccountWithLamports, - getTestRpc, + createRpc, selectStateTreeInfo, TreeInfo, CTOKEN_PROGRAM_ID, VERSION, featureFlags, } from '@lightprotocol/stateless.js'; -import { WasmFactory } from '@lightprotocol/hasher.rs'; import { createMint, mintTo } from '../../src/actions'; import { getTokenPoolInfos, @@ -43,8 +42,7 @@ describe('transfer-interface', () => { let tokenPoolInfos: TokenPoolInfo[]; beforeAll(async () => { - const lightWasm = await WasmFactory.getInstance(); - rpc = await getTestRpc(lightWasm); + rpc = createRpc(); payer = await newAccountWithLamports(rpc, 10e9); mintAuthority = Keypair.generate(); const mintKeypair = Keypair.generate(); diff --git a/js/compressed-token/tests/e2e/transfer.test.ts b/js/compressed-token/tests/e2e/transfer.test.ts index 98379e5174..d32d0b7e90 100644 --- a/js/compressed-token/tests/e2e/transfer.test.ts +++ b/js/compressed-token/tests/e2e/transfer.test.ts @@ -11,15 +11,13 @@ import { Rpc, bn, newAccountWithLamports, - getTestRpc, - TestRpc, + createRpc, dedupeSigner, buildAndSignTx, sendAndConfirmTx, TreeInfo, selectStateTreeInfo, } from '@lightprotocol/stateless.js'; -import { WasmFactory } from '@lightprotocol/hasher.rs'; import { createMint, mintTo, transfer } from '../../src/actions'; import { TOKEN_2022_PROGRAM_ID } from '@solana/spl-token'; import { CompressedTokenProgram } from '../../src/program'; @@ -90,7 +88,7 @@ async function assertTransfer( const TEST_TOKEN_DECIMALS = 2; describe('transfer', () => { - let rpc: TestRpc | Rpc; + let rpc: Rpc; let payer: Signer; let bob: Signer; let charlie: Signer; @@ -100,9 +98,7 @@ describe('transfer', () => { let stateTreeInfo: TreeInfo; beforeAll(async () => { - const lightWasm = await WasmFactory.getInstance(); - rpc = await getTestRpc(lightWasm); - // rpc = createRpc(); + rpc = createRpc(); payer = await newAccountWithLamports(rpc, 1e9); mintAuthority = Keypair.generate(); const mintKeypair = Keypair.generate(); @@ -297,7 +293,7 @@ describe('e2e transfer with multiple accounts', () => { let stateTreeInfo: TreeInfo; beforeAll(async () => { - rpc = await getTestRpc(await WasmFactory.getInstance()); + rpc = createRpc(); payer = await newAccountWithLamports(rpc, 1e9); mintAuthority = Keypair.generate(); const mintKeypair = Keypair.generate(); diff --git a/js/compressed-token/tests/e2e/unwrap.test.ts b/js/compressed-token/tests/e2e/unwrap.test.ts index 97ad4d811d..806ac5f1a8 100644 --- a/js/compressed-token/tests/e2e/unwrap.test.ts +++ b/js/compressed-token/tests/e2e/unwrap.test.ts @@ -4,13 +4,12 @@ import { Rpc, bn, newAccountWithLamports, - getTestRpc, + createRpc, selectStateTreeInfo, TreeInfo, VERSION, featureFlags, } from '@lightprotocol/stateless.js'; -import { WasmFactory } from '@lightprotocol/hasher.rs'; import { createMint, mintTo } from '../../src/actions'; import { getAssociatedTokenAddressSync, @@ -51,8 +50,7 @@ describe('createUnwrapInstruction', () => { let tokenPoolInfos: TokenPoolInfo[]; beforeAll(async () => { - const lightWasm = await WasmFactory.getInstance(); - rpc = await getTestRpc(lightWasm); + rpc = createRpc(); payer = await newAccountWithLamports(rpc, 10e9); mintAuthority = Keypair.generate(); const mintKeypair = Keypair.generate(); @@ -147,8 +145,7 @@ describe('unwrap action', () => { let tokenPoolInfos: TokenPoolInfo[]; beforeAll(async () => { - const lightWasm = await WasmFactory.getInstance(); - rpc = await getTestRpc(lightWasm); + rpc = createRpc(); payer = await newAccountWithLamports(rpc, 10e9); mintAuthority = Keypair.generate(); const mintKeypair = Keypair.generate(); @@ -471,8 +468,7 @@ describe('unwrap Token-2022', () => { let stateTreeInfo: TreeInfo; beforeAll(async () => { - const lightWasm = await WasmFactory.getInstance(); - rpc = await getTestRpc(lightWasm); + rpc = createRpc(); payer = await newAccountWithLamports(rpc, 10e9); stateTreeInfo = selectStateTreeInfo(await rpc.getStateTreeInfos()); }, 60_000); diff --git a/js/compressed-token/tests/e2e/wrap.test.ts b/js/compressed-token/tests/e2e/wrap.test.ts index 4a8f67ed6c..7801594db4 100644 --- a/js/compressed-token/tests/e2e/wrap.test.ts +++ b/js/compressed-token/tests/e2e/wrap.test.ts @@ -4,14 +4,13 @@ import { Rpc, bn, newAccountWithLamports, - getTestRpc, + createRpc, selectStateTreeInfo, TreeInfo, CTOKEN_PROGRAM_ID, VERSION, featureFlags, } from '@lightprotocol/stateless.js'; -import { WasmFactory } from '@lightprotocol/hasher.rs'; import { createMint, mintTo, decompress } from '../../src/actions'; import { createAssociatedTokenAccount, @@ -56,8 +55,7 @@ describe('createWrapInstruction', () => { let tokenPoolInfos: TokenPoolInfo[]; beforeAll(async () => { - const lightWasm = await WasmFactory.getInstance(); - rpc = await getTestRpc(lightWasm); + rpc = createRpc(); payer = await newAccountWithLamports(rpc, 10e9); mintAuthority = Keypair.generate(); const mintKeypair = Keypair.generate(); @@ -188,8 +186,7 @@ describe('wrap action', () => { let tokenPoolInfos: TokenPoolInfo[]; beforeAll(async () => { - const lightWasm = await WasmFactory.getInstance(); - rpc = await getTestRpc(lightWasm); + rpc = createRpc(); payer = await newAccountWithLamports(rpc, 10e9); mintAuthority = Keypair.generate(); const mintKeypair = Keypair.generate(); @@ -486,8 +483,7 @@ describe('wrap with non-ATA accounts', () => { let tokenPoolInfos: TokenPoolInfo[]; beforeAll(async () => { - const lightWasm = await WasmFactory.getInstance(); - rpc = await getTestRpc(lightWasm); + rpc = createRpc(); payer = await newAccountWithLamports(rpc, 10e9); mintAuthority = Keypair.generate(); const mintKeypair = Keypair.generate(); @@ -577,8 +573,7 @@ describe('wrap Token-2022 to CToken', () => { let stateTreeInfo: TreeInfo; beforeAll(async () => { - const lightWasm = await WasmFactory.getInstance(); - rpc = await getTestRpc(lightWasm); + rpc = createRpc(); payer = await newAccountWithLamports(rpc, 10e9); stateTreeInfo = selectStateTreeInfo(await rpc.getStateTreeInfos()); }, 60_000); diff --git a/js/stateless.js/tests/e2e/compress.test.ts b/js/stateless.js/tests/e2e/compress.test.ts index 4592aca688..66155756de 100644 --- a/js/stateless.js/tests/e2e/compress.test.ts +++ b/js/stateless.js/tests/e2e/compress.test.ts @@ -9,7 +9,7 @@ import { featureFlags, } from '../../src/constants'; import { newAccountWithLamports } from '../../src/test-helpers/test-utils'; -import { Rpc } from '../../src/rpc'; +import { Rpc, createRpc } from '../../src/rpc'; import { LightSystemProgram, TreeInfo, @@ -20,8 +20,6 @@ import { decompress, selectStateTreeInfo, } from '../../src'; -import { TestRpc, getTestRpc } from '../../src/test-helpers/test-rpc'; -import { WasmFactory } from '@lightprotocol/hasher.rs'; /// TODO: make available to developers via utils function txFees( @@ -83,8 +81,7 @@ describe('compress', () => { let stateTreeInfo: TreeInfo; beforeAll(async () => { - const lightWasm = await WasmFactory.getInstance(); - rpc = await getTestRpc(lightWasm); + rpc = createRpc(); payer = await newAccountWithLamports(rpc, 1e9, 256); stateTreeInfo = selectStateTreeInfo(await rpc.getStateTreeInfos()); }); @@ -98,7 +95,7 @@ describe('compress', () => { ); await createAccount( - rpc as TestRpc, + rpc, payer, [ new Uint8Array([ @@ -114,7 +111,7 @@ describe('compress', () => { await expect( createAccountWithLamports( - rpc as TestRpc, + rpc, payer, [ new Uint8Array([ @@ -132,7 +129,7 @@ describe('compress', () => { // 0 lamports => 0 input accounts selected, so outputStateTreeInfo is required await createAccountWithLamports( - rpc as TestRpc, + rpc, payer, [ new Uint8Array([ @@ -148,7 +145,7 @@ describe('compress', () => { ); await createAccount( - rpc as TestRpc, + rpc, payer, [ new Uint8Array([ @@ -163,7 +160,7 @@ describe('compress', () => { ); await createAccount( - rpc as TestRpc, + rpc, payer, [ new Uint8Array([ @@ -178,7 +175,7 @@ describe('compress', () => { ); await expect( createAccount( - rpc as TestRpc, + rpc, payer, [ new Uint8Array([ @@ -245,7 +242,7 @@ describe('compress', () => { ); await createAccountWithLamports( - rpc as TestRpc, + rpc, payer, [ new Uint8Array([ diff --git a/js/stateless.js/tests/e2e/test-rpc.test.ts b/js/stateless.js/tests/e2e/test-rpc.test.ts index 6a5b137299..fb0825131c 100644 --- a/js/stateless.js/tests/e2e/test-rpc.test.ts +++ b/js/stateless.js/tests/e2e/test-rpc.test.ts @@ -9,13 +9,14 @@ import { import { newAccountWithLamports } from '../../src/test-helpers/test-utils'; import { compress, decompress, transfer } from '../../src/actions'; import { bn, CompressedAccountWithMerkleContext } from '../../src/state'; -import { getTestRpc, TestRpc } from '../../src/test-helpers/test-rpc'; +import { Rpc } from '../../src/rpc'; +import { getTestRpc } from '../../src/test-helpers/test-rpc/test-rpc'; import { WasmFactory } from '@lightprotocol/hasher.rs'; /// TODO: add test case for payer != address describe('test-rpc', () => { const { merkleTree } = defaultTestStateTreeAccounts(); - let rpc: TestRpc; + let rpc: Rpc; let payer: Signer; let preCompressBalance: number; diff --git a/js/stateless.js/tests/e2e/transfer.test.ts b/js/stateless.js/tests/e2e/transfer.test.ts index b2fbcb6997..4c5db7d87a 100644 --- a/js/stateless.js/tests/e2e/transfer.test.ts +++ b/js/stateless.js/tests/e2e/transfer.test.ts @@ -1,11 +1,9 @@ import { describe, it, assert, beforeAll } from 'vitest'; import { Signer } from '@solana/web3.js'; import { newAccountWithLamports } from '../../src/test-helpers/test-utils'; -import { Rpc } from '../../src/rpc'; +import { Rpc, createRpc } from '../../src/rpc'; import { bn, compress } from '../../src'; import { transfer } from '../../src/actions/transfer'; -import { getTestRpc } from '../../src/test-helpers/test-rpc'; -import { WasmFactory } from '@lightprotocol/hasher.rs'; describe('transfer', () => { let rpc: Rpc; @@ -13,8 +11,7 @@ describe('transfer', () => { let bob: Signer; beforeAll(async () => { - const lightWasm = await WasmFactory.getInstance(); - rpc = await getTestRpc(lightWasm); + rpc = createRpc(); payer = await newAccountWithLamports(rpc, 2e9, 256); bob = await newAccountWithLamports(rpc, 2e9, 256); diff --git a/justfile b/justfile index e3624721d5..cb686dc191 100644 --- a/justfile +++ b/justfile @@ -36,7 +36,7 @@ lint: lint-rust js::lint lint-rust: cargo +nightly fmt --all -- --check - cargo clippy --workspace --all-features -- -D warnings + cargo clippy --workspace --all-features --all-targets -- -D warnings format: cargo +nightly fmt --all diff --git a/program-tests/compressed-token-test/tests/v1.rs b/program-tests/compressed-token-test/tests/v1.rs index 9c4f55d2af..5b9072b0c2 100644 --- a/program-tests/compressed-token-test/tests/v1.rs +++ b/program-tests/compressed-token-test/tests/v1.rs @@ -4892,6 +4892,7 @@ async fn test_transfer_with_photon_and_batched_tree() { sbf_programs: vec![], upgradeable_programs: vec![], limit_ledger_size: None, + use_surfpool: true, }) .await; diff --git a/program-tests/system-cpi-v2-test/tests/event.rs b/program-tests/system-cpi-v2-test/tests/event.rs index 9ed2aae062..d25554354c 100644 --- a/program-tests/system-cpi-v2-test/tests/event.rs +++ b/program-tests/system-cpi-v2-test/tests/event.rs @@ -539,6 +539,7 @@ async fn generate_photon_test_data_multiple_events() { )], upgradeable_programs: vec![], limit_ledger_size: None, + use_surfpool: true, }) .await; diff --git a/sdk-libs/client/Cargo.toml b/sdk-libs/client/Cargo.toml index fa614d97fe..eac878dc51 100644 --- a/sdk-libs/client/Cargo.toml +++ b/sdk-libs/client/Cargo.toml @@ -66,9 +66,11 @@ num-bigint = { workspace = true } base64 = { workspace = true } bs58 = { workspace = true } tokio = { workspace = true, features = ["rt", "time"] } +reqwest = { workspace = true } futures = { workspace = true } smallvec = { workspace = true } +serde_json = { workspace = true } tracing = { workspace = true } lazy_static = { workspace = true } rand = { workspace = true } diff --git a/sdk-libs/client/README.md b/sdk-libs/client/README.md index dca731877c..8c46cbc68c 100644 --- a/sdk-libs/client/README.md +++ b/sdk-libs/client/README.md @@ -44,6 +44,7 @@ async fn main() -> Result<(), Box> { sbf_programs: vec![], upgradeable_programs: vec![], limit_ledger_size: None, + use_surfpool: true, }; spawn_validator(config).await; diff --git a/sdk-libs/client/src/lib.rs b/sdk-libs/client/src/lib.rs index c8e4c9fcca..3d228f3de8 100644 --- a/sdk-libs/client/src/lib.rs +++ b/sdk-libs/client/src/lib.rs @@ -42,6 +42,7 @@ //! sbf_programs: vec![], //! upgradeable_programs: vec![], //! limit_ledger_size: None, +//! use_surfpool: true, //! }; //! spawn_validator(config).await; //! diff --git a/sdk-libs/client/src/local_test_validator.rs b/sdk-libs/client/src/local_test_validator.rs index b0b7dfbcbc..a6413aa591 100644 --- a/sdk-libs/client/src/local_test_validator.rs +++ b/sdk-libs/client/src/local_test_validator.rs @@ -13,6 +13,8 @@ pub struct LightValidatorConfig { /// Use this when the program needs a valid upgrade authority (e.g., for compression config) pub upgradeable_programs: Vec<(String, String, String)>, pub limit_ledger_size: Option, + /// Use surfpool instead of solana-test-validator + pub use_surfpool: bool, } impl Default for LightValidatorConfig { @@ -24,6 +26,7 @@ impl Default for LightValidatorConfig { sbf_programs: vec![], upgradeable_programs: vec![], limit_ledger_size: None, + use_surfpool: true, } } } @@ -58,20 +61,37 @@ pub async fn spawn_validator(config: LightValidatorConfig) { path.push_str(" --skip-prover"); } - println!("Starting validator with command: {}", path); - - let child = Command::new("sh") - .arg("-c") - .arg(path) - .stdin(Stdio::null()) // Detach from stdin - .stdout(Stdio::null()) // Detach from stdout - .stderr(Stdio::null()) // Detach from stderr - .spawn() - .expect("Failed to start server process"); + if config.use_surfpool { + path.push_str(" --use-surfpool"); + } - // Explicitly `drop` the process to ensure we don't wait on it - std::mem::drop(child); + println!("Starting validator with command: {}", path); - tokio::time::sleep(tokio::time::Duration::from_secs(config.wait_time)).await; + if config.use_surfpool { + // The CLI starts surfpool, prover, and photon, then exits once all + // services are ready. Wait for it to finish so we know everything + // is up before the test proceeds. + let mut child = Command::new("sh") + .arg("-c") + .arg(path) + .stdin(Stdio::null()) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) + .spawn() + .expect("Failed to start server process"); + let status = child.wait().expect("Failed to wait for CLI process"); + assert!(status.success(), "CLI exited with error: {}", status); + } else { + let child = Command::new("sh") + .arg("-c") + .arg(path) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn() + .expect("Failed to start server process"); + std::mem::drop(child); + tokio::time::sleep(tokio::time::Duration::from_secs(config.wait_time)).await; + } } } diff --git a/sdk-libs/client/src/rpc/client.rs b/sdk-libs/client/src/rpc/client.rs index 09dabfa7cb..2f5c5cca0f 100644 --- a/sdk-libs/client/src/rpc/client.rs +++ b/sdk-libs/client/src/rpc/client.rs @@ -430,6 +430,33 @@ impl LightClient { "Failed to find any parseable inner instructions".to_string(), )) } + + /// Instantly advances the validator to the given slot using surfpool's + /// `surfnet_timeTravel` RPC method. This is much faster than polling + /// `get_slot` in a loop and is intended for testing against surfpool. + /// + /// Returns the `EpochInfo` after the time travel, or an error if the + /// RPC call fails (e.g. when not running against surfpool). + pub async fn warp_to_slot(&self, slot: Slot) -> Result { + let url = self.client.url(); + let body = serde_json::json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "surfnet_timeTravel", + "params": [{ "absoluteSlot": slot }] + }); + let response = reqwest::Client::new() + .post(url) + .json(&body) + .send() + .await + .map_err(|e| RpcError::CustomError(format!("warp_to_slot failed: {e}")))?; + let result: serde_json::Value = response + .json() + .await + .map_err(|e| RpcError::CustomError(format!("warp_to_slot response error: {e}")))?; + Ok(result) + } } #[async_trait] diff --git a/sdk-tests/client-test/tests/light_client.rs b/sdk-tests/client-test/tests/light_client.rs index 22d799c888..9207e0f69c 100644 --- a/sdk-tests/client-test/tests/light_client.rs +++ b/sdk-tests/client-test/tests/light_client.rs @@ -55,6 +55,7 @@ async fn test_all_endpoints() { sbf_programs: vec![], upgradeable_programs: vec![], limit_ledger_size: None, + use_surfpool: true, }; spawn_validator(config).await;