SDK quickstart
@offrecord/sdk is the account the app runs on. This walks it from install to a verified receipt, in the order the calls run.
Install
npm i @offrecord/sdkThe package is ESM and runs in the browser. Key derivation, note scanning and receipt verification happen there, on native fetch and Web Crypto.
Create the client
import { createOffrecord } from '@offrecord/sdk'
const offrecord = createOffrecord({ apiUrl: 'https://api.example.com' })apiUrl is the Offrecord API. The client reads the Ring’s public state with no wallet:
const ring = await offrecord.ring()
const { account, listed } = await offrecord.resolve(address)A wallet signer
The SDK needs three things from a wallet: its address, a message signature and a transaction signature. Messages and transactions are bytes in and bytes out. With a Wallet Standard wallet:
import type { WalletSigner } from '@offrecord/sdk'
const { signMessage } = wallet.features['solana:signMessage']
const { signTransaction } = wallet.features['solana:signTransaction']
const signer: WalletSigner = {
address: walletAccount.address,
signMessage: async (message) =>
(await signMessage({ account: walletAccount, message }))[0].signature,
signTransaction: async (transaction) =>
(await signTransaction({ account: walletAccount, transaction }))[0].signedTransaction,
}Open the account
const account = await offrecord.open(signer)
await account.sync((s) => console.log(s.scanned, 'of', s.total))The first open asks for two message signatures: the account’s keys derive from one in the browser, and the other enrolls them in the Ring. Later calls in the same browser ask for none. Nothing moves. sync scans the Ring with the account’s viewing key and resolves when it has caught up.
In a browser that has already opened this account, offrecord.restore(address) returns it with no signature.
Balance
const holdings = await account.balance() // off the record
const wallet = await account.publicBalance() // on the record
for (const { asset, amount } of holdings) {
console.log(asset.symbol, amount, asset.decimals)
}Amounts are base units as decimal strings, so "25000000" of an asset with 6 decimals is 25. Keep them as strings or BigInt.
Deposit
const USDC = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'
const deposit = await account.deposit(
{ mint: USDC, amount: '25000000' },
{ onStage: (stage) => console.log(stage) },
)The wallet signs one transaction, and the call resolves when it confirms. For SOL, pass NATIVE_SOL_MINT, exported by the package.
Send
const input = { to: 'RecipientSolanaAddress', mint: USDC, amount: '10000000' }
const quote = await account.quote({ ...input, kind: 'send' })
console.log(quote.route, quote.relayCost, quote.receives)
const sent = await account.send(input, {
onStage: (stage) => console.log(stage), // signing, proving, relaying, following, confirmed
})to is a normal Solana address. When it has an Offrecord account, the payment arrives off the record. When it has none, quote.route is withdrawal and the payment arrives on the record at that address.
Withdraw
const out = await account.withdraw({ to: signer.address, mint: USDC, amount: '5000000' })
console.log(out.exitFee, out.relayCost)A withdrawal goes to any public address. Take a quote with kind: "withdraw" first to read the exit fee and what the destination receives.
Activity
const transfers = await account.activity()
for (const t of transfers) {
console.log(t.kind, t.status, t.asset.symbol, t.amount, t.counterparty)
}
const stop = account.subscribe(async () => {
console.log(await account.balance())
})subscribe fires when a scan finds new notes or a transfer changes status. Call the function it returns to stop.
Receipt
const link = await account.receipt(sent.id)The link opens that one transfer. Send it to whoever needs to see the payment.
Verify a receipt
const receipt = await offrecord.verifyReceipt(link)
console.log(receipt.amount, receipt.asset.symbol, receipt.sender, receipt.recipient, receipt.time)verifyReceipt needs no wallet and no account, so it runs for anyone who holds the link. Receipts and viewing keys explains what it checks.
Errors
import { OffrecordError } from '@offrecord/sdk'
try {
await account.send(input)
} catch (error) {
if (error instanceof OffrecordError && error.code === 'listed') {
// The address is on the block list.
}
}Every call rejects with an OffrecordError. The SDK reference lists each code.