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

Configure Environment

# .env
NEXT_PUBLIC_UTXOS_PROJECT_ID=your_project_id
NEXT_PUBLIC_NETWORK_ID=0  # 0 = testnet, 1 = mainnet
 
# Server-side only (for Maestro proxy)
MAESTRO_BITCOIN_API_KEY_TESTNET=your_testnet_key
MAESTRO_BITCOIN_API_KEY_MAINNET=your_mainnet_key

Initialize the Wallet

import { Web3Wallet, EnableWeb3WalletOptions } from "@utxos/sdk";
import { MaestroProvider } from "@meshsdk/bitcoin";
 
async function initBitcoinWallet() {
  // Optional: Configure provider via proxy (keeps API keys server-side)
  const bitcoinProvider = new MaestroProvider({
    network: "testnet",
    baseUrl: "/api/maestro",
  });
 
  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 enhanced functionality, configure a Maestro provider through a secure proxy.

⚠️

Keep API keys server-side. Never expose them in client code.

Environment Variables

# .env
MAESTRO_BITCOIN_API_KEY_TESTNET=your_testnet_key
MAESTRO_BITCOIN_API_KEY_MAINNET=your_mainnet_key

Proxy Route (Next.js App Router)

Create app/api/maestro/[...slug]/route.ts:

import { NextRequest } from "next/server";
 
export async function GET(
  request: NextRequest,
  { params }: { params: { slug: string[] } }
) {
  return handleMaestroRequest(request, params.slug, "GET");
}
 
export async function POST(
  request: NextRequest,
  { params }: { params: { slug: string[] } }
) {
  return handleMaestroRequest(request, params.slug, "POST");
}
 
async function handleMaestroRequest(
  request: NextRequest,
  slug: string[],
  method: string
) {
  const network = slug[0]; // "mainnet" | "testnet"
 
  const apiKey =
    network === "mainnet"
      ? process.env.MAESTRO_BITCOIN_API_KEY_MAINNET
      : process.env.MAESTRO_BITCOIN_API_KEY_TESTNET;
 
  if (!apiKey) {
    return Response.json(
      { error: `Missing Maestro API key for ${network}` },
      { status: 500 }
    );
  }
 
  const baseUrl =
    network === "mainnet"
      ? "https://xbt-mainnet.gomaestro-api.org/v0"
      : "https://xbt-testnet.gomaestro-api.org/v0";
 
  const endpoint = slug.slice(1).join("/");
  const url = `${baseUrl}/${endpoint}`;
 
  const body = method !== "GET" ? await request.json() : undefined;
 
  const response = await fetch(url, {
    method,
    headers: {
      "api-key": apiKey,
      Accept: "application/json",
      ...(method !== "GET" && { "Content-Type": "application/json" }),
    },
    body: body ? JSON.stringify(body) : undefined,
  });
 
  const data = await response.json();
  return Response.json(data, { status: response.status });
}

Use the Proxy

import { MaestroProvider } from "@meshsdk/bitcoin";
import { Web3Wallet } from "@utxos/sdk";
 
const bitcoinProvider = new MaestroProvider({
  network: "testnet",
  baseUrl: "/api/maestro",
});
 
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
Secure API keysKeep provider keys server-side via proxy
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");
}