Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Payjoin #1806

Draft
wants to merge 14 commits into
base: update_app_deps
Choose a base branch
from
401 changes: 401 additions & 0 deletions cw_bitcoin/lib/bitcoin_payjoin.dart

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions cw_bitcoin/lib/bitcoin_receive_page_option.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ class BitcoinReceivePageOption implements ReceivePageOption {

static const silent_payments = BitcoinReceivePageOption._('Silent Payments');

static const payjoin_payments = BitcoinReceivePageOption._('Payjoin');

const BitcoinReceivePageOption._(this.value);

final String value;
Expand All @@ -19,6 +21,7 @@ class BitcoinReceivePageOption implements ReceivePageOption {
}

static const all = [
BitcoinReceivePageOption.payjoin_payments,
BitcoinReceivePageOption.silent_payments,
BitcoinReceivePageOption.p2wpkh,
BitcoinReceivePageOption.p2tr,
Expand All @@ -39,6 +42,8 @@ class BitcoinReceivePageOption implements ReceivePageOption {
return P2shAddressType.p2wpkhInP2sh;
case BitcoinReceivePageOption.silent_payments:
return SilentPaymentsAddresType.p2sp;
case BitcoinReceivePageOption.payjoin_payments:
return SegwitAddresType.p2tr;
case BitcoinReceivePageOption.p2wpkh:
default:
return SegwitAddresType.p2wpkh;
Expand Down
100 changes: 84 additions & 16 deletions cw_bitcoin/lib/bitcoin_wallet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,9 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
initialBalance: initialBalance,
seedBytes: seedBytes,
encryptionFileUtils: encryptionFileUtils,
currency:
networkParam == BitcoinNetwork.testnet ? CryptoCurrency.tbtc : CryptoCurrency.btc,
currency: networkParam == BitcoinNetwork.testnet
? CryptoCurrency.tbtc
: CryptoCurrency.btc,
alwaysScan: alwaysScan,
) {
// in a standard BIP44 wallet, mainHd derivation path = m/84'/0'/0'/0 (account 0, index unspecified here)
Expand All @@ -80,11 +81,13 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
mainHd: hd,
sideHd: accountHD.childKey(Bip32KeyIndex(1)),
network: networkParam ?? network,
masterHd: seedBytes != null ? Bip32Slip10Secp256k1.fromSeed(seedBytes) : null,
masterHd:
seedBytes != null ? Bip32Slip10Secp256k1.fromSeed(seedBytes) : null,
);

autorun((_) {
this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress;
this.walletAddresses.isEnabledAutoGenerateSubaddress =
this.isEnabledAutoGenerateSubaddress;
});
}

Expand Down Expand Up @@ -185,8 +188,10 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
walletInfo.derivationInfo ??= DerivationInfo();

// set the default if not present:
walletInfo.derivationInfo!.derivationPath ??= snp?.derivationPath ?? electrum_path;
walletInfo.derivationInfo!.derivationType ??= snp?.derivationType ?? DerivationType.electrum;
walletInfo.derivationInfo!.derivationPath ??=
snp?.derivationPath ?? electrum_path;
walletInfo.derivationInfo!.derivationType ??=
snp?.derivationType ?? DerivationType.electrum;

Uint8List? seedBytes = null;
final mnemonic = keysData.mnemonic;
Expand Down Expand Up @@ -235,8 +240,44 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
void setLedger(Ledger setLedger, LedgerDevice setLedgerDevice) {
_ledger = setLedger;
_ledgerDevice = setLedgerDevice;
_bitcoinLedgerApp =
BitcoinLedgerApp(_ledger!, derivationPath: walletInfo.derivationInfo!.derivationPath!);
_bitcoinLedgerApp = BitcoinLedgerApp(_ledger!,
derivationPath: walletInfo.derivationInfo!.derivationPath!);
}

@override
Future<PSBTTransactionBuild> buildPayjoinTransaction({
required List<BitcoinBaseOutput> outputs,
required BigInt fee,
required BasedUtxoNetwork network,
required List<UtxoWithAddress> utxos,
required Map<String, PublicKeyWithDerivationPath> publicKeys,
String? memo,
bool enableRBF = false,
BitcoinOrdering inputOrdering = BitcoinOrdering.bip69,
BitcoinOrdering outputOrdering = BitcoinOrdering.bip69,
}) async {
final psbtReadyInputs = <PSBTReadyUtxoWithAddress>[];
for (final UtxoWithAddress utxo in utxos) {
debugPrint('[+] BITCOINWALLET => UTXO.utxo - ${utxo.utxo.toString()}');
final rawTx =
await electrumClient.getTransactionHex(hash: utxo.utxo.txHash);
final publicKeyAndDerivationPath =
publicKeys[utxo.ownerDetails.address.pubKeyHash()]!;

psbtReadyInputs.add(PSBTReadyUtxoWithAddress(
utxo: utxo.utxo,
rawTx: rawTx,
ownerDetails: utxo.ownerDetails,
ownerDerivationPath: publicKeyAndDerivationPath.derivationPath,
ownerMasterFingerprint: Uint8List(0),
ownerPublicKey: publicKeyAndDerivationPath.publicKey,
));
}

final psbt = PSBTTransactionBuild(
inputs: psbtReadyInputs, outputs: outputs, enableRBF: enableRBF);

return psbt;
}

@override
Expand All @@ -251,12 +292,15 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
BitcoinOrdering inputOrdering = BitcoinOrdering.bip69,
BitcoinOrdering outputOrdering = BitcoinOrdering.bip69,
}) async {
final masterFingerprint = await _bitcoinLedgerApp!.getMasterFingerprint(_ledgerDevice!);
final masterFingerprint =
await _bitcoinLedgerApp!.getMasterFingerprint(_ledgerDevice!);

final psbtReadyInputs = <PSBTReadyUtxoWithAddress>[];
for (final utxo in utxos) {
final rawTx = await electrumClient.getTransactionHex(hash: utxo.utxo.txHash);
final publicKeyAndDerivationPath = publicKeys[utxo.ownerDetails.address.pubKeyHash()]!;
final rawTx =
await electrumClient.getTransactionHex(hash: utxo.utxo.txHash);
final publicKeyAndDerivationPath =
publicKeys[utxo.ownerDetails.address.pubKeyHash()]!;

psbtReadyInputs.add(PSBTReadyUtxoWithAddress(
utxo: utxo.utxo,
Expand All @@ -268,23 +312,47 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
));
}

final psbt =
PSBTTransactionBuild(inputs: psbtReadyInputs, outputs: outputs, enableRBF: enableRBF);
final psbt = PSBTTransactionBuild(
inputs: psbtReadyInputs, outputs: outputs, enableRBF: enableRBF);

final rawHex = await _bitcoinLedgerApp!.signPsbt(_ledgerDevice!, psbt: psbt.psbt);
final rawHex =
await _bitcoinLedgerApp!.signPsbt(_ledgerDevice!, psbt: psbt.psbt);
return BtcTransaction.fromRaw(BytesUtils.toHexString(rawHex));
}

@override
Future<String> signPsbt(String preProcessedPsbt) async {
final psbt = PsbtV2()..deserialize(base64.decode(preProcessedPsbt));
final rawHex = await _bitcoinLedgerApp!.signPsbt(
_ledgerDevice!,
psbt: psbt,
);
return BytesUtils.toHexString(rawHex);
}

@override
Future<BtcTransaction> getBtcTransactionFromPsbt(
String preProcessedPsbt) async {
final psbt = PsbtV2()..deserialize(base64.decode(preProcessedPsbt));
final rawHex = await _bitcoinLedgerApp!.signPsbt(
_ledgerDevice!,
psbt: psbt,
);
return BtcTransaction.fromRaw(BytesUtils.toHexString(rawHex));
}

@override
Future<String> signMessage(String message, {String? address = null}) async {
if (walletInfo.isHardwareWallet) {
final addressEntry = address != null
? walletAddresses.allAddresses.firstWhere((element) => element.address == address)
? walletAddresses.allAddresses
.firstWhere((element) => element.address == address)
: null;
final index = addressEntry?.index ?? 0;
final isChange = addressEntry?.isHidden == true ? 1 : 0;
final accountPath = walletInfo.derivationInfo?.derivationPath;
final derivationPath = accountPath != null ? "$accountPath/$isChange/$index" : null;
final derivationPath =
accountPath != null ? "$accountPath/$isChange/$index" : null;

final signature = await _bitcoinLedgerApp!.signMessage(_ledgerDevice!,
message: ascii.encode(message), signDerivationPath: derivationPath);
Expand Down
57 changes: 40 additions & 17 deletions cw_bitcoin/lib/electrum_transaction_info.dart
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ class ElectrumTransactionInfo extends TransactionInfo {
this.to = to;
}

factory ElectrumTransactionInfo.fromElectrumVerbose(Map<String, Object> obj, WalletType type,
factory ElectrumTransactionInfo.fromElectrumVerbose(
Map<String, Object> obj, WalletType type,
{required List<BitcoinAddressRecord> addresses, required int height}) {
final addressesSet = addresses.map((addr) => addr.address).toSet();
final id = obj['txid'] as String;
Expand All @@ -64,18 +65,22 @@ class ElectrumTransactionInfo extends TransactionInfo {
for (dynamic vin in vins) {
final vout = vin['vout'] as int;
final out = vin['tx']['vout'][vout] as Map;
final outAddresses = (out['scriptPubKey']['addresses'] as List<Object>?)?.toSet();
inputsAmount += stringDoubleToBitcoinAmount((out['value'] as double? ?? 0).toString());
final outAddresses =
(out['scriptPubKey']['addresses'] as List<Object>?)?.toSet();
inputsAmount += stringDoubleToBitcoinAmount(
(out['value'] as double? ?? 0).toString());

if (outAddresses?.intersection(addressesSet).isNotEmpty ?? false) {
direction = TransactionDirection.outgoing;
}
}

for (dynamic out in vout) {
final outAddresses = out['scriptPubKey']['addresses'] as List<Object>? ?? [];
final outAddresses =
out['scriptPubKey']['addresses'] as List<Object>? ?? [];
final ntrs = outAddresses.toSet().intersection(addressesSet);
final value = stringDoubleToBitcoinAmount((out['value'] as double? ?? 0.0).toString());
final value = stringDoubleToBitcoinAmount(
(out['value'] as double? ?? 0.0).toString());
totalOutAmount += value;

if ((direction == TransactionDirection.incoming && ntrs.isNotEmpty) ||
Expand All @@ -97,9 +102,21 @@ class ElectrumTransactionInfo extends TransactionInfo {
confirmations: confirmations);
}

static bool isMine(
Script script,
BasedUtxoNetwork network, {
required Set<String> addresses,
}) {
final derivedAddress = addressFromOutputScript(script, network);
return addresses.contains(derivedAddress);
}

factory ElectrumTransactionInfo.fromElectrumBundle(
ElectrumTransactionBundle bundle, WalletType type, BasedUtxoNetwork network,
{required Set<String> addresses, int? height}) {
ElectrumTransactionBundle bundle,
WalletType type,
BasedUtxoNetwork network,
{required Set<String> addresses,
int? height}) {
final date = bundle.time != null
? DateTime.fromMillisecondsSinceEpoch(bundle.time! * 1000)
: DateTime.now();
Expand All @@ -115,16 +132,19 @@ class ElectrumTransactionInfo extends TransactionInfo {
final inputTransaction = bundle.ins[i];
final outTransaction = inputTransaction.outputs[input.txIndex];
inputAmount += outTransaction.amount.toInt();
if (addresses.contains(addressFromOutputScript(outTransaction.scriptPubKey, network))) {
if (addresses.contains(
addressFromOutputScript(outTransaction.scriptPubKey, network))) {
direction = TransactionDirection.outgoing;
inputAddresses.add(addressFromOutputScript(outTransaction.scriptPubKey, network));
inputAddresses
.add(addressFromOutputScript(outTransaction.scriptPubKey, network));
}
}

final receivedAmounts = <int>[];
for (final out in bundle.originalTransaction.outputs) {
totalOutAmount += out.amount.toInt();
final addressExists = addresses.contains(addressFromOutputScript(out.scriptPubKey, network));
final addressExists = addresses
.contains(addressFromOutputScript(out.scriptPubKey, network));
outputAddresses.add(addressFromOutputScript(out.scriptPubKey, network));

if (addressExists) {
Expand Down Expand Up @@ -157,7 +177,8 @@ class ElectrumTransactionInfo extends TransactionInfo {
confirmations: bundle.confirmations);
}

factory ElectrumTransactionInfo.fromJson(Map<String, dynamic> data, WalletType type) {
factory ElectrumTransactionInfo.fromJson(
Map<String, dynamic> data, WalletType type) {
final inputAddresses = data['inputAddresses'] as List<dynamic>? ?? [];
final outputAddresses = data['outputAddresses'] as List<dynamic>? ?? [];
final unspents = data['unspents'] as List<dynamic>? ?? [];
Expand All @@ -172,14 +193,16 @@ class ElectrumTransactionInfo extends TransactionInfo {
date: DateTime.fromMillisecondsSinceEpoch(data['date'] as int),
isPending: data['isPending'] as bool,
confirmations: data['confirmations'] as int,
inputAddresses:
inputAddresses.isEmpty ? [] : inputAddresses.map((e) => e.toString()).toList(),
outputAddresses:
outputAddresses.isEmpty ? [] : outputAddresses.map((e) => e.toString()).toList(),
inputAddresses: inputAddresses.isEmpty
? []
: inputAddresses.map((e) => e.toString()).toList(),
outputAddresses: outputAddresses.isEmpty
? []
: outputAddresses.map((e) => e.toString()).toList(),
to: data['to'] as String?,
unspents: unspents
.map((unspent) =>
BitcoinSilentPaymentsUnspent.fromJSON(null, unspent as Map<String, dynamic>))
.map((unspent) => BitcoinSilentPaymentsUnspent.fromJSON(
null, unspent as Map<String, dynamic>))
.toList(),
);
}
Expand Down
Loading