Skip to content

Solana ​

OpenRampKit supports Solana as a destination and as a source. This page tells you how to:

  • Send deposits to USDC (or SOL) on Solana.
  • Let users pay from a Solana wallet.
  • Connect a Solana wallet in the browser with @openrampkit/solana.

For every export of @openrampkit/solana, read the API reference.

What works ​

FlowProviderSurfaceNotes
Card or local QR to USDC on SolanaOnramp to USDC on Base, then Relay bridgeREDIRECT or QR, then noneTwo legs. Relay moves the USDC from Base to Solana.
EVM wallet to USDC on SolanaRelay walletWALLET_TX (EVM)The user signs on the EVM chain. Relay delivers on Solana.
Deposit address to USDC on SolanaRelay transferDEPOSIT_ADDRESSThe user sends from any EVM wallet or exchange.
Solana wallet to any chainRelay walletWALLET_TX (Solana)The Solana wallet signs Relay's Solana transaction.
Solana wallet to USDC on SolanaRelay wallet (same chain)WALLET_TX (Solana)A plain SPL transfer. The server checks it on chain.
Withdraw to a Solana addressRelay walletWALLET_TXThe user types a Solana address on the "To wallet" tab.
Solana devnet demoMock adapter solanaLocalChainWALLET_TX (Solana)A real SPL transfer of devnet USDC. The server checks it on chain.
Demo and testsMock adapterallNo money moves.

Chain and token ids ​

Chains are CAIP-2 ids. Solana mints are base58 and case-sensitive. OpenRampKit keeps their case. It lowercases EVM addresses only.

NameValueExport from @openrampkit/core
Solana mainnetsolana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdpSOLANA_MAINNET
Solana devnetsolana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1SOLANA_DEVNET
USDC mint (mainnet)EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1vSOLANA_USDC_MINT, also USDC[SOLANA_MAINNET]
USDC mint (devnet)4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDUSOLANA_DEVNET_USDC_MINT, also TESTNET_USDC[SOLANA_DEVNET]
Native SOLnative (9 decimals)SOLANA_NATIVE_DECIMALS

Relay uses its own chain id for Solana: 792703809. The Relay adapter maps it for you.

Send deposits to Solana ​

Create the session on your server with a Solana destination:

ts
import { SOLANA_MAINNET, SOLANA_USDC_MINT } from '@openrampkit/core'

const { clientSecret } = await openramp.sessions.create({
  userId: user.id,
  country: 'ID',
  destination: {
    type: 'crypto',
    chain: SOLANA_MAINNET,
    token: SOLANA_USDC_MINT,
    symbol: 'USDC',
    decimals: 6,
    address: user.solanaAddress, // base58 owner address, not a token account
  },
})

The server checks the address format. The address is the owner address. Relay and the SPL transfer send to the associated token account of that owner.

The planner finds these pathways:

  • Fiat methods (card, QRIS, VietQR, and more) go to USDC on Base first. Then the Relay bridge leg moves the USDC to Solana.
  • "Pay with wallet" and "Transfer crypto" use Relay directly.

Pay from a Solana wallet ​

Install the wallet adapter in your frontend:

bash
pnpm add @openrampkit/solana

The packages are not on npm yet. See Try it before the npm release.

solanaWallet() finds wallets through Wallet Standard. Phantom, Solflare, Backpack and most other Solana wallets register there.

tsx
'use client'
import { useMemo, useState } from 'react'
import { useAccount } from 'wagmi'
import { combineWallets } from '@openrampkit/core'
import { OpenRampProvider, DepositButton } from '@openrampkit/react'
import { solanaWallet } from '@openrampkit/solana'
import { wagmiWallet } from '@openrampkit/wagmi'
import { wagmiConfig } from '@/lib/wagmi'

export function Deposit() {
  const { isConnected } = useAccount()
  const sol = useMemo(() => solanaWallet({ rpcUrl: process.env.NEXT_PUBLIC_SOLANA_RPC_URL }), [])
  const [solAddress, setSolAddress] = useState<string>()
  const wallet = useMemo(
    () => (isConnected ? combineWallets(wagmiWallet(wagmiConfig), sol) : sol),
    [isConnected, sol, solAddress],
  )
  return (
    <OpenRampProvider baseUrl="/api/openramp" wallet={wallet}>
      <button onClick={() => sol.connect().then(setSolAddress)}>Connect Solana wallet</button>
      <DepositButton getClientSecret={getClientSecret} />
    </OpenRampProvider>
  )
}

combineWallets joins an EVM wallet and a Solana wallet:

  • It joins the accounts and the balances of both wallets.
  • It sends each WALLET_TX to the wallet of that chain (eip155 or solana).
  • The modal sends the account of the chain that the user pays from. For example, it sends the Solana address when the user pays with Solana USDC.

The controller reads the wallet once, when it starts. Connect the wallet before you open the modal, or open the modal again after the user connects.

Options ​

ts
solanaWallet({
  wallet,                // a Wallet Standard wallet, or a function that returns it. Default: the first Solana wallet.
  walletName: 'Phantom', // pick a registered wallet by name
  chain: SOLANA_MAINNET, // or SOLANA_DEVNET
  rpcUrl: 'https://...', // default: the public RPC of the chain (rate-limited)
  waitBetweenTxs: true,  // wait for each confirmation before the next transaction. Default true.
  waitForLast: false,    // also wait for the last confirmation. Default false.
  commitment: 'confirmed',
  confirmTimeoutMs: 60_000,
  tokens: [{ mint: '...', symbol: 'PYUSD', decimals: 6 }], // extra SPL balances, besides USDC
})

Use your own RPC in production. The public Solana RPC has low rate limits. The adapter uses the RPC for blockhashes, lookup tables, balances and confirmations.

Behavior ​

MethodWhat it does
connect()Calls standard:connect and returns the first account address
getAccounts()The wallet accounts on the chosen chain, as { chain, address }. [] when not connected or on the server.
getBalances()SOL and USDC (and your tokens) from the RPC. One failing call does not hide the other balances.
sendTransactions(chain, txs)Builds, signs and sends each Solana transaction in order. Returns the last signature (base58).

The adapter signs with solana:signAndSendTransaction. When the wallet does not have it, the adapter signs with solana:signTransaction and sends through the RPC.

The API reference lists all the methods and the other exports: getSolanaWallets(), isSolanaWallet(), walletStandardChain(), associatedTokenAddress() and DEFAULT_SOLANA_RPC_URLS.

Solana transactions in WALLET_TX ​

A WALLET_TX surface on Solana carries SolanaTxRequest items (kind: 'solana'). The wallet adds the fee payer and a recent blockhash.

typeFieldsWho makes it
instructionsinstructions, addressLookupTableAddressesRelay, for a Solana origin. The adapter builds a v0 transaction and uses the lookup tables.
transactiontransaction (base64 wire format)An adapter that builds the whole transaction
transferto, mint (or native), amount (base units), decimalsSame-chain moves. For an SPL token, the transaction creates the recipient token account when it is missing (idempotent), then sends TransferChecked. Token-2022 mints work.

TxRequest is EvmTxRequest | SolanaTxRequest. Use isSolanaTx(tx) to tell them apart.

Server-side checks ​

The Relay adapter checks same-chain Solana moves on chain. It does not trust the signature that the browser sends.

  • getSignatureStatuses: the signature must be confirmed or finalized, with no error.
  • getTransaction (jsonParsed): the recipient must get at least the quoted amount. For SPL tokens, the adapter adds the balance changes of the recipient's token accounts for the mint. For SOL, it uses the lamport change of the recipient.
  • The block time must not be before the payment started (5 minutes of clock slack).
  • One signature completes one payment only. A second session with the same signature fails.

For a "Transfer crypto" deposit to the destination itself, the adapter reads getTokenAccountsByOwner and getSignaturesForAddress. It counts each new signature for one session only.

Set a Solana RPC for these checks:

ts
relay({ apiKey: process.env.RELAY_API_KEY, rpcUrls: { [SOLANA_MAINNET]: process.env.SOLANA_RPC_URL! } })

Limits ​

  • A Relay open deposit address on Solana (the user sends from Solana) needs a Relay API key. Without a key, Relay refuses the request. Deposit addresses on EVM chains that deliver to Solana work without a key.
  • Relay's Solana route needs a Solana user. The quote uses a placeholder until the user connects a Solana wallet. The adapter quotes again with the real address when the payment starts.
  • Solana devnet has metadata and a USDC mint, but Relay does not route devnet. Use the mock adapter's solanaLocalChain leg for devnet demos. See Solana devnet.
  • The mock offramp (withdraw to cash) takes EVM USDC only.

Solana devnet ​

You can try a real Solana payment on devnet. The tokens on devnet have no value.

In the playground ​

Open the playground. Set Mode to Testnet (real wallet) and Network to Solana Devnet. Or open /playground/?mode=testnet&network=solana-devnet.

  1. Connect Phantom, Solflare, Backpack or another Wallet Standard wallet.
  2. Get devnet USDC from the Circle faucet. Choose Solana Devnet.
  3. Get a little devnet SOL for fees from the Solana faucet.
  4. In the widget, select Pay with wallet, enter an amount, and confirm in your wallet.

Your wallet signs one SPL transfer of devnet USDC to your own address. Thus you get the USDC back. The server checks the signature on chain, then the widget shows Deposit complete. The page shows a link to the transaction in Solana Explorer.

The page asks the wallet to sign only (solana:signTransaction). Then it sends the transaction through the devnet RPC. Thus the transaction goes to devnet, even when the network setting of the wallet is mainnet.

The parts ​

  • Server: the mock adapter with solanaLocalChain on devnet. See Solana local chain leg.
  • Session: a destination of devnet USDC to the address of the user.
  • Wallet: solanaWallet({ chain: SOLANA_DEVNET }) from @openrampkit/solana.
ts
import { SOLANA_DEVNET, SOLANA_DEVNET_USDC_MINT } from '@openrampkit/core'
import { mockAdapter } from '@openrampkit/adapter-mock'

const openramp = createOpenRamp({
  // ...
  adapters: [mockAdapter({ methods: ['wallet'], solanaLocalChain: { chain: SOLANA_DEVNET, rpcUrl: 'https://api.devnet.solana.com', mint: SOLANA_DEVNET_USDC_MINT } })],
})

await openramp.sessions.create({
  userId: user.id,
  destination: { type: 'crypto', chain: SOLANA_DEVNET, token: SOLANA_DEVNET_USDC_MINT, symbol: 'USDC', decimals: 6, address: user.solanaAddress },
})

The server completes the payment only when all of these are true:

  • The signature is confirmed or finalized, with no error.
  • The transaction is in a slot at or after the slot when the payment started.
  • The transaction moves at least the quoted amount of the mint into a token account of the recipient.
  • No other payment used the signature before.

From Node ​

The repo has a script that runs the same flow with a local devnet key. It uses the same packages: the server, the mock adapter, @openrampkit/client and solanaWallet.

bash
pnpm solana:key               # create the key (once) and show its address and balances
pnpm solana:key --airdrop     # also ask the public faucet for devnet SOL (rate limited)
pnpm solana:settle            # pay 1 devnet USDC to itself, then check that a second use of the signature fails
TOKEN=sol pnpm solana:settle  # pay 0.001 SOL to itself (needs devnet SOL only)

The key is in examples/playground/.solana-devnet-key.json. This file is in .gitignore. The scripts never print the secret. Fund the address from the faucets above.

Demo ​

The Next.js demo (pnpm dev:example) has a USDC on Solana destination. Pick it in the playground. With mock providers, the QRIS or card pathway goes to Base and then bridges to Solana. Set Wallet to "wagmi and Solana wallet" and click Connect Solana wallet to pay from Phantom or another wallet.

Proof on devnet ​

A real payment on Solana devnet went through the same code path as the playground: the mock solana-onchain leg, the server, and solanaPaidTo.

ItemValue
Sessionors_0a715aa02e6fa759a3c522e3
Payment1 devnet USDC (4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU), paid back to the payer
Signature4cAv4h7P…MnBxiKY
ResultThe session moved to COMPLETED.
ReplayA second session that used the same signature failed: "This transaction was already used for another payment."

Run it again with pnpm solana:settle after you fund the devnet key (see pnpm solana:key).

MIT licensed