feat: implement silent payment derivation paths and master HD wallet integration (#2708)

* feat: implement silent payment derivation paths and master HD wallet integration

- Added default derivation paths for silent payments (mainnet and testnet).
- Introduced `_masterHD` for handling seed-based key derivations.
- Updated silent payment address record to include spend derivation path.
- Refactored UTXO handling and scanning logic to support multiple receivers with unique derivation paths.
- Improved logging for better debugging of silent payment workflows.

* refactor: remove debug prints
This commit is contained in:
Konstantin Ullrich
2025-12-11 16:01:24 +02:00
committed by GitHub
parent 65c4ef90b6
commit 89863e3bec
4 changed files with 87 additions and 49 deletions
+7 -1
View File
@@ -1,4 +1,5 @@
import 'dart:convert';
import 'package:cw_bitcoin/electrum_derivations.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:mobx/mobx.dart';
@@ -133,7 +134,8 @@ class BitcoinSilentPaymentAddressRecord extends BaseBitcoinAddressRecord {
required this.silentPaymentTweak,
required super.network,
required super.type,
}) : super();
this.spendDerivationPath = SILENT_PAYMENTS_SPEND_PATH_TESTNET,
});
factory BitcoinSilentPaymentAddressRecord.fromJSON(String jsonSource,
{BasedUtxoNetwork? network}) {
@@ -155,10 +157,13 @@ class BitcoinSilentPaymentAddressRecord extends BaseBitcoinAddressRecord {
? BitcoinAddressType.values
.firstWhere((type) => type.toString() == decoded['type'] as String)
: SilentPaymentsAddresType.p2sp,
spendDerivationPath:
decoded['spend_derivation_path'] as String? ?? SILENT_PAYMENTS_SPEND_PATH_TESTNET,
);
}
final String? silentPaymentTweak;
final String spendDerivationPath;
@override
String toJSON() => json.encode({
@@ -172,5 +177,6 @@ class BitcoinSilentPaymentAddressRecord extends BaseBitcoinAddressRecord {
'type': type.toString(),
'network': network?.value,
'silent_payment_tweak': silentPaymentTweak,
'spend_derivation_path': spendDerivationPath,
});
}
+5
View File
@@ -1,5 +1,10 @@
import 'package:cw_core/wallet_info.dart';
const SILENT_PAYMENTS_SCAN_PATH = "m/352'/0'/0'/1'/0";
const SILENT_PAYMENTS_SPEND_PATH = "m/352'/0'/0'/0'/0";
const SILENT_PAYMENTS_SCAN_PATH_TESTNET = "m/352'/1'/0'/1'/0";
const SILENT_PAYMENTS_SPEND_PATH_TESTNET = "m/352'/1'/0'/0'/0";
Map<DerivationType, List<DerivationInfo>> electrum_derivations = {
DerivationType.electrum: [
DerivationInfo(
+70 -46
View File
@@ -4,14 +4,6 @@ import 'dart:io';
import 'dart:isolate';
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:cw_core/hardware/hardware_wallet_service.dart';
import 'package:cw_core/root_dir.dart';
import 'package:cw_core/utils/proxy_wrapper.dart';
import 'package:cw_bitcoin/bitcoin_amount_format.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_bitcoin/bitcoin_wallet.dart';
import 'package:cw_bitcoin/litecoin_wallet.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:blockchain_utils/blockchain_utils.dart';
import 'package:collection/collection.dart';
import 'package:cw_bitcoin/address_from_output.dart';
@@ -19,6 +11,7 @@ import 'package:cw_bitcoin/bitcoin_address_record.dart';
import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart';
import 'package:cw_bitcoin/bitcoin_transaction_priority.dart';
import 'package:cw_bitcoin/bitcoin_unspent.dart';
import 'package:cw_bitcoin/bitcoin_wallet.dart';
import 'package:cw_bitcoin/bitcoin_wallet_keys.dart';
import 'package:cw_bitcoin/electrum.dart' as electrum;
import 'package:cw_bitcoin/electrum_balance.dart';
@@ -27,31 +20,37 @@ import 'package:cw_bitcoin/electrum_transaction_history.dart';
import 'package:cw_bitcoin/electrum_transaction_info.dart';
import 'package:cw_bitcoin/electrum_wallet_addresses.dart';
import 'package:cw_bitcoin/exceptions.dart';
import 'package:cw_bitcoin/litecoin_wallet.dart';
import 'package:cw_bitcoin/pending_bitcoin_transaction.dart';
import 'package:cw_bitcoin/utils.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/encryption_file_utils.dart';
import 'package:cw_core/get_height_by_date.dart';
import 'package:cw_core/hardware/hardware_wallet_service.dart';
import 'package:cw_core/node.dart';
import 'package:cw_core/output_info.dart';
import 'package:cw_core/pathForWallet.dart';
import 'package:cw_core/pending_transaction.dart';
import 'package:cw_core/root_dir.dart';
import 'package:cw_core/sync_status.dart';
import 'package:cw_core/transaction_direction.dart';
import 'package:cw_core/transaction_priority.dart';
import 'package:cw_core/unspent_coin_type.dart';
import 'package:cw_core/unspent_coins_info.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/utils/proxy_wrapper.dart';
import 'package:cw_core/utils/socket_health_logger.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_keys_file.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:cw_core/unspent_coin_type.dart';
import 'package:cw_core/output_info.dart';
import 'package:flutter/foundation.dart';
import 'package:hex/hex.dart';
import 'package:hive/hive.dart';
import 'package:mobx/mobx.dart';
import 'package:rxdart/subjects.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:sp_scanner/sp_scanner.dart';
import 'package:hex/hex.dart';
import 'package:cw_core/utils/socket_health_logger.dart';
part 'electrum_wallet.g.dart';
@@ -76,8 +75,9 @@ abstract class ElectrumWalletBase
ElectrumBalance? initialBalance,
CryptoCurrency? currency,
bool? alwaysScan,
}) : accountHD =
getAccountHDWallet(currency, network, seedBytes, xpub, derivationInfo, walletInfo.hardwareWalletType),
}) : _masterHD = getMasterHD(seedBytes, network, walletInfo.hardwareWalletType),
accountHD = getAccountHDWallet(
currency, network, seedBytes, xpub, derivationInfo, walletInfo.hardwareWalletType),
syncStatus = NotConnectedSyncStatus(),
_password = password,
_feeRates = <int>[],
@@ -171,9 +171,18 @@ abstract class ElectrumWalletBase
}
}
static Bip32Slip10Secp256k1? getMasterHD(Uint8List? seedBytes,
[BasedUtxoNetwork? network, HardwareWalletType? hardwareWalletType]) {
if (seedBytes == null) return null;
return Bip32Slip10Secp256k1.fromSeed(
seedBytes, network != null ? getKeyNetVersion(network, hardwareWalletType) : null);
}
@observable
bool? alwaysScan;
final Bip32Slip10Secp256k1? _masterHD;
final Bip32Slip10Secp256k1 accountHD;
final String? _mnemonic;
@@ -383,6 +392,7 @@ abstract class ElectrumWalletBase
ScanData(
sendPort: receivePort.sendPort,
silentAddress: walletAddresses.silentAddress!,
masterHD: _masterHD!,
network: network,
height: height,
chainTip: chainTip,
@@ -745,10 +755,10 @@ abstract class ElectrumWalletBase
if (utx.bitcoinAddressRecord is BitcoinSilentPaymentAddressRecord) {
final unspentAddress = utx.bitcoinAddressRecord as BitcoinSilentPaymentAddressRecord;
privkey = walletAddresses.silentAddress!.b_spend.tweakAdd(
BigintUtils.fromBytes(
BytesUtils.fromHexString(unspentAddress.silentPaymentTweak!),
),
privkey = ECPrivate.fromHex(
_masterHD!.derivePath(unspentAddress.spendDerivationPath).privateKey.toHex())
.tweakAdd(
BigintUtils.fromBytes(BytesUtils.fromHexString(unspentAddress.silentPaymentTweak!)),
);
spendsSilentPayment = true;
isSilentPayment = true;
@@ -772,7 +782,6 @@ abstract class ElectrumWalletBase
pubKeyHex = hd.childKey(Bip32KeyIndex(utx.bitcoinAddressRecord.index)).publicKey.toHex();
}
final derivationPath =
"${_hardenedDerivationPath(derivationInfo.derivationPath ?? electrum_path)}"
"/${utx.bitcoinAddressRecord.isHidden ? "1" : "0"}"
@@ -1141,7 +1150,6 @@ abstract class ElectrumWalletBase
bool hasSilentPayment = false,
UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any,
}) async {
final utxoDetailsAll = _createUTXOS(
sendAll: true,
paysToSilentPayment: hasSilentPayment,
@@ -2768,8 +2776,8 @@ abstract class ElectrumWalletBase
@override
String formatCryptoAmount(String amount) {
final amountInt = int.parse(amount);
return bitcoinAmountToString(amount: amountInt);
final amountBigInt = BigInt.parse(amount);
return currency.formatAmount(amountBigInt);
}
/// Checks the health of the socket connection
@@ -2930,6 +2938,7 @@ class ScanNode {
class ScanData {
final SendPort sendPort;
final SilentPaymentOwner silentAddress;
final Bip32Slip10Secp256k1 masterHD;
final int height;
final ScanNode? node;
final BasedUtxoNetwork network;
@@ -2945,6 +2954,7 @@ class ScanData {
ScanData({
required this.sendPort,
required this.silentAddress,
required this.masterHD,
required this.height,
required this.node,
required this.network,
@@ -2962,6 +2972,7 @@ class ScanData {
return ScanData(
sendPort: scanData.sendPort,
silentAddress: scanData.silentAddress,
masterHD: scanData.masterHD,
height: newHeight,
node: scanData.node,
network: scanData.network,
@@ -3003,16 +3014,29 @@ Future<void> _handleScanSilentPayments(ScanData scanData) async {
log("connected to ${node.toString()}", LogLevel.info);
final receiver = Receiver(
scanData.silentAddress.b_scan.toHex(),
scanData.silentAddress.B_spend.toHex(),
scanData.network == BitcoinNetwork.testnet,
scanData.labelIndexes,
scanData.labelIndexes.length,
);
final receivers = [
Receiver(
scanData.silentAddress.b_scan.toHex(),
scanData.silentAddress.B_spend.toHex(),
scanData.network == BitcoinNetwork.testnet,
scanData.labelIndexes,
scanData.labelIndexes.length,
),
Receiver(
scanData.masterHD.derivePath(SILENT_PAYMENTS_SCAN_PATH_TESTNET).privateKey.toHex(),
scanData.masterHD.derivePath(SILENT_PAYMENTS_SPEND_PATH_TESTNET).publicKey.toHex(),
scanData.network == BitcoinNetwork.testnet,
scanData.labelIndexes,
scanData.labelIndexes.length,
)
];
log(
"using receiver: b_scan: ${scanData.silentAddress.b_scan.toHex()}, B_scan: ${scanData.silentAddress.B_spend.toHex()}, b_spend: ${scanData.silentAddress.B_spend.toHex()}, B_spend: ${scanData.silentAddress.B_spend.toHex()}, network: ${scanData.network.value}, labelIndexes: ${scanData.labelIndexes}",
"using receiver: b_scan: ${scanData.silentAddress.b_scan.toHex()}, b_spend: ${scanData.silentAddress.B_spend.toHex()}, network: ${scanData.network.value}, labelIndexes: ${scanData.labelIndexes}",
LogLevel.info,
);
log(
"using receiver: b_scan: ${receivers[1].bScan}, b_spend: ${receivers[1].BSpend}, network: ${scanData.network.value}, labelIndexes: ${scanData.labelIndexes}",
LogLevel.info,
);
@@ -3140,22 +3164,20 @@ Future<void> _handleScanSilentPayments(ScanData scanData) async {
final tweak = tweakData.tweak;
try {
final addToWallet = {};
final addToWallet = <String, dynamic>{};
// receivers.forEach((receiver) {
// NOTE: scanOutputs, from sp_scanner package, called from rust here
final scanResult = scanOutputs([outputPubkeys.keys.toList()], tweak, receiver);
receivers.forEach((receiver) {
// NOTE: scanOutputs, from sp_scanner package, called from rust here
final scanResult = scanOutputs([outputPubkeys.keys.toList()], tweak, receiver);
if (scanResult.isEmpty) {
continue;
}
if (scanResult.isEmpty) return;
if (addToWallet[receiver.BSpend] == null) {
addToWallet[receiver.BSpend] = scanResult;
} else {
addToWallet[receiver.BSpend].addAll(scanResult);
}
// });
if (addToWallet[receiver.BSpend] == null) {
addToWallet[receiver.BSpend] = scanResult;
} else {
addToWallet[receiver.BSpend].addAll(scanResult);
}
});
if (addToWallet.isEmpty) {
// no results tx, continue to next tx
@@ -3237,9 +3259,8 @@ Future<void> _handleScanSilentPayments(ScanData scanData) async {
final pos = matchingOutput.vout;
final spent = matchingOutput.spendingInput;
// final matchingSPWallet = scanData.silentPaymentsWallets.firstWhere(
// (receiver) => receiver.B_spend.toHex() == BSpend.toString(),
// );
final matchingReceiver =
receivers.indexWhere((receiver) => receiver.BSpend == BSpend);
// final labelIndex = labelValue != null ? scanData.labels[label] : 0;
// final balance = ElectrumBalance();
@@ -3255,6 +3276,9 @@ Future<void> _handleScanSilentPayments(ScanData scanData) async {
type: SegwitAddresType.p2tr,
txCount: 1,
balance: amount,
spendDerivationPath: matchingReceiver == 0
? SILENT_PAYMENTS_SPEND_PATH
: SILENT_PAYMENTS_SPEND_PATH_TESTNET,
);
final unspent = BitcoinSilentPaymentsUnspent(
@@ -3,6 +3,7 @@ import 'dart:io' show Platform;
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:blockchain_utils/blockchain_utils.dart';
import 'package:cw_bitcoin/bitcoin_address_record.dart';
import 'package:cw_bitcoin/electrum_derivations.dart';
import 'package:cw_core/unspent_coin_type.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_bitcoin/bitcoin_unspent.dart';
@@ -75,8 +76,10 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
super(walletInfo) {
if (masterHd != null) {
silentAddress = SilentPaymentOwner.fromPrivateKeys(
b_scan: ECPrivate.fromHex(masterHd.derivePath(SCAN_PATH).privateKey.toHex()),
b_spend: ECPrivate.fromHex(masterHd.derivePath(SPEND_PATH).privateKey.toHex()),
b_scan:
ECPrivate.fromHex(masterHd.derivePath(SILENT_PAYMENTS_SCAN_PATH).privateKey.toHex()),
b_spend:
ECPrivate.fromHex(masterHd.derivePath(SILENT_PAYMENTS_SPEND_PATH).privateKey.toHex()),
network: network,
);