icRamp Devlog #3 — icRamp Canister & Solana Integration

icRamp Devlog #3 — icRamp Canister & Solana Integration

8/21/20255 min • icramp
ICPSolanaChain FusionEscrowCanisters

This post continues the build of icRamp under my second ICP Chain Fusion grant. After the Bitcoin + Runes integration and the standalone Solana backend canister, I've now wired the icRamp orchestrator canister with Solana. The goal: support P2P orders across SOL and SPL tokens with a safe escrow lifecycle.

What was missing?

In the previous devlog, I finished the Solana backend:

  • patched Solana crates for wasm32-unknown-unknown
  • implemented account lookups, ATA creation, SOL/SPL sends
  • added a thin registry and vault interface

But icRamp itself still had no way to talk to this Solana canister. Until now, the backend only handled Bitcoin/EVM/ICP.

Persisting canister IDs

We first updated the icRamp init args to include a canister_ids struct:

#[derive(CandidType, Deserialize, Clone, Debug)]
pub struct CanisterIdsConfig {
    pub solana_backend_id: String,
    pub bitcoin_backend_id: String,
}
 
#[derive(Clone, Debug, CandidType, Deserialize)]
pub struct CanisterIds {
    pub solana_backend_id: Principal,
    pub bitcoin_backend_id: Principal,
}

At install time, the state converts the config into typed principals, so intercalls don’t need hardcoded IDs anymore. Both Solana and Bitcoin backends are now first-class citizens in icRamp’s state.

Intercalls (Solana backend)

We added intercanister calls wrapping the Solana backend APIs:

pub async fn solana_backend_send_sol(dst: String, lamports: Nat) -> Result<String>;
pub async fn solana_backend_send_spl_token(mint: String, to: String, amount: Nat) -> Result<String>;
 
// Fast confirmations for listeners
pub async fn solana_backend_get_tx(signature: String) -> Result<TxInfo>;
 
// Detailed tx metadata for validation (balances, token balances)
pub async fn solana_backend_get_tx_metadata(signature: String) -> Result<TxMetadata>;
 
// Escrow accounting (no L1 tx)
pub async fn solana_backend_deposit_funds(offramper: String, amount: u64, token_mint: Option<String>) -> Result<()>;
pub async fn solana_backend_complete_order(onramper_address: String, amount: u64, token_mint: Option<String>) -> Result<()>;
 
// Fee modeling for payout/refund on Solana
pub async fn solana_backend_estimate_fees(token_mint: Option<String>) -> Result<SolanaFeeEstimates>;
 
// Registry read (rate_symbol + decimals)
pub async fn solana_backend_get_token_info(mint: String) -> Result<TokenInfo>;

Each call reads the persisted Solana canister ID from state and delegates to the backend. This abstracts away signatures, RPC cycles, and ATA creation — icRamp only sees a clean Result.

Listeners & confirmations

We wired a listener pattern for Solana transactions (mirroring what we already had for Bitcoin):

  • Polls the backend get_tx until confirmations ≥ MIN_SOL_CONF (currently 2).
  • Retries with exponential backoff up to a max attempts threshold.
  • Dispatches an action once confirmed, e.g. canceling an order.
pub enum SolanaTransactionAction {
    CancelOrder { order_id: u64, amount: u64, offramper: String, token: Option<String> },
}

This lets icRamp refund deposits automatically if an order is canceled, with the guarantee of on-chain settlement first.

Deposit (SOL/SPL) — implemented

I finished the deposit path for Solana:

  • Validation now uses a fast vs detailed split:
    • get_tx (cheap) remains for the listener path (confirms ≥ MIN_SOL_CONF).
    • get_tx_metadata returns TxMetadata (slot + TransactionStatusMeta) and is used once to validate deposits:
      • SPL: detect a single positive delta in post_token_balances - pre_token_balances for the expected mint equal to the order amount.
      • SOL: detect a single positive lamports delta equal to the order amount.
  • Removed the need for a “canister address” on Solana deposits. Validation is address-agnostic (balance deltas).
  • On success, icRamp marks the signature as processed and calls the backend’s deposit_to_vault_canister to update escrow (vault) state.

Manual ops: there’s now create_solana_order_with_tx for controllers (mirrors EVM/Bitcoin), and I fixed the address type to AddressType::Solana.

Fee model (SOL/SPL)

To price orders correctly, I added Solana fee estimation and unit conversion:

  • Backend exposes estimate_fees(token_mint)SolanaFeeEstimates:
    • lock_lamports: lamports per signature + ATA rent if SPL (payout may need ATA).
    • withdraw_lamports: lamports per signature (refund assumes ATA exists).
    • Implemented via json_request to SOL RPC (getFees, getMinimumBalanceForRentExemption(165)) because the client doesn’t expose typed helpers.
  • On the icRamp side, order_crypto_fee converts these fees:
    • SOL: lamports are already base units.
    • SPL: convert lamports → SOL → token human units via rates, then to base units using decimals, rounding up to avoid under-charging; finally add the admin fee.

Types added to shared crate:

// fees
pub struct SolanaFeeEstimates { pub lock_lamports: u64, pub withdraw_lamports: u64 }
 
// tx metadata used for validation
pub struct TxMetadata { pub signature: String, pub slot: u64, pub meta: TransactionStatusMeta }

Cancel flow for Solana

We extended cancel_order to handle Solana assets:

  • Refund: send back SOL or SPL to the offramper’s address.
  • Listener: spawn a Solana tx listener waiting for 2+ confirmations.
  • Cancel: once confirmed, mark the order as canceled in icRamp state.

This ensures atomicity between L1 and the ICP canister world.

Complete order (SOL/SPL) — implemented

The payout side is now live:

  • Payout on L1 after fiat verification:
    • SOL → send_sol(onramper, amount)
    • SPL → send_spl_token(mint, onramper, amount)
  • Listener: spawn SolanaTransactionAction::CompleteOrder { order_id, amount, onramper, token } and poll get_tx until confirmations ≥ 2.
  • Escrow settle: once confirmed, call complete_order(onramper, amount, token) on the Solana backend to debit the vault, then mark the icRamp order completed and unset processing.

Notes:

  • We rely on balance deltas and signature confirmations.
  • SPL payouts may create the onramper ATA; fee cushion comes from the estimate_fees() model we added (lamports per sig + ATA rent when needed).

Next steps

  • Frontend listener: create order post-confirmation on Solana (skip backend listener for deposits).
  • Authentication: bind Solana addresses to user sessions (sign-in + address proofs).
  • Polish: fee cushions/prioritization fees if needed, and SLA metrics for Solana paths.

Stay Updated

Get notified when I publish new articles about Web3 development, hackathon experiences, and cryptography insights.

You might also like

icRamp Devlog #6 — icRamp Orders with Solana

Everything is ready for us to create orders in the frontend containing solana and executing the full offramping flow.

icRamp Devlog #4 — icRamp frontend Deployment Setup with Solana

Third Chain Fusion grant log: wiring icRamp's core backend with the Solana canister, persisting canister IDs, and preparing escrow flows for SOL/SPL assets.

icRamp Devlog #2 — Solana Canister, Registry & Vault

Second Chain Fusion grant: building a Solana canister with safe token registry and a thin vault to coordinate escrow.