This guide provides step-by-step instructions for developing and testing the SuperSwap Solana program.
- Development Environment Setup
- Understanding the Architecture
- Step-by-Step Development
- Testing Strategy
- Across Integration Details
- Jupiter Integration Details
- Debugging Guide
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source $HOME/.cargo/env
rustup default stable
rustup component add rustfmt clippyVerify installation:
rustc --version
cargo --versionsh -c "$(curl -sSfL https://release.solana.com/v1.18.22/install)"
export PATH="/home/ck/.local/share/solana/install/active_release/bin:$PATH"Verify installation:
solana --version
# Expected: solana-cli 1.18.22cargo install --git https://github.com/coral-xyz/anchor avm --locked --force
avm install 0.30.1
avm use 0.30.1Verify installation:
anchor --version
# Expected: anchor-cli 0.30.1curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
sudo apt-get install -y nodejs
npm install -g yarnVerify installation:
node --version
yarn --version# Generate a new keypair for development
solana-keygen new --outfile ~/.config/solana/id.json
# Set to localnet
solana config set --url localhost
# Verify configuration
solana config getcd /home/ck/Documents/superswap-sol
yarn installSuperSwap Program
│
├── Config (PDA)
│ ├── Admin authority
│ ├── Across handler
│ ├── Jupiter program ID
│ ├── USDC mint
│ ├── Fee configuration
│ └── Pause state
│
└── SwapOrder (PDA per order)
├── Order ID
├── Recipient
├── Amounts
├── Deadline
└── Status
┌──────────────────────────────────────────────────────────────┐
│ EVM Chain (Base) │
│ User initiates swap: cbBTC -> PUMP (Solana) │
└────────────────────┬─────────────────────────────────────────┘
│
│ 1. Swap cbBTC -> USDC on Base
│ (via SuperSwap EVM contract)
│
▼
┌──────────────────────────────────────────────────────────────┐
│ Across Protocol │
│ - Bridge USDC from Base to Solana │
│ - Include message with swap instructions │
└────────────────────┬─────────────────────────────────────────┘
│
│ 2. Bridge USDC with calldata
│
▼
┌──────────────────────────────────────────────────────────────┐
│ SuperSwap Solana Program │
│ - Receive USDC from Across │
│ - Parse swap instructions │
│ - Execute Jupiter swap │
│ - Send tokens to user OR refund on failure │
└────────────────────┬─────────────────────────────────────────┘
│
│ 3. Execute swap via CPI
│
▼
┌──────────────────────────────────────────────────────────────┐
│ Jupiter Aggregator │
│ Swap USDC -> PUMP with optimal routing │
└────────────────────┬─────────────────────────────────────────┘
│
│ 4. Transfer swapped tokens
│
▼
┌──────────────────────────────────────────────────────────────┐
│ User Wallet │
│ Receives PUMP tokens on Solana │
└──────────────────────────────────────────────────────────────┘
In terminal 1:
solana-test-validator --resetIn terminal 2 (monitor logs):
solana logsIn terminal 3:
cd /home/ck/Documents/superswap-sol
anchor buildThis creates:
target/deploy/superswap_sol.so- The compiled programtarget/idl/superswap_sol.json- Interface definitiontarget/types/superswap_sol.ts- TypeScript types
After first build, get the program ID:
solana-keygen pubkey target/deploy/superswap_sol-keypair.jsonUpdate the program ID in:
Anchor.toml-[programs.localnet]sectionprograms/superswap-sol/src/lib.rs-declare_id!()macro
Then rebuild:
anchor buildanchor deployVerify deployment:
solana program show <PROGRAM_ID>anchor test --skip-buildThis will:
- Initialize the program configuration
- Set admin, Across handler, Jupiter program ID
- Configure fees
Run specific test:
anchor test --skip-build --skip-deploy tests/superswap-sol.tsVerify:
- Admin can update configuration
- Non-admin cannot update (should fail)
- Pause/unpause works correctly
Jupiter V6 uses a swap instruction format:
interface JupiterSwapInstruction {
programId: PublicKey;
accounts: AccountMeta[];
data: Buffer;
}To get swap instructions:
import { createJupiterApiClient } from '@jup-ag/api';
const jupiterApi = createJupiterApiClient();
// Get quote
const quote = await jupiterApi.quoteGet({
inputMint: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', // USDC
outputMint: destinationMint,
amount: 1000000, // 1 USDC
slippageBps: 50,
});
// Get swap instructions
const { swapInstruction } = await jupiterApi.swapInstructionsPost({
swapRequest: {
quoteResponse: quote,
userPublicKey: programPDA.toString(),
},
});Create a test file tests/jupiter-swap.ts:
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { SuperswapSol } from "../target/types/superswap_sol";
import { createJupiterApiClient } from '@jup-ag/api';
describe("Jupiter swap integration", () => {
const provider = anchor.AnchorProvider.env();
anchor.setProvider(provider);
const program = anchor.workspace.SuperswapSol as Program<SuperswapSol>;
const jupiterApi = createJupiterApiClient();
it("Executes a real Jupiter swap", async () => {
// Get Jupiter quote
const quote = await jupiterApi.quoteGet({
inputMint: USDC_MINT,
outputMint: DESTINATION_MINT,
amount: 1000000,
slippageBps: 100,
});
// Get swap instructions
const { swapInstruction } = await jupiterApi.swapInstructionsPost({
swapRequest: {
quoteResponse: quote,
userPublicKey: configPda.toString(),
},
});
// Deserialize instruction
const ix = deserializeInstruction(swapInstruction);
// Call program with Jupiter swap data
await program.methods
.processBridgeAndSwap({
// ... params
jupiterSwapData: ix.data,
})
.accounts({
// ... accounts
})
.remainingAccounts(ix.accounts)
.rpc();
});
});The key challenge is executing Jupiter via CPI. In process_bridge_and_swap.rs:
use crate::utils::jupiter::execute_jupiter_swap;
// After fee deduction
let jupiter_accounts: Vec<AccountInfo> = ctx.remaining_accounts.to_vec();
// Execute Jupiter swap
execute_jupiter_swap(
ctx.accounts.jupiter_program.as_ref(),
¶ms.jupiter_swap_data,
&jupiter_accounts,
&[&[b"config", &[config.bump]]],
)?;
// Verify output amount
let output_amount = get_output_amount(&recipient_destination_account)?;
require!(
output_amount >= params.min_output_amount,
SuperSwapError::InsufficientOutputAmount
);In instructions/process_bridge_and_swap.rs:
// Wrap Jupiter swap in error handling
match execute_jupiter_swap(...) {
Ok(_) => {
// Verify output
if output_amount >= params.min_output_amount {
swap_order.status = OrderStatus::Completed;
msg!("Swap completed successfully");
} else {
// Insufficient output, refund
refund_usdc(
&config,
&mut swap_order,
&program_usdc_account,
&recipient_usdc_account,
&token_program,
)?;
}
},
Err(e) => {
// Swap failed, refund
msg!("Swap failed: {:?}", e);
refund_usdc(
&config,
&mut swap_order,
&program_usdc_account,
&recipient_usdc_account,
&token_program,
)?;
}
}Create test cases:
- Swap with impossible slippage (should refund)
- Swap with expired deadline (should refund)
- Swap with invalid token mint (should refund)
it("Refunds USDC on swap failure", async () => {
const initialBalance = await getTokenBalance(recipientUsdcAccount);
// Trigger swap with impossible parameters
await program.methods
.processBridgeAndSwap({
// ... params with minOutputAmount too high
})
.accounts({...})
.rpc();
const finalBalance = await getTokenBalance(recipientUsdcAccount);
// User should receive refund
assert.equal(finalBalance, initialBalance + usdcAmount);
// Order should be marked as refunded
const swapOrder = await program.account.swapOrder.fetch(swapOrderPda);
assert.equal(swapOrder.status, OrderStatus.Refunded);
});Across uses a message passing system. On Solana, the message is delivered as calldata.
Message structure:
pub struct AcrossMessage {
pub order_id: u64,
pub recipient: Pubkey,
pub usdc_amount: u64,
pub min_output_amount: u64,
pub destination_mint: Pubkey,
pub deadline: i64,
pub jupiter_swap_data: Vec<u8>,
}The Across handler is the account that calls your program when USDC arrives on Solana.
In production:
pub fn process_bridge_and_swap(
ctx: Context<ProcessBridgeAndSwap>,
params: ProcessBridgeAndSwapParams,
) -> Result<()> {
// Verify caller is Across handler
require!(
ctx.accounts.across_handler.key() == ctx.accounts.config.across_handler,
SuperSwapError::InvalidAcrossHandler
);
// ... rest of logic
}For local testing, use a mock Across handler:
// In tests, any keypair can be the "Across handler"
const mockAcrossHandler = Keypair.generate();
// Initialize with mock handler
await program.methods
.initialize({
acrossHandler: mockAcrossHandler.publicKey,
// ... other params
})
.rpc();
// Simulate Across delivery
await program.methods
.processBridgeAndSwap({...})
.accounts({
acrossHandler: mockAcrossHandler.publicKey,
// ...
})
.signers([mockAcrossHandler])
.rpc();-
Deploy program to devnet:
anchor deploy --provider.cluster devnet
-
Get Across handler address for Solana devnet from Across documentation
-
Update configuration:
await program.methods .updateConfig({ newAcrossHandler: ACROSS_HANDLER_DEVNET, // ... }) .rpc();
-
Test with real Across bridge:
- Bridge USDC from EVM testnet to Solana devnet
- Include message with swap parameters
- Monitor Solana for execution
Test each instruction independently:
# Test initialization
anchor test tests/initialize.spec.ts
# Test config updates
anchor test tests/config.spec.ts
# Test pause functionality
anchor test tests/pause.spec.tsTest full swap flow:
# Test complete swap flow
anchor test tests/integration.spec.ts
# Test refund logic
anchor test tests/refund.spec.tsDeploy to devnet and test with real protocols:
# Deploy to devnet
anchor deploy --provider.cluster devnet
# Run devnet tests
anchor test --provider.cluster devnetTest against mainnet state without deploying:
# Clone mainnet state
solana-test-validator --clone <JUPITER_PROGRAM> --clone <USDC_MINT>
# Run tests
anchor test --skip-deployCause: Program not deployed or wrong cluster
Solution:
solana config get
anchor deployCause: Anchor error, usually account validation failed
Solution:
- Check program logs:
solana logs - Verify account addresses match expected PDAs
- Check account ownership
Cause: Various - slippage, liquidity, invalid route
Solution:
- Check Jupiter quote is fresh (< 30 seconds old)
- Increase slippage tolerance
- Verify token accounts exist
- Check program logs for specific error
# Watch all logs
solana logs
# Filter for your program
solana logs | grep <PROGRAM_ID>
# Save logs to file
solana logs > debug.log# Inspect program account
solana account <ACCOUNT_ADDRESS>
# Decode account data
anchor account <ACCOUNT_TYPE> <ACCOUNT_ADDRESS># Get transaction details
solana confirm -v <TRANSACTION_SIGNATURE># Run tests with verbose output
RUST_LOG=debug anchor testMonitor compute units usage:
use anchor_lang::solana_program::log::sol_log_compute_units;
pub fn process_bridge_and_swap(ctx: Context<ProcessBridgeAndSwap>, params: ProcessBridgeAndSwapParams) -> Result<()> {
sol_log_compute_units();
// ... your logic
sol_log_compute_units();
Ok(())
}Minimize account reads:
// Bad: Multiple fetches
let config = ctx.accounts.config.load()?;
let fee = config.fee_bps;
let admin = config.admin;
// Good: Single fetch, multiple uses
let config = ctx.accounts.config.load()?;
let (fee, admin) = (config.fee_bps, config.admin);Use transaction batching when possible:
// Instead of multiple transactions
await program.methods.updateConfig1().rpc();
await program.methods.updateConfig2().rpc();
// Batch into one transaction
const tx = new Transaction();
tx.add(program.methods.updateConfig1().instruction());
tx.add(program.methods.updateConfig2().instruction());
await provider.sendAndConfirm(tx);-
Complete Jupiter Integration
- Implement full CPI to Jupiter
- Handle all Jupiter error cases
- Test with multiple token pairs
-
Production Hardening
- Security audit
- Stress testing
- Rate limiting considerations
-
SVM → EVM Flow
- Design reverse bridge flow
- Implement Solana → EVM instructions
- Integrate with Across for reverse bridging
-
Frontend Integration
- Build SDK for easy integration
- Create example frontend
- Document API for developers