From 0d0b4c79bace7e9ae38feb51e3f9d9d66df01cc9 Mon Sep 17 00:00:00 2001 From: Oliver Cai Date: Thu, 10 Sep 2026 10:43:34 -0400 Subject: [PATCH 1/4] readd whitelist --- solana/mainnet-swap-verify.js | 130 ++++++++++++++++++ .../programs/stable-swapper/src/constants.rs | 6 + solana/programs/stable-swapper/src/errors.rs | 4 +- solana/programs/stable-swapper/src/lib.rs | 17 ++- solana/programs/stable-swapper/src/state.rs | 17 ++- 5 files changed, 167 insertions(+), 7 deletions(-) create mode 100644 solana/mainnet-swap-verify.js diff --git a/solana/mainnet-swap-verify.js b/solana/mainnet-swap-verify.js new file mode 100644 index 0000000..9528b65 --- /dev/null +++ b/solana/mainnet-swap-verify.js @@ -0,0 +1,130 @@ +/* + * mainnet-swap-verify.js — post-migration smoke test for the stable-swapper pool. + * + * Executes a REAL, small USDC -> USDF swap on solana-mainnet to prove swaps are + * operational after the PR #9 upgrade + migrate. Moves real funds (a few cents). + * + * RUN ONLY AFTER the migration apply completes. + * + * Prereqs on the wallet (WALLET keypair): + * - a little SOL (~0.01) for fees + ATA rents + * - a small USDC balance (>= AMOUNT_USDC; e.g. 0.05 USDC) + * - USDC ATA exists automatically once funded; the script creates the USDF ATA + * if missing, and the program init_if_needed-creates the fee recipient's USDC ATA. + * + * Usage (from coinbase/stable-swapper/solana): + * WALLET=~/.config/solana/swap-verify.json \ + * IDL_PATH=/Users/olivercai/Desktop/smart-contracts/stable-swapper-svm/terraform/solana-prod/build/1.0.0/idl.json \ + * AMOUNT_USDC=0.05 \ + * node mainnet-swap-verify.js + */ +const fs = require("fs"); +const os = require("os"); +const anchor = require("@coral-xyz/anchor"); +const { PublicKey, Keypair, Connection } = require("@solana/web3.js"); +const { + TOKEN_PROGRAM_ID, + ASSOCIATED_TOKEN_PROGRAM_ID, + getAssociatedTokenAddressSync, + getOrCreateAssociatedTokenAccount, + getAccount, +} = require("@solana/spl-token"); + +const RPC = process.env.RPC || "https://api.mainnet-beta.solana.com"; +const USDC = new PublicKey("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"); +const USDF = new PublicKey("5AMAA9JV9H97YYVxx8F6FsCMmTwXSuTTQneiup4RYAUQ"); +const AMOUNT = Math.round(parseFloat(process.env.AMOUNT_USDC || "0.05") * 1e6); // USDC has 6 decimals + +function loadKeypair(p) { + const path = p.replace(/^~/, os.homedir()); + return Keypair.fromSecretKey(Uint8Array.from(JSON.parse(fs.readFileSync(path)))); +} + +(async () => { + if (!process.env.WALLET) throw new Error("set WALLET="); + const idlPath = + process.env.IDL_PATH || + "/Users/olivercai/Desktop/smart-contracts/stable-swapper-svm/terraform/solana-prod/build/1.0.0/idl.json"; + const idl = JSON.parse(fs.readFileSync(idlPath)); + + const payer = loadKeypair(process.env.WALLET); + const connection = new Connection(RPC, "confirmed"); + const provider = new anchor.AnchorProvider(connection, new anchor.Wallet(payer), { + commitment: "confirmed", + }); + const program = new anchor.Program(idl, provider); + const PROG = program.programId; + console.log("program:", PROG.toBase58(), "| payer:", payer.publicKey.toBase58()); + + const s = (x) => Buffer.from(x); + const [pool] = PublicKey.findProgramAddressSync([s("liquidity_pool")], PROG); + const [inVault] = PublicKey.findProgramAddressSync([s("token_vault"), pool.toBuffer(), USDC.toBuffer()], PROG); + const [outVault] = PublicKey.findProgramAddressSync([s("token_vault"), pool.toBuffer(), USDF.toBuffer()], PROG); + const [inVaultTA] = PublicKey.findProgramAddressSync([s("vault_token_account"), inVault.toBuffer()], PROG); + const [outVaultTA] = PublicKey.findProgramAddressSync([s("vault_token_account"), outVault.toBuffer()], PROG); + + // Confirm the pool is migrated + swaps enabled before spending anything. + const poolAcct = await program.account.liquidityPool.fetch(pool); + console.log("swaps_paused:", poolAcct.swapsPaused, "| fee_rate(bps):", poolAcct.feeRate.toString()); + if (poolAcct.swapsPaused) throw new Error("swaps are paused — unpause before verifying"); + const feeRecipient = poolAcct.feeRecipient; + + const userUsdc = getAssociatedTokenAddressSync(USDC, payer.publicKey); + const usdcBal = (await getAccount(connection, userUsdc)).amount; + if (usdcBal < BigInt(AMOUNT)) throw new Error(`USDC balance ${usdcBal} < amount ${AMOUNT}; fund the wallet with USDC`); + + // Ensure the USDF output ATA exists (program does NOT create it). + const userUsdf = ( + await getOrCreateAssociatedTokenAccount(connection, payer, USDF, payer.publicKey) + ).address; + const feeRecipientTA = getAssociatedTokenAddressSync(USDC, feeRecipient, true); + + const before = { + usdc: (await getAccount(connection, userUsdc)).amount, + usdf: (await getAccount(connection, userUsdf)).amount, + }; + console.log("before USDC:", before.usdc.toString(), "USDF:", before.usdf.toString()); + + const builder = program.methods + .swap(new anchor.BN(AMOUNT), new anchor.BN(1)) // min_out=1 base unit (verification, not slippage-sensitive) + .accounts({ + pool, + inVault, + outVault, + inVaultTokenAccount: inVaultTA, + outVaultTokenAccount: outVaultTA, + userFromTokenAccount: userUsdc, + toTokenAccount: userUsdf, + feeRecipientTokenAccount: feeRecipientTA, + feeRecipient, + fromMint: USDC, + toMint: USDF, + user: payer.publicKey, + tokenProgram: TOKEN_PROGRAM_ID, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + systemProgram: anchor.web3.SystemProgram.programId, + }); + + // DRY_RUN=1 simulates the swap (no funds moved, nothing committed) and prints logs. + if (process.env.DRY_RUN) { + const sim = await builder.simulate(); + console.log("DRY RUN — simulation logs:"); + (sim.raw || sim.logs || []).forEach((l) => console.log(" " + l)); + console.log("✅ simulation succeeded (no funds moved). Unset DRY_RUN to execute for real."); + return; + } + + const sig = await builder.rpc(); + + const after = { + usdc: (await getAccount(connection, userUsdc)).amount, + usdf: (await getAccount(connection, userUsdf)).amount, + }; + console.log("after USDC:", after.usdc.toString(), "USDF:", after.usdf.toString()); + console.log("USDC spent:", (before.usdc - after.usdc).toString(), "| USDF received:", (after.usdf - before.usdf).toString()); + console.log("✅ swap tx:", sig); + console.log(" https://explorer.solana.com/tx/" + sig); +})().catch((e) => { + console.error("❌ swap verify failed:", e.message || e); + process.exit(1); +}); diff --git a/solana/programs/stable-swapper/src/constants.rs b/solana/programs/stable-swapper/src/constants.rs index cc9397b..7d19a7e 100644 --- a/solana/programs/stable-swapper/src/constants.rs +++ b/solana/programs/stable-swapper/src/constants.rs @@ -12,6 +12,9 @@ pub const MAX_SUPPORTED_TOKENS: usize = 50; /// these addresses; only the cold-key configure authority can add or remove them. pub const MAX_WITHDRAW_RECIPIENTS: usize = 10; +/// Maximum number of whitelisted addresses +pub const MAX_WHITELISTED_ADDRESSES: usize = 100; + /// Minimum allowed token decimals pub const MIN_TOKEN_DECIMALS: u8 = 6; @@ -27,3 +30,6 @@ pub const TOKEN_VAULT_SEED: &[u8] = b"token_vault"; /// Seed for vault token account PDA pub const VAULT_TOKEN_ACCOUNT_SEED: &[u8] = b"vault_token_account"; + +/// Seed for address whitelist PDA +pub const ADDRESS_WHITELIST_SEED: &[u8] = b"address_whitelist"; diff --git a/solana/programs/stable-swapper/src/errors.rs b/solana/programs/stable-swapper/src/errors.rs index e2ce4f4..312735c 100644 --- a/solana/programs/stable-swapper/src/errors.rs +++ b/solana/programs/stable-swapper/src/errors.rs @@ -32,8 +32,8 @@ pub enum LiquidityError { DecimalNormalizationOverflow, #[msg("Token is disabled and cannot be used in swaps")] TokenDisabled, - #[msg("Deprecated: previously NotWhitelisted")] - DeprecatedNotWhitelisted, + #[msg("Address not whitelisted")] + NotWhitelisted, #[msg("Deprecated: previously MaxWhitelistedAddressesReached")] DeprecatedMaxWhitelistedAddressesReached, #[msg("Deprecated: previously AddressAlreadyWhitelisted")] diff --git a/solana/programs/stable-swapper/src/lib.rs b/solana/programs/stable-swapper/src/lib.rs index 2f08e5c..df8334e 100644 --- a/solana/programs/stable-swapper/src/lib.rs +++ b/solana/programs/stable-swapper/src/lib.rs @@ -14,10 +14,6 @@ use utils::*; declare_id!("pqgqKahpG1y2wsgxFhzaAnkV1cL9vk8MSg9qm4q646F"); -// NOTE: The previously deployed whitelist PDA (seeded b"address_whitelist") is orphaned -// on devnet/mainnet after whitelist removal. Its rent is intentionally forfeited; adding a -// close_whitelist instruction is not worth the complexity for the small amount involved. - #[program] pub mod stable_swapper { use super::*; @@ -173,6 +169,13 @@ pub mod stable_swapper { require!(amount_in > 0, LiquidityError::InvalidAmount); require!(min_amount_out > 0, LiquidityError::InvalidAmount); + if ctx.accounts.whitelist.enabled { + require!( + ctx.accounts.whitelist.is_whitelisted(&ctx.accounts.user.key()), + LiquidityError::NotWhitelisted + ); + } + // Check that neither token is disabled require!( !ctx.accounts.in_vault.disabled, @@ -907,6 +910,12 @@ pub struct Swap<'info> { #[account(mut)] pub user: Signer<'info>, + #[account( + seeds = [ADDRESS_WHITELIST_SEED], + bump = whitelist.bump + )] + pub whitelist: Account<'info, AddressWhitelist>, + pub token_program: Program<'info, Token>, pub associated_token_program: Program<'info, AssociatedToken>, pub system_program: Program<'info, System>, diff --git a/solana/programs/stable-swapper/src/state.rs b/solana/programs/stable-swapper/src/state.rs index a78ed74..d85302b 100644 --- a/solana/programs/stable-swapper/src/state.rs +++ b/solana/programs/stable-swapper/src/state.rs @@ -1,4 +1,4 @@ -use crate::constants::{MAX_SUPPORTED_TOKENS, MAX_WITHDRAW_RECIPIENTS}; +use crate::constants::{MAX_SUPPORTED_TOKENS, MAX_WHITELISTED_ADDRESSES, MAX_WITHDRAW_RECIPIENTS}; use anchor_lang::prelude::*; // Discriminator-stability invariant: Anchor derives the 8-byte account discriminator from @@ -64,3 +64,18 @@ pub struct TokenVault { impl TokenVault { pub const INIT_SPACE: usize = 32 + 8 + 1 + 1; // mint + reserved_amount (layout-only) + disabled + bump } + +#[account] +pub struct AddressWhitelist { + pub addresses: Vec, + pub enabled: bool, + pub bump: u8, +} + +impl AddressWhitelist { + pub const INIT_SPACE: usize = (4 + 32 * MAX_WHITELISTED_ADDRESSES) + 1 + 1; // addresses + enabled + bump + + pub fn is_whitelisted(&self, address: &Pubkey) -> bool { + self.addresses.contains(address) + } +} From 020de94271d39359d7e8b5e81f03d78354f9046b Mon Sep 17 00:00:00 2001 From: Oliver Cai Date: Thu, 10 Sep 2026 12:49:50 -0400 Subject: [PATCH 2/4] adjust lib --- solana/programs/stable-swapper/src/lib.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/solana/programs/stable-swapper/src/lib.rs b/solana/programs/stable-swapper/src/lib.rs index df8334e..5253a5a 100644 --- a/solana/programs/stable-swapper/src/lib.rs +++ b/solana/programs/stable-swapper/src/lib.rs @@ -47,6 +47,11 @@ pub mod stable_swapper { pool.liquidity_paused = false; pool.bump = ctx.bumps.pool; + let whitelist = &mut ctx.accounts.whitelist; + whitelist.addresses = Vec::new(); + whitelist.enabled = false; + whitelist.bump = ctx.bumps.whitelist; + msg!("Liquidity pool initialized with fee rate: {}", fee_rate); Ok(()) } @@ -708,6 +713,15 @@ pub struct Initialize<'info> { /// allowlist, which `configure_authority` manages via add/remove after initialization. pub withdraw_recipient: UncheckedAccount<'info>, + #[account( + init, + payer = payer, + space = 8 + AddressWhitelist::INIT_SPACE, + seeds = [ADDRESS_WHITELIST_SEED], + bump + )] + pub whitelist: Account<'info, AddressWhitelist>, + pub system_program: Program<'info, System>, } From 933339ec0499cd7a860ea57e320b3012ff2fcc24 Mon Sep 17 00:00:00 2001 From: Oliver Cai Date: Thu, 10 Sep 2026 13:21:02 -0400 Subject: [PATCH 3/4] Change to UncheckedAccount --- .../programs/stable-swapper/src/constants.rs | 3 -- solana/programs/stable-swapper/src/errors.rs | 4 +-- solana/programs/stable-swapper/src/lib.rs | 33 +++++++------------ solana/programs/stable-swapper/src/state.rs | 17 +--------- 4 files changed, 14 insertions(+), 43 deletions(-) diff --git a/solana/programs/stable-swapper/src/constants.rs b/solana/programs/stable-swapper/src/constants.rs index 7d19a7e..bb5262e 100644 --- a/solana/programs/stable-swapper/src/constants.rs +++ b/solana/programs/stable-swapper/src/constants.rs @@ -12,9 +12,6 @@ pub const MAX_SUPPORTED_TOKENS: usize = 50; /// these addresses; only the cold-key configure authority can add or remove them. pub const MAX_WITHDRAW_RECIPIENTS: usize = 10; -/// Maximum number of whitelisted addresses -pub const MAX_WHITELISTED_ADDRESSES: usize = 100; - /// Minimum allowed token decimals pub const MIN_TOKEN_DECIMALS: u8 = 6; diff --git a/solana/programs/stable-swapper/src/errors.rs b/solana/programs/stable-swapper/src/errors.rs index 312735c..e2ce4f4 100644 --- a/solana/programs/stable-swapper/src/errors.rs +++ b/solana/programs/stable-swapper/src/errors.rs @@ -32,8 +32,8 @@ pub enum LiquidityError { DecimalNormalizationOverflow, #[msg("Token is disabled and cannot be used in swaps")] TokenDisabled, - #[msg("Address not whitelisted")] - NotWhitelisted, + #[msg("Deprecated: previously NotWhitelisted")] + DeprecatedNotWhitelisted, #[msg("Deprecated: previously MaxWhitelistedAddressesReached")] DeprecatedMaxWhitelistedAddressesReached, #[msg("Deprecated: previously AddressAlreadyWhitelisted")] diff --git a/solana/programs/stable-swapper/src/lib.rs b/solana/programs/stable-swapper/src/lib.rs index 5253a5a..e1bcd4c 100644 --- a/solana/programs/stable-swapper/src/lib.rs +++ b/solana/programs/stable-swapper/src/lib.rs @@ -47,11 +47,6 @@ pub mod stable_swapper { pool.liquidity_paused = false; pool.bump = ctx.bumps.pool; - let whitelist = &mut ctx.accounts.whitelist; - whitelist.addresses = Vec::new(); - whitelist.enabled = false; - whitelist.bump = ctx.bumps.whitelist; - msg!("Liquidity pool initialized with fee rate: {}", fee_rate); Ok(()) } @@ -174,12 +169,10 @@ pub mod stable_swapper { require!(amount_in > 0, LiquidityError::InvalidAmount); require!(min_amount_out > 0, LiquidityError::InvalidAmount); - if ctx.accounts.whitelist.enabled { - require!( - ctx.accounts.whitelist.is_whitelisted(&ctx.accounts.user.key()), - LiquidityError::NotWhitelisted - ); - } + // Whitelist enforcement is permanently deprecated. `whitelist` stays in this + // instruction's account list (see the field below) purely so already-encoded + // callers/indexers built against the pre-deprecation `swap` signature keep working; + // nothing reads or writes it anymore. // Check that neither token is disabled require!( @@ -713,15 +706,6 @@ pub struct Initialize<'info> { /// allowlist, which `configure_authority` manages via add/remove after initialization. pub withdraw_recipient: UncheckedAccount<'info>, - #[account( - init, - payer = payer, - space = 8 + AddressWhitelist::INIT_SPACE, - seeds = [ADDRESS_WHITELIST_SEED], - bump - )] - pub whitelist: Account<'info, AddressWhitelist>, - pub system_program: Program<'info, System>, } @@ -924,11 +908,16 @@ pub struct Swap<'info> { #[account(mut)] pub user: Signer<'info>, + /// Deprecated whitelist PDA. Kept only so the account list matches the + /// pre-deprecation `swap` signature that existing callers/indexers already encode; + /// its data is never read. On mainnet this resolves to the orphaned whitelist account + /// left behind by the original removal; on fresh deployments it need not exist at all. + /// CHECK: seeds-verified PDA address only; deliberately not deserialized. #[account( seeds = [ADDRESS_WHITELIST_SEED], - bump = whitelist.bump + bump )] - pub whitelist: Account<'info, AddressWhitelist>, + pub whitelist: UncheckedAccount<'info>, pub token_program: Program<'info, Token>, pub associated_token_program: Program<'info, AssociatedToken>, diff --git a/solana/programs/stable-swapper/src/state.rs b/solana/programs/stable-swapper/src/state.rs index d85302b..a78ed74 100644 --- a/solana/programs/stable-swapper/src/state.rs +++ b/solana/programs/stable-swapper/src/state.rs @@ -1,4 +1,4 @@ -use crate::constants::{MAX_SUPPORTED_TOKENS, MAX_WHITELISTED_ADDRESSES, MAX_WITHDRAW_RECIPIENTS}; +use crate::constants::{MAX_SUPPORTED_TOKENS, MAX_WITHDRAW_RECIPIENTS}; use anchor_lang::prelude::*; // Discriminator-stability invariant: Anchor derives the 8-byte account discriminator from @@ -64,18 +64,3 @@ pub struct TokenVault { impl TokenVault { pub const INIT_SPACE: usize = 32 + 8 + 1 + 1; // mint + reserved_amount (layout-only) + disabled + bump } - -#[account] -pub struct AddressWhitelist { - pub addresses: Vec, - pub enabled: bool, - pub bump: u8, -} - -impl AddressWhitelist { - pub const INIT_SPACE: usize = (4 + 32 * MAX_WHITELISTED_ADDRESSES) + 1 + 1; // addresses + enabled + bump - - pub fn is_whitelisted(&self, address: &Pubkey) -> bool { - self.addresses.contains(address) - } -} From 2d91c1d33784bc42255d2c795ada1e6b7b4c3df8 Mon Sep 17 00:00:00 2001 From: Oliver Cai Date: Thu, 10 Sep 2026 14:23:25 -0400 Subject: [PATCH 4/4] delete test file --- solana/mainnet-swap-verify.js | 130 ---------------------------------- 1 file changed, 130 deletions(-) delete mode 100644 solana/mainnet-swap-verify.js diff --git a/solana/mainnet-swap-verify.js b/solana/mainnet-swap-verify.js deleted file mode 100644 index 9528b65..0000000 --- a/solana/mainnet-swap-verify.js +++ /dev/null @@ -1,130 +0,0 @@ -/* - * mainnet-swap-verify.js — post-migration smoke test for the stable-swapper pool. - * - * Executes a REAL, small USDC -> USDF swap on solana-mainnet to prove swaps are - * operational after the PR #9 upgrade + migrate. Moves real funds (a few cents). - * - * RUN ONLY AFTER the migration apply completes. - * - * Prereqs on the wallet (WALLET keypair): - * - a little SOL (~0.01) for fees + ATA rents - * - a small USDC balance (>= AMOUNT_USDC; e.g. 0.05 USDC) - * - USDC ATA exists automatically once funded; the script creates the USDF ATA - * if missing, and the program init_if_needed-creates the fee recipient's USDC ATA. - * - * Usage (from coinbase/stable-swapper/solana): - * WALLET=~/.config/solana/swap-verify.json \ - * IDL_PATH=/Users/olivercai/Desktop/smart-contracts/stable-swapper-svm/terraform/solana-prod/build/1.0.0/idl.json \ - * AMOUNT_USDC=0.05 \ - * node mainnet-swap-verify.js - */ -const fs = require("fs"); -const os = require("os"); -const anchor = require("@coral-xyz/anchor"); -const { PublicKey, Keypair, Connection } = require("@solana/web3.js"); -const { - TOKEN_PROGRAM_ID, - ASSOCIATED_TOKEN_PROGRAM_ID, - getAssociatedTokenAddressSync, - getOrCreateAssociatedTokenAccount, - getAccount, -} = require("@solana/spl-token"); - -const RPC = process.env.RPC || "https://api.mainnet-beta.solana.com"; -const USDC = new PublicKey("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"); -const USDF = new PublicKey("5AMAA9JV9H97YYVxx8F6FsCMmTwXSuTTQneiup4RYAUQ"); -const AMOUNT = Math.round(parseFloat(process.env.AMOUNT_USDC || "0.05") * 1e6); // USDC has 6 decimals - -function loadKeypair(p) { - const path = p.replace(/^~/, os.homedir()); - return Keypair.fromSecretKey(Uint8Array.from(JSON.parse(fs.readFileSync(path)))); -} - -(async () => { - if (!process.env.WALLET) throw new Error("set WALLET="); - const idlPath = - process.env.IDL_PATH || - "/Users/olivercai/Desktop/smart-contracts/stable-swapper-svm/terraform/solana-prod/build/1.0.0/idl.json"; - const idl = JSON.parse(fs.readFileSync(idlPath)); - - const payer = loadKeypair(process.env.WALLET); - const connection = new Connection(RPC, "confirmed"); - const provider = new anchor.AnchorProvider(connection, new anchor.Wallet(payer), { - commitment: "confirmed", - }); - const program = new anchor.Program(idl, provider); - const PROG = program.programId; - console.log("program:", PROG.toBase58(), "| payer:", payer.publicKey.toBase58()); - - const s = (x) => Buffer.from(x); - const [pool] = PublicKey.findProgramAddressSync([s("liquidity_pool")], PROG); - const [inVault] = PublicKey.findProgramAddressSync([s("token_vault"), pool.toBuffer(), USDC.toBuffer()], PROG); - const [outVault] = PublicKey.findProgramAddressSync([s("token_vault"), pool.toBuffer(), USDF.toBuffer()], PROG); - const [inVaultTA] = PublicKey.findProgramAddressSync([s("vault_token_account"), inVault.toBuffer()], PROG); - const [outVaultTA] = PublicKey.findProgramAddressSync([s("vault_token_account"), outVault.toBuffer()], PROG); - - // Confirm the pool is migrated + swaps enabled before spending anything. - const poolAcct = await program.account.liquidityPool.fetch(pool); - console.log("swaps_paused:", poolAcct.swapsPaused, "| fee_rate(bps):", poolAcct.feeRate.toString()); - if (poolAcct.swapsPaused) throw new Error("swaps are paused — unpause before verifying"); - const feeRecipient = poolAcct.feeRecipient; - - const userUsdc = getAssociatedTokenAddressSync(USDC, payer.publicKey); - const usdcBal = (await getAccount(connection, userUsdc)).amount; - if (usdcBal < BigInt(AMOUNT)) throw new Error(`USDC balance ${usdcBal} < amount ${AMOUNT}; fund the wallet with USDC`); - - // Ensure the USDF output ATA exists (program does NOT create it). - const userUsdf = ( - await getOrCreateAssociatedTokenAccount(connection, payer, USDF, payer.publicKey) - ).address; - const feeRecipientTA = getAssociatedTokenAddressSync(USDC, feeRecipient, true); - - const before = { - usdc: (await getAccount(connection, userUsdc)).amount, - usdf: (await getAccount(connection, userUsdf)).amount, - }; - console.log("before USDC:", before.usdc.toString(), "USDF:", before.usdf.toString()); - - const builder = program.methods - .swap(new anchor.BN(AMOUNT), new anchor.BN(1)) // min_out=1 base unit (verification, not slippage-sensitive) - .accounts({ - pool, - inVault, - outVault, - inVaultTokenAccount: inVaultTA, - outVaultTokenAccount: outVaultTA, - userFromTokenAccount: userUsdc, - toTokenAccount: userUsdf, - feeRecipientTokenAccount: feeRecipientTA, - feeRecipient, - fromMint: USDC, - toMint: USDF, - user: payer.publicKey, - tokenProgram: TOKEN_PROGRAM_ID, - associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, - systemProgram: anchor.web3.SystemProgram.programId, - }); - - // DRY_RUN=1 simulates the swap (no funds moved, nothing committed) and prints logs. - if (process.env.DRY_RUN) { - const sim = await builder.simulate(); - console.log("DRY RUN — simulation logs:"); - (sim.raw || sim.logs || []).forEach((l) => console.log(" " + l)); - console.log("✅ simulation succeeded (no funds moved). Unset DRY_RUN to execute for real."); - return; - } - - const sig = await builder.rpc(); - - const after = { - usdc: (await getAccount(connection, userUsdc)).amount, - usdf: (await getAccount(connection, userUsdf)).amount, - }; - console.log("after USDC:", after.usdc.toString(), "USDF:", after.usdf.toString()); - console.log("USDC spent:", (before.usdc - after.usdc).toString(), "| USDF received:", (after.usdf - before.usdf).toString()); - console.log("✅ swap tx:", sig); - console.log(" https://explorer.solana.com/tx/" + sig); -})().catch((e) => { - console.error("❌ swap verify failed:", e.message || e); - process.exit(1); -});