Add Solana Support to Your Bridge

Across is expanding to Solana, unlocking seamless USDC bridging between Ethereum Mainnet, supported EVM chains, and Solana. If you’re an integrator, builder, here’s what you need to know to get started quickly.


The Core Workflow: Building a Deposit Transaction

Executing a deposit from a user involves three main steps:

1

Fetch Fees

Get the latest transaction details by making a call to the /suggested-fees API.

2

Add Integrator ID

Include your unique integratorID in the transaction data.

If you need any help adding the integratorID, please reach out to us here.

3

Execute Deposit

Call the deposit() function in the SVM SpokePool contract to initiate the bridge.

Please ensure that you derive the required Program Derived Addresses (PDAs) (e.g., vault, depositorTokenAccount) and prepare parameters like inputAmount, outputAmount, destinationChainId, etc. We have attached a utility script below to help you out!

Retrieve suggested fee quote for a Solana deposit.

get

Returns suggested fees based inputToken+outputToken, originChainId, destinationChainId, and amount. Also includes data used to compute the fees.

Query parameters
inputTokenstringRequired

Address of token to bridge on the origin chain. Must be used together with parameter outputToken. Here we are using USDC.

Note that the address provided must exist on the specified originChainId below.

Example: {"value":"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"}
outputTokenstringRequired

Address of token to bridge on the destination chain. Must be used together with parameter inputToken. For ETH, use the wrapped address, like WETH.

Note that the address provided must exist on the specified destinationChainId below.

Example: {"value":"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"}
originChainIdall ofRequired

Chain ID where the specified token or inputToken exists.

Example: {"value":1}
destinationChainIdall ofRequired

The desired destination chain ID of the bridge transfer.

Example: {"value":34268394551451}
amountinteger · min: 1Required

Amount of the token to transfer.

Note that this amount is in the native decimals of the token. So, for WETH, this would be the amount of human-readable WETH multiplied by 1e18. For USDC, you would multiply the number of human-readable USDC by 1e6.

Example: {"value":"50000000"}
recipientstringOptional

Recipient of the deposit. Can be an EOA or a contract. If this is an EOA and message is defined, then the API will throw a 4xx error.

Example: GsiZqCTNRi4T3qZrixFdmhXVeA4CSUzS7c44EQ7Rw1Tw

messagestringOptional

Calldata passed to the recipient if recipient is a contract address. This calldata is passed to the recipient via the recipient's handleAcrossMessage() public function. Example: 0xABC123

relayerstringOptional

Optionally override the relayer address used to simulate the fillRelay() call that estimates the gas costs needed to fill a deposit. This simulation result impacts the returned suggested-fees. The reason to customize the EOA would be primarily if the recipientAddress is a contract and requires a certain relayer to submit the fill, or if one specific relayer has the necessary token balance to make the fill.

Example: 0x428AB2BA90Eba0a4Be7aF34C9Ac451ab061AC010

timestampintegerOptional

The quote timestamp used to compute the LP fees. When bridging with across, the user only specifies the quote timestamp in their transaction. The relayer then determines the utilization at that timestamp to determine the user's fee. This timestamp must be close (within 10 minutes or so) to the current time on the chain where the user is depositing funds and it should be <= the current block timestamp on mainnet. This allows the user to know exactly what LP fee they will pay before sending the transaction.

If this value isn't provided in the request, the API will assume the latest block timestamp on mainnet.

Example: 1653547649

Responses
200

Suggested fees for the transaction and supporting data

application/json
Responseany
get
Curl
curl -L \
  'https://app.across.to/api/suggested-fees?inputToken=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48&outputToken=EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v&originChainId=1&destinationChainId=34268394551451&recipient=GsiZqCTNRi4T3qZrixFdmhXVeA4CSUzS7c44EQ7Rw1Tw&amount=50000000&skipAmountLimit=true&allowUnmatchedDecimals=true'
{
  "estimatedFillTimeSec": 2,
  "capitalFeePct": "78750000000001",
  "capitalFeeTotal": "78750000000001",
  "relayGasFeePct": "155024308002",
  "relayGasFeeTotal": "155024308002",
  "relayFeePct": "78905024308003",
  "relayFeeTotal": "78905024308003",
  "lpFeePct": "0",
  "timestamp": "1754342087",
  "isAmountTooLow": false,
  "quoteBlock": "23070320",
  "exclusiveRelayer": "0x394311A6Aaa0D8E3411D8b62DE4578D41322d1bD",
  "exclusivityDeadline": 1754342267,
  "spokePoolAddress": "0x5c7BCd6E7De5423a257D81B442095A1a6ced35C5",
  "destinationSpokePoolAddress": "0x6f26Bf09B1C792e3228e5467807a900A503c0281",
  "totalRelayFee": {
    "pct": "78905024308003",
    "total": "78905024308003"
  },
  "relayerCapitalFee": {
    "pct": "78750000000001",
    "total": "78750000000001"
  },
  "relayerGasFee": {
    "pct": "155024308002",
    "total": "155024308002"
  },
  "lpFee": {
    "pct": "0",
    "total": "0"
  },
  "limits": {
    "minDeposit": "134862494200912",
    "maxDeposit": "1661211802629989209324",
    "maxDepositInstant": "231397155893653275446",
    "maxDepositShortDelay": "1661211802629989209324",
    "recommendedDepositInstant": "231397155893653275446"
  },
  "fillDeadline": "1754353917",
  "outputAmount": "999921094975691997",
  "inputToken": {
    "address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
    "symbol": "ETH",
    "decimals": 18,
    "chainId": 1
  },
  "outputToken": {
    "address": "0x4200000000000000000000000000000000000006",
    "symbol": "ETH",
    "decimals": 18,
    "chainId": 10
  },
  "id": "xn8fx-1754342218143-67be35cfbdb6"
}

Test Your Codebase

You can check that you have updated your relayer codebase properly by simply following the below script to bridge 10 USDC from Solana to Base.

1

Setup the Testing Environment

Start by making an empty folder and running the following commands on the terminal:

npm init -y
npm i @solana/web3.js @solana/kit @solana/spl-token bn.js @coral-xyz/anchor dotenv bs58 @across-protocol/contracts
2

Running the script

Now, make 2 files in the same directory:

  1. index.js : Main code for the script should be here. feel free to simply copy paste the script given below and add your relayer address on line 102 as the exclusiveRelayer .

  2. .env : Put your Solana Wallet's private key here to be able to bridge funds for testing your relayer.

import { Connection, Keypair, PublicKey, SystemProgram } from "@solana/web3.js";
import { getU64Encoder } from "@solana/kit";
import {
  ASSOCIATED_TOKEN_PROGRAM_ID,
  TOKEN_PROGRAM_ID,
  getAssociatedTokenAddressSync,
} from "@solana/spl-token";
import BN from "bn.js";
import anchor from "@coral-xyz/anchor";
import fetch from "node-fetch";
import dotenv from "dotenv";
import bs58 from "bs58";
import { SvmSpokeIdl } from "@across-protocol/contracts";

dotenv.config();

const u64Encoder = getU64Encoder();
const { Program, AnchorProvider } = anchor;

// 1️⃣ Provider + Program
const connection = new Connection("https://api.mainnet-beta.solana.com", "confirmed");

const wallet = Keypair.fromSecretKey(bs58.decode(process.env.SOLANA_PRIVATE_KEY));

const provider = new AnchorProvider(connection, {
  publicKey: wallet.publicKey,
  signTransaction: tx => wallet.signTransaction(tx),
  signAllTransactions: txs => txs.map(t => wallet.signTransaction(t)),
}, {});

const program = new Program(SvmSpokeIdl, {connection, provider});

// 2️⃣ Fetch quote
const API_URL = "https://app-frontend-v3-git-epic-solana-v1-uma.vercel.app/api/suggested-fees";

function evmToSolanaPK(evmAddress) {
  const hex = evmAddress.replace(/^0x/, "").toLowerCase();
  if (hex.length !== 40) throw new Error("Invalid EVM address");

  const buf = Buffer.alloc(32);
  Buffer.from(hex, "hex").copy(buf, 12); // right-align, zero-pad left 12 bytes
  return new PublicKey(buf);
}

async function getQuote() {
  const params = new URLSearchParams({
    inputToken: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
    outputToken: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
    destinationChainId: "8453",
    originChainId: "34268394551451",
    amount: "10000000",
    skipAmountLimit: "true",
    allowUnmatchedDecimals: "true",
  });

  const resp = await fetch(`${API_URL}?${params}`);
  if (!resp.ok) throw new Error(`Quote API error ${resp.status}`);
  return resp.json();
}

// 3️⃣ PDAs
async function derivePdas(depositor, inputToken) {
  const seed = 0;
  const programId = new PublicKey(SvmSpokeIdl.address);
  const [statePda] = PublicKey.findProgramAddressSync(
    [Buffer.from("state"), Buffer.from(u64Encoder.encode(seed))],
    programId
  );

  const depositorTokenAccount = getAssociatedTokenAddressSync(
    inputToken,
    depositor,
    true,
    TOKEN_PROGRAM_ID,
    ASSOCIATED_TOKEN_PROGRAM_ID
  );

  const [vaultPda] = PublicKey.findProgramAddressSync(
    [statePda.toBuffer(), TOKEN_PROGRAM_ID.toBuffer(), inputToken.toBuffer()],
    programId
  );

  const [eventAuthority] = PublicKey.findProgramAddressSync(
    [Buffer.from("__event_authority")],
    programId
  );

  return { statePda, depositorTokenAccount, vaultPda, eventAuthority };
}

// 4️⃣ Call deposit
(async () => {
  const quote = await getQuote();
  console.log("Quote data:", JSON.stringify(quote, null, 2));

  const depositor = wallet.publicKey;
  
  // Convert EVM addresses to Solana base58 addresses
  const recipient = wallet.publicKey;
  const inputToken = new PublicKey(quote.inputToken.address);
  const outputToken = evmToSolanaPK(quote.outputToken.address);
  const exclusiveRelayer = evmToSolanaPK(<add-your-relayer-address-here>); //replace this with your relayer address as a string

  const inputAmount = new BN(quote.inputAmount);
  const outputAmount = new BN(quote.outputAmount); // must be length 32
  const destinationChainId = new BN(quote.outputToken.chainId);
  const quoteTimestamp = quote.quoteTimestamp >>> 0; // u32
  const fillDeadline = quote.fillDeadline >>> 0; // u32
  const exclusivityParameter = quote.exclusivityParameter >>> 0; // u32
  const message = new Uint8Array([]);


  const { statePda, depositorTokenAccount, vaultPda, eventAuthority } =
    await derivePdas(depositor, inputToken);

  const txSig = await program.methods
    .deposit(
      depositor,
      recipient,
      inputToken,
      outputToken,
      inputAmount,
      outputAmount,
      destinationChainId,
      exclusiveRelayer,
      quoteTimestamp,
      fillDeadline,
      exclusivityParameter,
      message
    )
    .accounts({
      signer: depositor,
      state: statePda,
      delegate: depositor,
      depositorTokenAccount,
      vault: vaultPda,
      mint: inputToken,
      tokenProgram: TOKEN_PROGRAM_ID,
      associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
      systemProgram: SystemProgram.programId,
      eventAuthority,
      program: program.programId,
    })
    .signers([wallet])
    .rpc();

  console.log("✅ Deposit TX:", txSig);
})();

To run the script, simply head over to your terminal and run the following command and you can see the deposit transaction go forward and get filled by your relayer:

node index.js

Next Steps

Get start with building your crosschain apps using Across V4 today:

  1. Check the Bridge Integration Guide: For a step-by-step walkthrough of how to build a bridging experience in your application using our SDK.

  2. Review the API & SDK: The best way to start is with our high-level tools. They are V4-compatible and abstract away the protocol's complexity.

    • API Reference: For direct HTTP integration to get quotes and check transaction status.

    • App-SDK Guide: For deeper integrations within a TypeScript or JavaScript application.

  3. Join the Community: Have a technical question or want to connect with the team and other developers? Our Discord and telegram are the best places to get support.

Last updated