Skip to main content

Asset Handles

Learn how to interact with on-chain assets using the fluent Asset Handle APIs introduced in v0.3.0.

Overview

Asset handles provide a lightweight, fluent API for working with on-chain assets. Rather than composing raw Convex Lisp expressions, you create a handle object and call methods directly.

Three handle types are available:

  • 🪙 FungibleToken — CAD29 fungible tokens (@convex.fungible/*)
  • 📦 AssetHandle — Generic assets including NFTs (@convex.asset/*)
  • 🏷️ CnsHandle — Convex Name System (@convex.cns/*)

Handles are:

  • Lightweight — no network calls on construction
  • Stateless — they hold only the asset address and a reference back to the Convex client
  • Synchronous to createconvex.fungible('#128') returns immediately
No Account Needed for Queries

Read-only methods such as balance(), supply(), and decimals() work without setting an account. State-modifying methods like transfer() and mint() require an account and signer.

🪙 Fungible Tokens

Use convex.fungible() to create a FungibleToken handle for any CAD29-compliant token:

import { Convex, KeyPair } from '@convex-world/convex-ts';

const convex = new Convex('https://peer.convex.live');
const token = convex.fungible('#128');

Querying Token State

Read-only operations — no account needed:

// Balance of the client's current address
const bal = await token.balance();

// Balance of another account
const bal2 = await token.balance('#13');

// Total supply across all holders
const sup = await token.supply();

// Number of decimal places (for display formatting)
const dec = await token.decimals();

Transacting with Tokens

State-modifying operations — requires an account:

const keyPair = KeyPair.fromSeed(process.env.CONVEX_SEED!);
convex.setAccount('#1678', keyPair);

// Transfer tokens to another account
await token.transfer('#456', 1000);

// BigInt is supported for large amounts
await token.transfer('#456', 1000000000000000000n);

// Mint new tokens (must have minting authority)
await token.mint(5000);

// Burn tokens from your own balance
await token.burn(100);

Amount Validation

All amounts are validated as non-negative integers. The BalanceLike type accepts:

TypeExampleNotes
number1000Must be a non-negative integer
bigint1000000000000000000nFor amounts exceeding Number.MAX_SAFE_INTEGER
string"1000"Parsed as an integer string

Negative values, fractional numbers, and non-numeric strings throw immediately on the client side.

📦 Generic Assets (NFTs and More)

Use convex.asset() to create an AssetHandle for any asset that follows the Convex asset protocol — including NFTs, multi-token contracts, and other custom assets:

const asset = convex.asset('#256');

Querying Asset State

// Balance of the client's current address
const bal = await asset.balance();

// Total supply
const sup = await asset.supply();

Transferring Assets

The quantity parameter is flexible — it can be a number, bigint, or a CVM expression string for non-numeric asset quantities:

// Fungible-like numeric transfer
await asset.transfer('#456', 100);

// NFT set transfer using a CVM expression string
await asset.transfer('#456', '#{:foo :bar}');
CVM Expression Strings

When you pass a string quantity like '#{:foo :bar}', it is sandboxed inside a (query ...) form before being embedded in the transaction. This prevents injection of arbitrary code and ensures only valid CVM data expressions are accepted.

Offer / Accept Pattern

For trustless exchanges where two parties need to swap assets without trusting each other, use the offer/accept pattern:

// Party A: offer specific NFTs to Party B
await asset.offer('#456', '#{1 2 3}');

// Party B: accept a fungible quantity from Party A
await asset.accept('#456', 50);

The offer is recorded on-chain and can only be claimed by the designated recipient through accept().

🏷️ CNS (Convex Name System)

Use convex.cns() to create a CnsHandle for resolving and managing Convex Name System entries:

const handle = convex.cns('convex.core');

Reading CNS Entries

No account needed:

// Resolve a name to its address
const result = await handle.resolve(); // → #8

Writing CNS Entries

Requires an account with the appropriate CNS permissions:

const keyPair = KeyPair.fromSeed(process.env.CONVEX_SEED!);
convex.setAccount('#1678', keyPair);

// Update the address a name points to
await handle.set('#1678');

// Transfer control of the name to another account
await handle.setController('#99');

Name Validation

Names are validated on construction. The following will throw immediately — no network round-trip required:

  • Empty strings
  • Names starting with a digit
  • Names containing parentheses or other invalid characters
// These throw immediately:
convex.cns(''); // Error: invalid CNS name
convex.cns('123bad'); // Error: invalid CNS name
convex.cns('foo(bar)'); // Error: invalid CNS name

Error Handling

All handle methods throw ConvexError when the CVM returns an error. This is the same error type used by query() and transact():

import { Convex, KeyPair, ConvexError } from '@convex-world/convex-ts';

const convex = new Convex('https://peer.convex.live');
const keyPair = KeyPair.fromSeed(process.env.CONVEX_SEED!);
convex.setAccount('#1678', keyPair);

const token = convex.fungible('#128');

try {
await token.transfer('#456', 999999999);
} catch (e) {
if (e instanceof ConvexError) {
console.error('CVM error code:', e.code); // e.g. "FUNDS"
console.error('Execution info:', e.info); // { juice: 100, fees: 50, ... }
}
}

Common error codes you may encounter:

CodeMeaning
FUNDSInsufficient token balance for the operation
TRUSTCaller lacks permission (e.g. minting without authority)
ARGUMENTInvalid argument (e.g. negative amount reached the CVM)
STATEInvalid state for the operation
NOBODYTarget account does not exist

Class Hierarchy

The three handle types are organised as follows:

  • AssetHandle — base class for generic asset operations (balance, supply, transfer, offer, accept)
  • FungibleToken extends AssetHandle — adds optimised fungible-specific operations (decimals, mint, burn)
  • CnsHandle — independent class for name resolution (resolve, set, setController)

FungibleToken inherits all AssetHandle methods, so you can use offer() and accept() on fungible tokens as well.

Complete Example

Putting it all together — querying a token, checking a CNS name, and performing a transfer:

import { Convex, KeyPair, ConvexError } from '@convex-world/convex-ts';

async function main() {
const convex = new Convex('https://peer.convex.live');

// 1. Resolve a token address from CNS (no account needed)
const cns = convex.cns('my.custom.token');
const resolved = await cns.resolve();
console.log('Token address:', resolved.result);

// 2. Create a fungible token handle
const token = convex.fungible(resolved.result);

// 3. Query token metadata
const supply = await token.supply();
const decimals = await token.decimals();
console.log('Total supply:', supply, `(${decimals} decimal places)`);

// 4. Set up account for transactions
const keyPair = KeyPair.fromSeed(process.env.CONVEX_SEED!);
convex.setAccount(process.env.CONVEX_ADDRESS!, keyPair);

// 5. Check balance and transfer
const balance = await token.balance();
console.log('My balance:', balance);

if (balance > 0) {
try {
await token.transfer('#456', 100);
console.log('Transfer successful');
} catch (e) {
if (e instanceof ConvexError) {
console.error('Transfer failed:', e.code);
}
}
}
}

main().catch(console.error);

Next Steps

  • Queries — Learn about read-only queries and result handling
  • Transactions — Understand the full transaction lifecycle
  • Convex Lisp — Master the language behind asset contracts