Btcb2TransactionAssemblyData.fromServiceResponse constructor
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,
);
}