diff --git a/.gitignore b/.gitignore
index 970241189..ac0d42742 100644
--- a/.gitignore
+++ b/.gitignore
@@ -176,6 +176,7 @@ integration_test/playground.dart
# Monero.dart (Monero_C)
scripts/monero_c
+scripts/android/app_env.fish
# iOS generated framework bin
ios/MoneroWallet.framework/MoneroWallet
ios/WowneroWallet.framework/WowneroWallet
diff --git a/PRIVACY.md b/PRIVACY.md
index 76cfcc4d3..a5c8eddfb 100644
--- a/PRIVACY.md
+++ b/PRIVACY.md
@@ -5,7 +5,7 @@ Last modified: January 24, 2024
Introduction
============
- Cake Labs LLC ("Cake Labs", "Company", or "We") respect your privacy and are committed to protecting it through our compliance with this policy.
+ Cake Labs LLC ("Cake Labs", "Company", or "We") respects your privacy and are committed to protecting it through our compliance with this policy.
This policy describes the types of information we may collect from you or that you may provide when you use the App (our "App") and our practices for collecting, using, maintaining, protecting, and disclosing that information.
@@ -13,7 +13,7 @@ Introduction
- On this App.
- In email, text, and other electronic messages between you and this App.
It does not apply to information collected by:
- - Us offline or through any other means, including on any other App operated by Company or any third party (including our affiliates and subsidiaries); or
+ - Us offline or through any other means, including on any other App operated by the Company or any third party (including our affiliates and subsidiaries); or
- Any third party (including our affiliates and subsidiaries), including through any application or content (including advertising) that may link to or be accessible from or on the App.
Please read this policy carefully to understand our policies and practices regarding your information and how we will treat it. If you do not agree with our policies and practices, you have the choice to not use the App. By accessing or using this App, you agree to this privacy policy. This policy may change from time to time. Your continued use of this App after we make changes is deemed to be acceptance of those changes, so please check the policy periodically for updates.
diff --git a/README.md b/README.md
index 1c28f92a2..078c4437e 100644
--- a/README.md
+++ b/README.md
@@ -18,7 +18,7 @@
# Cake Wallet
-Cake Wallet is an open source, non-custodial, and private multi-currency crypto wallet for Android, iOS, macOS, and Linux.
+[Cake Wallet](https://cakewallet.com) is an open-source, non-custodial, and private multi-currency crypto wallet for Android, iOS, macOS, and Linux.
Cake Wallet includes support for several cryptocurrencies, including:
* Monero (XMR)
@@ -26,7 +26,7 @@ Cake Wallet includes support for several cryptocurrencies, including:
* Ethereum (ETH)
* Litecoin (LTC)
* Bitcoin Cash (BCH)
-* Polygon (MATIC)
+* Polygon (Pol)
* Solana (SOL)
* Nano (XNO)
* Haven (XHV)
@@ -44,7 +44,7 @@ Cake Wallet includes support for several cryptocurrencies, including:
* Create several wallets
* Select your own custom nodes/servers
* Address book
-* Backup to external location or iCloud
+* Backup to an external location or iCloud
* Send to OpenAlias, Unstoppable Domains, Yats, and FIO Crypto Handles
* Set desired network fee level
* Store local transaction notes
@@ -161,7 +161,7 @@ The only parts to be translated, if needed, are the values m and s after the var
4. Add the language to `lib/entities/language_service.dart` under both `supportedLocales` and `localeCountryCode`. Use the name of the language in the local language and in English in parentheses after for `supportedLocales`. Use the [ISO 3166-1 alpha-3 code](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-3) for `localeCountryCode`. You must choose one country, so choose the country with the most native speakers of this language or is otherwise best associated with this language.
-5. Add a relevant flag to `assets/images/flags/XXXX.png`, replacing XXXX with the 3 letters localeCountryCode. The image must be 42x26 pixels with a 3 pixels of transparent margin on all 4 sides. You can resize the flag with [paint.net](https://www.getpaint.net/) to 36x20 pixels, expand the canvas to 42x26 pixels with the flag anchored in the middle, and then manually delete the 3 pixels on each side to make transparent. Or you can use another program like Photoshop.
+5. Add a relevant flag to `assets/images/flags/XXXX.png`, replacing XXXX with the 3 letters localeCountryCode. The image must be 42x26 pixels with 3 pixels of transparent margin on all 4 sides. You can resize the flag with [paint.net](https://www.getpaint.net/) to 36x20 pixels, expand the canvas to 42x26 pixels with the flag anchored in the middle, and then manually delete the 3 pixels on each side to make it transparent. Or you can use another program like Photoshop.
6. Add the new language code to `tool/utils/translation/translation_constants.dart`
diff --git a/SECURITY.md b/SECURITY.md
index a1b489b76..e7c6baa02 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -9,4 +9,4 @@ If you need to report a vulnerability, please either:
## Supported Versions
-As we don't maintain prevoius versions of the app, only the latest release for each platform is supported and any updates will bump the version number.
+As we don't maintain previous versions of the app, only the latest release for each platform is supported and any updates will bump the version number.
diff --git a/android/app/src/main/AndroidManifestBase.xml b/android/app/src/main/AndroidManifestBase.xml
index 98bbb1236..5b080e3ec 100644
--- a/android/app/src/main/AndroidManifestBase.xml
+++ b/android/app/src/main/AndroidManifestBase.xml
@@ -14,7 +14,8 @@
-
+
+
diff --git a/build-guide-linux.md b/build-guide-linux.md
index 50ecc76fe..55264fbfa 100644
--- a/build-guide-linux.md
+++ b/build-guide-linux.md
@@ -55,7 +55,7 @@ Need to install flutter. For this please check section [How to install flutter o
### 3. Verify Installations
-Verify that the Flutter have been correctly installed on your system with the following command:
+Verify that the Flutter has been correctly installed on your system with the following command:
`$ flutter doctor`
@@ -163,7 +163,7 @@ And then export bundle:
`$ flatpak build-bundle export cake_wallet.flatpak com.cakewallet.CakeWallet`
-Result file: `cake_wallet.flatpak` should be generated in current directory.
+Result file: `cake_wallet.flatpak` should be generated in the current directory.
For install generated flatpak file use:
diff --git a/cw_bitcoin/lib/address_from_output.dart b/cw_bitcoin/lib/address_from_output.dart
deleted file mode 100644
index 73bc101c4..000000000
--- a/cw_bitcoin/lib/address_from_output.dart
+++ /dev/null
@@ -1,23 +0,0 @@
-import 'package:bitcoin_base/bitcoin_base.dart';
-
-String addressFromOutputScript(Script script, BasedUtxoNetwork network) {
- try {
- switch (script.getAddressType()) {
- case P2pkhAddressType.p2pkh:
- return P2pkhAddress.fromScriptPubkey(script: script).toAddress(network);
- case P2shAddressType.p2pkInP2sh:
- return P2shAddress.fromScriptPubkey(script: script).toAddress(network);
- case SegwitAddresType.p2wpkh:
- return P2wpkhAddress.fromScriptPubkey(script: script).toAddress(network);
- case P2shAddressType.p2pkhInP2sh:
- return P2shAddress.fromScriptPubkey(script: script).toAddress(network);
- case SegwitAddresType.p2wsh:
- return P2wshAddress.fromScriptPubkey(script: script).toAddress(network);
- case SegwitAddresType.p2tr:
- return P2trAddress.fromScriptPubkey(script: script).toAddress(network);
- default:
- }
- } catch (_) {}
-
- return '';
-}
diff --git a/cw_bitcoin/lib/bitcoin_address_record.dart b/cw_bitcoin/lib/bitcoin_address_record.dart
index 7e4b5f58f..d4dd8319f 100644
--- a/cw_bitcoin/lib/bitcoin_address_record.dart
+++ b/cw_bitcoin/lib/bitcoin_address_record.dart
@@ -1,34 +1,39 @@
import 'dart:convert';
import 'package:bitcoin_base/bitcoin_base.dart';
+import 'package:cw_bitcoin/electrum_wallet_addresses.dart';
abstract class BaseBitcoinAddressRecord {
BaseBitcoinAddressRecord(
this.address, {
required this.index,
- this.isHidden = false,
+ bool isChange = false,
int txCount = 0,
int balance = 0,
String name = '',
bool isUsed = false,
required this.type,
- required this.network,
+ bool? isHidden,
}) : _txCount = txCount,
_balance = balance,
_name = name,
- _isUsed = isUsed;
+ _isUsed = isUsed,
+ _isHidden = isHidden ?? isChange,
+ _isChange = isChange;
@override
bool operator ==(Object o) => o is BaseBitcoinAddressRecord && address == o.address;
final String address;
- bool isHidden;
+ bool _isHidden;
+ bool get isHidden => _isHidden;
+ final bool _isChange;
+ bool get isChange => _isChange;
final int index;
int _txCount;
int _balance;
String _name;
bool _isUsed;
- BasedUtxoNetwork? network;
int get txCount => _txCount;
@@ -42,7 +47,12 @@ abstract class BaseBitcoinAddressRecord {
bool get isUsed => _isUsed;
- void setAsUsed() => _isUsed = true;
+ void setAsUsed() {
+ _isUsed = true;
+ // TODO: check is hidden flow on addr list
+ _isHidden = true;
+ }
+
void setNewName(String label) => _name = label;
int get hashCode => address.hashCode;
@@ -53,27 +63,43 @@ abstract class BaseBitcoinAddressRecord {
}
class BitcoinAddressRecord extends BaseBitcoinAddressRecord {
+ final BitcoinDerivationInfo derivationInfo;
+ final CWBitcoinDerivationType derivationType;
+
BitcoinAddressRecord(
super.address, {
required super.index,
- super.isHidden = false,
+ required this.derivationInfo,
+ required this.derivationType,
+ super.isHidden,
+ super.isChange = false,
super.txCount = 0,
super.balance = 0,
super.name = '',
super.isUsed = false,
required super.type,
String? scriptHash,
- required super.network,
- }) : scriptHash = scriptHash ??
- (network != null ? BitcoinAddressUtils.scriptHash(address, network: network) : null);
+ BasedUtxoNetwork? network,
+ }) {
+ if (scriptHash == null && network == null) {
+ throw ArgumentError('either scriptHash or network must be provided');
+ }
- factory BitcoinAddressRecord.fromJSON(String jsonSource, {BasedUtxoNetwork? network}) {
+ this.scriptHash = scriptHash ?? BitcoinAddressUtils.scriptHash(address, network: network!);
+ }
+
+ factory BitcoinAddressRecord.fromJSON(String jsonSource) {
final decoded = json.decode(jsonSource) as Map;
return BitcoinAddressRecord(
decoded['address'] as String,
index: decoded['index'] as int,
+ derivationInfo: BitcoinDerivationInfo.fromJSON(
+ decoded['derivationInfo'] as Map,
+ ),
+ derivationType: CWBitcoinDerivationType.values[decoded['derivationType'] as int],
isHidden: decoded['isHidden'] as bool? ?? false,
+ isChange: decoded['isChange'] as bool? ?? false,
isUsed: decoded['isUsed'] as bool? ?? false,
txCount: decoded['txCount'] as int? ?? 0,
name: decoded['name'] as String? ?? '',
@@ -83,23 +109,19 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord {
.firstWhere((type) => type.toString() == decoded['type'] as String)
: SegwitAddresType.p2wpkh,
scriptHash: decoded['scriptHash'] as String?,
- network: network,
);
}
- String? scriptHash;
-
- String getScriptHash(BasedUtxoNetwork network) {
- if (scriptHash != null) return scriptHash!;
- scriptHash = BitcoinAddressUtils.scriptHash(address, network: network);
- return scriptHash!;
- }
+ late String scriptHash;
@override
String toJSON() => json.encode({
'address': address,
'index': index,
+ 'derivationInfo': derivationInfo.toJSON(),
+ 'derivationType': derivationType.index,
'isHidden': isHidden,
+ 'isChange': isChange,
'isUsed': isUsed,
'txCount': txCount,
'name': name,
@@ -107,21 +129,51 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord {
'type': type.toString(),
'scriptHash': scriptHash,
});
+
+ @override
+ operator ==(Object other) {
+ if (identical(this, other)) return true;
+
+ return other is BitcoinAddressRecord &&
+ other.address == address &&
+ other.index == index &&
+ other.derivationInfo == derivationInfo &&
+ other.scriptHash == scriptHash &&
+ other.type == type &&
+ other.derivationType == derivationType;
+ }
+
+ @override
+ int get hashCode =>
+ address.hashCode ^
+ index.hashCode ^
+ derivationInfo.hashCode ^
+ scriptHash.hashCode ^
+ type.hashCode ^
+ derivationType.hashCode;
}
class BitcoinSilentPaymentAddressRecord extends BaseBitcoinAddressRecord {
+ int get labelIndex => index;
+ final String? labelHex;
+
+ static bool isChangeAddress(int labelIndex) => labelIndex == 0;
+
BitcoinSilentPaymentAddressRecord(
super.address, {
- required super.index,
- super.isHidden = false,
+ required int labelIndex,
super.txCount = 0,
super.balance = 0,
super.name = '',
super.isUsed = false,
- required this.silentPaymentTweak,
- required super.network,
- required super.type,
- }) : super();
+ super.type = SilentPaymentsAddresType.p2sp,
+ super.isHidden,
+ this.labelHex,
+ }) : super(index: labelIndex, isChange: isChangeAddress(labelIndex)) {
+ if (labelIndex != 1 && labelHex == null) {
+ throw ArgumentError('label must be provided for silent address index != 1');
+ }
+ }
factory BitcoinSilentPaymentAddressRecord.fromJSON(String jsonSource,
{BasedUtxoNetwork? network}) {
@@ -129,36 +181,68 @@ class BitcoinSilentPaymentAddressRecord extends BaseBitcoinAddressRecord {
return BitcoinSilentPaymentAddressRecord(
decoded['address'] as String,
- index: decoded['index'] as int,
- isHidden: decoded['isHidden'] as bool? ?? false,
+ labelIndex: decoded['labelIndex'] as int,
isUsed: decoded['isUsed'] as bool? ?? false,
txCount: decoded['txCount'] as int? ?? 0,
name: decoded['name'] as String? ?? '',
balance: decoded['balance'] as int? ?? 0,
- network: (decoded['network'] as String?) == null
- ? network
- : BasedUtxoNetwork.fromName(decoded['network'] as String),
- silentPaymentTweak: decoded['silent_payment_tweak'] as String?,
- type: decoded['type'] != null && decoded['type'] != ''
- ? BitcoinAddressType.values
- .firstWhere((type) => type.toString() == decoded['type'] as String)
- : SilentPaymentsAddresType.p2sp,
+ labelHex: decoded['labelHex'] as String?,
);
}
- final String? silentPaymentTweak;
-
@override
String toJSON() => json.encode({
'address': address,
- 'index': index,
- 'isHidden': isHidden,
+ 'labelIndex': labelIndex,
'isUsed': isUsed,
'txCount': txCount,
'name': name,
'balance': balance,
'type': type.toString(),
- 'network': network?.value,
- 'silent_payment_tweak': silentPaymentTweak,
+ 'labelHex': labelHex,
+ });
+}
+
+class BitcoinReceivedSPAddressRecord extends BitcoinSilentPaymentAddressRecord {
+ final ECPrivate spendKey;
+
+ BitcoinReceivedSPAddressRecord(
+ super.address, {
+ required super.labelIndex,
+ super.txCount = 0,
+ super.balance = 0,
+ super.name = '',
+ super.isUsed = false,
+ required this.spendKey,
+ super.type = SegwitAddresType.p2tr,
+ super.labelHex,
+ }) : super(isHidden: true);
+
+ factory BitcoinReceivedSPAddressRecord.fromJSON(String jsonSource, {BasedUtxoNetwork? network}) {
+ final decoded = json.decode(jsonSource) as Map;
+
+ return BitcoinReceivedSPAddressRecord(
+ decoded['address'] as String,
+ labelIndex: decoded['index'] as int,
+ isUsed: decoded['isUsed'] as bool? ?? false,
+ txCount: decoded['txCount'] as int? ?? 0,
+ name: decoded['name'] as String? ?? '',
+ balance: decoded['balance'] as int? ?? 0,
+ labelHex: decoded['label'] as String?,
+ spendKey: ECPrivate.fromHex(decoded['spendKey'] as String),
+ );
+ }
+
+ @override
+ String toJSON() => json.encode({
+ 'address': address,
+ 'labelIndex': labelIndex,
+ 'isUsed': isUsed,
+ 'txCount': txCount,
+ 'name': name,
+ 'balance': balance,
+ 'type': type.toString(),
+ 'labelHex': labelHex,
+ 'spend_key': spendKey.toString(),
});
}
diff --git a/cw_bitcoin/lib/bitcoin_amount_format.dart b/cw_bitcoin/lib/bitcoin_amount_format.dart
deleted file mode 100644
index d5a42d984..000000000
--- a/cw_bitcoin/lib/bitcoin_amount_format.dart
+++ /dev/null
@@ -1,26 +0,0 @@
-import 'package:intl/intl.dart';
-import 'package:cw_core/crypto_amount_format.dart';
-
-const bitcoinAmountLength = 8;
-const bitcoinAmountDivider = 100000000;
-final bitcoinAmountFormat = NumberFormat()
- ..maximumFractionDigits = bitcoinAmountLength
- ..minimumFractionDigits = 1;
-
-String bitcoinAmountToString({required int amount}) => bitcoinAmountFormat.format(
- cryptoAmountToDouble(amount: amount, divider: bitcoinAmountDivider));
-
-double bitcoinAmountToDouble({required int amount}) =>
- cryptoAmountToDouble(amount: amount, divider: bitcoinAmountDivider);
-
-int stringDoubleToBitcoinAmount(String amount) {
- int result = 0;
-
- try {
- result = (double.parse(amount) * bitcoinAmountDivider).round();
- } catch (e) {
- result = 0;
- }
-
- return result;
-}
diff --git a/cw_bitcoin/lib/bitcoin_hardware_wallet_service.dart b/cw_bitcoin/lib/bitcoin_hardware_wallet_service.dart
index a02c51c69..415ae0e98 100644
--- a/cw_bitcoin/lib/bitcoin_hardware_wallet_service.dart
+++ b/cw_bitcoin/lib/bitcoin_hardware_wallet_service.dart
@@ -2,7 +2,6 @@ import 'dart:async';
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:blockchain_utils/blockchain_utils.dart';
-import 'package:cw_bitcoin/utils.dart';
import 'package:cw_core/hardware/hardware_account_data.dart';
import 'package:ledger_bitcoin/ledger_bitcoin.dart';
import 'package:ledger_flutter_plus/ledger_flutter_plus.dart';
@@ -12,8 +11,7 @@ class BitcoinHardwareWalletService {
final LedgerConnection ledgerConnection;
- Future> getAvailableAccounts(
- {int index = 0, int limit = 5}) async {
+ Future> getAvailableAccounts({int index = 0, int limit = 5}) async {
final bitcoinLedgerApp = BitcoinLedgerApp(ledgerConnection);
final masterFp = await bitcoinLedgerApp.getMasterFingerprint();
@@ -23,13 +21,14 @@ class BitcoinHardwareWalletService {
for (final i in indexRange) {
final derivationPath = "m/84'/0'/$i'";
- final xpub =
- await bitcoinLedgerApp.getXPubKey(derivationPath: derivationPath);
- Bip32Slip10Secp256k1 hd =
- Bip32Slip10Secp256k1.fromExtendedKey(xpub).childKey(Bip32KeyIndex(0));
+ final xpub = await bitcoinLedgerApp.getXPubKey(derivationPath: derivationPath);
+ final bip32 = Bip32Slip10Secp256k1.fromExtendedKey(xpub).childKey(Bip32KeyIndex(0));
- final address = generateP2WPKHAddress(
- hd: hd, index: 0, network: BitcoinNetwork.mainnet);
+ final fullPath = Bip32PathParser.parse(derivationPath).addElem(Bip32KeyIndex(0));
+
+ final address = ECPublic.fromBip32(bip32.derive(fullPath).publicKey)
+ .toP2wpkhAddress()
+ .toAddress(BitcoinNetwork.mainnet);
accounts.add(HardwareAccountData(
address: address,
diff --git a/cw_bitcoin/lib/bitcoin_transaction_credentials.dart b/cw_bitcoin/lib/bitcoin_transaction_credentials.dart
index 01e905fb0..f6d769735 100644
--- a/cw_bitcoin/lib/bitcoin_transaction_credentials.dart
+++ b/cw_bitcoin/lib/bitcoin_transaction_credentials.dart
@@ -1,13 +1,17 @@
-import 'package:cw_bitcoin/bitcoin_transaction_priority.dart';
import 'package:cw_core/output_info.dart';
+import 'package:cw_core/transaction_priority.dart';
import 'package:cw_core/unspent_coin_type.dart';
class BitcoinTransactionCredentials {
- BitcoinTransactionCredentials(this.outputs,
- {required this.priority, this.feeRate, this.coinTypeToSpendFrom = UnspentCoinType.any});
+ BitcoinTransactionCredentials(
+ this.outputs, {
+ required this.priority,
+ this.feeRate,
+ this.coinTypeToSpendFrom = UnspentCoinType.any,
+ });
final List outputs;
- final BitcoinTransactionPriority? priority;
+ final TransactionPriority? priority;
final int? feeRate;
final UnspentCoinType coinTypeToSpendFrom;
}
diff --git a/cw_bitcoin/lib/bitcoin_transaction_priority.dart b/cw_bitcoin/lib/bitcoin_transaction_priority.dart
index d1f45a545..26a4c2f62 100644
--- a/cw_bitcoin/lib/bitcoin_transaction_priority.dart
+++ b/cw_bitcoin/lib/bitcoin_transaction_priority.dart
@@ -1,49 +1,57 @@
import 'package:cw_core/transaction_priority.dart';
class BitcoinTransactionPriority extends TransactionPriority {
- const BitcoinTransactionPriority({required String title, required int raw})
- : super(title: title, raw: raw);
+ const BitcoinTransactionPriority({required super.title, required super.raw});
- static const List all = [fast, medium, slow, custom];
- static const BitcoinTransactionPriority slow =
- BitcoinTransactionPriority(title: 'Slow', raw: 0);
- static const BitcoinTransactionPriority medium =
- BitcoinTransactionPriority(title: 'Medium', raw: 1);
- static const BitcoinTransactionPriority fast =
- BitcoinTransactionPriority(title: 'Fast', raw: 2);
+// Unimportant: the lowest possible, confirms when it confirms no matter how long it takes
+ static const BitcoinTransactionPriority unimportant =
+ BitcoinTransactionPriority(title: 'Unimportant', raw: 0);
+// Normal: low fee, confirms in a reasonable time, normal because in most cases more than this is not needed, gets you in the next 2-3 blocks (about 1 hour)
+ static const BitcoinTransactionPriority normal =
+ BitcoinTransactionPriority(title: 'Normal', raw: 1);
+// Elevated: medium fee, confirms soon, elevated because it's higher than normal, gets you in the next 1-2 blocks (about 30 mins)
+ static const BitcoinTransactionPriority elevated =
+ BitcoinTransactionPriority(title: 'Elevated', raw: 2);
+// Priority: high fee, expected in the next block (about 10 mins).
+ static const BitcoinTransactionPriority priority =
+ BitcoinTransactionPriority(title: 'Priority', raw: 3);
+// Custom: any fee, user defined
static const BitcoinTransactionPriority custom =
- BitcoinTransactionPriority(title: 'Custom', raw: 3);
+ BitcoinTransactionPriority(title: 'Custom', raw: 4);
static BitcoinTransactionPriority deserialize({required int raw}) {
switch (raw) {
case 0:
- return slow;
+ return unimportant;
case 1:
- return medium;
+ return normal;
case 2:
- return fast;
+ return elevated;
case 3:
+ return priority;
+ case 4:
return custom;
default:
- throw Exception('Unexpected token: $raw for BitcoinTransactionPriority deserialize');
+ throw Exception('Unexpected token: $raw for TransactionPriority deserialize');
}
}
- String get units => 'sat';
-
@override
String toString() {
var label = '';
switch (this) {
- case BitcoinTransactionPriority.slow:
- label = 'Slow ~24hrs+'; // '${S.current.transaction_priority_slow} ~24hrs';
+ case BitcoinTransactionPriority.unimportant:
+ label = 'Unimportant ~24hrs+'; // '${S.current.transaction_priority_slow} ~24hrs';
break;
- case BitcoinTransactionPriority.medium:
- label = 'Medium'; // S.current.transaction_priority_medium;
+ case BitcoinTransactionPriority.normal:
+ label = 'Normal ~1hr+'; // S.current.transaction_priority_medium;
break;
- case BitcoinTransactionPriority.fast:
- label = 'Fast';
+ case BitcoinTransactionPriority.elevated:
+ label = 'Elevated';
+ break; // S.current.transaction_priority_fast;
+ case BitcoinTransactionPriority.priority:
+ label = 'Priority';
break; // S.current.transaction_priority_fast;
case BitcoinTransactionPriority.custom:
label = 'Custom';
@@ -61,19 +69,22 @@ class BitcoinTransactionPriority extends TransactionPriority {
}
}
-class LitecoinTransactionPriority extends BitcoinTransactionPriority {
- const LitecoinTransactionPriority({required String title, required int raw})
+class ElectrumTransactionPriority extends TransactionPriority {
+ const ElectrumTransactionPriority({required String title, required int raw})
: super(title: title, raw: raw);
- static const List all = [fast, medium, slow];
- static const LitecoinTransactionPriority slow =
- LitecoinTransactionPriority(title: 'Slow', raw: 0);
- static const LitecoinTransactionPriority medium =
- LitecoinTransactionPriority(title: 'Medium', raw: 1);
- static const LitecoinTransactionPriority fast =
- LitecoinTransactionPriority(title: 'Fast', raw: 2);
+ static const List all = [fast, medium, slow, custom];
- static LitecoinTransactionPriority deserialize({required int raw}) {
+ static const ElectrumTransactionPriority slow =
+ ElectrumTransactionPriority(title: 'Slow', raw: 0);
+ static const ElectrumTransactionPriority medium =
+ ElectrumTransactionPriority(title: 'Medium', raw: 1);
+ static const ElectrumTransactionPriority fast =
+ ElectrumTransactionPriority(title: 'Fast', raw: 2);
+ static const ElectrumTransactionPriority custom =
+ ElectrumTransactionPriority(title: 'Custom', raw: 3);
+
+ static ElectrumTransactionPriority deserialize({required int raw}) {
switch (raw) {
case 0:
return slow;
@@ -81,27 +92,31 @@ class LitecoinTransactionPriority extends BitcoinTransactionPriority {
return medium;
case 2:
return fast;
+ case 3:
+ return custom;
default:
- throw Exception('Unexpected token: $raw for LitecoinTransactionPriority deserialize');
+ throw Exception('Unexpected token: $raw for ElectrumTransactionPriority deserialize');
}
}
- @override
- String get units => 'Litoshi';
+ String get units => 'sat';
@override
String toString() {
var label = '';
switch (this) {
- case LitecoinTransactionPriority.slow:
- label = 'Slow'; // S.current.transaction_priority_slow;
+ case ElectrumTransactionPriority.slow:
+ label = 'Slow ~24hrs+'; // '${S.current.transaction_priority_slow} ~24hrs';
break;
- case LitecoinTransactionPriority.medium:
+ case ElectrumTransactionPriority.medium:
label = 'Medium'; // S.current.transaction_priority_medium;
break;
- case LitecoinTransactionPriority.fast:
- label = 'Fast'; // S.current.transaction_priority_fast;
+ case ElectrumTransactionPriority.fast:
+ label = 'Fast';
+ break; // S.current.transaction_priority_fast;
+ case ElectrumTransactionPriority.custom:
+ label = 'Custom';
break;
default:
break;
@@ -110,54 +125,171 @@ class LitecoinTransactionPriority extends BitcoinTransactionPriority {
return label;
}
+ String labelWithRate(int rate, int? customRate) {
+ final rateValue = this == custom ? customRate ??= 0 : rate;
+ return '${toString()} ($rateValue ${units}/byte)';
+ }
}
-class BitcoinCashTransactionPriority extends BitcoinTransactionPriority {
- const BitcoinCashTransactionPriority({required String title, required int raw})
- : super(title: title, raw: raw);
- static const List all = [fast, medium, slow];
- static const BitcoinCashTransactionPriority slow =
- BitcoinCashTransactionPriority(title: 'Slow', raw: 0);
- static const BitcoinCashTransactionPriority medium =
- BitcoinCashTransactionPriority(title: 'Medium', raw: 1);
- static const BitcoinCashTransactionPriority fast =
- BitcoinCashTransactionPriority(title: 'Fast', raw: 2);
+class LitecoinTransactionPriority extends ElectrumTransactionPriority {
+ const LitecoinTransactionPriority({required super.title, required super.raw});
- static BitcoinCashTransactionPriority deserialize({required int raw}) {
- switch (raw) {
- case 0:
+ @override
+ String get units => 'lit';
+}
+
+class BitcoinCashTransactionPriority extends ElectrumTransactionPriority {
+ const BitcoinCashTransactionPriority({required super.title, required super.raw});
+
+ @override
+ String get units => 'satoshi';
+}
+
+class BitcoinTransactionPriorities implements TransactionPriorities {
+ const BitcoinTransactionPriorities({
+ required this.unimportant,
+ required this.normal,
+ required this.elevated,
+ required this.priority,
+ required this.custom,
+ });
+
+ final int unimportant;
+ final int normal;
+ final int elevated;
+ final int priority;
+ final int custom;
+
+ @override
+ int operator [](TransactionPriority type) {
+ switch (type) {
+ case BitcoinTransactionPriority.unimportant:
+ return unimportant;
+ case BitcoinTransactionPriority.normal:
+ return normal;
+ case BitcoinTransactionPriority.elevated:
+ return elevated;
+ case BitcoinTransactionPriority.priority:
+ return priority;
+ case BitcoinTransactionPriority.custom:
+ return custom;
+ default:
+ throw Exception('Unexpected token: $type for TransactionPriorities operator[]');
+ }
+ }
+
+ @override
+ String labelWithRate(TransactionPriority priorityType, [int? rate]) {
+ late int rateValue;
+
+ if (priorityType == BitcoinTransactionPriority.custom) {
+ if (rate == null) {
+ throw Exception('Rate must be provided for custom transaction priority');
+ }
+ rateValue = rate;
+ } else {
+ rateValue = this[priorityType];
+ }
+
+ return '${priorityType.toString()} (${rateValue} ${priorityType.units}/byte)';
+ }
+
+ @override
+ Map toJson() {
+ return {
+ 'unimportant': unimportant,
+ 'normal': normal,
+ 'elevated': elevated,
+ 'priority': priority,
+ 'custom': custom,
+ };
+ }
+
+ static BitcoinTransactionPriorities fromJson(Map json) {
+ return BitcoinTransactionPriorities(
+ unimportant: json['unimportant'] as int,
+ normal: json['normal'] as int,
+ elevated: json['elevated'] as int,
+ priority: json['priority'] as int,
+ custom: json['custom'] as int,
+ );
+ }
+}
+
+class ElectrumTransactionPriorities implements TransactionPriorities {
+ const ElectrumTransactionPriorities({
+ required this.slow,
+ required this.medium,
+ required this.fast,
+ required this.custom,
+ });
+
+ final int slow;
+ final int medium;
+ final int fast;
+ final int custom;
+
+ @override
+ int operator [](TransactionPriority type) {
+ switch (type) {
+ case ElectrumTransactionPriority.slow:
return slow;
- case 1:
+ case ElectrumTransactionPriority.medium:
return medium;
- case 2:
+ case ElectrumTransactionPriority.fast:
return fast;
+ case ElectrumTransactionPriority.custom:
+ return custom;
default:
- throw Exception('Unexpected token: $raw for BitcoinCashTransactionPriority deserialize');
+ throw Exception('Unexpected token: $type for TransactionPriorities operator[]');
}
}
@override
- String get units => 'Satoshi';
+ String labelWithRate(TransactionPriority priorityType, [int? rate]) {
+ return '${priorityType.toString()} (${this[priorityType]} ${priorityType.units}/byte)';
+ }
- @override
- String toString() {
- var label = '';
-
- switch (this) {
- case BitcoinCashTransactionPriority.slow:
- label = 'Slow'; // S.current.transaction_priority_slow;
- break;
- case BitcoinCashTransactionPriority.medium:
- label = 'Medium'; // S.current.transaction_priority_medium;
- break;
- case BitcoinCashTransactionPriority.fast:
- label = 'Fast'; // S.current.transaction_priority_fast;
- break;
- default:
- break;
+ factory ElectrumTransactionPriorities.fromList(List list) {
+ if (list.length != 3) {
+ throw Exception(
+ 'Unexpected list length: ${list.length} for BitcoinElectrumTransactionPriorities.fromList');
}
- return label;
+ return ElectrumTransactionPriorities(
+ slow: list[0],
+ medium: list[1],
+ fast: list[2],
+ custom: 0,
+ );
+ }
+
+ @override
+ Map toJson() {
+ return {
+ 'slow': slow,
+ 'medium': medium,
+ 'fast': fast,
+ 'custom': custom,
+ };
+ }
+
+ static ElectrumTransactionPriorities fromJson(Map json) {
+ return ElectrumTransactionPriorities(
+ slow: json['slow'] as int,
+ medium: json['medium'] as int,
+ fast: json['fast'] as int,
+ custom: json['custom'] as int,
+ );
}
}
+TransactionPriorities deserializeTransactionPriorities(Map json) {
+ if (json.containsKey('unimportant')) {
+ return BitcoinTransactionPriorities.fromJson(json);
+ } else if (json.containsKey('slow')) {
+ return ElectrumTransactionPriorities.fromJson(json);
+ } else {
+ throw Exception('Unexpected token: $json for deserializeTransactionPriorities');
+ }
+}
diff --git a/cw_bitcoin/lib/bitcoin_unspent.dart b/cw_bitcoin/lib/bitcoin_unspent.dart
index 3691a7a22..93d9c25d5 100644
--- a/cw_bitcoin/lib/bitcoin_unspent.dart
+++ b/cw_bitcoin/lib/bitcoin_unspent.dart
@@ -1,17 +1,21 @@
import 'package:cw_bitcoin/bitcoin_address_record.dart';
import 'package:cw_core/unspent_transaction_output.dart';
+import 'package:bitcoin_base/bitcoin_base.dart';
class BitcoinUnspent extends Unspent {
BitcoinUnspent(BaseBitcoinAddressRecord addressRecord, String hash, int value, int vout)
: bitcoinAddressRecord = addressRecord,
super(addressRecord.address, hash, value, vout, null);
+ factory BitcoinUnspent.fromUTXO(BaseBitcoinAddressRecord address, ElectrumUtxo utxo) =>
+ BitcoinUnspent(address, utxo.txId, utxo.value.toInt(), utxo.vout);
+
factory BitcoinUnspent.fromJSON(BaseBitcoinAddressRecord? address, Map json) =>
BitcoinUnspent(
address ?? BitcoinAddressRecord.fromJSON(json['address_record'].toString()),
json['tx_hash'] as String,
- json['value'] as int,
- json['tx_pos'] as int,
+ int.parse(json['value'].toString()),
+ int.parse(json['tx_pos'].toString()),
);
Map toJson() {
@@ -25,43 +29,13 @@ class BitcoinUnspent extends Unspent {
}
final BaseBitcoinAddressRecord bitcoinAddressRecord;
-}
-
-class BitcoinSilentPaymentsUnspent extends BitcoinUnspent {
- BitcoinSilentPaymentsUnspent(
- BitcoinSilentPaymentAddressRecord addressRecord,
- String hash,
- int value,
- int vout, {
- required this.silentPaymentTweak,
- required this.silentPaymentLabel,
- }) : super(addressRecord, hash, value, vout);
@override
- factory BitcoinSilentPaymentsUnspent.fromJSON(
- BitcoinSilentPaymentAddressRecord? address, Map json) =>
- BitcoinSilentPaymentsUnspent(
- address ?? BitcoinSilentPaymentAddressRecord.fromJSON(json['address_record'].toString()),
- json['tx_hash'] as String,
- json['value'] as int,
- json['tx_pos'] as int,
- silentPaymentTweak: json['silent_payment_tweak'] as String?,
- silentPaymentLabel: json['silent_payment_label'] as String?,
- );
-
- @override
- Map toJson() {
- final json = {
- 'address_record': bitcoinAddressRecord.toJSON(),
- 'tx_hash': hash,
- 'value': value,
- 'tx_pos': vout,
- 'silent_payment_tweak': silentPaymentTweak,
- 'silent_payment_label': silentPaymentLabel,
- };
- return json;
+ bool operator ==(Object o) {
+ if (identical(this, o)) return true;
+ return o is BitcoinUnspent && hash == o.hash && vout == o.vout;
}
- String? silentPaymentTweak;
- String? silentPaymentLabel;
+ @override
+ int get hashCode => Object.hash(hash, vout);
}
diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart
index 908897845..748acdbbe 100644
--- a/cw_bitcoin/lib/bitcoin_wallet.dart
+++ b/cw_bitcoin/lib/bitcoin_wallet.dart
@@ -1,11 +1,16 @@
+import 'dart:async';
import 'dart:convert';
+import 'dart:isolate';
-import 'package:bip39/bip39.dart' as bip39;
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/bitcoin_mnemonic.dart';
+import 'package:cw_bitcoin/electrum_worker/methods/methods.dart';
import 'package:cw_bitcoin/psbt_transaction_builder.dart';
+import 'package:cw_bitcoin/bitcoin_transaction_priority.dart';
+import 'package:cw_bitcoin/bitcoin_unspent.dart';
+import 'package:cw_bitcoin/electrum_transaction_info.dart';
+import 'package:cw_bitcoin/electrum_wallet_addresses.dart';
import 'package:cw_core/encryption_file_utils.dart';
import 'package:cw_bitcoin/electrum_derivations.dart';
import 'package:cw_bitcoin/bitcoin_wallet_addresses.dart';
@@ -13,26 +18,33 @@ import 'package:cw_bitcoin/electrum_balance.dart';
import 'package:cw_bitcoin/electrum_wallet.dart';
import 'package:cw_bitcoin/electrum_wallet_snapshot.dart';
import 'package:cw_core/crypto_currency.dart';
+import 'package:cw_core/get_height_by_date.dart';
+import 'package:cw_core/sync_status.dart';
+import 'package:cw_core/transaction_direction.dart';
import 'package:cw_core/unspent_coins_info.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:flutter/foundation.dart';
import 'package:hive/hive.dart';
import 'package:ledger_bitcoin/ledger_bitcoin.dart';
import 'package:ledger_flutter_plus/ledger_flutter_plus.dart';
import 'package:mobx/mobx.dart';
+import 'package:sp_scanner/sp_scanner.dart';
part 'bitcoin_wallet.g.dart';
class BitcoinWallet = BitcoinWalletBase with _$BitcoinWallet;
abstract class BitcoinWalletBase extends ElectrumWallet with Store {
+ StreamSubscription? _receiveStream;
+
BitcoinWalletBase({
required String password,
required WalletInfo walletInfo,
required Box unspentCoinsInfo,
required EncryptionFileUtils encryptionFileUtils,
- Uint8List? seedBytes,
+ List? seedBytes,
String? mnemonic,
String? xpub,
String? addressPageType,
@@ -45,6 +57,8 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
List? initialSilentAddresses,
int initialSilentAddressIndex = 0,
bool? alwaysScan,
+ required bool mempoolAPIEnabled,
+ super.hdWallets,
}) : super(
mnemonic: mnemonic,
passphrase: passphrase,
@@ -61,16 +75,11 @@ 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,
+ mempoolAPIEnabled: mempoolAPIEnabled,
) {
- // in a standard BIP44 wallet, mainHd derivation path = m/84'/0'/0'/0 (account 0, index unspecified here)
- // the sideHd derivation path = m/84'/0'/0'/1 (account 1, index unspecified here)
- // String derivationPath = walletInfo.derivationInfo!.derivationPath!;
- // String sideDerivationPath = derivationPath.substring(0, derivationPath.length - 1) + "1";
- // final hd = bitcoin.HDWallet.fromSeed(seedBytes, network: networkType);
walletAddresses = BitcoinWalletAddresses(
walletInfo,
initialAddresses: initialAddresses,
@@ -78,17 +87,13 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
initialChangeAddressIndex: initialChangeAddressIndex,
initialSilentAddresses: initialSilentAddresses,
initialSilentAddressIndex: initialSilentAddressIndex,
- mainHd: hd,
- sideHd: accountHD.childKey(Bip32KeyIndex(1)),
network: networkParam ?? network,
- masterHd:
- seedBytes != null ? Bip32Slip10Secp256k1.fromSeed(seedBytes) : null,
isHardwareWallet: walletInfo.isHardwareWallet,
+ hdWallets: hdWallets,
);
autorun((_) {
- this.walletAddresses.isEnabledAutoGenerateSubaddress =
- this.isEnabledAutoGenerateSubaddress;
+ this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress;
});
}
@@ -107,21 +112,38 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
Map? initialRegularAddressIndex,
Map? initialChangeAddressIndex,
int initialSilentAddressIndex = 0,
+ required bool mempoolAPIEnabled,
}) async {
- late Uint8List seedBytes;
+ late List seedBytes;
+ final Map hdWallets = {};
- switch (walletInfo.derivationInfo?.derivationType) {
- case DerivationType.bip39:
- seedBytes = await bip39.mnemonicToSeed(
- mnemonic,
- passphrase: passphrase ?? "",
- );
+ for (final derivation in walletInfo.derivations ?? []) {
+ if (derivation.derivationType == DerivationType.bip39) {
+ seedBytes = Bip39SeedGenerator.generateFromString(mnemonic, passphrase);
+ hdWallets[CWBitcoinDerivationType.bip39] = Bip32Slip10Secp256k1.fromSeed(seedBytes);
break;
- case DerivationType.electrum:
- default:
- seedBytes = await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? "");
+ } else {
+ try {
+ seedBytes = ElectrumV2SeedGenerator.generateFromString(mnemonic, passphrase);
+ hdWallets[CWBitcoinDerivationType.electrum] = Bip32Slip10Secp256k1.fromSeed(seedBytes);
+ } catch (e) {
+ print("electrum_v2 seed error: $e");
+
+ try {
+ seedBytes = ElectrumV1SeedGenerator(mnemonic).generate();
+ hdWallets[CWBitcoinDerivationType.electrum] = Bip32Slip10Secp256k1.fromSeed(seedBytes);
+ } catch (e) {
+ print("electrum_v1 seed error: $e");
+ }
+ }
+
break;
+ }
}
+
+ hdWallets[CWBitcoinDerivationType.old] =
+ hdWallets[CWBitcoinDerivationType.bip39] ?? hdWallets[CWBitcoinDerivationType.electrum]!;
+
return BitcoinWallet(
mnemonic: mnemonic,
passphrase: passphrase ?? "",
@@ -138,6 +160,8 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
initialChangeAddressIndex: initialChangeAddressIndex,
addressPageType: addressPageType,
networkParam: network,
+ mempoolAPIEnabled: mempoolAPIEnabled,
+ hdWallets: hdWallets,
);
}
@@ -148,6 +172,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
required String password,
required EncryptionFileUtils encryptionFileUtils,
required bool alwaysScan,
+ required bool mempoolAPIEnabled,
}) async {
final network = walletInfo.network != null
? BasedUtxoNetwork.fromName(walletInfo.network!)
@@ -189,28 +214,43 @@ 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;
+ List? seedBytes = null;
+ final Map hdWallets = {};
final mnemonic = keysData.mnemonic;
final passphrase = keysData.passphrase;
if (mnemonic != null) {
- switch (walletInfo.derivationInfo!.derivationType) {
- case DerivationType.electrum:
- seedBytes = await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? "");
+ for (final derivation in walletInfo.derivations ?? []) {
+ if (derivation.derivationType == DerivationType.bip39) {
+ seedBytes = Bip39SeedGenerator.generateFromString(mnemonic, passphrase);
+ hdWallets[CWBitcoinDerivationType.bip39] = Bip32Slip10Secp256k1.fromSeed(seedBytes);
+
break;
- case DerivationType.bip39:
- default:
- seedBytes = await bip39.mnemonicToSeed(
- mnemonic,
- passphrase: passphrase ?? '',
- );
+ } else {
+ try {
+ seedBytes = ElectrumV2SeedGenerator.generateFromString(mnemonic, passphrase);
+ hdWallets[CWBitcoinDerivationType.electrum] = Bip32Slip10Secp256k1.fromSeed(seedBytes);
+ } catch (e) {
+ print("electrum_v2 seed error: $e");
+
+ try {
+ seedBytes = ElectrumV1SeedGenerator(mnemonic).generate();
+ hdWallets[CWBitcoinDerivationType.electrum] =
+ Bip32Slip10Secp256k1.fromSeed(seedBytes);
+ } catch (e) {
+ print("electrum_v1 seed error: $e");
+ }
+ }
+
break;
+ }
}
+
+ hdWallets[CWBitcoinDerivationType.old] =
+ hdWallets[CWBitcoinDerivationType.bip39] ?? hdWallets[CWBitcoinDerivationType.electrum]!;
}
return BitcoinWallet(
@@ -231,9 +271,59 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
addressPageType: snp?.addressPageType,
networkParam: network,
alwaysScan: alwaysScan,
+ mempoolAPIEnabled: mempoolAPIEnabled,
+ hdWallets: hdWallets,
);
}
+ Future getNodeIsElectrs() async {
+ final version = await sendWorker(ElectrumWorkerGetVersionRequest()) as List;
+
+ if (version.isNotEmpty) {
+ final server = version[0];
+
+ if (server.toLowerCase().contains('electrs')) {
+ node!.isElectrs = true;
+ node!.save();
+ return node!.isElectrs!;
+ }
+ }
+
+ node!.isElectrs = false;
+ node!.save();
+ return node!.isElectrs!;
+ }
+
+ Future getNodeSupportsSilentPayments() async {
+ return true;
+ // As of today (august 2024), only ElectrumRS supports silent payments
+ // if (!(await getNodeIsElectrs())) {
+ // return false;
+ // }
+
+ // if (node == null) {
+ // return false;
+ // }
+
+ // try {
+ // final tweaksResponse = await electrumClient.getTweaks(height: 0);
+
+ // if (tweaksResponse != null) {
+ // node!.supportsSilentPayments = true;
+ // node!.save();
+ // return node!.supportsSilentPayments!;
+ // }
+ // } on RequestFailedTimeoutException catch (_) {
+ // node!.supportsSilentPayments = false;
+ // node!.save();
+ // return node!.supportsSilentPayments!;
+ // } catch (_) {}
+
+ // node!.supportsSilentPayments = false;
+ // node!.save();
+ // return node!.supportsSilentPayments!;
+ }
+
LedgerConnection? _ledgerConnection;
BitcoinLedgerApp? _bitcoinLedgerApp;
@@ -261,9 +351,8 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
final psbtReadyInputs = [];
for (final utxo in utxos) {
final rawTx =
- await electrumClient.getTransactionHex(hash: utxo.utxo.txHash);
- final publicKeyAndDerivationPath =
- publicKeys[utxo.ownerDetails.address.pubKeyHash()]!;
+ (await getTransactionExpanded(hash: utxo.utxo.txHash)).originalTransaction.toHex();
+ final publicKeyAndDerivationPath = publicKeys[utxo.ownerDetails.address.pubKeyHash()]!;
psbtReadyInputs.add(PSBTReadyUtxoWithAddress(
utxo: utxo.utxo,
@@ -275,8 +364,8 @@ 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(psbt: psbt.psbt);
return BtcTransaction.fromRaw(BytesUtils.toHexString(rawHex));
@@ -286,20 +375,443 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
Future 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 isChange = addressEntry?.isChange == 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(
- message: ascii.encode(message), signDerivationPath: derivationPath);
+ final signature = await _bitcoinLedgerApp!
+ .signMessage(message: ascii.encode(message), signDerivationPath: derivationPath);
return base64Encode(signature);
}
return super.signMessage(message, address: address);
}
+
+ @action
+ Future setSilentPaymentsScanning(bool active) async {
+ silentPaymentsScanningActive = active;
+
+ if (active) {
+ syncStatus = AttemptingScanSyncStatus();
+
+ final tip = currentChainTip!;
+
+ if (tip == walletInfo.restoreHeight) {
+ syncStatus = SyncedTipSyncStatus(tip);
+ return;
+ }
+
+ if (tip > walletInfo.restoreHeight) {
+ _setListeners(walletInfo.restoreHeight);
+ }
+ } else {
+ alwaysScan = false;
+
+ // _isolate?.then((value) => value.kill(priority: Isolate.immediate));
+
+ // if (rpc!.isConnected) {
+ // syncStatus = SyncedSyncStatus();
+ // } else {
+ // syncStatus = NotConnectedSyncStatus();
+ // }
+ }
+ }
+
+ // @override
+ // @action
+ // Future updateAllUnspents() async {
+ // List updatedUnspentCoins = [];
+
+ // // Update unspents stored from scanned silent payment transactions
+ // transactionHistory.transactions.values.forEach((tx) {
+ // if (tx.unspents != null) {
+ // updatedUnspentCoins.addAll(tx.unspents!);
+ // }
+ // });
+
+ // // Set the balance of all non-silent payment and non-mweb addresses to 0 before updating
+ // walletAddresses.allAddresses
+ // .where((element) => element.type != SegwitAddresType.mweb)
+ // .forEach((addr) {
+ // if (addr is! BitcoinSilentPaymentAddressRecord) addr.balance = 0;
+ // });
+
+ // await Future.wait(walletAddresses.allAddresses
+ // .where((element) => element.type != SegwitAddresType.mweb)
+ // .map((address) async {
+ // updatedUnspentCoins.addAll(await fetchUnspent(address));
+ // }));
+
+ // unspentCoins.addAll(updatedUnspentCoins);
+
+ // if (unspentCoinsInfo.length != updatedUnspentCoins.length) {
+ // unspentCoins.forEach((coin) => addCoinInfo(coin));
+ // return;
+ // }
+
+ // await updateCoins(unspentCoins.toSet());
+ // await refreshUnspentCoinsInfo();
+ // }
+
+ @override
+ void updateCoin(BitcoinUnspent coin) {
+ final coinInfoList = unspentCoinsInfo.values.where(
+ (element) =>
+ element.walletId.contains(id) &&
+ element.hash.contains(coin.hash) &&
+ element.vout == coin.vout,
+ );
+
+ if (coinInfoList.isNotEmpty) {
+ final coinInfo = coinInfoList.first;
+
+ coin.isFrozen = coinInfo.isFrozen;
+ coin.isSending = coinInfo.isSending;
+ coin.note = coinInfo.note;
+ if (coin.bitcoinAddressRecord is! BitcoinSilentPaymentAddressRecord)
+ coin.bitcoinAddressRecord.balance += coinInfo.value;
+ } else {
+ addCoinInfo(coin);
+ }
+ }
+
+ @action
+ @override
+ Future startSync() async {
+ await _setInitialScanHeight();
+
+ await super.startSync();
+
+ if (alwaysScan == true) {
+ _setListeners(walletInfo.restoreHeight);
+ }
+ }
+
+ @action
+ @override
+ Future rescan({required int height, bool? doSingleScan}) async {
+ silentPaymentsScanningActive = true;
+ _setListeners(height, doSingleScan: doSingleScan);
+ }
+
+ // @action
+ // Future registerSilentPaymentsKey(bool register) async {
+ // silentPaymentsScanningActive = active;
+
+ // if (active) {
+ // syncStatus = AttemptingScanSyncStatus();
+
+ // final tip = await getUpdatedChainTip();
+
+ // if (tip == walletInfo.restoreHeight) {
+ // syncStatus = SyncedTipSyncStatus(tip);
+ // return;
+ // }
+
+ // if (tip > walletInfo.restoreHeight) {
+ // _setListeners(walletInfo.restoreHeight, chainTipParam: _currentChainTip);
+ // }
+ // } else {
+ // alwaysScan = false;
+
+ // _isolate?.then((value) => value.kill(priority: Isolate.immediate));
+
+ // if (electrumClient.isConnected) {
+ // syncStatus = SyncedSyncStatus();
+ // } else {
+ // syncStatus = NotConnectedSyncStatus();
+ // }
+ // }
+ // }
+
+ @action
+ Future registerSilentPaymentsKey() async {
+ // final registered = await electrumClient.tweaksRegister(
+ // secViewKey: walletAddresses.silentAddress!.b_scan.toHex(),
+ // pubSpendKey: walletAddresses.silentAddress!.B_spend.toHex(),
+ // labels: walletAddresses.silentAddresses
+ // .where((addr) => addr.type == SilentPaymentsAddresType.p2sp && addr.labelIndex >= 1)
+ // .map((addr) => addr.labelIndex)
+ // .toList(),
+ // );
+
+ // print("registered: $registered");
+ }
+
+ @action
+ void _updateSilentAddressRecord(BitcoinUnspent unspent) {
+ final receiveAddressRecord = unspent.bitcoinAddressRecord as BitcoinReceivedSPAddressRecord;
+ final silentAddress = walletAddresses.silentAddress!;
+ final silentPaymentAddress = SilentPaymentAddress(
+ version: silentAddress.version,
+ B_scan: silentAddress.B_scan,
+ B_spend: receiveAddressRecord.labelHex != null
+ ? silentAddress.B_spend.tweakAdd(
+ BigintUtils.fromBytes(BytesUtils.fromHexString(receiveAddressRecord.labelHex!)),
+ )
+ : silentAddress.B_spend,
+ );
+
+ final addressRecord = walletAddresses.silentAddresses
+ .firstWhere((address) => address.address == silentPaymentAddress.toString());
+ addressRecord.txCount += 1;
+ addressRecord.balance += unspent.value;
+
+ walletAddresses.addSilentAddresses(
+ [unspent.bitcoinAddressRecord as BitcoinSilentPaymentAddressRecord],
+ );
+ }
+
+ @override
+ @action
+ Future handleWorkerResponse(dynamic message) async {
+ super.handleWorkerResponse(message);
+
+ Map messageJson;
+ if (message is String) {
+ messageJson = jsonDecode(message) as Map;
+ } else {
+ messageJson = message as Map;
+ }
+ final workerMethod = messageJson['method'] as String;
+
+ switch (workerMethod) {
+ case ElectrumRequestMethods.tweaksSubscribeMethod:
+ final response = ElectrumWorkerTweaksSubscribeResponse.fromJson(messageJson);
+ onTweaksSyncResponse(response.result);
+ break;
+ }
+ }
+
+ @action
+ Future onTweaksSyncResponse(TweaksSyncResponse result) async {
+ if (result.transactions?.isNotEmpty == true) {
+ for (final map in result.transactions!.entries) {
+ final txid = map.key;
+ final tx = map.value;
+
+ if (tx.unspents != null) {
+ final existingTxInfo = transactionHistory.transactions[txid];
+ final txAlreadyExisted = existingTxInfo != null;
+
+ // Updating tx after re-scanned
+ if (txAlreadyExisted) {
+ existingTxInfo.amount = tx.amount;
+ existingTxInfo.confirmations = tx.confirmations;
+ existingTxInfo.height = tx.height;
+
+ final newUnspents = tx.unspents!
+ .where((unspent) => !(existingTxInfo.unspents?.any((element) =>
+ element.hash.contains(unspent.hash) &&
+ element.vout == unspent.vout &&
+ element.value == unspent.value) ??
+ false))
+ .toList();
+
+ if (newUnspents.isNotEmpty) {
+ newUnspents.forEach(_updateSilentAddressRecord);
+
+ existingTxInfo.unspents ??= [];
+ existingTxInfo.unspents!.addAll(newUnspents);
+
+ final newAmount = newUnspents.length > 1
+ ? newUnspents.map((e) => e.value).reduce((value, unspent) => value + unspent)
+ : newUnspents[0].value;
+
+ if (existingTxInfo.direction == TransactionDirection.incoming) {
+ existingTxInfo.amount += newAmount;
+ }
+
+ // Updates existing TX
+ transactionHistory.addOne(existingTxInfo);
+ // Update balance record
+ balance[currency]!.confirmed += newAmount;
+ }
+ } else {
+ // else: First time seeing this TX after scanning
+ tx.unspents!.forEach(_updateSilentAddressRecord);
+
+ // Add new TX record
+ transactionHistory.addOne(tx);
+ // Update balance record
+ balance[currency]!.confirmed += tx.amount;
+ }
+
+ await updateAllUnspents();
+ }
+ }
+ }
+
+ final newSyncStatus = result.syncStatus;
+
+ if (newSyncStatus != null) {
+ if (newSyncStatus is UnsupportedSyncStatus) {
+ nodeSupportsSilentPayments = false;
+ }
+
+ if (newSyncStatus is SyncingSyncStatus) {
+ syncStatus = SyncingSyncStatus(newSyncStatus.blocksLeft, newSyncStatus.ptc);
+ } else {
+ syncStatus = newSyncStatus;
+ }
+
+ await walletInfo.updateRestoreHeight(result.height!);
+ }
+ }
+
+ @action
+ Future _setListeners(int height, {bool? doSingleScan}) async {
+ if (currentChainTip == null) {
+ throw Exception("currentChainTip is null");
+ }
+
+ final chainTip = currentChainTip!;
+
+ if (chainTip == height) {
+ syncStatus = SyncedSyncStatus();
+ return;
+ }
+
+ syncStatus = AttemptingScanSyncStatus();
+
+ workerSendPort!.send(
+ ElectrumWorkerTweaksSubscribeRequest(
+ scanData: ScanData(
+ silentAddress: walletAddresses.silentAddress!,
+ network: network,
+ height: height,
+ chainTip: chainTip,
+ transactionHistoryIds: transactionHistory.transactions.keys.toList(),
+ labels: walletAddresses.labels,
+ labelIndexes: walletAddresses.silentAddresses
+ .where((addr) => addr.type == SilentPaymentsAddresType.p2sp && addr.labelIndex >= 1)
+ .map((addr) => addr.labelIndex)
+ .toList(),
+ isSingleScan: doSingleScan ?? false,
+ ),
+ ).toJson(),
+ );
+ }
+
+ @override
+ @action
+ Future