Expert guidance for implementing BRC-100 conforming wallets using Go wallet-toolbox. Covers wallet initialization, transaction creation/signing, key management, storage, and certificate operations following the BRC-100 standard in Go.
/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.
Comprehensive guide for implementing BRC-100 conforming wallets using the go-wallet-toolbox package.
import (
"github.com/bsv-blockchain/go-wallet-toolbox/pkg/wallet"
"github.com/bsv-blockchain/go-wallet-toolbox/pkg/storage"
"github.com/bsv-blockchain/go-wallet-toolbox/pkg/services"
sdk "github.com/bsv-blockchain/go-sdk/wallet"
"github.com/bsv-blockchain/go-sdk/primitives/ec"
"github.com/bsv-blockchain/go-sdk/transaction"
)
go get github.com/bsv-blockchain/go-wallet-toolbox
go get github.com/bsv-blockchain/go-sdk
package main
import (
"context"
"log"
"github.com/bsv-blockchain/go-wallet-toolbox/pkg/wallet"
"github.com/bsv-blockchain/go-wallet-toolbox/pkg/storage"
"github.com/bsv-blockchain/go-wallet-toolbox/pkg/services"
"github.com/bsv-blockchain/go-wallet-toolbox/pkg/defs"
sdk "github.com/bsv-blockchain/go-sdk/wallet"
"github.com/bsv-blockchain/go-sdk/primitives/ec"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
func createWallet() (*wallet.Wallet, error) {
ctx := context.Background()
// 1. Generate root key (or derive from mnemonic)
rootKey, err := ec.NewPrivateKey()
if err != nil {
return nil, err
}
// 2. Create key deriver
keyDeriver := sdk.NewKeyDeriver(rootKey)
// 3. Setup SQLite storage
db, err := gorm.Open(sqlite.Open("wallet.db"), &gorm.Config{})
if err != nil {
return nil, err
}
storageOpts := &storage.Options{
Db: db,
StorageIdentityKey: rootKey.PubKey().Compressed(),
StorageName: "main-wallet",
}
walletStorage, err := storage.NewStorage(storageOpts)
if err != nil {
return nil, err
}
// 4. Setup services (mainnet)
servicesOpts := &services.Options{
Network: defs.Mainnet,
ArcURL: "https://arc.taal.com",
WocURL: "https://api.whatsonchain.com/v1/bsv/main",
}
walletServices, err := services.NewServices(ctx, servicesOpts)
if err != nil {
return nil, err
}
// 5. Create wallet
w, err := wallet.NewWallet(
ctx,
keyDeriver,
walletStorage,
wallet.WithServices(walletServices),
wallet.WithNetwork(defs.Mainnet),
)
if err != nil {
return nil, err
}
return w, nil
}
import (
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
func createMySQLWallet(ctx context.Context, rootKey *ec.PrivateKey) (*wallet.Wallet, error) {
// MySQL DSN
dsn := "user:password@tcp(127.0.0.1:3306)/wallet_db?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
return nil, err
}
storageOpts := &storage.Options{
Db: db,
StorageIdentityKey: rootKey.PubKey().Compressed(),
StorageName: "mysql-wallet",
}
walletStorage, err := storage.NewStorage(storageOpts)
if err != nil {
return nil, err
}
keyDeriver := sdk.NewKeyDeriver(rootKey)
walletServices, err := services.NewServices(ctx, &services.Options{
Network: defs.Mainnet,
ArcURL: "https://arc.taal.com",
})
if err != nil {
return nil, err
}
return wallet.NewWallet(
ctx,
keyDeriver,
walletStorage,
wallet.WithServices(walletServices),
wallet.WithNetwork(defs.Mainnet),
)
}
func createTestnetWallet(ctx context.Context) (*wallet.Wallet, error) {
rootKey, _ := ec.NewPrivateKey()
keyDeriver := sdk.NewKeyDeriver(rootKey)
db, _ := gorm.Open(sqlite.Open("testnet.db"), &gorm.Config{})
storageOpts := &storage.Options{
Db: db,
StorageIdentityKey: rootKey.PubKey().Compressed(),
StorageName: "testnet-wallet",
}
walletStorage, _ := storage.NewStorage(storageOpts)
// Testnet services
walletServices, err := services.NewServices(ctx, &services.Options{
Network: defs.Testnet,
ArcURL: "https://arc-test.taal.com",
WocURL: "https://api.whatsonchain.com/v1/bsv/test",
})
if err != nil {
return nil, err
}
return wallet.NewWallet(
ctx,
keyDeriver,
walletStorage,
wallet.WithServices(walletServices),
wallet.WithNetwork(defs.Testnet),
)
}
import (
"github.com/bsv-blockchain/go-sdk/script"
sdk "github.com/bsv-blockchain/go-sdk/wallet"
)
func sendBSV(
ctx context.Context,
w *wallet.Wallet,
recipientAddress string,
satoshis uint64,
) (string, error) {
// Build locking script from address
lockingScript, err := script.NewP2PKHFromAddress(recipientAddress)
if err != nil {
return "", err
}
// Create action args
args := &sdk.CreateActionArgs{
Description: "Send BSV payment",
Outputs: []sdk.CreateActionOutput{
{
LockingScript: lockingScript.String(),
Satoshis: satoshis,
OutputDescription: fmt.Sprintf("Payment to %s", recipientAddress),
Basket: "default",
Tags: []string{"payment"},
},
},
Options: &sdk.CreateActionOptions{
AcceptDelayedBroadcast: false, // Broadcast immediately
RandomizeOutputs: true, // Privacy
},
}
// Create action
result, err := w.CreateAction(ctx, args)
if err != nil {
return "", err
}
if result.TxID != nil {
return *result.TxID, nil
}
// Handle signing if needed
if result.SignableTransaction != nil {
return signAndFinalize(ctx, w, result.SignableTransaction)
}
return "", fmt.Errorf("unexpected result format")
}
func signTransaction(
ctx context.Context,
w *wallet.Wallet,
reference string,
unlockingScripts map[uint32]sdk.UnlockingScriptSend,
) (*sdk.SignActionResult, error) {
args := &sdk.SignActionArgs{
Reference: reference,
Spends: unlockingScripts,
}
result, err := w.SignAction(ctx, args)
if err != nil {
return nil, err
}
return result, nil
}
func getBalance(ctx context.Context, w *wallet.Wallet) (uint64, error) {
// Use special operation for quick balance
args := &sdk.ListOutputsArgs{
Basket: "00000000000000000000000000000000", // Special basket for balance
}
result, err := w.ListOutputs(ctx, args)
if err != nil {
return 0, err
}
return uint64(result.TotalOutputs), nil
}
// Get detailed balance with UTXOs
func getDetailedBalance(ctx context.Context, w *wallet.Wallet) (*sdk.ListOutputsResult, error) {
args := &sdk.ListOutputsArgs{
Basket: "default",
Spendable: to.Ptr(true),
Limit: to.Ptr(uint32(100)),
Offset: to.Ptr(uint32(0)),
}
result, err := w.ListOutputs(ctx, args)
if err != nil {
return nil, err
}
log.Printf("Found %d spendable outputs", len(result.Outputs))
for _, output := range result.Outputs {
log.Printf(" %s: %d satoshis", output.Outpoint, output.Satoshis)
}
return result, nil
}
func listTransactions(ctx context.Context, w *wallet.Wallet) (*sdk.ListActionsResult, error) {
args := &sdk.ListActionsArgs{
Labels: []string{},
LabelQueryMode: to.Ptr(sdk.LabelQueryModeAny),
Limit: to.Ptr(uint32(50)),
Offset: to.Ptr(uint32(0)),
}
result, err := w.ListActions(ctx, args)
if err != nil {
return nil, err
}
log.Printf("Found %d actions", result.TotalActions)
for _, action := range result.Actions {
log.Printf(" %s: %s - %s", *action.TxID, action.Status, action.Description)
}
return result, nil
}
// Get identity key
func getIdentityKey(ctx context.Context, w *wallet.Wallet) (string, error) {
args := &sdk.GetPublicKeyArgs{
IdentityKey: to.Ptr(true),
}
result, err := w.GetPublicKey(ctx, args)
if err != nil {
return "", err
}
return result.PublicKey, nil
}
// Get derived key for protocol
func getDerivedKey(
ctx context.Context,
w *wallet.Wallet,
protocolID sdk.ProtocolID,
keyID string,
counterparty string,
) (string, error) {
args := &sdk.GetPublicKeyArgs{
ProtocolID: protocolID,
KeyID: keyID,
Counterparty: to.Ptr(counterparty),
}
result, err := w.GetPublicKey(ctx, args)
if err != nil {
return "", err
}
return result.PublicKey, nil
}
import "encoding/base64"
func encryptMessage(
ctx context.Context,
w *wallet.Wallet,
plaintext string,
recipientPubKey string,
) (string, error) {
args := &sdk.WalletEncryptArgs{
Plaintext: []byte(plaintext),
ProtocolID: sdk.ProtocolID{2, "secure-messaging"},
KeyID: "msg-key",
Counterparty: to.Ptr(recipientPubKey),
}
result, err := w.Encrypt(ctx, args)
if err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(result.Ciphertext), nil
}
func decryptMessage(
ctx context.Context,
w *wallet.Wallet,
ciphertext string,
senderPubKey string,
) (string, error) {
ciphertextBytes, err := base64.StdEncoding.DecodeString(ciphertext)
if err != nil {
return "", err
}
args := &sdk.WalletDecryptArgs{
Ciphertext: ciphertextBytes,
ProtocolID: sdk.ProtocolID{2, "secure-messaging"},
KeyID: "msg-key",
Counterparty: to.Ptr(senderPubKey),
}
result, err := w.Decrypt(ctx, args)
if err != nil {
return "", err
}
return string(result.Plaintext), nil
}
func signData(ctx context.Context, w *wallet.Wallet, data string) (string, error) {
args := &sdk.CreateSignatureArgs{
Data: []byte(data),
ProtocolID: sdk.ProtocolID{2, "document-signing"},
KeyID: "sig-key",
Counterparty: to.Ptr("self"),
}
result, err := w.CreateSignature(ctx, args)
if err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(result.Signature), nil
}
import (
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
func setupSQLiteStorage(dbPath string, identityKey []byte) (*storage.Storage, error) {
db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{})
if err != nil {
return nil, err
}
opts := &storage.Options{
Db: db,
StorageIdentityKey: identityKey,
StorageName: "sqlite-wallet",
}
return storage.NewStorage(opts)
}
import (
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
func setupMySQLStorage(dsn string, identityKey []byte) (*storage.Storage, error) {
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
return nil, err
}
opts := &storage.Options{
Db: db,
StorageIdentityKey: identityKey,
StorageName: "mysql-wallet",
}
return storage.NewStorage(opts)
}
import (
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
func setupPostgresStorage(dsn string, identityKey []byte) (*storage.Storage, error) {
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
return nil, err
}
opts := &storage.Options{
Db: db,
StorageIdentityKey: identityKey,
StorageName: "postgres-wallet",
}
return storage.NewStorage(opts)
}
func acquireDirectCertificate(
ctx context.Context,
w *wallet.Wallet,
certType string,
certifier string,
) (*sdk.AcquireCertificateResult, error) {
identityKey, _ := getIdentityKey(ctx, w)
args := &sdk.AcquireCertificateArgs{
AcquisitionProtocol: sdk.AcquisitionProtocolDirect,
Type: certType,
Certifier: certifier,
SerialNumber: []byte("cert-serial-123"),
Subject: identityKey,
RevocationOutpoint: "txid.vout",
Fields: map[string][]byte{
"name": []byte("Alice Smith"),
"email": []byte("alice@example.com"),
},
KeyringForSubject: map[string]string{
// Master keyring data
},
Signature: []byte("signature-bytes"),
}
return w.AcquireCertificate(ctx, args)
}
func requestCertificateIssuance(
ctx context.Context,
w *wallet.Wallet,
) (*sdk.AcquireCertificateResult, error) {
args := &sdk.AcquireCertificateArgs{
AcquisitionProtocol: sdk.AcquisitionProtocolIssuance,
Type: "https://example.com/kyc-certificate",
Certifier: "certifier-identity-key",
CertifierURL: to.Ptr("https://certifier.example.com"),
Fields: map[string]interface{}{
"name": "Alice Smith",
"birthdate": "1990-01-01",
"country": "US",
},
}
return w.AcquireCertificate(ctx, args)
}
func listCertificates(ctx context.Context, w *wallet.Wallet) (*sdk.ListCertificatesResult, error) {
args := &sdk.ListCertificatesArgs{
Certifiers: []string{"certifier-identity-key"},
Types: []string{"https://example.com/user-certificate"},
Limit: to.Ptr(uint32(50)),
Offset: to.Ptr(uint32(0)),
}
result, err := w.ListCertificates(ctx, args)
if err != nil {
return nil, err
}
log.Printf("Found %d certificates", result.TotalCertificates)
for _, cert := range result.Certificates {
log.Printf(" Type: %s, Certifier: %s", cert.Type, cert.Certifier)
}
return result, nil
}
func proveCertificate(
ctx context.Context,
w *wallet.Wallet,
certificateID string,
) (*sdk.ProveCertificateResult, error) {
args := &sdk.ProveCertificateArgs{
CertificateID: certificateID,
FieldsToReveal: []string{"name", "email"},
Verifier: "verifier-identity-key",
Privileged: to.Ptr(false),
}
return w.ProveCertificate(ctx, args)
}
import (
"errors"
"github.com/bsv-blockchain/go-wallet-toolbox/pkg/defs"
)
func handleWalletError(err error) {
if err == nil {
return
}
// Check for specific error types
var invalidParamErr *defs.ErrInvalidParameter
if errors.As(err, &invalidParamErr) {
log.Printf("Invalid parameter: %s - %s", invalidParamErr.Parameter, invalidParamErr.Message)
return
}
var internalErr *defs.ErrInternal
if errors.As(err, &internalErr) {
log.Printf("Internal error: %s", internalErr.Message)
return
}
// Generic error
log.Printf("Wallet error: %v", err)
}
func sendWithErrorHandling(
ctx context.Context,
w *wallet.Wallet,
recipient string,
satoshis uint64,
) (string, error) {
txid, err := sendBSV(ctx, w, recipient, satoshis)
if err != nil {
handleWalletError(err)
// Check if it's a review actions error
if strings.Contains(err.Error(), "review") {
log.Printf("Transaction requires review")
// Handle review flow
}
return "", err
}
log.Printf("Transaction sent: %s", txid)
return txid, nil
}
type WalletManager struct {
wallet *wallet.Wallet
storage *storage.Storage
mu sync.RWMutex
}
func NewWalletManager(ctx context.Context, rootKey *ec.PrivateKey) (*WalletManager, error) {
db, err := gorm.Open(sqlite.Open("wallet.db"), &gorm.Config{})
if err != nil {
return nil, err
}
storageOpts := &storage.Options{
Db: db,
StorageIdentityKey: rootKey.PubKey().Compressed(),
StorageName: "managed-wallet",
}
walletStorage, err := storage.NewStorage(storageOpts)
if err != nil {
return nil, err
}
keyDeriver := sdk.NewKeyDeriver(rootKey)
walletServices, err := services.NewServices(ctx, &services.Options{
Network: defs.Mainnet,
ArcURL: "https://arc.taal.com",
})
if err != nil {
return nil, err
}
w, err := wallet.NewWallet(
ctx,
keyDeriver,
walletStorage,
wallet.WithServices(walletServices),
wallet.WithNetwork(defs.Mainnet),
)
if err != nil {
return nil, err
}
return &WalletManager{
wallet: w,
storage: walletStorage,
}, nil
}
func (wm *WalletManager) GetWallet() *wallet.Wallet {
wm.mu.RLock()
defer wm.mu.RUnlock()
return wm.wallet
}
func (wm *WalletManager) Close() error {
wm.mu.Lock()
defer wm.mu.Unlock()
// Cleanup resources
return nil
}
func sendWithRetry(
ctx context.Context,
w *wallet.Wallet,
recipient string,
satoshis uint64,
maxRetries int,
) (string, error) {
var lastErr error
for attempt := 1; attempt <= maxRetries; attempt++ {
txid, err := sendBSV(ctx, w, recipient, satoshis)
if err == nil {
return txid, nil
}
lastErr = err
log.Printf("Attempt %d failed: %v", attempt, err)
if attempt < maxRetries {
time.Sleep(time.Second * time.Duration(attempt))
}
}
return "", fmt.Errorf("all %d retries failed: %w", maxRetries, lastErr)
}
import "github.com/bsv-blockchain/go-wallet-toolbox/pkg/monitor"
func setupMonitor(
ctx context.Context,
walletStorage *storage.Storage,
walletServices *services.WalletServices,
) (*monitor.Monitor, error) {
opts := &monitor.Options{
Storage: walletStorage,
Services: walletServices,
Network: defs.Mainnet,
}
m, err := monitor.NewMonitor(ctx, opts)
if err != nil {
return nil, err
}
// Start monitoring in background
go func() {
if err := m.Start(ctx); err != nil {
log.Printf("Monitor error: %v", err)
}
}()
return m, nil
}
| Task | Function | Key Args |
|---|---|---|
| Send BSV | CreateAction() | Outputs, Options |
| Check balance | ListOutputs() | Special basket |
| 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 use context for cancellation, handle errors explicitly, and follow Go best practices for concurrent wallet access!