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
- Setup
- Address Management
- Balance and UTXOs
- Transfers
- PSBT Operations
- Message Signing
- API Reference
- Complete Examples
- Error Handling
- Data Provider Setup
- Security
Prerequisites
You need the following before integrating the Bitcoin wallet:
- UTXOS project ID from utxos.dev/dashboard
- Optional: Maestro API key for enhanced on-chain queries
Setup
Install Dependencies
npm install @utxos/sdk @meshsdk/bitcoinConfigure 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_keyInitialize 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 = regtestAddress Types
| Type | Format | Prefix (Mainnet) | Prefix (Testnet) |
|---|---|---|---|
| Native SegWit | p2wpkh | bc1q | tb1q |
| Taproot | p2tr | bc1p | tb1p |
| Wrapped SegWit | p2sh | 3 | 2 |
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
| Parameter | Type | Description |
|---|---|---|
| recipients | Array | List of recipient objects |
| recipients[].address | string | Bitcoin address |
| recipients[].amount | number | Amount 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
| Parameter | Type | Description |
|---|---|---|
| psbt | string | Base64-encoded PSBT |
| signInputs | object | Map of address to input indices |
| broadcast | boolean | Broadcast 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
| Parameter | Type | Description |
|---|---|---|
| address | string | Address to sign with |
| message | string | Message to sign |
| protocol | string | Signing protocol (“ECDSA”) |
API Reference
Methods
| Method | Returns | Description |
|---|---|---|
getAddresses(purposes?, message?) | Promise<Address[]> | Get wallet addresses |
getPublicKey() | string | Get wallet public key |
getNetworkId() | number | Get 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
| Error | Cause | Solution |
|---|---|---|
| Insufficient funds | Balance too low | Check balance before sending |
| Invalid address | Malformed address | Validate address format |
| read-only wallet | No signing capability | Use 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_keyProxy 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.
| Practice | Description |
|---|---|
| Validate addresses | Verify address format before sending |
| Use testnet first | Test thoroughly before mainnet deployment |
| Check balances | Verify sufficient funds before transfers |
| Review PSBTs | Inspect PSBT details before signing |
| Monitor fees | Check fee rates to avoid overpaying |
| Secure API keys | Keep provider keys server-side via proxy |
| Handle errors | Implement 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");
}