icRamp Devlog #2 — Solana Canister, Registry & Vault

icRamp Devlog #2 — Solana Canister, Registry & Vault

8/15/20258 min • icramp
ICPSolanaChain FusionSPLCanisters

This post documents the second grant I received in the ICP Chain Fusion: Bitcoin Edition: after a $5K grant for Bitcoin + Runes integration, I earned a $10K grant to design and ship the Solana canister and wire it into icRamp. Below is the full build: patches, architecture, calls, and CLI tests.

Why a Solana canister for icRamp?

  • Escrow UX: the canister acts as a neutral escrow for SOL/SPL funds during P2P orders.
  • Deterministic ops: we derive ATAs, gate tokens via a registry, and keep a thin vault mirrored on-chain by the backend (which triggers the actual transfers).
  • No panics: everything returns structured errors (candid Result), so the frontend can surface precise failures.

Workspace & patches (wasm-friendly Solana)

We need the Solana crates patched to avoid JS bindings under wasm32-unknown-unknown:

# Cargo.toml (workspace)
[workspace]
members = ["backend", "bitcoin_backend", "solana_backend", "tests"]
resolver = "2"
 
[workspace.dependencies]
candid = "0.10"
serde = "1.0"
ic-cdk = "0.17"
ic-cdk-timers = "0.11"
ic-btc-interface = "0.2"
bitcoin = "0.32"
bitcoin_backend = { path = "bitcoin_backend", default-features = false, features = ["types", "canister"] }
num-traits = "0.2.19"
base64 = "0.22"
 
# Transitive dependency
getrandom = { version = "0.2.16", default-features = false, features = ["custom"] }
 
# Forked Solana crates without wasm-bindgen requirements
[patch.crates-io]
solana-program = { git = "https://github.com/dfinity/solana-sdk", tag = "46ca4e2-js-feature-flag" }
solana-signature = { git = "https://github.com/dfinity/solana-sdk", tag = "46ca4e2-js-feature-flag" }
solana-message = { git = "https://github.com/dfinity/solana-sdk", tag = "46ca4e2-js-feature-flag" }
solana-pubkey = { git = "https://github.com/dfinity/solana-sdk", tag = "46ca4e2-js-feature-flag" }
solana-system-interface = { git = "https://github.com/dfinity/solana-system-program", tag = "6185b40-js-feature-flag" }
solana-instruction = { git = "https://github.com/dfinity/solana-sdk", tag = "46ca4e2-js-feature-flag" }
solana-account = { git = "https://github.com/dfinity/solana-sdk", tag = "46ca4e2-js-feature-flag" }
solana-keypair = { git = "https://github.com/dfinity/solana-sdk", tag = "46ca4e2-js-feature-flag" }
solana-feature-gate-interface = { git = "https://github.com/dfinity/solana-sdk", tag = "46ca4e2-js-feature-flag" }
solana-program-entrypoint = { git = "https://github.com/dfinity/solana-sdk", tag = "46ca4e2-js-feature-flag" }
solana-program-error = { git = "https://github.com/dfinity/solana-sdk", tag = "46ca4e2-js-feature-flag" }
solana-transaction = { git = "https://github.com/dfinity/solana-sdk", tag = "46ca4e2-js-feature-flag" }
solana-sysvar = { git = "https://github.com/dfinity/solana-sdk", tag = "46ca4e2-js-feature-flag" }
solana-sysvar-id = { git = "https://github.com/dfinity/solana-sdk", tag = "46ca4e2-js-feature-flag" }
solana-nonce = { git = "https://github.com/dfinity/solana-sdk", tag = "46ca4e2-js-feature-flag" }
solana-hash = { git = "https://github.com/dfinity/solana-sdk", tag = "46ca4e2-js-feature-flag" }

Solana canister crate:

# solana_backend/Cargo.toml
[package]
name = "solana_backend"
version = "0.1.0"
edition = "2024"
 
[lib]
crate-type = ["cdylib", "rlib"]
 
[dependencies]
ic-cdk = { workspace = true }
ic-cdk-timers = { workspace = true }
ic-stable-structures = "0.6.5"
ic-ed25519 = { version = "0.2.0", optional = false }
base64 = { workspace = true }
candid = { workspace = true }
serde = { workspace = true }
bs58 = "0.5.1"
 
sol_rpc_client = { package = "sol_rpc_client", git = "https://github.com/dfinity/sol-rpc-canister", rev = "715f278", features = ["ed25519"], default-features = false }
sol_rpc_types  = { package = "sol_rpc_types",  git = "https://github.com/dfinity/sol-rpc-canister", rev = "715f278", default-features = false }
 
solana-program = "=2.3.0"
solana-signature = "=2.3.0"
solana-transaction = "=2.2.3"
solana-transaction-status-client-types = "=2.3.3"
solana-message = "=2.4.0"
solana-pubkey = "=2.4.0"
solana-instruction = "=2.3.0"
solana-system-interface = "1.0.0"
 
num-traits = { workspace = true }
thiserror = "2.0"
 
[dependencies.getrandom]
version = "0.2.16"
default-features = false
features = ["custom"]
 
[features]
default = []

We pin sol_rpc_client / sol_rpc_types at rev = "715f278" to match the canister interface we use (data slicing, etc.).

Architecture

Three blocks:

  1. Solana operations
  • Derive canister & per-owner addresses.
  • Compute ATAs; transfer SOL/SPL.
  • SPL sends automatically create destination ATA when missing.
  1. Token registry (allowlist)
  • Register mints via register_tokens(Vec<TokenInfo>):
    • decimals: u8
    • symbol: String (UI)
    • rate_symbol: String (used by pricing, e.g. “USDC”, “JUP”, etc.)
  • The canister fetches decimals on-chain (Mint byte 44) using getAccountInfo(Base64) with dataSlice { offset: 44, length: 1 }.
  • Reject unregistered or unsupported mints.

APIs:

  • get_registered_tokens() still returns HashMap<mint, TokenInfo>.
  • register_tokens([(mint, symbol, rate_symbol)]) validates mints and stores decimals and symbols.
  • get_token_info(mint) returns the full TokenInfo.

icRamp uses get_token_info to pick rate_symbol and decimals when converting SOL fees into SPL base units.

  1. Vault (thin canister layer)
  • Two stable maps: OFFRAMPER_VAULTS and ONRAMPER_VAULTS.
  • Escrow-like flows: deposit → lock → complete and deposit → lock → unlock → cancel.
  • Backend pairs vault mutations with real on-chain sends.

Architecture overview

icRamp Solana escrow flow — overview

Build, DID & Deploy

# Build + extract candid
cargo build --release --target wasm32-unknown-unknown --package solana_backend
candid-extractor target/wasm32-unknown-unknown/release/solana_backend.wasm > solana_backend/solana_backend.did
 
# Generate TS bindings for frontend
dfx generate

Deploy Solana RPC canister + API keys:

dfx deploy sol_rpc
dfx canister call sol_rpc updateApiKeys "(vec {
  record { variant { AlchemyDevnet }; opt \"$ALCHEMY_KEY\" };
  record { variant { AnkrDevnet };   opt \"$ANKR_KEY\"   };
})"

Deploy the Solana backend canister (Devnet example):

dfx deploy solana_backend --argument "(
  variant { Reinstall = record {
    sol_rpc_canister_id = opt principal \"tghme-zyaaa-aaaar-qarca-cai\";
    ed25519_key_name    = variant { LocalDevelopment };
    network             = variant { Devnet };
    proxy_url           = \"https://ic2p2ramp.xyz\";
  }}
)"

Upgrades later:

dfx deploy solana_backend --argument "(variant { Upgrade = null })" --upgrade-unchanged

Using the canister

Addresses & balances

# Canister Solana pubkey
dfx canister call solana_backend canister_solana_account '()'
# Get SOL balance
dfx canister call solana_backend get_balance '("<solana-pubkey>")'

Registry (allowlist) flow

# 1) Register mint (decimals resolved on-chain)
dfx canister call solana_backend register_tokens '(vec { "FxoGGtuyjfVybdA3X5WgxzNhjvSN73R5zqPYg3on8hwE" })'
 
# 2) Inspect registry
dfx canister call solana_backend get_registered_tokens '()'
# -> (vec { record { "<mint>"; 6 : nat8 } })
 
# 3) Gate checks
dfx canister call solana_backend is_token_supported '("FxoGGtuyjfVybdA3X5WgxzNhjvSN73R5zqPYg3on8hwE")'
# -> (variant { Ok })

Transfers

# SOL (lamports)
dfx canister call solana_backend send_sol '(opt principal "u6s2n-gx777-77774-qaaba-cai", "CnzUv9EqfVv5yiYWugHiVzmvuBBzRxHYK64DkLhSWsUj", 1000)'
# -> returns tx signature
 
# SPL (base units)
dfx canister call solana_backend send_spl_token '(opt principal "u6s2n-gx777-77774-qaaba-cai", "FxoGGtuyjfVybdA3X5WgxzNhjvSN73R5zqPYg3on8hwE", "CnzUv9EqfVv5yiYWugHiVzmvuBBzRxHYK64DkLhSWsUj", 1000000)'
# tip: if recipient ATA is missing, the canister creates it in the same tx before transfer

Base units everywhere: e.g. 1_000_000 for 1 token with 6 decimals.

Vault flows (tested)

Sequence: register_token → deposit_to_vault → lock_funds → send (create ATA if needed) → complete_order

Let's set up the principal identities for the onramper and offramper, plus a predefined mint:

# create if not present
# dfx identity new maker
dfx identity use maker
export OFF=$(dfx identity get-principal)
 
# likewise for onramper
# dfx identity new taker
dfx identity use taker
export ON=$(dfx identity get-principal)
 
MINT="FxoGGtuyjfVybdA3X5WgxzNhjvSN73R5zqPYg3on8hwE"

A) SOL — deposit → lock → complete

# deposit 1_000_000 lamports to OFF
dfx canister call solana_backend deposit_to_vault_canister '("'$OFF'", 1000000, null)'
# -> (variant { Ok })
dfx canister call solana_backend get_offramper_deposits '("'$OFF'")'
# -> lamports = 1_000_000
 
# lock to ON
dfx canister call solana_backend lock_funds '("'$OFF'", "'$ON'", 1000000, null)'
# -> (variant { Ok })
dfx canister call solana_backend get_offramper_deposits '("'$OFF'")'
# -> lamports = 0
dfx canister call solana_backend get_onramper_deposits '("'$ON'")'
# -> lamports = 1_000_000
 
# complete (release ON)
dfx canister call solana_backend complete_order '("'$ON'", 1000000, null)'
# -> (variant { Ok })
dfx canister call solana_backend get_onramper_deposits '("'$ON'")'
# -> lamports = 0

B) SPL — deposit → lock → unlock → cancel

# deposit 1 token (6 decimals) to OFF
dfx canister call solana_backend deposit_to_vault_canister '("'$OFF'", 1000000, opt "'$MINT'")'
# -> (variant { Ok })
dfx canister call solana_backend get_offramper_deposits '("'$OFF'")'
# -> tokens[{MINT}] = 1_000_000
 
# lock to ON
dfx canister call solana_backend lock_funds '("'$OFF'", "'$ON'", 1000000, opt "'$MINT'")'
# -> (variant { Ok })
dfx canister call solana_backend get_onramper_deposits '("'$ON'")'
# -> tokens[{MINT}] = 1_000_000
 
# unlock back to OFF
dfx canister call solana_backend unlock_funds '("'$OFF'", "'$ON'", 1000000, opt "'$MINT'")'
# -> (variant { Ok })
dfx canister call solana_backend get_offramper_deposits '("'$OFF'")'
# -> tokens[{MINT}] = 1_000_000
dfx canister call solana_backend get_onramper_deposits '("'$ON'")'
# -> tokens = {}
 
# cancel original OFF deposit
dfx canister call solana_backend cancel_deposit '("'$OFF'", 1000000, opt "'$MINT'")'
# -> (variant { Ok })
dfx canister call solana_backend get_offramper_deposits '("'$OFF'")'
# -> tokens = {}

Error handling & safety

  • No panics in public APIs: RPC/parse failures bubble as Err variants (e.g., RpcError, UnsupportedToken, InsufficientBalance).
  • Registry gate: is_token_supported checks allowlist before any vault mutation or transfer; decimals are canonical from chain.
  • Transfers: SPL transfer instruction creates the recipient ATA when missing; source ATA existence is checked up-front for clear errors.

Troubleshooting

  • UnsupportedToken(..): register the mint first via register_tokens.
  • InvalidAccountData on SPL transfer: destination ATA didn’t exist; ensure the create-ATA instruction precedes transfer (the canister path handles this).
  • Weird amounts: pass base units: lamports for SOL; raw mint units for SPL (10^decimals).

Failure & rollback

Expanded flows with UI, parallel orders, and error/timeout handling

Closing

This wraps the Solana canister milestone for icRamp under the second Chain Fusion grant. In the next blog entry we will wire-up the solana backend to our onramper backend and plug it all together in the frontend to see solana p2p onramping orders live on devnet.

Stay tuned to get all the details :)

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 #3 — icRamp Canister & Solana Integration

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.