Merge branch 'electrum-sp-refactors' into improve-sending-tx-for-electrum

This commit is contained in:
Omar Hatem 2024-11-06 18:22:46 +02:00 committed by GitHub
commit 5b60fb1f0c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
119 changed files with 4938 additions and 2736 deletions

1
.gitignore vendored
View file

@ -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

View file

@ -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.

View file

@ -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`

View file

@ -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.

View file

@ -14,7 +14,8 @@
<!-- required for API 18 - 30 -->
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" android:maxSdkVersion="30" />
<!-- required for API <= 29 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="29" />

View file

@ -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:

View file

@ -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 '';
}

View file

@ -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<String, dynamic>,
),
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(),
});
}

View file

@ -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;
}

View file

@ -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<List<HardwareAccountData>> getAvailableAccounts(
{int index = 0, int limit = 5}) async {
Future<List<HardwareAccountData>> 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,

View file

@ -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<OutputInfo> outputs;
final BitcoinTransactionPriority? priority;
final TransactionPriority? priority;
final int? feeRate;
final UnspentCoinType coinTypeToSpendFrom;
}

View file

@ -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<BitcoinTransactionPriority> 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<LitecoinTransactionPriority> 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<ElectrumTransactionPriority> 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<BitcoinCashTransactionPriority> 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<String, int> toJson() {
return {
'unimportant': unimportant,
'normal': normal,
'elevated': elevated,
'priority': priority,
'custom': custom,
};
}
static BitcoinTransactionPriorities fromJson(Map<String, dynamic> 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)';
}
factory ElectrumTransactionPriorities.fromList(List<int> list) {
if (list.length != 3) {
throw Exception(
'Unexpected list length: ${list.length} for BitcoinElectrumTransactionPriorities.fromList');
}
return ElectrumTransactionPriorities(
slow: list[0],
medium: list[1],
fast: list[2],
custom: 0,
);
}
@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;
Map<String, int> toJson() {
return {
'slow': slow,
'medium': medium,
'fast': fast,
'custom': custom,
};
}
return label;
static ElectrumTransactionPriorities fromJson(Map<String, dynamic> 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<String, dynamic> 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');
}
}

View file

@ -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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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?,
);
bool operator ==(Object o) {
if (identical(this, o)) return true;
return o is BitcoinUnspent && hash == o.hash && vout == o.vout;
}
@override
Map<String, dynamic> toJson() {
final json = <String, dynamic>{
'address_record': bitcoinAddressRecord.toJSON(),
'tx_hash': hash,
'value': value,
'tx_pos': vout,
'silent_payment_tweak': silentPaymentTweak,
'silent_payment_label': silentPaymentLabel,
};
return json;
}
String? silentPaymentTweak;
String? silentPaymentLabel;
int get hashCode => Object.hash(hash, vout);
}

View file

@ -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<dynamic>? _receiveStream;
BitcoinWalletBase({
required String password,
required WalletInfo walletInfo,
required Box<UnspentCoinsInfo> unspentCoinsInfo,
required EncryptionFileUtils encryptionFileUtils,
Uint8List? seedBytes,
List<int>? seedBytes,
String? mnemonic,
String? xpub,
String? addressPageType,
@ -45,6 +57,8 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
List<BitcoinSilentPaymentAddressRecord>? 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<String, int>? initialRegularAddressIndex,
Map<String, int>? initialChangeAddressIndex,
int initialSilentAddressIndex = 0,
required bool mempoolAPIEnabled,
}) async {
late Uint8List seedBytes;
late List<int> seedBytes;
final Map<CWBitcoinDerivationType, Bip32Slip10Secp256k1> hdWallets = {};
switch (walletInfo.derivationInfo?.derivationType) {
case DerivationType.bip39:
seedBytes = await bip39.mnemonicToSeed(
mnemonic,
passphrase: passphrase ?? "",
);
for (final derivation in walletInfo.derivations ?? <DerivationInfo>[]) {
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,30 +214,45 @@ 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<int>? seedBytes = null;
final Map<CWBitcoinDerivationType, Bip32Slip10Secp256k1> 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 ?? <DerivationInfo>[]) {
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(
mnemonic: mnemonic,
xpub: keysData.xPub,
@ -231,9 +271,59 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
addressPageType: snp?.addressPageType,
networkParam: network,
alwaysScan: alwaysScan,
mempoolAPIEnabled: mempoolAPIEnabled,
hdWallets: hdWallets,
);
}
Future<bool> getNodeIsElectrs() async {
final version = await sendWorker(ElectrumWorkerGetVersionRequest()) as List<String>;
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<bool> 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 = <PSBTReadyUtxoWithAddress>[];
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<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 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<void> 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<void> updateAllUnspents() async {
// List<BitcoinUnspent> 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<void> startSync() async {
await _setInitialScanHeight();
await super.startSync();
if (alwaysScan == true) {
_setListeners(walletInfo.restoreHeight);
}
}
@action
@override
Future<void> rescan({required int height, bool? doSingleScan}) async {
silentPaymentsScanningActive = true;
_setListeners(height, doSingleScan: doSingleScan);
}
// @action
// Future<void> 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<void> 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<void> handleWorkerResponse(dynamic message) async {
super.handleWorkerResponse(message);
Map<String, dynamic> messageJson;
if (message is String) {
messageJson = jsonDecode(message) as Map<String, dynamic>;
} else {
messageJson = message as Map<String, dynamic>;
}
final workerMethod = messageJson['method'] as String;
switch (workerMethod) {
case ElectrumRequestMethods.tweaksSubscribeMethod:
final response = ElectrumWorkerTweaksSubscribeResponse.fromJson(messageJson);
onTweaksSyncResponse(response.result);
break;
}
}
@action
Future<void> 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<void> _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<Map<String, ElectrumTransactionInfo>> fetchTransactions() async {
throw UnimplementedError();
// try {
// final Map<String, ElectrumTransactionInfo> historiesWithDetails = {};
// await Future.wait(
// BITCOIN_ADDRESS_TYPES.map(
// (type) => fetchTransactionsForAddressType(historiesWithDetails, type),
// ),
// );
// transactionHistory.transactions.values.forEach((tx) async {
// final isPendingSilentPaymentUtxo =
// (tx.isPending || tx.confirmations == 0) && historiesWithDetails[tx.id] == null;
// if (isPendingSilentPaymentUtxo) {
// final info = await fetchTransactionInfo(hash: tx.id, height: tx.height);
// if (info != null) {
// tx.confirmations = info.confirmations;
// tx.isPending = tx.confirmations == 0;
// transactionHistory.addOne(tx);
// await transactionHistory.save();
// }
// }
// });
// return historiesWithDetails;
// } catch (e) {
// print("fetchTransactions $e");
// return {};
// }
}
@override
@action
Future<void> updateTransactions([List<BitcoinAddressRecord>? addresses]) async {
super.updateTransactions();
transactionHistory.transactions.values.forEach((tx) {
if (tx.unspents != null &&
tx.unspents!.isNotEmpty &&
tx.height != null &&
tx.height! > 0 &&
(currentChainTip ?? 0) > 0) {
tx.confirmations = currentChainTip! - tx.height! + 1;
}
});
}
// @action
// Future<ElectrumBalance> fetchBalances() async {
// final balance = await super.fetchBalances();
// int totalFrozen = balance.frozen;
// int totalConfirmed = balance.confirmed;
// // Add values from unspent coins that are not fetched by the address list
// // i.e. scanned silent payments
// transactionHistory.transactions.values.forEach((tx) {
// if (tx.unspents != null) {
// tx.unspents!.forEach((unspent) {
// if (unspent.bitcoinAddressRecord is BitcoinSilentPaymentAddressRecord) {
// if (unspent.isFrozen) totalFrozen += unspent.value;
// totalConfirmed += unspent.value;
// }
// });
// }
// });
// return ElectrumBalance(
// confirmed: totalConfirmed,
// unconfirmed: balance.unconfirmed,
// frozen: totalFrozen,
// );
// }
@override
@action
Future<void> onHeadersResponse(ElectrumHeaderResponse response) async {
super.onHeadersResponse(response);
_setInitialScanHeight();
// New headers received, start scanning
if (alwaysScan == true && syncStatus is SyncedSyncStatus) {
_setListeners(walletInfo.restoreHeight);
}
}
Future<void> _setInitialScanHeight() async {
final validChainTip = currentChainTip != null && currentChainTip != 0;
if (validChainTip && walletInfo.restoreHeight == 0) {
await walletInfo.updateRestoreHeight(currentChainTip!);
}
}
static String _hardenedDerivationPath(String derivationPath) =>
derivationPath.substring(0, derivationPath.lastIndexOf("'") + 1);
@override
@action
void syncStatusReaction(SyncStatus syncStatus) {
switch (syncStatus.runtimeType) {
case SyncingSyncStatus:
return;
case SyncedTipSyncStatus:
// Message is shown on the UI for 3 seconds, then reverted to synced
Timer(Duration(seconds: 3), () {
if (this.syncStatus is SyncedTipSyncStatus) this.syncStatus = SyncedSyncStatus();
});
break;
default:
super.syncStatusReaction(syncStatus);
}
}
}

View file

@ -1,7 +1,6 @@
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:blockchain_utils/bip/bip/bip32/bip32.dart';
import 'package:blockchain_utils/blockchain_utils.dart';
import 'package:cw_bitcoin/electrum_wallet_addresses.dart';
import 'package:cw_bitcoin/utils.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:mobx/mobx.dart';
@ -12,33 +11,101 @@ class BitcoinWalletAddresses = BitcoinWalletAddressesBase with _$BitcoinWalletAd
abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with Store {
BitcoinWalletAddressesBase(
WalletInfo walletInfo, {
required super.mainHd,
required super.sideHd,
required super.network,
required super.isHardwareWallet,
required super.hdWallets,
super.initialAddresses,
super.initialRegularAddressIndex,
super.initialChangeAddressIndex,
super.initialSilentAddresses,
super.initialSilentAddressIndex = 0,
super.masterHd,
}) : super(walletInfo);
@override
String getAddress(
{required int index, required Bip32Slip10Secp256k1 hd, BitcoinAddressType? addressType}) {
if (addressType == P2pkhAddressType.p2pkh)
return generateP2PKHAddress(hd: hd, index: index, network: network);
Future<void> init() async {
await generateInitialAddresses(type: SegwitAddresType.p2wpkh);
if (addressType == SegwitAddresType.p2tr)
return generateP2TRAddress(hd: hd, index: index, network: network);
if (!isHardwareWallet) {
await generateInitialAddresses(type: P2pkhAddressType.p2pkh);
await generateInitialAddresses(type: P2shAddressType.p2wpkhInP2sh);
await generateInitialAddresses(type: SegwitAddresType.p2tr);
await generateInitialAddresses(type: SegwitAddresType.p2wsh);
}
if (addressType == SegwitAddresType.p2wsh)
return generateP2WSHAddress(hd: hd, index: index, network: network);
await updateAddressesInBox();
}
if (addressType == P2shAddressType.p2wpkhInP2sh)
return generateP2SHAddress(hd: hd, index: index, network: network);
@override
BitcoinBaseAddress generateAddress({
required CWBitcoinDerivationType derivationType,
required bool isChange,
required int index,
required BitcoinAddressType addressType,
required BitcoinDerivationInfo derivationInfo,
}) {
final hdWallet = hdWallets[derivationType]!;
return generateP2WPKHAddress(hd: hd, index: index, network: network);
if (derivationType == CWBitcoinDerivationType.old) {
final pub = hdWallet
.childKey(Bip32KeyIndex(isChange ? 1 : 0))
.childKey(Bip32KeyIndex(index))
.publicKey;
switch (addressType) {
case P2pkhAddressType.p2pkh:
return ECPublic.fromBip32(pub).toP2pkhAddress();
case SegwitAddresType.p2tr:
return ECPublic.fromBip32(pub).toP2trAddress();
case SegwitAddresType.p2wsh:
return ECPublic.fromBip32(pub).toP2wshAddress();
case P2shAddressType.p2wpkhInP2sh:
return ECPublic.fromBip32(pub).toP2wpkhInP2sh();
case SegwitAddresType.p2wpkh:
return ECPublic.fromBip32(pub).toP2wpkhAddress();
default:
throw ArgumentError('Invalid address type');
}
}
switch (addressType) {
case P2pkhAddressType.p2pkh:
return P2pkhAddress.fromDerivation(
bip32: hdWallet,
derivationInfo: derivationInfo,
isChange: isChange,
index: index,
);
case SegwitAddresType.p2tr:
return P2trAddress.fromDerivation(
bip32: hdWallet,
derivationInfo: derivationInfo,
isChange: isChange,
index: index,
);
case SegwitAddresType.p2wsh:
return P2wshAddress.fromDerivation(
bip32: hdWallet,
derivationInfo: derivationInfo,
isChange: isChange,
index: index,
);
case P2shAddressType.p2wpkhInP2sh:
return P2shAddress.fromDerivation(
bip32: hdWallet,
derivationInfo: derivationInfo,
isChange: isChange,
index: index,
type: P2shAddressType.p2wpkhInP2sh,
);
case SegwitAddresType.p2wpkh:
return P2wpkhAddress.fromDerivation(
bip32: hdWallet,
derivationInfo: derivationInfo,
isChange: isChange,
index: index,
);
default:
throw ArgumentError('Invalid address type');
}
}
}

View file

@ -7,8 +7,6 @@ class BitcoinNewWalletCredentials extends WalletCredentials {
required String name,
WalletInfo? walletInfo,
String? password,
DerivationType? derivationType,
String? derivationPath,
String? passphrase,
this.mnemonic,
String? parentAddress,
@ -28,19 +26,15 @@ class BitcoinRestoreWalletFromSeedCredentials extends WalletCredentials {
required String name,
required String password,
required this.mnemonic,
required super.derivations,
WalletInfo? walletInfo,
required DerivationType derivationType,
required String derivationPath,
String? passphrase,
}) : super(
name: name,
password: password,
passphrase: passphrase,
walletInfo: walletInfo,
derivationInfo: DerivationInfo(
derivationType: derivationType,
derivationPath: derivationPath,
));
);
final String mnemonic;
}

View file

@ -2,7 +2,6 @@ import 'dart:io';
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:cw_bitcoin/bitcoin_mnemonic.dart';
import 'package:cw_bitcoin/bitcoin_mnemonics_bip39.dart';
import 'package:cw_bitcoin/mnemonic_is_incorrect_exception.dart';
import 'package:cw_bitcoin/bitcoin_wallet_creation_credentials.dart';
import 'package:cw_core/encryption_file_utils.dart';
import 'package:cw_core/unspent_coins_info.dart';
@ -14,18 +13,24 @@ import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:hive/hive.dart';
import 'package:collection/collection.dart';
import 'package:bip39/bip39.dart' as bip39;
class BitcoinWalletService extends WalletService<
BitcoinNewWalletCredentials,
BitcoinRestoreWalletFromSeedCredentials,
BitcoinRestoreWalletFromWIFCredentials,
BitcoinRestoreWalletFromHardware> {
BitcoinWalletService(this.walletInfoSource, this.unspentCoinsInfoSource, this.alwaysScan, this.isDirect);
BitcoinWalletService(
this.walletInfoSource,
this.unspentCoinsInfoSource,
this.alwaysScan,
this.isDirect,
this.mempoolAPIEnabled,
);
final Box<WalletInfo> walletInfoSource;
final Box<UnspentCoinsInfo> unspentCoinsInfoSource;
final bool alwaysScan;
final bool mempoolAPIEnabled;
final bool isDirect;
@override
@ -57,6 +62,7 @@ class BitcoinWalletService extends WalletService<
unspentCoinsInfo: unspentCoinsInfoSource,
network: network,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
mempoolAPIEnabled: mempoolAPIEnabled,
);
await wallet.save();
@ -80,6 +86,7 @@ class BitcoinWalletService extends WalletService<
walletInfo: walletInfo,
unspentCoinsInfo: unspentCoinsInfoSource,
alwaysScan: alwaysScan,
mempoolAPIEnabled: mempoolAPIEnabled,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
);
await wallet.init();
@ -93,6 +100,7 @@ class BitcoinWalletService extends WalletService<
walletInfo: walletInfo,
unspentCoinsInfo: unspentCoinsInfoSource,
alwaysScan: alwaysScan,
mempoolAPIEnabled: mempoolAPIEnabled,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
);
await wallet.init();
@ -118,6 +126,7 @@ class BitcoinWalletService extends WalletService<
walletInfo: currentWalletInfo,
unspentCoinsInfo: unspentCoinsInfoSource,
alwaysScan: alwaysScan,
mempoolAPIEnabled: mempoolAPIEnabled,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
);
@ -146,6 +155,7 @@ class BitcoinWalletService extends WalletService<
unspentCoinsInfo: unspentCoinsInfoSource,
networkParam: network,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
mempoolAPIEnabled: mempoolAPIEnabled,
);
await wallet.save();
await wallet.init();
@ -160,10 +170,6 @@ class BitcoinWalletService extends WalletService<
@override
Future<BitcoinWallet> restoreFromSeed(BitcoinRestoreWalletFromSeedCredentials credentials,
{bool? isTestnet}) async {
if (!validateMnemonic(credentials.mnemonic) && !bip39.validateMnemonic(credentials.mnemonic)) {
throw BitcoinMnemonicIsIncorrectException();
}
final network = isTestnet == true ? BitcoinNetwork.testnet : BitcoinNetwork.mainnet;
credentials.walletInfo?.network = network.value;
@ -175,8 +181,8 @@ class BitcoinWalletService extends WalletService<
unspentCoinsInfo: unspentCoinsInfoSource,
network: network,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
mempoolAPIEnabled: mempoolAPIEnabled,
);
await wallet.save();
await wallet.init();
return wallet;
}

View file

@ -3,17 +3,9 @@ import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:cw_bitcoin/bitcoin_amount_format.dart';
import 'package:flutter/foundation.dart';
import 'package:rxdart/rxdart.dart';
enum ConnectionStatus { connected, disconnected, connecting, failed }
String jsonrpcparams(List<Object> params) {
final _params = params.map((val) => '"${val.toString()}"').join(',');
return '[$_params]';
}
String jsonrpc(
{required String method,
required List<Object> params,
@ -317,13 +309,38 @@ class ElectrumClient {
Future<Map<String, dynamic>> getHeader({required int height}) async =>
await call(method: 'blockchain.block.get_header', params: [height]) as Map<String, dynamic>;
BehaviorSubject<Object>? tweaksSubscribe({required int height, required int count}) {
return subscribe<Object>(
BehaviorSubject<Object>? tweaksSubscribe({required int height, required int count}) =>
subscribe<Object>(
id: 'blockchain.tweaks.subscribe',
method: 'blockchain.tweaks.subscribe',
params: [height, count, false],
params: [height, count, true],
);
Future<dynamic> tweaksRegister({
required String secViewKey,
required String pubSpendKey,
List<int> labels = const [],
}) =>
call(
method: 'blockchain.tweaks.register',
params: [secViewKey, pubSpendKey, labels],
);
Future<dynamic> tweaksErase({required String pubSpendKey}) => call(
method: 'blockchain.tweaks.erase',
params: [pubSpendKey],
);
BehaviorSubject<Object>? tweaksScan({required String pubSpendKey}) => subscribe<Object>(
id: 'blockchain.tweaks.scan',
method: 'blockchain.tweaks.scan',
params: [pubSpendKey],
);
Future<dynamic> tweaksGet({required String pubSpendKey}) => call(
method: 'blockchain.tweaks.get',
params: [pubSpendKey],
);
}
Future<dynamic> getTweaks({required int height}) async =>
await callWithTimeout(method: 'blockchain.tweaks.subscribe', params: [height, 1, false]);
@ -369,20 +386,20 @@ class ElectrumClient {
return [];
});
Future<List<int>> feeRates({BasedUtxoNetwork? network}) async {
try {
final topDoubleString = await estimatefee(p: 1);
final middleDoubleString = await estimatefee(p: 5);
final bottomDoubleString = await estimatefee(p: 10);
final top = (stringDoubleToBitcoinAmount(topDoubleString.toString()) / 1000).round();
final middle = (stringDoubleToBitcoinAmount(middleDoubleString.toString()) / 1000).round();
final bottom = (stringDoubleToBitcoinAmount(bottomDoubleString.toString()) / 1000).round();
// Future<List<int>> feeRates({BasedUtxoNetwork? network}) async {
// try {
// final topDoubleString = await estimatefee(p: 1);
// final middleDoubleString = await estimatefee(p: 5);
// final bottomDoubleString = await estimatefee(p: 10);
// final top = (stringDoubleToBitcoinAmount(topDoubleString.toString()) / 1000).round();
// final middle = (stringDoubleToBitcoinAmount(middleDoubleString.toString()) / 1000).round();
// final bottom = (stringDoubleToBitcoinAmount(bottomDoubleString.toString()) / 1000).round();
return [bottom, middle, top];
} catch (_) {
return [];
}
}
// return [bottom, middle, top];
// } catch (_) {
// return [];
// }
// }
// https://electrumx.readthedocs.io/en/latest/protocol-methods.html#blockchain-headers-subscribe
// example response:
@ -527,6 +544,7 @@ class ElectrumClient {
_tasks[method]?.subject?.add(params.last);
break;
case 'blockchain.tweaks.subscribe':
case 'blockchain.tweaks.scan':
final params = request['params'] as List<dynamic>;
_tasks[_tasks.keys.first]?.subject?.add(params.last);
break;

View file

@ -1,5 +1,5 @@
import 'dart:convert';
import 'package:cw_bitcoin/bitcoin_amount_format.dart';
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:cw_core/balance.dart';
class ElectrumBalance extends Balance {
@ -31,32 +31,35 @@ class ElectrumBalance extends Balance {
int confirmed;
int unconfirmed;
final int frozen;
int frozen;
int secondConfirmed = 0;
int secondUnconfirmed = 0;
@override
String get formattedAvailableBalance =>
bitcoinAmountToString(amount: confirmed - frozen);
BitcoinAmountUtils.bitcoinAmountToString(amount: confirmed - frozen);
@override
String get formattedAdditionalBalance => bitcoinAmountToString(amount: unconfirmed);
String get formattedAdditionalBalance =>
BitcoinAmountUtils.bitcoinAmountToString(amount: unconfirmed);
@override
String get formattedUnAvailableBalance {
final frozenFormatted = bitcoinAmountToString(amount: frozen);
final frozenFormatted = BitcoinAmountUtils.bitcoinAmountToString(amount: frozen);
return frozenFormatted == '0.0' ? '' : frozenFormatted;
}
@override
String get formattedSecondAvailableBalance => bitcoinAmountToString(amount: secondConfirmed);
String get formattedSecondAvailableBalance =>
BitcoinAmountUtils.bitcoinAmountToString(amount: secondConfirmed);
@override
String get formattedSecondAdditionalBalance => bitcoinAmountToString(amount: secondUnconfirmed);
String get formattedSecondAdditionalBalance =>
BitcoinAmountUtils.bitcoinAmountToString(amount: secondUnconfirmed);
@override
String get formattedFullAvailableBalance =>
bitcoinAmountToString(amount: confirmed + secondConfirmed - frozen);
BitcoinAmountUtils.bitcoinAmountToString(amount: confirmed + secondConfirmed - frozen);
String toJSON() => json.encode({
'confirmed': confirmed,

View file

@ -4,11 +4,8 @@ import 'package:cw_core/encryption_file_utils.dart';
import 'package:cw_bitcoin/electrum_transaction_info.dart';
import 'package:cw_core/pathForWallet.dart';
import 'package:cw_core/transaction_history.dart';
import 'package:cw_core/utils/file.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:mobx/mobx.dart';
import 'package:cw_core/transaction_history.dart';
import 'package:cw_bitcoin/electrum_transaction_info.dart';
part 'electrum_transaction_history.g.dart';

View file

@ -1,9 +1,7 @@
import 'dart:convert';
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:cw_bitcoin/address_from_output.dart';
import 'package:cw_bitcoin/bitcoin_address_record.dart';
import 'package:cw_bitcoin/bitcoin_amount_format.dart';
import 'package:cw_bitcoin/bitcoin_unspent.dart';
import 'package:cw_core/transaction_direction.dart';
import 'package:cw_core/transaction_info.dart';
@ -12,17 +10,39 @@ import 'package:cw_core/wallet_type.dart';
import 'package:hex/hex.dart';
class ElectrumTransactionBundle {
ElectrumTransactionBundle(this.originalTransaction,
{required this.ins, required this.confirmations, this.time});
ElectrumTransactionBundle(
this.originalTransaction, {
required this.ins,
required this.confirmations,
this.time,
});
final BtcTransaction originalTransaction;
final List<BtcTransaction> ins;
final int? time;
final int confirmations;
Map<String, dynamic> toJson() {
return {
'originalTransaction': originalTransaction.toHex(),
'ins': ins.map((e) => e.toHex()).toList(),
'confirmations': confirmations,
'time': time,
};
}
static ElectrumTransactionBundle fromJson(Map<String, dynamic> data) {
return ElectrumTransactionBundle(
BtcTransaction.fromRaw(data['originalTransaction'] as String),
ins: (data['ins'] as List<Object>).map((e) => BtcTransaction.fromRaw(e as String)).toList(),
confirmations: data['confirmations'] as int,
time: data['time'] as int?,
);
}
}
class ElectrumTransactionInfo extends TransactionInfo {
List<BitcoinSilentPaymentsUnspent>? unspents;
List<BitcoinUnspent>? unspents;
bool isReceivedSilentPayment;
ElectrumTransactionInfo(
@ -75,7 +95,8 @@ class ElectrumTransactionInfo extends TransactionInfo {
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());
inputsAmount +=
BitcoinAmountUtils.stringDoubleToBitcoinAmount((out['value'] as double? ?? 0).toString());
if (outAddresses?.intersection(addressesSet).isNotEmpty ?? false) {
direction = TransactionDirection.outgoing;
@ -85,7 +106,8 @@ class ElectrumTransactionInfo extends TransactionInfo {
for (dynamic out in vout) {
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 = BitcoinAmountUtils.stringDoubleToBitcoinAmount(
(out['value'] as double? ?? 0.0).toString());
totalOutAmount += value;
if ((direction == TransactionDirection.incoming && ntrs.isNotEmpty) ||
@ -121,22 +143,33 @@ class ElectrumTransactionInfo extends TransactionInfo {
List<String> inputAddresses = [];
List<String> outputAddresses = [];
try {
for (var i = 0; i < bundle.originalTransaction.inputs.length; i++) {
final input = bundle.originalTransaction.inputs[i];
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(
BitcoinAddressUtils.addressFromOutputScript(outTransaction.scriptPubKey, network))) {
direction = TransactionDirection.outgoing;
inputAddresses.add(addressFromOutputScript(outTransaction.scriptPubKey, network));
inputAddresses.add(
BitcoinAddressUtils.addressFromOutputScript(outTransaction.scriptPubKey, network));
}
}
} catch (e) {
print(bundle.originalTransaction.txId());
print("original: ${bundle.originalTransaction}");
print("bundle.inputs: ${bundle.originalTransaction.inputs}");
print("ins: ${bundle.ins}");
rethrow;
}
final receivedAmounts = <int>[];
for (final out in bundle.originalTransaction.outputs) {
totalOutAmount += out.amount.toInt();
final addressExists = addresses.contains(addressFromOutputScript(out.scriptPubKey, network));
final address = addressFromOutputScript(out.scriptPubKey, network);
final addressExists = addresses
.contains(BitcoinAddressUtils.addressFromOutputScript(out.scriptPubKey, network));
final address = BitcoinAddressUtils.addressFromOutputScript(out.scriptPubKey, network);
if (address.isNotEmpty) outputAddresses.add(address);
@ -208,8 +241,7 @@ class ElectrumTransactionInfo extends TransactionInfo {
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) => BitcoinUnspent.fromJSON(null, unspent as Map<String, dynamic>))
.toList(),
isReceivedSilentPayment: data['isReceivedSilentPayment'] as bool? ?? false,
);
@ -221,11 +253,11 @@ class ElectrumTransactionInfo extends TransactionInfo {
@override
String amountFormatted() =>
'${formatAmount(bitcoinAmountToString(amount: amount))} ${walletTypeToCryptoCurrency(type).title}';
'${formatAmount(BitcoinAmountUtils.bitcoinAmountToString(amount: amount))} ${walletTypeToCryptoCurrency(type).title}';
@override
String? feeFormatted() => fee != null
? '${formatAmount(bitcoinAmountToString(amount: fee!))} ${walletTypeToCryptoCurrency(type).title}'
? '${formatAmount(BitcoinAmountUtils.bitcoinAmountToString(amount: fee!))} ${walletTypeToCryptoCurrency(type).title}'
: '';
@override

File diff suppressed because it is too large Load diff

View file

@ -11,6 +11,8 @@ import 'package:mobx/mobx.dart';
part 'electrum_wallet_addresses.g.dart';
enum CWBitcoinDerivationType { old, electrum, bip39, mweb }
class ElectrumWalletAddresses = ElectrumWalletAddressesBase with _$ElectrumWalletAddresses;
const List<BitcoinAddressType> BITCOIN_ADDRESS_TYPES = [
@ -33,8 +35,7 @@ const List<BitcoinAddressType> BITCOIN_CASH_ADDRESS_TYPES = [
abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
ElectrumWalletAddressesBase(
WalletInfo walletInfo, {
required this.mainHd,
required this.sideHd,
required this.hdWallets,
required this.network,
required this.isHardwareWallet,
List<BitcoinAddressRecord>? initialAddresses,
@ -43,18 +44,15 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
List<BitcoinSilentPaymentAddressRecord>? initialSilentAddresses,
int initialSilentAddressIndex = 0,
List<BitcoinAddressRecord>? initialMwebAddresses,
Bip32Slip10Secp256k1? masterHd,
BitcoinAddressType? initialAddressPageType,
}) : _addresses = ObservableList<BitcoinAddressRecord>.of((initialAddresses ?? []).toSet()),
}) : _allAddresses = ObservableList.of(initialAddresses ?? []),
addressesByReceiveType =
ObservableList<BaseBitcoinAddressRecord>.of((<BitcoinAddressRecord>[]).toSet()),
receiveAddresses = ObservableList<BitcoinAddressRecord>.of((initialAddresses ?? [])
.where((addressRecord) => !addressRecord.isHidden && !addressRecord.isUsed)
.toSet()),
changeAddresses = ObservableList<BitcoinAddressRecord>.of((initialAddresses ?? [])
.where((addressRecord) => addressRecord.isHidden && !addressRecord.isUsed)
.toSet()),
receiveAddresses = ObservableList<BitcoinAddressRecord>.of(
(initialAddresses ?? []).where((addressRecord) => !addressRecord.isChange).toSet()),
// TODO: feature to change change address type. For now fixed to p2wpkh, the cheapest type
changeAddresses = ObservableList<BitcoinAddressRecord>.of(
(initialAddresses ?? []).where((addressRecord) => addressRecord.isChange).toSet()),
currentReceiveAddressIndexByType = initialRegularAddressIndex ?? {},
currentChangeAddressIndexByType = initialChangeAddressIndex ?? {},
_addressPageType = initialAddressPageType ??
@ -67,34 +65,24 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
mwebAddresses =
ObservableList<BitcoinAddressRecord>.of((initialMwebAddresses ?? []).toSet()),
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()),
network: network,
);
// TODO: initial silent address, not every time
silentAddress = SilentPaymentOwner.fromBip32(bip32);
if (silentAddresses.length == 0) {
silentAddresses.add(BitcoinSilentPaymentAddressRecord(
silentAddress.toString(),
index: 0,
isHidden: false,
labelIndex: 1,
name: "",
silentPaymentTweak: null,
network: network,
type: SilentPaymentsAddresType.p2sp,
));
silentAddresses.add(BitcoinSilentPaymentAddressRecord(
silentAddress!.toLabeledSilentPaymentAddress(0).toString(),
index: 0,
isHidden: true,
name: "",
silentPaymentTweak: BytesUtils.toHexString(silentAddress!.generateLabel(0)),
network: network,
labelIndex: 0,
labelHex: BytesUtils.toHexString(silentAddress!.generateLabel(0)),
type: SilentPaymentsAddresType.p2sp,
));
}
}
updateAddressesByMatch();
}
@ -103,7 +91,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
static const defaultChangeAddressesCount = 17;
static const gap = 20;
final ObservableList<BitcoinAddressRecord> _addresses;
final ObservableList<BitcoinAddressRecord> _allAddresses;
final ObservableList<BaseBitcoinAddressRecord> addressesByReceiveType;
final ObservableList<BitcoinAddressRecord> receiveAddresses;
final ObservableList<BitcoinAddressRecord> changeAddresses;
@ -112,8 +100,11 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
// TODO: add this variable in `litecoin_wallet_addresses` and just add a cast in cw_bitcoin to use it
final ObservableList<BitcoinAddressRecord> mwebAddresses;
final BasedUtxoNetwork network;
final Bip32Slip10Secp256k1 mainHd;
final Bip32Slip10Secp256k1 sideHd;
final Map<CWBitcoinDerivationType, Bip32Slip10Secp256k1> hdWallets;
Bip32Slip10Secp256k1 get bip32 =>
hdWallets[CWBitcoinDerivationType.bip39] ?? hdWallets[CWBitcoinDerivationType.electrum]!;
final bool isHardwareWallet;
@observable
@ -129,7 +120,15 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
String? activeSilentAddress;
@computed
List<BitcoinAddressRecord> get allAddresses => _addresses;
List<BitcoinAddressRecord> get allAddresses => _allAddresses.toList();
@computed
Set<String> get allScriptHashes =>
_allAddresses.map((addressRecord) => addressRecord.scriptHash).toSet();
BitcoinAddressRecord getFromAddresses(String address) {
return _allAddresses.firstWhere((element) => element.address == address);
}
@override
@computed
@ -174,31 +173,34 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
return;
}
if (addressPageType == SilentPaymentsAddresType.p2sp) {
final selected = silentAddresses.firstWhere((addressRecord) => addressRecord.address == addr);
late BitcoinSilentPaymentAddressRecord selected;
try {
selected = silentAddresses.firstWhere((addressRecord) => addressRecord.address == addr);
} catch (_) {
selected = silentAddresses[0];
}
if (selected.silentPaymentTweak != null && silentAddress != null) {
if (selected.labelHex != null && silentAddress != null) {
activeSilentAddress =
silentAddress!.toLabeledSilentPaymentAddress(selected.index).toString();
silentAddress!.toLabeledSilentPaymentAddress(selected.labelIndex).toString();
} else {
activeSilentAddress = silentAddress!.toString();
}
return;
}
try {
final addressRecord = _addresses.firstWhere(
final addressRecord = _allAddresses.firstWhere(
(addressRecord) => addressRecord.address == addr,
);
previousAddressRecord = addressRecord;
receiveAddresses.remove(addressRecord);
receiveAddresses.insert(0, addressRecord);
} catch (e) {
print("ElectrumWalletAddressBase: set address ($addr): $e");
}
}
@override
String get primaryAddress => getAddress(index: 0, hd: mainHd, addressType: addressPageType);
String get primaryAddress => _allAddresses.first.address;
Map<String, int> currentReceiveAddressIndexByType;
@ -223,7 +225,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
@computed
int get totalCountOfReceiveAddresses => addressesByReceiveType.fold(0, (acc, addressRecord) {
if (!addressRecord.isHidden) {
if (!addressRecord.isChange) {
return acc + 1;
}
return acc;
@ -231,7 +233,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
@computed
int get totalCountOfChangeAddresses => addressesByReceiveType.fold(0, (acc, addressRecord) {
if (addressRecord.isHidden) {
if (addressRecord.isChange) {
return acc + 1;
}
return acc;
@ -240,26 +242,25 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
@override
Future<void> init() async {
if (walletInfo.type == WalletType.bitcoinCash) {
await _generateInitialAddresses(type: P2pkhAddressType.p2pkh);
await generateInitialAddresses(type: P2pkhAddressType.p2pkh);
} else if (walletInfo.type == WalletType.litecoin) {
await _generateInitialAddresses(type: SegwitAddresType.p2wpkh);
await generateInitialAddresses(type: SegwitAddresType.p2wpkh);
if ((Platform.isAndroid || Platform.isIOS) && !isHardwareWallet) {
await _generateInitialAddresses(type: SegwitAddresType.mweb);
await generateInitialAddresses(type: SegwitAddresType.mweb);
}
} else if (walletInfo.type == WalletType.bitcoin) {
await _generateInitialAddresses();
await generateInitialAddresses(type: SegwitAddresType.p2wpkh);
if (!isHardwareWallet) {
await _generateInitialAddresses(type: P2pkhAddressType.p2pkh);
await _generateInitialAddresses(type: P2shAddressType.p2wpkhInP2sh);
await _generateInitialAddresses(type: SegwitAddresType.p2tr);
await _generateInitialAddresses(type: SegwitAddresType.p2wsh);
await generateInitialAddresses(type: P2pkhAddressType.p2pkh);
await generateInitialAddresses(type: P2shAddressType.p2wpkhInP2sh);
await generateInitialAddresses(type: SegwitAddresType.p2tr);
await generateInitialAddresses(type: SegwitAddresType.p2wsh);
}
}
updateAddressesByMatch();
updateReceiveAddresses();
updateChangeAddresses();
_validateAddresses();
await updateAddressesInBox();
if (currentReceiveAddressIndex >= receiveAddresses.length) {
@ -272,16 +273,13 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
}
@action
Future<BitcoinAddressRecord> getChangeAddress({List<BitcoinUnspent>? inputs, List<BitcoinOutput>? outputs, bool isPegIn = false}) async {
Future<BitcoinAddressRecord> getChangeAddress({
List<BitcoinUnspent>? inputs,
List<BitcoinOutput>? outputs,
bool isPegIn = false,
}) async {
updateChangeAddresses();
if (changeAddresses.isEmpty) {
final newAddresses = await _createNewAddresses(gap,
startIndex: totalCountOfChangeAddresses > 0 ? totalCountOfChangeAddresses - 1 : 0,
isHidden: true);
addAddresses(newAddresses);
}
if (currentChangeAddressIndex >= changeAddresses.length) {
currentChangeAddressIndex = 0;
}
@ -297,7 +295,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
final labels = <String, String>{};
for (int i = 0; i < silentAddresses.length; i++) {
final silentAddressRecord = silentAddresses[i];
final silentPaymentTweak = silentAddressRecord.silentPaymentTweak;
final silentPaymentTweak = silentAddressRecord.labelHex;
if (silentPaymentTweak != null &&
SilentPaymentAddress.regex.hasMatch(silentAddressRecord.address)) {
@ -321,53 +319,87 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
final address = BitcoinSilentPaymentAddressRecord(
silentAddress!.toLabeledSilentPaymentAddress(currentSilentAddressIndex).toString(),
index: currentSilentAddressIndex,
isHidden: false,
labelIndex: currentSilentAddressIndex,
name: label,
silentPaymentTweak:
BytesUtils.toHexString(silentAddress!.generateLabel(currentSilentAddressIndex)),
network: network,
labelHex: BytesUtils.toHexString(silentAddress!.generateLabel(currentSilentAddressIndex)),
type: SilentPaymentsAddresType.p2sp,
);
silentAddresses.add(address);
updateAddressesByMatch();
Future.delayed(Duration.zero, () => updateAddressesByMatch());
return address;
}
final newAddressIndex = addressesByReceiveType.fold(
0, (int acc, addressRecord) => addressRecord.isHidden == false ? acc + 1 : acc);
0, (int acc, addressRecord) => addressRecord.isChange == false ? acc + 1 : acc);
final derivationInfo = BitcoinAddressUtils.getDerivationFromType(addressPageType);
final address = BitcoinAddressRecord(
getAddress(index: newAddressIndex, hd: mainHd, addressType: addressPageType),
getAddress(
derivationType: CWBitcoinDerivationType.bip39,
isChange: false,
index: newAddressIndex,
isHidden: false,
addressType: addressPageType,
derivationInfo: derivationInfo,
),
index: newAddressIndex,
isChange: false,
name: label,
type: addressPageType,
network: network,
derivationInfo: BitcoinAddressUtils.getDerivationFromType(addressPageType),
derivationType: CWBitcoinDerivationType.bip39,
);
_addresses.add(address);
updateAddressesByMatch();
_allAddresses.add(address);
Future.delayed(Duration.zero, () => updateAddressesByMatch());
return address;
}
String getAddress({
BitcoinBaseAddress generateAddress({
required CWBitcoinDerivationType derivationType,
required bool isChange,
required int index,
required Bip32Slip10Secp256k1 hd,
BitcoinAddressType? addressType,
}) =>
'';
required BitcoinAddressType addressType,
required BitcoinDerivationInfo derivationInfo,
}) {
throw UnimplementedError();
}
String getAddress({
required CWBitcoinDerivationType derivationType,
required bool isChange,
required int index,
required BitcoinAddressType addressType,
required BitcoinDerivationInfo derivationInfo,
}) {
return generateAddress(
derivationType: derivationType,
isChange: isChange,
index: index,
addressType: addressType,
derivationInfo: derivationInfo,
).toAddress(network);
}
Future<String> getAddressAsync({
required CWBitcoinDerivationType derivationType,
required bool isChange,
required int index,
required Bip32Slip10Secp256k1 hd,
BitcoinAddressType? addressType,
required BitcoinAddressType addressType,
required BitcoinDerivationInfo derivationInfo,
}) async =>
getAddress(index: index, hd: hd, addressType: addressType);
getAddress(
derivationType: derivationType,
isChange: isChange,
index: index,
addressType: addressType,
derivationInfo: derivationInfo,
);
@action
void addBitcoinAddressTypes() {
final lastP2wpkh = _addresses
final lastP2wpkh = _allAddresses
.where((addressRecord) =>
_isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2wpkh))
.toList()
@ -378,7 +410,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
addressesMap[address] = 'Active - P2WPKH';
}
final lastP2pkh = _addresses.firstWhere(
final lastP2pkh = _allAddresses.firstWhere(
(addressRecord) => _isUnusedReceiveAddressByType(addressRecord, P2pkhAddressType.p2pkh));
if (lastP2pkh.address != address) {
addressesMap[lastP2pkh.address] = 'P2PKH';
@ -386,7 +418,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
addressesMap[address] = 'Active - P2PKH';
}
final lastP2sh = _addresses.firstWhere((addressRecord) =>
final lastP2sh = _allAddresses.firstWhere((addressRecord) =>
_isUnusedReceiveAddressByType(addressRecord, P2shAddressType.p2wpkhInP2sh));
if (lastP2sh.address != address) {
addressesMap[lastP2sh.address] = 'P2SH';
@ -394,7 +426,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
addressesMap[address] = 'Active - P2SH';
}
final lastP2tr = _addresses.firstWhere(
final lastP2tr = _allAddresses.firstWhere(
(addressRecord) => _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2tr));
if (lastP2tr.address != address) {
addressesMap[lastP2tr.address] = 'P2TR';
@ -402,7 +434,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
addressesMap[address] = 'Active - P2TR';
}
final lastP2wsh = _addresses.firstWhere(
final lastP2wsh = _allAddresses.firstWhere(
(addressRecord) => _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2wsh));
if (lastP2wsh.address != address) {
addressesMap[lastP2wsh.address] = 'P2WSH';
@ -411,7 +443,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
}
silentAddresses.forEach((addressRecord) {
if (addressRecord.type != SilentPaymentsAddresType.p2sp || addressRecord.isHidden) {
if (addressRecord.type != SilentPaymentsAddresType.p2sp || addressRecord.isChange) {
return;
}
@ -425,8 +457,9 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
});
}
@action
void addLitecoinAddressTypes() {
final lastP2wpkh = _addresses
final lastP2wpkh = _allAddresses
.where((addressRecord) =>
_isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2wpkh))
.toList()
@ -437,7 +470,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
addressesMap[address] = 'Active - P2WPKH';
}
final lastMweb = _addresses.firstWhere(
final lastMweb = _allAddresses.firstWhere(
(addressRecord) => _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.mweb));
if (lastMweb.address != address) {
addressesMap[lastMweb.address] = 'MWEB';
@ -446,8 +479,9 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
}
}
@action
void addBitcoinCashAddressTypes() {
final lastP2pkh = _addresses.firstWhere(
final lastP2pkh = _allAddresses.firstWhere(
(addressRecord) => _isUnusedReceiveAddressByType(addressRecord, P2pkhAddressType.p2pkh));
if (lastP2pkh.address != address) {
addressesMap[lastP2pkh.address] = 'P2PKH';
@ -457,13 +491,14 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
}
@override
@action
Future<void> updateAddressesInBox() async {
try {
addressesMap.clear();
addressesMap[address] = 'Active';
allAddressesMap.clear();
_addresses.forEach((addressRecord) {
_allAddresses.forEach((addressRecord) {
allAddressesMap[addressRecord.address] = addressRecord.name;
});
@ -490,7 +525,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
@action
void updateAddress(String address, String label) {
BaseBitcoinAddressRecord? foundAddress;
_addresses.forEach((addressRecord) {
_allAddresses.forEach((addressRecord) {
if (addressRecord.address == address) {
foundAddress = addressRecord;
}
@ -509,11 +544,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
if (foundAddress != null) {
foundAddress!.setNewName(label);
if (foundAddress is BitcoinAddressRecord) {
final index = _addresses.indexOf(foundAddress);
_addresses.remove(foundAddress);
_addresses.insert(index, foundAddress as BitcoinAddressRecord);
} else {
if (foundAddress is! BitcoinAddressRecord) {
final index = silentAddresses.indexOf(foundAddress as BitcoinSilentPaymentAddressRecord);
silentAddresses.remove(foundAddress);
silentAddresses.insert(index, foundAddress as BitcoinSilentPaymentAddressRecord);
@ -530,86 +561,103 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
}
addressesByReceiveType.clear();
addressesByReceiveType.addAll(_addresses.where(_isAddressPageTypeMatch).toList());
addressesByReceiveType.addAll(_allAddresses.where(_isAddressPageTypeMatch).toList());
}
@action
void updateReceiveAddresses() {
receiveAddresses.removeRange(0, receiveAddresses.length);
final newAddresses =
_addresses.where((addressRecord) => !addressRecord.isHidden && !addressRecord.isUsed);
final newAddresses = _allAddresses.where((addressRecord) => !addressRecord.isChange);
receiveAddresses.addAll(newAddresses);
}
@action
void updateChangeAddresses() {
changeAddresses.removeRange(0, changeAddresses.length);
final newAddresses = _addresses.where((addressRecord) =>
addressRecord.isHidden &&
!addressRecord.isUsed &&
// TODO: feature to change change address type. For now fixed to p2wpkh, the cheapest type
final newAddresses = _allAddresses.where((addressRecord) =>
addressRecord.isChange &&
(walletInfo.type != WalletType.bitcoin || addressRecord.type == SegwitAddresType.p2wpkh));
changeAddresses.addAll(newAddresses);
}
@action
Future<void> discoverAddresses(List<BitcoinAddressRecord> addressList, bool isHidden,
Future<String?> Function(BitcoinAddressRecord) getAddressHistory,
{BitcoinAddressType type = SegwitAddresType.p2wpkh}) async {
final newAddresses = await _createNewAddresses(gap,
startIndex: addressList.length, isHidden: isHidden, type: type);
Future<List<BitcoinAddressRecord>> discoverAddresses({
required CWBitcoinDerivationType derivationType,
required bool isChange,
required BitcoinAddressType type,
required BitcoinDerivationInfo derivationInfo,
}) async {
final gap = (isChange
? ElectrumWalletAddressesBase.defaultChangeAddressesCount
: ElectrumWalletAddressesBase.defaultReceiveAddressesCount);
final newAddresses = await _createNewAddresses(
derivationType: derivationType,
gap,
isChange: isChange,
type: type,
derivationInfo: derivationInfo,
);
addAddresses(newAddresses);
return newAddresses;
}
final addressesWithHistory = await Future.wait(newAddresses.map(getAddressHistory));
final isLastAddressUsed = addressesWithHistory.last == addressList.last.address;
@action
Future<void> generateInitialAddresses({required BitcoinAddressType type}) async {
for (final derivationType in hdWallets.keys) {
if (derivationType == CWBitcoinDerivationType.old && type == SegwitAddresType.p2wpkh) {
continue;
}
if (isLastAddressUsed) {
discoverAddresses(addressList, isHidden, getAddressHistory, type: type);
final derivationInfo = BitcoinAddressUtils.getDerivationFromType(
type,
isElectrum: derivationType == CWBitcoinDerivationType.electrum,
);
await discoverAddresses(
derivationType: derivationType,
isChange: false,
type: type,
derivationInfo: derivationInfo,
);
await discoverAddresses(
derivationType: derivationType,
isChange: true,
type: type,
derivationInfo: derivationInfo,
);
}
}
Future<void> _generateInitialAddresses(
{BitcoinAddressType type = SegwitAddresType.p2wpkh}) async {
var countOfReceiveAddresses = 0;
var countOfHiddenAddresses = 0;
_addresses.forEach((addr) {
if (addr.type == type) {
if (addr.isHidden) {
countOfHiddenAddresses += 1;
return;
}
countOfReceiveAddresses += 1;
}
});
if (countOfReceiveAddresses < defaultReceiveAddressesCount) {
final addressesCount = defaultReceiveAddressesCount - countOfReceiveAddresses;
final newAddresses = await _createNewAddresses(addressesCount,
startIndex: countOfReceiveAddresses, isHidden: false, type: type);
addAddresses(newAddresses);
}
if (countOfHiddenAddresses < defaultChangeAddressesCount) {
final addressesCount = defaultChangeAddressesCount - countOfHiddenAddresses;
final newAddresses = await _createNewAddresses(addressesCount,
startIndex: countOfHiddenAddresses, isHidden: true, type: type);
addAddresses(newAddresses);
}
}
Future<List<BitcoinAddressRecord>> _createNewAddresses(int count,
{int startIndex = 0, bool isHidden = false, BitcoinAddressType? type}) async {
@action
Future<List<BitcoinAddressRecord>> _createNewAddresses(
int count, {
required CWBitcoinDerivationType derivationType,
required BitcoinDerivationInfo derivationInfo,
bool isChange = false,
BitcoinAddressType? type,
}) async {
final list = <BitcoinAddressRecord>[];
final startIndex = (isChange ? receiveAddresses : changeAddresses)
.where((addr) => addr.derivationType == derivationType && addr.type == type)
.length;
for (var i = startIndex; i < count + startIndex; i++) {
final address = BitcoinAddressRecord(
await getAddressAsync(index: i, hd: _getHd(isHidden), addressType: type ?? addressPageType),
await getAddressAsync(
derivationType: derivationType,
isChange: isChange,
index: i,
isHidden: isHidden,
addressType: type ?? addressPageType,
derivationInfo: derivationInfo,
),
index: i,
isChange: isChange,
isHidden: derivationType == CWBitcoinDerivationType.old && type != SegwitAddresType.p2wpkh,
type: type ?? addressPageType,
network: network,
derivationInfo: derivationInfo,
derivationType: derivationType,
);
list.add(address);
}
@ -618,12 +666,27 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
}
@action
void addAddresses(Iterable<BitcoinAddressRecord> addresses) {
final addressesSet = this._addresses.toSet();
addressesSet.addAll(addresses);
this._addresses.clear();
this._addresses.addAll(addressesSet);
void updateAdresses(Iterable<BitcoinAddressRecord> addresses) {
for (final address in addresses) {
final index = _allAddresses.indexWhere((element) => element.address == address.address);
_allAddresses.replaceRange(index, index + 1, [address]);
updateAddressesByMatch();
updateReceiveAddresses();
updateChangeAddresses();
}
}
@action
void addAddresses(Iterable<BitcoinAddressRecord> addresses) {
this._allAddresses.addAll(addresses);
updateAddressesByMatch();
updateReceiveAddresses();
updateChangeAddresses();
this.hiddenAddresses.addAll(addresses
.where((addressRecord) => addressRecord.isHidden)
.map((addressRecord) => addressRecord.address));
}
@action
@ -644,24 +707,6 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
updateAddressesByMatch();
}
void _validateAddresses() {
_addresses.forEach((element) async {
if (element.type == SegwitAddresType.mweb) {
// this would add a ton of startup lag for mweb addresses since we have 1000 of them
return;
}
if (!element.isHidden &&
element.address !=
await getAddressAsync(index: element.index, hd: mainHd, addressType: element.type)) {
element.isHidden = true;
} else if (element.isHidden &&
element.address !=
await getAddressAsync(index: element.index, hd: sideHd, addressType: element.type)) {
element.isHidden = false;
}
});
}
@action
Future<void> setAddressType(BitcoinAddressType type) async {
_addressPageType = type;
@ -674,12 +719,11 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
return _isAddressByType(addressRecord, addressPageType);
}
Bip32Slip10Secp256k1 _getHd(bool isHidden) => isHidden ? sideHd : mainHd;
bool _isAddressByType(BitcoinAddressRecord addr, BitcoinAddressType type) => addr.type == type;
bool _isUnusedReceiveAddressByType(BitcoinAddressRecord addr, BitcoinAddressType type) =>
!addr.isHidden && !addr.isUsed && addr.type == type;
bool _isUnusedReceiveAddressByType(BitcoinAddressRecord addr, BitcoinAddressType type) {
return !addr.isChange && !addr.isUsed && addr.type == type;
}
@action
void deleteSilentPaymentAddress(String address) {

View file

@ -3,10 +3,8 @@ import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:cw_bitcoin/bitcoin_address_record.dart';
import 'package:cw_bitcoin/electrum_balance.dart';
import 'package:cw_core/encryption_file_utils.dart';
import 'package:cw_bitcoin/electrum_derivations.dart';
import 'package:cw_core/pathForWallet.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/utils/file.dart';
import 'package:cw_core/wallet_type.dart';
class ElectrumWalletSnapshot {
@ -68,7 +66,7 @@ class ElectrumWalletSnapshot {
final addressesTmp = data['addresses'] as List? ?? <Object>[];
final addresses = addressesTmp
.whereType<String>()
.map((addr) => BitcoinAddressRecord.fromJSON(addr, network: network))
.map((addr) => BitcoinAddressRecord.fromJSON(addr))
.toList();
final silentAddressesTmp = data['silent_addresses'] as List? ?? <Object>[];
@ -80,7 +78,7 @@ class ElectrumWalletSnapshot {
final mwebAddressTmp = data['mweb_addresses'] as List? ?? <Object>[];
final mwebAddresses = mwebAddressTmp
.whereType<String>()
.map((addr) => BitcoinAddressRecord.fromJSON(addr, network: network))
.map((addr) => BitcoinAddressRecord.fromJSON(addr))
.toList();
final alwaysScan = data['alwaysScan'] as bool? ?? false;
@ -93,7 +91,7 @@ class ElectrumWalletSnapshot {
final derivationType = DerivationType
.values[(data['derivationTypeIndex'] as int?) ?? DerivationType.electrum.index];
final derivationPath = data['derivationPath'] as String? ?? electrum_path;
final derivationPath = data['derivationPath'] as String? ?? ELECTRUM_PATH;
try {
regularAddressIndexByType = {

View file

@ -0,0 +1,794 @@
import 'dart:async';
import 'dart:convert';
import 'dart:isolate';
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_transaction_priority.dart';
import 'package:cw_bitcoin/bitcoin_unspent.dart';
import 'package:cw_core/get_height_by_date.dart';
import 'package:cw_bitcoin/electrum_balance.dart';
import 'package:cw_bitcoin/electrum_transaction_info.dart';
import 'package:cw_bitcoin/electrum_worker/electrum_worker_methods.dart';
import 'package:cw_bitcoin/electrum_worker/electrum_worker_params.dart';
import 'package:cw_bitcoin/electrum_worker/methods/methods.dart';
import 'package:cw_core/sync_status.dart';
import 'package:cw_core/transaction_direction.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:http/http.dart' as http;
import 'package:sp_scanner/sp_scanner.dart';
class ElectrumWorker {
final SendPort sendPort;
ElectrumApiProvider? _electrumClient;
BasedUtxoNetwork? _network;
ElectrumWorker._(this.sendPort, {ElectrumApiProvider? electrumClient})
: _electrumClient = electrumClient;
static void run(SendPort sendPort) {
final worker = ElectrumWorker._(sendPort);
final receivePort = ReceivePort();
sendPort.send(receivePort.sendPort);
receivePort.listen(worker.handleMessage);
}
void _sendResponse<T, U>(ElectrumWorkerResponse<T, U> response) {
sendPort.send(jsonEncode(response.toJson()));
}
void _sendError(ElectrumWorkerErrorResponse response) {
sendPort.send(jsonEncode(response.toJson()));
}
void handleMessage(dynamic message) async {
print("Worker: received message: $message");
try {
Map<String, dynamic> messageJson;
if (message is String) {
messageJson = jsonDecode(message) as Map<String, dynamic>;
} else {
messageJson = message as Map<String, dynamic>;
}
final workerMethod = messageJson['method'] as String;
switch (workerMethod) {
case ElectrumWorkerMethods.connectionMethod:
await _handleConnect(
ElectrumWorkerConnectionRequest.fromJson(messageJson),
);
break;
case ElectrumWorkerMethods.txHashMethod:
await _handleGetTxExpanded(
ElectrumWorkerTxExpandedRequest.fromJson(messageJson),
);
break;
case ElectrumRequestMethods.headersSubscribeMethod:
await _handleHeadersSubscribe(
ElectrumWorkerHeadersSubscribeRequest.fromJson(messageJson),
);
break;
case ElectrumRequestMethods.scripthashesSubscribeMethod:
await _handleScriphashesSubscribe(
ElectrumWorkerScripthashesSubscribeRequest.fromJson(messageJson),
);
break;
case ElectrumRequestMethods.getBalanceMethod:
await _handleGetBalance(
ElectrumWorkerGetBalanceRequest.fromJson(messageJson),
);
break;
case ElectrumRequestMethods.getHistoryMethod:
await _handleGetHistory(
ElectrumWorkerGetHistoryRequest.fromJson(messageJson),
);
break;
case ElectrumRequestMethods.listunspentMethod:
await _handleListUnspent(
ElectrumWorkerListUnspentRequest.fromJson(messageJson),
);
break;
case ElectrumRequestMethods.broadcastMethod:
await _handleBroadcast(
ElectrumWorkerBroadcastRequest.fromJson(messageJson),
);
break;
case ElectrumRequestMethods.tweaksSubscribeMethod:
await _handleScanSilentPayments(
ElectrumWorkerTweaksSubscribeRequest.fromJson(messageJson),
);
break;
case ElectrumRequestMethods.estimateFeeMethod:
await _handleGetFeeRates(
ElectrumWorkerGetFeesRequest.fromJson(messageJson),
);
break;
case ElectrumRequestMethods.versionMethod:
await _handleGetVersion(
ElectrumWorkerGetVersionRequest.fromJson(messageJson),
);
break;
}
} catch (e, s) {
print(s);
_sendError(ElectrumWorkerErrorResponse(error: e.toString()));
}
}
Future<void> _handleConnect(ElectrumWorkerConnectionRequest request) async {
_network = request.network;
_electrumClient = await ElectrumApiProvider.connect(
ElectrumTCPService.connect(
request.uri,
onConnectionStatusChange: (status) {
_sendResponse(ElectrumWorkerConnectionResponse(status: status, id: request.id));
},
defaultRequestTimeOut: const Duration(seconds: 5),
connectionTimeOut: const Duration(seconds: 5),
),
);
}
Future<void> _handleHeadersSubscribe(ElectrumWorkerHeadersSubscribeRequest request) async {
final listener = _electrumClient!.subscribe(ElectrumHeaderSubscribe());
if (listener == null) {
_sendError(ElectrumWorkerHeadersSubscribeError(error: 'Failed to subscribe'));
return;
}
listener((event) {
_sendResponse(
ElectrumWorkerHeadersSubscribeResponse(result: event, id: request.id),
);
});
}
Future<void> _handleScriphashesSubscribe(
ElectrumWorkerScripthashesSubscribeRequest request,
) async {
await Future.wait(request.scripthashByAddress.entries.map((entry) async {
final address = entry.key;
final scripthash = entry.value;
final listener = await _electrumClient!.subscribe(
ElectrumScriptHashSubscribe(scriptHash: scripthash),
);
if (listener == null) {
_sendError(ElectrumWorkerScripthashesSubscribeError(error: 'Failed to subscribe'));
return;
}
// https://electrumx.readthedocs.io/en/latest/protocol-basics.html#status
// The status of the script hash is the hash of the tx history, or null if the string is empty because there are no transactions
listener((status) async {
print("status: $status");
_sendResponse(ElectrumWorkerScripthashesSubscribeResponse(
result: {address: status},
id: request.id,
));
});
}));
}
Future<void> _handleGetHistory(ElectrumWorkerGetHistoryRequest result) async {
final Map<String, AddressHistoriesResponse> histories = {};
final addresses = result.addresses;
await Future.wait(addresses.map((addressRecord) async {
final history = await _electrumClient!.request(ElectrumScriptHashGetHistory(
scriptHash: addressRecord.scriptHash,
));
if (history.isNotEmpty) {
addressRecord.setAsUsed();
addressRecord.txCount = history.length;
await Future.wait(history.map((transaction) async {
final txid = transaction['tx_hash'] as String;
final height = transaction['height'] as int;
late ElectrumTransactionInfo tx;
try {
// Exception thrown on null
tx = result.storedTxs.firstWhere((tx) => tx.id == txid);
if (height > 0) {
tx.height = height;
// the tx's block itself is the first confirmation so add 1
tx.confirmations = result.chainTip - height + 1;
tx.isPending = tx.confirmations == 0;
}
} catch (_) {
tx = ElectrumTransactionInfo.fromElectrumBundle(
await _getTransactionExpanded(
hash: txid,
currentChainTip: result.chainTip,
mempoolAPIEnabled: result.mempoolAPIEnabled,
),
result.walletType,
result.network,
addresses: result.addresses.map((addr) => addr.address).toSet(),
height: height,
);
}
final addressHistories = histories[addressRecord.address];
if (addressHistories != null) {
addressHistories.txs.add(tx);
} else {
histories[addressRecord.address] = AddressHistoriesResponse(
addressRecord: addressRecord,
txs: [tx],
walletType: result.walletType,
);
}
return Future.value(null);
}));
}
return histories;
}));
_sendResponse(ElectrumWorkerGetHistoryResponse(
result: histories.values.toList(),
id: result.id,
));
}
// Future<void> _handleListUnspents(ElectrumWorkerGetBalanceRequest request) async {
// final balanceFutures = <Future<Map<String, dynamic>>>[];
// for (final scripthash in request.scripthashes) {
// final balanceFuture = _electrumClient!.request(
// ElectrumGetScriptHashBalance(scriptHash: scripthash),
// );
// balanceFutures.add(balanceFuture);
// }
// var totalConfirmed = 0;
// var totalUnconfirmed = 0;
// final balances = await Future.wait(balanceFutures);
// for (final balance in balances) {
// final confirmed = balance['confirmed'] as int? ?? 0;
// final unconfirmed = balance['unconfirmed'] as int? ?? 0;
// totalConfirmed += confirmed;
// totalUnconfirmed += unconfirmed;
// }
// _sendResponse(ElectrumWorkerGetBalanceResponse(
// result: ElectrumBalance(
// confirmed: totalConfirmed,
// unconfirmed: totalUnconfirmed,
// frozen: 0,
// ),
// ));
// }
Future<void> _handleGetBalance(ElectrumWorkerGetBalanceRequest request) async {
final balanceFutures = <Future<Map<String, dynamic>>>[];
for (final scripthash in request.scripthashes) {
final balanceFuture = _electrumClient!.request(
ElectrumGetScriptHashBalance(scriptHash: scripthash),
);
balanceFutures.add(balanceFuture);
}
var totalConfirmed = 0;
var totalUnconfirmed = 0;
final balances = await Future.wait(balanceFutures);
for (final balance in balances) {
final confirmed = balance['confirmed'] as int? ?? 0;
final unconfirmed = balance['unconfirmed'] as int? ?? 0;
totalConfirmed += confirmed;
totalUnconfirmed += unconfirmed;
}
_sendResponse(
ElectrumWorkerGetBalanceResponse(
result: ElectrumBalance(
confirmed: totalConfirmed,
unconfirmed: totalUnconfirmed,
frozen: 0,
),
id: request.id,
),
);
}
Future<void> _handleListUnspent(ElectrumWorkerListUnspentRequest request) async {
final unspents = <String, List<ElectrumUtxo>>{};
await Future.wait(request.scripthashes.map((scriptHash) async {
final scriptHashUnspents = await _electrumClient!.request(
ElectrumScriptHashListUnspent(scriptHash: scriptHash),
);
if (scriptHashUnspents.isNotEmpty) {
unspents[scriptHash] = scriptHashUnspents;
}
}));
_sendResponse(ElectrumWorkerListUnspentResponse(utxos: unspents, id: request.id));
}
Future<void> _handleBroadcast(ElectrumWorkerBroadcastRequest request) async {
final txHash = await _electrumClient!.request(
ElectrumBroadCastTransaction(transactionRaw: request.transactionRaw),
);
_sendResponse(ElectrumWorkerBroadcastResponse(txHash: txHash, id: request.id));
}
Future<void> _handleGetTxExpanded(ElectrumWorkerTxExpandedRequest request) async {
final tx = await _getTransactionExpanded(
hash: request.txHash,
currentChainTip: request.currentChainTip,
mempoolAPIEnabled: false,
getConfirmations: false,
);
_sendResponse(ElectrumWorkerTxExpandedResponse(expandedTx: tx, id: request.id));
}
Future<ElectrumTransactionBundle> _getTransactionExpanded({
required String hash,
required int currentChainTip,
required bool mempoolAPIEnabled,
bool getConfirmations = true,
}) async {
int? time;
int? height;
int? confirmations;
final transactionHex = await _electrumClient!.request(
ElectrumGetTransactionHex(transactionHash: hash),
);
if (getConfirmations) {
if (mempoolAPIEnabled) {
try {
final txVerbose = await http.get(
Uri.parse(
"http://mempool.cakewallet.com:8999/api/v1/tx/$hash/status",
),
);
if (txVerbose.statusCode == 200 &&
txVerbose.body.isNotEmpty &&
jsonDecode(txVerbose.body) != null) {
height = jsonDecode(txVerbose.body)['block_height'] as int;
final blockHash = await http.get(
Uri.parse(
"http://mempool.cakewallet.com:8999/api/v1/block-height/$height",
),
);
if (blockHash.statusCode == 200 &&
blockHash.body.isNotEmpty &&
jsonDecode(blockHash.body) != null) {
final blockResponse = await http.get(
Uri.parse(
"http://mempool.cakewallet.com:8999/api/v1/block/${blockHash.body}",
),
);
if (blockResponse.statusCode == 200 &&
blockResponse.body.isNotEmpty &&
jsonDecode(blockResponse.body)['timestamp'] != null) {
time = int.parse(jsonDecode(blockResponse.body)['timestamp'].toString());
}
}
}
} catch (_) {}
}
if (height != null) {
if (time == null && height > 0) {
time = (getDateByBitcoinHeight(height).millisecondsSinceEpoch / 1000).round();
}
final tip = currentChainTip;
if (tip > 0 && height > 0) {
// Add one because the block itself is the first confirmation
confirmations = tip - height + 1;
}
}
}
final original = BtcTransaction.fromRaw(transactionHex);
final ins = <BtcTransaction>[];
for (final vin in original.inputs) {
final inputTransactionHex = await _electrumClient!.request(
ElectrumGetTransactionHex(transactionHash: vin.txId),
);
ins.add(BtcTransaction.fromRaw(inputTransactionHex));
}
return ElectrumTransactionBundle(
original,
ins: ins,
time: time,
confirmations: confirmations ?? 0,
);
}
Future<void> _handleGetFeeRates(ElectrumWorkerGetFeesRequest request) async {
if (request.mempoolAPIEnabled) {
try {
final recommendedFees = await ApiProvider.fromMempool(
_network!,
baseUrl: "http://mempool.cakewallet.com:8999/api",
).getRecommendedFeeRate();
final unimportantFee = recommendedFees.economyFee!.satoshis;
final normalFee = recommendedFees.low.satoshis;
int elevatedFee = recommendedFees.medium.satoshis;
int priorityFee = recommendedFees.high.satoshis;
// Bitcoin only: adjust fee rates to avoid equal fee values
// elevated fee should be higher than normal fee
if (normalFee == elevatedFee) {
elevatedFee++;
}
// priority fee should be higher than elevated fee
while (priorityFee <= elevatedFee) {
priorityFee++;
}
// this guarantees that, even if all fees are low and equal,
// higher priority fee txs can be consumed when chain fees start surging
_sendResponse(
ElectrumWorkerGetFeesResponse(
result: BitcoinTransactionPriorities(
unimportant: unimportantFee,
normal: normalFee,
elevated: elevatedFee,
priority: priorityFee,
custom: unimportantFee,
),
),
);
} catch (e) {
_sendError(ElectrumWorkerGetFeesError(error: e.toString()));
}
} else {
_sendResponse(
ElectrumWorkerGetFeesResponse(
result: ElectrumTransactionPriorities.fromList(
await _electrumClient!.getFeeRates(),
),
),
);
}
}
Future<void> _handleScanSilentPayments(ElectrumWorkerTweaksSubscribeRequest request) async {
final scanData = request.scanData;
int syncHeight = scanData.height;
int initialSyncHeight = syncHeight;
int getCountPerRequest(int syncHeight) {
if (scanData.isSingleScan) {
return 1;
}
final amountLeft = scanData.chainTip - syncHeight + 1;
return amountLeft;
}
final receiver = Receiver(
scanData.silentAddress.b_scan.toHex(),
scanData.silentAddress.B_spend.toHex(),
scanData.network == BitcoinNetwork.testnet,
scanData.labelIndexes,
scanData.labelIndexes.length,
);
// Initial status UI update, send how many blocks in total to scan
final initialCount = getCountPerRequest(syncHeight);
_sendResponse(ElectrumWorkerTweaksSubscribeResponse(
result: TweaksSyncResponse(
height: syncHeight,
syncStatus: StartingScanSyncStatus(syncHeight),
),
));
final listener = await _electrumClient!.subscribe(
ElectrumTweaksSubscribe(height: syncHeight, count: initialCount),
);
Future<void> listenFn(ElectrumTweaksSubscribeResponse response) async {
// success or error msg
final noData = response.message != null;
if (noData) {
// re-subscribe to continue receiving messages, starting from the next unscanned height
final nextHeight = syncHeight + 1;
final nextCount = getCountPerRequest(nextHeight);
if (nextCount > 0) {
final nextListener = await _electrumClient!.subscribe(
ElectrumTweaksSubscribe(height: syncHeight, count: initialCount),
);
nextListener?.call(listenFn);
}
return;
}
// Continuous status UI update, send how many blocks left to scan
final syncingStatus = scanData.isSingleScan
? SyncingSyncStatus(1, 0)
: SyncingSyncStatus.fromHeightValues(scanData.chainTip, initialSyncHeight, syncHeight);
_sendResponse(ElectrumWorkerTweaksSubscribeResponse(
result: TweaksSyncResponse(height: syncHeight, syncStatus: syncingStatus),
));
final tweakHeight = response.block;
try {
final blockTweaks = response.blockTweaks;
for (final txid in blockTweaks.keys) {
final tweakData = blockTweaks[txid];
final outputPubkeys = tweakData!.outputPubkeys;
final tweak = tweakData.tweak;
try {
// scanOutputs called from rust here
final addToWallet = scanOutputs(outputPubkeys.keys.toList(), tweak, receiver);
if (addToWallet.isEmpty) {
// no results tx, continue to next tx
continue;
}
// placeholder ElectrumTransactionInfo object to update values based on new scanned unspent(s)
final txInfo = ElectrumTransactionInfo(
WalletType.bitcoin,
id: txid,
height: tweakHeight,
amount: 0,
fee: 0,
direction: TransactionDirection.incoming,
isPending: false,
isReplaced: false,
date: scanData.network == BitcoinNetwork.mainnet
? getDateByBitcoinHeight(tweakHeight)
: DateTime.now(),
confirmations: scanData.chainTip - tweakHeight + 1,
unspents: [],
isReceivedSilentPayment: true,
);
addToWallet.forEach((label, value) {
(value as Map<String, dynamic>).forEach((output, tweak) {
final t_k = tweak.toString();
final receivingOutputAddress = ECPublic.fromHex(output)
.toTaprootAddress(tweak: false)
.toAddress(scanData.network);
final matchingOutput = outputPubkeys[output]!;
final amount = matchingOutput.amount;
final pos = matchingOutput.vout;
final receivedAddressRecord = BitcoinReceivedSPAddressRecord(
receivingOutputAddress,
labelIndex: 1, // TODO: get actual index/label
isUsed: true,
spendKey: scanData.silentAddress.b_spend.tweakAdd(
BigintUtils.fromBytes(BytesUtils.fromHexString(t_k)),
),
txCount: 1,
balance: amount,
);
final unspent = BitcoinUnspent(receivedAddressRecord, txid, amount, pos);
txInfo.unspents!.add(unspent);
txInfo.amount += unspent.value;
});
});
_sendResponse(ElectrumWorkerTweaksSubscribeResponse(
result: TweaksSyncResponse(transactions: {txInfo.id: txInfo}),
));
} catch (e, stacktrace) {
print(stacktrace);
print(e.toString());
}
}
} catch (e, stacktrace) {
print(stacktrace);
print(e.toString());
}
syncHeight = tweakHeight;
if (tweakHeight >= scanData.chainTip || scanData.isSingleScan) {
if (tweakHeight >= scanData.chainTip)
_sendResponse(ElectrumWorkerTweaksSubscribeResponse(
result: TweaksSyncResponse(
height: syncHeight,
syncStatus: SyncedTipSyncStatus(scanData.chainTip),
),
));
if (scanData.isSingleScan) {
_sendResponse(ElectrumWorkerTweaksSubscribeResponse(
result: TweaksSyncResponse(height: syncHeight, syncStatus: SyncedSyncStatus()),
));
}
}
}
listener?.call(listenFn);
}
Future<void> _handleGetVersion(ElectrumWorkerGetVersionRequest request) async {
_sendResponse(ElectrumWorkerGetVersionResponse(
result: (await _electrumClient!.request(
ElectrumVersion(
clientName: "",
protocolVersion: ["1.4"],
),
)),
id: request.id));
}
}
Future<void> delegatedScan(ScanData scanData) async {
// int syncHeight = scanData.height;
// int initialSyncHeight = syncHeight;
// BehaviorSubject<Object>? tweaksSubscription = null;
// final electrumClient = scanData.electrumClient;
// await electrumClient.connectToUri(
// scanData.node?.uri ?? Uri.parse("tcp://electrs.cakewallet.com:50001"),
// useSSL: scanData.node?.useSSL ?? false,
// );
// if (tweaksSubscription == null) {
// scanData.sendPort.send(SyncResponse(syncHeight, StartingScanSyncStatus(syncHeight)));
// tweaksSubscription = await electrumClient.tweaksScan(
// pubSpendKey: scanData.silentAddress.B_spend.toHex(),
// );
// Future<void> listenFn(t) async {
// final tweaks = t as Map<String, dynamic>;
// final msg = tweaks["message"];
// // success or error msg
// final noData = msg != null;
// if (noData) {
// return;
// }
// // Continuous status UI update, send how many blocks left to scan
// final syncingStatus = scanData.isSingleScan
// ? SyncingSyncStatus(1, 0)
// : SyncingSyncStatus.fromHeightValues(scanData.chainTip, initialSyncHeight, syncHeight);
// scanData.sendPort.send(SyncResponse(syncHeight, syncingStatus));
// final blockHeight = tweaks.keys.first;
// final tweakHeight = int.parse(blockHeight);
// try {
// final blockTweaks = tweaks[blockHeight] as Map<String, dynamic>;
// for (var j = 0; j < blockTweaks.keys.length; j++) {
// final txid = blockTweaks.keys.elementAt(j);
// final details = blockTweaks[txid] as Map<String, dynamic>;
// final outputPubkeys = (details["output_pubkeys"] as Map<dynamic, dynamic>);
// final spendingKey = details["spending_key"].toString();
// try {
// // placeholder ElectrumTransactionInfo object to update values based on new scanned unspent(s)
// final txInfo = ElectrumTransactionInfo(
// WalletType.bitcoin,
// id: txid,
// height: tweakHeight,
// amount: 0,
// fee: 0,
// direction: TransactionDirection.incoming,
// isPending: false,
// isReplaced: false,
// date: scanData.network == BitcoinNetwork.mainnet
// ? getDateByBitcoinHeight(tweakHeight)
// : DateTime.now(),
// confirmations: scanData.chainTip - tweakHeight + 1,
// unspents: [],
// isReceivedSilentPayment: true,
// );
// outputPubkeys.forEach((pos, value) {
// final secKey = ECPrivate.fromHex(spendingKey);
// final receivingOutputAddress =
// secKey.getPublic().toTaprootAddress(tweak: false).toAddress(scanData.network);
// late int amount;
// try {
// amount = int.parse(value[1].toString());
// } catch (_) {
// return;
// }
// final receivedAddressRecord = BitcoinReceivedSPAddressRecord(
// receivingOutputAddress,
// labelIndex: 0,
// isUsed: true,
// spendKey: secKey,
// txCount: 1,
// balance: amount,
// );
// final unspent = BitcoinUnspent(
// receivedAddressRecord,
// txid,
// amount,
// int.parse(pos.toString()),
// );
// txInfo.unspents!.add(unspent);
// txInfo.amount += unspent.value;
// });
// scanData.sendPort.send({txInfo.id: txInfo});
// } catch (_) {}
// }
// } catch (_) {}
// syncHeight = tweakHeight;
// if (tweakHeight >= scanData.chainTip || scanData.isSingleScan) {
// if (tweakHeight >= scanData.chainTip)
// scanData.sendPort.send(SyncResponse(
// syncHeight,
// SyncedTipSyncStatus(scanData.chainTip),
// ));
// if (scanData.isSingleScan) {
// scanData.sendPort.send(SyncResponse(syncHeight, SyncedSyncStatus()));
// }
// await tweaksSubscription!.close();
// await electrumClient.close();
// }
// }
// tweaksSubscription?.listen(listenFn);
// }
// if (tweaksSubscription == null) {
// return scanData.sendPort.send(
// SyncResponse(syncHeight, UnsupportedSyncStatus()),
// );
// }
}
class ScanNode {
final Uri uri;
final bool? useSSL;
ScanNode(this.uri, this.useSSL);
}

View file

@ -0,0 +1,17 @@
class ElectrumWorkerMethods {
const ElectrumWorkerMethods._(this.method);
final String method;
static const String connectionMethod = "connection";
static const String unknownMethod = "unknown";
static const String txHashMethod = "txHash";
static const ElectrumWorkerMethods connect = ElectrumWorkerMethods._(connectionMethod);
static const ElectrumWorkerMethods unknown = ElectrumWorkerMethods._(unknownMethod);
static const ElectrumWorkerMethods txHash = ElectrumWorkerMethods._(txHashMethod);
@override
String toString() {
return method;
}
}

View file

@ -0,0 +1,53 @@
// import 'dart:convert';
import 'package:cw_bitcoin/electrum_worker/electrum_worker_methods.dart';
abstract class ElectrumWorkerRequest {
abstract final String method;
abstract final int? id;
Map<String, dynamic> toJson();
ElectrumWorkerRequest.fromJson(Map<String, dynamic> json);
}
class ElectrumWorkerResponse<RESULT, RESPONSE> {
ElectrumWorkerResponse({
required this.method,
required this.result,
this.error,
this.id,
});
final String method;
final RESULT result;
final String? error;
final int? id;
RESPONSE resultJson(RESULT result) {
throw UnimplementedError();
}
factory ElectrumWorkerResponse.fromJson(Map<String, dynamic> json) {
throw UnimplementedError();
}
Map<String, dynamic> toJson() {
return {'method': method, 'result': resultJson(result), 'error': error, 'id': id};
}
}
class ElectrumWorkerErrorResponse {
ElectrumWorkerErrorResponse({required this.error, this.id});
String get method => ElectrumWorkerMethods.unknown.method;
final int? id;
final String error;
factory ElectrumWorkerErrorResponse.fromJson(Map<String, dynamic> json) {
return ElectrumWorkerErrorResponse(error: json['error'] as String, id: json['id'] as int);
}
Map<String, dynamic> toJson() {
return {'method': method, 'error': error, 'id': id};
}
}

View file

@ -0,0 +1,56 @@
part of 'methods.dart';
class ElectrumWorkerBroadcastRequest implements ElectrumWorkerRequest {
ElectrumWorkerBroadcastRequest({required this.transactionRaw, this.id});
final String transactionRaw;
final int? id;
@override
final String method = ElectrumRequestMethods.broadcast.method;
@override
factory ElectrumWorkerBroadcastRequest.fromJson(Map<String, dynamic> json) {
return ElectrumWorkerBroadcastRequest(
transactionRaw: json['transactionRaw'] as String,
id: json['id'] as int?,
);
}
@override
Map<String, dynamic> toJson() {
return {'method': method, 'transactionRaw': transactionRaw};
}
}
class ElectrumWorkerBroadcastError extends ElectrumWorkerErrorResponse {
ElectrumWorkerBroadcastError({
required super.error,
super.id,
}) : super();
@override
String get method => ElectrumRequestMethods.broadcast.method;
}
class ElectrumWorkerBroadcastResponse extends ElectrumWorkerResponse<String, String> {
ElectrumWorkerBroadcastResponse({
required String txHash,
super.error,
super.id,
}) : super(result: txHash, method: ElectrumRequestMethods.broadcast.method);
@override
String resultJson(result) {
return result;
}
@override
factory ElectrumWorkerBroadcastResponse.fromJson(Map<String, dynamic> json) {
return ElectrumWorkerBroadcastResponse(
txHash: json['result'] as String,
error: json['error'] as String?,
id: json['id'] as int?,
);
}
}

View file

@ -0,0 +1,73 @@
part of 'methods.dart';
class ElectrumWorkerConnectionRequest implements ElectrumWorkerRequest {
ElectrumWorkerConnectionRequest({
required this.uri,
required this.network,
this.id,
});
final Uri uri;
final BasedUtxoNetwork network;
final int? id;
@override
final String method = ElectrumWorkerMethods.connect.method;
@override
factory ElectrumWorkerConnectionRequest.fromJson(Map<String, dynamic> json) {
return ElectrumWorkerConnectionRequest(
uri: Uri.parse(json['uri'] as String),
network: BasedUtxoNetwork.values.firstWhere(
(e) => e.toString() == json['network'] as String,
),
id: json['id'] as int?,
);
}
@override
Map<String, dynamic> toJson() {
return {
'method': method,
'uri': uri.toString(),
'network': network.toString(),
};
}
}
class ElectrumWorkerConnectionError extends ElectrumWorkerErrorResponse {
ElectrumWorkerConnectionError({
required super.error,
super.id,
}) : super();
@override
String get method => ElectrumWorkerMethods.connect.method;
}
class ElectrumWorkerConnectionResponse extends ElectrumWorkerResponse<ConnectionStatus, String> {
ElectrumWorkerConnectionResponse({
required ConnectionStatus status,
super.error,
super.id,
}) : super(
result: status,
method: ElectrumWorkerMethods.connect.method,
);
@override
String resultJson(result) {
return result.toString();
}
@override
factory ElectrumWorkerConnectionResponse.fromJson(Map<String, dynamic> json) {
return ElectrumWorkerConnectionResponse(
status: ConnectionStatus.values.firstWhere(
(e) => e.toString() == json['result'] as String,
),
error: json['error'] as String?,
id: json['id'] as int?,
);
}
}

View file

@ -0,0 +1,61 @@
part of 'methods.dart';
class ElectrumWorkerGetBalanceRequest implements ElectrumWorkerRequest {
ElectrumWorkerGetBalanceRequest({required this.scripthashes, this.id});
final Set<String> scripthashes;
final int? id;
@override
final String method = ElectrumRequestMethods.getBalance.method;
@override
factory ElectrumWorkerGetBalanceRequest.fromJson(Map<String, dynamic> json) {
return ElectrumWorkerGetBalanceRequest(
scripthashes: (json['scripthashes'] as List<String>).toSet(),
id: json['id'] as int?,
);
}
@override
Map<String, dynamic> toJson() {
return {'method': method, 'scripthashes': scripthashes.toList()};
}
}
class ElectrumWorkerGetBalanceError extends ElectrumWorkerErrorResponse {
ElectrumWorkerGetBalanceError({
required super.error,
super.id,
}) : super();
@override
final String method = ElectrumRequestMethods.getBalance.method;
}
class ElectrumWorkerGetBalanceResponse
extends ElectrumWorkerResponse<ElectrumBalance, Map<String, int>?> {
ElectrumWorkerGetBalanceResponse({
required super.result,
super.error,
super.id,
}) : super(method: ElectrumRequestMethods.getBalance.method);
@override
Map<String, int>? resultJson(result) {
return {"confirmed": result.confirmed, "unconfirmed": result.unconfirmed};
}
@override
factory ElectrumWorkerGetBalanceResponse.fromJson(Map<String, dynamic> json) {
return ElectrumWorkerGetBalanceResponse(
result: ElectrumBalance(
confirmed: json['result']['confirmed'] as int,
unconfirmed: json['result']['unconfirmed'] as int,
frozen: 0,
),
error: json['error'] as String?,
id: json['id'] as int?,
);
}
}

View file

@ -0,0 +1,60 @@
part of 'methods.dart';
class ElectrumWorkerGetFeesRequest implements ElectrumWorkerRequest {
ElectrumWorkerGetFeesRequest({
required this.mempoolAPIEnabled,
this.id,
});
final bool mempoolAPIEnabled;
final int? id;
@override
final String method = ElectrumRequestMethods.estimateFee.method;
@override
factory ElectrumWorkerGetFeesRequest.fromJson(Map<String, dynamic> json) {
return ElectrumWorkerGetFeesRequest(
mempoolAPIEnabled: json['mempoolAPIEnabled'] as bool,
id: json['id'] as int?,
);
}
@override
Map<String, dynamic> toJson() {
return {'method': method, 'mempoolAPIEnabled': mempoolAPIEnabled};
}
}
class ElectrumWorkerGetFeesError extends ElectrumWorkerErrorResponse {
ElectrumWorkerGetFeesError({
required super.error,
super.id,
}) : super();
@override
String get method => ElectrumRequestMethods.estimateFee.method;
}
class ElectrumWorkerGetFeesResponse
extends ElectrumWorkerResponse<TransactionPriorities, Map<String, int>> {
ElectrumWorkerGetFeesResponse({
required super.result,
super.error,
super.id,
}) : super(method: ElectrumRequestMethods.estimateFee.method);
@override
Map<String, int> resultJson(result) {
return result.toJson();
}
@override
factory ElectrumWorkerGetFeesResponse.fromJson(Map<String, dynamic> json) {
return ElectrumWorkerGetFeesResponse(
result: deserializeTransactionPriorities(json['result'] as Map<String, dynamic>),
error: json['error'] as String?,
id: json['id'] as int?,
);
}
}

View file

@ -0,0 +1,120 @@
part of 'methods.dart';
class ElectrumWorkerGetHistoryRequest implements ElectrumWorkerRequest {
ElectrumWorkerGetHistoryRequest({
required this.addresses,
required this.storedTxs,
required this.walletType,
required this.chainTip,
required this.network,
required this.mempoolAPIEnabled,
this.id,
});
final List<BitcoinAddressRecord> addresses;
final List<ElectrumTransactionInfo> storedTxs;
final WalletType walletType;
final int chainTip;
final BasedUtxoNetwork network;
final bool mempoolAPIEnabled;
final int? id;
@override
final String method = ElectrumRequestMethods.getHistory.method;
@override
factory ElectrumWorkerGetHistoryRequest.fromJson(Map<String, dynamic> json) {
final walletType = WalletType.values[json['walletType'] as int];
return ElectrumWorkerGetHistoryRequest(
addresses: (json['addresses'] as List)
.map((e) => BitcoinAddressRecord.fromJSON(e as String))
.toList(),
storedTxs: (json['storedTxIds'] as List)
.map((e) => ElectrumTransactionInfo.fromJson(e as Map<String, dynamic>, walletType))
.toList(),
walletType: walletType,
chainTip: json['chainTip'] as int,
network: BasedUtxoNetwork.fromName(json['network'] as String),
mempoolAPIEnabled: json['mempoolAPIEnabled'] as bool,
id: json['id'] as int?,
);
}
@override
Map<String, dynamic> toJson() {
return {
'method': method,
'addresses': addresses.map((e) => e.toJSON()).toList(),
'storedTxIds': storedTxs.map((e) => e.toJson()).toList(),
'walletType': walletType.index,
'chainTip': chainTip,
'network': network.value,
'mempoolAPIEnabled': mempoolAPIEnabled,
};
}
}
class ElectrumWorkerGetHistoryError extends ElectrumWorkerErrorResponse {
ElectrumWorkerGetHistoryError({
required super.error,
super.id,
}) : super();
@override
final String method = ElectrumRequestMethods.getHistory.method;
}
class AddressHistoriesResponse {
final BitcoinAddressRecord addressRecord;
final List<ElectrumTransactionInfo> txs;
final WalletType walletType;
AddressHistoriesResponse(
{required this.addressRecord, required this.txs, required this.walletType});
factory AddressHistoriesResponse.fromJson(Map<String, dynamic> json) {
final walletType = WalletType.values[json['walletType'] as int];
return AddressHistoriesResponse(
addressRecord: BitcoinAddressRecord.fromJSON(json['address'] as String),
txs: (json['txs'] as List)
.map((e) => ElectrumTransactionInfo.fromJson(e as Map<String, dynamic>, walletType))
.toList(),
walletType: walletType,
);
}
Map<String, dynamic> toJson() {
return {
'address': addressRecord.toJSON(),
'txs': txs.map((e) => e.toJson()).toList(),
'walletType': walletType.index,
};
}
}
class ElectrumWorkerGetHistoryResponse
extends ElectrumWorkerResponse<List<AddressHistoriesResponse>, List<Map<String, dynamic>>> {
ElectrumWorkerGetHistoryResponse({
required super.result,
super.error,
super.id,
}) : super(method: ElectrumRequestMethods.getHistory.method);
@override
List<Map<String, dynamic>> resultJson(result) {
return result.map((e) => e.toJson()).toList();
}
@override
factory ElectrumWorkerGetHistoryResponse.fromJson(Map<String, dynamic> json) {
return ElectrumWorkerGetHistoryResponse(
result: (json['result'] as List)
.map((e) => AddressHistoriesResponse.fromJson(e as Map<String, dynamic>))
.toList(),
error: json['error'] as String?,
id: json['id'] as int?,
);
}
}

View file

@ -0,0 +1,63 @@
part of 'methods.dart';
class ElectrumWorkerTxExpandedRequest implements ElectrumWorkerRequest {
ElectrumWorkerTxExpandedRequest({
required this.txHash,
required this.currentChainTip,
this.id,
});
final String txHash;
final int currentChainTip;
final int? id;
@override
final String method = ElectrumWorkerMethods.txHash.method;
@override
factory ElectrumWorkerTxExpandedRequest.fromJson(Map<String, dynamic> json) {
return ElectrumWorkerTxExpandedRequest(
txHash: json['txHash'] as String,
currentChainTip: json['currentChainTip'] as int,
id: json['id'] as int?,
);
}
@override
Map<String, dynamic> toJson() {
return {'method': method, 'txHash': txHash, 'currentChainTip': currentChainTip};
}
}
class ElectrumWorkerTxExpandedError extends ElectrumWorkerErrorResponse {
ElectrumWorkerTxExpandedError({
required String error,
super.id,
}) : super(error: error);
@override
String get method => ElectrumWorkerMethods.txHash.method;
}
class ElectrumWorkerTxExpandedResponse
extends ElectrumWorkerResponse<ElectrumTransactionBundle, Map<String, dynamic>> {
ElectrumWorkerTxExpandedResponse({
required ElectrumTransactionBundle expandedTx,
super.error,
super.id,
}) : super(result: expandedTx, method: ElectrumWorkerMethods.txHash.method);
@override
Map<String, dynamic> resultJson(result) {
return result.toJson();
}
@override
factory ElectrumWorkerTxExpandedResponse.fromJson(Map<String, dynamic> json) {
return ElectrumWorkerTxExpandedResponse(
expandedTx: ElectrumTransactionBundle.fromJson(json['result'] as Map<String, dynamic>),
error: json['error'] as String?,
id: json['id'] as int?,
);
}
}

View file

@ -0,0 +1,54 @@
part of 'methods.dart';
class ElectrumWorkerHeadersSubscribeRequest implements ElectrumWorkerRequest {
ElectrumWorkerHeadersSubscribeRequest({this.id});
@override
final String method = ElectrumRequestMethods.headersSubscribe.method;
final int? id;
@override
factory ElectrumWorkerHeadersSubscribeRequest.fromJson(Map<String, dynamic> json) {
return ElectrumWorkerHeadersSubscribeRequest(
id: json['id'] as int?,
);
}
@override
Map<String, dynamic> toJson() {
return {'method': method};
}
}
class ElectrumWorkerHeadersSubscribeError extends ElectrumWorkerErrorResponse {
ElectrumWorkerHeadersSubscribeError({
required super.error,
super.id,
}) : super();
@override
final String method = ElectrumRequestMethods.headersSubscribe.method;
}
class ElectrumWorkerHeadersSubscribeResponse
extends ElectrumWorkerResponse<ElectrumHeaderResponse, Map<String, dynamic>> {
ElectrumWorkerHeadersSubscribeResponse({
required super.result,
super.error,
super.id,
}) : super(method: ElectrumRequestMethods.headersSubscribe.method);
@override
Map<String, dynamic> resultJson(result) {
return result.toJson();
}
@override
factory ElectrumWorkerHeadersSubscribeResponse.fromJson(Map<String, dynamic> json) {
return ElectrumWorkerHeadersSubscribeResponse(
result: ElectrumHeaderResponse.fromJson(json['result'] as Map<String, dynamic>),
error: json['error'] as String?,
id: json['id'] as int?,
);
}
}

View file

@ -0,0 +1,60 @@
part of 'methods.dart';
class ElectrumWorkerListUnspentRequest implements ElectrumWorkerRequest {
ElectrumWorkerListUnspentRequest({required this.scripthashes, this.id});
final List<String> scripthashes;
final int? id;
@override
final String method = ElectrumRequestMethods.listunspent.method;
@override
factory ElectrumWorkerListUnspentRequest.fromJson(Map<String, dynamic> json) {
return ElectrumWorkerListUnspentRequest(
scripthashes: json['scripthashes'] as List<String>,
id: json['id'] as int?,
);
}
@override
Map<String, dynamic> toJson() {
return {'method': method, 'scripthashes': scripthashes};
}
}
class ElectrumWorkerListUnspentError extends ElectrumWorkerErrorResponse {
ElectrumWorkerListUnspentError({
required super.error,
super.id,
}) : super();
@override
String get method => ElectrumRequestMethods.listunspent.method;
}
class ElectrumWorkerListUnspentResponse
extends ElectrumWorkerResponse<Map<String, List<ElectrumUtxo>>, Map<String, dynamic>> {
ElectrumWorkerListUnspentResponse({
required Map<String, List<ElectrumUtxo>> utxos,
super.error,
super.id,
}) : super(result: utxos, method: ElectrumRequestMethods.listunspent.method);
@override
Map<String, dynamic> resultJson(result) {
return result.map((key, value) => MapEntry(key, value.map((e) => e.toJson()).toList()));
}
@override
factory ElectrumWorkerListUnspentResponse.fromJson(Map<String, dynamic> json) {
return ElectrumWorkerListUnspentResponse(
utxos: (json['result'] as Map<String, dynamic>).map(
(key, value) => MapEntry(key,
(value as List).map((e) => ElectrumUtxo.fromJson(e as Map<String, dynamic>)).toList()),
),
error: json['error'] as String?,
id: json['id'] as int?,
);
}
}

View file

@ -0,0 +1,22 @@
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:cw_bitcoin/bitcoin_address_record.dart';
import 'package:cw_bitcoin/electrum_balance.dart';
import 'package:cw_bitcoin/electrum_worker/electrum_worker_methods.dart';
import 'package:cw_bitcoin/electrum_worker/electrum_worker_params.dart';
import 'package:cw_bitcoin/electrum_transaction_info.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:cw_core/sync_status.dart';
import 'package:cw_core/transaction_priority.dart';
import 'package:cw_bitcoin/bitcoin_transaction_priority.dart';
part 'connection.dart';
part 'headers_subscribe.dart';
part 'scripthashes_subscribe.dart';
part 'get_balance.dart';
part 'get_history.dart';
part 'get_tx_expanded.dart';
part 'broadcast.dart';
part 'list_unspent.dart';
part 'tweaks_subscribe.dart';
part 'get_fees.dart';
part 'version.dart';

View file

@ -0,0 +1,60 @@
part of 'methods.dart';
class ElectrumWorkerScripthashesSubscribeRequest implements ElectrumWorkerRequest {
ElectrumWorkerScripthashesSubscribeRequest({
required this.scripthashByAddress,
this.id,
});
final Map<String, String> scripthashByAddress;
final int? id;
@override
final String method = ElectrumRequestMethods.scriptHashSubscribe.method;
@override
factory ElectrumWorkerScripthashesSubscribeRequest.fromJson(Map<String, dynamic> json) {
return ElectrumWorkerScripthashesSubscribeRequest(
scripthashByAddress: json['scripthashes'] as Map<String, String>,
id: json['id'] as int?,
);
}
@override
Map<String, dynamic> toJson() {
return {'method': method, 'scripthashes': scripthashByAddress};
}
}
class ElectrumWorkerScripthashesSubscribeError extends ElectrumWorkerErrorResponse {
ElectrumWorkerScripthashesSubscribeError({
required super.error,
super.id,
}) : super();
@override
final String method = ElectrumRequestMethods.scriptHashSubscribe.method;
}
class ElectrumWorkerScripthashesSubscribeResponse
extends ElectrumWorkerResponse<Map<String, String>?, Map<String, String>?> {
ElectrumWorkerScripthashesSubscribeResponse({
required super.result,
super.error,
super.id,
}) : super(method: ElectrumRequestMethods.scriptHashSubscribe.method);
@override
Map<String, String>? resultJson(result) {
return result;
}
@override
factory ElectrumWorkerScripthashesSubscribeResponse.fromJson(Map<String, dynamic> json) {
return ElectrumWorkerScripthashesSubscribeResponse(
result: json['result'] as Map<String, String>?,
error: json['error'] as String?,
id: json['id'] as int?,
);
}
}

View file

@ -0,0 +1,157 @@
part of 'methods.dart';
class ScanData {
final SilentPaymentOwner silentAddress;
final int height;
final BasedUtxoNetwork network;
final int chainTip;
final List<String> transactionHistoryIds;
final Map<String, String> labels;
final List<int> labelIndexes;
final bool isSingleScan;
ScanData({
required this.silentAddress,
required this.height,
required this.network,
required this.chainTip,
required this.transactionHistoryIds,
required this.labels,
required this.labelIndexes,
required this.isSingleScan,
});
factory ScanData.fromHeight(ScanData scanData, int newHeight) {
return ScanData(
silentAddress: scanData.silentAddress,
height: newHeight,
network: scanData.network,
chainTip: scanData.chainTip,
transactionHistoryIds: scanData.transactionHistoryIds,
labels: scanData.labels,
labelIndexes: scanData.labelIndexes,
isSingleScan: scanData.isSingleScan,
);
}
Map<String, dynamic> toJson() {
return {
'silentAddress': silentAddress.toJson(),
'height': height,
'network': network.value,
'chainTip': chainTip,
'transactionHistoryIds': transactionHistoryIds,
'labels': labels,
'labelIndexes': labelIndexes,
'isSingleScan': isSingleScan,
};
}
static ScanData fromJson(Map<String, dynamic> json) {
return ScanData(
silentAddress: SilentPaymentOwner.fromJson(json['silentAddress'] as Map<String, dynamic>),
height: json['height'] as int,
network: BasedUtxoNetwork.fromName(json['network'] as String),
chainTip: json['chainTip'] as int,
transactionHistoryIds:
(json['transactionHistoryIds'] as List).map((e) => e as String).toList(),
labels: json['labels'] as Map<String, String>,
labelIndexes: (json['labelIndexes'] as List).map((e) => e as int).toList(),
isSingleScan: json['isSingleScan'] as bool,
);
}
}
class ElectrumWorkerTweaksSubscribeRequest implements ElectrumWorkerRequest {
ElectrumWorkerTweaksSubscribeRequest({
required this.scanData,
this.id,
});
final ScanData scanData;
final int? id;
@override
final String method = ElectrumRequestMethods.tweaksSubscribe.method;
@override
factory ElectrumWorkerTweaksSubscribeRequest.fromJson(Map<String, dynamic> json) {
return ElectrumWorkerTweaksSubscribeRequest(
scanData: ScanData.fromJson(json['scanData'] as Map<String, dynamic>),
id: json['id'] as int?,
);
}
@override
Map<String, dynamic> toJson() {
return {'method': method, 'scanData': scanData.toJson()};
}
}
class ElectrumWorkerTweaksSubscribeError extends ElectrumWorkerErrorResponse {
ElectrumWorkerTweaksSubscribeError({
required super.error,
super.id,
}) : super();
@override
final String method = ElectrumRequestMethods.tweaksSubscribe.method;
}
class TweaksSyncResponse {
int? height;
SyncStatus? syncStatus;
Map<String, ElectrumTransactionInfo>? transactions = {};
TweaksSyncResponse({this.height, this.syncStatus, this.transactions});
Map<String, dynamic> toJson() {
return {
'height': height,
'syncStatus': syncStatus == null ? null : syncStatusToJson(syncStatus!),
'transactions': transactions?.map((key, value) => MapEntry(key, value.toJson())),
};
}
static TweaksSyncResponse fromJson(Map<String, dynamic> json) {
return TweaksSyncResponse(
height: json['height'] as int?,
syncStatus: json['syncStatus'] == null
? null
: syncStatusFromJson(json['syncStatus'] as Map<String, dynamic>),
transactions: json['transactions'] == null
? null
: (json['transactions'] as Map<String, dynamic>).map(
(key, value) => MapEntry(
key,
ElectrumTransactionInfo.fromJson(
value as Map<String, dynamic>,
WalletType.bitcoin,
)),
),
);
}
}
class ElectrumWorkerTweaksSubscribeResponse
extends ElectrumWorkerResponse<TweaksSyncResponse, Map<String, dynamic>> {
ElectrumWorkerTweaksSubscribeResponse({
required super.result,
super.error,
super.id,
}) : super(method: ElectrumRequestMethods.tweaksSubscribe.method);
@override
Map<String, dynamic> resultJson(result) {
return result.toJson();
}
@override
factory ElectrumWorkerTweaksSubscribeResponse.fromJson(Map<String, dynamic> json) {
return ElectrumWorkerTweaksSubscribeResponse(
result: TweaksSyncResponse.fromJson(json['result'] as Map<String, dynamic>),
error: json['error'] as String?,
id: json['id'] as int?,
);
}
}

View file

@ -0,0 +1,52 @@
part of 'methods.dart';
class ElectrumWorkerGetVersionRequest implements ElectrumWorkerRequest {
ElectrumWorkerGetVersionRequest({this.id});
final int? id;
@override
final String method = ElectrumRequestMethods.version.method;
@override
factory ElectrumWorkerGetVersionRequest.fromJson(Map<String, dynamic> json) {
return ElectrumWorkerGetVersionRequest(id: json['id'] as int?);
}
@override
Map<String, dynamic> toJson() {
return {'method': method};
}
}
class ElectrumWorkerGetVersionError extends ElectrumWorkerErrorResponse {
ElectrumWorkerGetVersionError({
required super.error,
super.id,
}) : super();
@override
String get method => ElectrumRequestMethods.version.method;
}
class ElectrumWorkerGetVersionResponse extends ElectrumWorkerResponse<List<String>, List<String>> {
ElectrumWorkerGetVersionResponse({
required super.result,
super.error,
super.id,
}) : super(method: ElectrumRequestMethods.version.method);
@override
List<String> resultJson(result) {
return result;
}
@override
factory ElectrumWorkerGetVersionResponse.fromJson(Map<String, dynamic> json) {
return ElectrumWorkerGetVersionResponse(
result: json['result'] as List<String>,
error: json['error'] as String?,
id: json['id'] as int?,
);
}
}

View file

@ -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_flutter_plus/ledger_flutter_plus.dart';
import 'package:ledger_litecoin/ledger_litecoin.dart';
@ -12,8 +11,7 @@ class LitecoinHardwareWalletService {
final LedgerConnection ledgerConnection;
Future<List<HardwareAccountData>> getAvailableAccounts(
{int index = 0, int limit = 5}) async {
Future<List<HardwareAccountData>> getAvailableAccounts({int index = 0, int limit = 5}) async {
final litecoinLedgerApp = LitecoinLedgerApp(ledgerConnection);
await litecoinLedgerApp.getVersion();
@ -27,14 +25,18 @@ class LitecoinHardwareWalletService {
final xpub = await litecoinLedgerApp.getXPubKey(
accountsDerivationPath: derivationPath,
xPubVersion: int.parse(hex.encode(xpubVersion.public), radix: 16));
final hd = Bip32Slip10Secp256k1.fromExtendedKey(xpub, xpubVersion)
.childKey(Bip32KeyIndex(0));
final bip32 =
Bip32Slip10Secp256k1.fromExtendedKey(xpub, xpubVersion).childKey(Bip32KeyIndex(0));
final address = generateP2WPKHAddress(
hd: hd, index: 0, network: LitecoinNetwork.mainnet);
final address = P2wpkhAddress.fromDerivation(
bip32: bip32,
derivationInfo: BitcoinDerivationInfos.LITECOIN,
isChange: false,
index: 0,
);
accounts.add(HardwareAccountData(
address: address,
address: address.toAddress(LitecoinNetwork.mainnet),
accountIndex: i,
derivationPath: derivationPath,
xpub: xpub,

View file

@ -1,12 +1,12 @@
import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';
import 'package:convert/convert.dart' as convert;
import 'dart:math';
import 'package:collection/collection.dart';
import 'package:crypto/crypto.dart';
import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart';
// import 'package:cw_bitcoin/electrum_wallet_addresses.dart';
import 'package:cw_core/cake_hive.dart';
import 'package:cw_core/mweb_utxo.dart';
import 'package:cw_mweb/mwebd.pbgrpc.dart';
@ -21,8 +21,6 @@ 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/pending_bitcoin_transaction.dart';
import 'package:cw_bitcoin/utils.dart';
import 'package:cw_bitcoin/electrum_derivations.dart';
import 'package:cw_core/encryption_file_utils.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/pending_transaction.dart';
@ -70,6 +68,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
Map<String, int>? initialChangeAddressIndex,
int? initialMwebHeight,
bool? alwaysScan,
required bool mempoolAPIEnabled,
}) : super(
mnemonic: mnemonic,
password: password,
@ -83,10 +82,11 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
encryptionFileUtils: encryptionFileUtils,
currency: CryptoCurrency.ltc,
alwaysScan: alwaysScan,
mempoolAPIEnabled: mempoolAPIEnabled,
) {
if (seedBytes != null) {
mwebHd = Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath(
"m/1000'") as Bip32Slip10Secp256k1;
mwebHd =
Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath("m/1000'") as Bip32Slip10Secp256k1;
mwebEnabled = alwaysScan ?? false;
} else {
mwebHd = null;
@ -98,38 +98,24 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
initialRegularAddressIndex: initialRegularAddressIndex,
initialChangeAddressIndex: initialChangeAddressIndex,
initialMwebAddresses: initialMwebAddresses,
mainHd: hd,
sideHd: accountHD.childKey(Bip32KeyIndex(1)),
network: network,
mwebHd: mwebHd,
mwebEnabled: mwebEnabled,
isHardwareWallet: walletInfo.isHardwareWallet,
hdWallets: hdWallets,
);
autorun((_) {
this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress;
});
reaction((_) => mwebSyncStatus, (status) async {
if (mwebSyncStatus is FailedSyncStatus) {
// we failed to connect to mweb, check if we are connected to the litecoin node:
late int nodeHeight;
try {
nodeHeight = await electrumClient.getCurrentBlockChainTip() ?? 0;
} catch (_) {
nodeHeight = 0;
}
if (nodeHeight == 0) {
// we aren't connected to the litecoin node, so the current electrum_wallet reactions will take care of this case for us
} else {
// we're connected to the litecoin node, but we failed to connect to mweb, try again after a few seconds:
await CwMweb.stop();
await Future.delayed(const Duration(seconds: 5));
startSync();
}
} else if (mwebSyncStatus is SyncingSyncStatus) {
syncStatus = mwebSyncStatus;
} else if (mwebSyncStatus is SyncronizingSyncStatus) {
if (syncStatus is! SyncronizingSyncStatus) {
} else if (mwebSyncStatus is SynchronizingSyncStatus) {
if (syncStatus is! SynchronizingSyncStatus) {
syncStatus = mwebSyncStatus;
}
} else if (mwebSyncStatus is SyncedSyncStatus) {
@ -154,8 +140,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
List<int> get scanSecret => mwebHd!.childKey(Bip32KeyIndex(0x80000000)).privateKey.privKey.raw;
List<int> get spendSecret => mwebHd!.childKey(Bip32KeyIndex(0x80000001)).privateKey.privKey.raw;
static Future<LitecoinWallet> create(
{required String mnemonic,
static Future<LitecoinWallet> create({
required String mnemonic,
required String password,
required WalletInfo walletInfo,
required Box<UnspentCoinsInfo> unspentCoinsInfo,
@ -166,8 +152,11 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
List<BitcoinAddressRecord>? initialMwebAddresses,
ElectrumBalance? initialBalance,
Map<String, int>? initialRegularAddressIndex,
Map<String, int>? initialChangeAddressIndex}) async {
Map<String, int>? initialChangeAddressIndex,
required bool mempoolAPIEnabled,
}) async {
late Uint8List seedBytes;
late BitcoinDerivationType derivationType;
switch (walletInfo.derivationInfo?.derivationType) {
case DerivationType.bip39:
@ -175,10 +164,12 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
mnemonic,
passphrase: passphrase ?? "",
);
derivationType = BitcoinDerivationType.bip39;
break;
case DerivationType.electrum:
default:
seedBytes = await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? "");
derivationType = BitcoinDerivationType.electrum;
break;
}
return LitecoinWallet(
@ -195,6 +186,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
initialRegularAddressIndex: initialRegularAddressIndex,
initialChangeAddressIndex: initialChangeAddressIndex,
addressPageType: addressPageType,
mempoolAPIEnabled: mempoolAPIEnabled,
);
}
@ -204,6 +196,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
required Box<UnspentCoinsInfo> unspentCoinsInfo,
required String password,
required bool alwaysScan,
required bool mempoolAPIEnabled,
required EncryptionFileUtils encryptionFileUtils,
}) async {
final hasKeysFile = await WalletKeysFile.hasKeysFile(name, walletInfo.type);
@ -239,10 +232,11 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
walletInfo.derivationInfo ??= DerivationInfo();
// set the default if not present:
walletInfo.derivationInfo!.derivationPath ??= snp?.derivationPath ?? electrum_path;
walletInfo.derivationInfo!.derivationPath ??= snp?.derivationPath ?? ELECTRUM_PATH;
walletInfo.derivationInfo!.derivationType ??= snp?.derivationType ?? DerivationType.electrum;
Uint8List? seedBytes = null;
late BitcoinDerivationType derivationType;
final mnemonic = keysData.mnemonic;
final passphrase = keysData.passphrase;
@ -253,10 +247,12 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
mnemonic,
passphrase: passphrase ?? "",
);
derivationType = BitcoinDerivationType.bip39;
break;
case DerivationType.electrum:
default:
seedBytes = await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? "");
derivationType = BitcoinDerivationType.electrum;
break;
}
}
@ -277,6 +273,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
initialChangeAddressIndex: snp?.changeAddressIndex,
addressPageType: snp?.addressPageType,
alwaysScan: snp?.alwaysScan,
mempoolAPIEnabled: mempoolAPIEnabled,
);
}
@ -301,16 +298,16 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
return;
}
if (mwebSyncStatus is SyncronizingSyncStatus) {
if (mwebSyncStatus is SynchronizingSyncStatus) {
return;
}
print("STARTING SYNC - MWEB ENABLED: $mwebEnabled");
_syncTimer?.cancel();
try {
mwebSyncStatus = SyncronizingSyncStatus();
mwebSyncStatus = SynchronizingSyncStatus();
try {
await subscribeForUpdates();
await subscribeForUpdates([]);
} catch (e) {
print("failed to subcribe for updates: $e");
}
@ -338,8 +335,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
return;
}
final nodeHeight =
await electrumClient.getCurrentBlockChainTip() ?? 0; // current block height of our node
final nodeHeight = await currentChainTip ?? 0;
if (nodeHeight == 0) {
// we aren't connected to the ltc node yet
@ -430,13 +426,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
@action
@override
Future<void> rescan({
required int height,
int? chainTip,
ScanData? scanData,
bool? doSingleScan,
bool? usingElectrs,
}) async {
Future<void> rescan({required int height}) async {
_syncTimer?.cancel();
await walletInfo.updateRestoreHeight(height);
@ -559,8 +549,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
}
_utxoStream = responseStream.listen((Utxo sUtxo) async {
// we're processing utxos, so our balance could still be innacurate:
if (mwebSyncStatus is! SyncronizingSyncStatus && mwebSyncStatus is! SyncingSyncStatus) {
mwebSyncStatus = SyncronizingSyncStatus();
if (mwebSyncStatus is! SynchronizingSyncStatus && mwebSyncStatus is! SyncingSyncStatus) {
mwebSyncStatus = SynchronizingSyncStatus();
processingUtxos = true;
_processingTimer?.cancel();
_processingTimer = Timer.periodic(const Duration(seconds: 2), (timer) async {
@ -631,7 +621,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
}
final status = await CwMweb.status(StatusRequest());
final height = await electrumClient.getCurrentBlockChainTip();
final height = await currentChainTip;
if (height == null || status.blockHeaderHeight != height) return;
if (status.mwebUtxosHeight != height) return; // we aren't synced
int amount = 0;
@ -766,7 +756,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
});
// copy coin control attributes to mwebCoins:
await updateCoins(mwebUnspentCoins);
// await updateCoins(mwebUnspentCoins);
// get regular ltc unspents (this resets unspentCoins):
await super.updateAllUnspents();
// add the mwebCoins:
@ -774,94 +764,126 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
}
@override
Future<ElectrumBalance> fetchBalances() async {
final balance = await super.fetchBalances();
if (!mwebEnabled) {
return balance;
@action
Future<Map<String, ElectrumTransactionInfo>> fetchTransactions() async {
throw UnimplementedError();
// try {
// final Map<String, ElectrumTransactionInfo> historiesWithDetails = {};
// await Future.wait(LITECOIN_ADDRESS_TYPES
// .map((type) => fetchTransactionsForAddressType(historiesWithDetails, type)));
// return historiesWithDetails;
// } catch (e) {
// print("fetchTransactions $e");
// return {};
// }
}
// update unspent balances:
await updateUnspent();
// @override
// @action
// Future<void> subscribeForUpdates([
// Iterable<BitcoinAddressRecord>? unsubscribedScriptHashes,
// ]) async {
// final unsubscribedScriptHashes = walletAddresses.allAddresses.where(
// (address) =>
// !scripthashesListening.contains(address.scriptHash) &&
// address.type != SegwitAddresType.mweb,
// );
int confirmed = balance.confirmed;
int unconfirmed = balance.unconfirmed;
int confirmedMweb = 0;
int unconfirmedMweb = 0;
try {
mwebUtxosBox.values.forEach((utxo) {
if (utxo.height > 0) {
confirmedMweb += utxo.value.toInt();
} else {
unconfirmedMweb += utxo.value.toInt();
}
});
if (unconfirmedMweb > 0) {
unconfirmedMweb = -1 * (confirmedMweb - unconfirmedMweb);
}
} catch (_) {}
// return super.subscribeForUpdates(unsubscribedScriptHashes);
// }
for (var addressRecord in walletAddresses.allAddresses) {
addressRecord.balance = 0;
addressRecord.txCount = 0;
}
// @override
// Future<ElectrumBalance> fetchBalances() async {
// final balance = await super.fetchBalances();
unspentCoins.forEach((coin) {
final coinInfoList = unspentCoinsInfo.values.where(
(element) =>
element.walletId.contains(id) &&
element.hash.contains(coin.hash) &&
element.vout == coin.vout,
);
// if (!mwebEnabled) {
// return balance;
// }
if (coinInfoList.isNotEmpty) {
final coinInfo = coinInfoList.first;
// // update unspent balances:
// await updateUnspent();
coin.isFrozen = coinInfo.isFrozen;
coin.isSending = coinInfo.isSending;
coin.note = coinInfo.note;
if (coin.bitcoinAddressRecord is! BitcoinSilentPaymentAddressRecord)
coin.bitcoinAddressRecord.balance += coinInfo.value;
} else {
super.addCoinInfo(coin);
}
});
// int confirmed = balance.confirmed;
// int unconfirmed = balance.unconfirmed;
// int confirmedMweb = 0;
// int unconfirmedMweb = 0;
// try {
// mwebUtxosBox.values.forEach((utxo) {
// if (utxo.height > 0) {
// confirmedMweb += utxo.value.toInt();
// } else {
// unconfirmedMweb += utxo.value.toInt();
// }
// });
// if (unconfirmedMweb > 0) {
// unconfirmedMweb = -1 * (confirmedMweb - unconfirmedMweb);
// }
// } catch (_) {}
// update the txCount for each address using the tx history, since we can't rely on mwebd
// to have an accurate count, we should just keep it in sync with what we know from the tx history:
for (final tx in transactionHistory.transactions.values) {
// if (tx.isPending) continue;
if (tx.inputAddresses == null || tx.outputAddresses == null) {
continue;
}
final txAddresses = tx.inputAddresses! + tx.outputAddresses!;
for (final address in txAddresses) {
final addressRecord = walletAddresses.allAddresses
.firstWhereOrNull((addressRecord) => addressRecord.address == address);
if (addressRecord == null) {
continue;
}
addressRecord.txCount++;
}
}
// for (var addressRecord in walletAddresses.allAddresses) {
// addressRecord.balance = 0;
// addressRecord.txCount = 0;
// }
return ElectrumBalance(
confirmed: confirmed,
unconfirmed: unconfirmed,
frozen: balance.frozen,
secondConfirmed: confirmedMweb,
secondUnconfirmed: unconfirmedMweb,
);
}
// unspentCoins.forEach((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 {
// super.addCoinInfo(coin);
// }
// });
// // update the txCount for each address using the tx history, since we can't rely on mwebd
// // to have an accurate count, we should just keep it in sync with what we know from the tx history:
// for (final tx in transactionHistory.transactions.values) {
// // if (tx.isPending) continue;
// if (tx.inputAddresses == null || tx.outputAddresses == null) {
// continue;
// }
// final txAddresses = tx.inputAddresses! + tx.outputAddresses!;
// for (final address in txAddresses) {
// final addressRecord = walletAddresses.allAddresses
// .firstWhereOrNull((addressRecord) => addressRecord.address == address);
// if (addressRecord == null) {
// continue;
// }
// addressRecord.txCount++;
// }
// }
// return ElectrumBalance(
// confirmed: confirmed,
// unconfirmed: unconfirmed,
// frozen: balance.frozen,
// secondConfirmed: confirmedMweb,
// secondUnconfirmed: unconfirmedMweb,
// );
// }
@override
int feeRate(TransactionPriority priority) {
if (priority is LitecoinTransactionPriority) {
if (priority is ElectrumTransactionPriority) {
switch (priority) {
case LitecoinTransactionPriority.slow:
case ElectrumTransactionPriority.slow:
return 1;
case LitecoinTransactionPriority.medium:
case ElectrumTransactionPriority.medium:
return 2;
case LitecoinTransactionPriority.fast:
case ElectrumTransactionPriority.fast:
return 3;
}
}
@ -873,25 +895,14 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
Future<int> calcFee({
required List<UtxoWithAddress> utxos,
required List<BitcoinBaseOutput> outputs,
required BasedUtxoNetwork network,
String? memo,
required int feeRate,
List<ECPrivateInfo>? inputPrivKeyInfos,
List<Outpoint>? vinOutpoints,
}) async {
final spendsMweb = utxos.any((utxo) => utxo.utxo.scriptType == SegwitAddresType.mweb);
final paysToMweb = outputs
.any((output) => output.toOutput.scriptPubKey.getAddressType() == SegwitAddresType.mweb);
if (!spendsMweb && !paysToMweb) {
return await super.calcFee(
utxos: utxos,
outputs: outputs,
network: network,
memo: memo,
feeRate: feeRate,
inputPrivKeyInfos: inputPrivKeyInfos,
vinOutpoints: vinOutpoints,
);
return await super.calcFee(utxos: utxos, outputs: outputs, memo: memo, feeRate: feeRate);
}
if (!mwebEnabled) {
@ -901,7 +912,9 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
if (outputs.length == 1 && outputs[0].toOutput.amount == BigInt.zero) {
outputs = [
BitcoinScriptOutput(
script: outputs[0].toOutput.scriptPubKey, value: utxos.sumOfUtxosValue())
script: outputs[0].toOutput.scriptPubKey,
value: utxos.sumOfUtxosValue(),
)
];
}
@ -933,9 +946,9 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
.map((output) =>
BitcoinScriptOutput(script: output.scriptPubKey, value: output.amount))
.toList(),
network: network,
memo: memo,
feeRate: feeRate) +
feeRate: feeRate,
) +
feeRate * 41;
}
return fee.toInt() + feeIncrease;
@ -949,8 +962,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
if (!mwebEnabled) {
tx.changeAddressOverride =
(await (walletAddresses as LitecoinWalletAddresses)
.getChangeAddress(isPegIn: false))
(await (walletAddresses as LitecoinWalletAddresses).getChangeAddress(isPegIn: false))
.address;
return tx;
}
@ -990,8 +1002,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
bool isPegIn = !hasMwebInput && hasMwebOutput;
bool isRegular = !hasMwebInput && !hasMwebOutput;
tx.changeAddressOverride =
(await (walletAddresses as LitecoinWalletAddresses)
tx.changeAddressOverride = (await (walletAddresses as LitecoinWalletAddresses)
.getChangeAddress(isPegIn: isPegIn || isRegular))
.address;
if (!hasMwebInput && !hasMwebOutput) {
@ -1025,12 +1036,12 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
witnesses: tx2.inputs.asMap().entries.map((e) {
final utxo = unspentCoins
.firstWhere((utxo) => utxo.hash == e.value.txId && utxo.vout == e.value.txIndex);
final key = generateECPrivate(
hd: utxo.bitcoinAddressRecord.isHidden
? walletAddresses.sideHd
: walletAddresses.mainHd,
index: utxo.bitcoinAddressRecord.index,
network: network);
final addressRecord = (utxo.bitcoinAddressRecord as BitcoinAddressRecord);
final path = addressRecord.derivationInfo.derivationPath
.addElem(
Bip32KeyIndex(BitcoinAddressUtils.getAccountFromChange(addressRecord.isChange)))
.addElem(Bip32KeyIndex(addressRecord.index));
final key = ECPrivate.fromBip32(bip32: bip32.derive(path));
final digest = tx2.getTransactionSegwitDigit(
txInIndex: e.key,
script: key.getPublic().toP2pkhAddress().toScriptPubKey(),
@ -1113,10 +1124,17 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
@override
Future<String> signMessage(String message, {String? address = null}) async {
final index = address != null
? walletAddresses.allAddresses.firstWhere((element) => element.address == address).index
: null;
final HD = index == null ? hd : hd.childKey(Bip32KeyIndex(index));
Bip32Slip10Secp256k1 HD = bip32;
final record = walletAddresses.allAddresses.firstWhere((element) => element.address == address);
if (record.isChange) {
HD = HD.childKey(Bip32KeyIndex(1));
} else {
HD = HD.childKey(Bip32KeyIndex(0));
}
HD = HD.childKey(Bip32KeyIndex(record.index));
final priv = ECPrivate.fromHex(HD.privateKey.privKey.toHex());
final privateKey = ECDSAPrivateKey.fromBytes(
@ -1240,8 +1258,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
@override
void setLedgerConnection(LedgerConnection connection) {
_ledgerConnection = connection;
_litecoinLedgerApp =
LitecoinLedgerApp(_ledgerConnection!, derivationPath: walletInfo.derivationInfo!.derivationPath!);
_litecoinLedgerApp = LitecoinLedgerApp(_ledgerConnection!,
derivationPath: walletInfo.derivationInfo!.derivationPath!);
}
@override
@ -1258,7 +1276,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
}) async {
final readyInputs = <LedgerTransaction>[];
for (final utxo in utxos) {
final rawTx = await electrumClient.getTransactionHex(hash: utxo.utxo.txHash);
final rawTx =
(await getTransactionExpanded(hash: utxo.utxo.txHash)).originalTransaction.toHex();
final publicKeyAndDerivationPath = publicKeys[utxo.ownerDetails.address.pubKeyHash()]!;
readyInputs.add(LedgerTransaction(
@ -1277,19 +1296,17 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
if (maybeChangePath != null) changePath ??= maybeChangePath.derivationPath;
}
final rawHex = await _litecoinLedgerApp!.createTransaction(
inputs: readyInputs,
outputs: outputs
.map((e) => TransactionOutput.fromBigInt(
(e as BitcoinOutput).value, Uint8List.fromList(e.address.toScriptPubKey().toBytes())))
.map((e) => TransactionOutput.fromBigInt((e as BitcoinOutput).value,
Uint8List.fromList(e.address.toScriptPubKey().toBytes())))
.toList(),
changePath: changePath,
sigHashType: 0x01,
additionals: ["bech32"],
isSegWit: true,
useTrustedInputForSegwit: true
);
useTrustedInputForSegwit: true);
return BtcTransaction.fromRaw(rawHex);
}

View file

@ -6,8 +6,6 @@ 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_unspent.dart';
import 'package:cw_bitcoin/electrum_wallet.dart';
import 'package:cw_bitcoin/utils.dart';
import 'package:cw_bitcoin/electrum_wallet_addresses.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_mweb/cw_mweb.dart';
@ -16,19 +14,16 @@ import 'package:mobx/mobx.dart';
part 'litecoin_wallet_addresses.g.dart';
class LitecoinWalletAddresses = LitecoinWalletAddressesBase
with _$LitecoinWalletAddresses;
class LitecoinWalletAddresses = LitecoinWalletAddressesBase with _$LitecoinWalletAddresses;
abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses
with Store {
abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with Store {
LitecoinWalletAddressesBase(
WalletInfo walletInfo, {
required super.mainHd,
required super.sideHd,
required super.network,
required super.isHardwareWallet,
required this.mwebHd,
required this.mwebEnabled,
required super.hdWallets,
super.initialAddresses,
super.initialMwebAddresses,
super.initialRegularAddressIndex,
@ -46,14 +41,13 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses
List<String> mwebAddrs = [];
bool generating = false;
List<int> get scanSecret =>
mwebHd!.childKey(Bip32KeyIndex(0x80000000)).privateKey.privKey.raw;
List<int> get scanSecret => mwebHd!.childKey(Bip32KeyIndex(0x80000000)).privateKey.privKey.raw;
List<int> get spendPubkey =>
mwebHd!.childKey(Bip32KeyIndex(0x80000001)).publicKey.pubKey.compressed;
@override
Future<void> init() async {
if (!isHardwareWallet) await initMwebAddresses();
if (!super.isHardwareWallet) await initMwebAddresses();
await super.init();
}
@ -104,12 +98,16 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses
List<BitcoinAddressRecord> addressRecords = mwebAddrs
.asMap()
.entries
.map((e) => BitcoinAddressRecord(
.map(
(e) => BitcoinAddressRecord(
e.value,
index: e.key,
type: SegwitAddresType.mweb,
network: network,
))
derivationInfo: BitcoinAddressUtils.getDerivationFromType(SegwitAddresType.p2wpkh),
derivationType: CWBitcoinDerivationType.bip39,
),
)
.toList();
addMwebAddresses(addressRecords);
print("set ${addressRecords.length} mweb addresses");
@ -121,30 +119,47 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses
await ensureMwebAddressUpToIndexExists(20);
return;
}
}
@override
String getAddress({
BitcoinBaseAddress generateAddress({
required CWBitcoinDerivationType derivationType,
required bool isChange,
required int index,
required Bip32Slip10Secp256k1 hd,
BitcoinAddressType? addressType,
required BitcoinAddressType addressType,
required BitcoinDerivationInfo derivationInfo,
}) {
if (addressType == SegwitAddresType.mweb) {
return hd == sideHd ? mwebAddrs[0] : mwebAddrs[index + 1];
return MwebAddress.fromAddress(address: mwebAddrs[0], network: network);
}
return P2wpkhAddress.fromDerivation(
bip32: bip32,
derivationInfo: derivationInfo,
isChange: isChange,
index: index,
);
}
return generateP2WPKHAddress(hd: hd, index: index, network: network);
}
@override
Future<String> getAddressAsync({
required CWBitcoinDerivationType derivationType,
required bool isChange,
required int index,
required Bip32Slip10Secp256k1 hd,
BitcoinAddressType? addressType,
required BitcoinAddressType addressType,
required BitcoinDerivationInfo derivationInfo,
}) async {
if (addressType == SegwitAddresType.mweb) {
await ensureMwebAddressUpToIndexExists(index);
}
return getAddress(index: index, hd: hd, addressType: addressType);
return getAddress(
derivationType: derivationType,
isChange: isChange,
index: index,
addressType: addressType,
derivationInfo: derivationInfo,
);
}
@action
@ -198,6 +213,8 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses
index: 0,
type: SegwitAddresType.mweb,
network: network,
derivationInfo: BitcoinAddressUtils.getDerivationFromType(SegwitAddresType.p2wpkh),
derivationType: CWBitcoinDerivationType.bip39,
);
}

View file

@ -23,12 +23,18 @@ class LitecoinWalletService extends WalletService<
BitcoinRestoreWalletFromWIFCredentials,
BitcoinRestoreWalletFromHardware> {
LitecoinWalletService(
this.walletInfoSource, this.unspentCoinsInfoSource, this.alwaysScan, this.isDirect);
this.walletInfoSource,
this.unspentCoinsInfoSource,
this.alwaysScan,
this.isDirect,
this.mempoolAPIEnabled,
);
final Box<WalletInfo> walletInfoSource;
final Box<UnspentCoinsInfo> unspentCoinsInfoSource;
final bool alwaysScan;
final bool isDirect;
final bool mempoolAPIEnabled;
@override
WalletType getType() => WalletType.litecoin;
@ -55,6 +61,7 @@ class LitecoinWalletService extends WalletService<
walletInfo: credentials.walletInfo!,
unspentCoinsInfo: unspentCoinsInfoSource,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
mempoolAPIEnabled: mempoolAPIEnabled,
);
await wallet.save();
await wallet.init();
@ -68,7 +75,6 @@ class LitecoinWalletService extends WalletService<
@override
Future<LitecoinWallet> openWallet(String name, String password) async {
final walletInfo = walletInfoSource.values
.firstWhereOrNull((info) => info.id == WalletBase.idFor(name, getType()))!;
@ -80,6 +86,7 @@ class LitecoinWalletService extends WalletService<
unspentCoinsInfo: unspentCoinsInfoSource,
alwaysScan: alwaysScan,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
mempoolAPIEnabled: mempoolAPIEnabled,
);
await wallet.init();
saveBackup(name);
@ -93,6 +100,7 @@ class LitecoinWalletService extends WalletService<
unspentCoinsInfo: unspentCoinsInfoSource,
alwaysScan: alwaysScan,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
mempoolAPIEnabled: mempoolAPIEnabled,
);
await wallet.init();
return wallet;
@ -135,6 +143,7 @@ class LitecoinWalletService extends WalletService<
unspentCoinsInfo: unspentCoinsInfoSource,
alwaysScan: alwaysScan,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
mempoolAPIEnabled: mempoolAPIEnabled,
);
await currentWallet.renameWalletFiles(newName);
@ -161,6 +170,7 @@ class LitecoinWalletService extends WalletService<
walletInfo: credentials.walletInfo!,
unspentCoinsInfo: unspentCoinsInfoSource,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
mempoolAPIEnabled: mempoolAPIEnabled,
);
await wallet.save();
await wallet.init();
@ -186,6 +196,7 @@ class LitecoinWalletService extends WalletService<
walletInfo: credentials.walletInfo!,
unspentCoinsInfo: unspentCoinsInfoSource,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
mempoolAPIEnabled: mempoolAPIEnabled,
);
await wallet.save();
await wallet.init();

View file

@ -1,10 +1,10 @@
import 'package:cw_bitcoin/electrum_worker/electrum_worker_params.dart';
import 'package:cw_bitcoin/electrum_worker/methods/methods.dart';
import 'package:grpc/grpc.dart';
import 'package:cw_bitcoin/exceptions.dart';
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:blockchain_utils/blockchain_utils.dart';
import 'package:cw_core/pending_transaction.dart';
import 'package:cw_bitcoin/electrum.dart';
import 'package:cw_bitcoin/bitcoin_amount_format.dart';
import 'package:cw_bitcoin/electrum_transaction_info.dart';
import 'package:cw_core/transaction_direction.dart';
import 'package:cw_core/wallet_type.dart';
@ -15,11 +15,10 @@ class PendingBitcoinTransaction with PendingTransaction {
PendingBitcoinTransaction(
this._tx,
this.type, {
required this.electrumClient,
required this.sendWorker,
required this.amount,
required this.fee,
required this.feeRate,
this.network,
required this.hasChange,
this.isSendAll = false,
this.hasTaprootInputs = false,
@ -29,11 +28,10 @@ class PendingBitcoinTransaction with PendingTransaction {
final WalletType type;
final BtcTransaction _tx;
final ElectrumClient electrumClient;
Future<dynamic> Function(ElectrumWorkerRequest) sendWorker;
final int amount;
final int fee;
final String feeRate;
final BasedUtxoNetwork? network;
final bool isSendAll;
final bool hasChange;
final bool hasTaprootInputs;
@ -51,10 +49,10 @@ class PendingBitcoinTransaction with PendingTransaction {
String get hex => hexOverride ?? _tx.serialize();
@override
String get amountFormatted => bitcoinAmountToString(amount: amount);
String get amountFormatted => BitcoinAmountUtils.bitcoinAmountToString(amount: amount);
@override
String get feeFormatted => bitcoinAmountToString(amount: fee);
String get feeFormatted => BitcoinAmountUtils.bitcoinAmountToString(amount: fee);
@override
int? get outputCount => _tx.outputs.length;
@ -80,40 +78,39 @@ class PendingBitcoinTransaction with PendingTransaction {
Future<void> _commit() async {
int? callId;
final result = await electrumClient.broadcastTransaction(
transactionRaw: hex, network: network, idCallback: (id) => callId = id);
final result = await sendWorker(ElectrumWorkerBroadcastRequest(transactionRaw: hex)) as String;
if (result.isEmpty) {
if (callId != null) {
final error = electrumClient.getErrorMessage(callId!);
// if (result.isEmpty) {
// if (callId != null) {
// final error = sendWorker(getErrorMessage(callId!));
if (error.contains("dust")) {
if (hasChange) {
throw BitcoinTransactionCommitFailedDustChange();
} else if (!isSendAll) {
throw BitcoinTransactionCommitFailedDustOutput();
} else {
throw BitcoinTransactionCommitFailedDustOutputSendAll();
}
}
// if (error.contains("dust")) {
// if (hasChange) {
// throw BitcoinTransactionCommitFailedDustChange();
// } else if (!isSendAll) {
// throw BitcoinTransactionCommitFailedDustOutput();
// } else {
// throw BitcoinTransactionCommitFailedDustOutputSendAll();
// }
// }
if (error.contains("bad-txns-vout-negative")) {
throw BitcoinTransactionCommitFailedVoutNegative();
}
// if (error.contains("bad-txns-vout-negative")) {
// throw BitcoinTransactionCommitFailedVoutNegative();
// }
if (error.contains("non-BIP68-final")) {
throw BitcoinTransactionCommitFailedBIP68Final();
}
// if (error.contains("non-BIP68-final")) {
// throw BitcoinTransactionCommitFailedBIP68Final();
// }
if (error.contains("min fee not met")) {
throw BitcoinTransactionCommitFailedLessThanMin();
}
// if (error.contains("min fee not met")) {
// throw BitcoinTransactionCommitFailedLessThanMin();
// }
throw BitcoinTransactionCommitFailed(errorMessage: error);
}
// throw BitcoinTransactionCommitFailed(errorMessage: error);
// }
throw BitcoinTransactionCommitFailed();
}
// throw BitcoinTransactionCommitFailed();
// }
}
Future<void> _ltcCommit() async {

View file

@ -1,54 +0,0 @@
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:blockchain_utils/blockchain_utils.dart';
ECPrivate generateECPrivate({
required Bip32Slip10Secp256k1 hd,
required BasedUtxoNetwork network,
required int index,
}) =>
ECPrivate(hd.childKey(Bip32KeyIndex(index)).privateKey);
String generateP2WPKHAddress({
required Bip32Slip10Secp256k1 hd,
required BasedUtxoNetwork network,
required int index,
}) =>
ECPublic.fromBip32(hd.childKey(Bip32KeyIndex(index)).publicKey)
.toP2wpkhAddress()
.toAddress(network);
String generateP2SHAddress({
required Bip32Slip10Secp256k1 hd,
required BasedUtxoNetwork network,
required int index,
}) =>
ECPublic.fromBip32(hd.childKey(Bip32KeyIndex(index)).publicKey)
.toP2wpkhInP2sh()
.toAddress(network);
String generateP2WSHAddress({
required Bip32Slip10Secp256k1 hd,
required BasedUtxoNetwork network,
required int index,
}) =>
ECPublic.fromBip32(hd.childKey(Bip32KeyIndex(index)).publicKey)
.toP2wshAddress()
.toAddress(network);
String generateP2PKHAddress({
required Bip32Slip10Secp256k1 hd,
required BasedUtxoNetwork network,
required int index,
}) =>
ECPublic.fromBip32(hd.childKey(Bip32KeyIndex(index)).publicKey)
.toP2pkhAddress()
.toAddress(network);
String generateP2TRAddress({
required Bip32Slip10Secp256k1 hd,
required BasedUtxoNetwork network,
required int index,
}) =>
ECPublic.fromBip32(hd.childKey(Bip32KeyIndex(index)).publicKey)
.toTaprootAddress()
.toAddress(network);

View file

@ -87,18 +87,18 @@ packages:
dependency: "direct overridden"
description:
path: "."
ref: cake-update-v8
resolved-ref: fc045a11db3d85d806ca67f75e8b916c706745a2
url: "https://github.com/cake-tech/bitcoin_base"
ref: cake-update-v15
resolved-ref: "49db5748d2edc73c0c8213e11ab6a39fa3a7ff7f"
url: "https://github.com/cake-tech/bitcoin_base.git"
source: git
version: "4.7.0"
blockchain_utils:
dependency: "direct main"
description:
path: "."
ref: cake-update-v2
resolved-ref: "59fdf29d72068e0522a96a8953ed7272833a9f57"
url: "https://github.com/cake-tech/blockchain_utils"
ref: cake-update-v3
resolved-ref: "9b64c43bcfe129e7f01300a63607fde083dd0357"
url: "https://github.com/cake-tech/blockchain_utils.git"
source: git
version: "3.3.0"
bluez:
@ -415,10 +415,10 @@ packages:
dependency: transitive
description:
name: google_identity_services_web
sha256: "5be191523702ba8d7a01ca97c17fca096822ccf246b0a9f11923a6ded06199b6"
sha256: "0c56c2c5d60d6dfaf9725f5ad4699f04749fb196ee5a70487a46ef184837ccf6"
url: "https://pub.dev"
source: hosted
version: "0.3.1+4"
version: "0.3.0+2"
googleapis_auth:
dependency: transitive
description:
@ -471,10 +471,10 @@ packages:
dependency: "direct main"
description:
name: http
sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010
sha256: a2bbf9d017fcced29139daa8ed2bba4ece450ab222871df93ca9eec6f80c34ba
url: "https://pub.dev"
source: hosted
version: "1.2.2"
version: "1.2.0"
http2:
dependency: transitive
description:
@ -849,10 +849,10 @@ packages:
dependency: transitive
description:
name: shared_preferences_web
sha256: "59dc807b94d29d52ddbb1b3c0d3b9d0a67fc535a64e62a5542c8db0513fcb6c2"
sha256: "7b15ffb9387ea3e237bb7a66b8a23d2147663d391cafc5c8f37b2e7b4bde5d21"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
version: "2.2.2"
shared_preferences_windows:
dependency: transitive
description:
@ -918,9 +918,9 @@ packages:
dependency: "direct main"
description:
path: "."
ref: "sp_v4.0.0"
resolved-ref: ca1add293bd1e06920aa049b655832da50d0dab2
url: "https://github.com/cake-tech/sp_scanner"
ref: cake-update-v3
resolved-ref: "2c21e53fd652e0aee1ee5fcd891376c10334237b"
url: "https://github.com/cake-tech/sp_scanner.git"
source: git
version: "0.0.1"
stack_trace:
@ -1047,18 +1047,18 @@ packages:
dependency: transitive
description:
name: web
sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27"
sha256: "4188706108906f002b3a293509234588823c8c979dc83304e229ff400c996b05"
url: "https://pub.dev"
source: hosted
version: "0.5.1"
version: "0.4.2"
web_socket_channel:
dependency: transitive
description:
name: web_socket_channel
sha256: "58c6666b342a38816b2e7e50ed0f1e261959630becd4c879c4f26bfa14aa5a42"
sha256: "939ab60734a4f8fa95feacb55804fa278de28bdeef38e616dc08e44a84adea23"
url: "https://pub.dev"
source: hosted
version: "2.4.5"
version: "2.4.3"
xdg_directories:
dependency: transitive
description:

View file

@ -29,14 +29,14 @@ dependencies:
blockchain_utils:
git:
url: https://github.com/cake-tech/blockchain_utils
ref: cake-update-v2
ref: cake-update-v3
cw_mweb:
path: ../cw_mweb
grpc: ^3.2.4
sp_scanner:
git:
url: https://github.com/cake-tech/sp_scanner
ref: sp_v4.0.0
url: https://github.com/cake-tech/sp_scanner.git
ref: cake-update-v3
bech32:
git:
url: https://github.com/cake-tech/bech32.git
@ -63,8 +63,12 @@ dependency_overrides:
protobuf: ^3.1.0
bitcoin_base:
git:
url: https://github.com/cake-tech/bitcoin_base
ref: cake-update-v9
url: https://github.com/cake-tech/bitcoin_base.git
ref: cake-update-v15
blockchain_utils:
git:
url: https://github.com/cake-tech/blockchain_utils
ref: cake-update-v3
pointycastle: 3.7.4
ffi: 2.1.0

View file

@ -6,6 +6,7 @@ import 'package:cw_bitcoin/bitcoin_mnemonics_bip39.dart';
import 'package:cw_bitcoin/bitcoin_transaction_priority.dart';
import 'package:cw_bitcoin/electrum_balance.dart';
import 'package:cw_bitcoin/electrum_wallet.dart';
import 'package:cw_bitcoin/electrum_wallet_addresses.dart';
import 'package:cw_bitcoin/electrum_wallet_snapshot.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/encryption_file_utils.dart';
@ -37,6 +38,7 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
ElectrumBalance? initialBalance,
Map<String, int>? initialRegularAddressIndex,
Map<String, int>? initialChangeAddressIndex,
required bool mempoolAPIEnabled,
}) : super(
mnemonic: mnemonic,
password: password,
@ -48,25 +50,30 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
seedBytes: seedBytes,
currency: CryptoCurrency.bch,
encryptionFileUtils: encryptionFileUtils,
passphrase: passphrase) {
passphrase: passphrase,
mempoolAPIEnabled: mempoolAPIEnabled,
hdWallets: {CWBitcoinDerivationType.bip39: bitcoinCashHDWallet(seedBytes)},
) {
walletAddresses = BitcoinCashWalletAddresses(
walletInfo,
initialAddresses: initialAddresses,
initialRegularAddressIndex: initialRegularAddressIndex,
initialChangeAddressIndex: initialChangeAddressIndex,
mainHd: hd,
sideHd: accountHD.childKey(Bip32KeyIndex(1)),
network: network,
initialAddressPageType: addressPageType,
isHardwareWallet: walletInfo.isHardwareWallet,
hdWallets: hdWallets,
);
autorun((_) {
this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress;
});
}
static Future<BitcoinCashWallet> create(
{required String mnemonic,
@override
BitcoinCashNetwork get network => BitcoinCashNetwork.mainnet;
static Future<BitcoinCashWallet> create({
required String mnemonic,
required String password,
required WalletInfo walletInfo,
required Box<UnspentCoinsInfo> unspentCoinsInfo,
@ -76,7 +83,9 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
List<BitcoinAddressRecord>? initialAddresses,
ElectrumBalance? initialBalance,
Map<String, int>? initialRegularAddressIndex,
Map<String, int>? initialChangeAddressIndex}) async {
Map<String, int>? initialChangeAddressIndex,
required bool mempoolAPIEnabled,
}) async {
return BitcoinCashWallet(
mnemonic: mnemonic,
password: password,
@ -90,6 +99,7 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
initialChangeAddressIndex: initialChangeAddressIndex,
addressPageType: P2pkhAddressType.p2pkh,
passphrase: passphrase,
mempoolAPIEnabled: mempoolAPIEnabled,
);
}
@ -99,6 +109,7 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
required Box<UnspentCoinsInfo> unspentCoinsInfo,
required String password,
required EncryptionFileUtils encryptionFileUtils,
required bool mempoolAPIEnabled,
}) async {
final hasKeysFile = await WalletKeysFile.hasKeysFile(name, walletInfo.type);
@ -141,17 +152,21 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
return BitcoinAddressRecord(
addr.address,
index: addr.index,
isHidden: addr.isHidden,
isChange: addr.isChange,
type: P2pkhAddressType.p2pkh,
network: BitcoinCashNetwork.mainnet,
derivationInfo: BitcoinAddressUtils.getDerivationFromType(P2pkhAddressType.p2pkh),
derivationType: CWBitcoinDerivationType.bip39,
);
} catch (_) {
return BitcoinAddressRecord(
AddressUtils.getCashAddrFormat(addr.address),
index: addr.index,
isHidden: addr.isHidden,
isChange: addr.isChange,
type: P2pkhAddressType.p2pkh,
network: BitcoinCashNetwork.mainnet,
derivationInfo: BitcoinAddressUtils.getDerivationFromType(P2pkhAddressType.p2pkh),
derivationType: CWBitcoinDerivationType.bip39,
);
}
}).toList(),
@ -162,6 +177,7 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
initialChangeAddressIndex: snp?.changeAddressIndex,
addressPageType: P2pkhAddressType.p2pkh,
passphrase: keysData.passphrase,
mempoolAPIEnabled: mempoolAPIEnabled,
);
}
@ -193,13 +209,13 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
@override
int feeRate(TransactionPriority priority) {
if (priority is BitcoinCashTransactionPriority) {
if (priority is ElectrumTransactionPriority) {
switch (priority) {
case BitcoinCashTransactionPriority.slow:
case ElectrumTransactionPriority.slow:
return 1;
case BitcoinCashTransactionPriority.medium:
case ElectrumTransactionPriority.medium:
return 5;
case BitcoinCashTransactionPriority.fast:
case ElectrumTransactionPriority.fast:
return 10;
}
}
@ -209,17 +225,39 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
@override
Future<String> signMessage(String message, {String? address = null}) async {
int? index;
try {
index = address != null
? walletAddresses.allAddresses.firstWhere((element) => element.address == address).index
: null;
} catch (_) {}
final HD = index == null ? hd : hd.childKey(Bip32KeyIndex(index));
Bip32Slip10Secp256k1 HD = bip32;
final record = walletAddresses.allAddresses.firstWhere((element) => element.address == address);
if (record.isChange) {
HD = HD.childKey(Bip32KeyIndex(1));
} else {
HD = HD.childKey(Bip32KeyIndex(0));
}
HD = HD.childKey(Bip32KeyIndex(record.index));
final priv = ECPrivate.fromWif(
WifEncoder.encode(HD.privateKey.raw, netVer: network.wifNetVer),
netVersion: network.wifNetVer,
);
return priv.signMessage(StringUtils.encode(message));
}
@override
Future<int> calcFee({
required List<UtxoWithAddress> utxos,
required List<BitcoinBaseOutput> outputs,
String? memo,
required int feeRate,
}) async =>
feeRate *
ForkedTransactionBuilder.estimateTransactionSize(
utxos: utxos,
outputs: outputs,
network: network,
memo: memo,
);
static Bip32Slip10Secp256k1 bitcoinCashHDWallet(List<int> seedBytes) =>
Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath("m/44'/145'/0'") as Bip32Slip10Secp256k1;
}

View file

@ -1,7 +1,5 @@
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:blockchain_utils/blockchain_utils.dart';
import 'package:cw_bitcoin/electrum_wallet_addresses.dart';
import 'package:cw_bitcoin/utils.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:mobx/mobx.dart';
@ -12,10 +10,9 @@ class BitcoinCashWalletAddresses = BitcoinCashWalletAddressesBase with _$Bitcoin
abstract class BitcoinCashWalletAddressesBase extends ElectrumWalletAddresses with Store {
BitcoinCashWalletAddressesBase(
WalletInfo walletInfo, {
required super.mainHd,
required super.sideHd,
required super.network,
required super.isHardwareWallet,
required super.hdWallets,
super.initialAddresses,
super.initialRegularAddressIndex,
super.initialChangeAddressIndex,
@ -23,9 +20,17 @@ abstract class BitcoinCashWalletAddressesBase extends ElectrumWalletAddresses wi
}) : super(walletInfo);
@override
String getAddress(
{required int index,
required Bip32Slip10Secp256k1 hd,
BitcoinAddressType? addressType}) =>
generateP2PKHAddress(hd: hd, index: index, network: network);
BitcoinBaseAddress generateAddress({
required CWBitcoinDerivationType derivationType,
required bool isChange,
required int index,
required BitcoinAddressType addressType,
required BitcoinDerivationInfo derivationInfo,
}) =>
P2pkhAddress.fromDerivation(
bip32: bip32,
derivationInfo: derivationInfo,
isChange: isChange,
index: index,
);
}

View file

@ -18,11 +18,17 @@ class BitcoinCashWalletService extends WalletService<
BitcoinCashRestoreWalletFromSeedCredentials,
BitcoinCashRestoreWalletFromWIFCredentials,
BitcoinCashNewWalletCredentials> {
BitcoinCashWalletService(this.walletInfoSource, this.unspentCoinsInfoSource, this.isDirect);
BitcoinCashWalletService(
this.walletInfoSource,
this.unspentCoinsInfoSource,
this.isDirect,
this.mempoolAPIEnabled,
);
final Box<WalletInfo> walletInfoSource;
final Box<UnspentCoinsInfo> unspentCoinsInfoSource;
final bool isDirect;
final bool mempoolAPIEnabled;
@override
WalletType getType() => WalletType.bitcoinCash;
@ -42,6 +48,7 @@ class BitcoinCashWalletService extends WalletService<
unspentCoinsInfo: unspentCoinsInfoSource,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
passphrase: credentials.passphrase,
mempoolAPIEnabled: mempoolAPIEnabled,
);
await wallet.save();
await wallet.init();
@ -61,6 +68,7 @@ class BitcoinCashWalletService extends WalletService<
walletInfo: walletInfo,
unspentCoinsInfo: unspentCoinsInfoSource,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
mempoolAPIEnabled: mempoolAPIEnabled,
);
await wallet.init();
saveBackup(name);
@ -73,6 +81,7 @@ class BitcoinCashWalletService extends WalletService<
walletInfo: walletInfo,
unspentCoinsInfo: unspentCoinsInfoSource,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
mempoolAPIEnabled: mempoolAPIEnabled,
);
await wallet.init();
return wallet;
@ -96,7 +105,9 @@ class BitcoinCashWalletService extends WalletService<
name: currentName,
walletInfo: currentWalletInfo,
unspentCoinsInfo: unspentCoinsInfoSource,
encryptionFileUtils: encryptionFileUtilsFor(isDirect));
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
mempoolAPIEnabled: mempoolAPIEnabled,
);
await currentWallet.renameWalletFiles(newName);
await saveBackup(newName);
@ -133,7 +144,8 @@ class BitcoinCashWalletService extends WalletService<
walletInfo: credentials.walletInfo!,
unspentCoinsInfo: unspentCoinsInfoSource,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
passphrase: credentials.passphrase
passphrase: credentials.passphrase,
mempoolAPIEnabled: mempoolAPIEnabled,
);
await wallet.save();
await wallet.init();

View file

@ -2,7 +2,7 @@ import 'package:cw_bitcoin/exceptions.dart';
import 'package:bitbox/bitbox.dart' as bitbox;
import 'package:cw_core/pending_transaction.dart';
import 'package:cw_bitcoin/electrum.dart';
import 'package:cw_bitcoin/bitcoin_amount_format.dart';
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:cw_bitcoin/electrum_transaction_info.dart';
import 'package:cw_core/transaction_direction.dart';
import 'package:cw_core/wallet_type.dart';
@ -31,10 +31,10 @@ class PendingBitcoinCashTransaction with PendingTransaction {
String get hex => _tx.toHex();
@override
String get amountFormatted => bitcoinAmountToString(amount: amount);
String get amountFormatted => BitcoinAmountUtils.bitcoinAmountToString(amount: amount);
@override
String get feeFormatted => bitcoinAmountToString(amount: fee);
String get feeFormatted => BitcoinAmountUtils.bitcoinAmountToString(amount: fee);
final List<void Function(ElectrumTransactionInfo transaction)> _listeners;
@ -74,7 +74,8 @@ class PendingBitcoinCashTransaction with PendingTransaction {
void addListener(void Function(ElectrumTransactionInfo transaction) listener) =>
_listeners.add(listener);
ElectrumTransactionInfo transactionInfo() => ElectrumTransactionInfo(type,
ElectrumTransactionInfo transactionInfo() => ElectrumTransactionInfo(
type,
id: id,
height: 0,
amount: amount,

View file

@ -28,7 +28,7 @@ dependencies:
blockchain_utils:
git:
url: https://github.com/cake-tech/blockchain_utils
ref: cake-update-v2
ref: cake-update-v3
dev_dependencies:
flutter_test:
@ -41,8 +41,12 @@ dependency_overrides:
watcher: ^1.1.0
bitcoin_base:
git:
url: https://github.com/cake-tech/bitcoin_base
ref: cake-update-v9
url: https://github.com/cake-tech/bitcoin_base.git
ref: cake-update-v15
blockchain_utils:
git:
url: https://github.com/cake-tech/blockchain_utils
ref: cake-update-v3
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec

View file

@ -45,7 +45,7 @@ class SyncedTipSyncStatus extends SyncedSyncStatus {
final int tip;
}
class SyncronizingSyncStatus extends SyncStatus {
class SynchronizingSyncStatus extends SyncStatus {
@override
double progress() => 0.0;
}
@ -96,3 +96,58 @@ class LostConnectionSyncStatus extends NotConnectedSyncStatus {
@override
String toString() => 'Reconnecting';
}
Map<String, dynamic> syncStatusToJson(SyncStatus? status) {
if (status == null) {
return {};
}
return {
'progress': status.progress(),
'type': status.runtimeType.toString(),
'data': status is SyncingSyncStatus
? {'blocksLeft': status.blocksLeft, 'ptc': status.ptc}
: status is SyncedTipSyncStatus
? {'tip': status.tip}
: status is FailedSyncStatus
? {'error': status.error}
: status is StartingScanSyncStatus
? {'beginHeight': status.beginHeight}
: null
};
}
SyncStatus syncStatusFromJson(Map<String, dynamic> json) {
final type = json['type'] as String;
final data = json['data'] as Map<String, dynamic>?;
switch (type) {
case 'StartingScanSyncStatus':
return StartingScanSyncStatus(data!['beginHeight'] as int);
case 'SyncingSyncStatus':
return SyncingSyncStatus(data!['blocksLeft'] as int, data['ptc'] as double);
case 'SyncedTipSyncStatus':
return SyncedTipSyncStatus(data!['tip'] as int);
case 'FailedSyncStatus':
return FailedSyncStatus(error: data!['error'] as String?);
case 'SynchronizingSyncStatus':
return SynchronizingSyncStatus();
case 'NotConnectedSyncStatus':
return NotConnectedSyncStatus();
case 'AttemptingSyncStatus':
return AttemptingSyncStatus();
case 'AttemptingScanSyncStatus':
return AttemptingScanSyncStatus();
case 'ConnectedSyncStatus':
return ConnectedSyncStatus();
case 'ConnectingSyncStatus':
return ConnectingSyncStatus();
case 'UnsupportedSyncStatus':
return UnsupportedSyncStatus();
case 'TimedOutSyncStatus':
return TimedOutSyncStatus();
case 'LostConnectionSyncStatus':
return LostConnectionSyncStatus();
default:
throw Exception('Unknown sync status type: $type');
}
}

View file

@ -1,6 +1,20 @@
import 'package:cw_core/enumerable_item.dart';
abstract class TransactionPriority extends EnumerableItem<int>
with Serializable<int> {
const TransactionPriority({required String title, required int raw}) : super(title: title, raw: raw);
abstract class TransactionPriority extends EnumerableItem<int> with Serializable<int> {
const TransactionPriority({required super.title, required super.raw});
String get units => '';
String toString() {
return title;
}
}
abstract class TransactionPriorities {
const TransactionPriorities();
int operator [](TransactionPriority type);
String labelWithRate(TransactionPriority type);
Map<String, int> toJson();
factory TransactionPriorities.fromJson(Map<String, int> json) {
throw UnimplementedError();
}
}

View file

@ -9,6 +9,7 @@ abstract class WalletCredentials {
this.password,
this.passphrase,
this.derivationInfo,
this.derivations,
this.hardwareWalletType,
this.parentAddress,
}) {
@ -25,5 +26,6 @@ abstract class WalletCredentials {
String? passphrase;
WalletInfo? walletInfo;
DerivationInfo? derivationInfo;
List<DerivationInfo>? derivations;
HardwareWalletType? hardwareWalletType;
}

View file

@ -79,6 +79,7 @@ class WalletInfo extends HiveObject {
this.yatLastUsedAddressRaw,
this.showIntroCakePayCard,
this.derivationInfo,
this.derivations,
this.hardwareWalletType,
this.parentAddress,
) : _yatLastUsedAddressController = StreamController<String>.broadcast();
@ -97,6 +98,7 @@ class WalletInfo extends HiveObject {
String yatEid = '',
String yatLastUsedAddressRaw = '',
DerivationInfo? derivationInfo,
List<DerivationInfo>? derivations,
HardwareWalletType? hardwareWalletType,
String? parentAddress,
}) {
@ -114,6 +116,7 @@ class WalletInfo extends HiveObject {
yatLastUsedAddressRaw,
showIntroCakePayCard,
derivationInfo,
derivations,
hardwareWalletType,
parentAddress,
);
@ -196,8 +199,8 @@ class WalletInfo extends HiveObject {
@HiveField(24)
List<String>? manualAddresses;
@HiveField(25)
List<DerivationInfo>? derivations;
String get yatLastUsedAddress => yatLastUsedAddressRaw ?? '';

View file

@ -27,7 +27,10 @@ mixin WalletKeysFile<BalanceType extends Balance, HistoryType extends Transactio
final path = "$rootPath${isBackup ? ".backup" : ""}";
dev.log("Saving .keys file '$path'");
await encryptionFileUtils.write(
path: path, password: password, data: walletKeysData.toJSON());
path: path,
password: password,
data: walletKeysData.toJSON(),
);
} catch (_) {}
}

View file

@ -23,6 +23,8 @@ abstract class HavenWalletAddressesBase extends WalletAddressesWithAccount<Accou
@observable
String address;
String get primaryAddress => address;
// @override
@observable
Account? account;

View file

@ -0,0 +1 @@
/home/rafael/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/

View file

@ -17,12 +17,12 @@ dependencies:
path: ../cw_evm
on_chain:
git:
url: https://github.com/cake-tech/On_chain
ref: cake-update-v2
url: https://github.com/cake-tech/on_chain.git
ref: cake-update-v3
blockchain_utils:
git:
url: https://github.com/cake-tech/blockchain_utils
ref: cake-update-v2
ref: cake-update-v3
mobx: ^2.3.0+1
bip39: ^1.0.6
hive: ^2.2.3
@ -34,6 +34,13 @@ dev_dependencies:
build_runner: ^2.3.3
mobx_codegen: ^2.1.1
hive_generator: ^1.1.3
dependency_overrides:
blockchain_utils:
git:
url: https://github.com/cake-tech/blockchain_utils
ref: cake-update-v3
flutter:
# assets:
# - images/a_dot_burr.jpeg

View file

@ -5,17 +5,15 @@ class CWBitcoin extends Bitcoin {
required String name,
required String mnemonic,
required String password,
required DerivationType derivationType,
required String derivationPath,
required List<DerivationInfo>? derivations,
String? passphrase,
}) =>
BitcoinRestoreWalletFromSeedCredentials(
name: name,
mnemonic: mnemonic,
password: password,
derivationType: derivationType,
derivationPath: derivationPath,
passphrase: passphrase,
derivations: derivations,
);
@override
@ -54,7 +52,7 @@ class CWBitcoin extends Bitcoin {
name: name, hwAccountData: accountData, walletInfo: walletInfo);
@override
TransactionPriority getMediumTransactionPriority() => BitcoinTransactionPriority.medium;
TransactionPriority getMediumTransactionPriority() => ElectrumTransactionPriority.medium;
@override
List<String> getWordList() => wordlist;
@ -72,18 +70,18 @@ class CWBitcoin extends Bitcoin {
}
@override
List<TransactionPriority> getTransactionPriorities() => BitcoinTransactionPriority.all;
List<TransactionPriority> getTransactionPriorities() => ElectrumTransactionPriority.all;
@override
List<TransactionPriority> getLitecoinTransactionPriorities() => LitecoinTransactionPriority.all;
List<TransactionPriority> getLitecoinTransactionPriorities() => ElectrumTransactionPriority.all;
@override
TransactionPriority deserializeBitcoinTransactionPriority(int raw) =>
BitcoinTransactionPriority.deserialize(raw: raw);
ElectrumTransactionPriority.deserialize(raw: raw);
@override
TransactionPriority deserializeLitecoinTransactionPriority(int raw) =>
LitecoinTransactionPriority.deserialize(raw: raw);
ElectrumTransactionPriority.deserialize(raw: raw);
@override
int getFeeRate(Object wallet, TransactionPriority priority) {
@ -113,7 +111,7 @@ class CWBitcoin extends Bitcoin {
UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any,
}) {
final bitcoinFeeRate =
priority == BitcoinTransactionPriority.custom && feeRate != null ? feeRate : null;
priority == ElectrumTransactionPriority.custom && feeRate != null ? feeRate : null;
return BitcoinTransactionCredentials(
outputs
.map((out) => OutputInfo(
@ -127,7 +125,7 @@ class CWBitcoin extends Bitcoin {
formattedCryptoAmount: out.formattedCryptoAmount,
memo: out.memo))
.toList(),
priority: priority as BitcoinTransactionPriority,
priority: priority as ElectrumTransactionPriority,
feeRate: bitcoinFeeRate,
coinTypeToSpendFrom: coinTypeToSpendFrom,
);
@ -144,7 +142,7 @@ class CWBitcoin extends Bitcoin {
address: addr.address,
txCount: addr.txCount,
balance: addr.balance,
isChange: addr.isHidden))
isChange: addr.isChange))
.toList();
}
@ -167,12 +165,7 @@ class CWBitcoin extends Bitcoin {
final p2shAddr = sk.getPublic().toP2pkhInP2sh();
final estimatedTx = await electrumWallet.estimateSendAllTx(
[BitcoinOutput(address: p2shAddr, value: BigInt.zero)],
getFeeRate(
wallet,
wallet.type == WalletType.litecoin
? priority as LitecoinTransactionPriority
: priority as BitcoinTransactionPriority,
),
getFeeRate(wallet, priority),
);
return estimatedTx.amount;
@ -189,19 +182,20 @@ class CWBitcoin extends Bitcoin {
@override
String formatterBitcoinAmountToString({required int amount}) =>
bitcoinAmountToString(amount: amount);
BitcoinAmountUtils.bitcoinAmountToString(amount: amount);
@override
double formatterBitcoinAmountToDouble({required int amount}) =>
bitcoinAmountToDouble(amount: amount);
BitcoinAmountUtils.bitcoinAmountToDouble(amount: amount);
@override
int formatterStringDoubleToBitcoinAmount(String amount) => stringDoubleToBitcoinAmount(amount);
int formatterStringDoubleToBitcoinAmount(String amount) =>
BitcoinAmountUtils.stringDoubleToBitcoinAmount(amount);
@override
String bitcoinTransactionPriorityWithLabel(TransactionPriority priority, int rate,
{int? customRate}) =>
(priority as BitcoinTransactionPriority).labelWithRate(rate, customRate);
(priority as ElectrumTransactionPriority).labelWithRate(rate, customRate);
@override
List<BitcoinUnspent> getUnspents(Object wallet,
@ -224,30 +218,52 @@ class CWBitcoin extends Bitcoin {
await bitcoinWallet.updateAllUnspents();
}
WalletService createBitcoinWalletService(Box<WalletInfo> walletInfoSource,
Box<UnspentCoinsInfo> unspentCoinSource, bool alwaysScan, bool isDirect) {
return BitcoinWalletService(walletInfoSource, unspentCoinSource, alwaysScan, isDirect);
WalletService createBitcoinWalletService(
Box<WalletInfo> walletInfoSource,
Box<UnspentCoinsInfo> unspentCoinSource,
bool alwaysScan,
bool isDirect,
bool mempoolAPIEnabled,
) {
return BitcoinWalletService(
walletInfoSource,
unspentCoinSource,
alwaysScan,
isDirect,
mempoolAPIEnabled,
);
}
WalletService createLitecoinWalletService(Box<WalletInfo> walletInfoSource,
Box<UnspentCoinsInfo> unspentCoinSource, bool alwaysScan, bool isDirect) {
return LitecoinWalletService(walletInfoSource, unspentCoinSource, alwaysScan, isDirect);
WalletService createLitecoinWalletService(
Box<WalletInfo> walletInfoSource,
Box<UnspentCoinsInfo> unspentCoinSource,
bool alwaysScan,
bool isDirect,
bool mempoolAPIEnabled,
) {
return LitecoinWalletService(
walletInfoSource,
unspentCoinSource,
alwaysScan,
isDirect,
mempoolAPIEnabled,
);
}
@override
TransactionPriority getBitcoinTransactionPriorityMedium() => BitcoinTransactionPriority.medium;
TransactionPriority getBitcoinTransactionPriorityMedium() => ElectrumTransactionPriority.fast;
@override
TransactionPriority getBitcoinTransactionPriorityCustom() => BitcoinTransactionPriority.custom;
TransactionPriority getBitcoinTransactionPriorityCustom() => ElectrumTransactionPriority.custom;
@override
TransactionPriority getLitecoinTransactionPriorityMedium() => LitecoinTransactionPriority.medium;
TransactionPriority getLitecoinTransactionPriorityMedium() => ElectrumTransactionPriority.medium;
@override
TransactionPriority getBitcoinTransactionPrioritySlow() => BitcoinTransactionPriority.slow;
TransactionPriority getBitcoinTransactionPrioritySlow() => ElectrumTransactionPriority.medium;
@override
TransactionPriority getLitecoinTransactionPrioritySlow() => LitecoinTransactionPriority.slow;
TransactionPriority getLitecoinTransactionPrioritySlow() => ElectrumTransactionPriority.slow;
@override
Future<void> setAddressType(Object wallet, dynamic option) async {
@ -320,20 +336,12 @@ class CWBitcoin extends Bitcoin {
}
@override
Future<List<DerivationInfo>> getDerivationsFromMnemonic({
Future<List<BitcoinDerivationInfo>> getDerivationsFromMnemonic({
required String mnemonic,
required Node node,
String? passphrase,
}) async {
List<DerivationInfo> list = [];
List<DerivationType> types = await compareDerivationMethods(mnemonic: mnemonic, node: node);
if (types.length == 1 && types.first == DerivationType.electrum) {
return [getElectrumDerivations()[DerivationType.electrum]!.first];
}
final electrumClient = ElectrumClient();
await electrumClient.connectToUri(node.uri, useSSL: node.useSSL);
List<BitcoinDerivationInfo> list = [];
late BasedUtxoNetwork network;
switch (node.type) {
@ -346,72 +354,34 @@ class CWBitcoin extends Bitcoin {
break;
}
for (DerivationType dType in electrum_derivations.keys) {
late Uint8List seedBytes;
if (dType == DerivationType.electrum) {
seedBytes = await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? "");
} else if (dType == DerivationType.bip39) {
seedBytes = bip39.mnemonicToSeed(mnemonic, passphrase: passphrase ?? '');
}
for (DerivationInfo dInfo in electrum_derivations[dType]!) {
var electrumSeedBytes;
try {
DerivationInfo dInfoCopy = DerivationInfo(
derivationType: dInfo.derivationType,
derivationPath: dInfo.derivationPath,
description: dInfo.description,
scriptType: dInfo.scriptType,
);
electrumSeedBytes = ElectrumV2SeedGenerator.generateFromString(mnemonic, passphrase);
} catch (e) {
print("electrum_v2 seed error: $e");
String balancePath = dInfoCopy.derivationPath!;
int derivationDepth = _countCharOccurrences(balancePath, '/');
// for BIP44
if (derivationDepth == 3 || derivationDepth == 1) {
// we add "/0" so that we generate account 0
balancePath += "/0";
}
final hd = Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath(balancePath)
as Bip32Slip10Secp256k1;
// derive address at index 0:
String? address;
switch (dInfoCopy.scriptType) {
case "p2wpkh":
address = generateP2WPKHAddress(hd: hd, network: network, index: 0);
break;
case "p2pkh":
address = generateP2PKHAddress(hd: hd, network: network, index: 0);
break;
case "p2wpkh-p2sh":
address = generateP2SHAddress(hd: hd, network: network, index: 0);
break;
case "p2tr":
address = generateP2TRAddress(hd: hd, network: network, index: 0);
break;
default:
continue;
}
final sh = BitcoinAddressUtils.scriptHash(address, network: network);
final history = await electrumClient.getHistory(sh);
final balance = await electrumClient.getBalance(sh);
dInfoCopy.balance = balance.entries.firstOrNull?.value.toString() ?? "0";
dInfoCopy.address = address;
dInfoCopy.transactionsCount = history.length;
list.add(dInfoCopy);
} catch (e, s) {
print("derivationInfoError: $e");
print("derivationInfoStack: $s");
if (passphrase != null && passphrase.isEmpty) {
try {
// TODO: language pick
electrumSeedBytes = ElectrumV1SeedGenerator(mnemonic).generate();
} catch (e) {
print("electrum_v1 seed error: $e");
}
}
}
// sort the list such that derivations with the most transactions are first:
list.sort((a, b) => b.transactionsCount.compareTo(a.transactionsCount));
if (electrumSeedBytes != null) {
list.add(BitcoinDerivationInfos.ELECTRUM);
}
var bip39SeedBytes;
try {
bip39SeedBytes = Bip39SeedGenerator.generateFromString(mnemonic, passphrase);
} catch (_) {}
if (bip39SeedBytes != null) {
list.add(BitcoinDerivationInfos.BIP84);
}
return list;
}
@ -443,7 +413,7 @@ class CWBitcoin extends Bitcoin {
@override
int getTransactionVSize(Object wallet, String transactionHex) {
final bitcoinWallet = wallet as ElectrumWallet;
return bitcoinWallet.transactionVSize(transactionHex);
return BtcTransaction.fromRaw(transactionHex).getVSize();
}
@override
@ -458,7 +428,7 @@ class CWBitcoin extends Bitcoin {
{int? size}) {
final bitcoinWallet = wallet as ElectrumWallet;
return bitcoinWallet.feeAmountForPriority(
priority as BitcoinTransactionPriority, inputsCount, outputsCount);
priority as ElectrumTransactionPriority, inputsCount, outputsCount);
}
@override
@ -482,8 +452,13 @@ class CWBitcoin extends Bitcoin {
@override
int getMaxCustomFeeRate(Object wallet) {
final bitcoinWallet = wallet as ElectrumWallet;
return (bitcoinWallet.feeRate(BitcoinTransactionPriority.fast) * 10).round();
final electrumWallet = wallet as ElectrumWallet;
final feeRates = electrumWallet.feeRates;
final maxFee = electrumWallet.feeRates is ElectrumTransactionPriorities
? ElectrumTransactionPriority.fast
: BitcoinTransactionPriority.priority;
return (electrumWallet.feeRate(maxFee) * 10).round();
}
@override
@ -526,7 +501,7 @@ class CWBitcoin extends Bitcoin {
address: addr.address,
txCount: addr.txCount,
balance: addr.balance,
isChange: addr.isHidden))
isChange: addr.isChange))
.toList();
}
@ -541,7 +516,7 @@ class CWBitcoin extends Bitcoin {
address: addr.address,
txCount: addr.txCount,
balance: addr.balance,
isChange: addr.isHidden))
isChange: addr.isChange))
.toList();
}
@ -564,7 +539,7 @@ class CWBitcoin extends Bitcoin {
@override
Future<void> setScanningActive(Object wallet, bool active) async {
final bitcoinWallet = wallet as ElectrumWallet;
final bitcoinWallet = wallet as BitcoinWallet;
bitcoinWallet.setSilentPaymentsScanning(active);
}
@ -574,10 +549,16 @@ class CWBitcoin extends Bitcoin {
return bitcoinWallet.isTestnet;
}
@override
Future<void> registerSilentPaymentsKey(Object wallet, bool active) async {
final bitcoinWallet = wallet as BitcoinWallet;
return await bitcoinWallet.registerSilentPaymentsKey();
}
@override
Future<bool> checkIfMempoolAPIIsEnabled(Object wallet) async {
final bitcoinWallet = wallet as ElectrumWallet;
return await bitcoinWallet.checkIfMempoolAPIIsEnabled();
return await bitcoinWallet.mempoolAPIEnabled;
}
@override
@ -595,13 +576,13 @@ class CWBitcoin extends Bitcoin {
@override
Future<void> rescan(Object wallet, {required int height, bool? doSingleScan}) async {
final bitcoinWallet = wallet as ElectrumWallet;
final bitcoinWallet = wallet as BitcoinWallet;
bitcoinWallet.rescan(height: height, doSingleScan: doSingleScan);
}
@override
Future<bool> getNodeIsElectrsSPEnabled(Object wallet) async {
final bitcoinWallet = wallet as ElectrumWallet;
final bitcoinWallet = wallet as BitcoinWallet;
return bitcoinWallet.getNodeSupportsSilentPayments();
}

View file

@ -6,8 +6,17 @@ class CWBitcoinCash extends BitcoinCash {
@override
WalletService createBitcoinCashWalletService(
Box<WalletInfo> walletInfoSource, Box<UnspentCoinsInfo> unspentCoinSource, bool isDirect) {
return BitcoinCashWalletService(walletInfoSource, unspentCoinSource, isDirect);
Box<WalletInfo> walletInfoSource,
Box<UnspentCoinsInfo> unspentCoinSource,
bool isDirect,
bool mempoolAPIEnabled,
) {
return BitcoinCashWalletService(
walletInfoSource,
unspentCoinSource,
isDirect,
mempoolAPIEnabled,
);
}
@override
@ -30,21 +39,23 @@ class CWBitcoinCash extends BitcoinCash {
@override
WalletCredentials createBitcoinCashRestoreWalletFromSeedCredentials(
{required String name, required String mnemonic, required String password, String? passphrase}) =>
{required String name,
required String mnemonic,
required String password,
String? passphrase}) =>
BitcoinCashRestoreWalletFromSeedCredentials(
name: name, mnemonic: mnemonic, password: password, passphrase: passphrase);
@override
TransactionPriority deserializeBitcoinCashTransactionPriority(int raw) =>
BitcoinCashTransactionPriority.deserialize(raw: raw);
ElectrumTransactionPriority.deserialize(raw: raw);
@override
TransactionPriority getDefaultTransactionPriority() => BitcoinCashTransactionPriority.medium;
TransactionPriority getDefaultTransactionPriority() => ElectrumTransactionPriority.medium;
@override
List<TransactionPriority> getTransactionPriorities() => BitcoinCashTransactionPriority.all;
List<TransactionPriority> getTransactionPriorities() => ElectrumTransactionPriority.all;
@override
TransactionPriority getBitcoinCashTransactionPrioritySlow() =>
BitcoinCashTransactionPriority.slow;
TransactionPriority getBitcoinCashTransactionPrioritySlow() => ElectrumTransactionPriority.slow;
}

View file

@ -51,7 +51,7 @@ String syncStatusTitle(SyncStatus syncStatus) {
return S.current.sync_status_timed_out;
}
if (syncStatus is SyncronizingSyncStatus) {
if (syncStatus is SynchronizingSyncStatus) {
return S.current.sync_status_syncronizing;
}

View file

@ -267,6 +267,8 @@ abstract class Web3WalletServiceBase with Store {
final keyForWallet = getKeyForStoringTopicsForWallet();
if (keyForWallet.isEmpty) return;
final currentTopicsForWallet = getPairingTopicsForWallet(keyForWallet);
final filteredPairings =
@ -360,6 +362,10 @@ abstract class Web3WalletServiceBase with Store {
String getKeyForStoringTopicsForWallet() {
List<ChainKeyModel> chainKeys = walletKeyService.getKeysForChain(appStore.wallet!);
if (chainKeys.isEmpty) {
return '';
}
final keyForPairingTopic =
PreferencesKey.walletConnectPairingTopicsListForWallet(chainKeys.first.publicKey);
@ -386,6 +392,8 @@ abstract class Web3WalletServiceBase with Store {
// Get key specific to the current wallet
final key = getKeyForStoringTopicsForWallet();
if (key.isEmpty) return;
// Get all pairing topics attached to this key
final pairingTopicsForWallet = getPairingTopicsForWallet(key);

View file

@ -373,8 +373,8 @@ Future<void> setup({
_walletInfoSource,
getIt.get<AdvancedPrivacySettingsViewModel>(param1: newWalletArgs.type),
getIt.get<SeedSettingsViewModel>(),
newWalletArguments: newWalletArgs,));
newWalletArguments: newWalletArgs,
));
getIt.registerFactory<NewWalletTypeViewModel>(() => NewWalletTypeViewModel(_walletInfoSource));
@ -397,60 +397,50 @@ Future<void> setup({
);
getIt.registerFactoryParam<WalletUnlockPage, WalletUnlockArguments, bool>((args, closable) {
return WalletUnlockPage(
getIt.get<WalletUnlockLoadableViewModel>(param1: args),
args.callback,
return WalletUnlockPage(getIt.get<WalletUnlockLoadableViewModel>(param1: args), args.callback,
args.authPasswordHandler,
closable: closable);
}, instanceName: 'wallet_unlock_loadable');
getIt.registerFactory<WalletUnlockPage>(
() => getIt.get<WalletUnlockPage>(
param1: WalletUnlockArguments(
callback: (bool successful, _) {
param1: WalletUnlockArguments(callback: (bool successful, _) {
if (successful) {
final authStore = getIt.get<AuthenticationStore>();
authStore.allowed();
}}),
}
}),
param2: false,
instanceName: 'wallet_unlock_loadable'),
instanceName: 'wallet_password_login');
getIt.registerFactoryParam<WalletUnlockPage, WalletUnlockArguments, bool>((args, closable) {
return WalletUnlockPage(
getIt.get<WalletUnlockVerifiableViewModel>(param1: args),
args.callback,
return WalletUnlockPage(getIt.get<WalletUnlockVerifiableViewModel>(param1: args), args.callback,
args.authPasswordHandler,
closable: closable);
}, instanceName: 'wallet_unlock_verifiable');
getIt.registerFactoryParam<WalletUnlockLoadableViewModel, WalletUnlockArguments, void>((args, _) {
final currentWalletName = getIt
.get<SharedPreferences>()
.getString(PreferencesKey.currentWalletName) ?? '';
final currentWalletName =
getIt.get<SharedPreferences>().getString(PreferencesKey.currentWalletName) ?? '';
final currentWalletTypeRaw =
getIt.get<SharedPreferences>()
.getInt(PreferencesKey.currentWalletType) ?? 0;
getIt.get<SharedPreferences>().getInt(PreferencesKey.currentWalletType) ?? 0;
final currentWalletType = deserializeFromInt(currentWalletTypeRaw);
return WalletUnlockLoadableViewModel(
getIt.get<AppStore>(),
getIt.get<WalletLoadingService>(),
return WalletUnlockLoadableViewModel(getIt.get<AppStore>(), getIt.get<WalletLoadingService>(),
walletName: args.walletName ?? currentWalletName,
walletType: args.walletType ?? currentWalletType);
});
getIt.registerFactoryParam<WalletUnlockVerifiableViewModel, WalletUnlockArguments, void>((args, _) {
final currentWalletName = getIt
.get<SharedPreferences>()
.getString(PreferencesKey.currentWalletName) ?? '';
getIt.registerFactoryParam<WalletUnlockVerifiableViewModel, WalletUnlockArguments, void>(
(args, _) {
final currentWalletName =
getIt.get<SharedPreferences>().getString(PreferencesKey.currentWalletName) ?? '';
final currentWalletTypeRaw =
getIt.get<SharedPreferences>()
.getInt(PreferencesKey.currentWalletType) ?? 0;
getIt.get<SharedPreferences>().getInt(PreferencesKey.currentWalletType) ?? 0;
final currentWalletType = deserializeFromInt(currentWalletTypeRaw);
return WalletUnlockVerifiableViewModel(
getIt.get<AppStore>(),
return WalletUnlockVerifiableViewModel(getIt.get<AppStore>(),
walletName: args.walletName ?? currentWalletName,
walletType: args.walletType ?? currentWalletType);
});
@ -785,7 +775,6 @@ Future<void> setup({
);
getIt.registerFactoryParam<WalletEditPage, WalletEditPageArguments, void>((arguments, _) {
return WalletEditPage(
pageArguments: WalletEditPageArguments(
walletEditViewModel: getIt.get<WalletEditViewModel>(param1: arguments.walletListViewModel),
@ -884,8 +873,9 @@ Future<void> setup({
getIt.registerFactory(() => TrocadorProvidersViewModel(getIt.get<SettingsStore>()));
getIt.registerFactory(() {
return OtherSettingsViewModel(getIt.get<SettingsStore>(), getIt.get<AppStore>().wallet!,
getIt.get<SendViewModel>());});
return OtherSettingsViewModel(
getIt.get<SettingsStore>(), getIt.get<AppStore>().wallet!, getIt.get<SendViewModel>());
});
getIt.registerFactory(() {
return SecuritySettingsViewModel(getIt.get<SettingsStore>());
@ -893,7 +883,8 @@ Future<void> setup({
getIt.registerFactory(() => WalletSeedViewModel(getIt.get<AppStore>().wallet!));
getIt.registerFactory<SeedSettingsViewModel>(() => SeedSettingsViewModel(getIt.get<AppStore>(), getIt.get<SeedSettingsStore>()));
getIt.registerFactory<SeedSettingsViewModel>(
() => SeedSettingsViewModel(getIt.get<AppStore>(), getIt.get<SeedSettingsStore>()));
getIt.registerFactoryParam<WalletSeedPage, bool, void>((bool isWalletCreated, _) =>
WalletSeedPage(getIt.get<WalletSeedViewModel>(), isNewWalletCreated: isWalletCreated));
@ -1037,6 +1028,7 @@ Future<void> setup({
_unspentCoinsInfoSource,
getIt.get<SettingsStore>().silentPaymentsAlwaysScan,
SettingsStoreBase.walletPasswordDirectInput,
getIt.get<SettingsStore>().useMempoolFeeAPI,
);
case WalletType.litecoin:
return bitcoin!.createLitecoinWalletService(
@ -1044,16 +1036,22 @@ Future<void> setup({
_unspentCoinsInfoSource,
getIt.get<SettingsStore>().mwebAlwaysScan,
SettingsStoreBase.walletPasswordDirectInput,
getIt.get<SettingsStore>().useMempoolFeeAPI,
);
case WalletType.ethereum:
return ethereum!.createEthereumWalletService(
_walletInfoSource, SettingsStoreBase.walletPasswordDirectInput);
case WalletType.bitcoinCash:
return bitcoinCash!.createBitcoinCashWalletService(_walletInfoSource,
_unspentCoinsInfoSource, SettingsStoreBase.walletPasswordDirectInput);
return bitcoinCash!.createBitcoinCashWalletService(
_walletInfoSource,
_unspentCoinsInfoSource,
SettingsStoreBase.walletPasswordDirectInput,
getIt.get<SettingsStore>().useMempoolFeeAPI,
);
case WalletType.nano:
case WalletType.banano:
return nano!.createNanoWalletService(_walletInfoSource, SettingsStoreBase.walletPasswordDirectInput);
return nano!.createNanoWalletService(
_walletInfoSource, SettingsStoreBase.walletPasswordDirectInput);
case WalletType.polygon:
return polygon!.createPolygonWalletService(
_walletInfoSource, SettingsStoreBase.walletPasswordDirectInput);
@ -1061,7 +1059,8 @@ Future<void> setup({
return solana!.createSolanaWalletService(
_walletInfoSource, SettingsStoreBase.walletPasswordDirectInput);
case WalletType.tron:
return tron!.createTronWalletService(_walletInfoSource, SettingsStoreBase.walletPasswordDirectInput);
return tron!.createTronWalletService(
_walletInfoSource, SettingsStoreBase.walletPasswordDirectInput);
case WalletType.wownero:
return wownero!.createWowneroWalletService(_walletInfoSource, _unspentCoinsInfoSource);
case WalletType.none:
@ -1100,8 +1099,7 @@ Future<void> setup({
param1: derivations,
)));
getIt.registerFactoryParam<TransactionDetailsViewModel, List<dynamic>, void>(
(params, _) {
getIt.registerFactoryParam<TransactionDetailsViewModel, List<dynamic>, void>((params, _) {
final transactionInfo = params[0] as TransactionInfo;
final canReplaceByFee = params[1] as bool? ?? false;
final wallet = getIt.get<AppStore>().wallet!;
@ -1114,16 +1112,14 @@ Future<void> setup({
sendViewModel: getIt.get<SendViewModel>(),
canReplaceByFee: canReplaceByFee,
);
}
);
});
getIt.registerFactoryParam<TransactionDetailsPage, TransactionInfo, void>(
(TransactionInfo transactionInfo, _) => TransactionDetailsPage(
transactionDetailsViewModel: getIt.get<TransactionDetailsViewModel>(
param1: [transactionInfo, false])));
transactionDetailsViewModel:
getIt.get<TransactionDetailsViewModel>(param1: [transactionInfo, false])));
getIt.registerFactoryParam<RBFDetailsPage, List<dynamic>, void>(
(params, _) {
getIt.registerFactoryParam<RBFDetailsPage, List<dynamic>, void>((params, _) {
final transactionInfo = params[0] as TransactionInfo;
final txHex = params[1] as String;
return RBFDetailsPage(
@ -1132,8 +1128,7 @@ Future<void> setup({
),
rawTransaction: txHex,
);
}
);
});
getIt.registerFactoryParam<NewWalletTypePage, NewWalletTypeArguments, void>(
(newWalletTypeArguments, _) {
@ -1155,8 +1150,7 @@ Future<void> setup({
getIt.registerFactory(() => CakeFeaturesViewModel(getIt.get<CakePayService>()));
getIt.registerFactory(() => BackupService(getIt.get<SecureStorage>(), _walletInfoSource,
_transactionDescriptionBox,
getIt.get<KeyService>(), getIt.get<SharedPreferences>()));
_transactionDescriptionBox, getIt.get<KeyService>(), getIt.get<SharedPreferences>()));
getIt.registerFactory(() => BackupViewModel(
getIt.get<SecureStorage>(), getIt.get<SecretStore>(), getIt.get<BackupService>()));

View file

@ -114,7 +114,7 @@ class FiatCurrency extends EnumerableItem<String> with Serializable<String> impl
FiatCurrency.tur.raw: FiatCurrency.tur,
};
static FiatCurrency deserialize({required String raw}) => _all[raw]!;
static FiatCurrency deserialize({required String raw}) => _all[raw] ?? FiatCurrency.usd;
@override
bool operator ==(Object other) => other is FiatCurrency && other.raw == raw;

View file

@ -48,6 +48,7 @@ class PreferencesKey {
static const customBitcoinFeeRate = 'custom_electrum_fee_rate';
static const silentPaymentsCardDisplay = 'silentPaymentsCardDisplay';
static const silentPaymentsAlwaysScan = 'silentPaymentsAlwaysScan';
static const silentPaymentsKeyRegistered = 'silentPaymentsKeyRegistered';
static const mwebCardDisplay = 'mwebCardDisplay';
static const mwebEnabled = 'mwebEnabled';
static const hasEnabledMwebBefore = 'hasEnabledMwebBefore';

View file

@ -16,7 +16,6 @@ import 'package:cake_wallet/exchange/exchange_template.dart';
import 'package:cake_wallet/exchange/trade.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/locales/locale.dart';
import 'package:cake_wallet/monero/monero.dart';
import 'package:cake_wallet/reactions/bootstrap.dart';
import 'package:cake_wallet/router.dart' as Router;
import 'package:cake_wallet/routes.dart';

View file

@ -10,7 +10,6 @@ import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/dropdown_item_
import 'package:cake_wallet/src/screens/wallet_unlock/wallet_unlock_arguments.dart';
import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
import 'package:cake_wallet/store/settings_store.dart';
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
import 'package:cake_wallet/themes/extensions/menu_theme.dart';
import 'package:cake_wallet/utils/show_bar.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
@ -100,6 +99,11 @@ class _DesktopWalletSelectionDropDownState extends State<DesktopWalletSelectionD
),
];
final selectedItem = dropDownItems.firstWhere(
(element) => element.isSelected,
orElse: () => dropDownItems.first,
);
return DropdownButton<DesktopDropdownItem>(
items: dropDownItems
.map(
@ -115,7 +119,7 @@ class _DesktopWalletSelectionDropDownState extends State<DesktopWalletSelectionD
dropdownColor: themeData.extension<CakeMenuTheme>()!.backgroundColor,
style: TextStyle(color: themeData.extension<CakeTextTheme>()!.titleColor),
selectedItemBuilder: (context) => dropDownItems.map((item) => item.child).toList(),
value: dropDownItems.firstWhere((element) => element.isSelected),
value: selectedItem,
underline: const SizedBox(),
focusColor: Colors.transparent,
borderRadius: BorderRadius.circular(15.0),

View file

@ -72,7 +72,10 @@ class AddressPage extends BasePage {
bool isMobileView = responsiveLayoutUtil.shouldRenderMobileUI;
return MergeSemantics(
return Row(
mainAxisSize: MainAxisSize.min,
children: [
MergeSemantics(
child: SizedBox(
height: isMobileView ? 37 : 45,
width: isMobileView ? 37 : 45,
@ -90,6 +93,31 @@ class AddressPage extends BasePage {
),
),
),
),
MergeSemantics(
child: SizedBox(
height: isMobileView ? 37 : 45,
width: isMobileView ? 37 : 45,
child: ButtonTheme(
minWidth: double.minPositive,
child: Semantics(
label: !isMobileView ? S.of(context).close : S.of(context).seed_alert_back,
child: TextButton(
style: ButtonStyle(
overlayColor: MaterialStateColor.resolveWith((states) => Colors.transparent),
),
onPressed: () => onClose(context),
child: Icon(
Icons.more_vert,
color: titleColor(context),
size: 16,
),
),
),
),
),
),
],
);
}

View file

@ -1,27 +1,13 @@
import 'package:cake_wallet/src/screens/nano_accounts/nano_account_list_page.dart';
import 'package:cake_wallet/src/screens/receive/widgets/address_list.dart';
import 'package:cake_wallet/src/widgets/keyboard_done_button.dart';
import 'package:cake_wallet/themes/extensions/balance_page_theme.dart';
import 'package:cake_wallet/themes/extensions/keyboard_theme.dart';
import 'package:cake_wallet/themes/extensions/receive_page_theme.dart';
import 'package:cake_wallet/src/widgets/gradient_background.dart';
import 'package:cake_wallet/src/widgets/section_divider.dart';
import 'package:cake_wallet/themes/theme_base.dart';
import 'package:cake_wallet/utils/share_util.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/screens/monero_accounts/monero_account_list_page.dart';
import 'package:cake_wallet/src/screens/receive/widgets/header_tile.dart';
import 'package:cake_wallet/src/screens/receive/widgets/address_cell.dart';
import 'package:cake_wallet/view_model/wallet_address_list/wallet_account_list_header.dart';
import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_header.dart';
import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart';
import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart';
import 'package:cake_wallet/src/screens/receive/widgets/qr_widget.dart';
import 'package:keyboard_actions/keyboard_actions.dart';

View file

@ -1,6 +1,3 @@
import 'dart:math';
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/routes.dart';
@ -37,7 +34,6 @@ class AddressList extends StatefulWidget {
}
class _AddressListState extends State<AddressList> {
bool showHiddenAddresses = false;
void _toggleHiddenAddresses() {
@ -131,7 +127,8 @@ class _AddressListState extends State<AddressList> {
showTrailingButton: widget.addressListViewModel.showAddManualAddresses,
showSearchButton: true,
onSearchCallback: updateItems,
trailingButtonTap: () => Navigator.of(context).pushNamed(Routes.newSubaddress).then((value) {
trailingButtonTap: () =>
Navigator.of(context).pushNamed(Routes.newSubaddress).then((value) {
updateItems(); // refresh the new address
}),
trailingIcon: Icon(
@ -148,7 +145,8 @@ class _AddressListState extends State<AddressList> {
cell = Container();
} else {
cell = Observer(builder: (_) {
final isCurrent = item.address == widget.addressListViewModel.address.address && editable;
final isCurrent =
item.address == widget.addressListViewModel.address.address && editable;
final backgroundColor = isCurrent
? Theme.of(context).extension<ReceivePageTheme>()!.currentTileBackgroundColor
: Theme.of(context).extension<ReceivePageTheme>()!.tilesBackgroundColor;
@ -156,17 +154,17 @@ class _AddressListState extends State<AddressList> {
? Theme.of(context).extension<ReceivePageTheme>()!.currentTileTextColor
: Theme.of(context).extension<ReceivePageTheme>()!.tilesTextColor;
return AddressCell.fromItem(
item,
isCurrent: isCurrent,
hasBalance: widget.addressListViewModel.isBalanceAvailable,
hasReceived: widget.addressListViewModel.isReceivedAvailable,
// hasReceived:
backgroundColor: (kDebugMode && item.isHidden) ?
Theme.of(context).colorScheme.error :
(kDebugMode && item.isManual) ? Theme.of(context).colorScheme.error.withBlue(255) :
backgroundColor,
backgroundColor: (kDebugMode && item.isHidden)
? Theme.of(context).colorScheme.error
: (kDebugMode && item.isManual)
? Theme.of(context).colorScheme.error.withBlue(255)
: backgroundColor,
textColor: textColor,
onTap: (_) {
if (widget.onSelect != null) {
@ -176,7 +174,9 @@ class _AddressListState extends State<AddressList> {
widget.addressListViewModel.setAddress(item);
},
onEdit: editable
? () => Navigator.of(context).pushNamed(Routes.newSubaddress, arguments: item).then((value) {
? () => Navigator.of(context)
.pushNamed(Routes.newSubaddress, arguments: item)
.then((value) {
updateItems(); // refresh the new address
})
: null,
@ -190,8 +190,8 @@ class _AddressListState extends State<AddressList> {
return index != 0
? cell
: ClipRRect(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(30), topRight: Radius.circular(30)),
borderRadius:
BorderRadius.only(topLeft: Radius.circular(30), topRight: Radius.circular(30)),
child: cell,
);
},
@ -202,5 +202,4 @@ class _AddressListState extends State<AddressList> {
await widget.addressListViewModel.toggleHideAddress(item);
updateItems();
}
}

View file

@ -54,8 +54,10 @@ class WalletRestorePage extends BasePage {
_validateOnChange(isPolyseed: isPolyseed);
},
displayWalletPassword: walletRestoreViewModel.hasWalletPassword,
onPasswordChange: (String password) => walletRestoreViewModel.walletPassword = password,
onRepeatedPasswordChange: (String repeatedPassword) => walletRestoreViewModel.repeatedWalletPassword = repeatedPassword));
onPasswordChange: (String password) =>
walletRestoreViewModel.walletPassword = password,
onRepeatedPasswordChange: (String repeatedPassword) =>
walletRestoreViewModel.repeatedWalletPassword = repeatedPassword));
break;
case WalletRestoreMode.keys:
_pages.add(WalletRestoreFromKeysFrom(
@ -69,8 +71,10 @@ class WalletRestorePage extends BasePage {
},
displayPrivateKeyField: walletRestoreViewModel.hasRestoreFromPrivateKey,
displayWalletPassword: walletRestoreViewModel.hasWalletPassword,
onPasswordChange: (String password) => walletRestoreViewModel.walletPassword = password,
onRepeatedPasswordChange: (String repeatedPassword) => walletRestoreViewModel.repeatedWalletPassword = repeatedPassword,
onPasswordChange: (String password) =>
walletRestoreViewModel.walletPassword = password,
onRepeatedPasswordChange: (String repeatedPassword) =>
walletRestoreViewModel.repeatedWalletPassword = repeatedPassword,
onHeightOrDateEntered: (value) => walletRestoreViewModel.isButtonEnabled = value));
break;
default:
@ -105,6 +109,7 @@ class WalletRestorePage extends BasePage {
// DerivationType derivationType = DerivationType.unknown;
// String? derivationPath = null;
DerivationInfo? derivationInfo;
List<DerivationInfo>? derivations;
@override
Function(BuildContext)? get pushToNextWidget => (context) {
@ -342,6 +347,7 @@ class WalletRestorePage extends BasePage {
}
credentials['derivationInfo'] = this.derivationInfo;
credentials['derivations'] = this.derivations;
credentials['walletType'] = walletRestoreViewModel.type;
return credentials;
}
@ -379,12 +385,13 @@ class WalletRestorePage extends BasePage {
walletRestoreViewModel.state = IsExecutingState();
DerivationInfo? dInfo;
// get info about the different derivations:
List<DerivationInfo> derivations =
await walletRestoreViewModel.getDerivationInfo(_credentials());
if (walletRestoreViewModel.type == WalletType.nano) {
DerivationInfo? dInfo;
int derivationsWithHistory = 0;
int derivationWithHistoryIndex = 0;
for (int i = 0; i < derivations.length; i++) {
@ -411,6 +418,9 @@ class WalletRestorePage extends BasePage {
}
this.derivationInfo = dInfo;
} else {
this.derivations = derivations;
}
await walletRestoreViewModel.create(options: _credentials());
seedSettingsViewModel.setPassphrase(null);

View file

@ -37,6 +37,13 @@ class SilentPaymentsSettingsPage extends BasePage {
_silentPaymentsSettingsViewModel.setSilentPaymentsAlwaysScan(value);
},
),
SettingsSwitcherCell(
title: S.current.silent_payments_register_key,
value: _silentPaymentsSettingsViewModel.silentPaymentsKeyRegistered,
onValueChange: (_, bool value) {
_silentPaymentsSettingsViewModel.registerSilentPaymentsKey(value);
},
),
SettingsCellWithArrow(
title: S.current.silent_payments_scanning,
handler: (BuildContext context) => Navigator.of(context).pushNamed(Routes.rescan),

View file

@ -52,7 +52,9 @@ class BottomSheetListenerState extends State<BottomSheetListener> {
);
},
);
if (!item.completer.isCompleted) {
item.completer.complete(value);
}
widget.bottomSheetService.resetCurrentSheet();
}
}

View file

@ -25,6 +25,7 @@ class CakeImageWidget extends StatelessWidget {
imageUrl!,
height: height,
width: width,
errorBuilder: (_, __, ___) => Icon(Icons.error),
);
}
@ -33,6 +34,7 @@ class CakeImageWidget extends StatelessWidget {
imageUrl!,
height: height,
width: width,
placeholderBuilder: (_) => Icon(Icons.error),
);
}

View file

@ -46,6 +46,7 @@ class _ServicesUpdatesWidgetState extends State<ServicesUpdatesWidget> {
"assets/images/notification_icon.svg",
color: Theme.of(context).extension<DashboardPageTheme>()!.pageTitleTextColor,
width: 30,
placeholderBuilder: (_) => Icon(Icons.error),
),
);
}
@ -136,6 +137,7 @@ class _ServicesUpdatesWidgetState extends State<ServicesUpdatesWidget> {
"assets/images/notification_icon.svg",
color: Theme.of(context).extension<DashboardPageTheme>()!.pageTitleTextColor,
width: 30,
placeholderBuilder: (_) => Icon(Icons.error),
),
if (state.hasData && state.data!.hasUpdates && !wasOpened)
Container(

View file

@ -91,8 +91,9 @@ abstract class TransactionFilterStoreBase with Store {
(displayOutgoing && item.transaction.direction == TransactionDirection.outgoing) ||
(displayIncoming &&
item.transaction.direction == TransactionDirection.incoming &&
!bitcoin!.txIsReceivedSilentPayment(item.transaction)) ||
(displaySilentPayments && bitcoin!.txIsReceivedSilentPayment(item.transaction));
!(bitcoin?.txIsReceivedSilentPayment(item.transaction) ?? false)) ||
(displaySilentPayments &&
(bitcoin?.txIsReceivedSilentPayment(item.transaction) ?? false));
} else if (item is AnonpayTransactionListItem) {
allowed = displayIncoming;
}

View file

@ -114,6 +114,7 @@ abstract class SettingsStoreBase with Store {
required this.customBitcoinFeeRate,
required this.silentPaymentsCardDisplay,
required this.silentPaymentsAlwaysScan,
required this.silentPaymentsKeyRegistered,
required this.mwebAlwaysScan,
required this.mwebCardDisplay,
required this.mwebEnabled,
@ -344,8 +345,8 @@ abstract class SettingsStoreBase with Store {
reaction(
(_) => bitcoinSeedType,
(BitcoinSeedType bitcoinSeedType) => sharedPreferences.setInt(
PreferencesKey.bitcoinSeedType, bitcoinSeedType.raw));
(BitcoinSeedType bitcoinSeedType) =>
sharedPreferences.setInt(PreferencesKey.bitcoinSeedType, bitcoinSeedType.raw));
reaction(
(_) => nanoSeedType,
@ -428,8 +429,10 @@ abstract class SettingsStoreBase with Store {
reaction((_) => useTronGrid,
(bool useTronGrid) => _sharedPreferences.setBool(PreferencesKey.useTronGrid, useTronGrid));
reaction((_) => useMempoolFeeAPI,
(bool useMempoolFeeAPI) => _sharedPreferences.setBool(PreferencesKey.useMempoolFeeAPI, useMempoolFeeAPI));
reaction(
(_) => useMempoolFeeAPI,
(bool useMempoolFeeAPI) =>
_sharedPreferences.setBool(PreferencesKey.useMempoolFeeAPI, useMempoolFeeAPI));
reaction((_) => defaultNanoRep,
(String nanoRep) => _sharedPreferences.setString(PreferencesKey.defaultNanoRep, nanoRep));
@ -559,6 +562,11 @@ abstract class SettingsStoreBase with Store {
(bool silentPaymentsAlwaysScan) => _sharedPreferences.setBool(
PreferencesKey.silentPaymentsAlwaysScan, silentPaymentsAlwaysScan));
reaction(
(_) => silentPaymentsKeyRegistered,
(bool silentPaymentsKeyRegistered) => _sharedPreferences.setBool(
PreferencesKey.silentPaymentsKeyRegistered, silentPaymentsKeyRegistered));
reaction(
(_) => mwebAlwaysScan,
(bool mwebAlwaysScan) =>
@ -790,6 +798,9 @@ abstract class SettingsStoreBase with Store {
@observable
bool silentPaymentsAlwaysScan;
@observable
bool silentPaymentsKeyRegistered;
@observable
bool mwebAlwaysScan;
@ -959,6 +970,8 @@ abstract class SettingsStoreBase with Store {
sharedPreferences.getBool(PreferencesKey.silentPaymentsCardDisplay) ?? true;
final silentPaymentsAlwaysScan =
sharedPreferences.getBool(PreferencesKey.silentPaymentsAlwaysScan) ?? false;
final silentPaymentsKeyRegistered =
sharedPreferences.getBool(PreferencesKey.silentPaymentsKeyRegistered) ?? false;
final mwebAlwaysScan = sharedPreferences.getBool(PreferencesKey.mwebAlwaysScan) ?? false;
final mwebCardDisplay = sharedPreferences.getBool(PreferencesKey.mwebCardDisplay) ?? true;
final mwebEnabled = sharedPreferences.getBool(PreferencesKey.mwebEnabled) ?? false;
@ -1230,6 +1243,7 @@ abstract class SettingsStoreBase with Store {
customBitcoinFeeRate: customBitcoinFeeRate,
silentPaymentsCardDisplay: silentPaymentsCardDisplay,
silentPaymentsAlwaysScan: silentPaymentsAlwaysScan,
silentPaymentsKeyRegistered: silentPaymentsKeyRegistered,
mwebAlwaysScan: mwebAlwaysScan,
mwebCardDisplay: mwebCardDisplay,
mwebEnabled: mwebEnabled,
@ -1396,6 +1410,8 @@ abstract class SettingsStoreBase with Store {
sharedPreferences.getBool(PreferencesKey.silentPaymentsCardDisplay) ?? true;
silentPaymentsAlwaysScan =
sharedPreferences.getBool(PreferencesKey.silentPaymentsAlwaysScan) ?? false;
silentPaymentsKeyRegistered =
sharedPreferences.getBool(PreferencesKey.silentPaymentsKeyRegistered) ?? false;
mwebAlwaysScan = sharedPreferences.getBool(PreferencesKey.mwebAlwaysScan) ?? false;
mwebCardDisplay = sharedPreferences.getBool(PreferencesKey.mwebCardDisplay) ?? true;
mwebEnabled = sharedPreferences.getBool(PreferencesKey.mwebEnabled) ?? false;
@ -1658,7 +1674,8 @@ abstract class SettingsStoreBase with Store {
deviceName = windowsInfo.productName;
} catch (e) {
print(e);
print('likely digitalProductId is null wait till https://github.com/fluttercommunity/plus_plugins/pull/3188 is merged');
print(
'likely digitalProductId is null wait till https://github.com/fluttercommunity/plus_plugins/pull/3188 is merged');
deviceName = "Windows Device";
}
}

View file

@ -62,6 +62,14 @@ class ExceptionHandler {
await _addDeviceInfo(_file!);
// Check if a mail client is available
final bool canSend = await FlutterMailer.canSendMail();
if (Platform.isIOS && !canSend) {
debugPrint('Mail app is not available');
return;
}
final MailOptions mailOptions = MailOptions(
subject: 'Mobile App Issue',
recipients: ['support@cakewallet.com'],

View file

@ -53,8 +53,18 @@ class ImageUtil {
);
} else {
return isSvg
? SvgPicture.asset(imagePath, height: _height, width: _width)
: Image.asset(imagePath, height: _height, width: _width);
? SvgPicture.asset(
imagePath,
height: _height,
width: _width,
placeholderBuilder: (_) => Icon(Icons.error),
)
: Image.asset(
imagePath,
height: _height,
width: _width,
errorBuilder: (_, __, ___) => Icon(Icons.error),
);
}
}
}

View file

@ -90,6 +90,7 @@ abstract class DashboardViewModelBase with Store {
value: () => transactionFilterStore.displayOutgoing,
caption: S.current.outgoing,
onChanged: transactionFilterStore.toggleOutgoing),
if (appStore.wallet!.type == WalletType.bitcoin)
FilterItem(
value: () => transactionFilterStore.displaySilentPayments,
caption: S.current.silent_payments,
@ -435,7 +436,10 @@ abstract class DashboardViewModelBase with Store {
}
@computed
bool get hasMweb => wallet.type == WalletType.litecoin && (Platform.isIOS || Platform.isAndroid) && !wallet.isHardwareWallet;
bool get hasMweb =>
wallet.type == WalletType.litecoin &&
(Platform.isIOS || Platform.isAndroid) &&
!wallet.isHardwareWallet;
@computed
bool get showMwebCard => hasMweb && settingsStore.mwebCardDisplay && !mwebEnabled;

View file

@ -113,21 +113,12 @@ abstract class WalletRestorationFromQRVMBase extends WalletCreationVM with Store
);
case WalletType.bitcoin:
case WalletType.litecoin:
final derivationInfoList = await getDerivationInfoFromQRCredentials(restoreWallet);
DerivationInfo derivationInfo;
if (derivationInfoList.isEmpty) {
derivationInfo = getDefaultCreateDerivation()!;
} else {
derivationInfo = derivationInfoList.first;
}
return bitcoin!.createBitcoinRestoreWalletFromSeedCredentials(
name: name,
mnemonic: restoreWallet.mnemonicSeed ?? '',
password: password,
passphrase: restoreWallet.passphrase,
derivationType: derivationInfo.derivationType!,
derivationPath: derivationInfo.derivationPath!,
derivations: [],
);
case WalletType.bitcoinCash:
return bitcoinCash!.createBitcoinCashRestoreWalletFromSeedCredentials(
@ -144,8 +135,7 @@ abstract class WalletRestorationFromQRVMBase extends WalletCreationVM with Store
passphrase: restoreWallet.passphrase,
);
case WalletType.nano:
final derivationInfo =
(await getDerivationInfoFromQRCredentials(restoreWallet)).first;
final derivationInfo = (await getDerivationInfoFromQRCredentials(restoreWallet)).first;
return nano!.createNanoRestoreWalletFromSeedCredentials(
name: name,
mnemonic: restoreWallet.mnemonicSeed ?? '',
@ -190,8 +180,8 @@ abstract class WalletRestorationFromQRVMBase extends WalletCreationVM with Store
}
@override
Future<WalletBase> processFromRestoredWallet(WalletCredentials credentials,
RestoredWallet restoreWallet) async {
Future<WalletBase> processFromRestoredWallet(
WalletCredentials credentials, RestoredWallet restoreWallet) async {
try {
switch (restoreWallet.restoreMode) {
case WalletRestoreMode.keys:

View file

@ -400,7 +400,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
final updatedOutputs = bitcoin!.updateOutputs(pendingTransaction!, outputs);
if (outputs.length == updatedOutputs.length) {
outputs = ObservableList.of(updatedOutputs);
outputs.clear();
outputs.addAll(updatedOutputs);
}
}

View file

@ -20,6 +20,9 @@ abstract class SilentPaymentsSettingsViewModelBase with Store {
@computed
bool get silentPaymentsAlwaysScan => _settingsStore.silentPaymentsAlwaysScan;
@computed
bool get silentPaymentsKeyRegistered => _settingsStore.silentPaymentsKeyRegistered;
@action
void setSilentPaymentsCardDisplay(bool value) {
_settingsStore.silentPaymentsCardDisplay = value;
@ -30,4 +33,10 @@ abstract class SilentPaymentsSettingsViewModelBase with Store {
_settingsStore.silentPaymentsAlwaysScan = value;
if (value) bitcoin!.setScanningActive(_wallet, true);
}
@action
void registerSilentPaymentsKey(bool value) {
_settingsStore.silentPaymentsKeyRegistered = value;
bitcoin!.registerSilentPaymentsKey(_wallet, true);
}
}

View file

@ -83,18 +83,19 @@ abstract class TransactionDetailsViewModelBase with Store {
break;
}
final descriptionKey =
'${transactionInfo.txHash}_${wallet.walletAddresses.primaryAddress}';
final description = transactionDescriptionBox.values.firstWhere(
(val) => val.id == descriptionKey || val.id == transactionInfo.txHash,
orElse: () => TransactionDescription(id: descriptionKey));
if (showRecipientAddress && !isRecipientAddressShown) {
try {
final recipientAddress = transactionDescriptionBox.values
.firstWhere((val) => val.id == transactionInfo.txHash)
.recipientAddress;
final recipientAddress = description.recipientAddress;
if (recipientAddress?.isNotEmpty ?? false) {
items.add(StandartListItem(
title: S.current.transaction_details_recipient_address, value: recipientAddress!));
}
} catch (_) {
// FIX-ME: Unhandled exception
title: S.current.transaction_details_recipient_address,
value: recipientAddress!));
}
}
@ -110,12 +111,6 @@ abstract class TransactionDetailsViewModelBase with Store {
} catch (e) {}
}));
final descriptionKey = '${transactionInfo.txHash}_${wallet.walletAddresses.primaryAddress}';
final description = transactionDescriptionBox.values.firstWhere(
(val) => val.id == descriptionKey || val.id == transactionInfo.txHash,
orElse: () => TransactionDescription(id: descriptionKey));
items.add(TextFieldListItem(
title: S.current.note_tap_to_change,
value: description.note,

View file

@ -101,6 +101,7 @@ abstract class WalletCreationVMBase with Store {
address: '',
showIntroCakePayCard: (!walletCreationService.typeExists(type)) && type != WalletType.haven,
derivationInfo: credentials.derivationInfo ?? getDefaultCreateDerivation(),
derivations: credentials.derivations,
hardwareWalletType: credentials.hardwareWalletType,
parentAddress: credentials.parentAddress,
);
@ -200,15 +201,36 @@ abstract class WalletCreationVMBase with Store {
switch (walletType) {
case WalletType.bitcoin:
case WalletType.litecoin:
final derivationList = await bitcoin!.getDerivationsFromMnemonic(
final bitcoinDerivations = await bitcoin!.getDerivationsFromMnemonic(
mnemonic: restoreWallet.mnemonicSeed!,
node: node,
passphrase: restoreWallet.passphrase,
);
if (derivationList.firstOrNull?.transactionsCount == 0 && derivationList.length > 1)
return [];
return derivationList;
List<DerivationInfo> list = [];
for (var derivation in bitcoinDerivations) {
if (derivation.derivationType == DerivationType.electrum) {
list.add(
DerivationInfo(
derivationType: DerivationType.electrum,
derivationPath: "m/0'",
description: "Electrum",
scriptType: "p2wpkh",
),
);
} else {
list.add(
DerivationInfo(
derivationType: DerivationType.bip39,
derivationPath: "m/84'/0'/0'",
description: "Standard BIP84 native segwit",
scriptType: "p2wpkh",
),
);
}
}
return list;
case WalletType.nano:
return nanoUtil!.getDerivationsFromMnemonic(

View file

@ -91,6 +91,7 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store {
final height = options['height'] as int? ?? 0;
name = options['name'] as String;
DerivationInfo? derivationInfo = options["derivationInfo"] as DerivationInfo?;
List<DerivationInfo>? derivations = options["derivations"] as List<DerivationInfo>?;
if (mode == WalletRestoreMode.seed) {
final seed = options['seed'] as String;
@ -105,8 +106,7 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store {
mnemonic: seed,
password: password,
passphrase: passphrase,
derivationType: derivationInfo!.derivationType!,
derivationPath: derivationInfo.derivationPath!,
derivations: derivations,
);
case WalletType.haven:
return haven!.createHavenRestoreWalletFromSeedCredentials(
@ -256,11 +256,36 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store {
case WalletType.litecoin:
String? mnemonic = credentials['seed'] as String?;
String? passphrase = credentials['passphrase'] as String?;
return bitcoin!.getDerivationsFromMnemonic(
final bitcoinDerivations = await bitcoin!.getDerivationsFromMnemonic(
mnemonic: mnemonic!,
node: node,
passphrase: passphrase,
);
List<DerivationInfo> list = [];
for (var derivation in bitcoinDerivations) {
if (derivation.derivationType.toString().endsWith("electrum")) {
list.add(
DerivationInfo(
derivationType: DerivationType.electrum,
derivationPath: "m/0'",
description: "Electrum",
scriptType: "p2wpkh",
),
);
} else {
list.add(
DerivationInfo(
derivationType: DerivationType.bip39,
derivationPath: "m/84'/0'/0'",
description: "Standard BIP84 native segwit",
scriptType: "p2wpkh",
),
);
}
}
return list;
case WalletType.nano:
String? mnemonic = credentials['seed'] as String?;
String? seedKey = credentials['private_key'] as String?;

View file

@ -133,8 +133,8 @@ dependency_overrides:
protobuf: ^3.1.0
bitcoin_base:
git:
url: https://github.com/cake-tech/bitcoin_base
ref: cake-update-v9
url: https://github.com/cake-tech/bitcoin_base.git
ref: cake-update-v15
ffi: 2.1.0
flutter_icons:

View file

@ -709,6 +709,7 @@
"silent_payments_always_scan": "حدد المدفوعات الصامتة دائمًا المسح الضوئي",
"silent_payments_disclaimer": "العناوين الجديدة ليست هويات جديدة. إنها إعادة استخدام هوية موجودة مع ملصق مختلف.",
"silent_payments_display_card": "عرض بطاقة المدفوعات الصامتة",
"silent_payments_register_key": "سجل عرض مفتاح المسح الأسرع",
"silent_payments_scan_from_date": "فحص من التاريخ",
"silent_payments_scan_from_date_or_blockheight": "يرجى إدخال ارتفاع الكتلة الذي تريد بدء المسح الضوئي للمدفوعات الصامتة الواردة ، أو استخدام التاريخ بدلاً من ذلك. يمكنك اختيار ما إذا كانت المحفظة تواصل مسح كل كتلة ، أو تتحقق فقط من الارتفاع المحدد.",
"silent_payments_scan_from_height": "فحص من ارتفاع الكتلة",

View file

@ -709,6 +709,7 @@
"silent_payments_always_scan": "Задайте мълчаливи плащания винаги сканиране",
"silent_payments_disclaimer": "Новите адреси не са нови идентичности. Това е повторна употреба на съществуваща идентичност с различен етикет.",
"silent_payments_display_card": "Показване на безшумни плащания карта",
"silent_payments_register_key": "Регистрирайте ключа за преглед на по -бързото сканиране",
"silent_payments_scan_from_date": "Сканиране от дата",
"silent_payments_scan_from_date_or_blockheight": "Моля, въведете височината на блока, която искате да започнете да сканирате за входящи безшумни плащания, или вместо това използвайте датата. Можете да изберете дали портфейлът продължава да сканира всеки блок или проверява само определената височина.",
"silent_payments_scan_from_height": "Сканиране от височината на блока",

View file

@ -709,6 +709,7 @@
"silent_payments_always_scan": "Nastavit tiché platby vždy skenování",
"silent_payments_disclaimer": "Nové adresy nejsou nové identity. Je to opětovné použití existující identity s jiným štítkem.",
"silent_payments_display_card": "Zobrazit kartu Silent Payments",
"silent_payments_register_key": "Zobrazení zaregistrujte klíč pro rychlejší skenování",
"silent_payments_scan_from_date": "Skenovat od data",
"silent_payments_scan_from_date_or_blockheight": "Zadejte výšku bloku, kterou chcete začít skenovat, zda jsou přicházející tiché platby, nebo místo toho použijte datum. Můžete si vybrat, zda peněženka pokračuje v skenování každého bloku nebo zkontroluje pouze zadanou výšku.",
"silent_payments_scan_from_height": "Skenování z výšky bloku",

View file

@ -710,6 +710,7 @@
"silent_payments_always_scan": "Setzen Sie stille Zahlungen immer scannen",
"silent_payments_disclaimer": "Neue Adressen sind keine neuen Identitäten. Es ist eine Wiederverwendung einer bestehenden Identität mit einem anderen Etikett.",
"silent_payments_display_card": "Zeigen Sie stille Zahlungskarte",
"silent_payments_register_key": "Registrieren Sie die Ansichtsschlüssel für schnelleres Scannen",
"silent_payments_scan_from_date": "Scan ab Datum",
"silent_payments_scan_from_date_or_blockheight": "Bitte geben Sie die Blockhöhe ein, die Sie für eingehende stille Zahlungen scannen möchten, oder verwenden Sie stattdessen das Datum. Sie können wählen, ob die Wallet jeden Block scannt oder nur die angegebene Höhe überprüft.",
"silent_payments_scan_from_height": "Scan aus der Blockhöhe scannen",

View file

@ -712,6 +712,7 @@
"silent_payments_always_scan": "Set Silent Payments always scanning",
"silent_payments_disclaimer": "New addresses are not new identities. It is a re-use of an existing identity with a different label.",
"silent_payments_display_card": "Show Silent Payments card",
"silent_payments_register_key": "Register view key for faster scanning",
"silent_payments_scan_from_date": "Scan from date",
"silent_payments_scan_from_date_or_blockheight": "Please enter the block height you want to start scanning for incoming Silent Payments or use the date instead. You can choose if the wallet continues scanning every block, or checks only the specified height.",
"silent_payments_scan_from_height": "Scan from block height",

View file

@ -1,5 +1,5 @@
{
"about_cake_pay": "Cake Pay le permite comprar fácilmente tarjetas de regalo con activos virtuales, gastables instantáneamente en más de 150 000 comerciantes en los Estados Unidos.",
"about_cake_pay": "Cake Pay te permite comprar fácilmente tarjetas de regalo con activos virtuales, gastables instantáneamente en más de 150,000 comerciantes en los Estados Unidos.",
"account": "Cuenta",
"accounts": "Cuentas",
"accounts_subaddresses": "Cuentas y subdirecciones",
@ -21,13 +21,13 @@
"add_token_disclaimer_check": "He confirmado la dirección del contrato del token y la información utilizando una fuente confiable. Agregar información maliciosa o incorrecta puede resultar en una pérdida de fondos.",
"add_token_warning": "No edite ni agregue tokens según las instrucciones de los estafadores.\n¡Confirme siempre las direcciones de los tokens con fuentes acreditadas!",
"add_value": "Añadir valor",
"address": "DIRECCIÓN",
"address": "Dirección",
"address_book": "Libreta de direcciones",
"address_book_menu": "Libreta de direcciones",
"address_detected": "Dirección detectada",
"address_from_domain": "Esta dirección es de ${domain} en Unstoppable Domains",
"address_from_yat": "Esta dirección es de ${emoji} en Yat",
"address_label": "Address label",
"address_label": "Etiqueta de dirección",
"address_remove_contact": "Remover contacto",
"address_remove_content": "¿Estás seguro de que quieres eliminar el contacto seleccionado?",
"addresses": "Direcciones",
@ -37,12 +37,12 @@
"agree_and_continue": "Aceptar y continuar",
"agree_to": "Al crear una cuenta, aceptas ",
"alert_notice": "Aviso",
"all": "TODOS",
"all": "Todos",
"all_trades": "Todos los oficios",
"all_transactions": "Todas las transacciones",
"alphabetical": "Alfabético",
"already_have_account": "¿Ya tienes una cuenta?",
"always": "siempre",
"always": "Siempre",
"amount": "Cantidad: ",
"amount_is_below_minimum_limit": "Su saldo después de las tarifas sería menor que la cantidad mínima necesaria para el intercambio (${min})",
"amount_is_estimate": "El monto recibido es un estimado",
@ -54,17 +54,17 @@
"arrive_in_this_address": "${currency} ${tag}llegará a esta dirección",
"ascending": "Ascendente",
"ask_each_time": "Pregunta cada vez",
"auth_store_ban_timeout": "prohibición de tiempo de espera",
"auth_store_ban_timeout": "Prohibición de tiempo de espera",
"auth_store_banned_for": "Prohibido para ",
"auth_store_banned_minutes": " minutos",
"auth_store_incorrect_password": "Contraseña PIN",
"authenticated": "Autenticados",
"authentication": "Autenticación",
"auto_generate_addresses": "Auto Generar direcciones",
"auto_generate_addresses": "Auto-generar nuevas direcciones",
"auto_generate_subaddresses": "Generar subdirecciones automáticamente",
"automatic": "Automático",
"available_balance": "Balance disponible",
"available_balance_description": "Su saldo disponible es la cantidad de fondos que puede gastar. Los fondos que se muestran aquí se pueden gastar inmediatamente.",
"available_balance_description": "Tu saldo disponible es la cantidad de fondos que puedes gastar. Los fondos que se muestran aquí, se pueden gastar inmediatamente.",
"avg_savings": "Ahorro promedio",
"awaitDAppProcessing": "Espere a que la dApp termine de procesarse.",
"awaiting_payment_confirmation": "Esperando confirmación de pago",
@ -78,8 +78,8 @@
"billing_address_info": "Si se le solicita una dirección de facturación, proporcione su dirección de envío",
"biometric_auth_reason": "Escanee su huella digital para autenticar",
"bitcoin_dark_theme": "Tema oscuro de Bitcoin",
"bitcoin_light_theme": "Tema de la luz de Bitcoin",
"bitcoin_payments_require_1_confirmation": "Los pagos de Bitcoin requieren 1 confirmación, que puede demorar 20 minutos o más. ¡Gracias por su paciencia! Se le enviará un correo electrónico cuando se confirme el pago.",
"bitcoin_light_theme": "Tema claro de Bitcoin",
"bitcoin_payments_require_1_confirmation": "Los pagos de Bitcoin requieren 1 confirmación, que puede demorar 20 minutos o más. ¡Gracias por tu paciencia! Se te enviará un correo electrónico cuando se confirme el pago.",
"block_remaining": "1 bloqueo restante",
"Blocks_remaining": "${status} Bloques restantes",
"bluetooth": "Bluetooth",
@ -93,26 +93,26 @@
"buy_with": "Compra con",
"by_cake_pay": "por Cake Pay",
"cake_2fa_preset": "Pastel 2FA preestablecido",
"cake_dark_theme": "Tema oscuro del pastel",
"cake_pay_account_note": "Regístrese con solo una dirección de correo electrónico para ver y comprar tarjetas. ¡Algunas incluso están disponibles con descuento!",
"cake_pay_learn_more": "¡Compre y canjee tarjetas de regalo al instante en la aplicación!\nDeslice el dedo de izquierda a derecha para obtener más información.",
"cake_pay_save_order": "La tarjeta debe enviarse a su correo electrónico dentro de 1 día hábil \n Guardar su ID de pedido:",
"cake_pay_subtitle": "Compre tarjetas prepagas y tarjetas de regalo en todo el mundo",
"cake_pay_web_cards_subtitle": "Compre tarjetas de prepago y tarjetas de regalo en todo el mundo",
"cake_dark_theme": "Tema oscuro",
"cake_pay_account_note": "Regístrate con solo una dirección de correo electrónico para ver y comprar tarjetas. ¡Algunas incluso están disponibles con descuento!",
"cake_pay_learn_more": "¡Compra y canjea tarjetas de regalo al instante en la aplicación!\nDesliza el dedo de izquierda a derecha para obtener más información.",
"cake_pay_save_order": "La tarjeta debe enviarse a tu correo electrónico dentro de 1 día hábil \n Guardar su ID de pedido:",
"cake_pay_subtitle": "Compra tarjetas prepagadas y tarjetas de regalo en todo el mundo",
"cake_pay_web_cards_subtitle": "Compra tarjetas de prepago y tarjetas de regalo en todo el mundo",
"cake_pay_web_cards_title": "Tarjetas Web Cake Pay",
"cake_wallet": "Cake Wallet",
"cakepay_prepaid_card": "Tarjeta de Débito Prepago CakePay",
"camera_consent": "Su cámara será utilizada para capturar una imagen con fines de identificación por ${provider}. Consulte su Política de privacidad para obtener más detalles.",
"camera_permission_is_required": "Se requiere permiso de la cámara.\nHabilítelo desde la configuración de la aplicación.",
"camera_consent": "Su cámara será utilizada para capturar una imagen con fines de identificación por ${provider}. Consulta tu Política de privacidad para obtener más detalles.",
"camera_permission_is_required": "Se requiere permiso de la cámara.\nHabilítalo desde la configuración de la aplicación.",
"cancel": "Cancelar",
"card_address": "Dirección:",
"cardholder_agreement": "Acuerdo del titular de la tarjeta",
"cards": "Cartas",
"chains": "Cadenas",
"change": "Cambio",
"change_backup_password_alert": "Sus archivos de respaldo anteriores no estarán disponibles para importar con la nueva contraseña de respaldo. La nueva contraseña de respaldo se utilizará solo para los nuevos archivos de respaldo. ¿Está seguro de que desea cambiar la contraseña de respaldo?",
"change_backup_password_alert": "Tus archivos de respaldo anteriores no estarán disponibles para importar con la nueva contraseña de respaldo. La nueva contraseña de respaldo se utilizará solo para los nuevos archivos de respaldo. ¿Está seguro de que desea cambiar la contraseña de respaldo?",
"change_currency": "Cambiar moneda",
"change_current_node": "¿Está seguro de cambiar el nodo actual a ${node}?",
"change_current_node": "¿Estás seguro de cambiar el nodo actual a ${node}?",
"change_current_node_title": "Cambiar el nodo actual",
"change_exchange_provider": "Cambiar proveedor de intercambio",
"change_language": "Cambiar idioma",
@ -125,38 +125,38 @@
"change_wallet_alert_title": "Cambiar billetera actual",
"choose_account": "Elegir cuenta",
"choose_address": "\n\nPor favor elija la dirección:",
"choose_card_value": "Elija un valor de tarjeta",
"choose_derivation": "Elija la derivación de la billetera",
"choose_from_available_options": "Elija entre las opciones disponibles:",
"choose_card_value": "Elige un valor de tarjeta",
"choose_derivation": "Elige la derivación de la billetera",
"choose_from_available_options": "Elige entre las opciones disponibles:",
"choose_one": "Elige uno",
"choose_relay": "Por favor elija un relé para usar",
"choose_wallet_currency": "Por favor, elija la moneda de la billetera:",
"choose_wallet_group": "Elija el grupo de billetera",
"choose_relay": "Por favor elige un relay para usar",
"choose_wallet_currency": "Por favor, elige la moneda de la billetera:",
"choose_wallet_group": "Elige el grupo de billetera",
"clear": "Claro",
"clearnet_link": "enlace Clearnet",
"close": "Cerca",
"coin_control": "Control de monedas (opcional)",
"cold_or_recover_wallet": "Agregue una billetera fría o recupere una billetera de papel",
"cold_or_recover_wallet": "Agrega una billetera fría o recupera una billetera de papel",
"color_theme": "Tema de color",
"commit_transaction_amount_fee": "Confirmar transacción\nCantidad: ${amount}\nCuota: ${fee}",
"confirm": "Confirmar",
"confirm_delete_template": "Esta acción eliminará esta plantilla. ¿Desea continuar?",
"confirm_delete_wallet": "Esta acción eliminará esta billetera. ¿Desea continuar?",
"confirm_delete_template": "Esta acción eliminará esta plantilla. ¿Deseas continuar?",
"confirm_delete_wallet": "Esta acción eliminará esta billetera. ¿Deseas continuar?",
"confirm_fee_deduction": "Confirmar la deducción de la tarifa",
"confirm_fee_deduction_content": "¿Acepta deducir la tarifa de la producción?",
"confirm_passphrase": "Confirmar la frase de pases",
"confirm_fee_deduction_content": "¿Aceptas deducir la tarifa de la producción?",
"confirm_passphrase": "Confirmar la contraseña",
"confirm_sending": "Confirmar envío",
"confirm_silent_payments_switch_node": "Su nodo actual no admite pagos silenciosos \\ ncake billet cambiará a un nodo compatible, solo para escanear",
"confirm_silent_payments_switch_node": "Tu nodo actual no admite pagos silenciosos \\ nCake cambiará a un nodo compatible, solo para escanear",
"confirmations": "Confirmaciones",
"confirmed": "Saldo confirmado",
"confirmed_tx": "Confirmado",
"congratulations": "Felicidades!",
"connect_an_existing_yat": "Conectar un Yat existente",
"connect_yats": "Conectar Yats",
"connect_your_hardware_wallet": "Conecte su billetera de hardware con Bluetooth o USB",
"connect_your_hardware_wallet_ios": "Conecte su billetera de hardware con Bluetooth",
"connect_your_hardware_wallet": "Conecta tu billetera de hardware con Bluetooth o USB",
"connect_your_hardware_wallet_ios": "Conecta tu billetera de hardware con Bluetooth",
"connection_sync": "Conexión y sincronización",
"connectWalletPrompt": "Conecte su billetera con WalletConnect para realizar transacciones",
"connectWalletPrompt": "Conecte tu billetera con WalletConnect para realizar transacciones",
"contact": "Contacto",
"contact_list_contacts": "Contactos",
"contact_list_wallets": "Mis billeteras",
@ -187,7 +187,7 @@
"custom_drag": "Custom (mantenía y arrastre)",
"custom_redeem_amount": "Cantidad de canje personalizada",
"custom_value": "Valor personalizado",
"dark_theme": "Oscura",
"dark_theme": "Oscuro",
"debit_card": "Tarjeta de Débito",
"debit_card_terms": "El almacenamiento y el uso de su número de tarjeta de pago (y las credenciales correspondientes a su número de tarjeta de pago) en esta billetera digital están sujetos a los Términos y condiciones del acuerdo del titular de la tarjeta aplicable con el emisor de la tarjeta de pago, en vigor desde tiempo al tiempo.",
"decimal_places_error": "Demasiados lugares decimales",
@ -197,21 +197,21 @@
"delete": "Borrar",
"delete_account": "Eliminar cuenta",
"delete_wallet": "Eliminar billetera",
"delete_wallet_confirm_message": "¿Está seguro de que desea eliminar la billetera ${wallet_name}?",
"deleteConnectionConfirmationPrompt": "¿Está seguro de que desea eliminar la conexión a",
"delete_wallet_confirm_message": "¿Estás seguro de que deseas eliminar la billetera ${wallet_name}?",
"deleteConnectionConfirmationPrompt": "¿Estás seguro de que deseas eliminar la conexión a",
"denominations": "Denominaciones",
"derivationpath": "Ruta de derivación",
"descending": "Descendente",
"description": "Descripción",
"destination_tag": "Etiqueta de destino:",
"dfx_option_description": "Compre criptografía con EUR y CHF. Para clientes minoristas y corporativos en Europa",
"dfx_option_description": "Compre cripto con EUR y CHF. Para clientes minoristas y corporativos en Europa",
"didnt_get_code": "¿No recibiste el código?",
"digit_pin": "-dígito PIN",
"digital_and_physical_card": " tarjeta de débito prepago digital y física",
"disable": "Desactivar",
"disable_bulletin": "Desactivar el boletín de estado del servicio",
"disable_buy": "Desactivar acción de compra",
"disable_cake_2fa": "Desactivar pastel 2FA",
"disable_cake_2fa": "Desactivar 2FA",
"disable_exchange": "Deshabilitar intercambio",
"disable_fee_api_warning": "Al apagar esto, las tasas de tarifas pueden ser inexactas en algunos casos, por lo que puede terminar pagando en exceso o pagando menos las tarifas por sus transacciones",
"disable_fiat": "Deshabilitar fiat",
@ -224,7 +224,7 @@
"displayable": "Visualizable",
"do_not_have_enough_gas_asset": "No tienes suficiente ${currency} para realizar una transacción con las condiciones actuales de la red blockchain. Necesita más ${currency} para pagar las tarifas de la red blockchain, incluso si envía un activo diferente.",
"do_not_send": "no enviar",
"do_not_share_warning_text": "No comparta estos con nadie más, incluido el soporte.\n\n¡Sus fondos pueden ser y serán robados!",
"do_not_share_warning_text": "No compartas estos con nadie más, incluido el soporte.\n\n¡Tus fondos pueden ser y serán robados!",
"do_not_show_me": "no me muestres esto otra vez",
"domain_looks_up": "Búsquedas de dominio",
"donation_link_details": "Detalles del enlace de donación",
@ -238,21 +238,21 @@
"enable": "Permitir",
"enable_mempool_api": "API de Mempool para tarifas y fechas precisas",
"enable_replace_by_fee": "Habilitar reemplazar por tarea",
"enable_silent_payments_scanning": "Comience a escanear pagos silenciosos, hasta que se alcance la punta",
"enable_silent_payments_scanning": "Comienza a escanear pagos silenciosos, hasta que se alcance la altura actual",
"enabled": "Activado",
"enter_amount": "Ingrese la cantidad",
"enter_backup_password": "Ingrese la contraseña de respaldo aquí",
"enter_amount": "Ingresa la cantidad",
"enter_backup_password": "Ingresa la contraseña de respaldo aquí",
"enter_code": "Ingresar código",
"enter_seed_phrase": "Ingrese su frase de semillas",
"enter_totp_code": "Ingrese el código TOTP.",
"enter_wallet_password": "Ingrese la contraseña de la billetera",
"enter_seed_phrase": "Ingresa su frase de semillas",
"enter_totp_code": "Ingresa el código TOTP.",
"enter_wallet_password": "Ingresa la contraseña de la billetera",
"enter_your_note": "Ingresa tu nota…",
"enter_your_pin": "Introduce tu PIN",
"enter_your_pin_again": "Ingrese su PIN nuevamente",
"enterTokenID": "Ingrese el ID del token",
"enterWalletConnectURI": "Ingrese el URI de WalletConnect",
"enter_your_pin_again": "Ingresa su PIN nuevamente",
"enterTokenID": "Ingresa el ID del token",
"enterWalletConnectURI": "Ingresa el URI de WalletConnect",
"error": "Error",
"error_dialog_content": "Vaya, tenemos un error.\n\nEnvíe el informe de bloqueo a nuestro equipo de soporte para mejorar la aplicación.",
"error_dialog_content": "Vaya, tenemos un error.\n\nEnvía el informe de error a nuestro equipo de soporte para mejorar la aplicación.",
"error_text_account_name": "El nombre de la cuenta solo puede contener letras, números \ny debe tener entre 1 y 15 caracteres de longitud",
"error_text_address": "La dirección de la billetera debe corresponder al tipo \nde criptomoneda",
"error_text_amount": "La cantidad solo puede contener números",
@ -281,7 +281,7 @@
"etherscan_history": "historia de etherscan",
"event": "Evento",
"events": "Eventos",
"exchange": "Intercambio",
"exchange": "Intercambiar",
"exchange_incorrect_current_wallet_for_xmr": "Si desea intercambiar XMR desde su billetera de pastel Monero Balance, primero cambie a su billetera Monero.",
"exchange_new_template": "Nueva plantilla",
"exchange_provider_unsupported": "¡${providerName} ya no es compatible!",
@ -304,14 +304,14 @@
"fee_rate": "Tarifa",
"fetching": "Cargando",
"fiat_api": "Fiat API",
"fiat_balance": "Equilibrio Fiat",
"fiat_balance": "Balance fiat",
"field_required": "Este campo es obligatorio",
"fill_code": "Por favor complete el código de verificación proporcionado a su correo electrónico",
"fill_code": "Por favor completa el código de verificación proporcionado en tu correo electrónico",
"filter_by": "Filtrado por",
"first_wallet_text": "Impresionante billetera para Monero, Bitcoin, Ethereum, Litecoin, y Haven",
"fixed_pair_not_supported": "Este par fijo no es compatible con los servicios de intercambio seleccionados",
"fixed_rate": "Tipo de interés fijo",
"fixed_rate_alert": "Podrá ingresar la cantidad recibida cuando el modo de tarifa fija esté marcado. ¿Quieres cambiar al modo de tarifa fija?",
"fixed_rate_alert": "Podrás ingresar la cantidad recibida cuando el modo de tarifa fija esté marcado. ¿Quieres cambiar al modo de tarifa fija?",
"forgot_password": "Olvidé mi contraseña",
"freeze": "Congelar",
"frequently_asked_questions": "Preguntas frecuentes",
@ -333,7 +333,7 @@
"gross_balance": "Saldo bruto",
"group_by_type": "Grupo por tipo",
"haven_app": "Haven by Cake Wallet",
"haven_app_wallet_text": "Awesome wallet for Haven",
"haven_app_wallet_text": "Increíble billetera para Haven",
"help": "ayuda",
"hidden_balance": "Balance oculto",
"hide_details": "Ocultar detalles",
@ -349,9 +349,9 @@
"incoming": "Entrante",
"incorrect_seed": "El texto ingresado no es válido.",
"inputs": "Entradas",
"insufficient_lamport_for_tx": "No tiene suficiente SOL para cubrir la transacción y su tarifa de transacción. Por favor, agregue más SOL a su billetera o reduzca la cantidad de sol que está enviando.",
"insufficient_lamports": "No tiene suficiente SOL para cubrir la transacción y su tarifa de transacción. Necesita al menos ${solValueNeeded} sol. Por favor, agregue más sol a su billetera o reduzca la cantidad de sol que está enviando",
"insufficientFundsForRentError": "No tiene suficiente SOL para cubrir la tarifa de transacción y alquilar para la cuenta. Por favor, agregue más sol a su billetera o reduzca la cantidad de sol que está enviando",
"insufficient_lamport_for_tx": "No tienes suficiente SOL para cubrir la transacción y su tarifa de transacción. Por favor, agrega más SOL a su billetera o reduce la cantidad de sol que está enviando.",
"insufficient_lamports": "No tienes suficiente SOL para cubrir la transacción y su tarifa de transacción. Necesita al menos ${solValueNeeded} sol. Por favor, agrega más sol a su billetera o reduzca la cantidad de sol que está enviando",
"insufficientFundsForRentError": "No tienes suficiente SOL para cubrir la tarifa de transacción y alquilar para la cuenta. Por favor, agrega más sol a su billetera o reduce la cantidad de sol que está enviando",
"introducing_cake_pay": "¡Presentamos Cake Pay!",
"invalid_input": "Entrada inválida",
"invalid_password": "Contraseña invalida",
@ -359,12 +359,12 @@
"is_percentage": "es",
"last_30_days": "Últimos 30 días",
"learn_more": "Aprende más",
"ledger_connection_error": "No se pudo conectar con su libro mayor. Inténtalo de nuevo.",
"ledger_error_device_locked": "El libro mayor está bloqueado",
"ledger_connection_error": "No se pudo conectar con ledger. Inténtalo de nuevo.",
"ledger_error_device_locked": "Ledger está bloqueado",
"ledger_error_tx_rejected_by_user": "Transacción rechazada en el dispositivo",
"ledger_error_wrong_app": "Por favor, asegúrese de abrir la aplicación correcta en su libro mayor.",
"ledger_please_enable_bluetooth": "Habilite Bluetooth para detectar su libro mayor",
"light_theme": "Ligera",
"ledger_error_wrong_app": "Por favor, asegúrate de abrir la aplicación correcta en su libro mayor.",
"ledger_please_enable_bluetooth": "Habilita tu Bluetooth para detectar tu ledger",
"light_theme": "Ligero",
"litecoin_enable_mweb_sync": "Habilitar el escaneo mweb",
"litecoin_mweb": "Mweb",
"litecoin_mweb_always_scan": "Establecer mweb siempre escaneo",
@ -372,8 +372,8 @@
"litecoin_mweb_dismiss": "Despedir",
"litecoin_mweb_display_card": "Mostrar tarjeta MWEB",
"litecoin_mweb_enable_later": "Puede elegir habilitar MWEB nuevamente en la configuración de visualización.",
"litecoin_mweb_pegin": "Meter",
"litecoin_mweb_pegout": "Estirar la pata",
"litecoin_mweb_pegin": "Convertir",
"litecoin_mweb_pegout": "Recuperar",
"litecoin_mweb_scanning": "Escaneo mweb",
"litecoin_mweb_settings": "Configuración de MWEB",
"litecoin_mweb_warning": "El uso de MWEB inicialmente descargará ~ 600 MB de datos, y puede tomar hasta 30 minutos según la velocidad de la red. Estos datos iniciales solo se descargarán una vez y estarán disponibles para todas las billeteras de Litecoin",
@ -401,9 +401,9 @@
"min_value": "Min: ${value} ${currency}",
"minutes_to_pin_code": "${minute} minutos",
"mm": "mm",
"modify_2fa": "Modificar torta 2FA",
"monero_com": "Monero.com by Cake Wallet",
"monero_com_wallet_text": "Awesome wallet for Monero",
"modify_2fa": "Modificar 2FA",
"monero_com": "Monero.com por Cake Wallet",
"monero_com_wallet_text": "Increíble billetera para Monero",
"monero_dark_theme": "Tema oscuro de Monero",
"monero_light_theme": "Tema ligero de Monero",
"moonpay_alert_text": "El valor de la cantidad debe ser mayor o igual a ${minAmount} ${fiatCurrency}",
@ -412,12 +412,12 @@
"mweb_unconfirmed": "Mweb no confirmado",
"name": "Nombre",
"nano_current_rep": "Representante actual",
"nano_gpt_thanks_message": "¡Gracias por usar nanogpt! ¡Recuerde regresar al navegador después de que se complete su transacción!",
"nano_gpt_thanks_message": "¡Gracias por usar nanogpt! ¡Recuerda regresar al navegador después de que se complete su transacción!",
"nano_pick_new_rep": "Elija un nuevo representante",
"nanogpt_subtitle": "Todos los modelos más nuevos (GPT-4, Claude). \\nSin suscripción, pague con cripto.",
"narrow": "Angosto",
"new_first_wallet_text": "Mantenga fácilmente su criptomoneda segura",
"new_node_testing": "Prueba de nuevos nodos",
"new_first_wallet_text": "Mantén fácilmente tu criptomoneda segura",
"new_node_testing": "Prueba nuevos nodos",
"new_subaddress_create": "Crear",
"new_subaddress_label_name": "Nombre de etiqueta",
"new_subaddress_title": "Nueva direccion",
@ -426,10 +426,10 @@
"newConnection": "Nueva conexión",
"no_cards_found": "No se encuentran cartas",
"no_id_needed": "¡No se necesita identificación!",
"no_id_required": "No se requiere identificación. Recargue y gaste en cualquier lugar",
"no_relay_on_domain": "No hay una retransmisión para el dominio del usuario o la retransmisión no está disponible. Elija un relé para usar.",
"no_relays": "Sin relevos",
"no_relays_message": "Encontramos un registro Nostr NIP-05 para este usuario, pero no contiene ningún relé. Indique al destinatario que agregue retransmisiones a su registro Nostr.",
"no_id_required": "No se requiere identificación. Recarga y gaste en cualquier lugar",
"no_relay_on_domain": "No hay una retransmisión para el dominio del usuario o la retransmisión no está disponible. Elige un relay para usar.",
"no_relays": "Sin relays",
"no_relays_message": "Encontramos un registro Nostr NIP-05 para este usuario, pero no contiene ningún relay. Indica al destinatario que agregue retransmisiones a su registro Nostr.",
"node_address": "Dirección de nodo",
"node_connection_failed": "La conexión falló",
"node_connection_successful": "La conexión fue exitosa",
@ -449,15 +449,15 @@
"offline": "fuera de línea",
"ok": "OK",
"old_fee": "Tarifa antigua",
"onion_link": "Enlace de cebolla",
"onion_link": "Enlace de cebolla (Tor)",
"online": "En línea",
"onramper_option_description": "Compre rápidamente cripto con muchos métodos de pago. Disponible en la mayoría de los países. Los diferenciales y las tarifas varían.",
"onramper_option_description": "Compra rápidamente cripto con muchos métodos de pago. Disponible en la mayoría de los países. Los diferenciales y las tarifas varían.",
"open_gift_card": "Abrir tarjeta de regalo",
"optional_description": "Descripción opcional",
"optional_email_hint": "Correo electrónico de notificación del beneficiario opcional",
"optional_name": "Nombre del destinatario opcional",
"optionally_order_card": "Opcionalmente pide una tarjeta física.",
"orbot_running_alert": "Asegúrese de que Orbot se esté ejecutando antes de conectarse a este nodo.",
"orbot_running_alert": "Asegúrate de que Orbot se esté ejecutando antes de conectarte a este nodo.",
"order_by": "Ordenar",
"order_id": "Identificación del pedido",
"order_physical_card": "Pedir tarjeta física",
@ -466,7 +466,7 @@
"outdated_electrum_wallet_receive_warning": "Si esta billetera tiene una semilla de 12 palabras y se creó en Cake, NO deposite Bitcoin en esta billetera. Cualquier BTC transferido a esta billetera se puede perder. Cree una nueva billetera de 24 palabras (toque el menú en la parte superior derecha, seleccione Monederos, elija Crear nueva billetera, luego seleccione Bitcoin) e INMEDIATAMENTE mueva su BTC allí. Las nuevas carteras BTC (24 palabras) de Cake son seguras",
"outgoing": "Saliente",
"outputs": "Salidas",
"overwrite_amount": "Overwrite amount",
"overwrite_amount": "Sobreescribir monto",
"pairingInvalidEvent": "Evento de emparejamiento no válido",
"passphrase": "Passfrase (opcional)",
"passphrases_doesnt_match": "Las frases de contrato no coinciden, intente nuevamente",
@ -482,18 +482,18 @@
"pin_is_incorrect": "PIN es incorrecto",
"pin_number": "Número PIN",
"placeholder_contacts": "Tus contactos se mostrarán aquí",
"placeholder_transactions": "Sus transacciones se mostrarán aquí",
"please_fill_totp": "Complete el código de 8 dígitos presente en su otro dispositivo",
"please_make_selection": "Seleccione a continuación para crear o recuperar su billetera.",
"please_reference_document": "Consulte los documentos a continuación para obtener más información.",
"please_select": "Por favor seleccione:",
"please_select_backup_file": "Seleccione el archivo de respaldo e ingrese la contraseña de respaldo.",
"placeholder_transactions": "Tus transacciones se mostrarán aquí",
"please_fill_totp": "Completa el código de 8 dígitos presente en su otro dispositivo",
"please_make_selection": "Selecciona a continuación para crear o recuperar su billetera.",
"please_reference_document": "Consulta los documentos a continuación para obtener más información.",
"please_select": "Por favor selecciona:",
"please_select_backup_file": "Selecciona el archivo de respaldo e ingrese la contraseña de respaldo.",
"please_try_to_connect_to_another_node": "Intenta conectarte a otro nodo",
"please_wait": "Espere por favor",
"please_wait": "Espera por favor",
"polygonscan_history": "Historial de PolygonScan",
"powered_by": "Energizado por ${title}",
"powered_by": "Posible gracias a ${title}",
"pre_seed_button_text": "Entiendo. Muéstrame mi semilla",
"pre_seed_description": "En la página siguiente verá una serie de ${words} palabras. Esta es su semilla única y privada y es la ÚNICA forma de recuperar su billetera en caso de pérdida o mal funcionamiento. Es SU responsabilidad escribirlo y guardarlo en un lugar seguro fuera de la aplicación Cake Wallet.",
"pre_seed_description": "En la página siguiente verás una serie de ${words} palabras. Esta es su semilla única y privada y es la ÚNICA forma de recuperar tu billetera en caso de pérdida o mal funcionamiento. Es TU responsabilidad escribirla y guardarla en un lugar seguro fuera de la aplicación Cake Wallet.",
"pre_seed_title": "IMPORTANTE",
"prepaid_cards": "Tajetas prepagadas",
"prevent_screenshots": "Evitar capturas de pantalla y grabación de pantalla",
@ -502,17 +502,17 @@
"privacy_settings": "Configuración de privacidad",
"private_key": "Clave privada",
"proceed_after_one_minute": "Si la pantalla no continúa después de 1 minuto, revisa tu correo electrónico.",
"proceed_on_device": "Continúe con su dispositivo",
"proceed_on_device_description": "Siga las instrucciones solicitadas en su billetera de hardware",
"proceed_on_device": "Continúa con tu dispositivo",
"proceed_on_device_description": "Sigue las instrucciones solicitadas en su billetera de hardware",
"profile": "Perfil",
"provider_error": "${provider} error",
"public_key": "Clave pública",
"purchase_gift_card": "Comprar tarjeta de regalo",
"purple_dark_theme": "Tema morado oscuro",
"qr_fullscreen": "Toque para abrir el código QR en pantalla completa",
"qr_payment_amount": "This QR code contains a payment amount. Do you want to overwrite the current value?",
"qr_payment_amount": "Este código QR contiene un monto de pago. ¿Quieres sobreescribirlo?",
"quantity": "Cantidad",
"question_to_disable_2fa": "¿Está seguro de que desea deshabilitar Cake 2FA? Ya no se necesitará un código 2FA para acceder a la billetera y a ciertas funciones.",
"question_to_disable_2fa": "¿Estás seguro de que desea deshabilitar Cake 2FA? Ya no se necesitará un código 2FA para acceder a la billetera y a ciertas funciones.",
"receivable_balance": "Saldo de cuentas por cobrar",
"receive": "Recibir",
"receive_amount": "Cantidad",
@ -529,12 +529,12 @@
"remaining": "restante",
"remove": "Retirar",
"remove_node": "Eliminar nodo",
"remove_node_message": "¿Está seguro de que desea eliminar el nodo seleccionado?",
"rename": "Rebautizar",
"remove_node_message": "¿Estás seguro de que desea eliminar el nodo seleccionado?",
"rename": "Renombrar",
"rep_warning": "Advertencia representativa",
"rep_warning_sub": "Su representante no parece estar en buena posición. Toque aquí para seleccionar uno nuevo",
"repeat_wallet_password": "Repita la contraseña de billetera",
"repeated_password_is_incorrect": "La contraseña repetida es incorrecta. Repita la contraseña de la billetera nuevamente.",
"rep_warning_sub": "Tu representante no parece estar en buena posición. Toca aquí para seleccionar uno nuevo",
"repeat_wallet_password": "Repite la contraseña de billetera",
"repeated_password_is_incorrect": "La contraseña repetida es incorrecta. Repite la contraseña de la billetera nuevamente.",
"require_for_adding_contacts": "Requerido para agregar contactos",
"require_for_all_security_and_backup_settings": "Requerido para todas las configuraciones de seguridad y copia de seguridad",
"require_for_assessing_wallet": "Requerido para acceder a la billetera",
@ -566,17 +566,17 @@
"restore_recover": "Recuperar",
"restore_restore_wallet": "Recuperar Cartera",
"restore_seed_keys_restore": "Restauración de semillas / llaves",
"restore_spend_key_private": "Spend clave (privado)",
"restore_spend_key_private": "Llave de gasto (privada)",
"restore_title_from_backup": "Restaurar desde un archivo de respaldo",
"restore_title_from_hardware_wallet": "Restaurar desde la billetera de hardware",
"restore_title_from_keys": "De las claves",
"restore_title_from_seed": "De la semilla",
"restore_title_from_seed_keys": "Restaurar desde semilla/claves",
"restore_view_key_private": "View clave (privado)",
"restore_title_from_hardware_wallet": "Restaurar desde una cartera fría",
"restore_title_from_keys": "Usando las claves",
"restore_title_from_seed": "Usando la semilla",
"restore_title_from_seed_keys": "Restaurar usando semilla/claves",
"restore_view_key_private": "Llave de vista (privado)",
"restore_wallet": "Restaurar billetera",
"restore_wallet_name": "Nombre de la billetera",
"restore_wallet_restore_description": "Restaurar billetera",
"robinhood_option_description": "Compre y transfiera instantáneamente utilizando su tarjeta de débito, cuenta bancaria o saldo de Robinhood. Solo EE. UU.",
"robinhood_option_description": "Compra y transfiere instantáneamente utilizando su tarjeta de débito, cuenta bancaria o saldo de Robinhood. Solo EE. UU.",
"router_no_route": "No hay ruta definida para ${name}",
"save": "Salvar",
"save_backup_password": "Asegúrese de haber guardado su contraseña de respaldo. No podrá importar sus archivos de respaldo sin él.",
@ -585,7 +585,7 @@
"saved_the_trade_id": "He salvado comercial ID",
"scan_one_block": "Escanear un bloque",
"scan_qr_code": "Escanear código QR",
"scan_qr_code_to_get_address": "Escanee el código QR para obtener la dirección",
"scan_qr_code_to_get_address": "Escanea el código QR para obtener la dirección",
"scan_qr_on_device": "Escanea este código QR en otro dispositivo",
"search": "Búsqueda",
"search_add_token": "Buscar/Agregar token",
@ -612,25 +612,25 @@
"seed_language_german": "Alemán",
"seed_language_italian": "Italiana/Italiano",
"seed_language_japanese": "Japonés",
"seed_language_korean": "coreano",
"seed_language_korean": "Coreano",
"seed_language_next": "Próximo",
"seed_language_portuguese": "Portugués",
"seed_language_russian": "Ruso",
"seed_language_spanish": "Español",
"seed_phrase_length": "Longitud de la frase inicial",
"seed_reminder": "Anótelos en caso de que pierda o borre su teléfono",
"seed_reminder": "Anótalos en caso de que pierdas o borres tu aplicación",
"seed_share": "Compartir semillas",
"seed_title": "Semilla",
"seedtype": "Type de semillas",
"seedtype_alert_content": "Compartir semillas con otras billeteras solo es posible con Bip39 Seed Type.",
"seedtype_alert_title": "Alerta de type de semillas",
"seedtype_legacy": "Legado (25 palabras)",
"seedtype_polyseed": "Polieta (16 palabras)",
"seedtype": "Tipos de semillas",
"seedtype_alert_content": "Compartir semillas con otras billeteras solo es posible con semillas bip39 - un tipo específico de semilla.",
"seedtype_alert_title": "Alerta de tipo de semillas",
"seedtype_legacy": "Semilla clásica-legacy (25 palabras)",
"seedtype_polyseed": "Poli-semilla (16 palabras)",
"seedtype_wownero": "Wownero (14 palabras)",
"select_backup_file": "Seleccionar archivo de respaldo",
"select_buy_provider_notice": "Seleccione un proveedor de compra arriba. Puede omitir esta pantalla configurando su proveedor de compra predeterminado en la configuración de la aplicación.",
"select_destination": "Seleccione el destino del archivo de copia de seguridad.",
"select_sell_provider_notice": "Seleccione un proveedor de venta arriba. Puede omitir esta pantalla configurando su proveedor de venta predeterminado en la configuración de la aplicación.",
"select_buy_provider_notice": "Selecciona un proveedor de compra arriba. Puede omitir esta pantalla configurando su proveedor de compra predeterminado en la configuración de la aplicación.",
"select_destination": "Selecciona el destino del archivo de copia de seguridad.",
"select_sell_provider_notice": "Selecciona un proveedor de venta arriba. Puede omitir esta pantalla configurando su proveedor de venta predeterminado en la configuración de la aplicación.",
"sell": "Vender",
"sell_alert_content": "Actualmente solo admitimos la venta de Bitcoin, Ethereum y Litecoin. Cree o cambie a su billetera Bitcoin, Ethereum o Litecoin.",
"sell_monero_com_alert_content": "Aún no se admite la venta de Monero",
@ -676,16 +676,16 @@
"settings_only_transactions": "Solo transacciones",
"settings_personal": "Personal",
"settings_save_recipient_address": "Guardar dirección del destinatario",
"settings_support": "Apoyo",
"settings_support": "Ayuda",
"settings_terms_and_conditions": "Términos y Condiciones",
"settings_title": "Configuraciones",
"settings_trades": "Comercia",
"settings_transactions": "Transacciones",
"settings_wallets": "Carteras",
"setup_2fa": "Configurar pastel 2FA",
"setup_2fa_text": "Cake 2FA funciona utilizando TOTP como segundo factor de autenticación.\n\nEl TOTP de Cake 2FA requiere SHA-512 y soporte de 8 dígitos; esto proporciona una mayor seguridad. Puede encontrar más información y aplicaciones compatibles en la guía.",
"setup_2fa": "Configurar 2FA",
"setup_2fa_text": "Cake 2FA funciona utilizando TOTP como segundo factor de autenticación.\n\nEl TOTP de Cake 2FA requiere SHA-512 y soporte de 8 dígitos; esto proporciona una mayor seguridad. Puedes encontrar más información y aplicaciones compatibles en la guía.",
"setup_pin": "PIN de configuración",
"setup_successful": "Su PIN se ha configurado correctamente!",
"setup_successful": "Tu PIN se ha configurado correctamente!",
"setup_totp_recommended": "Configurar TOTP",
"setup_warning_2fa_text": "Deberá restaurar su billetera a partir de la semilla mnemotécnica.\n\nEl soporte de Cake no podrá ayudarlo si pierde el acceso a su 2FA o a sus semillas mnemotécnicas.\nCake 2FA es una segunda autenticación para ciertas acciones en la billetera. Antes de usar Cake 2FA, recomendamos leer la guía.NO es tan seguro como el almacenamiento en frío.\n\nSi pierde el acceso a su aplicación 2FA o a sus claves TOTP, perderá el acceso a esta billetera. ",
"setup_your_debit_card": "Configura tu tarjeta de débito",
@ -704,23 +704,24 @@
"signature": "Firma",
"signature_invalid_error": "La firma no es válida para el mensaje dado",
"signTransaction": "Firmar transacción",
"signup_for_card_accept_terms": "Regístrese para obtener la tarjeta y acepte los términos.",
"signup_for_card_accept_terms": "Regístrate para obtener la tarjeta y acepte los términos.",
"silent_payment": "Pago silencioso",
"silent_payments": "Pagos silenciosos",
"silent_payments_always_scan": "Establecer pagos silenciosos siempre escaneando",
"silent_payments_disclaimer": "Las nuevas direcciones no son nuevas identidades. Es una reutilización de una identidad existente con una etiqueta diferente.",
"silent_payments_display_card": "Mostrar tarjeta de pagos silenciosos",
"silent_payments_register_key": "Clave de vista de registro para escaneo más rápido",
"silent_payments_scan_from_date": "Escanear desde la fecha",
"silent_payments_scan_from_date_or_blockheight": "Ingrese la altura del bloque que desea comenzar a escanear para pagos silenciosos entrantes, o use la fecha en su lugar. Puede elegir si la billetera continúa escaneando cada bloque, o verifica solo la altura especificada.",
"silent_payments_scan_from_height": "Escanear desde la altura del bloque",
"silent_payments_scanned_tip": "Escaneado hasta la punta! (${tip})",
"silent_payments_scan_from_date_or_blockheight": "Ingresa la altura de bloque que desea comenzar a escanear para pagos silenciosos entrantes, o usa la fecha en su lugar. Puedes elegir si la billetera continúa escaneando cada bloque, o verifica solo la altura especificada.",
"silent_payments_scan_from_height": "Escanear desde la altura de bloque específico",
"silent_payments_scanned_tip": "Escaneado hasta la altura actual! (${tip})",
"silent_payments_scanning": "Escaneo de pagos silenciosos",
"silent_payments_settings": "Configuración de pagos silenciosos",
"single_seed_wallets_group": "Billeteras de semillas individuales",
"slidable": "deslizable",
"sort_by": "Ordenar por",
"spend_key_private": "Spend clave (privado)",
"spend_key_public": "Spend clave (público)",
"spend_key_private": "Llave de gasto (privada)",
"spend_key_public": "Llave de gasto (pública)",
"status": "Estado: ",
"string_default": "Por defecto",
"subaddress_title": "Lista de subdirecciones",
@ -729,14 +730,14 @@
"successful": "Exitoso",
"support_description_guides": "Documentación y apoyo para problemas comunes",
"support_description_live_chat": "¡GRATIS y RÁPIDO! Los representantes de apoyo capacitado están disponibles para ayudar",
"support_description_other_links": "Únase a nuestras comunidades o comuníquese con nosotros nuestros socios a través de otros métodos",
"support_description_other_links": "Únete a nuestras comunidades o comunícate con nosotros nuestros socios a través de otros métodos",
"support_title_guides": "Guías de billetera para pastel",
"support_title_live_chat": "Soporte vital",
"support_title_live_chat": "Soporte en tiempo real",
"support_title_other_links": "Otros enlaces de soporte",
"sweeping_wallet": "Billetera de barrido",
"sweeping_wallet": "Barrer billetera (gastar todos los fondos disponibles)",
"sweeping_wallet_alert": "Esto no debería llevar mucho tiempo. NO DEJES ESTA PANTALLA O SE PUEDEN PERDER LOS FONDOS BARRIDOS",
"switchToETHWallet": "Cambie a una billetera Ethereum e inténtelo nuevamente.",
"switchToEVMCompatibleWallet": "Cambie a una billetera compatible con EVM e inténtelo nuevamente (Ethereum, Polygon)",
"switchToETHWallet": "Cambia a una billetera Ethereum e inténtelo nuevamente.",
"switchToEVMCompatibleWallet": "Cambia a una billetera compatible con EVM e inténtelo nuevamente (Ethereum, Polygon)",
"symbol": "Símbolo",
"sync_all_wallets": "Sincronizar todas las billeteras",
"sync_status_attempting_scan": "Intento de escaneo",
@ -759,7 +760,7 @@
"third_intro_content": "Los Yats también viven fuera de Cake Wallet. Cualquier dirección de billetera en la tierra se puede reemplazar con un Yat!",
"third_intro_title": "Yat juega muy bien con otras",
"thorchain_contract_address_not_supported": "Thorchain no admite enviar a una dirección de contrato",
"thorchain_taproot_address_not_supported": "El proveedor de Thorchain no admite las direcciones de Taproot. Cambie la dirección o seleccione un proveedor diferente.",
"thorchain_taproot_address_not_supported": "El proveedor de Thorchain no admite las direcciones de Taproot. Cambia la dirección o selecciona un proveedor diferente.",
"time": "${minutes}m ${seconds}s",
"tip": "Consejo:",
"today": "Hoy",
@ -772,8 +773,8 @@
"tor_only": "solo Tor",
"total": "Total",
"total_saving": "Ahorro Total",
"totp_2fa_failure": "Código incorrecto. Intente con un código diferente o genere una nueva clave secreta. Use una aplicación 2FA compatible que admita códigos de 8 dígitos y SHA512.",
"totp_2fa_success": "¡Éxito! Cake 2FA habilitado para esta billetera. Recuerde guardar su semilla mnemotécnica en caso de que pierda el acceso a la billetera.",
"totp_2fa_failure": "Código incorrecto. Intente con un código diferente o genere una nueva clave secreta. Usa una aplicación 2FA compatible que admita códigos de 8 dígitos y SHA512.",
"totp_2fa_success": "¡Éxito! Cake 2FA habilitado para esta billetera. Recuerda guardar tu semilla mnemotécnica en caso de que pierdas el acceso a la billetera.",
"totp_auth_url": "URL de autenticación TOTP",
"totp_code": "Código TOTP",
"totp_secret_code": "Código secreto TOTP",
@ -826,21 +827,21 @@
"transaction_priority_slow": "Lento",
"transaction_sent": "Transacción enviada!",
"transaction_sent_notice": "Si la pantalla no continúa después de 1 minuto, revisa un explorador de bloques y tu correo electrónico.",
"transactions": "Actas",
"transactions": "Transacciones",
"transactions_by_date": "Transacciones por fecha",
"trongrid_history": "Historia trongrid",
"trusted": "de confianza",
"tx_commit_exception_no_dust_on_change": "La transacción se rechaza con esta cantidad. Con estas monedas puede enviar ${min} sin cambios o ${max} que devuelve el cambio.",
"tx_commit_failed": "La confirmación de transacción falló. Póngase en contacto con el soporte.",
"tx_commit_failed_no_peers": "La transacción no se transmitió, intente nuevamente en un segundo más o menos",
"tx_commit_failed": "La confirmación de transacción falló. Ponte en contacto con el soporte.",
"tx_commit_failed_no_peers": "La transacción no se transmitió, intenta nuevamente en un segundo más o menos",
"tx_invalid_input": "Está utilizando el tipo de entrada incorrecto para este tipo de pago",
"tx_no_dust_exception": "La transacción se rechaza enviando una cantidad demasiado pequeña. Intente aumentar la cantidad.",
"tx_not_enough_inputs_exception": "No hay suficientes entradas disponibles. Seleccione más bajo control de monedas",
"tx_not_enough_inputs_exception": "No hay suficientes entradas disponibles. Selecciona más bajo control de monedas",
"tx_rejected_bip68_final": "La transacción tiene entradas no confirmadas y no ha podido reemplazar por tarifa.",
"tx_rejected_dust_change": "Transacción rechazada por reglas de red, bajo cambio de cambio (polvo). Intente enviar todo o reducir la cantidad.",
"tx_rejected_dust_output": "Transacción rechazada por reglas de red, baja cantidad de salida (polvo). Aumente la cantidad.",
"tx_rejected_dust_change": "Transacción rechazada por reglas de red, bajo cambio de cambio (polvo). Intenta enviar todo o reducir la cantidad.",
"tx_rejected_dust_output": "Transacción rechazada por reglas de red, baja cantidad de salida (polvo). Aumenta la cantidad.",
"tx_rejected_dust_output_send_all": "Transacción rechazada por reglas de red, baja cantidad de salida (polvo). Verifique el saldo de monedas seleccionadas bajo control de monedas.",
"tx_rejected_vout_negative": "No es suficiente saldo para pagar las tarifas de esta transacción. Verifique el saldo de monedas bajo control de monedas.",
"tx_rejected_vout_negative": "No es suficiente saldo para pagar las tarifas de esta transacción. Verifica el saldo de monedas bajo control de monedas.",
"tx_wrong_balance_exception": "No tiene suficiente ${currency} para enviar esta cantidad.",
"tx_wrong_balance_with_amount_exception": "No tiene suficiente ${currency} para enviar la cantidad total de ${amount}",
"tx_zero_fee_exception": "No se puede enviar transacciones con 0 tarifa. Intente aumentar la tasa o verificar su conexión para las últimas estimaciones.",
@ -849,7 +850,7 @@
"unconfirmed": "Saldo no confirmado",
"understand": "Entiendo",
"unlock": "desbloquear",
"unmatched_currencies": "La moneda de su billetera actual no coincide con la del QR escaneado",
"unmatched_currencies": "La moneda de tu billetera actual no coincide con la del QR escaneado",
"unspent_change": "Cambiar",
"unspent_coins_details_title": "Detalles de monedas no gastadas",
"unspent_coins_title": "Monedas no gastadas",
@ -858,11 +859,11 @@
"upto": "hasta ${value}",
"usb": "USB",
"use": "Utilizar a ",
"use_card_info_three": "Utilice la tarjeta digital en línea o con métodos de pago sin contacto.",
"use_card_info_three": "Utiliza la tarjeta digital en línea o con métodos de pago sin contacto.",
"use_card_info_two": "Los fondos se convierten a USD cuando se mantienen en la cuenta prepaga, no en monedas digitales.",
"use_ssl": "Utilice SSL",
"use_ssl": "Utiliza SSL",
"use_suggested": "Usar sugerido",
"use_testnet": "Use TestNet",
"use_testnet": "Usar TestNet",
"value": "Valor",
"value_type": "Tipo de valor",
"variable_pair_not_supported": "Este par de variables no es compatible con los intercambios seleccionados",
@ -871,17 +872,17 @@
"verify_with_2fa": "Verificar con Cake 2FA",
"version": "Versión ${currentVersion}",
"view_all": "Ver todo",
"view_in_block_explorer": "View in Block Explorer",
"view_key_private": "View clave (privado)",
"view_key_public": "View clave (público)",
"view_transaction_on": "View Transaction on ",
"view_in_block_explorer": "Ver en explorador de bloques",
"view_key_private": "Llave de vista (privada)",
"view_key_public": "Llave de vista (pública)",
"view_transaction_on": "Ver transacción en ",
"voting_weight": "Peso de votación",
"waitFewSecondForTxUpdate": "Espere unos segundos para que la transacción se refleje en el historial de transacciones.",
"waitFewSecondForTxUpdate": "Espera unos segundos para que la transacción se refleje en el historial de transacciones.",
"wallet_group": "Grupo de billetera",
"wallet_group_description_four": "para crear una billetera con una semilla completamente nueva.",
"wallet_group_description_four": "Para crear una billetera con una semilla completamente nueva.",
"wallet_group_description_one": "En la billetera de pastel, puedes crear un",
"wallet_group_description_three": "Para ver las billeteras disponibles y/o la pantalla de grupos de billeteras. O elegir",
"wallet_group_description_two": "seleccionando una billetera existente para compartir una semilla con. Cada grupo de billetera puede contener una sola billetera de cada tipo de moneda. \n\n puede seleccionar",
"wallet_group_description_two": "Seleccionando una billetera existente para compartir una semilla con. Cada grupo de billetera puede contener una sola billetera de cada tipo de moneda. \n\n puedes seleccionar",
"wallet_group_empty_state_text_one": "Parece que no tienes ningún grupo de billetera compatible !\n\n toque",
"wallet_group_empty_state_text_two": "a continuación para hacer uno nuevo.",
"wallet_keys": "Billetera semilla/claves",
@ -889,7 +890,7 @@
"wallet_list_edit_group_name": "Editar nombre de grupo",
"wallet_list_edit_wallet": "Editar billetera",
"wallet_list_failed_to_load": "No se pudo cargar ${wallet_name} la billetera. ${error}",
"wallet_list_failed_to_remove": "Error al elimina ${wallet_name} billetera. ${error}",
"wallet_list_failed_to_remove": "Error al eliminar ${wallet_name} billetera. ${error}",
"wallet_list_load_wallet": "Billetera de carga",
"wallet_list_loading_wallet": "Billetera ${wallet_name} de carga",
"wallet_list_removing_wallet": "Retirar ${wallet_name} billetera",
@ -898,8 +899,8 @@
"wallet_list_wallet_name": "Nombre de la billetera",
"wallet_menu": "Menú de billetera",
"wallet_name": "Nombre de la billetera",
"wallet_name_exists": "Wallet con ese nombre ya ha existido",
"wallet_password_is_empty": "La contraseña de billetera está vacía. La contraseña de la billetera no debe estar vacía",
"wallet_name_exists": "Cartera con ese nombre ya existe, escoge otro",
"wallet_password_is_empty": "La contraseña de billetera está vacía. La contraseña de la billetera no puede estar vacía",
"wallet_recovery_height": "Altura de recuperación",
"wallet_restoration_store_incorrect_seed_length": "Longitud de semilla incorrecta",
"wallet_seed": "Semilla de billetera",
@ -913,28 +914,28 @@
"what_is_silent_payments": "¿Qué son los pagos silenciosos?",
"widgets_address": "Dirección",
"widgets_or": "o",
"widgets_restore_from_blockheight": "Restaurar desde blockheight",
"widgets_restore_from_blockheight": "Restaurar desde altura de bloque",
"widgets_restore_from_date": "Restaurar desde fecha",
"widgets_seed": "Semilla",
"wouoldLikeToConnect": "quisiera conectar",
"write_down_backup_password": "Escriba su contraseña de respaldo, que se utiliza para la importación de sus archivos de respaldo.",
"xlm_extra_info": "No olvide especificar el ID de nota al enviar la transacción XLM para el intercambio",
"write_down_backup_password": "Escribe su contraseña de respaldo, que se utiliza para la importación de sus archivos de respaldo.",
"xlm_extra_info": "No olvides especificar el ID de nota al enviar la transacción XLM para el intercambio",
"xmr_available_balance": "Saldo disponible",
"xmr_full_balance": "Balance total",
"xmr_hidden": "Oculto",
"xmr_to_error": "Error de XMR.TO",
"xmr_to_error_description": "Monto invalido. Límite máximo de 8 dígitos después del punto decimal",
"xrp_extra_info": "No olvide especificar la etiqueta de destino al enviar la transacción XRP para el intercambio",
"xrp_extra_info": "No olvides especificar la etiqueta de destino al enviar la transacción XRP para el intercambio",
"yat": "Yat",
"yat_address": "Dirección de Yat",
"yat_alert_content": "Los usuarios de Cake Wallet ahora pueden enviar y recibir todas sus monedas favoritas con un nombre de usuario único basado en emoji.",
"yat_alert_title": "Envíe y reciba criptomonedas más fácilmente con Yat",
"yat_alert_title": "Envía y recibe criptomonedas más fácilmente con Yat",
"yat_error": "Error de Yat",
"yat_error_content": "No hay direcciones vinculadas con este Yat. Prueba con otro Yat",
"yat_popup_content": "Ahora puede enviar y recibir criptografía en Cake Wallet con su Yat, un nombre de usuario corto basado en emoji. Administre Yats en cualquier momento en la pantalla de configuración",
"yat_popup_title": "La dirección de su billetera se puede emojificar.",
"yat_popup_content": "Ahora puede enviar y recibir criptografía en Cake Wallet con su Yat, un nombre de usuario corto basado en emoji. Administra Yats en cualquier momento en la pantalla de configuración",
"yat_popup_title": "La dirección de tu billetera se puede emojificar.",
"yesterday": "Ayer",
"you_now_have_debit_card": "Ahora tiene una tarjeta de débito",
"you_now_have_debit_card": "Ahora tienes una tarjeta de débito",
"you_pay": "Tú pagas",
"you_will_get": "Convertir a",
"you_will_send": "Convertir de",

View file

@ -709,6 +709,7 @@
"silent_payments_always_scan": "Définir les paiements silencieux toujours à la scanne",
"silent_payments_disclaimer": "Les nouvelles adresses ne sont pas de nouvelles identités. Il s'agit d'une réutilisation d'une identité existante avec une étiquette différente.",
"silent_payments_display_card": "Afficher la carte de paiement silencieuse",
"silent_payments_register_key": "Enregistrez la touche Afficher pour une analyse plus rapide",
"silent_payments_scan_from_date": "Analyser à partir de la date",
"silent_payments_scan_from_date_or_blockheight": "Veuillez saisir la hauteur du bloc que vous souhaitez commencer à scanner pour les paiements silencieux entrants, ou utilisez la date à la place. Vous pouvez choisir si le portefeuille continue de numériser chaque bloc ou ne vérifie que la hauteur spécifiée.",
"silent_payments_scan_from_height": "Scan à partir de la hauteur du bloc",

View file

@ -711,6 +711,7 @@
"silent_payments_always_scan": "Saita biya na shiru koyaushe",
"silent_payments_disclaimer": "Sabbin adiresoshin ba sabon tsari bane. Wannan shine sake amfani da asalin asalin tare da wata alama daban.",
"silent_payments_display_card": "Nuna katin silent",
"silent_payments_register_key": "Yi rijista mabuɗin don bincika sauri",
"silent_payments_scan_from_date": "Scan daga kwanan wata",
"silent_payments_scan_from_date_or_blockheight": "Da fatan za a shigar da toshe wurin da kake son fara bincika don biyan silins mai shigowa, ko, yi amfani da kwanan wata. Zaka iya zabar idan walat ɗin ya ci gaba da bincika kowane toshe, ko duba tsinkaye da aka ƙayyade.",
"silent_payments_scan_from_height": "Scan daga tsayin daka",

View file

@ -711,6 +711,7 @@
"silent_payments_always_scan": "मूक भुगतान हमेशा स्कैनिंग सेट करें",
"silent_payments_disclaimer": "नए पते नई पहचान नहीं हैं। यह एक अलग लेबल के साथ एक मौजूदा पहचान का पुन: उपयोग है।",
"silent_payments_display_card": "मूक भुगतान कार्ड दिखाएं",
"silent_payments_register_key": "तेजी से स्कैनिंग के लिए रजिस्टर व्यू कुंजी",
"silent_payments_scan_from_date": "तिथि से स्कैन करना",
"silent_payments_scan_from_date_or_blockheight": "कृपया उस ब्लॉक ऊंचाई दर्ज करें जिसे आप आने वाले मूक भुगतान के लिए स्कैन करना शुरू करना चाहते हैं, या, इसके बजाय तारीख का उपयोग करें। आप चुन सकते हैं कि क्या वॉलेट हर ब्लॉक को स्कैन करना जारी रखता है, या केवल निर्दिष्ट ऊंचाई की जांच करता है।",
"silent_payments_scan_from_height": "ब्लॉक ऊंचाई से स्कैन करें",

View file

@ -709,6 +709,7 @@
"silent_payments_always_scan": "Postavite tiho plaćanje uvijek skeniranje",
"silent_payments_disclaimer": "Nove adrese nisu novi identiteti. To je ponovna upotreba postojećeg identiteta s drugom oznakom.",
"silent_payments_display_card": "Prikaži karticu tihih plaćanja",
"silent_payments_register_key": "Registrirajte ključ za brže skeniranje",
"silent_payments_scan_from_date": "Skeniranje iz datuma",
"silent_payments_scan_from_date_or_blockheight": "Unesite visinu bloka koju želite započeti skeniranje za dolazna tiha plaćanja ili umjesto toga upotrijebite datum. Možete odabrati da li novčanik nastavlja skenirati svaki blok ili provjerava samo navedenu visinu.",
"silent_payments_scan_from_height": "Skeniranje s visine bloka",

View file

@ -701,6 +701,7 @@
"silent_payments_always_scan": "Միացնել Լուռ Վճարումներ մշտական սկանավորումը",
"silent_payments_disclaimer": "Նոր հասցեները նոր ինքնություն չեն։ Դա այլ պիտակով գոյություն ունեցող ինքնության վերագործածում է",
"silent_payments_display_card": "Ցուցադրել Լուռ Վճարումներ քարտը",
"silent_payments_register_key": "Գրանցեք Դիտել ստեղնը `ավելի արագ սկանավորման համար",
"silent_payments_scan_from_date": "Սկանավորել ամսաթվից",
"silent_payments_scan_from_date_or_blockheight": "Խնդրում ենք մուտքագրել բլոկի բարձրությունը, որտեղից դուք ցանկանում եք սկսել սկանավորել մուտքային Լուռ Վճարումները կամ տեղափոխել ամսաթվի փոխարեն։ Դուք կարող եք ընտրել, արդյոք դրամապանակը շարունակելու է սկանավորել ամեն բլոկ կամ ստուգել միայն սահմանված բարձրությունը",
"silent_payments_scan_from_height": "Բլոկի բարձրությունից սկանավորել",

Some files were not shown because too many files have changed in this diff Show more