Documentation Index
Fetch the complete documentation index at: https://mintlify.com/satsigner/satsigner/llms.txt
Use this file to discover all available pages before exploring further.
Overview
SatSigner integrates with multiple Bitcoin APIs to provide comprehensive wallet functionality:- BDK (Bitcoin Dev Kit) - Wallet creation, transaction building, and signing
- Electrum - Blockchain queries and address monitoring
- Esplora - Block explorer API for transaction and UTXO data
- Nostr - Multisig coordination protocol
apps/mobile/api/ with full TypeScript support.
BDK Integration
Wallet Creation
BDK provides the core wallet functionality:api/bdk.ts
import {
Address,
Blockchain,
DatabaseConfig,
Descriptor,
DescriptorPublicKey,
DescriptorSecretKey,
Mnemonic,
TxBuilder,
Wallet
} from 'bdk-rn'
import {
AddressIndex,
BlockchainElectrumConfig,
BlockchainEsploraConfig,
BlockChainNames,
KeychainKind,
Network
} from 'bdk-rn/lib/lib/enums'
type WalletData = {
fingerprint: string
derivationPath: string
externalDescriptor: string
internalDescriptor: string
wallet: Wallet
keyFingerprints?: string[] // Optional for multisig
}
async function getWalletDataFromMnemonic(
mnemonic: string,
scriptVersion: 'P2PKH' | 'P2SH-P2WPKH' | 'P2WPKH' | 'P2TR',
passphrase: string | undefined,
network: Network
): Promise<WalletData> {
const externalDescriptor = await getDescriptorObject(
mnemonic,
scriptVersion,
KeychainKind.External,
passphrase,
network
)
const internalDescriptor = await getDescriptorObject(
mnemonic,
scriptVersion,
KeychainKind.Internal,
passphrase,
network
)
const [{ fingerprint, derivationPath }, wallet] = await Promise.all([
parseDescriptor(externalDescriptor),
getWalletFromDescriptor(externalDescriptor, internalDescriptor, network)
])
return {
fingerprint,
derivationPath,
externalDescriptor: await externalDescriptor.asString(),
internalDescriptor: await internalDescriptor.asString(),
wallet
}
}
async function getWalletFromDescriptor(
externalDescriptor: Descriptor,
internalDescriptor: Descriptor | null,
network: Network
): Promise<Wallet> {
const dbConfig = await new DatabaseConfig().memory()
const wallet = await new Wallet().create(
externalDescriptor,
internalDescriptor,
network,
dbConfig
)
return wallet
}
Descriptor Creation
async function getDescriptorObject(
mnemonic: string,
scriptVersion: 'P2PKH' | 'P2SH-P2WPKH' | 'P2WPKH' | 'P2TR',
kind: KeychainKind,
passphrase: string | undefined,
network: Network
): Promise<Descriptor> {
const descriptor = getPrivateDescriptorFromMnemonic(
mnemonic,
scriptVersion,
kind,
passphrase,
network
)
const descriptorObject = await new Descriptor().create(descriptor, network)
return descriptorObject
}
function getPrivateDescriptorFromMnemonic(
mnemonic: string,
scriptVersion: string,
kind: KeychainKind,
passphrase: string | undefined,
network: Network
): string {
const path = derivationPath(scriptVersion, network, kind)
const secret = passphrase ? `${mnemonic}/${passphrase}` : mnemonic
switch (scriptVersion) {
case 'P2PKH':
return `pkh([${fingerprint}${path}]${xpub}/<0;1>/*)`
case 'P2SH-P2WPKH':
return `sh(wpkh([${fingerprint}${path}]${xpub}/<0;1>/*))`
case 'P2WPKH':
return `wpkh([${fingerprint}${path}]${xpub}/<0;1>/*)`
case 'P2TR':
return `tr([${fingerprint}${path}]${xpub}/<0;1>/*)`
default:
throw new Error(`Unsupported script version: ${scriptVersion}`)
}
}
Multisig Wallet Creation
async function createMultisigWallet(
account: Account,
network: Network
): Promise<WalletData> {
const scriptVersion = account.keys[0]?.scriptVersion || 'P2WSH'
const multisigScriptType = getMultisigScriptTypeFromScriptVersion(scriptVersion)
// Extract key data with proper derivation paths and fingerprints
const keyData = await Promise.all(
account.keys.map(async (key) => {
let extendedPublicKey = ''
let fingerprint = ''
if (typeof key.secret === 'object') {
fingerprint = key.secret.fingerprint || key.fingerprint || ''
if (key.secret.extendedPublicKey) {
extendedPublicKey = key.secret.extendedPublicKey
} else if (key.secret.externalDescriptor) {
extendedPublicKey = getExtendedKeyFromDescriptor(
key.secret.externalDescriptor
)
}
}
return { fingerprint, extendedPublicKey }
})
)
// Sort keys by extended public key for consistent ordering
const sortedKeyData = keyData.sort((a, b) =>
a.extendedPublicKey.localeCompare(b.extendedPublicKey)
)
const policyDerivationPath = getMultisigDerivationPathFromScriptVersion(
scriptVersion,
network
)
const cleanPolicyPath = policyDerivationPath.replace(/^m\/?/i, '')
// Build key section with policy-based derivation paths
const keySection = sortedKeyData
.map(({ fingerprint, extendedPublicKey }) => {
return `[${fingerprint}/${cleanPolicyPath}]${extendedPublicKey}/<0;1>/*`
})
.join(',')
// Create descriptor based on script type
let finalDescriptor = ''
switch (multisigScriptType) {
case 'P2SH':
finalDescriptor = `sh(sortedmulti(${account.keysRequired},${keySection}))`
break
case 'P2SH-P2WSH':
finalDescriptor = `sh(wsh(sortedmulti(${account.keysRequired},${keySection})))`
break
case 'P2WSH':
finalDescriptor = `wsh(sortedmulti(${account.keysRequired},${keySection}))`
break
case 'P2TR':
finalDescriptor = `tr(sortedmulti(${account.keysRequired},${keySection}))`
break
}
// Create separate descriptors for external and internal addresses
const externalDescriptor = finalDescriptor.replace(/<0;1>/g, '0')
const internalDescriptor = finalDescriptor.replace(/<0;1>/g, '1')
const externalDesc = await new Descriptor().create(externalDescriptor, network)
const internalDesc = await new Descriptor().create(internalDescriptor, network)
const wallet = await getWalletFromDescriptor(externalDesc, internalDesc, network)
const parsedDescriptor = await parseDescriptor(externalDesc)
return {
fingerprint: parsedDescriptor.fingerprint,
derivationPath: parsedDescriptor.derivation Path,
externalDescriptor: finalDescriptor,
internalDescriptor: '',
wallet,
keyFingerprints: sortedKeyData.map((kd) => kd.fingerprint)
}
}
Wallet Sync
async function syncWallet(
wallet: Wallet,
backend: 'electrum' | 'esplora',
blockchainConfig: BlockchainElectrumConfig | BlockchainEsploraConfig
): Promise<void> {
const blockchain = await getBlockchain(backend, blockchainConfig)
await wallet.sync(blockchain)
}
async function getBlockchain(
backend: 'electrum' | 'esplora',
config: BlockchainElectrumConfig | BlockchainEsploraConfig
): Promise<Blockchain> {
let blockchainName: BlockChainNames = BlockChainNames.Electrum
if (backend === 'esplora') blockchainName = BlockChainNames.Esplora
const blockchain = await new Blockchain().create(config, blockchainName)
return blockchain
}
Transaction Building
async function buildTransaction(
wallet: Wallet,
data: {
inputs: Utxo[]
outputs: Output[]
fee: number
options: { rbf: boolean }
},
network: Network
): Promise<TxBuilderResult> {
const transactionBuilder = await new TxBuilder().create()
// Add specific UTXOs as inputs
await transactionBuilder.addUtxos(
data.inputs.map((utxo) => ({ txid: utxo.txid, vout: utxo.vout }))
)
await transactionBuilder.manuallySelectedOnly()
// Add outputs
for (const output of data.outputs) {
const recipient = await getScriptPubKeyFromAddress(output.to, network)
await transactionBuilder.addRecipient(recipient, output.amount)
}
// Set absolute fee
await transactionBuilder.feeAbsolute(data.fee)
// Enable RBF if requested
if (data.options.rbf) await transactionBuilder.enableRbf()
const transactionBuilderResult = await transactionBuilder.finish(wallet)
return transactionBuilderResult
}
async function getScriptPubKeyFromAddress(
address: string,
network: Network
): Promise<Address> {
const recipientAddress = await new Address().create(address, network)
return recipientAddress.scriptPubKey()
}
Transaction Signing and Broadcasting
async function signTransaction(
transaction: TxBuilderResult,
wallet: Wallet
): Promise<PartiallySignedTransaction> {
const partiallySignedTransaction = await wallet.sign(transaction.psbt)
return partiallySignedTransaction
}
async function broadcastTransaction(
psbt: PartiallySignedTransaction,
blockchain: Blockchain
): Promise<string> {
const transaction = await psbt.extractTx()
const txid = await blockchain.broadcast(transaction)
return txid
}
Electrum Integration
Client Setup
api/electrum.ts
import * as bitcoinjs from 'bitcoinjs-lib'
import BlueWalletElectrumClient from 'electrum-client'
import TcpSocket from 'react-native-tcp-socket'
class ElectrumClient {
client: ModifiedClient
network: bitcoinjs.networks.Network
constructor({
host,
port,
protocol = 'ssl',
network = 'signet'
}: {
host: string
port: number
protocol?: 'tcp' | 'tls' | 'ssl'
network?: Network
}) {
const net = TcpSocket
const tls = TcpSocket
this.client = new ModifiedClient(net, tls, port, host, protocol, {})
this.network = bitcoinjsNetwork(network)
}
static fromUrl(url: string, network: Network): ElectrumClient {
const port = url.replace(/.*:/, '')
const protocol = url.replace(/::\/\/.*/, '')
const host = url.replace(`${protocol}://`, '').replace(`:${port}`, '')
if (
!(isValidDomainName(host) || isValidIPAddress(host)) ||
!port.match(/^[0-9]+$/) ||
(protocol !== 'ssl' && protocol !== 'tls' && protocol !== 'tcp')
) {
throw new Error('Invalid backend URL')
}
return new ElectrumClient({
host,
port: Number(port),
protocol,
network
})
}
async init() {
await this.client.initElectrum({
client: 'satsigner',
version: '1.4'
})
}
close() {
this.client.close()
}
}
Address Queries
class ElectrumClient {
addressToScriptHash(address: string): string {
const script = bitcoinjs.address.toOutputScript(address, this.network)
const hash = bitcoinjs.crypto.sha256(script)
const reversedHash = new Buffer(hash.reverse())
return reversedHash.toString('hex')
}
async getAddressBalance(address: string): Promise<{
confirmed: number
unconfirmed: number
}> {
const scriptHash = this.addressToScriptHash(address)
const balance = await this.client.blockchainScripthash_getBalance(scriptHash)
return balance
}
async getAddressUtxos(address: string): Promise<{
height: number
tx_hash: string
tx_pos: number
value: number
}[]> {
const scriptHash = this.addressToScriptHash(address)
const result = await this.client.blockchainScripthash_listunspent(scriptHash)
return result
}
async getAddressTransactions(address: string): Promise<{
height: number
tx_hash: string
}[]> {
const scriptHash = this.addressToScriptHash(address)
const result = await this.client.blockchainScripthash_getHistory(scriptHash)
return result
}
async getAddressInfo(
address: string,
addressKeychain: 'external' | 'internal' = 'external'
): Promise<{
utxos: Utxo[]
transactions: Transaction[]
balance: { confirmed: number; unconfirmed: number }
}> {
const addressUtxos = await this.getAddressUtxos(address)
const utxoHeights = addressUtxos.map((value) => value.height)
const utxoTimestamps = await this.getBlockTimestamps(utxoHeights)
const utxos = this.parseAddressUtxos(
address,
addressUtxos,
utxoTimestamps,
addressKeychain
)
const addressTxs = await this.getAddressTransactions(address)
const txIds = addressTxs.map((value) => value.tx_hash)
const rawTransactions = await this.getTransactions(txIds)
const txHeights = addressTxs.map((value) => value.height)
const txTimestamps = await this.getBlockTimestamps(txHeights)
const balance = await this.getAddressBalance(address)
const transactions = this.parseAddressTransactions(
address,
rawTransactions,
txHeights,
txTimestamps
)
return { utxos, transactions, balance }
}
}
Transaction Parsing
parseAddressUtxos(
address: string,
utxos: ElectrumUtxo[],
timestamps: number[],
addressKeychain: string
): Utxo[] {
return utxos.map((electrumUtxo, index) => ({
txid: electrumUtxo.tx_hash,
value: electrumUtxo.value,
vout: electrumUtxo.tx_pos,
addressTo: address,
keychain: addressKeychain,
timestamp: new Date(timestamps[index] * 1000),
label: '',
script: [...bitcoinjs.address.toOutputScript(address, this.network)]
}))
}
parseAddressTransactions(
address: string,
rawTransactions: string[],
heights: number[],
timestamps: number[]
): Transaction[] {
const transactions: Transaction[] = []
const parsedTransactions: TxDecoded[] = []
const txDictionary: Record<string, number> = {}
rawTransactions.forEach((rawTx, index) => {
const parsedTx = TxDecoded.fromHex(rawTx)
const tx: Transaction = {
id: parsedTx.getId(),
type: 'send',
sent: 0,
received: 0,
address,
blockHeight: heights[index],
timestamp: new Date(timestamps[index] * 1000),
lockTime: parsedTx.locktime,
version: parsedTx.version,
label: '',
raw: parseHexToBytes(rawTx),
vout: [],
vin: [],
vsize: parsedTx.virtualSize(),
weight: parsedTx.weight(),
size: parsedTx.byteLength(),
prices: {}
}
transactions.push(tx)
parsedTransactions.push(parsedTx)
txDictionary[tx.id] = index
})
// Compute sent and received values
for (let i = 0; i < transactions.length; i++) {
const currentTx = parsedTransactions[i]
const outputCount = Number(currentTx.getOutputCount().value)
for (let j = 0; j < outputCount; j++) {
const addr = currentTx.generateOutputScriptAddress(j, this.network)
const value = Number(currentTx.getOutputValue(j).value)
const script = [...currentTx.outs[j].script]
transactions[i].vout.push({ address: addr, value, script })
if (addr === address) {
transactions[i].received += value
}
}
transactions[i].type =
transactions[i].received > transactions[i].sent ? 'receive' : 'send'
}
return transactions
}
Esplora Integration
api/esplora.ts
class Esplora {
baseUrl: string
constructor(baseUrl: string) {
this.baseUrl = baseUrl
}
async getTxStatus(txid: string): Promise<{
confirmed: boolean
block_height?: number
block_hash?: string
block_time?: number
}> {
const response = await fetch(`${this.baseUrl}/tx/${txid}/status`)
return await response.json()
}
async getTxHex(txid: string): Promise<string> {
const response = await fetch(`${this.baseUrl}/tx/${txid}/hex`)
return await response.text()
}
async getAddressUtxos(address: string): Promise<{
txid: string
vout: number
value: number
status: {
confirmed: boolean
block_height?: number
}
}[]> {
const response = await fetch(`${this.baseUrl}/address/${address}/utxo`)
return await response.json()
}
async getAddressTxs(address: string): Promise<any[]> {
const response = await fetch(`${this.baseUrl}/address/${address}/txs`)
return await response.json()
}
async getFeeEstimates(): Promise<Record<string, number>> {
const response = await fetch(`${this.baseUrl}/fee-estimates`)
return await response.json()
}
async broadcastTx(txHex: string): Promise<string> {
const response = await fetch(`${this.baseUrl}/tx`, {
method: 'POST',
body: txHex
})
return await response.text()
}
}
export default Esplora
Error Handling
All API calls should include proper error handling:try {
const wallet = await getWalletDataFromMnemonic(
mnemonic,
scriptVersion,
passphrase,
network
)
return { success: true, wallet }
} catch (error) {
console.error('Wallet creation failed:', error)
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
}
}
Best Practices
- Type Safety - Use TypeScript for all API calls and responses
- Error Handling - Always wrap API calls in try-catch blocks
- Connection Management - Close Electrum connections when done
- Caching - Cache blockchain data to reduce API calls
- Validation - Validate all inputs before making API calls
- Timeouts - Set appropriate timeouts for network requests
- Retries - Implement retry logic for failed requests
- Testing - Write integration tests for all API interactions
Resources
- Component Development - UI components
- State Management - Zustand stores
- Testing - API testing strategies
- BDK Documentation
- Electrum Protocol
- Esplora API