upsertUTXO method

  1. @override
Future<void> upsertUTXO(
  1. String walletId,
  2. BitcoinUtxo utxo
)
override

Inserts or updates the (walletId, txid, vout) row.

updated_at is the UTXO's BitcoinUtxo.updatedAt; is_available follows the status (available only) and says nothing more than that — it was called is_spendable until migration v024 (bead libspiffy-p8qc), a name that promised WalletBalances.isSpendable, a rule over the whole wallet state that no per-row column can hold. The spend history is only ever added to (audit S-20, data retention): spent_at records the first store as spent and is never cleared, and spent_in_tx_id is never overwritten once set. A block height, plugin metadata or derivation index the update lacks keeps the stored value. The reservation columns follow the UTXO (a release clears them; bead libspiffy-viy).

Implementation

@override
Future<void> upsertUTXO(String walletId, BitcoinUtxo utxo) async {
  _ensureInitialized();

  final spent = utxo.status == UTXOStatus.spent;
  await _pool!.execute(
    Sql.named('''
      INSERT INTO bitcoin_utxos (
        wallet_id, txid, vout, utxo_key, satoshis, script_pub_key, address,
        block_height, confirmations, status, created_at, updated_at, spent_at,
        spent_in_tx_id, script_type, is_available, category, plugin_metadata,
        derivation_index, reserved_by_tx_id, reservation_reason,
        reservation_expires_at, reservation_priority, status_before_reservation
      ) VALUES (
        @walletId, @txid, @vout, @utxoKey, @satoshis, @scriptPubKey, @address,
        @blockHeight, @confirmations, @status, @createdAt, @updatedAt, @spentAt,
        @spentInTxId, @scriptType, @isAvailable, @category,
        CAST(@pluginMetadata AS JSONB),
        @derivationIndex, @reservedByTxId, @reservationReason,
        @reservationExpiresAt, @reservationPriority, @statusBeforeReservation
      )
      ON CONFLICT (wallet_id, txid, vout) DO UPDATE SET
        satoshis = EXCLUDED.satoshis,
        script_pub_key = EXCLUDED.script_pub_key,
        address = EXCLUDED.address,
        -- A zero-confirmation update (reorg, audit 3b0) clears the height.
        block_height = CASE WHEN EXCLUDED.confirmations > 0
            THEN COALESCE(EXCLUDED.block_height, bitcoin_utxos.block_height)
            ELSE EXCLUDED.block_height END,
        confirmations = EXCLUDED.confirmations,
        status = EXCLUDED.status,
        updated_at = EXCLUDED.updated_at,
        spent_at = COALESCE(bitcoin_utxos.spent_at, EXCLUDED.spent_at),
        is_available = EXCLUDED.is_available,
        plugin_metadata = COALESCE(EXCLUDED.plugin_metadata, bitcoin_utxos.plugin_metadata),
        spent_in_tx_id = COALESCE(bitcoin_utxos.spent_in_tx_id, EXCLUDED.spent_in_tx_id),
        derivation_index = COALESCE(EXCLUDED.derivation_index, bitcoin_utxos.derivation_index),
        reserved_by_tx_id = EXCLUDED.reserved_by_tx_id,
        reservation_reason = EXCLUDED.reservation_reason,
        reservation_expires_at = EXCLUDED.reservation_expires_at,
        reservation_priority = EXCLUDED.reservation_priority,
        status_before_reservation = EXCLUDED.status_before_reservation
    '''),
    parameters: {
      'walletId': walletId,
      'txid': utxo.txid,
      'vout': utxo.vout,
      'utxoKey': '${utxo.txid}:${utxo.vout}',
      'satoshis': utxo.satoshis.toInt(),
      'scriptPubKey': utxo.scriptPubKey,
      'address': utxo.address,
      'blockHeight': utxo.blockHeight,
      'confirmations': utxo.confirmations ?? 0,
      'status': utxo.status.name,
      'createdAt': utxo.createdAt,
      'updatedAt': utxo.updatedAt,
      'spentAt': spent ? utxo.updatedAt : null,
      'scriptType': 'p2pkh',
      'isAvailable': utxo.status == UTXOStatus.available,
      'category': 'funding',
      'pluginMetadata': utxo.pluginMetadata == null
          ? null
          : jsonEncode(utxo.pluginMetadata),
      'spentInTxId': utxo.spentInTxId,
      'derivationIndex': utxo.derivationIndex,
      'reservedByTxId': utxo.reservedByTxId,
      'reservationReason': utxo.reservationReason,
      'reservationExpiresAt': utxo.reservationExpiresAt,
      'reservationPriority': utxo.reservationPriority,
      'statusBeforeReservation': utxo.statusBeforeReservation?.name,
    },
  );
}