finalizeAndExtract function
Finalizes a SIGNED (but not yet finalized) PSBT — after the MPC ceremony
(WalletSigner.signTransaction) attaches a partial signature per input,
this reads each input's own PSBT_IN_PARTIAL_SIG entry for publicKeyHex
(written by the server) and builds the P2WPKH witness stack
signature, pubkey by hand, exactly mirroring kotlin-btc's
finalizeAndExtract (there: reading bitcoin-kmp's own parsed partialSigs
map instead of hand-parsing PSBT bytes). Returns the finalized,
broadcastable raw tx hex.
Implementation
String finalizeAndExtract(String signedPsbtBase64, String publicKeyHex) {
final bytes = base64.decode(signedPsbtBase64);
final cursor = _Cursor(bytes);
final magic = cursor.readBytes(_psbtMagic.length);
if (!_bytesEqual(magic, _psbtMagic)) {
throw StateError('failed to parse signed PSBT: bad magic bytes');
}
Uint8List? unsignedTxBytes;
while (true) {
final keyLen = cursor.readVarInt();
if (keyLen == 0) break;
final key = cursor.readBytes(keyLen);
final valueLen = cursor.readVarInt();
final value = cursor.readBytes(valueLen);
if (key.length == 1 && key[0] == 0x00) {
unsignedTxBytes = value;
}
}
if (unsignedTxBytes == null) {
throw StateError('failed to parse signed PSBT: missing global unsigned tx');
}
final unsignedTx = _parseUnsignedTx(unsignedTxBytes);
final pubkeyBytes = _hexToBytes(publicKeyHex);
final witnesses = <Uint8List>[];
for (var index = 0; index < unsignedTx.inputs.length; index++) {
Uint8List? partialSigForOurKey;
Uint8List? finalWitness;
while (true) {
final keyLen = cursor.readVarInt();
if (keyLen == 0) break;
final key = cursor.readBytes(keyLen);
final valueLen = cursor.readVarInt();
final value = cursor.readBytes(valueLen);
final keyType = key[0];
final keyData = key.sublist(1);
if (keyType == 0x02 && _bytesEqual(keyData, pubkeyBytes)) {
partialSigForOurKey = value;
} else if (keyType == 0x08) {
// PSBT_IN_FINAL_SCRIPTWITNESS (BIP174) — already finalized. 0x07 is
// PSBT_IN_FINAL_SCRIPTSIG (non-witness inputs, never used here); an
// earlier version of this file had these swapped.
finalWitness = value;
}
}
if (finalWitness != null) {
witnesses.add(finalWitness);
continue;
}
if (partialSigForOurKey == null) {
throw StateError(
'PSBT input $index has no partial signature for our pubkey — MPC signing did not cover this input',
);
}
witnesses.add(_buildWitnessStack([partialSigForOurKey, pubkeyBytes]));
}
// Output maps carry nothing we need — skip them.
for (var index = 0; index < unsignedTx.outputs.length; index++) {
while (true) {
final keyLen = cursor.readVarInt();
if (keyLen == 0) break;
cursor.readBytes(keyLen);
final valueLen = cursor.readVarInt();
cursor.readBytes(valueLen);
}
}
final finalTxBytes = _serializeFinalTx(unsignedTx, witnesses);
return _bytesToHex(finalTxBytes);
}