
icRamp Devlog #3 — icRamp Canister & Solana Integration
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
returnsTxMetadata
(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.
- SPL: detect a single positive delta in
- 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)
- SOL →
- Listener: spawn
SolanaTransactionAction::CompleteOrder { order_id, amount, onramper, token }
and pollget_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.