Btcb2TransactionAssemblyData.fromServiceResponse constructor

Btcb2TransactionAssemblyData.fromServiceResponse(
  1. Map<String, dynamic> response
)

Parses and validates a complete BTCB2 transaction assembly response.

Implementation

factory Btcb2TransactionAssemblyData.fromServiceResponse(
  Map<String, dynamic> response,
) {
  final payload = _map(response, 'signing_payload');
  if (_string(payload, 'type') != payloadType) {
    throw const FormatException('Unexpected BTCB2 signing payload type.');
  }

  final signingInfo = _map(response, 'signing_info');
  _expect(signingInfo, 'method', 'btcb2_unified_sighash');
  _expect(signingInfo, 'digest_algorithm', 'tagged-sha256');
  _expect(signingInfo, 'digest_encoding', 'bytes');
  _expect(signingInfo, 'signature_algorithm', 'secp256k1-ecdsa');
  _expect(signingInfo, 'signature_encoding', 'der-plus-sighash-byte');
  _expect(signingInfo, 'serialized_transaction_encoding', 'hex');

  final data = _map(payload, 'data');
  if (_integer(data, 'version') != 2) {
    throw const FormatException('Unsupported BTCB2 assembly version.');
  }
  if (!BTCB2Chain.matchesName(_string(data, 'chain'))) {
    throw const FormatException('Unsupported BTCB2 chain name.');
  }
  _expect(data, 'network', 'mainnet');

  final sender = _map(data, 'sender');
  final senderAddress = _string(sender, 'address');
  final senderPath = _string(sender, 'derivationPath');
  if (senderPath != Btcb2Coin.defaultDerivationPath) {
    throw FormatException(
      'BTCB2 only supports derivation path ${Btcb2Coin.defaultDerivationPath}.',
    );
  }

  final identity = _map(data, 'chainIdentity');
  if (_integer(identity, 'activationHeight') != activationHeight ||
      _hex(identity, 'activationHash', length: 32) != activationBlockHash) {
    throw const FormatException('Unexpected BTCB2 chain identity.');
  }

  final sighash = _map(data, 'sighash');
  if (_integer(sighash, 'type') != unifiedSighashType ||
      _string(sighash, 'typeHex').toLowerCase() != '0x21' ||
      _integer(sighash, 'epoch') != 0 ||
      _string(sighash, 'tag') != 'UnifiedSighash') {
    throw const FormatException('Unsupported BTCB2 sighash settings.');
  }

  final transaction = _map(data, 'transaction');
  final transactionVersion = _integer(transaction, 'version');
  if (transactionVersion != 2) {
    throw const FormatException('BTCB2 transaction version must be 2.');
  }
  final lockTime = _uint32(transaction, 'lockTime');

  final inputJson = _listOfMaps(data, 'inputs');
  final outputJson = _listOfMaps(data, 'outputs');
  if (inputJson.isEmpty || outputJson.isEmpty) {
    throw const FormatException('BTCB2 inputs and outputs cannot be empty.');
  }
  final inputs = inputJson.map(Btcb2TransactionInput.fromJson).toList();
  final outputs = outputJson.map(Btcb2TransactionOutput.fromJson).toList();
  final fee = Btcb2TransactionFee.fromJson(_map(data, 'fee'));

  final seenOutpoints = <String>{};
  final senderScript = Btcb2Coin.scriptPubKeyFromAddress(senderAddress);
  if (!Btcb2Coin.isP2pkhScript(senderScript)) {
    throw const FormatException(
      'BTCB2 sender must be a mainnet P2PKH address.',
    );
  }
  for (final input in inputs) {
    final outpoint = '${input.transactionId}:${input.outputIndex}';
    if (!seenOutpoints.add(outpoint)) {
      throw FormatException('Duplicate BTCB2 input: $outpoint');
    }
    if (input.sequence != 0xfffffffd ||
        input.address != senderAddress ||
        input.derivationPath != senderPath ||
        input.scriptType != 0 ||
        !Btcb2Coin.isP2pkhScript(input.scriptPubKey) ||
        !BytesUtils.bytesEqual(input.scriptPubKey, input.scriptCode) ||
        !BytesUtils.bytesEqual(input.scriptPubKey, senderScript)) {
      throw FormatException('Invalid BTCB2 input metadata for $outpoint.');
    }
  }
  for (final output in outputs) {
    final expected = Btcb2Coin.scriptPubKeyFromAddress(output.address);
    if (!BytesUtils.bytesEqual(output.scriptPubKey, expected)) {
      throw FormatException(
        'Output script does not match address ${output.address}.',
      );
    }
  }

  final totalInput = inputs.fold<int>(
    0,
    (sum, item) => sum + item.amountSats,
  );
  final totalOutput = outputs.fold<int>(
    0,
    (sum, item) => sum + item.amountSats,
  );
  if (totalInput > _maxMoneySats ||
      totalOutput > _maxMoneySats ||
      fee.amountSats > _maxMoneySats ||
      totalInput != totalOutput + fee.amountSats) {
    throw const FormatException(
      'BTCB2 input/output/fee arithmetic is invalid.',
    );
  }

  return Btcb2TransactionAssemblyData._(
    senderAddress: senderAddress,
    senderDerivationPath: senderPath,
    inputs: List.unmodifiable(inputs),
    outputs: List.unmodifiable(outputs),
    fee: fee,
    transactionVersion: transactionVersion,
    lockTime: lockTime,
    totalInputSats: totalInput,
    totalOutputSats: totalOutput,
  );
}