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

Terminal
npm i @offrecord/sdk

The 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

offrecord.ts
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:

TypeScript
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:

signer.ts
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

TypeScript
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

TypeScript
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

deposit.ts
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

send.ts
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

withdraw.ts
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

TypeScript
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

TypeScript
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

TypeScript
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

TypeScript
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.