@talerfinance/vaults-sdk
v0.3.2
Published
Embed Taler vaults (deposit stablecoins for yield, withdraw to USD) into any wallet or app. Headless core, drop-in web component, and React hooks.
Maintainers
Readme
Taler Vaults SDK
Add a stablecoin yield vault to your wallet or app. Your users deposit USDC/USDT, earn onchain yield, and cash out to USD on any chain. You drop in a component or a few function calls, no DeFi plumbing. Powered by NEAR Intents 1Click.
Quick start
The fastest path is the widget. Two lines of HTML render the whole deposit/withdraw UI:
<script type="module" src="https://cdn.jsdelivr.net/npm/@talerfinance/vaults-sdk/widget.js"></script>
<taler-vault></taler-vault>For React or your own UI, install it:
npm install @talerfinance/vaults-sdkThat's the whole setup, no keys or config. The SDK handles wallet connection, network switching, quoting, transfers, and status polling; users connect their own wallet in the browser. Everything is optional to customize (a different vault, a referralCode, a staging apiBase); see Config.
Want to try it before wiring up a wallet UI? quoteDeposit and quoteWithdraw return a live quote from just an address string on taler.account; no signature, no funds moving. See Core.
Which one do I use?
The SDK ships in three forms. They all talk to the same engine underneath, so you pick one based on how much of the UI you want to build yourself. More work to the right, more control to the right.
| Form | What you get | What you build | Use it when |
|---|---|---|---|
| Widget | The entire deposit/withdraw UI as a single HTML tag <taler-vault>. Wallet connect, APY chart, deposit, withdraw, live status: all done. | Nothing. Drop in the tag, maybe recolor it with CSS. | You want it working today and don't need a custom design. |
| React hooks | Data + actions as React hooks (useDeposit, useVaultBalance, ...), each with isLoading / error. No visuals. | Your own components and styling; the hooks give you the data and the buttons' behavior. | You have a React app and want it to look like your app, not ours. |
| Core | A plain typed JavaScript object with methods (deposit(), withdraw(), getBalance()). No React, no UI. | Everything visual, in whatever framework (or none). | You're not on React, or you want total control. |
Think of it as: Widget = the finished appliance, React = the parts kit with wiring, Core = the raw engine.
Whichever you pick, fees, the vault allowlist, and the 1Click key are enforced by your Taler API on the server. The SDK is just a client and cannot change them.
Widget: one tag, full UI
<script type="module" src="https://cdn.jsdelivr.net/npm/@talerfinance/vaults-sdk/widget.js"></script>
<taler-vault></taler-vault>
<!-- Optional: enables the manual-deposit QR -->
<script src="https://cdn.jsdelivr.net/npm/qrcode-generator/qrcode.js"></script>Renders the whole card: APY/TVL chart, deposit (wallet or manual-from-any-chain), withdraw, live status. jsdelivr.net serves the package straight from npm; no CDN to run. (unpkg.com/@talerfinance/vaults-sdk/widget.js works the same way, or self-host the file.)
Attributes (all optional): vault (default tlo), api-base (default https://api.taler.finance), theme (dark | light).
Brand it with CSS custom properties on the host, no forking needed:
taler-vault {
--taler-accent: #7c3aed;
--taler-bg: #0e0e12;
--taler-radius: 10px;
}Events (bubble, composed):
el.addEventListener("taler:complete", (e) => console.log(e.detail.received, "TLO"));
// also: taler:connected, taler:quote, taler:submitted, taler:status, taler:errorReact hooks: your UI, our logic
import { useTalerClient, useWallet, useVaultPerformance, useDeposit } from "@talerfinance/vaults-sdk/react";
function Vault() {
const taler = useTalerClient({ apiBase: "https://api.taler.finance", vault: "tlo" });
const { wallets, account, connect } = useWallet(taler);
const { data: perf } = useVaultPerformance(taler);
const { deposit, status, isPending, error } = useDeposit(taler);
if (!account) return wallets.map((w) => <button key={w.info.uuid} onClick={() => connect(w)}>{w.info.name}</button>);
return (
<>
<p>APY {perf?.current_apy_7d.toFixed(2)}%</p>
<button disabled={isPending} onClick={() => deposit({ from: "USDC@base", amount: "15" })}>
{isPending ? "..." : "Deposit 15 USDC"}
</button>
{status && <span>{status}</span>}
{error && <span>{error.code}</span>}
</>
);
}Hooks: useTalerClient, useWallet, useVaultPerformance, useVaultBalance, useDeposit, useWithdraw (+ useTalerVault aggregate). Each async hook returns { data | ..., isLoading | isPending, error }. React is a peer dependency. In Next.js the widget is client-only (customElements); import it in a "use client" component.
useTalerClient keeps the same TalerVaults instance across re-renders as long as apiBase/vault don't change, even if you pass a new headers/fetch object literal every render; those are applied to the existing instance rather than recreating it, so a connected wallet is never silently dropped by a re-render.
Bring your own connected wallet
If your app already manages wallet connection (wagmi, RainbowKit, viem), skip useWallet's own picker and bridge your existing provider into taler.connect() instead. It accepts any EIP-1193 provider:
import { useAccount } from "wagmi";
import { useEffect } from "react";
function useBridgeWagmiWallet(taler) {
const { connector, address, isConnected } = useAccount();
useEffect(() => {
if (!isConnected || !connector) return;
connector.getProvider().then((provider) => {
taler.connect({ provider, info: { name: connector.name, uuid: connector.id } });
});
}, [taler, connector, address, isConnected]);
}Once bridged, taler.account and the rest of the hooks (useDeposit, useWithdraw, ...) work exactly as if the user had connected through the SDK's own picker.
Core: headless, do it all yourself
import { TalerVaults, TalerError, ErrorCode } from "@talerfinance/vaults-sdk";
const taler = new TalerVaults({ apiBase: "https://api.taler.finance", vault: "tlo" });
await taler.connect(); // discovers wallet, tracks account/chain changes
// data
await taler.getPerformance(); // { current_apy_7d, current_tvl, chart }
await taler.getBalance(); // { balance, worthUsd }
await taler.sources(); // every funding asset, with canAutoSend (EVM wallet can sign it)
await taler.getSourceBalance({ from: "USDC@base" }); // { balance, raw, decimals } via RPC; EVM sources only, null otherwise
// deposit: `from` is a source id or "SYMBOL@chain"
const preview = await taler.quoteDeposit({ from: "USDC@base", amount: "15" });
const op = await taler.deposit({ from: "USDC@base", amount: "15" });
op.on("status", (s) => console.log(s.status));
op.intentHashes; // fills in mid-flight; see "Tracking the swap leg" below
const result = await op.done; // resolves on success; throws TalerError on failure
// manual deposit from a non-EVM chain (Tron, BTC, XRP, Solana...)
const m = await taler.deposit({ from: "USDT@tron", amount: "25", refundAddress: "T..." });
m.depositAddress; m.depositMemo; // show address + QR; user sends from any wallet
await m.done;
// withdraw, always USD out
await taler.withdraw({ to: "USDC@base", amount: "10" });Typed errors
Everything throws TalerError with a .code you can switch on:
try { await taler.deposit({ from: "USDC@base", amount: "1" }); }
catch (e) {
if (e.code === ErrorCode.NO_LIQUIDITY) show("Try a different amount");
else if (e.code === ErrorCode.USER_REJECTED) show("Cancelled");
else if (e.code === ErrorCode.INSUFFICIENT_BALANCE) show("Not enough balance");
else show(e.message);
}Connection
| Code | Fires when | Show the user |
|---|---|---|
| NO_WALLET | No EVM wallet was found (or a named one wasn't) | "Install a wallet" |
| NOT_CONNECTED | A quote/deposit/withdraw call ran before connect() succeeded | "Connect a wallet first" |
| USER_REJECTED | The connect or transaction prompt was declined in the wallet | "Cancelled" |
Inputs
| Code | Fires when | Show the user |
|---|---|---|
| UNKNOWN_SOURCE | from/to doesn't match any funding source | Check the source id or "SYMBOL@chain" string |
| NO_REFUND_ADDRESS | A manual (non-EVM) deposit is missing refundAddress | "Enter a refund address on that chain" |
| NO_PAYOUT_ADDRESS | A withdrawal to a non-EVM chain is missing payoutAddress | "Enter a payout address on that chain" |
| NO_LIQUIDITY | No quote available for that amount right now (too small, or a transient gap) | "Try a different amount" |
On-chain
| Code | Fires when | Show the user |
|---|---|---|
| INSUFFICIENT_BALANCE | The wallet doesn't have enough of the token being sent | "Not enough balance for this amount" |
| INSUFFICIENT_GAS | The wallet doesn't have enough native token to cover network fees | "Not enough [ETH/...] for network fees" |
| REFUNDED | The deposit settled back to the sender instead of completing | Show the refund; no action needed |
| DEPOSIT_FAILED | The deposit/withdraw ended in a terminal failure | "Something went wrong, try again" |
Everything else
| Code | Fires when |
|---|---|
| API_ERROR | The Taler API rejected the request for a reason other than liquidity |
| NETWORK | A request to the API or wallet didn't complete (offline, timeout, or an unmapped wallet error) |
| WRONG_NETWORK | Internal: asked to switch to a chain the SDK has no config for; shouldn't happen from user action |
| BAD_CONFIG | An unknown vault was passed to new TalerVaults(); a coding mistake, not user-facing |
INSUFFICIENT_BALANCE/INSUFFICIENT_GAS are matched from the wallet's own error message (there's no structured revert code to read), so they cover the common cases, not every possible on-chain failure. The raw message and .cause are always preserved on the error for your own matching if you hit something not covered.
Status
A deposit/withdraw moves through statuses as it settles: PENDING_DEPOSIT → KNOWN_DEPOSIT_TX → PROCESSING → SUCCESS (or REFUNDED/FAILED; INCOMPLETE_DEPOSIT if a partial send is detected). Read them off op.on("status", ...), or status from useDeposit/useWithdraw.
To build a step UI without hardcoding the seven strings yourself, StatusCode and STATUS_LABELS are exported from the same place as ErrorCode:
import { STATUS_LABELS } from "@talerfinance/vaults-sdk";
const { label, tone } = STATUS_LABELS[op.status]; // tone: "wait" | "ok" | "bad"This is the same map the widget itself uses internally, so a custom UI stays consistent with it.
Tracking the swap leg
op.txHash + op.explorer link to the deposit you sent (Etherscan/Basescan/...). That's only the origin-chain send; the actual swap/bridge happens as a separate NEAR intents settlement. Once it's known (usually once status reaches PROCESSING), it shows up as op.intentHashes (an array; can grow to more than one hash for a multi-step route), and intentExplorerUrl() links to it:
import { intentExplorerUrl } from "@talerfinance/vaults-sdk";
op.on("status", () => {
if (op.intentHashes.length) console.log(intentExplorerUrl(op.intentHashes[0]));
});The widget shows this itself, as a second link next to the deposit tx once it's available.
Any chain, in and out
Deposit with any USDC/USDT, on any chain 1Click supports. Withdraw the same way: pick any chain and the vault pays out USDC/USDT there.
| Chain type | Deposit | Withdraw |
|---|---|---|
| EVM (Base, Arbitrum, Ethereum, Optimism, Polygon) | SDK signs the transfer with the connected wallet | SDK signs the transfer; funds land in the same wallet |
| Everything else (Tron, Solana, NEAR, TON, Sui...) | SDK returns a deposit address and QR code; send from any wallet on that chain | Pass a payoutAddress on that chain (your EVM address will not work there); the SDK still signs the outgoing TLO transfer with your wallet |
TLO itself lives on Ethereum, so every deposit or withdraw involves an Ethereum-side step and mainnet gas. Non-EVM legs (the deposit-in or payout-out) route through 1Click and typically settle in under a minute.
The widget defaults to whatever chain your connected wallet is on and offers an "another chain" expander for anything else, so most users never have to think about this.
Config
All options are optional:
new TalerVaults({
apiBase: "https://api.taler.finance", // default; override for staging/local
vault: "tlo", // default
referralCode: "acme", // optional: revenue-share attribution
fetch: customFetch, // optional (SSR / auth)
headers: { Authorization: "Bearer ..." }, // optional
});fetch/headers can also be updated after construction, e.g. to rotate an auth token on a long-lived instance, without recreating the client or losing wallet state: taler.setHeaders({ Authorization: "Bearer ..." }), taler.setFetch(customFetch).
Backed by a Taler API exposing /oneclick/* and /taler/vaults/:vault/performance (see packages/oneclick in the indexer-api).
