Integration Overview
The Multiliquid Protocol on Solana can be integrated via the official TypeScript SDK, which provides a complete interface for quoting, building, and executing swaps against the on-chain program.
Installation
npm install @uniformlabs/multiliquid-svm-sdk
Peer dependencies (must be installed separately):
@solana/web3.js ^1.95
@coral-xyz/anchor ^0.32
npm install @solana/web3.js @coral-xyz/anchor
Client Initialization
The SDK provides a MultiliquidClient class that wraps all functionality:
import { Connection } from "@solana/web3.js";
import { MultiliquidClient } from "@uniformlabs/multiliquid-svm-sdk";
const connection = new Connection("https://api.mainnet-beta.solana.com");
const client = new MultiliquidClient({
connection,
cluster: "mainnet-beta", // "devnet" | "mainnet-beta"
commitment: "confirmed", // optional, default: "confirmed"
});
The cluster parameter determines which built-in pair registry is used. Both devnet and mainnet use the same program ID: FKeT8H2RSgsamrABNNxwT5f9g3n9msfm6D5AvocjrJAD.
Pair Discovery
Built-In Registry (No RPC)
The SDK ships with a hardcoded registry of known pairs for instant lookup:
const pairs = client.getPairs();
// Returns all registered pairs for the configured cluster
// Filter by asset
const usdcPairs = client.getPairs({
stableMint: new PublicKey("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"),
});
Each returned entry includes the pair PDA, both mints, the liquidity provider, and token decimals:
for (const pair of pairs) {
console.log(pair.assetSymbol); // e.g. "USTB"
console.log(pair.pairAddress.toBase58());
console.log(pair.stableMint.toBase58());
console.log(pair.assetMint.toBase58());
console.log(pair.liquidityProvider.toBase58());
console.log(pair.stableDecimals); // e.g. 6
console.log(pair.assetDecimals); // e.g. 9
}
// Use the entry directly in swap params
const pair = pairs[0];
const quote = await client.getQuote({
user: wallet.publicKey,
liquidityProvider: pair.liquidityProvider,
stableMint: pair.stableMint,
assetMint: pair.assetMint,
amount: new BN(1_000_000_000),
swapDirection: SwapDirection.StableToAsset,
swapType: SwapType.ExactIn,
});
On-Chain Discovery (RPC)
For dynamically discovering pairs not in the registry:
const pairs = await client.discoverPairs({
stableMint: USDC_MINT,
});
// Returns the same PairRegistryEntry format as getPairs()
Checking Pair Status
Before executing a swap, verify the pair is active:
const status = await client.checkPauseStatus(stableMint, assetMint, lp);
if (status.anyPaused) {
console.log("Swap blocked:", status.pauseReasons);
// e.g. ["ProgramPaused"], ["PairPaused"], ["RwaPaused"], ["StablePaused"]
}
The protocol has four independent pause levels (global, asset, LP config, pair). All must be unpaused for swaps to execute.
Quoting
The SDK offers two quoting methods: client-side math replication and on-chain simulation.
Client-Side Quote
Replicates the on-chain Rust math exactly using BigInt. Fetches current NAV prices from oracles and computes the swap result:
import { BN } from "@coral-xyz/anchor";
import { SwapDirection, SwapType } from "@uniformlabs/multiliquid-svm-sdk";
const USDC = new PublicKey("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v");
const USTB = new PublicKey("CCz3SGVziFeLYk2xfEstkiqJfYkjaSWb2GCABYsVcjo2");
const LP = new PublicKey("C8Mi6kn7ajFWuNe4ZmsR9A6fdqRYhzXFoqVBGMsdJ2Uf");
const quote = await client.getQuote({
user: wallet.publicKey,
liquidityProvider: LP,
stableMint: USDC,
assetMint: USTB,
amount: new BN(1_000_000_000), // 1000 USDC (6 decimals)
swapDirection: SwapDirection.StableToAsset,
swapType: SwapType.ExactIn,
});
console.log("Output:", MultiliquidClient.toHumanReadable(quote.amountOut, 9));
console.log("Protocol fees:", quote.protocolFees.toString());
console.log("Asset NAV:", quote.assetNav.toString());
console.log("Stable NAV:", quote.stableNav.toString());
The SwapQuote includes:
| Field | Description |
|---|
amountIn | Total input amount |
amountOut | Output amount received |
protocolFees | Protocol fee collected |
discountAmount | LP fee amount (redemption or discount) |
assetNav | RWA NAV price (9 decimals) |
stableNav | Stablecoin NAV price (9 decimals) |
Simulation Quote
Runs the swap instruction against the validator via simulateTransaction and parses the emitted event:
const simQuote = await client.getQuoteViaSimulation({
user: wallet.publicKey,
liquidityProvider: LP,
stableMint: USDC,
assetMint: USTB,
amount: new BN(1_000_000_000),
swapDirection: SwapDirection.StableToAsset,
swapType: SwapType.ExactIn,
});
console.log("Output:", simQuote.amountOut.toString());
console.log("Compute units:", simQuote.computeUnitsConsumed);
The simulation quote also returns computeUnitsConsumed, which is useful for setting compute budget instructions.
Executing Swaps
The SDK is instruction-first: the primary API returns TransactionInstruction objects for maximum composability. A convenience method for building full transactions is also available.
Building a Swap Transaction
const { transaction, accounts } = await client.buildSwapTransaction({
user: wallet.publicKey,
liquidityProvider: LP,
stableMint: USDC,
assetMint: USTB,
amount: new BN(1_000_000_000), // 1000 USDC
swapDirection: SwapDirection.StableToAsset,
swapType: SwapType.ExactIn,
minAmountOut: new BN(990_000_000), // slippage protection
});
// Sign and send
transaction.sign([wallet]);
const signature = await connection.sendTransaction(transaction);
await connection.confirmTransaction(signature, "confirmed");
Building Individual Instructions
For more control, build the swap instruction separately and compose it with other instructions (e.g., compute budget):
import { ComputeBudgetProgram } from "@solana/web3.js";
const { instruction, setupInstructions } = await client.buildSwapInstruction({
user: wallet.publicKey,
liquidityProvider: LP,
stableMint: USDC,
assetMint: USTB,
amount: new BN(1_000_000_000),
swapDirection: SwapDirection.StableToAsset,
swapType: SwapType.ExactIn,
minAmountOut: new BN(990_000_000),
});
// setupInstructions contains ATA creation if needed (autoCreateAta defaults to true)
// Compose with compute budget:
const instructions = [
ComputeBudgetProgram.setComputeUnitLimit({ units: 150_000 }),
ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 50_000 }),
...setupInstructions,
instruction,
];
The SDK does not include ComputeBudgetProgram instructions automatically. Set compute unit limits and priority fees based on your requirements. Use getQuoteViaSimulation() to measure actual compute units consumed.
Swap Examples
Buy RWA with USDC (ExactIn)
Swap exactly 1000 USDC for USTB, accepting at minimum 990 USTB:
const { transaction } = await client.buildSwapTransaction({
user: wallet.publicKey,
liquidityProvider: LP,
stableMint: USDC,
assetMint: USTB,
amount: new BN(1_000_000_000), // 1000 USDC (6 decimals)
swapDirection: SwapDirection.StableToAsset,
swapType: SwapType.ExactIn,
minAmountOut: new BN(990_000_000_000), // min 990 USTB (9 decimals)
});
Sell RWA for USDC (ExactIn)
Swap exactly 100 USTB for USDC, accepting at minimum 95 USDC:
const { transaction } = await client.buildSwapTransaction({
user: wallet.publicKey,
liquidityProvider: LP,
stableMint: USDC,
assetMint: USTB,
amount: new BN(100_000_000_000), // 100 USTB (9 decimals)
swapDirection: SwapDirection.AssetToStable,
swapType: SwapType.ExactIn,
minAmountOut: new BN(95_000_000), // min 95 USDC (6 decimals)
});
Buy Exact RWA Amount (ExactOut)
Receive exactly 100 USTB, spending at most 105 USDC:
const { transaction } = await client.buildSwapTransaction({
user: wallet.publicKey,
liquidityProvider: LP,
stableMint: USDC,
assetMint: USTB,
amount: new BN(100_000_000_000), // exact 100 USTB out (9 decimals)
swapDirection: SwapDirection.StableToAsset,
swapType: SwapType.ExactOut,
maxAmountIn: new BN(105_000_000), // max 105 USDC (6 decimals)
});
Sell RWA for Exact USDC (ExactOut)
Receive exactly 1000 USDC, spending at most 1010 USTB:
const { transaction } = await client.buildSwapTransaction({
user: wallet.publicKey,
liquidityProvider: LP,
stableMint: USDC,
assetMint: USTB,
amount: new BN(1_000_000_000), // exact 1000 USDC out (6 decimals)
swapDirection: SwapDirection.AssetToStable,
swapType: SwapType.ExactOut,
maxAmountIn: new BN(1_010_000_000_000), // max 1010 USTB (9 decimals)
});
Swap Parameters Reference
| Parameter | Type | Description |
|---|
user | PublicKey | Transaction signer |
liquidityProvider | PublicKey | LP that owns the pair |
stableMint | PublicKey | Stablecoin token mint |
assetMint | PublicKey | RWA token mint |
amount | BN | Primary amount in native token units (input for ExactIn, output for ExactOut) |
swapDirection | SwapDirection | StableToAsset or AssetToStable |
swapType | SwapType | ExactIn or ExactOut |
minAmountOut | BN (optional) | Minimum output for ExactIn swaps |
maxAmountIn | BN (optional) | Maximum input for ExactOut swaps |
autoCreateAta | boolean (optional) | Auto-create ATAs if missing (default: true) |
Event Parsing
Parse SwapExecuted events from transaction logs:
// From a transaction signature
const events = await client.parseSwapEventsFromTransaction(signature);
for (const event of events) {
console.log("Amount in:", event.amountIn.toString());
console.log("Amount out:", event.amountOut.toString());
console.log("Protocol fee:", event.protocolFeeAmount.toString());
console.log("Direction:", event.swapDirection);
}
// From raw logs (e.g., from websocket subscription)
const events = client.parseSwapEventsFromLogs(logs);
Error Handling
The SDK provides structured error parsing for on-chain program errors:
try {
const signature = await connection.sendTransaction(transaction);
await connection.confirmTransaction(signature);
} catch (error) {
const parsed = client.parseSwapError(error);
if (parsed) {
console.log("Error:", parsed.name, "-", parsed.message);
console.log("Category:", parsed.category);
switch (parsed.category) {
case "slippage":
// AmountOutTooLow or AmountInTooHigh — re-quote with fresh state
break;
case "paused":
// ProgramPaused, PairPaused, RwaPaused, StablePaused — wait or skip
break;
case "oracle":
// InvalidNav — NAV source unavailable or divergent
break;
case "liquidity":
// InsufficientLiquidity — try smaller amount or different pair
break;
case "input_validation":
// Fix input parameters
break;
case "math":
// MathOverflow/Underflow — trade may be too small or too large
break;
}
}
}
Convert between native token amounts (with decimals) and human-readable strings:
// Native to human-readable
MultiliquidClient.toHumanReadable(100_000_000n, 6); // "100"
MultiliquidClient.toHumanReadable(99_800_000_000n, 9); // "99.8"
// Human-readable to native
MultiliquidClient.toNativeAmount("100", 6); // 100_000_000n
MultiliquidClient.toNativeAmount("99.8", 9); // 99_800_000_000n
Reading On-Chain State
Fetch All Swap State (Single RPC Call)
const state = await client.fetchSwapState(stableMint, assetMint, lp);
console.log("Protocol fees:", state.globalConfig.protocolFeesBps, "bps");
console.log("Redemption fee:", state.pair.redemptionFeeBps, "bps");
console.log("Discount rate:", state.pair.discountRateBps, "bps");
console.log("Pair paused:", state.pair.paused);
console.log("Program paused:", state.globalConfig.paused);
Fetch Individual Accounts
const globalConfig = await client.fetchGlobalConfig();
const pair = await client.fetchPair(pairAddress);
const assetConfig = await client.fetchAssetConfig(configAddress);
PDA Derivation
All program accounts are deterministic PDAs. The SDK provides derivation helpers:
const [globalConfig] = client.deriveGlobalConfig();
const [assetConfig] = client.deriveAssetConfig(mint);
const [pair] = client.derivePair(lp, stableMint, assetMint);
const [vault] = client.deriveVault(mint, lp);
const [feeVault] = client.deriveFeeVault(stableMint);
const [lpStableConfig] = client.deriveLpStableConfig(stableMint, lp);
const [programAuthority] = client.deriveProgramAuthority();
Integration Best Practices
Security
- Use Hardware Wallets: Never store private keys in code or environment variables for production
- Simulate First: Always call
getQuote() or getQuoteViaSimulation() before executing
- Set Slippage: Always provide
minAmountOut or maxAmountIn for production swaps
- Check Pause State: Call
checkPauseStatus() before building transactions
- Use Client-Side Quotes:
getQuote() is faster than getQuoteViaSimulation() for most use cases
- Set Compute Budget: Use
computeUnitsConsumed from simulation to set appropriate compute limits
- Reuse Client: Create one
MultiliquidClient instance and reuse across operations
- Use Built-In Registry:
getPairs() returns instantly without RPC calls
Operational
- Priority Fees: Set appropriate priority fees via
ComputeBudgetProgram.setComputeUnitPrice() for time-sensitive operations
- Confirmation: Wait for
confirmed or finalized commitment before treating a swap as complete
- Event Monitoring: Use
parseSwapEventsFromTransaction() to verify swap results after execution
- Error Recovery: Use
parseSwapError() to categorize failures and implement appropriate retry logic
Testing
Devnet
Configure the client for devnet testing:
const connection = new Connection("https://api.devnet.solana.com");
const client = new MultiliquidClient({
connection,
cluster: "devnet",
});
// Devnet pairs use mock tokens — see SDK registry for addresses
const devnetPairs = client.getPairs();
Mainnet
const connection = new Connection("https://api.mainnet-beta.solana.com");
const client = new MultiliquidClient({
connection,
cluster: "mainnet-beta",
});
Support and Resources