@uniformlabs/multiliquid-evm-sdk
v0.1.1
Published
TypeScript SDK for the Multiliquid protocol, built on viem.
Readme
@uniformlabs/multiliquid-evm-sdk
TypeScript SDK for the Multiliquid protocol, built on viem.
Typed, composable access to swap execution, quote previews with simulation, asset/price reads, stablecoin delegate queries, yield operations, and event parsing against the production Multiliquid contracts.
Installation
npm install @uniformlabs/multiliquid-evm-sdk viemQuick Start
import { createMultiliquidClient, mainnet } from "@uniformlabs/multiliquid-evm-sdk";
import { createPublicClient, createWalletClient, http } from "viem";
import { mainnet as viemMainnet } from "viem/chains";
import { privateKeyToAccount } from "viem/accounts";
const account = privateKeyToAccount("0x...");
const publicClient = createPublicClient({ chain: viemMainnet, transport: http() });
const walletClient = createWalletClient({ chain: viemMainnet, transport: http(), account });
const ml = createMultiliquidClient({
deployment: mainnet,
publicClient,
walletClient,
});
// Get a quote
const quote = await ml.quote.swapIntoRWA({
rwaID: mainnet.assetIds.rwa.ULTRA,
stablecoinID: mainnet.assetIds.stablecoin.USDC,
stablecoinAmount: 1_000_000n, // 1 USDC (6 decimals)
user: account.address,
simulate: true, // optional: verify the swap would succeed on-chain
});
console.log("RWA output:", quote.rwaAmount);
console.log("Simulation passed:", quote.simulation?.success);
// Execute the swap
const txHash = await ml.swap.swapIntoRWA({
rwaID: mainnet.assetIds.rwa.ULTRA,
stablecoinID: mainnet.assetIds.stablecoin.USDC,
stablecoinAmount: 1_000_000n,
minRwaAmount: quote.rwaAmount, // use quote output as slippage bound
});Client Setup
createMultiliquidClient returns a client with seven modules. Pass a publicClient for read-only access, or add a walletClient to enable swap execution and yield accrual.
import { createMultiliquidClient, mainnet } from "@uniformlabs/multiliquid-evm-sdk";
// Read-only (quotes, asset reads, events)
const ml = createMultiliquidClient({
deployment: mainnet,
publicClient,
});
// Read + write (adds swap execution, yield accrual)
const ml = createMultiliquidClient({
deployment: mainnet,
publicClient,
walletClient,
});The SDK ships with mainnet and sepolia chain presets. You can provide a custom ChainDeployment for any EVM chain — see Custom Chains.
Modules
Quotes
Get price quotes for all swap types. Every quote method accepts an optional simulate: true flag that runs an eth_call simulation to verify the swap would succeed (catches blacklist, pause, whitelist, balance, and allowance issues).
// Stablecoin → RWA (exact input)
const quote = await ml.quote.swapIntoRWA({
rwaID: mainnet.assetIds.rwa.ULTRA,
stablecoinID: mainnet.assetIds.stablecoin.USDC,
stablecoinAmount: 1_000_000n,
user: account.address,
simulate: true,
});
// quote.rwaAmount, quote.protocolFee, quote.redemptionFee, quote.simulation
// Stablecoin → RWA (exact output)
await ml.quote.swapIntoRWAExactOut({
rwaID,
stablecoinID,
rwaAmount: 500_000n,
user: account.address,
});
// RWA → Stablecoin (exact input / exact output)
await ml.quote.swapIntoStablecoin({ stablecoinID, rwaID, rwaAmount: 1_000_000n });
await ml.quote.swapIntoStablecoinExactOut({
stablecoinID,
rwaID,
stablecoinAmount: 1_000_000n,
user,
});
// RWA → RWA (through a stablecoin issuer)
await ml.quote.swapRWAToRWA({
stablecoinID,
rwaInID,
rwaOutID,
exactOut: false,
rwaInAmount: 1_000_000n,
rwaOutAmount: 0n,
});
// Stablecoin → Stablecoin
await ml.quote.swapStablecoinToStablecoin({
stablecoinInID,
stablecoinOutID,
exactOut: false,
useDelegateForStablecoinOut: true,
stablecoinInAmount: 1_000_000n,
stablecoinOutAmount: 0n,
user,
});Simulation results are returned on the quote object:
if (quote.simulation?.success) {
console.log("Gas estimate:", quote.simulation.gasEstimate);
} else if (quote.simulation) {
console.log("Would revert:", quote.simulation.error);
}Swaps
Execute swap transactions. Requires a walletClient. All methods return a transaction hash — the SDK does not wait for receipts.
const txHash = await ml.swap.swapIntoRWA({
rwaID: mainnet.assetIds.rwa.ULTRA,
stablecoinID: mainnet.assetIds.stablecoin.USDC,
stablecoinAmount: 1_000_000n,
minRwaAmount: 950_000n, // slippage protection
});Available: swapIntoRWA, swapIntoRWAExactOut, swapIntoStablecoin, swapIntoStablecoinExactOut, swapRWAToRWA, swapStablecoinToStablecoin.
Build Calldata Without Sending
For smart account batching, meta-transactions, or custom submission flows:
const calldata = ml.swap.buildSwapCalldata("swapIntoRWA", {
rwaID: mainnet.assetIds.rwa.ULTRA,
stablecoinID: mainnet.assetIds.stablecoin.USDC,
stablecoinAmount: 1_000_000n,
minRwaAmount: 950_000n,
});
await walletClient.sendTransaction({
to: mainnet.addresses.multiliquidSwap,
data: calldata,
});Assets
Read-only queries for asset information, prices, fees, and access control. Bulk reads are batched via Multicall3.
// Individual reads
const rwaInfo = await ml.assets.getRWAInfo(mainnet.assetIds.rwa.ULTRA);
const stablecoinInfo = await ml.assets.getStablecoinInfo(mainnet.assetIds.stablecoin.USDC);
const price = await ml.assets.getRWAPrice(mainnet.assetIds.rwa.ULTRA);
// Bulk reads (multicalled — single RPC round-trip)
const allRWAs = await ml.assets.getAllRWAInfo(); // all known RWAs
const allStables = await ml.assets.getAllStablecoinInfo(); // all known stablecoins
const allPrices = await ml.assets.getAllPrices(); // all RWA + stablecoin prices
// Fee configuration
const feeTiers = await ml.assets.getFeeTiers();
const discount = await ml.assets.getDiscountRate(stablecoinID, rwaID);
const redemptionFee = await ml.assets.getRedemptionFee(stablecoinID, rwaID);
// Access control
const paused = await ml.assets.isPaused();
const blacklisted = await ml.assets.isBlacklisted(userAddress);
const whitelisted = await ml.assets.isWhitelistedForRWA(rwaID, from, to, amount);Delegates
Read from stablecoin delegate contracts. The SDK resolves delegate addresses automatically from the stablecoin ID.
const info = await ml.delegates.getDelegateInfo(mainnet.assetIds.stablecoin.USDC);
// info.type → "balanceSheet" | "yield"
// info.rwaCustodyAddress, info.stablecoinCustodyAddress
// info.rwaColdStorageAddresses, info.stablecoinColdStorageAddresses
const isRwaOk = await ml.delegates.isRWAWhitelisted(stablecoinID, rwaTokenAddress);
const isStableOk = await ml.delegates.isStablecoinWhitelisted(
stablecoinID,
backingStablecoinAddress,
);Yield
Operations for yield-bearing stablecoin delegates (e.g., TSY_YIELD). Throws NotYieldBearingError if called on a non-yield-bearing stablecoin.
const tsyYield = mainnet.assetIds.stablecoin.TSY_YIELD;
// Read yield data
const currentDay = await ml.yield.getCurrentDay(tsyYield);
const rate = await ml.yield.getDailyRate(tsyYield, currentDay - 1n);
const rates = await ml.yield.getDailyRates(tsyYield, 1n, currentDay); // multicalled
// Yield-adjusted amounts
const yieldAmount = await ml.yield.getYieldAmount({
stablecoinID: tsyYield,
user: "0x...",
redeemValue: 1_000_000_000_000_000_000n, // 1 WAD
stablecoinWithdrawal: true,
});
// Accrue interest (requires walletClient)
await ml.yield.accrueInterest(tsyYield, userAddress);
await ml.yield.batchAccrueInterest(tsyYield, [user1, user2, user3]);
await ml.yield.accrueInterestPaged(tsyYield, userAddress, 365n); // max 365 days per txEvents
Parse and subscribe to Multiliquid swap events. Events are returned as a discriminated union with six types: SwapIntoRWA, SwapIntoRWAExactOut, SwapIntoStablecoin, SwapIntoStablecoinExactOut, RWAToRWASwap, StablecoinToStablecoinSwap.
// Fetch historical events
const events = await ml.events.getEvents({
fromBlock: 19_000_000n,
toBlock: "latest",
user: "0x...", // optional: filter by user
rwaID: mainnet.assetIds.rwa.ULTRA, // optional: filter by RWA
eventTypes: ["SwapIntoRWA"], // optional: filter by type
});
for (const event of events) {
console.log(event.type, event.rwaAmount, event.stablecoinAmount);
}
// Watch for new events in real-time
const unsubscribe = ml.events.watchEvents({ user: "0x..." }, (event) =>
console.log("New event:", event.type),
);
// Stop watching
unsubscribe();
// Parse a raw viem log
const parsed = ml.events.parseLog(rawLog);Multicall
Compose arbitrary read calls into a single RPC round-trip. Fully typed — return types are inferred from the ABI and function name.
import { multiliquidSwapAbi, priceAdapterAbi } from "@uniformlabs/multiliquid-evm-sdk";
const [rwaInfo, price] = await ml.multicall.read([
{
address: mainnet.addresses.multiliquidSwap,
abi: multiliquidSwapAbi,
functionName: "rwaInfo",
args: [mainnet.assetIds.rwa.ULTRA],
},
{
address: "0x21A20895ec8262b749eB16dE62BBE6fe23345187",
abi: priceAdapterAbi,
functionName: "getPrice",
},
]);Error Handling
All contract interactions are wrapped in typed error decoding. Revert reasons are parsed into specific error subclasses with structured fields.
import {
MultiliquidError,
MultiliquidContractError,
UserBlacklistedError,
UserNotWhitelistedError,
ContractPausedError,
InsufficientRWAOutputError,
} from "@uniformlabs/multiliquid-evm-sdk";
try {
await ml.swap.swapIntoRWA({ ... });
} catch (error) {
if (error instanceof UserBlacklistedError) {
console.log("Blacklisted user:", error.args.user);
} else if (error instanceof UserNotWhitelistedError) {
console.log("Not whitelisted:", error.args.rwaID, error.args.user);
} else if (error instanceof ContractPausedError) {
console.log("Contract is paused");
} else if (error instanceof InsufficientRWAOutputError) {
console.log("Slippage exceeded");
} else if (error instanceof MultiliquidContractError) {
console.log("Contract error:", error.errorName, error.args);
} else if (error instanceof MultiliquidError) {
console.log("SDK error:", error.message);
}
}A standalone decodeMultiliquidError(data) function is also exported for decoding raw revert data outside the SDK's call wrappers.
Error Classes
| Class | Solidity Error | Structured Args |
| ----------------------------------- | ------------------------------ | ------------------------------------------------- |
| RWANotAcceptedError | RWANotAccepted | — |
| StablecoinNotAcceptedError | StablecoinNotAccepted | — |
| InsufficientRWAOutputError | InsufficientRWAOutput | — |
| InsufficientStablecoinOutputError | InsufficientStablecoinOutput | — |
| InsufficientRWAInputError | InsufficientRWAInput | — |
| InsufficientStablecoinInputError | InsufficientStablecoinInput | — |
| UserBlacklistedError | UserBlacklisted | { user } |
| UserNotWhitelistedError | UserNotWhitelisted | { rwaID, user } |
| StablecoinPriceOutsideBandError | StablecoinPriceOutsideBand | { stablecoinID, adminValue, oraclePrice, band } |
| PriceAdapterNotSetError | PriceAdapterNotSet | { tokenID } |
| ContractPausedError | EnforcedPause | — |
| NotYieldBearingError | (SDK-level) | — |
| NoWalletClientError | (SDK-level) | — |
Custom Chains
Provide your own ChainDeployment for any EVM chain where Multiliquid is deployed:
import { createMultiliquidClient } from "@uniformlabs/multiliquid-evm-sdk";
import type { ChainDeployment } from "@uniformlabs/multiliquid-evm-sdk";
const myChain: ChainDeployment = {
chainId: 42161,
addresses: {
multiliquidSwap: "0x...",
// Optional: preset delegate/adapter/token addresses to avoid on-chain lookups
delegates: { "0xstablecoinId...": "0xdelegateAddr..." },
priceAdapters: { "0xrwaId...": "0xadapterAddr..." },
tokens: { "0xassetId...": "0xtokenAddr..." },
multicall3: "0x...", // defaults to canonical 0xcA11bde05977b3631167028862bE2a173976CA11
},
assetIds: {
rwa: { MY_RWA: "0x..." },
stablecoin: { MY_STABLE: "0x..." },
},
};
const ml = createMultiliquidClient({ deployment: myChain, publicClient });Constants
import { WAD, ROLES } from "@uniformlabs/multiliquid-evm-sdk";
WAD; // 1_000_000_000_000_000_000n (1e18)
ROLES.DEFAULT_ADMIN; // bytes32(0)
ROLES.OPERATOR; // keccak256("OPERATOR_ROLE")
ROLES.BLACKLISTER; // keccak256("BLACKLISTER_ROLE")ABIs
All contract ABIs are exported as const assertions for direct use with viem:
import {
multiliquidSwapAbi,
stablecoinDelegateBaseAbi,
treasuryDelegateAbi,
balanceSheetDelegateAbi,
priceAdapterAbi,
yieldBearingDelegateAbi,
whitelistAdapterAbi,
erc20Abi,
} from "@uniformlabs/multiliquid-evm-sdk";Supported Assets
Mainnet
RWAs: ULTRA, JTRSY, WTGXX, BENJI, USTB, VBILL
Stablecoins: USDC, TSY_YIELD
Access IDs via mainnet.assetIds.rwa.ULTRA, mainnet.assetIds.stablecoin.USDC, etc.
Sepolia Testnet
RWAs: MOCK_RWA_WHITELIST, MOCK_RWA_NO_WHITELIST
Stablecoins: USDC, TSY_YIELD
Access IDs via sepolia.assetIds.rwa.MOCK_RWA_WHITELIST, etc.
Requirements
- Node.js >= 18
- viem >= 2.x
- TypeScript >= 5.x (recommended)
License
ISC
