Skip to content

Latest commit

 

History

History
371 lines (282 loc) · 12.5 KB

File metadata and controls

371 lines (282 loc) · 12.5 KB

API Reference

Every exported symbol in src/, in dependency order. Each entry has the signature, what it does, when it throws, and a minimal example.

Types refer to viem (Hex, Address, WalletClient, PublicClient). All amounts are bigint in token base units (so 1 USDC = 1_000_000n, 1 VIRTUAL = 10n ** 18n).


src/abi.ts

Pure constants. No functions, no I/O. Re-import from here whenever you need addresses or selectors so a future address change is a one-file edit.

Addresses

export const BONDINGV5_PROXY = '0x1a540088125d00dd3990f9da45ca0859af4d3b01';
export const FROUTER_V3      = '0x02fe8ec3d9bbf7318eb54590bcc39198a8b47ded';
export const VIRTUAL         = '0x0b3e328455c4059EEb9e3f84b5543F74E24e7E1b';
export const USDC            = '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913';
export const WETH            = '0x4200000000000000000000000000000000000006';
export const UNIV3_ROUTER    = '0x2626664c2603336E57B271c5C0b26F421741e481';
export const UNIV3_QUOTER    = '0x3d4e44Eb1374240CE5F1B871ab261CD16335B76a';
export const UNIV2_ROUTER    = '0x4752ba5DBc23f44D87826276BF6Fd6b1C372aD24';
export const WETH_VIRTUAL_POOL_500 = '0x9c087eb773291e50cf6c6a90ef0f4500e349b903';

Selectors (Hex)

export const SEL_BALANCE_OF       = '0x70a08231';
export const SEL_APPROVE          = '0x095ea7b3';
export const SEL_ALLOWANCE        = '0xdd62ed3e';
export const SEL_BOND_BUY         = '0x706910ff';
export const SEL_BOND_SELL        = '0xb233e056';
export const SEL_TOKEN_INFO       = '0xf5dab711';
export const SEL_EXACT_IN_SINGLE  = '0x04e45aaf';
export const SEL_EXACT_IN         = '0xb858183f';
export const SEL_QUOTE_EXACT_IN_SINGLE = '0xc6a5026a';
export const SEL_QUOTE_EXACT_IN   = '0xcdca1753';
export const SEL_V2_SWAP          = '0x38ed1739';
export const SEL_V2_SWAP_FOT      = '0x5c11d795';

Minimal ABIs

export const ERC20_ABI = [/* balanceOf, approve, allowance, decimals */];
export const BONDING_V5_ABI = [/* bondBuy, bondSell, tokenInfo */];
export const UNIV3_ROUTER_ABI = [/* exactInputSingle, exactInput */];
export const UNIV3_QUOTER_ABI = [/* quoteExactInputSingle, quoteExactInput */];

Use the ABIs with viem's encodeFunctionData / decodeFunctionResult if you don't want to hand-roll calldata. The library uses the raw selectors for speed, but the ABIs are exported for callers who prefer the typed path.


src/bondingV5.ts — the core trading library

getTokenInfo

async function getTokenInfo(
  publicClient: PublicClient,
  token: Address
): Promise<{ pair: Address; trading: boolean; tradingOnUniswap: boolean }>

Returns the on-chain state of a Virtuals token. Decides which router to use.

Field Meaning
pair The FPair (bonding curve) address. Useful for direct curve reads.
trading true while the token is bonded. false after graduation.
tradingOnUniswap true after graduation. Stays false while bonded.

Throws if the address is not a Virtuals token (call reverts with no data).

const info = await getTokenInfo(client, '0xabc...');
if (info.trading) {
  // bonded — use BondingV5
} else if (info.tradingOnUniswap) {
  // graduated — use UniV3
} else {
  throw new Error('token in invalid state');
}

quoteBuyOnCurve

async function quoteBuyOnCurve(
  publicClient: PublicClient,
  token: Address,
  virtualIn: bigint
): Promise<bigint>

Returns the curve's expected output (in token base units) for spending virtualIn VIRTUAL. Uses the curve reserves directly — no fee-on-transfer adjustment. The actual delivered amount after a trade may be 0–100 bps lower for FoT tokens.

quoteSellOnCurve

async function quoteSellOnCurve(
  publicClient: PublicClient,
  token: Address,
  tokenIn: bigint
): Promise<bigint>

Symmetric to quoteBuyOnCurve. Returns expected VIRTUAL output for selling tokenIn of token.

bondingV5BuyOnCurve

async function bondingV5BuyOnCurve(
  wallet: WalletClient,
  publicClient: PublicClient,
  args: {
    token: Address;
    virtualIn: bigint;
    slippageBps?: number;   // default 100 (1%)
    deadline?: bigint;      // default now + 600s
  }
): Promise<{
  txHash: Hex;
  quoted: bigint;
  delivered: bigint;       // pre/post balanceOf diff
  slippageRealisedBps: number;
}>

The main buy primitive. Steps:

  1. quoteBuyOnCurve to get expected output.
  2. Compute minOut = quoted * (10000 - slippageBps) / 10000.
  3. approveIfNeeded(VIRTUAL, FROUTER_V3, virtualIn).
  4. balanceBefore = balanceOf(wallet, token).
  5. Submit bondBuy(token, virtualIn, minOut) to BONDINGV5_PROXY.
  6. Wait for receipt.
  7. balanceAfter = balanceOf(wallet, token).
  8. delivered = balanceAfter - balanceBefore.
  9. If delivered < minOut * 9500 / 10000 → throw BondingV5DeliveryShortfall.
  10. Return.

Throws:

  • BondingV5GraduatedError if the token has already graduated. Caller should switch to swapV3.
  • BondingV5InsufficientVirtual if wallet has < virtualIn VIRTUAL.
  • BondingV5DeliveryShortfall if FoT reconciliation fails (rare).
  • viem's standard TransactionExecutionError for tx-level failures.
const result = await bondingV5BuyOnCurve(wallet, publicClient, {
  token: '0xabc...',
  virtualIn: 10n ** 18n,    // 1 VIRTUAL
  slippageBps: 200,         // 2%
});
console.log(`got ${result.delivered} tokens, tx ${result.txHash}`);

bondingV5SellOnCurve

async function bondingV5SellOnCurve(
  wallet: WalletClient,
  publicClient: PublicClient,
  args: {
    token: Address;
    tokenIn: bigint;
    slippageBps?: number;
    deadline?: bigint;
  }
): Promise<{ txHash: Hex; quoted: bigint; delivered: bigint; slippageRealisedBps: number }>

Symmetric to the buy. Approves FRouterV3 for the token (not VIRTUAL), then calls bondSell on the V5 proxy. Same throw conditions.

swapV3

async function swapV3(
  wallet: WalletClient,
  publicClient: PublicClient,
  args: {
    tokenIn: Address;
    tokenOut: Address;
    amountIn: bigint;
    feeTier?: 100 | 500 | 3000 | 10000;   // default 10000 for graduated tokens, 500 for stable pairs
    slippageBps?: number;                  // default 100
    deadline?: bigint;
    forceV2?: boolean;                     // fallback to UniV2 router if no V3 liquidity
  }
): Promise<{ txHash: Hex; quoted: bigint; delivered: bigint }>

Post-graduation trading. Also used internally by usdcToVirtual / virtualToUsdc.

  1. Quote via quoteV3Single (uses QuoterV2).
  2. Compute minOut.
  3. Approve SwapRouter02 as spender of tokenIn.
  4. Call exactInputSingle.

If forceV2 === true, skips V3 entirely and uses swapExactTokensForTokensSupportingFeeOnTransferTokens on UniV2 router with path [tokenIn, tokenOut].

Throws:

  • V3NoLiquidity if the quote returns 0.
  • Standard tx errors.

quoteV3Single

async function quoteV3Single(
  publicClient: PublicClient,
  args: { tokenIn: Address; tokenOut: Address; amountIn: bigint; feeTier: number }
): Promise<bigint>

eth_call against QuoterV2 → quoteExactInputSingle. Returns 0 if there's no pool at that fee tier.

usdcToVirtual

async function usdcToVirtual(
  wallet: WalletClient,
  publicClient: PublicClient,
  args: { amountIn: bigint; slippageBps?: number }
): Promise<{ txHashes: [Hex, Hex]; deliveredVirtual: bigint }>

The funding-to-trading conversion. Always two hops. Returns both tx hashes.

const { txHashes, deliveredVirtual } = await usdcToVirtual(wallet, client, {
  amountIn: 100_000_000n,  // 100 USDC
});

virtualToUsdc

async function virtualToUsdc(
  wallet: WalletClient,
  publicClient: PublicClient,
  args: { amountIn: bigint; slippageBps?: number }
): Promise<{ txHashes: [Hex, Hex]; deliveredUsdc: bigint }>

Reverse direction. VIRTUAL → WETH → USDC, two single hops.

approveIfNeeded

async function approveIfNeeded(
  wallet: WalletClient,
  publicClient: PublicClient,
  args: { token: Address; spender: Address; amount: bigint }
): Promise<Hex | null>

Reads current allowance. If allowance >= amount returns null (no tx submitted). Otherwise submits approve(spender, type(uint256).max) and returns the tx hash.

The library caches (wallet, token, spender) → maxAllowance in memory once an approve has been observed in the current process, so subsequent calls in the same process skip the RPC roundtrip. This cache is per-process; restarting the trader will hit RPC once per (wallet, token) on first trade.

encodeV3Path

function encodeV3Path(steps: Array<{ token: Address; feeTier?: number }>): Hex

Encodes a multi-hop path. The last element has no feeTier. See CONTRACTS.md#path-encoding.

encodeV3Path([
  { token: USDC, feeTier: 500 },
  { token: WETH, feeTier: 500 },
  { token: VIRTUAL },
])

Used only for callStatic against QuoterV2. Never submit as a real swap (see InvalidFEOpcode in CONTRACTS.md).

parseTokenAmount

function parseTokenAmount(amount: string, decimals: number): bigint

parseTokenAmount('1.5', 18) === 1_500_000_000_000_000_000n. Thin wrapper around viem.parseUnits that handles trailing zeros and scientific notation defensively.

formatTokenAmount

function formatTokenAmount(amount: bigint, decimals: number, precision?: number): string

formatTokenAmount(1_500_000_000_000_000_000n, 18, 4) === '1.5000'.


src/acpCli.ts — ACP smart-wallet bridge

This file is only relevant if you're using ACP (Virtuals' Agent Commerce Protocol) smart wallets. If you're trading from a plain EOA, skip this file entirely.

ACP wallets are ERC-4337 smart wallets controlled by a session signer. You can't sign tx's directly — you submit a user op via the ACP CLI, and the bundler relays it on-chain. This file shells out to the CLI.

acpExec

async function acpExec(args: {
  walletId: string;
  to: Address;
  data: Hex;
  value?: bigint;
  cwd?: string;          // per-wallet config dir; see ACP_WALLETS.md
}): Promise<{ txHash: Hex; userOpHash: Hex }>

Submits an arbitrary call as a user op via the ACP CLI. Returns both the user op hash and the eventual settled tx hash.

The CLI is the binary acp (installed separately, not bundled with this repo). It expects the wallet's session signer and config in cwd. See ACP_WALLETS.md for setup.

The tx hash key varies between ACP CLI versions. The library tries txHash, transactionHash, hash, and userOpHash in that order. If you see a result with only userOpHash you can resolve the settled tx by polling the bundler — but for most use cases userOpHash is sufficient as an idempotency key.

acpBalanceOf

async function acpBalanceOf(args: { walletId: string; token: Address; cwd?: string }): Promise<bigint>

Convenience wrapper that runs acp balance --token <addr> and parses the output. Equivalent to calling balanceOf from a public client with the smart wallet's address.


Error classes

All errors thrown by this library extend Error and have a code field for programmatic handling.

Class code When
BondingV5GraduatedError BONDED_TOKEN_GRADUATED Caller passed a graduated token to a bondBuy/bondSell function. Switch to swapV3.
BondingV5InsufficientVirtual INSUFFICIENT_VIRTUAL Wallet VIRTUAL balance < virtualIn.
BondingV5InsufficientToken INSUFFICIENT_TOKEN Wallet token balance < tokenIn (sell).
BondingV5DeliveryShortfall DELIVERY_SHORTFALL FoT reconciliation: post-trade balance diff is > 5% below minOut. Probably a rug or extreme FoT.
V3NoLiquidity V3_NO_LIQUIDITY QuoterV2 returned 0 for the requested pair/fee.
V3QuoteRevert V3_QUOTE_REVERT QuoterV2 call reverted — usually means the pool doesn't exist at all.
AcpCliMissing ACP_CLI_MISSING acp binary not on PATH.

All errors include the relevant args (token address, requested amount, etc.) on the data field.

try {
  await bondingV5BuyOnCurve(...);
} catch (err) {
  if (err.code === 'BONDED_TOKEN_GRADUATED') {
    return swapV3(...);     // auto-fallback
  }
  throw err;
}

What is NOT exported

A few helpers in bondingV5.ts are intentionally private:

  • readReserves(pair) — internal curve-reserves fetch. Use quoteBuyOnCurve instead.
  • buildBondBuyCalldata(...) — calldata builder. Use the high-level functions.
  • reconcileFoT(...) — the FoT delivery check. Hard-coded 5% tolerance; if you need a different threshold, fork the file.

These may change between versions. The public API above is stable.