Swapping
Integrations
Advanced Scenarios
Vault Swaps
Solana

Solana Vault Swaps

Initiating a Solana Vault Swap requires making a program call to Chainflip's Swap Endpoint program. The program addresses can be found in Testnet addresses and Mainnet addresses.

Solana Vault Swaps follow the same pattern mentioned in the Vault Swaps overview:

  1. Request the swap parameter encoding.
  2. Use the return values to build a custom Solana transaction.
  3. Sign and broadcast the transaction to the Solana network.

Swap Event Data Account

To ensure the correctness and the data availability of the Vault Swap's metadata, the data is stored on-chain (on Solana) until the swap is processed by the Chainflip protocol. In the Vault swap transaction the Swap Endpoint program will create a program derived address (PDA) where the data is stored.

The user will need to pay a very small fees to create the account, also known as rent in the Solana blockchain. The rent is the amount of SOL required to store the data on-chain. Once the swap is processed the Chainflip protocol will automatically close the account and return the full rent to the user.

When creating the account two seeds are used - the user address and an arbitrary seed provided by the user. This arbitrary seed is to allow the same user to make multiple Vault Swaps without colliding with previous swaps before those accounts are closed.

There is no security risk if that value is predictable (e.g. increasing counter) or even if two users were to use the same value. It is recommended to just use a random seed generated on the fly for each swap. The seed must be a hex string of up to 32 bytes.

Swap Parameter Encoding via Broker API

1. Request the encoded parameters via RPC

Example Request for a swap from SOL to ETH:

curl -H "Content-Type: application/json" -d '{
    "id": 1,
    "jsonrpc": "2.0",
    "method": "broker_request_swap_parameter_encoding",
    "params": {
        "source_asset": { "chain": "Solana", "asset": "SOL" },
        "destination_asset": { "chain": "Ethereum", "asset": "ETH" },
        "destination_address": "0xc64722AD9613851b10E26fF8118A7696A0f956f2",
        "broker_commission": 0,
        "extra_parameters": {
            "chain": "Solana",
            "from": "BdyHK5DckpAFGcbZveGLPjjMEaADGfNeqcRXKoyd33kA",
            "seed": "0x1234",
            "input_amount": 1000000000,
            "refund_parameters": {
                "retry_duration": 10,
                "refund_address": "BdyHK5DckpAFGcbZveGLPjjMEaADGfNeqcRXKoyd33kA",
                "min_price": "0x0"
            }
        }
    }
}' http://my-broker-api:9944

Example response, a Solana Instruction (opens in a new tab):

{
  "jsonrpc": "2.0",
  "result": {
    "chain": "Solana",
    "program_id": "35uYgHdfZQT4kHkaaXQ6ZdCkK5LFrsk43btTLbGCRCNT",
    "accounts": [
      {
        "pubkey": "BttvFNSRKrkHugwDP6SpnBejCKKskHowJif1HGgBtTfG",
        "is_signer": false,
        "is_writable": false
      },
      {
        "pubkey": "EWaGcrFXhf9Zq8yxSdpAa75kZmDXkRxaP17sYiL6UpZN",
        "is_signer": false,
        "is_writable": true
      },
      {
        "pubkey": "BdyHK5DckpAFGcbZveGLPjjMEaADGfNeqcRXKoyd33kA",
        "is_signer": true,
        "is_writable": true
      },
      {
        "pubkey": "EHGpoTBQKvE1BP8i1chsE5TUhjD49WUsPn2otmRLwg1d",
        "is_signer": false,
        "is_writable": true
      },
      {
        "pubkey": "2tmtGLQcBd11BMiE9B1tAkQXwmPNgR79Meki2Eme4Ec9",
        "is_signer": false,
        "is_writable": true
      },
      {
        "pubkey": "11111111111111111111111111111111",
        "is_signer": false,
        "is_writable": false
      }
    ],
    "data": "0xa3265ce2f3698dc400ca9a3b000000000100000014000000c64722ad9613851b10e26ff8118a7696a0f956f201000000006a000000000a0000009e0d6a70e12d54edf90971cc977fa26a1d3bb4b0b26e72470171c36b0006b01f000000000000000000000000000000000000000000000000000000000000000000009059e6d854b769a505d01148af212bf8cb7f8469a7153edce8dcaedd9d299125000000020000001234"
  },
  "id": 1
}

2. Construct the Transaction

The broker_request_swap_parameter_encoding RPC call returns the instruction that will initiate a Vault swap. As per a regular Solana instruction (e.g. see web3.js Instruction (opens in a new tab)), it contains:

  • ProgramId: The address of the Chainflip's Solana program to call.
  • Accounts: The accounts required to execute the instruction.
  • Data: The instruction data to send to the program.

These are the three parameters from an instruction that shall be included in a Solana instruction. The instruction will make the adequate call to the SwapEndpoint program. In can be used in combination with other instructions in the same transaction.

Below you can find an example of how to create Solana transaction with the Vault Swap instruction using the Solana web3.js library.

import {
  PublicKey,
  sendAndConfirmTransaction,
  TransactionInstruction,
  Transaction,
  AccountMeta,
} from '@solana/web3.js';
 
// Submit the encoding request to the Broker API.
const response = await fetch("http://my-broker-api:9944", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    id: 1,
    jsonrpc: "2.0",
    method: "broker_request_swap_parameter_encoding",
    params: {
        source_asset: { "chain": "Solana", "asset": "SOL" },
        destination_asset: { "chain": "Ethereum", "asset": "ETH" },
        destination_address: "0xc64722AD9613851b10E26fF8118A7696A0f956f2",
        broker_commission: 0,
        extra_parameters: {
            chain: "Solana",
            from: "BdyHK5DckpAFGcbZveGLPjjMEaADGfNeqcRXKoyd33kA",
            seed: "0x1234",
            input_amount: 1000000000,
            refund_parameters: {
                "retry_duration": 10,
                "refund_address": "BdyHK5DckpAFGcbZveGLPjjMEaADGfNeqcRXKoyd33kA",
                "min_price": "0x0"
            }
        }
    },
  }),
});
 
const vaultSwapDetails = await response.json() as {
  chain: string;
  program_id: string;
  accounts: {
    pubkey: string;
    is_signer: boolean;
    is_writable: boolean;
  }[];
  data: string;
};
 
// Convert vaultSwapDetails.instruction.accounts into web3.AccountMeta[]
const keys: AccountMeta[] = [];
for (const account of vaultSwapDetails.accounts) {
  keys.push({
    pubkey: new PublicKey(account.pubkey),
    isSigner: account.is_signer,
    isWritable: account.is_writable,
  });
}
 
const instruction = new TransactionInstruction({
  keys,
  programId: new PublicKey(vaultSwapDetails.program_id),
  data: Buffer.from(vaultSwapDetails.data.slice(2), 'hex'),
});
 
const transaction = new Transaction().add(instruction);

3. Sign and Send

The simplest way to sign, send and wait for confirmation for the transaction created previously in a single step is the following:

await sendAndConfirmTransaction(
  connection,
  transaction,
  [userKeypair]
);

Interface References

Vault Contract interface references

The Anchor IDL for the SwapEndpoint can be found on-chain in any Solana explorer such as Solscan (opens in a new tab) or SolExplorer (opens in a new tab).

The IDL can be used to make program calls to the SwapEndpoint program on Solana. The relevant functions for the Vault swaps are x_swap_native and x_swap_token for native SOL or for SPL-tokens respectively. However, some of the parameters are not static (e.g. aggKey), which makes the manual crafting of these transactions a bit more complex.

These dynamic values can be read from either the State Chain or Solana itself, since they are all stored on-chain. For easier integration the Broker API can be used to get the instruction to call the SwapEndpoint program. That will return the correct encoded data so the only step left is to send and sign the transaction.

pub struct CcmParams {
    pub message: Vec<u8>,
    pub gas_amount: u64,
}
 
pub struct SwapNativeParams {
    amount: u64,
    dst_chain: u32,
    dst_address: Vec<u8>,
    dst_token: u32,
    ccm_parameters: Option<CcmParams>,
    cf_parameters: Vec<u8>,
}
 
pub struct SwapTokenParams {
    pub amount: u64,
    pub dst_chain: u32,
    pub dst_address: Vec<u8>,
    pub dst_token: u32,
    pub ccm_parameters: Option<CcmParams>,
    pub cf_parameters: Vec<u8>,
    pub decimals: u8,
}
 
  pub fn x_swap_native(
      ctx: Context<SwapNativeCtx>,
      swap_native_params: SwapNativeParams,
      seed: Vec<u8>,
  ) -> Result<();
 
 
pub fn x_swap_token(
    ctx: Context<SwapTokenCtx>,
    swap_token_params: SwapTokenParams,
    seed: Vec<u8>,
) -> Result<()>;
 

Chainflip's program does some checks but it cannot check that all the parameters are valid and correctly encoded. Incorrectly encoded parameters might result in the loss of user funds.

Therefore, it's advised to use the RPC provided to craft Vault Swap. Otherwise make sure to thoroughly test your logic before initiating a swap with real funds.

Example with the Anchor framework

Using the Anchor framework to parse the Swap Endpoint's IDL it's easy to call the program directly. Below is an example for native and token Vault swaps. Using mainnet account values and arbitrary swap parameters as an example.

import * as anchor from '@coral-xyz/anchor';
import {SystemProgram, Transaction, PublicKey} from '@solana/web3.js';
 
const userKeypair = ... // load the user keypair
const swapEndpointProgramId = new PublicKey("4FVuGMuzuFAo5KWLnVNknDkNZ84er2wcrtJ79pfyoZqH");
 
const eventAccountSeed = crypto.randomBytes(32);
const [eventAccountPubkey] = PublicKey.findProgramAddressSync(
  [Buffer.from('swap_event'), userKeypair.publicKey.toBuffer(), eventAccountSeed],
  swapEndpointProgramId,
);
 
const [swapEndpointNativeVault] = PublicKey.findProgramAddressSync(
  [Buffer.from('swap_endpoint_native_vault')],
  swapEndpointProgramId,
);
 
const tx = cfSwapEndpointProgram.methods
    .xSwapNative(
      {
        amount: new anchor.BN(100018),
        dstChain: 1,
        dstAddress: Buffer.from("0xb5d85CBf7cB3EE0D56b3bB207D5Fc4B82f43F511", "hex"),
        dstToken: 1,
        // use "null" if not ccm
        ccmParameters: {
          message: Buffer.from("0x1234", "hex"),
          gasAmount: new anchor.BN(100000)
        }, 
        cfParameters: Buffer.from("0x", "hex"),
      }, 
      eventAccountSeed,
    )
    .accountsPartial({
      dataAccount: new PublicKey("ACLMuTFvDAb3oecQQGkTVqpUbhCKHG3EZ9uNXHK1W9ka"),
      nativeVault: swapEndpointNativeVault,
      from: userKeypair.publicKey,
      eventDataAccount: eventAccountPubkey,
      swapEndpointDataAccount: new PublicKey("FmAcjWaRFUxGWBfGT7G3CzcFeJFsewQ4KPJVG4f6fcob"),
      systemProgram: anchor.web3.SystemProgram.programId,
    })
    .signers([userKeypair])
    .transaction();
await sendAndConfirmTransaction(connection, tx, [userKeypair]);
import * as anchor from '@coral-xyz/anchor';
import crypto from 'crypto';
import {SystemProgram, Transaction, PublicKey} from '@solana/web3.js';
import {TOKEN_PROGRAM_ID, getAssociatedTokenAddressSync} from '@solana/spl-token';
 
const userKeypair = ... // load the user keypair
 
const swapEndpointProgramId = new PublicKey("4FVuGMuzuFAo5KWLnVNknDkNZ84er2wcrtJ79pfyoZqH");
const vaultProgramId = new PublicKey("AusZPVXPoUM8QJJ2SL4KwvRGCQ22cDg6Y4rg7EvFrxi7");
const eventAccountSeed = crypto.randomBytes(32);
const mint = new PublicKey("24PNhTaNtomHhoy3fTRaMhAFCRj4uHqhZEEoWrKDbR5p");
const userAta = getAssociatedTokenAddressSync(mint, userKeypair.publicKey);
 
const [eventAccountPubkey] = PublicKey.findProgramAddressSync(
  [Buffer.from('swap_event'), userKeypair.publicKey.toBuffer(), eventAccountSeed],
  swapEndpointProgramId,
);
 
const [tokenSupportedAccount] = PublicKey.findProgramAddressSync(
  [Buffer.from('supported_token'), mint.toBuffer()],
  vaultProgramId,
);
 
const tx = await cfSwapEndpointProgram.methods
  .xSwapToken(
    {
      amount: new anchor.BN(100018),
      dstChain: 1,
      dstAddress: Buffer.from("0xb5d85CBf7cB3EE0D56b3bB207D5Fc4B82f43F511", "hex"),
      dstToken: 1,
       // use "null" if not ccm
      ccmParameters: {
        message: Buffer.from("0x1234", "hex"),
        gasAmount: new anchor.BN(100000)
      }, 
      cfParameters: Buffer.from("0x", "hex"),
      decimals: 6,
    },
    eventAccountSeed,
  )
  .accountsPartial({
    dataAccount: new PublicKey("ACLMuTFvDAb3oecQQGkTVqpUbhCKHG3EZ9uNXHK1W9ka"),
    tokenVaultAssociatedTokenAccount: new PublicKey("8KNqCBB1LKWbtjNxY9v2g1fSBKm2ZRgNNv7rmx2bE6Ce"),
    from: userKeypair.publicKey,
    fromTokenAccount: userAta,
    eventDataAccount: eventAccountPubkey,
    swapEndpointDataAccount: new PublicKey("FmAcjWaRFUxGWBfGT7G3CzcFeJFsewQ4KPJVG4f6fcob"),
    tokenSupportedAccount,
    tokenProgram: TOKEN_PROGRAM_ID,
    mint,
    systemProgram: anchor.web3.SystemProgram.programId,
  })
  .signers([userKeypair])
  .transaction();
 
const txHash = await sendAndConfirmTransaction(connection, tx, [userKeypair]);

Parameter reference

ParamDescriptionData Type
amountAmount of the source token to be swappedu64
dstChainDestination chain IDu32
dstAddressAddress where the swapped tokens will be sent to on the destination chain. Addresses must be encoded into a bytes type valid for the destination chain. You can check out a reference on how to do address encoding for each of the chains from our SDK (opens in a new tab).Vec<u8>
dstTokenDestination token.u32
ccmParametersAdditional metadata for swaps with Cross-Chain Messaging. Should be set to None for simple swaps. See CCM support for more details.Option<(message, gasAmount)>
messageMessage that is passed to the destination address on the destination chain. It must be shorter than 15k bytes.Vec<u8>
gasAmountGas budget for the call on the destination chain in gas units or compute units. This amount should cover the execution of the receiver's logic.u64
cfParametersAdditional metadata for additional features. It cannot be empty and must be correctly encoded according to Chainflip Parameters.Vec<u8>
seedSeed used to derive the eventDataAccount. The maximum valid length is 32-bytesVec<u8>

Accounts Reference

AccountDescriptionAccount type
dataAccountChainflip's Vault Data Account. See Mainnet addresses.Data Account
nativeVaultSwap Endpoint Native Vault. Can be derived from the Vault account.Account
fromAddress initiating the swap. This account will pay the transaction fees and the input swap amount.Signer
eventDataAccountAccount PDA where the swap data is to be stored. It is derived in part from the seed parameterAccount
swapEndpointDataAccountChainflip's Swap Endpoint Data Account. See Mainnet addresses.Data Account
systemProgramSolana System Program AccountAccount
fromTokenAccountToken account where the tokens should be transferred from. Normally this is the "from"'s associated token account. This is only needed when swapping tokens.Signer
tokenSupportedAccountChainflip's Token Supported Account for the specific token. This is only needed when swapping tokens. Mainnet addresses can be found [here]( Mainnet addresses).Data Account
mintAddress of the SPL-token to swap. The token should be supported by Chainflip, otherwise the transaction will revert. Mainnet addresses can be found [here]( Mainnet addresses).Data Account