Skip to main content

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
All API implementations are in 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

  1. Type Safety - Use TypeScript for all API calls and responses
  2. Error Handling - Always wrap API calls in try-catch blocks
  3. Connection Management - Close Electrum connections when done
  4. Caching - Cache blockchain data to reduce API calls
  5. Validation - Validate all inputs before making API calls
  6. Timeouts - Set appropriate timeouts for network requests
  7. Retries - Implement retry logic for failed requests
  8. Testing - Write integration tests for all API interactions

Resources