Solana
This guide explains how to interact with the rhino.fi Bridge program on Solana.
Contract Overview
The Bridge program exposes the following function for bridging:
pub fn deposit_with_id(
ctx: Context<DepositLiquidity>,
amount: u64,
commitment_id: u128,
)
IDL
{
version: '0.1.0',
name: 'bridge',
constants: [
{
name: 'AUTHORITY_SEED',
type: 'string',
value: '"authority"',
},
{
name: 'BRIDGE_SEED',
type: 'string',
value: '"rhino_bridge"',
},
],
instructions: [
{
name: 'createBridge',
accounts: [
{
name: 'bridge',
isMut: true,
isSigner: false,
},
{
name: 'admin',
isMut: false,
isSigner: false,
docs: ['The admin of the Bridge'],
},
{
name: 'payer',
isMut: true,
isSigner: true,
docs: ['The account paying for all rents'],
},
{
name: 'systemProgram',
isMut: false,
isSigner: false,
docs: ['Solana ecosystem accounts'],
},
],
args: [],
},
{
name: 'deposit',
accounts: [
{
name: 'bridge',
isMut: false,
isSigner: false,
},
{
name: 'pool',
isMut: false,
isSigner: false,
},
{
name: 'poolAuthority',
isMut: false,
isSigner: false,
},
{
name: 'depositor',
isMut: false,
isSigner: true,
docs: ['The account paying for all rents'],
},
{
name: 'mint',
isMut: false,
isSigner: false,
},
{
name: 'poolAccount',
isMut: true,
isSigner: false,
},
{
name: 'depositorAccount',
isMut: true,
isSigner: false,
},
{
name: 'payer',
isMut: true,
isSigner: true,
docs: ['The account paying for all rents'],
},
{
name: 'tokenProgram',
isMut: false,
isSigner: false,
docs: ['Solana ecosystem accounts'],
},
{
name: 'associatedTokenProgram',
isMut: false,
isSigner: false,
},
{
name: 'systemProgram',
isMut: false,
isSigner: false,
},
{
name: 'eventAuthority',
isMut: false,
isSigner: false,
},
{
name: 'program',
isMut: false,
isSigner: false,
},
],
args: [
{
name: 'amount',
type: 'u64',
},
{
name: 'ethAddressUpper',
type: 'u32',
},
{
name: 'ethAddressLower',
type: 'u128',
},
],
},
{
name: 'depositWithId',
accounts: [
{
name: 'bridge',
isMut: false,
isSigner: false,
},
{
name: 'pool',
isMut: false,
isSigner: false,
},
{
name: 'poolAuthority',
isMut: false,
isSigner: false,
},
{
name: 'depositor',
isMut: false,
isSigner: true,
docs: ['The account paying for all rents'],
},
{
name: 'mint',
isMut: false,
isSigner: false,
},
{
name: 'poolAccount',
isMut: true,
isSigner: false,
},
{
name: 'depositorAccount',
isMut: true,
isSigner: false,
},
{
name: 'payer',
isMut: true,
isSigner: true,
docs: ['The account paying for all rents'],
},
{
name: 'tokenProgram',
isMut: false,
isSigner: false,
docs: ['Solana ecosystem accounts'],
},
{
name: 'associatedTokenProgram',
isMut: false,
isSigner: false,
},
{
name: 'systemProgram',
isMut: false,
isSigner: false,
},
{
name: 'eventAuthority',
isMut: false,
isSigner: false,
},
{
name: 'program',
isMut: false,
isSigner: false,
},
],
args: [
{
name: 'amount',
type: 'u64',
},
{
name: 'commitmentId',
type: 'u128',
},
],
},
{
name: 'createPool',
accounts: [
{
name: 'bridge',
isMut: false,
isSigner: false,
},
{
name: 'pool',
isMut: true,
isSigner: false,
},
{
name: 'poolAuthority',
isMut: false,
isSigner: false,
},
{
name: 'mint',
isMut: false,
isSigner: false,
},
{
name: 'poolAccount',
isMut: true,
isSigner: false,
},
{
name: 'admin',
isMut: false,
isSigner: true,
docs: ['The admin of the Bridge'],
},
{
name: 'payer',
isMut: true,
isSigner: true,
docs: ['The account paying for all rents'],
},
{
name: 'tokenProgram',
isMut: false,
isSigner: false,
docs: ['Solana ecosystem accounts'],
},
{
name: 'associatedTokenProgram',
isMut: false,
isSigner: false,
},
{
name: 'systemProgram',
isMut: false,
isSigner: false,
},
],
args: [],
},
{
name: 'allowOperator',
accounts: [
{
name: 'bridge',
isMut: false,
isSigner: false,
},
{
name: 'operatorStorage',
isMut: true,
isSigner: false,
},
{
name: 'operator',
isMut: false,
isSigner: false,
},
{
name: 'admin',
isMut: false,
isSigner: true,
docs: ['The admin of the Bridge'],
},
{
name: 'payer',
isMut: true,
isSigner: true,
docs: ['The account paying for all rents'],
},
{
name: 'systemProgram',
isMut: false,
isSigner: false,
docs: ['Solana ecosystem accounts'],
},
],
args: [
{
name: 'isAllowed',
type: 'bool',
},
],
},
{
name: 'withdraw',
accounts: [
{
name: 'bridge',
isMut: false,
isSigner: false,
},
{
name: 'pool',
isMut: false,
isSigner: false,
},
{
name: 'poolAuthority',
isMut: false,
isSigner: false,
},
{
name: 'operatorStorage',
isMut: false,
isSigner: false,
},
{
name: 'recipient',
isMut: false,
isSigner: false,
},
{
name: 'operator',
isMut: false,
isSigner: true,
},
{
name: 'mint',
isMut: false,
isSigner: false,
},
{
name: 'poolAccount',
isMut: true,
isSigner: false,
},
{
name: 'recipientAccount',
isMut: true,
isSigner: false,
},
{
name: 'payer',
isMut: true,
isSigner: true,
docs: ['The account paying for all rents'],
},
{
name: 'tokenProgram',
isMut: false,
isSigner: false,
docs: ['Solana ecosystem accounts'],
},
{
name: 'associatedTokenProgram',
isMut: false,
isSigner: false,
},
{
name: 'systemProgram',
isMut: false,
isSigner: false,
},
],
args: [
{
name: 'amount',
type: 'u64',
},
],
},
{
name: 'transferAdmin',
accounts: [
{
name: 'bridge',
isMut: true,
isSigner: false,
},
{
name: 'newAdmin',
isMut: false,
isSigner: false,
docs: ['The admin of the Bridge'],
},
{
name: 'admin',
isMut: false,
isSigner: true,
docs: ['The admin of the Bridge'],
},
{
name: 'systemProgram',
isMut: false,
isSigner: false,
docs: ['Solana ecosystem accounts'],
},
],
args: [],
},
],
accounts: [
{
name: 'bridge',
type: {
kind: 'struct',
fields: [
{
name: 'admin',
docs: ['Account that has admin authority over the Bridge'],
type: 'publicKey',
},
],
},
},
{
name: 'pool',
type: {
kind: 'struct',
fields: [
{
name: 'bridge',
docs: ['Primary key of the Bridge'],
type: 'publicKey',
},
{
name: 'mint',
docs: ['Mint of token A'],
type: 'publicKey',
},
],
},
},
{
name: 'operator',
type: {
kind: 'struct',
fields: [
{
name: 'operator',
docs: ['Public key of the operator'],
type: 'publicKey',
},
{
name: 'isAllowed',
docs: ['Whether the operator has access'],
type: 'bool',
},
],
},
},
],
events: [
{
name: 'BridgedDeposit',
fields: [
{
name: 'from',
type: 'publicKey',
index: false,
},
{
name: 'to',
type: 'publicKey',
index: false,
},
{
name: 'token',
type: 'publicKey',
index: false,
},
{
name: 'amount',
type: 'u64',
index: false,
},
{
name: 'ethAddressUpper',
type: 'u32',
index: false,
},
{
name: 'ethAddressLower',
type: 'u128',
index: false,
},
],
},
{
name: 'BridgedDepositWithId',
fields: [
{
name: 'from',
type: 'publicKey',
index: false,
},
{
name: 'to',
type: 'publicKey',
index: false,
},
{
name: 'token',
type: 'publicKey',
index: false,
},
{
name: 'amount',
type: 'u64',
index: false,
},
{
name: 'commitmentId',
type: 'u128',
index: false,
},
],
},
{
name: 'BridgeWithdraw',
fields: [
{
name: 'from',
type: 'publicKey',
index: false,
},
{
name: 'to',
type: 'publicKey',
index: false,
},
{
name: 'amount',
type: 'u64',
index: false,
},
],
},
],
errors: [
{
code: 6000,
name: 'DepositNotAllowed',
msg: 'DEPOSIT_NOT_ALLOWED',
},
{
code: 6001,
name: 'Unauthorized',
msg: 'UNAUTHORIZED',
},
{
code: 6002,
name: 'NotEnoughLiquidity',
msg: 'NOT_ENOUGH_LIQUIDITY',
},
{
code: 6003,
name: 'NotEnoughBalance',
msg: 'NOT_ENOUGH_BALANCE',
},
{
code: 6004,
name: 'InvalidAmount',
msg: 'INVALID_AMOUNT',
},
],
}
Setup
For this example we’ll use @solana/web3.js
, @solana/spl-token
and @coral-xyz/anchor
to interact with the contract, but you can replace it with any Solana library of your choice.
Make sure to install it first:
yarn add @solana/web3.js @solana/spl-token @coral-xyz/anchor
Set-up helpers for Solana accounts
import { PublicKey } from '@solana/web3.js'
import { getAssociatedTokenAddressSync } from '@solana/spl-token'
import { Buffer } from 'buffer'
export const getSolanaBridgeKey = async (bridgeContractAddress) => {
const programId = new PublicKey(bridgeContractAddress)
return PublicKey.findProgramAddressSync([Buffer.from('rhino_bridge')], programId)[0]
}
export const getSolanaPoolKey = async (bridgeContractAddress, mint) => {
const programId = new PublicKey(bridgeContractAddress)
const bridgeKey = await getSolanaBridgeKey(bridgeContractAddress)
const mintId = new PublicKey(mint)
return PublicKey.findProgramAddressSync([bridgeKey.toBuffer(), mintId.toBuffer()], programId)[0]
}
export const getSolanaPoolAuthority = async (bridgeContractAddress, mint) => {
const programId = new PublicKey(bridgeContractAddress)
const bridgeKey = await getSolanaBridgeKey(bridgeContractAddress)
const mintId = new PublicKey(mint)
return PublicKey.findProgramAddressSync(
[bridgeKey.toBuffer(), mintId.toBuffer(), Buffer.from('authority')],
programId,
)[0]
}
export const getSolanaPoolTokenAccount = async (bridgeContractAddress, mint) => {
const poolAuthority = await getSolanaPoolAuthority(bridgeContractAddress, mint)
const mintId = new PublicKey(mint)
return getAssociatedTokenAddressSync(mintId, poolAuthority, true)
}
export const getSolanaDepositorAccount = async (secondaryWalletAddress, mint) => {
const mintId = new PublicKey(mint)
const depositor = new PublicKey(secondaryWalletAddress)
return getAssociatedTokenAddressSync(mintId, depositor, true)
}
Interacting with the Contract
To execute a bridge transaction, call the depositWithId
(ERC20) or depositNativeWithId
(native) function as follows:
The commitmentId
required by the contract corresponds to the quoteId
returned by the API.
To obtain the commitmentId
, first request a quote from the API, then commit it. The response will include the quoteId
, which should be used as the commitmentId
in the contract call.
See Quickstart for details.
import {Connection, PublicKey} from '@solana/web3.js'
import {AnchorProvider, Program, BN} from '@coral-xyz/anchor'
import {
getSolanaBridgeKey,
getSolanaDepositorAccount,
getSolanaPoolAuthority,
getSolanaPoolKey,
getSolanaPoolTokenAccount,
} from './solanaBridgeHelpers'
import {IDL} from "./idl";
import getBridgeConfigs from './getBridgeConfigs'
// Import a solana wallet / or use your wallet library
const secretKey = base58.decode(SOLANA_PRIVATE_KEY)
const keypair = Keypair.fromSecretKey(new Uint8Array(secretKey))
const wallet = new Wallet(keypair)
export const callSolanaBridgeContract = async (
{
token,
chain, // SOLANA
commitmentId,
amount,
}) => {
// See API Integration -> Fetch Bridge Configs for the full implementation
const configs = await getBridgeConfigs()
const chainConfig = configs[chain]
const solanaWalletAddress = wallet.publicKey.toString()
const tokenConfig = chainConfig.tokens[token]
const tokenAmount = +amount * 10 ** tokenConfig.decimals
const tokenAddress = tokenConfig.address
const bridgeContractAddress = chainConfig.contractAddress
const connection = new Connection(chainConfig.rpc)
const options = AnchorProvider.defaultOptions()
const anchorProvider = new AnchorProvider(connection, wallet, options)
const depositor = new PublicKey(solanaWalletAddress)
const bridgeKey = await getSolanaBridgeKey(bridgeContractAddress)
const poolKey = await getSolanaPoolKey(bridgeContractAddress, tokenAddress)
const poolAuthority = await getSolanaPoolAuthority(bridgeContractAddress, tokenAddress)
const poolAccount = await getSolanaPoolTokenAccount(bridgeContractAddress, tokenAddress)
const depositorAccount = await getSolanaDepositorAccount(solanaWalletAddress, tokenAddress)
const program = new Program(IDL, bridgeContractAddress, anchorProvider)
const accounts = {
bridge: bridgeKey,
pool: poolKey,
poolAuthority,
poolAccount: poolAccount,
depositor,
mint: new PublicKey(tokenAddress),
depositorAccount: depositorAccount,
}
const depositTxHash = await program.methods['depositWithId'](new BN(tokenAmount), new BN(commitmentId, 'hex'))
.accounts(accounts)
.signers([])
.rpc()
await connection.getSignatureStatus(depositTxHash)
return depositTxHash
}