@concrete-xyz/sdk
v2.1.0
Published
Concrete SDK for interacting with vault contracts
Readme
@concrete-xyz/sdk
SDK for interacting with vault contracts on multiple EVM networks.
Read the 1.x migration guide before upgrading to 2.x.
⚠️ Beta Warning: This package is currently in closed beta. If you encounter any issues, please contact us for support.
Installation
The SDK requires Node 20.10 or later.
Core SDK
npm install @concrete-xyz/sdk viem@^2viem ^2 is a peer dependency: the SDK takes viem PublicClient and WalletClient instances and does not bundle viem.
React Integration
npm install @concrete-xyz/sdk viem@^2
# Then import from the react subpath
import { useVault } from "@concrete-xyz/sdk/react";Wagmi Integration
npm install @concrete-xyz/sdk viem@^2 wagmi@^2 @tanstack/react-query@^5
# Then import from the wagmi subpath
import { useVault, useVaultQuery } from "@concrete-xyz/sdk/wagmi";viem and wagmi are both ^2. wagmi ^3 is not supported.
react (^18 || ^19), wagmi and @tanstack/react-query are optional peer dependencies, needed only
for the subpath that uses them. Keep a single copy of each at the top level of your app — two copies of
React break hooks, and two copies of wagmi split the WagmiProvider context.
Error reporting
The SDK reports nothing by default. Register a sink to receive instrumented errors:
import { setErrorReporter } from "@concrete-xyz/sdk";
setErrorReporter((error, report) => Sentry.captureException(error, report));Quick Start
1. Core SDK (Vanilla JavaScript/TypeScript)
For direct usage without React or Wagmi:
import { getVault } from "@concrete-xyz/sdk";
import { createPublicClient, http } from "viem";
import { mainnet } from "viem/chains";
// Create public client
const publicClient = createPublicClient({ chain: mainnet, transport: http("YOUR_RPC_URL") });
// Create vault instance
const vault = getVault(
"v1", // vault version ("v1" or "v2")
"0x15cE9bE6609db102b70D68ca75a39c555bEa5Fac", // vault address
1, // chainId
publicClient, // viem PublicClient for read operations
);
// Get vault details
const vaultDetails = await vault.getVaultDetails();
console.log("Vault Symbol:", vaultDetails.symbolDetails);React Integration
For React applications, the SDK provides a custom hook useVault that integrates seamlessly with your existing viem clients.
Key Features:
- Takes explicit viem client instances: a
PublicClientfor reads and aWalletClientfor writes - Full control over network configuration
- Integrates with any React state management solution
React: useVault(version, address, chainId, publicClient, walletClient)
The React hook for interacting with vault contracts. Takes the same arguments as getVault and memoizes the instance.
Parameters:
version("v1" | "v2"): The vault contract version to useaddress(string): The vault contract addresschainId(number): The blockchain network chain IDpublicClient(PublicClient, optional): viem public client for read operationswalletClient(WalletClient, optional): viem wallet client with anaccountfor write operations
Returns: Vault instance with all available methods
Usage Example:
The wallet client is created after mount and stored in state. Without an injected provider, the vault remains available for read operations. Replace 0xYOUR_ADDRESS with a connected account.
"use client";
import { useEffect, useState } from "react";
import { useVault } from "@concrete-xyz/sdk/react";
import { createPublicClient, createWalletClient, custom, http, type WalletClient } from "viem";
import { mainnet } from "viem/chains";
import "viem/window";
const publicClient = createPublicClient({ chain: mainnet, transport: http("YOUR_RPC_URL") });
export default function VaultComponent() {
const [walletClient, setWalletClient] = useState<WalletClient>();
useEffect(() => {
if (!window.ethereum) return;
setWalletClient(
createWalletClient({
account: "0xYOUR_ADDRESS",
chain: mainnet,
transport: custom(window.ethereum),
}),
);
}, []);
const vault = useVault("v1", "0x15cE9bE6609db102b70D68ca75a39c555bEa5Fac", 1, publicClient, walletClient);
return <div>Vault: {vault.getAddress()}</div>;
}Wagmi Integration
For applications using Wagmi, the SDK provides a simplified hook that automatically uses Wagmi's context.
Key Features:
- Automatic public and wallet client detection
- Seamless integration with Wagmi's ecosystem
- Minimal configuration required
- Built-in TanStack Query integration with
useVaultQuery - Automatic caching and state management
Wagmi: useVault(config)
The Wagmi hook for interacting with vault contracts. Automatically uses Wagmi's configured public and wallet clients.
Parameters:
config(object):version("v1" | "v2"): The vault contract version to useaddress(string): The vault contract addresschainId(number): The blockchain network chain IDfallbackRpcUrl(string, optional): Fallback RPC URL if wallet is not connected
Returns: Vault instance with all available methods
Usage Example:
import { useVault } from "@concrete-xyz/sdk/wagmi";
export default function VaultComponent() {
const vault = useVault({
version: "v1",
address: "0x585934AfBf1FA9f563b80283F8B916Dd8F66a9b6",
chainId: 80084, // Berachain
});
// Use vault methods
const vaultDetails = await vault.getVaultDetails();
return <div>Vault: {String(vault)}</div>;
}Wagmi: useVaultQuery(options)
The Wagmi hook for querying vault data with automatic caching and state management. Built on top of TanStack Query for optimal performance.
Parameters:
options(object):vault(object): Vault configurationversion("v1" | "v2"): The vault contract version to useaddress(string): The vault contract addresschainId(number): The blockchain network chain IDfallbackRpcUrl(string, optional): Fallback RPC URL if wallet is not connected
queryKey(array): Additional key segments for TanStack Query cachingqueryFn(function): Function that receives the vault instance and returns the data to query- ...other TanStack Query options
Returns: TanStack Query result object with data, isLoading, error, etc.
Usage Example:
import { useVaultQuery } from "@concrete-xyz/sdk/wagmi";
export default function VaultComponent() {
const vaultQuery = useVaultQuery({
vault: {
version: "v1",
address: "0x585934AfBf1FA9f563b80283F8B916Dd8F66a9b6",
chainId: 80084, // Berachain
},
queryKey: ["vaultDetails"],
queryFn: async (vault) => await vault.getVaultDetails(),
});
return (
<div>
<pre>
{JSON.stringify(vaultQuery.data, null, 2)}
</pre>
</div>
);
}Hook Comparison
Key Differences:
- React version: Accepts optional
publicClientandwalletClientinstances as separate parameters - Wagmi version: Uses a config object and automatically uses Wagmi's configured public and wallet clients
- Wagmi version: Includes additional
useVaultQueryhook for TanStack Query integration - Wagmi version: Supports
fallbackRpcUrlfor scenarios when the wallet is not connected - Both versions: Require vault version ("v1" or "v2") and return the same vault instance with identical methods
Vanilla Examples
1. Get Vault Data
import { getVault } from "@concrete-xyz/sdk";
import { createPublicClient, http } from "viem";
import { mainnet } from "viem/chains";
const publicClient = createPublicClient({ chain: mainnet, transport: http("YOUR_RPC_URL") });
const vault = getVault("v1", "0x15cE9bE6609db102b70D68ca75a39c555bEa5Fac", 1, publicClient); // chainId 1 = Ethereum
// Get complete vault information
const vaultDetails = await vault.getVaultDetails();
console.log("Vault Symbol:", vaultDetails.vaultAsset.symbol);
console.log("Underlying Asset:", vaultDetails.underlying.symbol);2. Preview Deposit
const depositAmount = 10n ** BigInt(await vault.getUnderlyingDecimals());
const vaultTokensReceiving = await vault.getPreviewDeposit(depositAmount);
console.log(`Input: ${await vault.toUnderlyingDecimals(depositAmount)} ${vaultDetails.underlying.symbol}`);
console.log(`Output: ${vaultTokensReceiving} ${vaultDetails.vaultAsset.symbol}`);getPreviewDeposit resolves a number on v1 and a bigint on v2. Read the value through
String() or narrow on the version if you support both.
3. Execute Deposit
import { createWalletClient } from "viem";
import { privateKeyToAccount } from "viem/accounts";
// You need a wallet client with an account for write operations
const walletClient = createWalletClient({
account: privateKeyToAccount("0xYOUR_PRIVATE_KEY"),
chain: mainnet,
transport: http("YOUR_RPC_URL"),
});
const vaultWithWallet = getVault("v1", "0x15cE9bE6609db102b70D68ca75a39c555bEa5Fac", 1, publicClient, walletClient); // chainId 1 = Ethereum
// Approve tokens first
const underlying = await vaultWithWallet.getUnderlyingErc20();
const approveTx = await underlying.approve(vaultWithWallet.getAddress(), depositAmount);
await approveTx.wait();
// Execute deposit
const depositTx = await vaultWithWallet.deposit(depositAmount);
const receipt = await depositTx.wait(); // viem TransactionReceipt; throws if the transaction reverted
console.log("Deposit successful:", receipt.transactionHash);4. Execute Withdrawal
const withdrawAmount = 10n ** BigInt(await vault.decimals());
const underlyingReceiving = await vault.getPreviewRedeem(withdrawAmount);
console.log(`Input: ${await vault.applyDecimals(withdrawAmount)} ${vaultDetails.vaultAsset.symbol}`);
console.log(`Output: ${underlyingReceiving} ${vaultDetails.underlying.symbol}`);
// Execute withdrawal (using the same wallet client from the previous example)
const withdrawTx = await vaultWithWallet.redeem(withdrawAmount);
const receipt = await withdrawTx.wait();
console.log("Withdrawal successful:", receipt.transactionHash);4.1 Withdrawal queue (v2)
V2 vaults use an epoch-based withdrawal queue. After requesting a withdrawal, funds are processed per epoch; when the epoch is finalized you can claim underlying, or cancel while the epoch is still active/inactive.
const vault = getVault("v2", vaultAddress, chainId, publicClient, walletClient);
const requests = await vault.getAllWithdrawQueueRequests(walletClient.account.address);
// Each request: { amount, epoch, epochState, claimable, cancelable, claim(), cancel(), ... }Claim (when request.claimable is true):
const request = requests.find((r) => r.claimable);
if (request) {
const tx = await request.claim();
await tx.wait();
}Cancel (when request.cancelable is true):
const request = requests.find((r) => r.cancelable);
if (request) {
const tx = await request.cancel();
await tx.wait();
}5. Get APY Details
Use expectedApy for live APY and apy for historical APY. Both are decimal strings: "0.085" represents 8.5%. When expectedApy is missing, display the live rate as unavailable. A returned "0" is a valid rate.
import { getVault } from "@concrete-xyz/sdk";
const vault = getVault("v2", vaultAddress, chainId, publicClient);
const apyDetails = await vault.getApyDetails();
console.log({
liveApy: apyDetails.expectedApy,
liveApy7Days: apyDetails.expectedApy7Days,
historicalApy: apyDetails.apy,
asOf: apyDetails.timestamp,
});The API returns a snapshot with a timestamp. The SDK passes through its fee treatment, so confirm that treatment before labeling a rate as net of fees. Concrete's live APY UI uses expectedApy; CMS overrides and display rounding remain outside the SDK.
The same live fields are available in getConcreteApi().apy.getAllVaultsApy(), indexed by chain ID and lowercase vault address.
6. Get Oracle Price
import { getVault } from "@concrete-xyz/sdk";
import { createPublicClient, http } from "viem";
import { arbitrum } from "viem/chains";
const publicClient = createPublicClient({ chain: arbitrum, transport: http("https://arb1.arbitrum.io/rpc") });
const vault = getVault("v1", "0xE2d8267D285a7ae1eDf48498fF044241d04e9608", 42161, publicClient); // chainId 42161 = Arbitrum
// Get underlying asset price from oracle
const underlyingPrice = await vault.getUnderlyingPrice();
console.log("Underlying Price:", underlyingPrice);7. Read the V2 deposit cooldown
A deposit cooldown locks newly deposited shares for a configured period. Resolve the vault's DepositLockWithFeeHook to read that duration in seconds:
const lock = await vault.getDepositLockWithFeeHook();
const cooldownSeconds = lock ? await lock.depositLockDuration() : undefined;An unresolved hook means this adapter cannot provide cooldown information. A nonzero duration requires the relevant deposit/mint hook flags to enforce locking. A zero duration disables locks on new deposits, but existing locks can remain active.
For async vaults, the Withdrawal Queue processes requests in separate accounting periods called Epochs. The deposit cooldown alone does not determine when a withdrawal pays out.
| Read | Method | Result |
| ----------------- | ------------------------------------- | -------------------------------------------------------------- |
| Locked shares | effectiveTotalLocked(account) | Shares still locked |
| Unlocked shares | getUnlockedShares(account) | Shares available without early unlocking |
| Stored lock count | storedLockCount(account) | Number of stored records, including expired locks |
| Individual lock | getStoredLock(account, index) | { shares, unlockTimestamp, duration }, with times in seconds |
| Early unlock fee | previewEarlyUnlock(account, shares) | Fee in vault-share base units |
Read earlyUnlockEnabled() to check whether early unlocking is allowed. getEnabledDetails().enabled describes early unlocking with a fee; use the lock duration and hook flags to assess deposit locking.
8. Read the V2 TVL cap and remaining capacity
The deposit cap limits the total assets the vault accepts across all depositors. Read the cap and remaining capacity in underlying-token units:
import { formatUnits, maxUint256 } from "viem";
const [capRaw, minimumDepositRaw] = await vault.getDepositLimits();
const remainingCapacityRaw = await vault.maxDeposit(receiverAddress);
const decimals = await vault.getUnderlyingDecimals();
console.log({
cap: capRaw === maxUint256 ? "unlimited" : formatUnits(capRaw, decimals),
minimumDeposit: formatUnits(minimumDepositRaw, decimals),
remainingCapacity: formatUnits(remainingCapacityRaw, decimals),
});Keep raw bigint values for calculations and use underlying-token decimals for display. The cap is denominated in the underlying token, rather than USD. A zero cap disables deposits. maxDeposit(receiverAddress) returns zero when deposits are paused, the cap is reached, or remaining capacity is below the minimum deposit.
getWithdrawLimits() instead returns maximum and minimum per-transaction withdrawal amounts. For deposits routed through additional hooks, including multi-asset deposits, check those hooks' restrictions separately.
9. Get a withdrawal forecast
getWithdrawalForecast reads the ordinary v2 async Withdrawal Queue forecast from the public API. It returns the schedule, queue pressure, receiver positions, forecast portions, and current claimable assets. No RPC client or wallet is required.
import { getVault, type WithdrawalForecast } from "@concrete-xyz/sdk";
const vault = getVault("v2", vaultAddress, chainId, undefined, undefined, true);
const current: WithdrawalForecast = await vault.getWithdrawalForecast({ account });
const preview = await vault.getWithdrawalForecast({ account, additionalShares: 1_000_000n });account is the withdrawal receiver. A preview adds positive additionalShares to that receiver's existing requests and assumes the caller, share owner, and receiver are all account. An ineligible preview does not remove existing requests or claims.
Amounts are bigint in share or underlying-asset base units. Use shareDecimals and assetDecimals to format them. Epoch IDs and block numbers are also bigint. Timestamps are UTC ISO 8601 strings. snapshot.maxAgeSeconds is in seconds. policy.thresholdPercent retains the exact decimal percentage as a string.
| Field | Use |
| ------------------------------- | ------------------------------------------------------------------------------------------------ |
| status, reasons, snapshot | Check availability and source freshness before displaying estimates. |
| schedule | Distinguish scheduled close/process times from observed Epoch state. |
| pressure | Read the separate cap and idle-liquidity budgets, queued shares, and capacity for a new request. |
| existing | Read outstanding requests and their forecast portions. Processed claims are separate. |
| claimable | Read assets and Epochs currently available to claim. |
| preview | Check eligibility and the forecast for existing requests plus the proposed self-request. |
Dates are conditional estimates, not guaranteed payment times. A portion with outcome direct_payment pays assets directly. A claim portion requires a later claim after processing. A scheduled date alone does not enable a claim. Check assumptions and unallocatedSharesRaw; fullAvailableAt is null when the forecast cannot allocate the full amount.
Preserve partial, unavailable, and unsupported results and their reasons. A null amount is unknown, not zero. The request aborts after 30 seconds. Network, API and timeout errors reject the call. See recipe 09 for a runnable read and optional preview.
Supported Networks
The SDK supports multiple EVM networks including: Ethereum, Arbitrum, Morph, Berachain, and Katana.
Network Types:
- Mainnet: Ethereum, Arbitrum
- Testnet: Morph, Berachain, Katana
Note: Network support may vary based on your specific deployment. Always verify network compatibility before production use.
API Reference
getVault(version, address, chainId, publicClient?, walletClient?, silent?)
Creates a new Vault instance for interacting with vault contracts.
Parameters:
version("v1" | "v2"): The vault contract version to use.address(string): The vault contract addresschainId(number): The blockchain network chain IDpublicClient(PublicClient, optional): viem public client for read operations. Without it, on-chain reads and writes are unavailable; API-backed methods such asgetApyDetails()still workwalletClient(WalletClient, optional): viem wallet client with anaccountfor write operationssilent(boolean, optional): Suppress the warning logged when nopublicClientis passed
Returns: Vault instance
Vault Methods
The vault is an abstraction of a ERC-4626 Tokenized Vault, which in turn is an ERC-20 token that represents shares of underlying assets.
Technically, all ERC-4626 vault methods are available, but the SDK provides a simplified interface focusing on the most commonly used operations. Some less frequently used methods may not be directly exposed through the SDK wrapper.
Commonly used methods (examples):
getVaultDetails(): Get complete vault informationpreviewConversion(amount): Preview deposit/withdrawal conversiondeposit(amount): Deposit underlying tokens for vault tokensredeem(amount): Redeem vault tokens for underlying tokensapprove(spender, amount): Approve token spendingtotalAssets(): Get total assets in vaultsymbol(): Get vault symbol
Write methods resolve { hash, wait }. wait(confirmations?) resolves the viem TransactionReceipt and throws when the transaction reverted.
Using ABI Directly
The SDK ships contract ABIs under two wildcard subpaths:
| Subpath | Form | Use it for |
| ------------------------------------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------ |
| @concrete-xyz/sdk/abi/<contract> | Typed viem ABI (as const satisfies Abi) | TypeScript. Literal types survive, so viem infers function names, arguments, and return types. |
| @concrete-xyz/sdk/raw-abi/<contract>.json | Standard JSON ABI array | Non-TypeScript consumers, other libraries (ethers, web3.py), codegen, and contract verification. |
Prefer the typed subpath in TypeScript. A JSON import widens the literal types to string, and viem then loses all inference.
import abi from "@concrete-xyz/sdk/abi/vault";
import rawAbi from "@concrete-xyz/sdk/raw-abi/vault.json" with { type: "json" };
// Typed: viem infers "totalAssets" and its return type from the ABI.
const total = await publicClient.readContract({ address, abi, functionName: "totalAssets" });
// Raw: a plain JSON ABI array, for tooling that does not read TypeScript types.
console.log(rawAbi);The typed abi/* subpath covers the contracts the SDK compiles:
asset-pricer, hook-abstract, hook-container, hook-deposit-lock-with-fee, hook-multi-asset-deposit-cap, hook-whitelist-user-deposit, hurdle-rate-oracle, hurdle-rate-oracle-offchain, multicall, vault, vault-v2-async, weth, withdraw-queue
The raw raw-abi/*.json subpath covers those and four more:
oapp, strategy, vault-distributor, vault-registry
The with { type: "json" } attribute needs Node 20.10 or later. In TypeScript, set "module" to "nodenext", "esnext", or "preserve" to use it. The typed subpath needs neither.
Migrating direct ABI imports
Direct ABI imports use the two wildcard subpaths described in Using ABI Directly. Earlier releases exposed the internal build paths instead; those specifiers no longer resolve.
Before:
import abi from "@concrete-xyz/sdk/dist/src/core/contracts/abi/vault.mjs";
import rawAbi from "@concrete-xyz/sdk/dist/src/core/contracts/raw-abi/vault.json" with { type: "json" };After:
import abi from "@concrete-xyz/sdk/abi/vault";
import rawAbi from "@concrete-xyz/sdk/raw-abi/vault.json" with { type: "json" };The same change applies to each contract, not only vault. Four contracts have no typed subpath. The ABI contents are unchanged.
Migrating from 1.x
Version 2.0.0 is built on viem and declares viem ^2 as a peer dependency. Install it next to the SDK.
npm install @concrete-xyz/sdk@^2 viem@^2getVault and React useVault arguments
The provider and signer arguments of 1.x are replaced by viem clients: a PublicClient for reads and a WalletClient (with an account) for writes. version, address, chainId, and silent are unchanged.
// 1.x
const vault = getVault("v1", address, 1, provider, signer);
// 2.x
const publicClient = createPublicClient({ chain: mainnet, transport: http(rpcUrl) });
const walletClient = createWalletClient({ account, chain: mainnet, transport: http(rpcUrl) });
const vault = getVault("v1", address, 1, publicClient, walletClient);The Wagmi hooks need no code change: useVault resolves both clients from your Wagmi config, and fallbackRpcUrl now creates a viem public client over http(fallbackRpcUrl).
If you update clients on an existing vault instance, replace vault.updateProviders(provider, signer) with vault.updateClients(publicClient, walletClient).
Oracle price arguments
getConcreteApi().oracle.getPrice(quote, symbol, chainId) is replaced by getConcreteApi().oracle.getPrice(tokenAddress, chainId). Use the underlying token's address instead of its symbol. vault.getUnderlyingPrice() now takes no quote argument and resolves the underlying address itself.
Live APY, cooldown, and caps
Use the APY example to select expectedApy for live rates. The cooldown example covers getDepositLockWithFeeHook(), available in 2.0.0. Existing deposit-cap methods remain available as shown in the TVL cap example.
Direct ABI imports
Replace @concrete-xyz/sdk/dist/src/core/contracts/abi/vault.json with @concrete-xyz/sdk/abi/vault. The default export is now a typed viem ABI array. Remove any JSON import assertion or attribute from this import. The raw JSON is still available, as @concrete-xyz/sdk/raw-abi/vault.json. See Using ABI Directly.
Transaction receipts
Write methods still resolve { hash, wait }, and wait(confirmations?) still throws when the transaction reverted. The value wait() resolves is now a viem TransactionReceipt; the Wagmi mutation hooks (deposit, withdraw, queue.action) expose the same receipt as data. wait() has no receipt timeout.
| 1.x | 2.x |
| -------------------------------- | ---------------------------------------------- |
| receipt.hash | receipt.transactionHash |
| receipt.blockNumber (number) | receipt.blockNumber (bigint) |
| receipt.status (1 | 0) | receipt.status ("success" | "reverted") |
