Solana Swap Examples
This guide demonstrates how to execute Silent Swaps with Solana assets using the Core SDK in a Node.js backend environment. Solana swaps require special handling for bridge operations and address formats.
Prerequisites
- Solana Web3.js library
- EVM wallet (for facilitator operations and deposit calldata)
- Solana keypair or wallet adapter
- Understanding of CAIP-19 asset identifiers
Setup
import {
createSilentSwapClient,
createViemSigner,
parseTransactionRequestForViem,
createSignInMessage,
createEip712DocForWalletGeneration,
createEip712DocForOrder,
createHdFacilitatorGroupFromEntropy,
queryDepositCount,
hexToBytes,
quoteResponseToEip712Document,
solveOptimalUsdcAmount,
caip19FungibleEvmToken,
caip19SplToken,
DeliveryMethod,
FacilitatorKeyType,
PublicKeyArgGroups,
ENVIRONMENT,
N_RELAY_CHAIN_ID_SOLANA,
SB58_ADDR_SOL_PROGRAM_SYSTEM,
isSolanaNativeToken,
parseSolanaCaip19,
fetchRelayQuote,
createPhonyDepositCalldata,
X_MAX_IMPACT_PERCENT,
getRelayStatus,
type SilentSwapClient,
type EvmSigner,
type SolveUsdcResult,
} from '@silentswap/sdk';
import { createWalletClient, http, publicActions, erc20Abi, encodeFunctionData, erc20Abi as erc20AbiForEncode } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { avalanche, mainnet } from 'viem/chains';
import { Connection, Keypair, PublicKey, Transaction } from '@solana/web3.js';
import { getAssociatedTokenAddress, createTransferInstruction, getAccount } from '@solana/spl-token';
import BigNumber from 'bignumber.js';
// Create EVM wallet client (required for facilitator operations)
const evmAccount = privateKeyToAccount(process.env.EVM_PRIVATE_KEY as `0x${string}`);
const evmClient = createWalletClient({
account: evmAccount,
chain: avalanche,
transport: http(),
}).extend(publicActions);
// Create EVM signer
const evmSigner = createViemSigner(evmAccount, evmClient);
// Create SilentSwap client
const silentswap = createSilentSwapClient({
environment: ENVIRONMENT.MAINNET,
baseUrl: 'https://api.silentswap.com',
});
// Create Solana connection
const solanaConnection = new Connection('https://api.mainnet-beta.solana.com', 'confirmed');
// Load Solana keypair (in production, use secure key management)
const solanaKeypair = Keypair.fromSecretKey(
Buffer.from(JSON.parse(process.env.SOLANA_SECRET_KEY || '[]'))
);
const solanaAddress = solanaKeypair.publicKey.toString();Example 1: Solana Native SOL → EVM Token Swap
This example swaps native SOL on Solana to USDC on Ethereum.
async function executeSolanaToEvmSwap(
solAmount: string, // Human-readable amount (e.g., "1.5")
recipientEvmAddress: `0x${string}`,
destinationTokenAddress: `0x${string}`,
destinationChainId: number = 1 // Ethereum mainnet
) {
try {
// Step 1: Authenticate and derive entropy (using EVM wallet)
console.log('Step 1: Authenticating with SilentSwap...');
const entropy = await authenticateAndDeriveEntropy(silentswap, evmSigner);
console.log('✓ Authentication successful');
// Step 2: Create facilitator group
console.log('\nStep 2: Creating facilitator group...');
const depositCount = await queryDepositCount(evmAccount.address);
const group = await createHdFacilitatorGroupFromEntropy(
hexToBytes(entropy),
depositCount
);
console.log(`✓ Facilitator group created (deposit count: ${depositCount})`);
// Step 3: Calculate optimal USDC amount from Solana bridge
console.log('\nStep 3: Calculating bridge USDC amount...');
// Parse Solana CAIP-19 for native SOL
const solanaCaip19 = `solana:5Q544fKrFoe6tsEbD7S8EmxGTJYAKtTVhAW5Q5pge4j1/slip44:501`;
const solanaParsed = parseSolanaCaip19(solanaCaip19);
if (!solanaParsed) {
throw new Error('Invalid Solana CAIP-19 format');
}
// Convert SOL amount to lamports (9 decimals)
const solAmountBN = BigNumber(solAmount);
const solAmountInLamports = solAmountBN.shiftedBy(9).toFixed(0);
// Origin currency for relay.link: system program for native SOL
const originCurrency = SB58_ADDR_SOL_PROGRAM_SYSTEM;
// Create phony deposit calldata for solving (uses EVM signer address)
const depositorAddress = silentswap.s0xDepositorAddress;
const phonyDepositCalldata = createPhonyDepositCalldata(evmAccount.address);
// Solve for optimal USDC amount
// CRITICAL: Pass Solana address for 'user' parameter, EVM address for recipient and deposit calldata
const bridgeResult: SolveUsdcResult = await solveOptimalUsdcAmount(
N_RELAY_CHAIN_ID_SOLANA,
originCurrency, // System program address for native SOL
solAmountInLamports,
solanaAddress, // Solana user address (base58)
phonyDepositCalldata,
X_MAX_IMPACT_PERCENT,
depositorAddress,
evmAccount.address, // EVM address for recipient and deposit calldata
);
console.log(`✓ Bridge will provide ${BigNumber(bridgeResult.usdcAmountOut.toString()).shiftedBy(-6).toFixed()} USDC (provider: ${bridgeResult.provider})`);
// Step 4: Get quote using bridged USDC amount
console.log('\nStep 4: Requesting quote with bridged USDC...');
const viewer = await group.viewer();
const { publicKeyBytes: pk65_viewer } = viewer.exportPublicKey(
'*',
FacilitatorKeyType.SECP256K1
);
const groupPublicKeys = await group.exportPublicKeys(1, [
...PublicKeyArgGroups.GENERIC,
]);
// Request quote with USDC amount from bridge
const [quoteError, quoteResponse] = await silentswap.quote({
signer: evmAccount.address, // EVM signer address (matches deposit calldata)
viewer: pk65_viewer,
outputs: [
{
method: DeliveryMethod.SNIP,
recipient: recipientEvmAddress,
asset: caip19FungibleEvmToken(destinationChainId, destinationTokenAddress),
value: bridgeResult.usdcAmountOut.toString() as `${bigint}`, // USDC amount in microUSDC
facilitatorPublicKeys: groupPublicKeys[0],
},
],
});
if (quoteError || !quoteResponse) {
throw new Error(`Failed to get quote: ${quoteError?.type}: ${quoteError?.error}`);
}
console.log(`✓ Quote received (Order ID: ${quoteResponse.quoteId})`);
// Step 5: Sign authorizations and create order
console.log('\nStep 5: Signing authorizations and creating order...');
const orderResponse = await createOrder(
silentswap,
evmSigner,
group,
quoteResponse,
{
sourceAsset: {
caip19: solanaCaip19,
amount: solAmountInLamports,
},
sourceSender: {
contactId: `caip10:solana:*:${solanaAddress}`,
},
}
);
console.log(`✓ Order created (Order ID: ${orderResponse.response.orderId})`);
// Step 6: Execute Solana bridge transaction
console.log('\nStep 6: Executing Solana bridge transaction...');
const depositTxHash = await executeSolanaBridge(
solanaCaip19,
solAmountInLamports,
bridgeResult.usdcAmountOut.toString(),
solanaAddress,
evmAccount.address,
orderResponse,
bridgeResult.provider
);
console.log(`✓ Bridge transaction completed: ${depositTxHash}`);
// Step 7: Watch for completion
console.log('\nStep 7: Watching for order completion...');
await watchForCompletion(
evmClient,
destinationTokenAddress,
recipientEvmAddress,
group,
6 // USDC decimals
);
return {
orderId: orderResponse.response.orderId,
depositHash: depositTxHash,
quote: quoteResponse,
bridgeProvider: bridgeResult.provider,
usdcAmountReceived: bridgeResult.usdcAmountOut.toString(),
};
} catch (err) {
console.error('Solana to EVM swap error:', err);
throw err;
}
}
// Helper: Execute Solana bridge transaction
async function executeSolanaBridge(
sourceAsset: string,
sourceAmount: string,
usdcAmount: string,
solanaSenderAddress: string,
evmSignerAddress: `0x${string}`,
orderResponse: any,
provider: 'relay' | 'debridge'
): Promise<string> {
if (provider !== 'relay') {
throw new Error('Only relay.link is supported for Solana swaps');
}
// Get deposit parameters from order
const depositParams = orderResponse.transaction.metadata?.params;
if (!depositParams) {
throw new Error('Missing deposit parameters in order response');
}
// Encode deposit calldata (matches React SDK implementation)
const depositorAddress = silentswap.s0xDepositorAddress;
const DEPOSITOR_ABI = [
{
inputs: [
{
components: [
{ internalType: 'address', name: 'signer', type: 'address' },
{ internalType: 'bytes32', name: 'orderId', type: 'bytes32' },
{ internalType: 'address', name: 'notary', type: 'address' },
{ internalType: 'address', name: 'approver', type: 'address' },
{ internalType: 'bytes', name: 'orderApproval', type: 'bytes' },
{ internalType: 'uint256', name: 'approvalExpiration', type: 'uint256' },
{ internalType: 'uint256', name: 'duration', type: 'uint256' },
{ internalType: 'bytes32', name: 'domainSepHash', type: 'bytes32' },
{ internalType: 'bytes32', name: 'payloadHash', type: 'bytes32' },
{ internalType: 'bytes', name: 'typedDataSignature', type: 'bytes' },
{ internalType: 'bytes', name: 'receiveAuthorization', type: 'bytes' },
],
internalType: 'struct SilentSwapV2Gateway.DepositParams',
name: 'params',
type: 'tuple',
},
],
name: 'depositProxy2',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
] as const;
const depositCalldata = encodeFunctionData({
abi: DEPOSITOR_ABI,
functionName: 'depositProxy2',
args: [
{
...depositParams,
signer: evmSignerAddress, // EVM signer address (matches quote request)
approvalExpiration: BigInt(String(depositParams.approvalExpiration)),
duration: BigInt(String(depositParams.duration)),
},
],
});
// Encode USDC approval calldata
const XG_UINT256_MAX = (1n << 256n) - 1n;
const S0X_ADDR_USDC_AVALANCHE = '0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E';
const approveUsdcCalldata = encodeFunctionData({
abi: erc20AbiForEncode,
functionName: 'approve',
args: [depositorAddress, XG_UINT256_MAX],
});
// Fetch Relay.link quote for execution (EXACT_OUTPUT with txs)
const relayQuote = await fetchRelayQuote({
user: solanaSenderAddress, // Solana address
referrer: 'silentswap',
originChainId: N_RELAY_CHAIN_ID_SOLANA,
destinationChainId: 43114, // Avalanche
originCurrency: SB58_ADDR_SOL_PROGRAM_SYSTEM, // Native SOL
destinationCurrency: S0X_ADDR_USDC_AVALANCHE,
amount: usdcAmount, // Target USDC amount
tradeType: 'EXACT_OUTPUT', // CRITICAL: use EXACT_OUTPUT for execution
recipient: evmSignerAddress, // EVM address for recipient
txsGasLimit: 600_000,
txs: [
{
to: S0X_ADDR_USDC_AVALANCHE,
value: '0',
data: approveUsdcCalldata,
},
{
to: depositorAddress,
value: '0',
data: depositCalldata,
},
],
});
// Execute Solana transactions from relay quote
const transaction = new Transaction();
for (const step of relayQuote.steps || []) {
if (step.kind !== 'transaction') continue;
for (const item of step.items) {
const itemData = item.data as any;
if ('instructions' in itemData) {
// Add Solana instructions to transaction
for (const instruction of itemData.instructions) {
transaction.add({
keys: instruction.keys.map((k: any) => ({
pubkey: new PublicKey(k.pubkey),
isSigner: k.isSigner,
isWritable: k.isWritable,
})),
programId: new PublicKey(instruction.programId),
data: Buffer.from(instruction.data, 'base64'),
});
}
}
}
}
// Sign and send Solana transaction
transaction.recentBlockhash = (await solanaConnection.getLatestBlockhash()).blockhash;
transaction.feePayer = solanaKeypair.publicKey;
transaction.sign(solanaKeypair);
const signature = await solanaConnection.sendRawTransaction(transaction.serialize());
await solanaConnection.confirmTransaction(signature, 'confirmed');
// Monitor bridge status
const requestId = relayQuote.steps?.find((s) => s.requestId)?.requestId;
if (!requestId) {
throw new Error('Missing relay.link request ID');
}
const depositTxHash = await monitorRelayBridgeStatus(requestId);
return depositTxHash;
}
// Helper: Monitor relay bridge status
async function monitorRelayBridgeStatus(requestId: string): Promise<string> {
const { getRelayStatus } = await import('@silentswap/sdk');
while (true) {
const status = await getRelayStatus(requestId);
if (status.status === 'success') {
return status.txHashes?.[0] || '0x';
}
if (status.status === 'failed' || status.status === 'refund') {
throw new Error(`Bridge failed: ${status.details || 'Unknown error'}`);
}
await new Promise((resolve) => setTimeout(resolve, 2000));
}
}Example 2: EVM Token → Solana SPL Token Swap
This example swaps USDC on Avalanche to USDC SPL token on Solana.
async function executeEvmToSolanaSwap(
usdcAmount: string, // Human-readable amount (e.g., "100")
recipientSolanaAddress: string, // Base58 Solana address
destinationTokenMint: string // Base58 SPL token mint address
) {
try {
// Step 1: Authenticate and derive entropy
console.log('Step 1: Authenticating with SilentSwap...');
const entropy = await authenticateAndDeriveEntropy(silentswap, evmSigner);
console.log('✓ Authentication successful');
// Step 2: Create facilitator group
console.log('\nStep 2: Creating facilitator group...');
const depositCount = await queryDepositCount(evmAccount.address);
const group = await createHdFacilitatorGroupFromEntropy(
hexToBytes(entropy),
depositCount
);
console.log(`✓ Facilitator group created (deposit count: ${depositCount})`);
// Step 3: Get quote (direct USDC deposit on Avalanche)
console.log('\nStep 3: Requesting quote...');
const viewer = await group.viewer();
const { publicKeyBytes: pk65_viewer } = viewer.exportPublicKey(
'*',
FacilitatorKeyType.SECP256K1
);
const groupPublicKeys = await group.exportPublicKeys(1, [
...PublicKeyArgGroups.GENERIC,
]);
// Convert USDC amount to microUSDC (6 decimals)
const usdcAmountBN = BigNumber(usdcAmount);
const usdcAmountMicro = usdcAmountBN.shiftedBy(6).toFixed(0);
// Create Solana destination CAIP-19
const solanaDestinationCaip19 = caip19SplToken(
'5Q544fKrFoe6tsEbD7S8EmxGTJYAKtTVhAW5Q5pge4j1', // Solana mainnet chain ID
destinationTokenMint
);
// Request quote
const [quoteError, quoteResponse] = await silentswap.quote({
signer: evmAccount.address,
viewer: pk65_viewer,
outputs: [
{
method: DeliveryMethod.SNIP,
recipient: recipientSolanaAddress, // Base58 Solana address (NOT CAIP-10)
asset: solanaDestinationCaip19,
value: usdcAmountMicro as `${bigint}`,
facilitatorPublicKeys: groupPublicKeys[0],
},
],
});
if (quoteError || !quoteResponse) {
throw new Error(`Failed to get quote: ${quoteError?.type}: ${quoteError?.error}`);
}
console.log(`✓ Quote received (Order ID: ${quoteResponse.quoteId})`);
// Step 4: Sign authorizations and create order
console.log('\nStep 4: Signing authorizations and creating order...');
const orderResponse = await createOrder(
silentswap,
evmSigner,
group,
quoteResponse,
{
sourceAsset: {
caip19: 'eip155:43114/erc20:0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E', // USDC on Avalanche
amount: usdcAmountMicro,
},
sourceSender: {
contactId: `caip10:eip155:43114:${evmAccount.address}`,
},
}
);
console.log(`✓ Order created (Order ID: ${orderResponse.response.orderId})`);
// Step 5: Execute deposit transaction (direct USDC deposit)
console.log('\nStep 5: Executing deposit transaction...');
const depositHash = await executeDeposit(evmClient, orderResponse);
console.log(`✓ Deposit transaction sent: ${depositHash}`);
// Step 6: Watch for completion on Solana
console.log('\nStep 6: Watching for order completion on Solana...');
await watchForSolanaCompletion(
solanaConnection,
destinationTokenMint,
recipientSolanaAddress,
group
);
return {
orderId: orderResponse.response.orderId,
depositHash,
quote: quoteResponse,
};
} catch (err) {
console.error('EVM to Solana swap error:', err);
throw err;
}
}
// Helper: Watch for completion on Solana
async function watchForSolanaCompletion(
connection: Connection,
tokenMint: string,
recipientAddress: string,
group: Awaited<ReturnType<typeof createHdFacilitatorGroupFromEntropy>>
) {
// Get facilitator account for Solana (coin type 501)
const facilitator0Sol = await group.account('501', 0);
const facilitator0SolEvm = await facilitator0Sol.evmSigner();
// In a real implementation, you would watch for SPL token transfers
// This is a simplified example - you'd use Solana webhooks or polling
const recipientPubkey = new PublicKey(recipientAddress);
const mintPubkey = new PublicKey(tokenMint);
const ata = await getAssociatedTokenAddress(mintPubkey, recipientPubkey);
console.log(`Watching for tokens at: ${ata.toString()}`);
// Poll for token account balance
return new Promise<void>((resolve) => {
const interval = setInterval(async () => {
try {
const account = await getAccount(connection, ata);
if (account.amount > 0n) {
console.log(`✓ Recipient received ${account.amount.toString()} tokens`);
clearInterval(interval);
resolve();
}
} catch (err) {
// Account doesn't exist yet, continue polling
}
}, 2000);
});
}Usage Examples
Example 1: Swap 1 SOL → USDC on Ethereum
const result = await executeSolanaToEvmSwap(
'1', // 1 SOL
'0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb5', // Recipient EVM address
'0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC on Ethereum
1 // Ethereum mainnet
);
console.log('Swap completed:', result);Example 2: Swap 100 USDC → USDC SPL on Solana
const result = await executeEvmToSolanaSwap(
'100', // 100 USDC
'9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM', // Recipient Solana address (base58)
'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v' // USDC SPL token mint
);
console.log('Swap completed:', result);Key Points
-
Dual Address Requirement:
- Solana address: For Solana transactions (source swaps)
- EVM address: For facilitator operations and deposit calldata
-
Bridge Provider: Solana swaps use relay.link automatically
-
Address Formats:
- Solana addresses: Base58 format (e.g.,
9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM) - EVM addresses: Hex format with 0x prefix
- Solana addresses: Base58 format (e.g.,
-
CAIP-19 Identifiers:
- Native SOL:
solana:<chainId>/slip44:501 - SPL Tokens:
solana:<chainId>/erc20:<tokenMint>
- Native SOL:
-
Trade Type: Execution quotes must use
EXACT_OUTPUTwithtxsparameter for Solana swaps
Helper Functions
These helper functions are used in the examples above. See the Complete Example for full implementations of common helpers.
// Helper: Authenticate and derive entropy
async function authenticateAndDeriveEntropy(
silentswap: SilentSwapClient,
signer: EvmSigner
): Promise<`0x${string}`> {
// Get nonce
const [nonceError, nonceResponse] = await silentswap.nonce(signer.address);
if (!nonceResponse || nonceError) {
throw new Error(`Failed to get nonce: ${nonceError?.type}: ${nonceError?.error}`);
}
// Create sign-in message
const signInMessage = createSignInMessage(
signer.address,
nonceResponse.nonce,
'silentswap.com'
);
// Sign message
const siweSignature = await signer.signEip191Message(signInMessage.message);
// Authenticate
const [authError, authResponse] = await silentswap.authenticate({
siwe: {
message: signInMessage.message,
signature: siweSignature,
},
});
if (!authResponse || authError) {
throw new Error(`Failed to authenticate: ${authError?.type}: ${authError?.error}`);
}
// Derive entropy from auth token
const eip712Doc = createEip712DocForWalletGeneration(authResponse.secretToken);
const entropy = await signer.signEip712TypedData(eip712Doc);
return entropy;
}
// Helper: Create order
async function createOrder(
silentswap: SilentSwapClient,
signer: EvmSigner,
group: Awaited<ReturnType<typeof createHdFacilitatorGroupFromEntropy>>,
quoteResponse: any,
metadata?: {
sourceAsset?: { caip19: string; amount: string };
sourceSender?: { contactId: string };
}
) {
// Sign authorizations (empty for Solana swaps)
const signedAuths = await Promise.all(
quoteResponse.authorizations.map(async (g_auth: any) => ({
...g_auth,
signature: '0x' as `0x${string}`, // No EIP-3009 for Solana
}))
);
// Sign the order's EIP-712
const orderDoc = quoteResponseToEip712Document(quoteResponse);
const signedQuote = await signer.signEip712TypedData(orderDoc);
// Approve proxy authorizations
const facilitatorReplies = await group.approveProxyAuthorizations(
quoteResponse.facilitators,
{
proxyPublicKey: silentswap.proxyPublicKey,
}
);
// Place the order
const [orderError, orderResponse] = await silentswap.order({
quote: quoteResponse.quote,
quoteId: quoteResponse.quoteId,
authorizations: signedAuths,
eip712Domain: orderDoc.domain,
signature: signedQuote,
facilitators: facilitatorReplies,
metadata,
});
if (orderError || !orderResponse) {
throw new Error(`Failed to place order: ${orderError?.type}: ${orderError?.error}`);
}
return orderResponse;
}
// Helper: Execute deposit (for EVM direct deposits)
async function executeDeposit(
client: ReturnType<typeof createWalletClient>,
orderResponse: any
) {
// Parse transaction request
const txRequestParams = parseTransactionRequestForViem(orderResponse.transaction);
// Send transaction
const hash = await client.sendTransaction(txRequestParams);
// Wait for confirmation
const txReceipt = await client.waitForTransactionReceipt({ hash });
console.log(
`Deposit confirmed: ${BigNumber(orderResponse.response.order.deposit)
.shiftedBy(-6)
.toFixed()} USDC at ${txReceipt.transactionHash}`
);
return hash;
}
// Helper: Monitor relay bridge status
async function monitorRelayBridgeStatus(requestId: string): Promise<string> {
while (true) {
const status = await getRelayStatus(requestId);
if (status.status === 'success') {
return status.txHashes?.[0] || '0x';
}
if (status.status === 'failed' || status.status === 'refund') {
throw new Error(`Bridge failed: ${status.details || 'Unknown error'}`);
}
await new Promise((resolve) => setTimeout(resolve, 2000));
}
}
// Helper: Watch for completion on EVM chain
async function watchForCompletion(
client: ReturnType<typeof createWalletClient>,
tokenAddress: `0x${string}`,
recipientAddress: `0x${string}`,
group: Awaited<ReturnType<typeof createHdFacilitatorGroupFromEntropy>>,
tokenDecimals: number
) {
// Get facilitator account for coin type 60 (ETH) at output index 0
const facilitator0Eth = await group.account('60', 0);
const facilitator0EthEvm = await facilitator0Eth.evmSigner();
// Create client for destination chain (Mainnet)
const destinationClient = createWalletClient({
chain: mainnet,
transport: http(),
}).extend(publicActions);
// Watch for ERC-20 transfer event
return new Promise<void>((resolve) => {
destinationClient.watchContractEvent({
address: tokenAddress,
abi: erc20Abi,
eventName: 'Transfer',
args: {
to: recipientAddress,
from: facilitator0EthEvm.address,
},
onLogs: (logs) => {
for (const log of logs) {
const { to, value } = log.args;
console.log(
`✓ Recipient ${to} received ${BigNumber(value!)
.shiftedBy(-tokenDecimals)
.toFixed()} tokens`
);
}
resolve();
},
});
});
}Next Steps
- Review the Complete Example for general swap patterns
- Learn about Bridge Operations for cross-chain bridging
- Check the React Solana Examples for frontend implementations