npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@uniformlabs/multiliquid-evm-sdk

v0.4.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, signed redemption intake, quote previews with simulation, asset/price reads, stablecoin delegate queries, issuer admin operations, yield operations, and event parsing against the production Multiliquid contracts.

Installation

npm install @uniformlabs/multiliquid-evm-sdk viem

Quick Start

import { ROUTE_IDS, 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,
});

const input = {
  routeId: ROUTE_IDS.STABLE_TO_RWA_EXACT_IN,
  assetInID: mainnet.assetIds.stablecoin.USDC,
  assetOutID: mainnet.assetIds.rwa.ULTRA,
  stablecoinDelegateID: mainnet.assetIds.stablecoin.USDC,
  assetInAmt: 1_000_000n, // 1 USDC (6 decimals)
  assetOutAmt: 0n,
};

// Get a quote.
const quote = await ml.quote.quoteSwap({
  user: account.address,
  input,
});

// Build the exact bounded leg you will submit, then simulate that same leg.
const boundedInput = { ...input, assetOutAmt: quote.tokenOutAmount };
const simulation = await ml.quote.quoteSwap({
  user: account.address,
  input: boundedInput,
  simulate: true,
});

console.log("RWA output:", quote.tokenOutAmount);
console.log("Simulation passed:", simulation.simulation?.success);

// Execute the swap
const txHash = await ml.swap.swap({
  inputs: [boundedInput],
});

Client Setup

createMultiliquidClient returns a client with ten modules. Pass a publicClient for read-only access, including native Grove liquidity checks, or add a walletClient to enable swap execution, redemption signing, issuer admin writes, and yield accrual. Redemption HTTP methods use https://api.multiliquid.xyz by default; override apiBaseUrl or inject apiFetch when needed.

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, issuer admin writes, yield accrual)
const ml = createMultiliquidClient({
  deployment: mainnet,
  publicClient,
  walletClient,
});

// Optional API override for redemption submit/status calls
const ml = createMultiliquidClient({
  deployment: mainnet,
  publicClient,
  walletClient,
  apiBaseUrl: "https://api.example.com",
  apiFetch: customFetch,
});

The SDK ships with mainnet and sepolia chain presets. You can provide a custom ChainDeployment for any EVM chain — see Custom Chains.

Remote Deployment Metadata

Static presets are the default. To opt into backend deployment metadata, fetch a merged deployment first:

import {
  DEFAULT_EVM_INFO_API_BASE_URL,
  createMultiliquidClient,
  loadEvmDeployment,
} from "@uniformlabs/multiliquid-evm-sdk";

const deployment = await loadEvmDeployment({ chain: "mainnet" });
const ml = createMultiliquidClient({ deployment, publicClient });

// Optional overrides:
await loadEvmDeployment({
  chain: "sepolia",
  apiBaseUrl: DEFAULT_EVM_INFO_API_BASE_URL,
  fetch: customFetch,
});

loadEvmDeployment uses https://api.multiliquid.xyz/v1/info/evm by default and merges the response over the static preset, including stablecoin delegates, RWA delegate contracts, token addresses, and price adapters. apiBaseUrl may be an API base URL such as https://api.multiliquid.xyz or the exact /v1/info/evm endpoint. If you do not call this helper, the SDK continues to use the hardcoded preset values.

Modules

Quotes

Get price quotes with the production quoteSwap(user, input) contract entry point. The SDK exposes this as ml.quote.quoteSwap(...); route selection is controlled by input.routeId. The method requires user because fees, whitelist checks, and yield accounting can depend on the account. Pass simulate: true to run an eth_call simulation against the actual swap(...) calldata.

const input = {
  routeId: ROUTE_IDS.STABLE_TO_RWA_EXACT_IN,
  assetInID: mainnet.assetIds.stablecoin.USDC,
  assetOutID: mainnet.assetIds.rwa.ULTRA,
  stablecoinDelegateID: mainnet.assetIds.stablecoin.USDC,
  assetInAmt: 1_000_000n,
  assetOutAmt: 0n,
};

const quote = await ml.quote.quoteSwap({
  user: account.address,
  input,
  simulate: true,
});
// quote.tokenInAmount, quote.tokenOutAmount, quote.issuerFee, quote.spreadFee, quote.lpFee

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);
}

simulate: true still quotes through the contract's quoteSwap(user, input) view function. The simulation is a separate local eth_call against swap(user, receiver ?? user, [input], permit ?? emptyPermit), using the exact SwapInputs leg you passed. By default the simulated msg.sender is user; for delegated swaps, pass operator so permit verification and delegated allowance checks run from the same address that will submit the transaction. Use receiver only when you want that simulated swap to test a non-user receiver, and put your execution bounds directly into the leg: exact-in routes use assetOutAmt as the minimum output, while exact-out routes use assetInAmt as the maximum input. If you use quote results to set those bounds, build the bounded leg first and simulate that same bounded leg before submitting it.

Swaps

Execute swap transactions with the production swap(user, receiver, inputs, permit) function. Requires a walletClient. The SDK exposes only ml.swap.swap(...) for execution; pass one or more SwapInputs legs. The method returns a transaction hash and does not wait for receipts.

const txHash = await ml.swap.swap({
  inputs: [
    {
      routeId: ROUTE_IDS.STABLE_TO_RWA_EXACT_IN,
      assetInID: mainnet.assetIds.stablecoin.USDC,
      assetOutID: mainnet.assetIds.rwa.ULTRA,
      stablecoinDelegateID: mainnet.assetIds.stablecoin.USDC,
      assetInAmt: 1_000_000n,
      assetOutAmt: 950_000n,
    },
  ],
});

Routes that use already-prefunded custody balances are selected by using the prefunded ROUTE_IDS values. The SDK exposes the route flags and preview reads; the contract still derives and executes any auto-liquidity legs internally.

For stablecoin-to-stablecoin legs, stablecoinDelegateID may be either assetInID or assetOutID, including when one side is yield-bearing. The selected delegate domain is part of signed approvals, so input-delegate and output-delegate forms authorize distinct calldata.

Auto-Liquidity Preview

autoLiquidityStablecoinIDs is contract configuration: when a requested leg uses a configured stablecoin delegate, the contract may derive internal LP prefund or sweep legs around the user-submitted leg. The SDK does not submit those derived legs. It can preview the configuration, LP custody vault, relevant allowances, balances, and Treasury credit inventory:

const preview = await ml.swap.getAutoLiquidityPreview({
  user: account.address,
  input,
});

console.log(preview.autoLiquidityStablecoinID);
console.log(preview.lpVault);

if (preview.prefund.enabled) {
  // Internal exact-out stablecoin-to-stablecoin leg the contract will derive.
  console.log(preview.prefund.input);
  console.log(preview.prefund.allowanceSufficient);
  console.log(preview.prefund.tokenBalanceSufficient);
  console.log(preview.prefund.yieldCreditsSufficient);
}

if (preview.sweep.eligible) {
  // Sweep amount is measured on-chain as the LP vault balance delta during swap execution.
  console.log(preview.sweep.inputAssetBalanceBefore);
  console.log(preview.sweep.estimatedInputAmount);
  console.log(preview.sweep.allowance);
  console.log(preview.sweep.allowanceSufficient);
}

For prefunded routes, preview.prefund.input shows the internally derived STABLE_TO_STABLE_EXACT_OUT leg and preview.prefund.quote shows the LP vault input needed at the time of preview. Prefund is skipped when the configured auto-liquidity stablecoin is already the requested output. If it is the requested input, prefund is skipped only when the requested leg also executes through that input delegate; output-delegate stable-to-stable execution can still prefund the output delegate's custody for secondary-market fills. If the auto-liquidity stablecoin is yield-bearing, the executing contract syncs the LP vault's yield state before pricing, so the preview is based on current view state and can shift if accrual is stale. For sweep routes, the final sweep amount cannot be known before execution because the contract measures the LP vault's token balance delta around the requested leg. A positive but insufficient sweep allowance can revert the whole swap, so preview.sweep.enabled is true only when the allowance covers the requested quote's input amount.

Multi-Leg Swaps and Signatures

const inputs = [
  {
    routeId: ROUTE_IDS.STABLE_TO_RWA_EXACT_IN,
    assetInID: stablecoinID,
    assetOutID: rwaID,
    stablecoinDelegateID: stablecoinID,
    assetInAmt: 1_000_000n,
    assetOutAmt: 950_000n,
  },
];

const permit = await ml.swap.signSwapApproval({
  user: account.address,
  operator: operatorAddress,
  receiver: account.address,
  inputs,
  deadline: BigInt(Math.floor(Date.now() / 1000) + 15 * 60),
});

// Submitted by the signed operator.
const txHash = await ml.swap.swap({
  user: account.address,
  receiver: account.address,
  inputs,
  permit,
});

signSwapApproval returns the full on-chain ApprovalPermit struct: { deadline, signature, nonce, unordered }. By default it signs an ordered SwapBatchApproval against the user's current nonces(user) value, so ordered permits must settle in sequence and each settlement invalidates any other outstanding ordered permit. The nonce field is ignored on-chain for ordered permits.

For permits that can settle in any order, sign an UnorderedSwapBatchApproval by passing unordered: true and an explicit nonce of your choice. Each unordered nonce is consumed independently and does not touch nonces(user):

const permit = await ml.swap.signSwapApproval({
  operator: operatorAddress,
  inputs,
  deadline,
  unordered: true,
  nonce: 1234n, // any uint256 not yet used by this user
});

await ml.swap.isUnorderedNonceUsed(account.address, 1234n); // false until settled or invalidated

// Signed by the user: permanently cancels an outstanding unordered permit (works while paused).
await ml.swap.invalidateUnorderedNonce({ nonce: 1234n });

Hand-built permits may omit nonce and unordered; the SDK encodes them as 0n and false.

For persistent delegated access, use adjustSwapInputAllowance, getSwapInputAllowance, and getSwapNonce. Standing swapInputAllowances only authorize delegated swaps when receiver is the same as user; delegated swaps to a non-user receiver require a one-shot signSwapApproval permit for that exact transaction. getDomainSeparator() reads the contract's public DOMAIN_SEPARATOR() value for EIP-712 approval diagnostics.

Build Calldata Without Sending

For smart account batching, meta-transactions, or custom submission flows:

const calldata = ml.swap.buildSwapCalldata({
  user: account.address,
  inputs: [
    {
      routeId: ROUTE_IDS.STABLE_TO_RWA_EXACT_IN,
      assetInID: mainnet.assetIds.stablecoin.USDC,
      assetOutID: mainnet.assetIds.rwa.ULTRA,
      stablecoinDelegateID: mainnet.assetIds.stablecoin.USDC,
      assetInAmt: 1_000_000n,
      assetOutAmt: 950_000n,
    },
  ],
});

await walletClient.sendTransaction({
  to: mainnet.addresses.multiliquidSwap,
  data: calldata,
});

JIT Redemptions

ml.redemptions builds and signs the canonical SwapBatchApproval envelope, submits it to POST /v1/redemptions, and reads its public lifecycle from GET /v1/redemptions/{requestId} or the filtered GET /v1/redemptions collection. The current backend flow is limited to the canonical Ethereum mainnet deployment and accepts exactly one TSY-to-USDC leg: route 6 (STABLE_TO_STABLE_EXACT_IN) or route 7 (STABLE_TO_STABLE_EXACT_OUT).

import { REDEMPTION_OPERATOR_ADDRESS } from "@uniformlabs/multiliquid-evm-sdk";

const redemptionInput = {
  routeId: ROUTE_IDS.STABLE_TO_STABLE_EXACT_OUT,
  assetInID: mainnet.assetIds.stablecoin.TSY_YIELD,
  assetOutID: mainnet.assetIds.stablecoin.USDC,
  stablecoinDelegateID: mainnet.assetIds.stablecoin.TSY_YIELD,
  assetInAmt: 2_000_000_000_000_000_000n, // maximum TSY input
  assetOutAmt: 1_000_000n, // exact 1 USDC output
};

const request = await ml.redemptions.signRedemption({
  operator: REDEMPTION_OPERATOR_ADDRESS,
  receiver: account.address,
  input: redemptionInput,
  // Optional: defaults to one hour from the current time.
  // deadline: BigInt(Math.floor(Date.now() / 1000) + 2 * 60 * 60),
});

const accepted = await ml.redemptions.submitRedemption(request);
const current = await ml.redemptions.getRedemption(accepted.requestId);
console.log(current.status, current.executionTxHash);

const history = await ml.redemptions.listRedemptions({
  user: account.address,
  limit: 25,
});
console.log(history.redemptions, history.nextCursor);

signRedemption reads the user's current Multiliquid nonce unless nonce is supplied and returns the complete request, including that nonce and the signature. When deadline is omitted, it defaults to one hour from the current time; an explicit deadline is preserved. It rejects deployments other than the canonical Ethereum mainnet contract because that is the domain verified by the current backend. For route 6, assetInAmt is the exact TSY input and assetOutAmt is the minimum USDC output. For route 7, assetInAmt is the maximum TSY input and assetOutAmt is the exact USDC output. The operator must equal the backend's configured redemption operator, the deadline must be more than ten minutes in the future at intake, and the TSY ERC-20 allowance to TreasuryDelegate is independent of the EIP-712 signature.

For EIP-1271, EIP-7702, custody, or other external signing flows, use buildRedemptionRequest with the resulting signature, then call submitRedemption. Request and response types use bigint; the SDK serializes all backend uint256 fields as decimal strings and parses response strings back to bigint. The status response follows the backend's direct Grove rebalance and settlement flow and exposes quote, reserve, Grove funding, execution, and actual-settlement fields as they become available.

Funding progresses through REBALANCE_APPROVAL_PENDING, REBALANCE_APPROVAL_SUBMITTED, REBALANCE_PENDING, and REBALANCE_SUBMITTED when needed, followed by SETTLEMENT_PENDING and SETTLEMENT_SUBMITTED. The SDK also recognizes FUNDED_BLOCKED. The retired DELEGATION_PENDING, AWAITING_CUSTODY_APPROVAL, and EXECUTION_SUBMITTED statuses are no longer accepted in responses or list filters.

Backend validation requires valid EIP-55 checksums for mixed-case addresses. External signatures must contain exactly one 0x prefix and a nonempty, even number of hex digits, with no surrounding whitespace and at most 1,024 decoded bytes. The SDK passes signatures through unchanged; the backend verifies them and returns HTTP 422 for invalid request values.

For pagination, reuse nextCursor verbatim with the same filters. Cursors issued before the backend cursor-format update in PR 120 are rejected with HTTP 400. If the error says cursor is invalid, discard the saved cursor and call listRedemptions with the same filters and no cursor to restart from the first page. The SDK surfaces this error so callers can explicitly reset their accumulated results.

Grove Basin JTRSY Liquidity

ml.grove.getJtrsyLiquidity() reads Grove's Basin, USDS/USDC pocket, and underlying PSM state directly from Ethereum. Every balance, allowance, configuration, and JTRSY preview is pinned to the returned blockNumber.

import { formatUnits } from "viem";

const liquidity = await ml.grove.getJtrsyLiquidity();

console.log("Available Grove USDC:", formatUnits(liquidity.availableUsdc, 6));
console.log("Maximum quoted USDC per swap:", formatUnits(liquidity.maxSwapUsdc, 6));
console.log("JTRSY required for that swap:", liquidity.jtrsyAmount);
console.log("Snapshot block:", liquidity.blockNumber);

availableUsdc is the full native capacity calculated by backend redemption intake: direct Basin USDC plus pocket USDC and PSM-bounded USDS conversion when the wrapper is live and has zero tout. This balance-derived capacity can exceed the Basin's per-swap USD value limit, including when rounding up the required JTRSY crosses that limit.

maxSwapUsdc is the largest output at or below availableUsdc that passes the Basin's previewSwapExactOut at the same block. jtrsyAmount is the required JTRSY input for maxSwapUsdc, which can be smaller than availableUsdc. Use this quote pair when sizing a single swap. Both quote amounts are zero when no positive output fits the swap limit or there is no inventory. All amounts are raw token units.

The SDK first previews the full inventory. On SwapSizeExceeded() it tries one raw USDC unit less (0.000001 USDC), then binary-searches if necessary to find the exact boundary. The usual case uses one preview; the one-unit rounding case uses two. A larger gap requires additional sequential previews, logarithmic in the raw USDC inventory. Every preview uses the same block. RPC failures and unrelated contract reverts still reject the call.

Use ml.grove.previewJtrsyIn(usdcAmountOut) for a specific raw USDC output; this method still rejects failed previews. A successful preview checks pricing and swap limits, but does not guarantee execution with a particular caller's balances, allowances, and transfer permissions. Grove reads are restricted to the mainnet deployment because the addresses and liquidity path are mainnet-specific.

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 rwaPrice = await ml.assets.getRWAPrice(mainnet.assetIds.rwa.ULTRA);
const stablecoinValue = await ml.assets.getStablecoinUSDValue(mainnet.assetIds.stablecoin.USDC);

// 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

// Bulk reads can also be filtered to specific IDs
const selectedRWAs = await ml.assets.getAllRWAInfo([mainnet.assetIds.rwa.ULTRA]);
const selectedStables = await ml.assets.getAllStablecoinInfo([mainnet.assetIds.stablecoin.USDC]);
const selectedPrices = await ml.assets.getAllPrices({
  rwaIDs: [mainnet.assetIds.rwa.ULTRA],
  stablecoinIDs: [mainnet.assetIds.stablecoin.USDC],
});

// Fee configuration
const protocolFeeConfig = await ml.assets.getProtocolFeeConfig(stablecoinID);
const issuerPaidProtocolFeeRate = await ml.assets.getIssuerPaidProtocolFeeRate(stablecoinID);
const spreadProtocolTakeRate = await ml.assets.getSpreadProtocolTakeRate();
const protocolFeeExempt = await ml.assets.isProtocolFeeExempt(stablecoinID);
const discount = await ml.assets.getDiscountRate(stablecoinID, rwaID);
const redemptionFee = await ml.assets.getRedemptionFee(stablecoinID, rwaID);
const acceptanceFee = await ml.assets.getStablecoinAcceptanceFee(
  stablecoinOutID,
  stablecoinInAddress,
);
const stablecoinRedemptionFee = await ml.assets.getStablecoinRedemptionFee(
  stablecoinOutID,
  stablecoinInAddress,
);

// 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 delegateAddress = await ml.delegates.getDelegateAddress(stablecoinID);
const rwaCustodyAddress = await ml.delegates.getRWACustodyAddress(stablecoinID);
const stablecoinCustodyAddress = await ml.delegates.getStablecoinCustodyAddress(stablecoinID);
const rwaColdStorageAddresses = await ml.delegates.getRWAColdStorageAddresses(stablecoinID);
const stablecoinColdStorageAddresses =
  await ml.delegates.getStablecoinColdStorageAddresses(stablecoinID);

const isRwaOk = await ml.delegates.isRWAWhitelisted(stablecoinID, rwaTokenAddress);
const isStableOk = await ml.delegates.isStablecoinWhitelisted(
  stablecoinID,
  backingStablecoinAddress,
);

Issuer Admin

Manage stablecoin delegate issuer-admin configuration. The SDK resolves the stablecoin delegate from stablecoinID and sends writes directly to that delegate contract. The contracts still enforce onlyIssuerAdmin; the SDK does not bypass on-chain permissions.

Reads only require a publicClient:

const isAdmin = await ml.issuerAdmin.isIssuerAdmin(
  mainnet.assetIds.stablecoin.USDC,
  account.address,
);

Writes require a walletClient whose account is an issuer admin for the target delegate. Fee and rate values are raw contract uint256 values, so pass them in the protocol's configured rate scale.

const stablecoinID = mainnet.assetIds.stablecoin.USDC;
const rwaID = mainnet.assetIds.rwa.ULTRA;

// Issuer admin role management
await ml.issuerAdmin.addIssuerAdmin({
  stablecoinID,
  issuerAdmin: "0x...",
});
await ml.issuerAdmin.removeIssuerAdmin({
  stablecoinID,
  issuerAdmin: "0x...",
});

// RWA backing controls
await ml.issuerAdmin.whitelistRWA({
  stablecoinID,
  rwa: "0x...", // RWA token address
  accepted: true,
});
await ml.issuerAdmin.setRWADiscountRate({
  stablecoinID,
  rwaID,
  rate: 50_000_000_000_000_000n,
});
await ml.issuerAdmin.setRWARedemptionFee({
  stablecoinID,
  rwaID,
  rate: 10_000_000_000_000_000n,
});

// Stablecoin backing controls
await ml.issuerAdmin.whitelistStablecoin({
  stablecoinID,
  stablecoin: "0x...", // backing stablecoin token address
  accepted: true,
});
await ml.issuerAdmin.setStablecoinAcceptanceFee({
  stablecoinID,
  stablecoin: "0x...",
  rate: 5_000_000_000_000_000n,
});
await ml.issuerAdmin.setStablecoinRedemptionFee({
  stablecoinID,
  stablecoin: "0x...",
  rate: 5_000_000_000_000_000n,
});
await ml.issuerAdmin.setAutoLiquidityStablecoinID({
  stablecoinID,
  autoLiquidityStablecoinID: mainnet.assetIds.stablecoin.TSY_YIELD,
});

// Custody, cold storage, and delegate-level blacklist controls
await ml.issuerAdmin.setRWACustodyAddress({ stablecoinID, custodyAddress: "0x..." });
await ml.issuerAdmin.setStablecoinCustodyAddress({ stablecoinID, custodyAddress: "0x..." });
await ml.issuerAdmin.setBlacklist({ stablecoinID, account: "0x...", blacklisted: true });
await ml.issuerAdmin.addRWAColdStorageAddress({ stablecoinID, coldStorageAddress: "0x..." });
await ml.issuerAdmin.removeRWAColdStorageAddress({ stablecoinID, coldStorageAddress: "0x..." });
await ml.issuerAdmin.addStablecoinColdStorageAddress({
  stablecoinID,
  coldStorageAddress: "0x...",
});
await ml.issuerAdmin.removeStablecoinColdStorageAddress({
  stablecoinID,
  coldStorageAddress: "0x...",
});

// Delegate pause controls
await ml.issuerAdmin.pause({ stablecoinID });
await ml.issuerAdmin.unpause({ stablecoinID });

Build Issuer Admin Calldata

For multisigs, smart accounts, or custom transaction submission flows, build calldata without sending:

const data = ml.issuerAdmin.buildIssuerAdminCalldata("whitelistRWA", {
  rwa: "0x...",
  accepted: true,
});

const tx = await ml.issuerAdmin.buildIssuerAdminTransaction(stablecoinID, "whitelistRWA", {
  rwa: "0x...",
  accepted: true,
});
// tx.to is the resolved stablecoin delegate address
// tx.data is the encoded delegate calldata

Issuer admin contract reverts such as NotIssuerAdmin, CannotRemoveSelf, InvalidIssuerAdmin, and AddressNotAllowed are decoded as MultiliquidContractError with errorName set to the Solidity error name.

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

// User yield accounting
const credits = await ml.yield.getCredits(tsyYield, userAddress);
const totalWithheldCredits = await ml.yield.getTotalWithheldCredits(tsyYield, userAddress);
const yieldMultiplier = await ml.yield.getYieldMultiplier(tsyYield, userAddress);
const lastAccrualDay = await ml.yield.getLastAccrualDay(tsyYield, userAddress);
const withheldCredits = await ml.yield.getWithheldCredits(tsyYield, userAddress);

// 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,
});
const amountForTargetValue = await ml.yield.getAmountForTargetValue({
  stablecoinID: tsyYield,
  user: "0x...",
  targetDollars: 1_000_000_000_000_000_000n, // 1 WAD
  stablecoinWithdrawal: true,
});

// Accrue all available interest and merge all eligible withheld-credit records
// (requires walletClient)
await ml.yield.accrueInterest(tsyYield, userAddress);

// Or bound both kinds of work for predictable transaction gas
await ml.yield.accrueInterest(tsyYield, userAddress, 365n, 100n);
await ml.yield.batchAccrueInterest(tsyYield, [user1, user2, user3]);

Events

Parse and subscribe to Multiliquid swap events. The production contract emits a single Swap event that includes the route flag, input/output IDs, operator, receiver, and fee breakdown.

// 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
  stablecoinID: mainnet.assetIds.stablecoin.USDC, // optional: filter by stablecoin
  eventTypes: ["Swap"], // optional
});

for (const event of events) {
  console.log(event.type, event.routeId, event.amountIn, event.amountOut);
}

// 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: "0xC105907F77693995583e1aE94C52d19Bad4111F6",
    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,
  StablecoinDelegateNotSetError,
} from "@uniformlabs/multiliquid-evm-sdk";

try {
  await ml.swap.swap({ inputs });
} 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("Minimum output was not met");
  } else if (error instanceof StablecoinDelegateNotSetError) {
    console.log("Stablecoin delegate is not configured:", error.stablecoinID);
  } 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. For remote deployment metadata failures, EvmDeploymentMetadataError.cause contains the underlying fetch or JSON parse error when one is available. Redemption HTTP and response-validation failures throw RedemptionApiError; its status, url, and cause fields preserve available failure context. Its optional retryAfter field preserves the raw Retry-After response header, including when an error body cannot be parsed as JSON. Custom apiFetch transports can expose it through a callable headers.get(name) method returning a string. Other header representations, including plain objects, remain supported and leave retryAfter undefined.

Redemption errors preserve the backend's HTTP status: 400 for malformed requests or invalid query/path/cursor values, 422 for valid JSON that fails validation, 413 for oversized bodies, and 500 for internal database or RPC failures. HTTP 429 responses include Retry-After; the backend sends a delay in seconds. Use it to schedule an explicit retry. The SDK does not retry requests automatically.

Error Classes

| Class | Solidity Error | Structured Args | | ------------------------------------ | --------------------------------- | ----------------------------------------------------- | | MultiliquidError | (SDK-level base) | — | | MultiliquidContractError | Decoded or unknown contract error | { errorName, 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 } | | USDValueNotSetError | USDValueNotSet | { tokenID } | | StablecoinPriceAdapterNotSetError | StablecoinPriceAdapterNotSet | { stablecoinID } | | StablecoinGuardrailBandNotSetError | StablecoinGuardrailBandNotSet | { stablecoinID } | | UnsupportedDecimalsError | UnsupportedDecimals | { decimals } | | AddressBlacklistedError | AddressBlacklisted | { account } | | StablecoinNotWhitelistedError | StablecoinNotWhitelisted | { stablecoin } | | InvalidMultiliquidSwapError | InvalidMultiliquidSwap | — | | InvalidStablecoinExchangeError | InvalidStablecoinExchange | { assetIn, assetOut, issuerStablecoin } | | RWANotWhitelistedError | RWANotWhitelisted | — | | InsufficientCreditsError | InsufficientCredits | { requested, available } | | InvalidYieldMultiplierError | InvalidYieldMultiplier | { multiplier } | | InvalidTargetValuesError | InvalidTargetValues | { feeTarget, feePlusRedemptionTarget, totalTarget } | | InsufficientSwapAllowanceError | InsufficientSwapAllowance | — | | IdenticalAssetsError | IdenticalAssets | — | | InvalidLegCountError | InvalidLegCount | — | | InvalidSignatureError | InvalidSignature | — | | PermitExpiredError | PermitExpired | — | | UnsupportedRouteIdError | UnsupportedRouteId | { routeId } | | ContractPausedError | EnforcedPause | — | | NotYieldBearingError | (SDK-level) | — | | NoWalletClientError | (SDK-level) | — | | EvmDeploymentMetadataError | (SDK-level) | — | | RedemptionApiError | (SDK-level) | { status, url, cause, retryAfter } | | StablecoinDelegateNotSetError | (SDK-level) | stablecoinID |

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 stablecoin delegates, RWA metadata delegates, adapters, and tokens
    delegates: { "0xstablecoinId...": "0xdelegateAddr..." },
    rwaDelegates: { "0xrwaId...": "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 {
  CANONICAL_MULTICALL3_ADDRESS,
  DEFAULT_EVM_INFO_API_BASE_URL,
  GROVE_MAINNET_ADDRESSES,
  REDEMPTION_OPERATOR_ADDRESS,
  WAD,
  ROLES,
  ROUTE_IDS,
} from "@uniformlabs/multiliquid-evm-sdk";

WAD; // 1_000_000_000_000_000_000n (1e18)
DEFAULT_EVM_INFO_API_BASE_URL; // https://api.multiliquid.xyz
CANONICAL_MULTICALL3_ADDRESS; // 0xcA11bde05977b3631167028862bE2a173976CA11
GROVE_MAINNET_ADDRESSES.basin; // 0xf08943f817e1F902dEbC884c7B19Ea5764594Ac9
REDEMPTION_OPERATOR_ADDRESS; // 0x4820B6710c3C267DbC0FB01A6Eb940D140b7900A
ROLES.DEFAULT_ADMIN; // bytes32(0)
ROLES.OPERATOR; // keccak256("OPERATOR_ROLE")
ROLES.BLACKLISTER; // keccak256("BLACKLISTER_ROLE")
ROUTE_IDS.STABLE_TO_RWA_EXACT_IN; // 0n
ROUTE_IDS.PREFUNDED_STABLE_TO_STABLE_EXACT_OUT; // 11n

ABIs

All contract ABIs are exported as const assertions for direct use with viem:

import {
  multiliquidSwapAbi,
  groveBasinAbi,
  stablecoinDelegateBaseAbi,
  treasuryDelegateAbi,
  balanceSheetDelegateAbi,
  priceAdapterAbi,
  yieldBearingDelegateAbi,
  whitelistAdapterAbi,
  erc20Abi,
  usdsPsmWrapperAbi,
} from "@uniformlabs/multiliquid-evm-sdk";

Supported Assets

Mainnet

RWAs: ULTRA, JTRSY, WTGXX, BENJI, USTB, VBILL, JAAA

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