@uniformlabs/multiliquid-svm-sdk
v0.3.2
Published
Multiliquid SVM SDK
Readme
@uniformlabs/multiliquid-svm-sdk
TypeScript SDK for swapping against and managing Multiliquid vaults on Solana. Handles pair discovery, NAV oracle resolution, client-side quoting with exact on-chain math replication, simulation-based quoting, and instruction/transaction building for swaps and LP-admin flows.
Current package version in this repository: 0.3.0.
Program IDs
| Network | Program ID |
| ------------ | ---------------------------------------------- |
| Devnet | HaWDr94LKJQT2fXuHJGsSGeQf6M7S68FXpEQLcE5RYs6 |
| Mainnet-Beta | HaWDr94LKJQT2fXuHJGsSGeQf6M7S68FXpEQLcE5RYs6 |
Installation
npm install @uniformlabs/multiliquid-svm-sdkRuntime dependencies are declared by the package, including @solana/web3.js ^1.98.4,
@coral-xyz/anchor ^0.32.1, and @solana/spl-token ^0.4.14.
Quick Start
import { Connection, Keypair, PublicKey } from "@solana/web3.js";
import { BN } from "@coral-xyz/anchor";
import { MultiliquidClient, SwapDirection, SwapType } from "@uniformlabs/multiliquid-svm-sdk";
const connection = new Connection("https://api.mainnet-beta.solana.com");
const client = new MultiliquidClient({
connection,
cluster: "mainnet-beta",
});
const wallet = Keypair.fromSecretKey(/* ... */);
const USDC = new PublicKey("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v");
const USTB = new PublicKey("CCz3SGVziFeLYk2xfEstkiqJfYkjaSWb2GCABYsVcjo2");
const LP = new PublicKey("C8Mi6kn7ajFWuNe4ZmsR9A6fdqRYhzXFoqVBGMsdJ2Uf");
// Buy USTB with 100 USDC
const { transaction } = await client.buildSwapTransaction({
user: wallet.publicKey,
liquidityProvider: LP,
stableMint: USDC,
assetMint: USTB,
amount: new BN(100_000_000), // 100 USDC (6 decimals)
swapDirection: SwapDirection.StableToAsset,
swapType: SwapType.ExactIn,
minAmountOut: new BN(41_000_000_000), // slippage protection
});
transaction.sign([wallet]);
const sig = await connection.sendTransaction(transaction);API
Client
Stateless client that holds connection config and delegates to standalone functions.
const client = new MultiliquidClient({
connection, // Solana Connection
cluster: "mainnet-beta", // "devnet" | "mainnet-beta" (default: "devnet")
commitment: "confirmed", // Commitment level for all RPC calls (default: "confirmed")
programId: undefined, // Override program ID (defaults per cluster)
registryApiUrl: undefined, // Override pair registry API base URL (defaults to Multiliquid production API)
registryFetchTimeoutMs: 10_000, // Optional API registry timeout; set 0 to disable timeout
onRegistryError: (error, context) => {
console.warn("Using static pair registry fallback", context, error);
},
});Pair Discovery
// API-backed registry lookup. Falls back to the static registry if unavailable.
const apiPairs = await client.getPairs({ stableMint, assetMint, liquidityProvider });
// Per-call cancellation is supported.
const abortablePairs = await client.getPairs(undefined, { signal: abortController.signal });
// Static registry lookup (no HTTP/RPC)
const offlinePairs = client.getOfflinePairs({ stableMint, assetMint, liquidityProvider });
// On-chain discovery via getProgramAccounts
const discoveredPairs = await client.discoverPairs({
stableMint,
assetMint,
liquidityProvider,
});All filters are optional and AND-combined.
Multiple LPs can list the same stableMint / assetMint pair. Include liquidityProvider when you need a specific pool rather than the first matching mint pair.
getPairs() is stateless and does not cache API results. Memoize or cache results in UI/server code when rendering pair lists repeatedly. If the API returns new mints that are not bundled in the static registry, the client uses its Solana connection to fetch mint decimals before returning the pair.
The default registry API base URL is https://api.multiliquid.xyz. registryApiUrl may be either that style of base URL or a full /v1/info/svm endpoint. API-backed entries may include vaultAuthority; static entries do not.
Quoting
// Client-side quote — replicates on-chain math exactly via BigInt
const quote = await client.getQuote({
user: wallet.publicKey,
liquidityProvider: LP,
stableMint: USDC,
assetMint: RWA,
amount: new BN(100_000_000),
swapDirection: SwapDirection.StableToAsset,
swapType: SwapType.ExactIn,
});
// => { amountIn, amountOut, protocolFees, discountAmount, amountInForVault, assetNav, stableNav }
// Simulation-based quote — simulates the swap on-chain without executing
const simQuote = await client.getQuoteViaSimulation(params);
// => { amountIn, amountOut, protocolFee, discountBps, pair, swapDirection, swapType, computeUnitsConsumed }getQuoteViaSimulation also returns computeUnitsConsumed, useful for setting a tight compute budget.
Instruction & Transaction Building
// Instruction-first: returns TransactionInstruction for composability
const { instruction, setupInstructions, accounts } = await client.buildSwapInstruction(params);
// Transaction convenience: returns unsigned VersionedTransaction
const { transaction, accounts } = await client.buildSwapTransaction(params);setupInstructionscontains ATA creation instructions whenautoCreateAta: true(default).- No
ComputeBudgetPrograminstructions are included — set compute units and priority fees yourself. - Set
autoCreateAta: falseif you manage ATA creation externally. - Swap builders automatically resolve oracle accounts and any Token-2022 transfer-hook accounts needed for the actual token transfers.
- Some mainnet RWA transfer hooks derive extra accounts from token account data; for those mints, pre-create the relevant user token account or pass an existing token account override before building the swap.
- Known mainnet RWA transfer hooks are exported as
MAINNET_RWA_TRANSFER_HOOKS; hook-bearing assets areACRED,BENJI,WTGXX, andVBILL.USTBandUSCCdo not currently have transfer hooks. remainingAccountsis only for additional/manual accounts beyond what the SDK derives automatically.
LP Admin & Liquidity
// Open a pair. liquidityProvider signs and pays; admin is fetched from global config if omitted.
const { instruction: initPairIx } = await client.buildInitPairInstruction({
liquidityProvider: wallet.publicKey,
stableMint: USDC,
assetMint: USTB,
redemptionFeeBps: 10,
discountRateBps: 15,
});
// Update pair fees and pause state
const { instruction: updatePairIx } = await client.buildUpdatePairInstruction({
liquidityProvider: wallet.publicKey,
stableMint: USDC,
assetMint: USTB,
redemptionFeeBps: 10,
discountRateBps: 15,
paused: false,
});
// Deposit liquidity, deriving the LP ATA and vault ATA unless overridden
const { instruction: addLiquidityIx, setupInstructions } = await client.buildAddLiquidityInstruction({
liquidityProvider: wallet.publicKey,
mint: USDC,
amount: new BN(1_000_000_000),
});
// Withdraw liquidity with an unsigned VersionedTransaction helper
const { transaction: removeLiquidityTx } = await client.buildRemoveLiquidityTransaction({
liquidityProvider: wallet.publicKey,
mint: USTB,
amount: new BN(500_000_000),
});
// Close a pair and return any remaining vault balances to LP token accounts
const { transaction: closePairTx } = await client.buildClosePairTransaction({
liquidityProvider: wallet.publicKey,
stableMint: USDC,
assetMint: USTB,
});buildInitPairInstruction/buildInitPairTransactionwrapinit_pairwith the LP as the signer/payer.adminis a non-signing account checked by global config and is fetched automatically unless provided.- Init-pair builders pre-validate the derived asset configs and reject swapped stable/RWA mints before building the instruction.
buildUpdatePairInstruction/buildUpdatePairTransactionwrap the on-chainupdate_pairinstruction.buildAddLiquidityInstructionreturnssetupInstructionswhen the LP ATA or LP vault ATA needs to be created.buildRemoveLiquidityInstructionreturnssetupInstructionswhen the LP ATA needs to be created.buildClosePairInstructionreturnssetupInstructionswhen LP recipient ATAs for the stable or asset mint need to be created, except when a Token-2022 transfer-hook mint will transfer during close; in that case the recipient account must already exist on-chain before hook accounts can be resolved.- Override
lpTokenAccountif the LP uses a non-ATA token account. - Override
lpStableTokenAccount/lpAssetTokenAccountif close-pair proceeds should be returned to non-ATA token accounts. - Add-liquidity vault ATAs are owned by the SDK-derived LP vault authority PDA and paid for by the liquidity provider. Set
autoCreateAta: falseif the app creates both the LP ATA and vault ATA itself. - Add/remove liquidity and close-pair builders also auto-resolve Token-2022 transfer-hook accounts.
- Close-pair only resolves vault balances and transfer-hook accounts for a mint when that mint's LP
UserVaultInfo.usedis1, meaning the on-chain close will transfer and close that vault. Recipient token accounts are still required by the on-chain account constraints. - For close-pair on a Token-2022 transfer-hook mint that will transfer, pre-create the LP recipient token account or pass
lpStableTokenAccount/lpAssetTokenAccount; transfer-hook meta resolution cannot see token accounts that would only be created by the same transaction's setup instructions. - Close-pair transfer-hook account resolution uses the vault balance observed at build time, while the on-chain instruction transfers the vault balance at execution time. Rebuild close-pair transactions shortly before sending if a hook derives accounts from transfer amount or the vault may change concurrently.
- Close-pair
accounts.remainingAccountspreserves the fullAccountMeta[]flags returned to Anchor. - For Token-2022 mints whose transfer hooks require destination token-account data, the add-liquidity vault ATA must already exist on-chain before hook accounts can be resolved. In that case, pre-create the vault ATA before building the add-liquidity instruction.
remainingAccountsremains available for extra/manual accounts beyond the SDK-derived set.
Swap Parameters
interface SwapParams {
user: PublicKey;
liquidityProvider: PublicKey;
stableMint: PublicKey;
assetMint: PublicKey;
amount: BN;
swapDirection: SwapDirection; // StableToAsset | AssetToStable
swapType: SwapType; // ExactIn | ExactOut
minAmountOut?: BN; // Slippage protection (ExactIn)
maxAmountIn?: BN; // Slippage protection (ExactOut)
userStableTokenAccount?: PublicKey; // Override ATA derivation
userAssetTokenAccount?: PublicKey; // Override ATA derivation
autoCreateAta?: boolean; // Default: true
remainingAccounts?: AccountMeta[]; // Optional extra accounts to append after SDK-derived accounts
}Account Fetching
const globalConfig = await client.fetchGlobalConfig();
const pair = await client.fetchPair(pairAddress);
const assetConfig = await client.fetchAssetConfig(configAddress);
const lpStableConfig = await client.fetchLpStableConfig(configAddress);
// Batch fetch all 5 accounts needed for a swap in a single RPC call
const state = await client.fetchSwapState(stableMint, assetMint, lp);
// Check pause status across all levels
const status = await client.checkPauseStatus(stableMint, assetMint, lp);
// => { globalPaused, pairPaused, rwaPaused, stablePaused, lpStablePaused, anyPaused, pauseReasons }PDA Derivation
All return [PublicKey, number] (address + bump).
client.deriveGlobalConfig();
client.deriveAssetConfig(mint);
client.derivePair(lp, stableMint, assetMint);
client.deriveVault(mint, lp, tokenProgram); // LP vault ATA; tokenProgram is optional and defaults to SPL Token
client.deriveVaultAuthority(lp);
client.deriveProgramAuthority();
client.deriveFeeVault(stableMint, tokenProgram); // fee vault ATA; tokenProgram is optional and defaults to SPL Token
client.deriveLpStableConfig(stableMint, lp);When deriving vault addresses manually for Token-2022 mints, pass the Token-2022 program ID as
tokenProgram. Swap and liquidity builders detect the mint owner and derive the correct vault ATAs
automatically.
Event Parsing
// From transaction logs
const events = client.parseSwapEventsFromLogs(logs);
// From a transaction signature
const events = await client.parseSwapEventsFromTransaction(signature);
// => SwapExecutedEvent[]
// { requestor, amountIn, protocolFeeAmount, discountBps, amountOut, pair, swapDirection, swapType }Error Handling
try {
await connection.sendTransaction(transaction);
} catch (err) {
const parsed = client.parseSwapError(err);
if (parsed) {
console.error(`[${parsed.code}] ${parsed.name}: ${parsed.message}`);
// parsed.category: "slippage" | "paused" | "oracle" | "input_validation" | "math" | "liquidity" | "user_funds"
}
}| Category | Meaning | Recommended Action |
| ------------------ | ----------------------------------- | ------------------------------- |
| slippage | Output below min or input above max | Re-quote with fresh state |
| paused | Program, pair, or asset is paused | Skip route until state changes |
| oracle | NAV returned 0 or oracle error | Skip route |
| input_validation | Invalid parameters (amount=0, etc.) | Fix input |
| math | Overflow/underflow or fee config | Trade may be too small |
| liquidity | Vault can't fill the swap | Try different LP or reduce size |
| user_funds | User balance or token account issue | Fund or create the account |
Amount Formatting
import { toHumanReadable, toNativeAmount } from "@uniformlabs/multiliquid-svm-sdk";
toHumanReadable(100_000_000n, 6); // "100"
toHumanReadable(99_800_000_000n, 9); // "99.8"
toNativeAmount("100", 6); // 100_000_000n
toNativeAmount("99.8", 9); // 99_800_000_000nStandalone Functions
Every MultiliquidClient method is also available as a standalone function:
import {
buildSwapInstruction,
buildSwapTransaction,
buildUpdatePairInstruction,
buildUpdatePairTransaction,
buildAddLiquidityInstruction,
buildAddLiquidityTransaction,
buildRemoveLiquidityInstruction,
buildRemoveLiquidityTransaction,
getQuote,
getQuoteViaSimulation,
getPairs,
getOfflinePairs,
discoverPairs,
fetchSwapState,
checkPauseStatus,
calculateSwapResults,
calculateNav,
resolveOracleAccounts,
parseSwapError,
parseSwapEventsFromLogs,
parseSwapEventsFromTransaction,
decodeSwapExecutedEvent,
detectTokenProgram,
ensureAtaInstructions,
toHumanReadable,
toNativeAmount,
deriveGlobalConfig,
deriveAssetConfig,
derivePair,
deriveVault,
deriveVaultAuthority,
deriveProgramAuthority,
deriveFeeVault,
deriveLpStableConfig,
PROGRAM_ID_DEVNET,
PROGRAM_ID_MAINNET,
DEFAULT_REGISTRY_API_URL,
DEFAULT_REGISTRY_FETCH_TIMEOUT_MS,
MAINNET_RWA_TRANSFER_HOOKS,
SWAP_COMPUTE_UNITS,
SWAP_ERROR_MAP,
} from "@uniformlabs/multiliquid-svm-sdk";Standalone getPairs() takes the cluster explicitly and accepts the same registry options used by the client:
const apiPairs = await getPairs("mainnet-beta", { stableMint }, { connection });
const offlinePairs = getOfflinePairs("mainnet-beta", { stableMint });Design Principles
- Instruction-first — Returns
TransactionInstructionfor composability. Compute budget is the consumer's responsibility. - Exact math — Client-side quoting replicates on-chain Rust math with BigInt to produce identical results.
- Stateless — No caching beyond a single method call.
- Commitment-consistent — All RPC calls within a single operation use the same commitment level.
- Pass-through errors — Anchor errors propagate directly.
parseSwapErroris an optional utility.
License
ISC
