SDK Examples

This guide provides full, copy-paste ready code examples for integrating Vault into your frontend application.


Coinchange Vault React SDK

Technology & Liquidity Infrastructure for non-custodial liquidity routing and technology-powered DeFi automation. No guarantee of returns. Rewards are variable and derived from blockchain protocol incentives and CeFi/DeFi strategies across CEX/DEX venues.

  • Contracts (per vault): BoringVault, TellerWithMultiAssetSupport, AccountantWithRateProviders, DelayedWithdraw, Lens
  • Frontend: React Provider + Hook with typed helpers for reads/writes
  • Integrators: wallets, fintechs, exchanges, treasury/enterprise apps

Install

npm i boring-vault-ui ethers
# or
yarn add boring-vault-ui ethers

Peer requirements

  • React 18+
  • Ethers v6
  • A stable RPC for your target network

Quick Start

Wrap your app with the Provider, then call the Hook from your components.

import React from "react"
import { BoringVaultV1Provider } from "boring-vault-ui"
import { JsonRpcProvider } from "ethers"

export default function AppRoot() {
  const ethersProvider = new JsonRpcProvider(process.env.NEXT_PUBLIC_RPC)

  return (
    <BoringVaultV1Provider
      vaultContract="0x..."        // BoringVault
      tellerContract="0x..."       // TellerWithMultiAssetSupport
      accountantContract="0x..."   // AccountantWithRateProviders
      lensContract="0x..."         // Lens (batched reads)
      ethersProvider={ethersProvider}
      depositTokens={[
        { address: "0xTokenA...", decimals: 18 },
        { address: "0xTokenB...", decimals: 6 }
      ]}
      baseToken={{ address: "0xBase...", decimals: 18 }}
      vaultDecimals={18}
    >
      {/* your app */}
    </BoringVaultV1Provider>
  )
}

Provider

BoringVaultV1Provider (required)

Props (exact names):

vaultContract: string
tellerContract: string
accountantContract: string
lensContract: string
ethersProvider: Ethers BrowserProvider or EIP-1193 provider
depositTokens: Array<{ address: string; decimals: number }>
baseToken: { address: string; decimals: number }
vaultDecimals: number
children: ReactNode

Notes

  • ethersProvider may be an Ethers v6 BrowserProvider or an EIP-1193 provider (the SDK normalizes to BrowserProvider internally).
  • depositTokens and any token refs in writes use only { address, decimals }.

Hook API

useBoringVaultV1()

Readiness

isBoringV1ContextReady: boolean

Vault reads (no wallet required)

fetchTotalAssets(): Promise<number>   // TVL in baseToken units (decimal-adjusted)
fetchShareValue():  Promise<number>   // value of 1 share in baseToken (decimal-adjusted)

User reads (wallet address)

fetchUserShares(userAddress: string):     Promise<number> // decimal-adjusted shares
fetchUserUnlockTime(userAddress: string): Promise<number> // unix seconds

Writes (positional args; all return status objects)

// Supply (a.k.a. deposit)
// depositAmount: human-readable string, e.g., "1.25"
// selectedToken: { address, decimals }
deposit(
  signer,                // Ethers v6 JsonRpcSigner
  depositAmount,         // string (human-readable)
  selectedToken          // { address: string; decimals: number }
): Promise<DepositStatus>

// Delayed Withdrawals (T+1 to T+5 business days)

// shareAmount: human-readable shares, e.g., "10.0"
// tokenOut: { address, decimals }
// maxLoss: percent as string ("1" => 1%); "0" => use contract default
// thirdPartyClaimer: allow a third party to complete on behalf of user
delayWithdraw(
  signer,
  shareAmount,
  tokenOut,
  maxLoss,
  thirdPartyClaimer
): Promise<WithdrawStatus>

// List active (non-zero) intents for the connected user
delayWithdrawStatuses(
  signer
): Promise<DelayWithdrawEntry[]>

// Cancel the user’s request for that tokenOut
delayWithdrawCancel(
  signer,
  tokenOut
): Promise<WithdrawStatus>

// Complete a matured request (UI must verify maturity before calling)
delayWithdrawComplete(
  signer,
  tokenOut
): Promise<WithdrawStatus>

Live status objects (reflect in your UI)

depositStatus:  DepositStatus
withdrawStatus: WithdrawStatus

Types

type DepositStatus = {
  initiated: boolean
  loading:   boolean
  success?:  boolean
  error?:    string
  tx_hash?:  string
}

type WithdrawStatus = {
  initiated: boolean
  loading:   boolean
  success?:  boolean
  error?:    string
  tx_hash?:  string
}

type DelayWithdrawEntry = {
  allowThirdPartyToComplete: boolean
  maxLoss: number                       // percent; 1 => 1%
  maturity: number                      // unix seconds
  shares: number                        // human-readable number
  exchangeRateAtTimeOfRequest: number   // human-readable number
  token: { address: string; decimals: number }
}

Usage Examples

Gate on readiness + show TVL / 1-share value

import React from "react"
import { useBoringVaultV1 } from "boring-vault-ui"

export function VaultOverview() {
  const { isBoringV1ContextReady, fetchTotalAssets, fetchShareValue } = useBoringVaultV1()
  const [tvl, setTvl] = React.useState(0)
  const [shareValue, setShareValue] = React.useState(0)

  React.useEffect(() => {
    if (!isBoringV1ContextReady) return
    fetchTotalAssets().then(setTvl)
    fetchShareValue().then(setShareValue)
  }, [isBoringV1ContextReady])

  if (!isBoringV1ContextReady) return <div>Loading vault…</div>

  return (
    <div>
      <div>TVL (base token): {tvl}</div>
      <div>1 Share Value (base token): {shareValue}</div>
    </div>
  )
}

User shares + unlock time

import React from "react"
import { useBoringVaultV1 } from "boring-vault-ui"

export function UserPosition({ user }) {
  const { isBoringV1ContextReady, fetchUserShares, fetchUserUnlockTime } = useBoringVaultV1()
  const [shares, setShares] = React.useState(0)
  const [unlockTs, setUnlockTs] = React.useState(0)

  React.useEffect(() => {
    if (!isBoringV1ContextReady || !user) return
    fetchUserShares(user).then(setShares)
    fetchUserUnlockTime(user).then(setUnlockTs)
  }, [isBoringV1ContextReady, user])

  if (!user) return <div>Connect wallet</div>
  return (
    <div>
      <div>Your shares: {shares}</div>
      <div>Unlock time (unix): {unlockTs}</div>
    </div>
  )
}

Supply (deposit)

import React from "react"
import { useBoringVaultV1 } from "boring-vault-ui"

export function DepositForm({ signer }) {
  const { deposit, depositStatus } = useBoringVaultV1()
  const [amount, setAmount] = React.useState("")
  const token = { address: "0xTokenA...", decimals: 18 }

  async function onSubmit(e) {
    e.preventDefault()
    await deposit(signer, amount, token)
  }

  return (
    <form onSubmit={onSubmit}>
      <input placeholder="Amount (e.g., 1.25)" value={amount} onChange={e => setAmount(e.target.value)} />
      <button type="submit" disabled={depositStatus.loading}>Supply</button>
      {depositStatus.initiated && <div>Submitting…</div>}
      {depositStatus.tx_hash && <div>Tx: {depositStatus.tx_hash}</div>}
      {depositStatus.error && <div>Error: {depositStatus.error}</div>}
      {depositStatus.success && <div>Success</div>}
    </form>
  )
}

Delayed withdrawals — request, list, cancel, complete

import React from "react"
import { useBoringVaultV1 } from "boring-vault-ui"

export function WithdrawFlow({ signer }) {
  const {
    delayWithdraw,
    delayWithdrawStatuses,
    delayWithdrawCancel,
    delayWithdrawComplete,
    withdrawStatus,
  } = useBoringVaultV1()

  const [shareAmount, setShareAmount] = React.useState("10.0")
  const [maxLoss, setMaxLoss] = React.useState("1")
  const [thirdParty, setThirdParty] = React.useState(false)
  const [pending, setPending] = React.useState([])
  const tokenOut = { address: "0xTokenA...", decimals: 18 }

  async function request(e) {
    e.preventDefault()
    await delayWithdraw(signer, shareAmount, tokenOut, maxLoss, thirdParty)
    await refresh()
  }

  async function refresh() {
    const list = await delayWithdrawStatuses(signer)
    setPending(list || [])
  }

  async function cancel() {
    await delayWithdrawCancel(signer, tokenOut)
    await refresh()
  }

  async function complete(maturity) {
    const now = Math.floor(Date.now() / 1000)
    if (now < maturity) {
      alert("Not yet matured")
      return
    }
    await delayWithdrawComplete(signer, tokenOut)
    await refresh()
  }

  React.useEffect(() => { refresh() }, [])

  return (
    <div>
      <form onSubmit={request}>
        <input value={shareAmount} onChange={e => setShareAmount(e.target.value)} placeholder="Shares (e.g., 10.0)" />
        <input value={maxLoss} onChange={e => setMaxLoss(e.target.value)} placeholder="Max loss % (e.g., 1)" />
        <label>
          <input type="checkbox" checked={thirdParty} onChange={e => setThirdParty(e.target.checked)} />
          Allow third party to complete
        </label>
        <button type="submit" disabled={withdrawStatus.loading}>Request</button>
      </form>

      <button onClick={refresh}>Refresh</button>

      {pending.length === 0 && <div>No active withdrawal requests</div>}
      {pending.map((p, i) => (
        <div key={i} style={{ border: "1px solid #444", padding: 8, marginTop: 8 }}>
          <div>TokenOut: {p.token.address} (decimals {p.token.decimals})</div>
          <div>Shares: {p.shares}</div>
          <div>Maturity (unix): {p.maturity}</div>
          <div>Max loss %: {p.maxLoss}</div>
          <div>3rd-party completion: {String(p.allowThirdPartyToComplete)}</div>
          <div>Rate at request: {p.exchangeRateAtTimeOfRequest}</div>

          <div style={{ marginTop: 8 }}>
            <button disabled={withdrawStatus.loading} onClick={cancel}>Cancel</button>
            <button disabled={withdrawStatus.loading} onClick={() => complete(p.maturity)}>Complete</button>
          </div>

          {withdrawStatus.initiated && <div>Submitting…</div>}
          {withdrawStatus.tx_hash && <div>Tx: {withdrawStatus.tx_hash}</div>}
          {withdrawStatus.error && <div>Error: {withdrawStatus.error}</div>}
          {withdrawStatus.success && <div>Success</div>}
        </div>
      ))}
    </div>
  )
}

Signer (viem → Ethers v6)

import { BrowserProvider } from "ethers"
// viemWalletClient is created by your app (e.g., wagmi/viem)
async function getEthersSigner(viemWalletClient) {
  const ethersProvider = new BrowserProvider(viemWalletClient.transport)
  return await ethersProvider.getSigner()
}

Decimal & Rate Handling

  • All read helpers return numbers already decimal-adjusted for display/UX.
  • All amount inputs to writes are human-readable strings (e.g., "1.25", "10.0").
  • The SDK’s deposit helper computes the minimumMint using current vault/accountant rates and a small slippage buffer; you do not need to hand-roll decimals/rate math.

UX & Product Rules

  • Gate all reads/writes on isBoringV1ContextReady.
  • Share-lock: surface fetchUserUnlockTime before enabling transfers/redeems.
  • Delayed withdrawals: treat as T+1 to T+5 business days; only enable Complete after maturity.
  • Stacking: calling delayWithdraw repeatedly for the same tokenOut stacks requested amounts—disable additional requests while one is ongoing.
  • Approvals: request the exact approval where possible (avoid unlimited by default).
  • Status UX: always reflect { initiated, loading, success?, error?, tx_hash? } for deposit/withdraw flows.
  • Positioning language: “technology-powered DeFi automation,” “non-custodial liquidity routing,” “rewards on digital asset balances.” Include: “No guarantee of returns. Rewards are variable and derived from blockchain protocol incentives and CeFi/DeFi strategies across CEX/DEX venues.”