Expert guidance for implementing BRC-100 conforming wallets using @bsv/wallet-toolbox. Covers wallet initialization, transaction creation/signing, key management, storage, and certificate operations following the BRC-100 standard.
/plugin marketplace add b-open-io/bsv-skills/plugin install bsv-skills@b-open-ioThis skill inherits all available tools. When active, it can use any tool Claude has access to.
This skill provides comprehensive guidance for implementing BRC-100 conforming wallets using the @bsv/wallet-toolbox package (v1.7.18+).
{
"@bsv/wallet-toolbox": "^1.7.18",
"@bsv/sdk": "^1.9.29"
}
| Class | Purpose | Use When |
|---|---|---|
| Wallet | Full BRC-100 wallet | Building production wallet apps |
| SimpleWalletManager | Lightweight wrapper | Simple key-based authentication |
| CWIStyleWalletManager | Multi-profile wallet | Advanced UMP token flows |
| WalletSigner | Transaction signing | Custom signing logic |
npm install @bsv/wallet-toolbox @bsv/sdk
# Optional storage backends:
npm install knex sqlite3 # SQLite
npm install knex mysql2 # MySQL
npm install idb # IndexedDB (browser)
import {
Wallet,
WalletStorageManager,
StorageKnex,
StorageIdb,
Services,
WalletServices,
PrivilegedKeyManager
} from '@bsv/wallet-toolbox'
import {
PrivateKey,
KeyDeriver,
Random,
Utils
} from '@bsv/sdk'
import { Wallet, StorageKnex, Services } from '@bsv/wallet-toolbox'
import { PrivateKey, Random } from '@bsv/sdk'
import Knex from 'knex'
async function createSimpleWallet() {
// 1. Create root private key (or derive from mnemonic)
const rootKey = new PrivateKey(Random(32))
const keyDeriver = {
rootKey,
identityKey: rootKey.toPublicKey().toString(),
derivePublicKey: async (protocolID, keyID, counterparty) => {
// Implement BRC-42 key derivation
const derivedKey = rootKey.deriveChild(protocolID, keyID)
return { publicKey: derivedKey.toPublicKey().toString() }
},
derivePrivateKey: async (protocolID, keyID, counterparty) => {
const derivedKey = rootKey.deriveChild(protocolID, keyID)
return { privateKey: derivedKey.toString() }
},
deriveSymmetricKey: async (protocolID, keyID, counterparty) => {
// Return shared secret
return { symmetricKey: Random(32) }
}
}
// 2. Configure SQLite storage
const knex = Knex({
client: 'sqlite3',
connection: { filename: './wallet.db' },
useNullAsDefault: true
})
const storage = new StorageKnex({
knex,
storageIdentityKey: rootKey.toPublicKey().toString(),
storageName: 'my-wallet-storage'
})
await storage.makeAvailable()
// 3. Configure services (mainnet)
const services = new Services({
chain: 'main',
bsvExchangeRate: { timestamp: new Date(), base: 'USD', rate: 50 },
bsvUpdateMsecs: 15 * 60 * 1000,
fiatExchangeRates: {
timestamp: new Date(),
base: 'USD',
rates: { EUR: 0.85, GBP: 0.73 }
},
fiatUpdateMsecs: 24 * 60 * 60 * 1000,
arcUrl: 'https://arc.taal.com',
arcConfig: {}
})
// 4. Create wallet
const wallet = new Wallet({
chain: 'main',
keyDeriver,
storage,
services
})
return wallet
}
import { Wallet, StorageIdb, Services } from '@bsv/wallet-toolbox'
import { PrivateKey, Random } from '@bsv/sdk'
async function createBrowserWallet() {
const rootKey = new PrivateKey(Random(32))
// Use IndexedDB for browser storage
const storage = new StorageIdb({
idb: await openDB('my-wallet-db', 1),
storageIdentityKey: rootKey.toPublicKey().toString(),
storageName: 'browser-wallet'
})
await storage.makeAvailable()
const services = new Services({
chain: 'main',
// ... services config
})
const wallet = new Wallet({
chain: 'main',
keyDeriver: createKeyDeriver(rootKey),
storage,
services
})
return wallet
}
import { CWIStyleWalletManager, OverlayUMPTokenInteractor } from '@bsv/wallet-toolbox'
async function createMultiProfileWallet() {
const manager = new CWIStyleWalletManager(
'example.com', // Admin originator
async (profilePrimaryKey, profilePrivilegedKeyManager, profileId) => {
// Build wallet for specific profile
const keyDeriver = createKeyDeriver(new PrivateKey(profilePrimaryKey))
const storage = await createStorage(profileId)
const services = new Services({ chain: 'main', /* ... */ })
return new Wallet({
chain: 'main',
keyDeriver,
storage,
services,
privilegedKeyManager: profilePrivilegedKeyManager
})
},
new OverlayUMPTokenInteractor(), // UMP token interactor
async (recoveryKey) => {
// Save recovery key (e.g., prompt user to write it down)
console.log('SAVE THIS RECOVERY KEY:', Utils.toBase64(recoveryKey))
return true
},
async (reason, test) => {
// Retrieve password from user
const password = prompt(`Enter password for: ${reason}`)
if (!password) throw new Error('Password required')
if (!test(password)) throw new Error('Invalid password')
return password
}
)
// Provide presentation key (e.g., from QR code scan)
const presentationKey = Random(32)
await manager.providePresentationKey(presentationKey)
// Provide password
await manager.providePassword('user-password')
// Now authenticated and ready to use
return manager
}
import { CreateActionArgs, CreateActionResult } from '@bsv/sdk'
async function sendBSV(
wallet: Wallet,
recipientAddress: string,
satoshis: number
) {
const args: CreateActionArgs = {
description: 'Send BSV payment',
outputs: [{
lockingScript: Script.fromAddress(recipientAddress).toHex(),
satoshis,
outputDescription: `Payment to ${recipientAddress}`,
basket: 'default',
tags: ['payment']
}],
options: {
acceptDelayedBroadcast: false, // Broadcast immediately
randomizeOutputs: true // Privacy
}
}
const result: CreateActionResult = await wallet.createAction(args)
if (result.txid) {
console.log('Transaction created:', result.txid)
return result.txid
} else {
console.log('Transaction pending signature')
return result.signableTransaction
}
}
async function signTransaction(
wallet: Wallet,
reference: string,
unlockingScripts: Record<number, { unlockingScript: string }>
) {
const result = await wallet.signAction({
reference,
spends: unlockingScripts
})
console.log('Transaction signed:', result.txid)
return result
}
async function getWalletBalance(wallet: Wallet) {
// Method 1: Quick balance (uses special operation)
const balance = await wallet.balance()
console.log(`Balance: ${balance} satoshis`)
// Method 2: Detailed balance with UTXOs
const detailed = await wallet.balanceAndUtxos('default')
console.log(`Total: ${detailed.total} satoshis`)
console.log(`UTXOs: ${detailed.utxos.length}`)
detailed.utxos.forEach(utxo => {
console.log(` ${utxo.outpoint}: ${utxo.satoshis} sats`)
})
return balance
}
import { ListOutputsArgs, ListOutputsResult } from '@bsv/sdk'
async function listSpendableOutputs(wallet: Wallet) {
const args: ListOutputsArgs = {
basket: 'default', // Change basket
spendable: true, // Only spendable outputs
limit: 100,
offset: 0,
tags: ['payment'] // Optional: filter by tags
}
const result: ListOutputsResult = await wallet.listOutputs(args)
console.log(`Found ${result.totalOutputs} outputs`)
result.outputs.forEach(output => {
console.log(` ${output.outpoint}: ${output.satoshis} sats`)
})
return result
}
async function listTransactionHistory(wallet: Wallet) {
const result = await wallet.listActions({
labels: [],
labelQueryMode: 'any',
limit: 50,
offset: 0
})
console.log(`Found ${result.totalActions} actions`)
result.actions.forEach(action => {
console.log(` ${action.txid}: ${action.status} - ${action.description}`)
})
return result
}
async function getIdentityKey(wallet: Wallet) {
// Get wallet's identity key
const result = await wallet.getPublicKey({ identityKey: true })
console.log('Identity Key:', result.publicKey)
return result.publicKey
}
async function getDerivedKey(wallet: Wallet) {
// Get derived key for specific protocol
const result = await wallet.getPublicKey({
protocolID: [2, 'my-app'],
keyID: 'encryption-key-1',
counterparty: 'recipient-identity-key'
})
return result.publicKey
}
async function encryptMessage(
wallet: Wallet,
plaintext: string,
recipientPubKey: string
) {
const result = await wallet.encrypt({
plaintext: Utils.toArray(plaintext, 'utf8'),
protocolID: [2, 'secure-messaging'],
keyID: 'msg-key',
counterparty: recipientPubKey
})
return Utils.toBase64(result.ciphertext)
}
async function decryptMessage(
wallet: Wallet,
ciphertext: string,
senderPubKey: string
) {
const result = await wallet.decrypt({
ciphertext: Utils.toArray(ciphertext, 'base64'),
protocolID: [2, 'secure-messaging'],
keyID: 'msg-key',
counterparty: senderPubKey
})
return Utils.toUTF8(result.plaintext)
}
async function signData(wallet: Wallet, data: string) {
const result = await wallet.createSignature({
data: Utils.toArray(data, 'utf8'),
protocolID: [2, 'document-signing'],
keyID: 'sig-key',
counterparty: 'self'
})
return Utils.toBase64(result.signature)
}
import Knex from 'knex'
import { StorageKnex } from '@bsv/wallet-toolbox'
async function setupSQLiteStorage() {
const knex = Knex({
client: 'sqlite3',
connection: { filename: './wallet.db' },
useNullAsDefault: true
})
const storage = new StorageKnex({
knex,
storageIdentityKey: 'your-identity-key',
storageName: 'main-storage'
})
await storage.makeAvailable()
return storage
}
async function setupMySQLStorage() {
const knex = Knex({
client: 'mysql2',
connection: {
host: 'localhost',
user: 'wallet_user',
password: 'secure_password',
database: 'wallet_db'
}
})
const storage = new StorageKnex({
knex,
storageIdentityKey: 'your-identity-key',
storageName: 'mysql-storage'
})
await storage.makeAvailable()
return storage
}
import { openDB, DBSchema } from 'idb'
import { StorageIdb } from '@bsv/wallet-toolbox'
interface WalletDB extends DBSchema {
// Storage schema is managed by StorageIdb
}
async function setupIndexedDBStorage() {
const db = await openDB<WalletDB>('wallet-db', 1, {
upgrade(db) {
// StorageIdb creates necessary object stores
}
})
const storage = new StorageIdb({
idb: db,
storageIdentityKey: 'your-identity-key',
storageName: 'browser-storage'
})
await storage.makeAvailable()
return storage
}
import { WalletStorageManager } from '@bsv/wallet-toolbox'
async function setupMultiStorage() {
const primaryStorage = await setupSQLiteStorage()
const backupStorage = await setupMySQLStorage()
const manager = new WalletStorageManager(
primaryStorage,
[backupStorage]
)
return manager
}
import { AcquireCertificateArgs } from '@bsv/sdk'
async function acquireDirectCertificate(wallet: Wallet) {
const args: AcquireCertificateArgs = {
acquisitionProtocol: 'direct',
type: 'https://example.com/user-certificate',
certifier: 'certifier-identity-key',
serialNumber: Utils.toArray('cert-serial-123', 'utf8'),
subject: await wallet.getIdentityKey(),
revocationOutpoint: 'txid.vout',
fields: {
name: Utils.toArray('Alice Smith', 'utf8'),
email: Utils.toArray('alice@example.com', 'utf8')
},
keyringForSubject: { /* master keyring */ },
signature: Utils.toArray('signature-bytes', 'base64')
}
const result = await wallet.acquireCertificate(args)
console.log('Certificate acquired:', result.certificateId)
return result
}
async function requestCertificateIssuance(wallet: Wallet) {
const args: AcquireCertificateArgs = {
acquisitionProtocol: 'issuance',
type: 'https://example.com/kyc-certificate',
certifier: 'certifier-identity-key',
certifierUrl: 'https://certifier.example.com',
fields: {
name: 'Alice Smith',
birthdate: '1990-01-01',
country: 'US'
}
}
const result = await wallet.acquireCertificate(args)
console.log('Certificate issued:', result.certificateId)
return result
}
async function listMyCertificates(wallet: Wallet) {
const result = await wallet.listCertificates({
certifiers: ['certifier-identity-key'],
types: ['https://example.com/user-certificate'],
limit: 50,
offset: 0
})
console.log(`Found ${result.totalCertificates} certificates`)
result.certificates.forEach(cert => {
console.log(` Type: ${cert.type}, Certifier: ${cert.certifier}`)
})
return result
}
async function proveCertificate(wallet: Wallet, certificateId: string) {
const result = await wallet.proveCertificate({
certificateId,
fieldsToReveal: ['name', 'email'],
verifier: 'verifier-identity-key',
privileged: false
})
console.log('Certificate proof:', result.keyringForVerifier)
return result
}
import {
WalletError,
WERR_INVALID_PARAMETER,
WERR_INTERNAL,
WERR_REVIEW_ACTIONS
} from '@bsv/wallet-toolbox'
try {
const result = await wallet.createAction({
description: 'Test transaction',
outputs: [/* ... */]
})
} catch (eu: unknown) {
const error = WalletError.fromUnknown(eu)
if (error.name === 'WERR_INVALID_PARAMETER') {
console.error('Invalid parameter:', error.message)
console.error('Stack:', error.stack)
} else if (error.name === 'WERR_REVIEW_ACTIONS') {
// Handle transaction review errors
console.error('Review required:', error.details)
} else {
console.error('Wallet error:', error.message)
}
}
async function handleCreateAction(wallet: Wallet) {
try {
return await wallet.createAction({
description: 'Payment',
outputs: [/* ... */]
})
} catch (eu: unknown) {
const error = WalletError.fromUnknown(eu)
if (error.name === 'WERR_REVIEW_ACTIONS') {
// Access detailed failure info
const details = error as any
console.error('Delayed results:', details.notDelayedResults)
console.error('Send results:', details.sendWithResults)
console.error('Failed txid:', details.txid)
// Handle double-spend
if (details.notDelayedResults?.some(r => r.status === 'doubleSpend')) {
console.error('Double-spend detected!')
console.error('Competing txs:', details.notDelayedResults[0].competingTxs)
}
}
throw error
}
}
class WalletManager {
private wallet: Wallet | null = null
async initialize(rootKey: PrivateKey) {
if (this.wallet) {
throw new Error('Wallet already initialized')
}
const storage = await this.setupStorage()
const services = this.setupServices()
const keyDeriver = this.createKeyDeriver(rootKey)
this.wallet = new Wallet({
chain: 'main',
keyDeriver,
storage,
services
})
return this.wallet
}
async destroy() {
if (this.wallet) {
await this.wallet.destroy()
this.wallet = null
}
}
getWallet(): Wallet {
if (!this.wallet) {
throw new Error('Wallet not initialized')
}
return this.wallet
}
private async setupStorage() {
// Storage setup logic
return await setupSQLiteStorage()
}
private setupServices() {
return new Services({
chain: 'main',
// ... config
})
}
private createKeyDeriver(rootKey: PrivateKey) {
// Key derivation logic
return {
rootKey,
identityKey: rootKey.toPublicKey().toString(),
// ... derivation methods
}
}
}
async function sendWithRetry(
wallet: Wallet,
args: CreateActionArgs,
maxRetries = 3
): Promise<string> {
let lastError: Error | null = null
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const result = await wallet.createAction(args)
if (result.txid) {
return result.txid
}
// Handle signature if needed
if (result.signableTransaction) {
const signed = await wallet.signAction({
reference: result.signableTransaction.reference,
spends: {} // Provide unlocking scripts
})
return signed.txid!
}
throw new Error('Unexpected result format')
} catch (error) {
lastError = error as Error
console.error(`Attempt ${attempt} failed:`, error)
if (attempt < maxRetries) {
await new Promise(resolve => setTimeout(resolve, 1000 * attempt))
}
}
}
throw lastError || new Error('All retries failed')
}
import { Monitor } from '@bsv/wallet-toolbox'
async function setupMonitor(wallet: Wallet) {
const monitor = new Monitor({
storage: wallet.storage,
services: wallet.services,
chain: 'main'
})
monitor.on('transaction', (status) => {
console.log('Transaction update:', status.txid, status.blockHeight)
})
monitor.on('error', (error) => {
console.error('Monitor error:', error)
})
await monitor.start()
return monitor
}
| Task | Method | Key Args |
|---|---|---|
| Send BSV | createAction() | outputs, options |
| Check balance | balance() | None |
| List UTXOs | listOutputs() | basket, spendable |
| Get history | listActions() | labels, limit |
| Get pubkey | getPublicKey() | protocolID, keyID |
| Encrypt data | encrypt() | plaintext, counterparty |
| Get certificate | acquireCertificate() | type, certifier |
Remember: Always handle errors properly, use privileged keys securely, and follow BRC-100 security levels for sensitive operations!