Files
hash-wallet/cw_bitcoin/lib/psbt/transaction_builder.dart
T
Konstantin UllrichandGitHub 0d353a0932 feat: enhance hardware wallet compatibility and improve transaction handling (#3517)
* feat: enhance hardware wallet compatibility and improve transaction handling

- Updated hardware wallet services for Bitcoin and Litecoin (Ledger & Trezor).
- Added `LitecoinTrezorService` for signing and managing Litecoin transactions.
- Improved `PSBTTransaction` builder to handle change outputs and derivation paths.
- Fixed exception handling in `wallet_hardware_restore_view_model`.
- Refactored output and input structures to ensure consistency and extend compatibility.

* refactor: address comments

* refactor: address comments

* refactor: address comments
2026-09-01 19:42:33 +08:00

177 lines
5.4 KiB
Dart

import "dart:convert";
import "dart:typed_data";
import "package:bitcoin_base/bitcoin_base.dart";
import "package:convert/convert.dart";
import "package:cw_core/output_info.dart";
import "package:cw_core/utils/print_verbose.dart";
import "package:ledger_bitcoin/psbt.dart";
class PSBTTransactionBuild {
PSBTTransactionBuild({
required List<PSBTReadyUtxoWithAddress> inputs,
required List<PSBTReadyBitcoinOutput> outputs,
bool enableRBF = true,
int locktime = 0,
}) {
psbt.setGlobalTxVersion(2);
psbt.setGlobalFallbackLocktime(locktime);
psbt.setGlobalInputCount(inputs.length);
psbt.setGlobalOutputCount(outputs.length);
for (var i = 0; i < inputs.length; i++) {
final input = inputs[i];
psbt.setInputPreviousTxId(
i,
Uint8List.fromList(hex.decode(input.utxo.txHash).reversed.toList()),
);
psbt.setInputOutputIndex(i, input.utxo.vout);
psbt.setInputSequence(i, enableRBF ? 0xfffffffd : 0xffffffff);
if (input.utxo.isSegwit()) {
setInputSegwit(i, input);
} else if (input.utxo.isP2shSegwit()) {
setInputP2shSegwit(i, input);
} else if (input.utxo.isP2tr()) {
// ToDo: (Konsti) Handle Taproot Inputs
} else {
setInputP2pkh(i, input);
}
}
for (var i = 0; i < outputs.length; i++) {
final output = outputs[i];
psbt.setOutputScript(i, Uint8List.fromList(output.address.toScriptPubKey().toBytes()));
psbt.setOutputAmount(i, output.value.toInt());
if (output.isChange &&
output.changeDerivationPath != null &&
output.changePublicKey != null &&
output.changeMasterFingerprint != null) {
psbt.setOutputBip32Derivation(
i,
Uint8List.fromList(hex.decode(output.changePublicKey!)),
output.changeMasterFingerprint!,
BIPPath.fromString(output.changeDerivationPath!).toPathArray(),
);
}
if (output.outputInfo != null) {
try {
final cwOutput = output.outputInfo!;
if (cwOutput.extra.containsKey("bip353_name") &&
cwOutput.extra.containsKey("bip353_proof")) {
final bip353Name = utf8.encode(cwOutput.extra["bip353_name"] as String);
final bip353Proof = base64.decode(cwOutput.extra["bip353_proof"] as String);
if (bip353Name.length > 255) {
printV("BIP353 name is too long, skipping");
continue;
}
final proof = Uint8List.fromList([
bip353Name.length,
...bip353Name,
...bip353Proof,
]);
psbt.setOutputDNSSECProof(i, proof);
}
} catch (e) {
printV("Error setting DNSSEC proof: $e");
}
}
}
}
final PsbtV2 psbt = PsbtV2();
void setInputP2pkh(int i, PSBTReadyUtxoWithAddress input) {
psbt.setInputNonWitnessUtxo(i, Uint8List.fromList(hex.decode(input.rawTx)));
psbt.setInputBip32Derivation(
i,
Uint8List.fromList(hex.decode(input.ownerPublicKey)),
input.ownerMasterFingerprint,
BIPPath.fromString(input.ownerDerivationPath).toPathArray(),
);
}
void setInputSegwit(int i, PSBTReadyUtxoWithAddress input) {
psbt.setInputNonWitnessUtxo(i, Uint8List.fromList(hex.decode(input.rawTx)));
psbt.setInputBip32Derivation(
i,
Uint8List.fromList(hex.decode(input.ownerPublicKey)),
input.ownerMasterFingerprint,
BIPPath.fromString(input.ownerDerivationPath).toPathArray(),
);
psbt.setInputWitnessUtxo(
i,
Uint8List.fromList(bigIntToUint64LE(input.utxo.value)),
Uint8List.fromList(input.ownerDetails.address.toScriptPubKey().toBytes()),
);
}
void setInputP2shSegwit(int i, PSBTReadyUtxoWithAddress input) {
psbt.setInputNonWitnessUtxo(i, Uint8List.fromList(hex.decode(input.rawTx)));
psbt.setInputBip32Derivation(
i,
Uint8List.fromList(hex.decode(input.ownerPublicKey)),
input.ownerMasterFingerprint,
BIPPath.fromString(input.ownerDerivationPath).toPathArray(),
);
psbt.setInputRedeemScript(
i,
Uint8List.fromList(input.ownerDetails.address.toScriptPubKey().toBytes()),
);
psbt.setInputWitnessUtxo(
i,
Uint8List.fromList(bigIntToUint64LE(input.utxo.value)),
Uint8List.fromList(input.ownerDetails.address.toScriptPubKey().toBytes()),
);
}
}
class PSBTReadyUtxoWithAddress extends UtxoWithAddress {
PSBTReadyUtxoWithAddress({
required super.utxo,
required this.rawTx,
required super.ownerDetails,
required this.ownerDerivationPath,
required this.ownerMasterFingerprint,
required this.ownerPublicKey,
});
final String rawTx;
final String ownerDerivationPath;
final Uint8List ownerMasterFingerprint;
final String ownerPublicKey;
}
class PSBTReadyBitcoinOutput extends BitcoinOutput {
PSBTReadyBitcoinOutput({
required super.address,
required super.value,
super.isSilentPayment = false,
super.isChange = false,
this.changeDerivationPath,
this.changeMasterFingerprint,
this.changePublicKey,
this.outputInfo,
});
factory PSBTReadyBitcoinOutput.fromOutput(BitcoinOutput output) => PSBTReadyBitcoinOutput(
address: output.address,
value: output.value,
isSilentPayment: output.isSilentPayment,
isChange: output.isChange,
);
final String? changeDerivationPath;
final Uint8List? changeMasterFingerprint;
final String? changePublicKey;
final OutputInfo? outputInfo;
}