Bitcoin Wallet

The UTXOS Bitcoin wallet provides address management, UTXO handling, transfers, PSBT signing, and message signing. The API follows the Xverse Wallet specification for compatibility with existing Bitcoin tooling.

💡

TLDR: Enable a Bitcoin wallet and get your address.

import { Web3Wallet } from "@utxos/sdk";
 
const wallet = await Web3Wallet.enable({
  projectId: process.env.NEXT_PUBLIC_UTXOS_PROJECT_ID,
  networkId: 0, // 0 = testnet, 1 = mainnet
});
 
const addresses = await wallet.bitcoin.getAddresses();
console.log("Address:", addresses[0].address);

Table of Contents

Prerequisites

You need the following before integrating the Bitcoin wallet:

Setup

Install Dependencies

npm install @utxos/sdk @meshsdk/bitcoin @meshsdk/provider

Configure Environment

# .env
NEXT_PUBLIC_UTXOS_PROJECT_ID=your_project_id
NEXT_PUBLIC_NETWORK_ID=0  # 0 = testnet, 1 = mainnet

Initialize the Wallet

import { Web3Wallet, EnableWeb3WalletOptions } from "@utxos/sdk";
import { BlockstreamBitcoinProvider } from "@meshsdk/provider";
 
async function initBitcoinWallet() {
  // Optional: Configure provider via proxy (same-origin Esplora access)
  const bitcoinProvider = new BlockstreamBitcoinProvider("testnet");
  (bitcoinProvider as { baseUrl: string }).baseUrl = "/api/blockstream/testnet";
 
  const options: EnableWeb3WalletOptions = {
    projectId: process.env.NEXT_PUBLIC_UTXOS_PROJECT_ID,
    networkId: parseInt(process.env.NEXT_PUBLIC_NETWORK_ID) || 0,
    bitcoinProvider, // Optional: enables enhanced functionality
  };
 
  const wallet = await Web3Wallet.enable(options);
  return wallet.bitcoin;
}

Verify Connection

const bitcoin = await initBitcoinWallet();
 
const addresses = await bitcoin.getAddresses();
console.log("Address:", addresses[0].address);
 
const balance = await bitcoin.getBalance();
console.log("Balance:", balance.total, "sats");

Address Management

Get wallet addresses with optional filtering by purpose.

// Get all addresses
const addresses = await bitcoin.getAddresses();
 
// Get specific address types
const filtered = await bitcoin.getAddresses(
  ["payment", "ordinals"],
  "Please provide your addresses",
);
 
addresses.forEach((addr) => {
  console.log("Address:", addr.address);
  console.log("Public Key:", addr.publicKey);
  console.log("Purpose:", addr.purpose); // "payment" | "ordinals"
  console.log("Type:", addr.addressType); // "p2wpkh" | "p2tr" | "p2sh"
  console.log("Network:", addr.network); // "mainnet" | "testnet" | "regtest"
});
 
// Get wallet public key
const publicKey = bitcoin.getPublicKey();
 
// Get network ID
const networkId = bitcoin.getNetworkId(); // 0 = testnet, 1 = mainnet, 2 = regtest

Address Types

TypeFormatPrefix (Mainnet)Prefix (Testnet)
Native SegWitp2wpkhbc1qtb1q
Taprootp2trbc1ptb1p
Wrapped SegWitp2sh32

Balance and UTXOs

Query wallet balance and unspent transaction outputs.

// Get balance
const balance = await bitcoin.getBalance();
console.log("Confirmed:", balance.confirmed, "sats");
console.log("Unconfirmed:", balance.unconfirmed, "sats");
console.log("Total:", balance.total, "sats");
 
// Convert to BTC
const btc = parseInt(balance.total) / 100_000_000;
console.log("Total:", btc, "BTC");
 
// Get UTXOs
const utxos = await bitcoin.getUTxOs();
utxos.forEach((utxo) => {
  console.log("TXID:", utxo.txid);
  console.log("Vout:", utxo.vout);
  console.log("Value:", utxo.value, "sats");
  console.log("Script:", utxo.script);
});
 
// Calculate total UTXO value
const totalValue = utxos.reduce((sum, u) => sum + u.value, 0);
console.log("Total UTXO Value:", totalValue, "sats");

Transfers

Send Bitcoin to one or more recipients.

const result = await bitcoin.sendTransfer({
  recipients: [
    { address: "tb1q...", amount: 100000 }, // 100,000 sats
    { address: "tb1p...", amount: 50000 },  // 50,000 sats
  ],
});
 
console.log("Transaction ID:", result.txid);

Transfer Parameters

ParameterTypeDescription
recipientsArrayList of recipient objects
recipients[].addressstringBitcoin address
recipients[].amountnumberAmount in satoshis

PSBT Operations

Sign Partially Signed Bitcoin Transactions for advanced use cases.

const signResult = await bitcoin.signPsbt({
  psbt: "cHNidP8BAF4CAAAAAe...", // Base64-encoded PSBT
  signInputs: {
    "tb1q...": [0, 1], // Address -> input indices to sign
  },
  broadcast: false, // Set true to broadcast after signing
});
 
console.log("Signed PSBT:", signResult.psbt);
 
if (signResult.txid) {
  console.log("Broadcast TXID:", signResult.txid);
}

PSBT Parameters

ParameterTypeDescription
psbtstringBase64-encoded PSBT
signInputsobjectMap of address to input indices
broadcastbooleanBroadcast after signing (default: false)

Message Signing

Sign arbitrary messages for authentication or verification.

const addresses = await bitcoin.getAddresses();
 
const signResult = await bitcoin.signMessage({
  address: addresses[0].address,
  message: "Sign this message to authenticate",
  protocol: "ECDSA",
});
 
console.log("Signature:", signResult.signature);
console.log("Message Hash:", signResult.messageHash);
console.log("Address:", signResult.address);

Sign Message Parameters

ParameterTypeDescription
addressstringAddress to sign with
messagestringMessage to sign
protocolstringSigning protocol (“ECDSA”)

API Reference

Methods

MethodReturnsDescription
getAddresses(purposes?, message?)Promise<Address[]>Get wallet addresses
getPublicKey()stringGet wallet public key
getNetworkId()numberGet current network ID
getBalance()Promise<Balance>Get wallet balance
getUTxOs()Promise<UTXO[]>Get unspent outputs
sendTransfer(params)Promise<{txid: string}>Send Bitcoin
signPsbt(params)Promise<SignResult>Sign a PSBT
signMessage(params)Promise<SignResult>Sign a message

Types

interface Address {
  address: string;
  publicKey: string;
  purpose: "payment" | "ordinals";
  addressType: "p2wpkh" | "p2tr" | "p2sh";
  network: "mainnet" | "testnet" | "regtest";
}
 
interface Balance {
  confirmed: string;
  unconfirmed: string;
  total: string;
}
 
interface UTXO {
  txid: string;
  vout: number;
  value: number;
  script: string;
}

Complete Examples

Example 1: Transfer with Balance Check

import { Web3Wallet } from "@utxos/sdk";
 
async function sendBitcoin(recipientAddress: string, amountSats: number) {
  const wallet = await Web3Wallet.enable({
    projectId: process.env.NEXT_PUBLIC_UTXOS_PROJECT_ID,
    networkId: 0,
  });
 
  const bitcoin = wallet.bitcoin;
 
  // Check balance first
  const balance = await bitcoin.getBalance();
  const totalSats = parseInt(balance.total);
 
  if (totalSats < amountSats + 10000) {
    throw new Error(`Insufficient balance: ${totalSats} sats available`);
  }
 
  // Send transfer
  const result = await bitcoin.sendTransfer({
    recipients: [{ address: recipientAddress, amount: amountSats }],
  });
 
  console.log("Transaction ID:", result.txid);
  return result.txid;
}
 
// Usage
await sendBitcoin("tb1q9pvjqz5u5sdgpatg3wn0ce4v3seqfrqhp4qd2e", 100000);

Example 2: PSBT Workflow

import { Web3Wallet } from "@utxos/sdk";
 
async function signAndBroadcastPsbt(psbtBase64: string) {
  const wallet = await Web3Wallet.enable({
    projectId: process.env.NEXT_PUBLIC_UTXOS_PROJECT_ID,
    networkId: 0,
  });
 
  const bitcoin = wallet.bitcoin;
  const addresses = await bitcoin.getAddresses();
  const utxos = await bitcoin.getUTxOs();
 
  console.log("Available addresses:", addresses.length);
  console.log("Available UTXOs:", utxos.length);
 
  const signResult = await bitcoin.signPsbt({
    psbt: psbtBase64,
    signInputs: {
      [addresses[0].address]: [0],
    },
    broadcast: true,
  });
 
  console.log("Broadcast TXID:", signResult.txid);
  return signResult.txid;
}

Example 3: Message Authentication

import { Web3Wallet } from "@utxos/sdk";
 
async function authenticateUser(challenge: string) {
  const wallet = await Web3Wallet.enable({
    projectId: process.env.NEXT_PUBLIC_UTXOS_PROJECT_ID,
    networkId: 0,
  });
 
  const bitcoin = wallet.bitcoin;
  const addresses = await bitcoin.getAddresses();
 
  const signResult = await bitcoin.signMessage({
    address: addresses[0].address,
    message: challenge,
    protocol: "ECDSA",
  });
 
  return {
    address: signResult.address,
    signature: signResult.signature,
    messageHash: signResult.messageHash,
  };
}

Error Handling

Handle common Bitcoin wallet errors gracefully.

import { ApiError } from "@utxos/sdk";
 
async function safeBitcoinOperation() {
  try {
    const result = await bitcoin.sendTransfer({
      recipients: [{ address: "tb1q...", amount: 50000 }],
    });
    return result;
  } catch (error) {
    if (error.message.includes("Insufficient funds")) {
      throw new Error("Not enough Bitcoin for this transaction");
    }
 
    if (error.message.includes("Invalid address")) {
      throw new Error("The recipient address is invalid");
    }
 
    if (error.message.includes("read-only wallet")) {
      throw new Error("Cannot sign with an address-only wallet");
    }
 
    throw error;
  }
}

Common Errors

ErrorCauseSolution
Insufficient fundsBalance too lowCheck balance before sending
Invalid addressMalformed addressValidate address format
read-only walletNo signing capabilityUse a full wallet, not watch-only

Set Up a Bitcoin Data Provider

For on-chain queries and transaction broadcast, configure a Blockstream Esplora provider. No API key is required.

The public Blockstream API is sufficient for development and typical production volume. Proxying through your backend avoids browser CORS issues and lets you record submit metrics.

Proxy Route (Next.js Pages Router)

Create pages/api/blockstream/[...slug].ts that forwards to https://blockstream.info/api (mainnet) or https://blockstream.info/testnet/api (testnet). UTXOS ships this proxy in the hosted app.

Use the Proxy

import { BlockstreamBitcoinProvider } from "@meshsdk/provider";
import { Web3Wallet } from "@utxos/sdk";
 
const bitcoinProvider = new BlockstreamBitcoinProvider("testnet");
(bitcoinProvider as { baseUrl: string }).baseUrl = "/api/blockstream/testnet";
 
const wallet = await Web3Wallet.enable({
  projectId: process.env.NEXT_PUBLIC_UTXOS_PROJECT_ID,
  networkId: 0,
  bitcoinProvider,
});

Security

⚠️

Follow these security practices for production deployments.

PracticeDescription
Validate addressesVerify address format before sending
Use testnet firstTest thoroughly before mainnet deployment
Check balancesVerify sufficient funds before transfers
Review PSBTsInspect PSBT details before signing
Monitor feesCheck fee rates to avoid overpaying
Handle errorsImplement comprehensive error handling

Integration Test

Verify your setup is working correctly.

import { Web3Wallet } from "@utxos/sdk";
 
async function testBitcoinIntegration() {
  const wallet = await Web3Wallet.enable({
    projectId: process.env.NEXT_PUBLIC_UTXOS_PROJECT_ID,
    networkId: 0,
  });
 
  const bitcoin = wallet.bitcoin;
 
  // Test addresses
  const addresses = await bitcoin.getAddresses();
  console.assert(addresses.length > 0, "No addresses returned");
  console.assert(
    addresses[0].address.startsWith("tb1"),
    "Invalid testnet address"
  );
  console.log("Addresses: OK");
 
  // Test balance
  const balance = await bitcoin.getBalance();
  console.assert(typeof balance.total === "string", "Invalid balance format");
  console.log("Balance: OK");
 
  // Test UTXOs
  const utxos = await bitcoin.getUTxOs();
  console.assert(Array.isArray(utxos), "Invalid UTXO format");
  console.log("UTXOs: OK");
 
  // Test message signing
  const signResult = await bitcoin.signMessage({
    address: addresses[0].address,
    message: "test",
    protocol: "ECDSA",
  });
  console.assert(typeof signResult.signature === "string", "Invalid signature");
  console.log("Message signing: OK");
 
  console.log("All tests passed");
}