mirror of
https://github.com/cake-tech/cake_wallet.git
synced 2025-06-28 12:29:51 +00:00
Merge branch 'electrum-sp-refactors' into improve-sending-tx-for-electrum
This commit is contained in:
commit
5b60fb1f0c
119 changed files with 4938 additions and 2736 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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`
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -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 '';
|
||||
}
|
|
@ -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(),
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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)';
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
var label = '';
|
||||
|
||||
switch (this) {
|
||||
case BitcoinCashTransactionPriority.slow:
|
||||
label = 'Slow'; // S.current.transaction_priority_slow;
|
||||
break;
|
||||
case BitcoinCashTransactionPriority.medium:
|
||||
label = 'Medium'; // S.current.transaction_priority_medium;
|
||||
break;
|
||||
case BitcoinCashTransactionPriority.fast:
|
||||
label = 'Fast'; // S.current.transaction_priority_fast;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
factory ElectrumTransactionPriorities.fromList(List<int> list) {
|
||||
if (list.length != 3) {
|
||||
throw Exception(
|
||||
'Unexpected list length: ${list.length} for BitcoinElectrumTransactionPriorities.fromList');
|
||||
}
|
||||
|
||||
return label;
|
||||
return ElectrumTransactionPriorities(
|
||||
slow: list[0],
|
||||
medium: list[1],
|
||||
fast: list[2],
|
||||
custom: 0,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, int> toJson() {
|
||||
return {
|
||||
'slow': slow,
|
||||
'medium': medium,
|
||||
'fast': fast,
|
||||
'custom': custom,
|
||||
};
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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?,
|
||||
);
|
||||
|
||||
@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;
|
||||
bool operator ==(Object o) {
|
||||
if (identical(this, o)) return true;
|
||||
return o is BitcoinUnspent && hash == o.hash && vout == o.vout;
|
||||
}
|
||||
|
||||
String? silentPaymentTweak;
|
||||
String? silentPaymentLabel;
|
||||
@override
|
||||
int get hashCode => Object.hash(hash, vout);
|
||||
}
|
||||
|
|
|
@ -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,28 +214,43 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
|
|||
walletInfo.derivationInfo ??= DerivationInfo();
|
||||
|
||||
// set the default if not present:
|
||||
walletInfo.derivationInfo!.derivationPath ??=
|
||||
snp?.derivationPath ?? electrum_path;
|
||||
walletInfo.derivationInfo!.derivationType ??=
|
||||
snp?.derivationType ?? DerivationType.electrum;
|
||||
walletInfo.derivationInfo!.derivationPath ??= snp?.derivationPath ?? electrum_path;
|
||||
walletInfo.derivationInfo!.derivationType ??= snp?.derivationType ?? DerivationType.electrum;
|
||||
|
||||
Uint8List? seedBytes = null;
|
||||
List<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(
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
));
|
||||
name: name,
|
||||
password: password,
|
||||
passphrase: passphrase,
|
||||
walletInfo: walletInfo,
|
||||
);
|
||||
|
||||
final String mnemonic;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
@ -37,7 +42,7 @@ class BitcoinWalletService extends WalletService<
|
|||
credentials.walletInfo?.network = network.value;
|
||||
|
||||
final String mnemonic;
|
||||
switch ( credentials.walletInfo?.derivationInfo?.derivationType) {
|
||||
switch (credentials.walletInfo?.derivationInfo?.derivationType) {
|
||||
case DerivationType.bip39:
|
||||
final strength = credentials.seedPhraseLength == 24 ? 256 : 128;
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>(
|
||||
id: 'blockchain.tweaks.subscribe',
|
||||
method: 'blockchain.tweaks.subscribe',
|
||||
params: [height, count, false],
|
||||
);
|
||||
}
|
||||
BehaviorSubject<Object>? tweaksSubscribe({required int height, required int count}) =>
|
||||
subscribe<Object>(
|
||||
id: 'blockchain.tweaks.subscribe',
|
||||
method: 'blockchain.tweaks.subscribe',
|
||||
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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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 = [];
|
||||
|
||||
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))) {
|
||||
direction = TransactionDirection.outgoing;
|
||||
inputAddresses.add(addressFromOutputScript(outTransaction.scriptPubKey, network));
|
||||
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(
|
||||
BitcoinAddressUtils.addressFromOutputScript(outTransaction.scriptPubKey, network))) {
|
||||
direction = TransactionDirection.outgoing;
|
||||
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
|
@ -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,33 +65,23 @@ 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,
|
||||
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,
|
||||
type: SilentPaymentsAddresType.p2sp,
|
||||
));
|
||||
}
|
||||
if (silentAddresses.length == 0) {
|
||||
silentAddresses.add(BitcoinSilentPaymentAddressRecord(
|
||||
silentAddress.toString(),
|
||||
labelIndex: 1,
|
||||
name: "",
|
||||
type: SilentPaymentsAddresType.p2sp,
|
||||
));
|
||||
silentAddresses.add(BitcoinSilentPaymentAddressRecord(
|
||||
silentAddress!.toLabeledSilentPaymentAddress(0).toString(),
|
||||
name: "",
|
||||
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(
|
||||
(addressRecord) => addressRecord.address == addr,
|
||||
);
|
||||
final addressRecord = _allAddresses.firstWhere(
|
||||
(addressRecord) => addressRecord.address == addr,
|
||||
);
|
||||
|
||||
previousAddressRecord = addressRecord;
|
||||
receiveAddresses.remove(addressRecord);
|
||||
receiveAddresses.insert(0, addressRecord);
|
||||
previousAddressRecord = 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,
|
||||
addressType: addressPageType,
|
||||
derivationInfo: derivationInfo,
|
||||
),
|
||||
index: newAddressIndex,
|
||||
isHidden: false,
|
||||
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);
|
||||
|
||||
final addressesWithHistory = await Future.wait(newAddresses.map(getAddressHistory));
|
||||
final isLastAddressUsed = addressesWithHistory.last == addressList.last.address;
|
||||
|
||||
if (isLastAddressUsed) {
|
||||
discoverAddresses(addressList, isHidden, getAddressHistory, type: type);
|
||||
}
|
||||
return newAddresses;
|
||||
}
|
||||
|
||||
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;
|
||||
@action
|
||||
Future<void> generateInitialAddresses({required BitcoinAddressType type}) async {
|
||||
for (final derivationType in hdWallets.keys) {
|
||||
if (derivationType == CWBitcoinDerivationType.old && type == SegwitAddresType.p2wpkh) {
|
||||
continue;
|
||||
}
|
||||
});
|
||||
|
||||
if (countOfReceiveAddresses < defaultReceiveAddressesCount) {
|
||||
final addressesCount = defaultReceiveAddressesCount - countOfReceiveAddresses;
|
||||
final newAddresses = await _createNewAddresses(addressesCount,
|
||||
startIndex: countOfReceiveAddresses, isHidden: false, type: type);
|
||||
addAddresses(newAddresses);
|
||||
}
|
||||
final derivationInfo = BitcoinAddressUtils.getDerivationFromType(
|
||||
type,
|
||||
isElectrum: derivationType == CWBitcoinDerivationType.electrum,
|
||||
);
|
||||
|
||||
if (countOfHiddenAddresses < defaultChangeAddressesCount) {
|
||||
final addressesCount = defaultChangeAddressesCount - countOfHiddenAddresses;
|
||||
final newAddresses = await _createNewAddresses(addressesCount,
|
||||
startIndex: countOfHiddenAddresses, isHidden: true, type: type);
|
||||
addAddresses(newAddresses);
|
||||
await discoverAddresses(
|
||||
derivationType: derivationType,
|
||||
isChange: false,
|
||||
type: type,
|
||||
derivationInfo: derivationInfo,
|
||||
);
|
||||
await discoverAddresses(
|
||||
derivationType: derivationType,
|
||||
isChange: true,
|
||||
type: type,
|
||||
derivationInfo: derivationInfo,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
addressType: type ?? addressPageType,
|
||||
derivationInfo: derivationInfo,
|
||||
),
|
||||
index: i,
|
||||
isHidden: isHidden,
|
||||
isChange: isChange,
|
||||
isHidden: derivationType == CWBitcoinDerivationType.old && type != SegwitAddresType.p2wpkh,
|
||||
type: type ?? addressPageType,
|
||||
network: network,
|
||||
derivationInfo: derivationInfo,
|
||||
derivationType: derivationType,
|
||||
);
|
||||
list.add(address);
|
||||
}
|
||||
|
@ -617,13 +665,28 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
|||
return list;
|
||||
}
|
||||
|
||||
@action
|
||||
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) {
|
||||
final addressesSet = this._addresses.toSet();
|
||||
addressesSet.addAll(addresses);
|
||||
this._addresses.clear();
|
||||
this._addresses.addAll(addressesSet);
|
||||
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) {
|
||||
|
|
|
@ -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 = {
|
||||
|
|
794
cw_bitcoin/lib/electrum_worker/electrum_worker.dart
Normal file
794
cw_bitcoin/lib/electrum_worker/electrum_worker.dart
Normal 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);
|
||||
}
|
17
cw_bitcoin/lib/electrum_worker/electrum_worker_methods.dart
Normal file
17
cw_bitcoin/lib/electrum_worker/electrum_worker_methods.dart
Normal 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;
|
||||
}
|
||||
}
|
53
cw_bitcoin/lib/electrum_worker/electrum_worker_params.dart
Normal file
53
cw_bitcoin/lib/electrum_worker/electrum_worker_params.dart
Normal 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};
|
||||
}
|
||||
}
|
56
cw_bitcoin/lib/electrum_worker/methods/broadcast.dart
Normal file
56
cw_bitcoin/lib/electrum_worker/methods/broadcast.dart
Normal 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?,
|
||||
);
|
||||
}
|
||||
}
|
73
cw_bitcoin/lib/electrum_worker/methods/connection.dart
Normal file
73
cw_bitcoin/lib/electrum_worker/methods/connection.dart
Normal 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?,
|
||||
);
|
||||
}
|
||||
}
|
61
cw_bitcoin/lib/electrum_worker/methods/get_balance.dart
Normal file
61
cw_bitcoin/lib/electrum_worker/methods/get_balance.dart
Normal 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?,
|
||||
);
|
||||
}
|
||||
}
|
60
cw_bitcoin/lib/electrum_worker/methods/get_fees.dart
Normal file
60
cw_bitcoin/lib/electrum_worker/methods/get_fees.dart
Normal 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?,
|
||||
);
|
||||
}
|
||||
}
|
120
cw_bitcoin/lib/electrum_worker/methods/get_history.dart
Normal file
120
cw_bitcoin/lib/electrum_worker/methods/get_history.dart
Normal 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?,
|
||||
);
|
||||
}
|
||||
}
|
63
cw_bitcoin/lib/electrum_worker/methods/get_tx_expanded.dart
Normal file
63
cw_bitcoin/lib/electrum_worker/methods/get_tx_expanded.dart
Normal 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?,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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?,
|
||||
);
|
||||
}
|
||||
}
|
60
cw_bitcoin/lib/electrum_worker/methods/list_unspent.dart
Normal file
60
cw_bitcoin/lib/electrum_worker/methods/list_unspent.dart
Normal 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?,
|
||||
);
|
||||
}
|
||||
}
|
22
cw_bitcoin/lib/electrum_worker/methods/methods.dart
Normal file
22
cw_bitcoin/lib/electrum_worker/methods/methods.dart
Normal 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';
|
|
@ -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?,
|
||||
);
|
||||
}
|
||||
}
|
157
cw_bitcoin/lib/electrum_worker/methods/tweaks_subscribe.dart
Normal file
157
cw_bitcoin/lib/electrum_worker/methods/tweaks_subscribe.dart
Normal 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?,
|
||||
);
|
||||
}
|
||||
}
|
52
cw_bitcoin/lib/electrum_worker/methods/version.dart
Normal file
52
cw_bitcoin/lib/electrum_worker/methods/version.dart
Normal 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?,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
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,20 +140,23 @@ 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,
|
||||
required String password,
|
||||
required WalletInfo walletInfo,
|
||||
required Box<UnspentCoinsInfo> unspentCoinsInfo,
|
||||
required EncryptionFileUtils encryptionFileUtils,
|
||||
String? passphrase,
|
||||
String? addressPageType,
|
||||
List<BitcoinAddressRecord>? initialAddresses,
|
||||
List<BitcoinAddressRecord>? initialMwebAddresses,
|
||||
ElectrumBalance? initialBalance,
|
||||
Map<String, int>? initialRegularAddressIndex,
|
||||
Map<String, int>? initialChangeAddressIndex}) async {
|
||||
static Future<LitecoinWallet> create({
|
||||
required String mnemonic,
|
||||
required String password,
|
||||
required WalletInfo walletInfo,
|
||||
required Box<UnspentCoinsInfo> unspentCoinsInfo,
|
||||
required EncryptionFileUtils encryptionFileUtils,
|
||||
String? passphrase,
|
||||
String? addressPageType,
|
||||
List<BitcoinAddressRecord>? initialAddresses,
|
||||
List<BitcoinAddressRecord>? initialMwebAddresses,
|
||||
ElectrumBalance? initialBalance,
|
||||
Map<String, int>? initialRegularAddressIndex,
|
||||
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 = {};
|
||||
|
||||
// update unspent balances:
|
||||
await updateUnspent();
|
||||
// await Future.wait(LITECOIN_ADDRESS_TYPES
|
||||
// .map((type) => fetchTransactionsForAddressType(historiesWithDetails, type)));
|
||||
|
||||
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 (_) {}
|
||||
|
||||
for (var addressRecord in walletAddresses.allAddresses) {
|
||||
addressRecord.balance = 0;
|
||||
addressRecord.txCount = 0;
|
||||
}
|
||||
|
||||
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,
|
||||
);
|
||||
// return historiesWithDetails;
|
||||
// } catch (e) {
|
||||
// print("fetchTransactions $e");
|
||||
// return {};
|
||||
// }
|
||||
}
|
||||
|
||||
// @override
|
||||
// @action
|
||||
// Future<void> subscribeForUpdates([
|
||||
// Iterable<BitcoinAddressRecord>? unsubscribedScriptHashes,
|
||||
// ]) async {
|
||||
// final unsubscribedScriptHashes = walletAddresses.allAddresses.where(
|
||||
// (address) =>
|
||||
// !scripthashesListening.contains(address.scriptHash) &&
|
||||
// address.type != SegwitAddresType.mweb,
|
||||
// );
|
||||
|
||||
// return super.subscribeForUpdates(unsubscribedScriptHashes);
|
||||
// }
|
||||
|
||||
// @override
|
||||
// Future<ElectrumBalance> fetchBalances() async {
|
||||
// final balance = await super.fetchBalances();
|
||||
|
||||
// if (!mwebEnabled) {
|
||||
// return balance;
|
||||
// }
|
||||
|
||||
// // update unspent balances:
|
||||
// await updateUnspent();
|
||||
|
||||
// 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 (_) {}
|
||||
|
||||
// for (var addressRecord in walletAddresses.allAddresses) {
|
||||
// addressRecord.balance = 0;
|
||||
// addressRecord.txCount = 0;
|
||||
// }
|
||||
|
||||
// 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(),
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -928,14 +941,14 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
|
|||
var feeIncrease = posOutputSum - expectedPegin;
|
||||
if (expectedPegin > 0 && fee == BigInt.zero) {
|
||||
feeIncrease += await super.calcFee(
|
||||
utxos: posUtxos,
|
||||
outputs: tx.outputs
|
||||
.map((output) =>
|
||||
BitcoinScriptOutput(script: output.scriptPubKey, value: output.amount))
|
||||
.toList(),
|
||||
network: network,
|
||||
memo: memo,
|
||||
feeRate: feeRate) +
|
||||
utxos: posUtxos,
|
||||
outputs: tx.outputs
|
||||
.map((output) =>
|
||||
BitcoinScriptOutput(script: output.scriptPubKey, value: output.amount))
|
||||
.toList(),
|
||||
memo: memo,
|
||||
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,10 +1002,9 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
|
|||
|
||||
bool isPegIn = !hasMwebInput && hasMwebOutput;
|
||||
bool isRegular = !hasMwebInput && !hasMwebOutput;
|
||||
tx.changeAddressOverride =
|
||||
(await (walletAddresses as LitecoinWalletAddresses)
|
||||
.getChangeAddress(isPegIn: isPegIn || isRegular))
|
||||
.address;
|
||||
tx.changeAddressOverride = (await (walletAddresses as LitecoinWalletAddresses)
|
||||
.getChangeAddress(isPegIn: isPegIn || isRegular))
|
||||
.address;
|
||||
if (!hasMwebInput && !hasMwebOutput) {
|
||||
tx.isMweb = false;
|
||||
return tx;
|
||||
|
@ -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())))
|
||||
.toList(),
|
||||
changePath: changePath,
|
||||
sigHashType: 0x01,
|
||||
additionals: ["bech32"],
|
||||
isSegWit: true,
|
||||
useTrustedInputForSegwit: true
|
||||
);
|
||||
inputs: readyInputs,
|
||||
outputs: outputs
|
||||
.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);
|
||||
|
||||
return BtcTransaction.fromRaw(rawHex);
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
e.value,
|
||||
index: e.key,
|
||||
type: SegwitAddresType.mweb,
|
||||
network: network,
|
||||
))
|
||||
.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({
|
||||
required int index,
|
||||
required Bip32Slip10Secp256k1 hd,
|
||||
BitcoinAddressType? addressType,
|
||||
}) {
|
||||
if (addressType == SegwitAddresType.mweb) {
|
||||
return hd == sideHd ? mwebAddrs[0] : mwebAddrs[index + 1];
|
||||
@override
|
||||
BitcoinBaseAddress generateAddress({
|
||||
required CWBitcoinDerivationType derivationType,
|
||||
required bool isChange,
|
||||
required int index,
|
||||
required BitcoinAddressType addressType,
|
||||
required BitcoinDerivationInfo derivationInfo,
|
||||
}) {
|
||||
if (addressType == SegwitAddresType.mweb) {
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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,46 +38,54 @@ 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,
|
||||
walletInfo: walletInfo,
|
||||
unspentCoinsInfo: unspentCoinsInfo,
|
||||
network: BitcoinCashNetwork.mainnet,
|
||||
initialAddresses: initialAddresses,
|
||||
initialBalance: initialBalance,
|
||||
seedBytes: seedBytes,
|
||||
currency: CryptoCurrency.bch,
|
||||
encryptionFileUtils: encryptionFileUtils,
|
||||
passphrase: passphrase) {
|
||||
mnemonic: mnemonic,
|
||||
password: password,
|
||||
walletInfo: walletInfo,
|
||||
unspentCoinsInfo: unspentCoinsInfo,
|
||||
network: BitcoinCashNetwork.mainnet,
|
||||
initialAddresses: initialAddresses,
|
||||
initialBalance: initialBalance,
|
||||
seedBytes: seedBytes,
|
||||
currency: CryptoCurrency.bch,
|
||||
encryptionFileUtils: encryptionFileUtils,
|
||||
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,
|
||||
required String password,
|
||||
required WalletInfo walletInfo,
|
||||
required Box<UnspentCoinsInfo> unspentCoinsInfo,
|
||||
required EncryptionFileUtils encryptionFileUtils,
|
||||
String? passphrase,
|
||||
String? addressPageType,
|
||||
List<BitcoinAddressRecord>? initialAddresses,
|
||||
ElectrumBalance? initialBalance,
|
||||
Map<String, int>? initialRegularAddressIndex,
|
||||
Map<String, int>? initialChangeAddressIndex}) async {
|
||||
@override
|
||||
BitcoinCashNetwork get network => BitcoinCashNetwork.mainnet;
|
||||
|
||||
static Future<BitcoinCashWallet> create({
|
||||
required String mnemonic,
|
||||
required String password,
|
||||
required WalletInfo walletInfo,
|
||||
required Box<UnspentCoinsInfo> unspentCoinsInfo,
|
||||
required EncryptionFileUtils encryptionFileUtils,
|
||||
String? passphrase,
|
||||
String? addressPageType,
|
||||
List<BitcoinAddressRecord>? initialAddresses,
|
||||
ElectrumBalance? initialBalance,
|
||||
Map<String, int>? initialRegularAddressIndex,
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
@ -92,11 +101,13 @@ class BitcoinCashWalletService extends WalletService<
|
|||
final currentWalletInfo = walletInfoSource.values
|
||||
.firstWhereOrNull((info) => info.id == WalletBase.idFor(currentName, getType()))!;
|
||||
final currentWallet = await BitcoinCashWalletBase.open(
|
||||
password: password,
|
||||
name: currentName,
|
||||
walletInfo: currentWalletInfo,
|
||||
unspentCoinsInfo: unspentCoinsInfoSource,
|
||||
encryptionFileUtils: encryptionFileUtilsFor(isDirect));
|
||||
password: password,
|
||||
name: currentName,
|
||||
walletInfo: currentWalletInfo,
|
||||
unspentCoinsInfo: unspentCoinsInfoSource,
|
||||
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
|
||||
mempoolAPIEnabled: mempoolAPIEnabled,
|
||||
);
|
||||
|
||||
await currentWallet.renameWalletFiles(newName);
|
||||
await saveBackup(newName);
|
||||
|
@ -128,12 +139,13 @@ class BitcoinCashWalletService extends WalletService<
|
|||
}
|
||||
|
||||
final wallet = await BitcoinCashWalletBase.create(
|
||||
password: credentials.password!,
|
||||
mnemonic: credentials.mnemonic,
|
||||
walletInfo: credentials.walletInfo!,
|
||||
unspentCoinsInfo: unspentCoinsInfoSource,
|
||||
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
|
||||
passphrase: credentials.passphrase
|
||||
password: credentials.password!,
|
||||
mnemonic: credentials.mnemonic,
|
||||
walletInfo: credentials.walletInfo!,
|
||||
unspentCoinsInfo: unspentCoinsInfoSource,
|
||||
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
|
||||
passphrase: credentials.passphrase,
|
||||
mempoolAPIEnabled: mempoolAPIEnabled,
|
||||
);
|
||||
await wallet.save();
|
||||
await wallet.init();
|
||||
|
|
|
@ -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,15 +74,16 @@ class PendingBitcoinCashTransaction with PendingTransaction {
|
|||
void addListener(void Function(ElectrumTransactionInfo transaction) listener) =>
|
||||
_listeners.add(listener);
|
||||
|
||||
ElectrumTransactionInfo transactionInfo() => ElectrumTransactionInfo(type,
|
||||
id: id,
|
||||
height: 0,
|
||||
amount: amount,
|
||||
direction: TransactionDirection.outgoing,
|
||||
date: DateTime.now(),
|
||||
isPending: true,
|
||||
confirmations: 0,
|
||||
fee: fee,
|
||||
isReplaced: false,
|
||||
ElectrumTransactionInfo transactionInfo() => ElectrumTransactionInfo(
|
||||
type,
|
||||
id: id,
|
||||
height: 0,
|
||||
amount: amount,
|
||||
direction: TransactionDirection.outgoing,
|
||||
date: DateTime.now(),
|
||||
isPending: true,
|
||||
confirmations: 0,
|
||||
fee: fee,
|
||||
isReplaced: false,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
|
@ -189,15 +192,15 @@ class WalletInfo extends HiveObject {
|
|||
|
||||
@HiveField(22)
|
||||
String? parentAddress;
|
||||
|
||||
|
||||
@HiveField(23)
|
||||
List<String>? hiddenAddresses;
|
||||
|
||||
@HiveField(24)
|
||||
List<String>? manualAddresses;
|
||||
|
||||
|
||||
|
||||
@HiveField(25)
|
||||
List<DerivationInfo>? derivations;
|
||||
|
||||
String get yatLastUsedAddress => yatLastUsedAddressRaw ?? '';
|
||||
|
||||
|
|
|
@ -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 (_) {}
|
||||
}
|
||||
|
||||
|
|
|
@ -23,6 +23,8 @@ abstract class HavenWalletAddressesBase extends WalletAddressesWithAccount<Accou
|
|||
@observable
|
||||
String address;
|
||||
|
||||
String get primaryAddress => address;
|
||||
|
||||
// @override
|
||||
@observable
|
||||
Account? account;
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
/home/rafael/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/
|
|
@ -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
|
||||
|
|
|
@ -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 ?? '');
|
||||
}
|
||||
var electrumSeedBytes;
|
||||
try {
|
||||
electrumSeedBytes = ElectrumV2SeedGenerator.generateFromString(mnemonic, passphrase);
|
||||
} catch (e) {
|
||||
print("electrum_v2 seed error: $e");
|
||||
|
||||
for (DerivationInfo dInfo in electrum_derivations[dType]!) {
|
||||
if (passphrase != null && passphrase.isEmpty) {
|
||||
try {
|
||||
DerivationInfo dInfoCopy = DerivationInfo(
|
||||
derivationType: dInfo.derivationType,
|
||||
derivationPath: dInfo.derivationPath,
|
||||
description: dInfo.description,
|
||||
scriptType: dInfo.scriptType,
|
||||
);
|
||||
|
||||
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");
|
||||
// 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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
170
lib/di.dart
170
lib/di.dart
|
@ -367,14 +367,14 @@ Future<void> setup({
|
|||
(WalletType type) => getIt.get<WalletService>(param1: type)));
|
||||
|
||||
getIt.registerFactoryParam<WalletNewVM, NewWalletArguments, void>(
|
||||
(newWalletArgs, _) => WalletNewVM(
|
||||
getIt.get<AppStore>(),
|
||||
getIt.get<WalletCreationService>(param1:newWalletArgs.type),
|
||||
_walletInfoSource,
|
||||
getIt.get<AdvancedPrivacySettingsViewModel>(param1: newWalletArgs.type),
|
||||
getIt.get<SeedSettingsViewModel>(),
|
||||
newWalletArguments: newWalletArgs,));
|
||||
|
||||
(newWalletArgs, _) => WalletNewVM(
|
||||
getIt.get<AppStore>(),
|
||||
getIt.get<WalletCreationService>(param1: newWalletArgs.type),
|
||||
_walletInfoSource,
|
||||
getIt.get<AdvancedPrivacySettingsViewModel>(param1: newWalletArgs.type),
|
||||
getIt.get<SeedSettingsViewModel>(),
|
||||
newWalletArguments: newWalletArgs,
|
||||
));
|
||||
|
||||
getIt.registerFactory<NewWalletTypeViewModel>(() => NewWalletTypeViewModel(_walletInfoSource));
|
||||
|
||||
|
@ -397,62 +397,52 @@ Future<void> setup({
|
|||
);
|
||||
|
||||
getIt.registerFactoryParam<WalletUnlockPage, WalletUnlockArguments, bool>((args, closable) {
|
||||
return WalletUnlockPage(
|
||||
getIt.get<WalletUnlockLoadableViewModel>(param1: args),
|
||||
args.callback,
|
||||
args.authPasswordHandler,
|
||||
closable: closable);
|
||||
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, _) {
|
||||
if (successful) {
|
||||
final authStore = getIt.get<AuthenticationStore>();
|
||||
authStore.allowed();
|
||||
}}),
|
||||
param2: false,
|
||||
instanceName: 'wallet_unlock_loadable'),
|
||||
instanceName: 'wallet_password_login');
|
||||
() => getIt.get<WalletUnlockPage>(
|
||||
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,
|
||||
args.authPasswordHandler,
|
||||
closable: closable);
|
||||
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>(),
|
||||
walletName: args.walletName ?? currentWalletName,
|
||||
walletType: args.walletType ?? currentWalletType);
|
||||
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>(),
|
||||
walletName: args.walletName ?? currentWalletName,
|
||||
walletType: args.walletType ?? currentWalletType);
|
||||
return WalletUnlockVerifiableViewModel(getIt.get<AppStore>(),
|
||||
walletName: args.walletName ?? currentWalletName,
|
||||
walletType: args.walletType ?? currentWalletType);
|
||||
});
|
||||
|
||||
getIt.registerFactoryParam<WalletRestorationFromQRVM, WalletType, void>((WalletType type, _) =>
|
||||
|
@ -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,40 +1099,36 @@ Future<void> setup({
|
|||
param1: derivations,
|
||||
)));
|
||||
|
||||
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!;
|
||||
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!;
|
||||
|
||||
return TransactionDetailsViewModel(
|
||||
transactionInfo: transactionInfo,
|
||||
transactionDescriptionBox: _transactionDescriptionBox,
|
||||
wallet: wallet,
|
||||
settingsStore: getIt.get<SettingsStore>(),
|
||||
sendViewModel: getIt.get<SendViewModel>(),
|
||||
canReplaceByFee: canReplaceByFee,
|
||||
);
|
||||
}
|
||||
);
|
||||
return TransactionDetailsViewModel(
|
||||
transactionInfo: transactionInfo,
|
||||
transactionDescriptionBox: _transactionDescriptionBox,
|
||||
wallet: wallet,
|
||||
settingsStore: getIt.get<SettingsStore>(),
|
||||
sendViewModel: getIt.get<SendViewModel>(),
|
||||
canReplaceByFee: canReplaceByFee,
|
||||
);
|
||||
});
|
||||
|
||||
getIt.registerFactoryParam<TransactionDetailsPage, TransactionInfo, void>(
|
||||
(TransactionInfo transactionInfo, _) => TransactionDetailsPage(
|
||||
transactionDetailsViewModel: getIt.get<TransactionDetailsViewModel>(
|
||||
param1: [transactionInfo, false])));
|
||||
(TransactionInfo transactionInfo, _) => TransactionDetailsPage(
|
||||
transactionDetailsViewModel:
|
||||
getIt.get<TransactionDetailsViewModel>(param1: [transactionInfo, false])));
|
||||
|
||||
getIt.registerFactoryParam<RBFDetailsPage, List<dynamic>, void>(
|
||||
(params, _) {
|
||||
final transactionInfo = params[0] as TransactionInfo;
|
||||
final txHex = params[1] as String;
|
||||
return RBFDetailsPage(
|
||||
transactionDetailsViewModel: getIt.get<TransactionDetailsViewModel>(
|
||||
param1: [transactionInfo, true],
|
||||
),
|
||||
rawTransaction: txHex,
|
||||
);
|
||||
}
|
||||
);
|
||||
getIt.registerFactoryParam<RBFDetailsPage, List<dynamic>, void>((params, _) {
|
||||
final transactionInfo = params[0] as TransactionInfo;
|
||||
final txHex = params[1] as String;
|
||||
return RBFDetailsPage(
|
||||
transactionDetailsViewModel: getIt.get<TransactionDetailsViewModel>(
|
||||
param1: [transactionInfo, true],
|
||||
),
|
||||
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>()));
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -72,24 +72,52 @@ class AddressPage extends BasePage {
|
|||
|
||||
bool isMobileView = responsiveLayoutUtil.shouldRenderMobileUI;
|
||||
|
||||
return 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),
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
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: !isMobileView ? _closeButton : _backButton,
|
||||
),
|
||||
),
|
||||
onPressed: () => onClose(context),
|
||||
child: !isMobileView ? _closeButton : _backButton,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -150,13 +178,13 @@ class AddressPage extends BasePage {
|
|||
Expanded(
|
||||
child: Observer(
|
||||
builder: (_) => QRWidget(
|
||||
formKey: _formKey,
|
||||
addressListViewModel: addressListViewModel,
|
||||
amountTextFieldFocusNode: _cryptoAmountFocus,
|
||||
amountController: _amountController,
|
||||
isLight: dashboardViewModel.settingsStore.currentTheme.type ==
|
||||
ThemeType.light,
|
||||
))),
|
||||
formKey: _formKey,
|
||||
addressListViewModel: addressListViewModel,
|
||||
amountTextFieldFocusNode: _cryptoAmountFocus,
|
||||
amountController: _amountController,
|
||||
isLight: dashboardViewModel.settingsStore.currentTheme.type ==
|
||||
ThemeType.light,
|
||||
))),
|
||||
SizedBox(height: 16),
|
||||
Observer(builder: (_) {
|
||||
if (addressListViewModel.hasAddressList) {
|
||||
|
|
|
@ -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';
|
||||
|
@ -116,13 +102,13 @@ class ReceivePage extends BasePage {
|
|||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(24, 50, 24, 24),
|
||||
child: QRWidget(
|
||||
addressListViewModel: addressListViewModel,
|
||||
formKey: _formKey,
|
||||
heroTag: _heroTag,
|
||||
amountTextFieldFocusNode: _cryptoAmountFocus,
|
||||
amountController: _amountController,
|
||||
isLight: currentTheme.type == ThemeType.light,
|
||||
),
|
||||
addressListViewModel: addressListViewModel,
|
||||
formKey: _formKey,
|
||||
heroTag: _heroTag,
|
||||
amountTextFieldFocusNode: _cryptoAmountFocus,
|
||||
amountController: _amountController,
|
||||
isLight: currentTheme.type == ThemeType.light,
|
||||
),
|
||||
),
|
||||
AddressList(addressListViewModel: addressListViewModel),
|
||||
Padding(
|
||||
|
|
|
@ -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,9 +127,10 @@ class _AddressListState extends State<AddressList> {
|
|||
showTrailingButton: widget.addressListViewModel.showAddManualAddresses,
|
||||
showSearchButton: true,
|
||||
onSearchCallback: updateItems,
|
||||
trailingButtonTap: () => Navigator.of(context).pushNamed(Routes.newSubaddress).then((value) {
|
||||
updateItems(); // refresh the new address
|
||||
}),
|
||||
trailingButtonTap: () =>
|
||||
Navigator.of(context).pushNamed(Routes.newSubaddress).then((value) {
|
||||
updateItems(); // refresh the new address
|
||||
}),
|
||||
trailingIcon: Icon(
|
||||
Icons.add,
|
||||
size: 20,
|
||||
|
@ -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,
|
||||
// hasReceived:
|
||||
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,9 +174,11 @@ class _AddressListState extends State<AddressList> {
|
|||
widget.addressListViewModel.setAddress(item);
|
||||
},
|
||||
onEdit: editable
|
||||
? () => Navigator.of(context).pushNamed(Routes.newSubaddress, arguments: item).then((value) {
|
||||
updateItems(); // refresh the new address
|
||||
})
|
||||
? () => Navigator.of(context)
|
||||
.pushNamed(Routes.newSubaddress, arguments: item)
|
||||
.then((value) {
|
||||
updateItems(); // refresh the new address
|
||||
})
|
||||
: null,
|
||||
isHidden: item.isHidden,
|
||||
onHide: () => _hideAddress(item),
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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,39 +385,43 @@ class WalletRestorePage extends BasePage {
|
|||
|
||||
walletRestoreViewModel.state = IsExecutingState();
|
||||
|
||||
DerivationInfo? dInfo;
|
||||
|
||||
// get info about the different derivations:
|
||||
List<DerivationInfo> derivations =
|
||||
await walletRestoreViewModel.getDerivationInfo(_credentials());
|
||||
|
||||
int derivationsWithHistory = 0;
|
||||
int derivationWithHistoryIndex = 0;
|
||||
for (int i = 0; i < derivations.length; i++) {
|
||||
if (derivations[i].transactionsCount > 0) {
|
||||
derivationsWithHistory++;
|
||||
derivationWithHistoryIndex = i;
|
||||
if (walletRestoreViewModel.type == WalletType.nano) {
|
||||
DerivationInfo? dInfo;
|
||||
|
||||
int derivationsWithHistory = 0;
|
||||
int derivationWithHistoryIndex = 0;
|
||||
for (int i = 0; i < derivations.length; i++) {
|
||||
if (derivations[i].transactionsCount > 0) {
|
||||
derivationsWithHistory++;
|
||||
derivationWithHistoryIndex = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (derivationsWithHistory > 1) {
|
||||
dInfo = await Navigator.of(context).pushNamed(
|
||||
Routes.restoreWalletChooseDerivation,
|
||||
arguments: derivations,
|
||||
) as DerivationInfo?;
|
||||
} else if (derivationsWithHistory == 1) {
|
||||
dInfo = derivations[derivationWithHistoryIndex];
|
||||
} else if (derivations.length == 1) {
|
||||
// we only return 1 derivation if we're pretty sure we know which one to use:
|
||||
dInfo = derivations.first;
|
||||
if (derivationsWithHistory > 1) {
|
||||
dInfo = await Navigator.of(context).pushNamed(
|
||||
Routes.restoreWalletChooseDerivation,
|
||||
arguments: derivations,
|
||||
) as DerivationInfo?;
|
||||
} else if (derivationsWithHistory == 1) {
|
||||
dInfo = derivations[derivationWithHistoryIndex];
|
||||
} else if (derivations.length == 1) {
|
||||
// we only return 1 derivation if we're pretty sure we know which one to use:
|
||||
dInfo = derivations.first;
|
||||
} else {
|
||||
// if we have multiple possible derivations, and none (or multiple) have histories
|
||||
// we just default to the most common one:
|
||||
dInfo = walletRestoreViewModel.getCommonRestoreDerivation();
|
||||
}
|
||||
|
||||
this.derivationInfo = dInfo;
|
||||
} else {
|
||||
// if we have multiple possible derivations, and none (or multiple) have histories
|
||||
// we just default to the most common one:
|
||||
dInfo = walletRestoreViewModel.getCommonRestoreDerivation();
|
||||
this.derivations = derivations;
|
||||
}
|
||||
|
||||
this.derivationInfo = dInfo;
|
||||
|
||||
await walletRestoreViewModel.create(options: _credentials());
|
||||
seedSettingsViewModel.setPassphrase(null);
|
||||
} catch (e) {
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -52,7 +52,9 @@ class BottomSheetListenerState extends State<BottomSheetListener> {
|
|||
);
|
||||
},
|
||||
);
|
||||
item.completer.complete(value);
|
||||
if (!item.completer.isCompleted) {
|
||||
item.completer.complete(value);
|
||||
}
|
||||
widget.bottomSheetService.resetCurrentSheet();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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'],
|
||||
|
|
|
@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -90,11 +90,12 @@ abstract class DashboardViewModelBase with Store {
|
|||
value: () => transactionFilterStore.displayOutgoing,
|
||||
caption: S.current.outgoing,
|
||||
onChanged: transactionFilterStore.toggleOutgoing),
|
||||
FilterItem(
|
||||
value: () => transactionFilterStore.displaySilentPayments,
|
||||
caption: S.current.silent_payments,
|
||||
onChanged: transactionFilterStore.toggleSilentPayments,
|
||||
),
|
||||
if (appStore.wallet!.type == WalletType.bitcoin)
|
||||
FilterItem(
|
||||
value: () => transactionFilterStore.displaySilentPayments,
|
||||
caption: S.current.silent_payments,
|
||||
onChanged: transactionFilterStore.toggleSilentPayments,
|
||||
),
|
||||
// FilterItem(
|
||||
// value: () => false,
|
||||
// caption: S.current.transactions_by_date,
|
||||
|
@ -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;
|
||||
|
|
|
@ -38,7 +38,7 @@ abstract class WalletRestorationFromQRVMBase extends WalletCreationVM with Store
|
|||
wif = '',
|
||||
address = '',
|
||||
super(appStore, walletInfoSource, walletCreationService, seedSettingsViewModel,
|
||||
type: type, isRecovery: true);
|
||||
type: type, isRecovery: true);
|
||||
|
||||
@observable
|
||||
int height;
|
||||
|
@ -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:
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -83,18 +83,19 @@ abstract class TransactionDetailsViewModelBase with Store {
|
|||
break;
|
||||
}
|
||||
|
||||
if (showRecipientAddress && !isRecipientAddressShown) {
|
||||
try {
|
||||
final recipientAddress = transactionDescriptionBox.values
|
||||
.firstWhere((val) => val.id == transactionInfo.txHash)
|
||||
.recipientAddress;
|
||||
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 (recipientAddress?.isNotEmpty ?? false) {
|
||||
items.add(StandartListItem(
|
||||
title: S.current.transaction_details_recipient_address, value: recipientAddress!));
|
||||
}
|
||||
} catch (_) {
|
||||
// FIX-ME: Unhandled exception
|
||||
if (showRecipientAddress && !isRecipientAddressShown) {
|
||||
final recipientAddress = description.recipientAddress;
|
||||
|
||||
if (recipientAddress?.isNotEmpty ?? false) {
|
||||
items.add(StandartListItem(
|
||||
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,
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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?;
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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": "فحص من ارتفاع الكتلة",
|
||||
|
|
|
@ -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": "Сканиране от височината на блока",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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,30 +914,30 @@
|
|||
"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",
|
||||
"yy": "YY"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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": "ब्लॉक ऊंचाई से स्कैन करें",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue