Merge remote-tracking branch 'origin/main'

This commit is contained in:
OmarHatem
2025-07-29 17:16:18 +03:00
174 changed files with 2039 additions and 682 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 156 KiB

After

Width:  |  Height:  |  Size: 351 KiB

+1 -1
View File
@@ -9,7 +9,7 @@ jobs:
PR_test_build:
runs-on: linux-amd64
container:
image: ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.0-go1.24.1-ruststablenightly
image: ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.4-ndkr28-go1.24.1-ruststablenightly
env:
STORE_PASS: test@cake_wallet
KEY_PASS: test@cake_wallet
+1 -1
View File
@@ -9,7 +9,7 @@ jobs:
PR_test_build:
runs-on: linux-amd64
container:
image: ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.0-go1.24.1-ruststablenightly
image: ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.4-ndkr28-go1.24.1-ruststablenightly
env:
STORE_PASS: test@cake_wallet
KEY_PASS: test@cake_wallet
+5 -4
View File
@@ -1,4 +1,4 @@
# docker buildx build --push --pull --platform linux/amd64,linux/arm64 . -f Dockerfile -t ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.0-go1.24.1-ruststablenightly
# docker buildx build --push --pull --platform linux/amd64,linux/arm64 . -f Dockerfile -t ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.4-ndkr28-go1.24.1-ruststablenightly
# Heavily inspired by cirrusci images
# https://github.com/cirruslabs/docker-images-android/blob/master/sdk/tools/Dockerfile
@@ -15,7 +15,7 @@ LABEL org.opencontainers.image.source=https://github.com/cake-tech/cake_wallet
ENV GOLANG_VERSION=1.24.1
# Pin Flutter version to latest known-working version
ENV FLUTTER_VERSION=3.27.0
ENV FLUTTER_VERSION=3.27.4
# Pin Android Studio, platform, and build tools versions to latest known-working version
# Comes from https://developer.android.com/studio/#command-tools
@@ -138,11 +138,12 @@ RUN ARCH=$(uname -m) && \
"build-tools;35.0.0"
# Install extra NDK dependency for sp_scanner
ENV ANDROID_NDK_VERSION=27.2.12479018
ENV ANDROID_NDK_VERSION=28.2.13676358
RUN ARCH=$(uname -m) && \
if [ "$ARCH" != "x86_64" ]; then exit 0; fi \
&& yes | sdkmanager "ndk;$ANDROID_NDK_VERSION" \
"ndk;27.0.12077973"
"ndk;27.0.12077973" \
"ndk;27.2.12479018"
# Install dependencies for tests
# Comes from https://github.com/ReactiveCircus/android-emulator-runner
+1 -1
View File
@@ -94,7 +94,7 @@ android {
}
}
ndkVersion "27.0.12077973"
ndkVersion "28.2.13676358"
}
flutter {
+2 -1
View File
@@ -2,4 +2,5 @@
uri: kaliumapi.appditto.com
path: /api
useSSL: true
is_default: true
is_default: true
isEnabledForAutoSwitching: true
@@ -2,9 +2,11 @@
uri: bitcoincash.stackwallet.com:50002
is_default: true
useSSL: true
isEnabledForAutoSwitching: true
-
uri: bch.aftrek.org:50002
useSSL: true
isEnabledForAutoSwitching: true
-
uri: node.minisatoshi.cash:50002
useSSL: true
+6
View File
@@ -2,8 +2,14 @@
uri: btc-electrum.cakewallet.com:50002
useSSL: true
isDefault: true
isEnabledForAutoSwitching: true
-
uri: electrs.cakewallet.com:50001
-
uri: fulcrum.sethforprivacy.com:50002
useSSL: true
isEnabledForAutoSwitching: true
-
uri: electrum.cakewallet.com:50002
useSSL: true
isEnabledForAutoSwitching: true
+3 -1
View File
@@ -1,6 +1,8 @@
-
uri: default-spv-nodes
is_default: true
isEnabledForAutoSwitching: true
-
uri: dcrd.sethforprivacy.com:9108
useSSL: true
useSSL: true
isEnabledForAutoSwitching: true
+2
View File
@@ -2,6 +2,7 @@
uri: ethereum-rpc.publicnode.com
useSSL: true
isDefault: true
isEnabledForAutoSwitching: true
-
uri: eth.llamarpc.com
-
@@ -10,5 +11,6 @@
uri: eth-mainnet.public.blastapi.io
-
uri: eth.nownodes.io
isEnabledForAutoSwitching: true
-
uri: ethereum.publicnode.com
Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

+2
View File
@@ -2,9 +2,11 @@
uri: ltc-electrum.cakewallet.com:50002
useSSL: true
isDefault: true
isEnabledForAutoSwitching: true
-
uri: litecoin.stackwallet.com:20063
useSSL: true
isEnabledForAutoSwitching: true
-
uri: electrum-ltc.bysh.me:50002
useSSL: true
+3
View File
@@ -1,14 +1,17 @@
-
uri: nano.nownodes.io
useSSL: true
isEnabledForAutoSwitching: true
-
uri: rpc.nano.to
is_default: true
useSSL: true
isEnabledForAutoSwitching: true
-
uri: node.nautilus.io
path: /api
useSSL: true
isEnabledForAutoSwitching: true
-
uri: app.natrium.io
path: /api
+3
View File
@@ -2,8 +2,11 @@
uri: rpc.nano.to
useSSL: true
is_default: true
isEnabledForAutoSwitching: true
-
uri: workers.perish.co
isEnabledForAutoSwitching: true
-
uri: worker.nanoriver.cc
useSSL: true
isEnabledForAutoSwitching: true
+3
View File
@@ -3,6 +3,7 @@
is_default: true
trusted: true
useSSL: true
isEnabledForAutoSwitching: true
-
uri: cakexmrl7bonq7ovjka5kuwuyd3f7qnkz6z6s6dmsy3uckwra7bvggyd.onion:18081
is_default: false
@@ -10,6 +11,8 @@
uri: node.sethforprivacy.com:443
useSSL: true
is_default: false
isEnabledForAutoSwitching: true
-
uri: nodes.hashvault.pro:18081
is_default: false
isEnabledForAutoSwitching: true
+3 -1
View File
@@ -4,7 +4,9 @@
uri: polygon-bor-rpc.publicnode.com
useSSL: true
isDefault: true
isEnabledForAutoSwitching: true
-
uri: polygon.llamarpc.com
-
uri: matic.nownodes.io
uri: matic.nownodes.io
isEnabledForAutoSwitching: true
+4 -1
View File
@@ -4,10 +4,13 @@
-
uri: api.mainnet-beta.solana.com:443
useSSL: true
isEnabledForAutoSwitching: true
-
uri: solana-rpc.publicnode.com:443
useSSL: true
isEnabledForAutoSwitching: true
-
uri: solana-mainnet.core.chainstack.com
useSSL: true
is_default: true
is_default: true
isEnabledForAutoSwitching: true
+3 -1
View File
@@ -6,6 +6,8 @@
uri: api.trongrid.io
is_default: true
useSSL: true
isEnabledForAutoSwitching: true
-
uri: trx.nownodes.io
useSSL: true
useSSL: true
isEnabledForAutoSwitching: true
+2
View File
@@ -2,10 +2,12 @@
uri: node3.monerodevs.org:34568
is_default: true
useSSL: false
isEnabledForAutoSwitching: true
-
uri: node2.monerodevs.org:34568
is_default: false
useSSL: false
isEnabledForAutoSwitching: true
-
uri: node.monerodevs.org:34568
is_default: false
+3 -1
View File
@@ -1,7 +1,9 @@
-
uri: 37.27.100.59:10500
useSSL: false
isEnabledForAutoSwitching: true
-
uri: zano.cakewallet.com:11211
is_default: true
useSSL: false
useSSL: false
isEnabledForAutoSwitching: true
@@ -28,6 +28,16 @@ class BitcoinReceivePageOption implements ReceivePageOption {
BitcoinReceivePageOption.p2pkh
];
static const allViewOnly = [
BitcoinReceivePageOption.p2wpkh,
// TODO: uncomment this after we properly derive keys and not use m/84 for
// all of them (as this breaks cupcake)
// BitcoinReceivePageOption.p2tr,
// BitcoinReceivePageOption.p2wsh,
// BitcoinReceivePageOption.p2sh,
// BitcoinReceivePageOption.p2pkh
];
static const allLitecoin = [
BitcoinReceivePageOption.p2wpkh,
BitcoinReceivePageOption.mweb,
+60 -7
View File
@@ -21,9 +21,11 @@ import 'package:cw_bitcoin/psbt/v0_deserialize.dart';
import 'package:cw_bitcoin/psbt/v0_finalizer.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/encryption_file_utils.dart';
import 'package:cw_core/output_info.dart';
import 'package:cw_core/payjoin_session.dart';
import 'package:cw_core/pending_transaction.dart';
import 'package:cw_core/unspent_coins_info.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_keys_file.dart';
import 'package:flutter/foundation.dart';
@@ -32,6 +34,9 @@ import 'package:ledger_bitcoin/ledger_bitcoin.dart';
import 'package:ledger_bitcoin/psbt.dart';
import 'package:ledger_flutter_plus/ledger_flutter_plus.dart';
import 'package:mobx/mobx.dart';
import 'package:ur/cbor_lite.dart';
import 'package:ur/ur.dart';
import 'package:ur/ur_decoder.dart';
part 'bitcoin_wallet.g.dart';
@@ -280,6 +285,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
Future<PsbtV2> buildPsbt({
required List<BitcoinBaseOutput> outputs,
required List<OutputInfo> cwOutputs,
required BigInt fee,
required BasedUtxoNetwork network,
required List<UtxoWithAddress> utxos,
@@ -308,7 +314,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
}
return PSBTTransactionBuild(
inputs: psbtReadyInputs, outputs: outputs, enableRBF: enableRBF)
inputs: psbtReadyInputs, outputs: outputs, enableRBF: enableRBF, cwOutputs: cwOutputs)
.psbt;
}
@@ -318,6 +324,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
required BigInt fee,
required BasedUtxoNetwork network,
required List<UtxoWithAddress> utxos,
required List<OutputInfo> cwOutputs,
required Map<String, PublicKeyWithDerivationPath> publicKeys,
String? memo,
bool enableRBF = false,
@@ -331,6 +338,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
fee: fee,
network: network,
utxos: utxos,
cwOutputs: cwOutputs,
publicKeys: publicKeys,
masterFingerprint: masterFingerprint,
memo: memo,
@@ -351,7 +359,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
as PendingBitcoinTransaction;
final payjoinUri = credentials.payjoinUri;
if (payjoinUri == null) return tx;
if (payjoinUri == null && !tx.shouldCommitUR()) return tx;
final transaction = await buildPsbt(
utxos: tx.utxos,
@@ -363,20 +371,26 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
isChange: e.isChange,
))
.toList(),
cwOutputs: credentials.outputs,
fee: BigInt.from(tx.fee),
network: network,
memo: credentials.outputs.first.memo,
outputOrdering: BitcoinOrdering.none,
enableRBF: true,
publicKeys: tx.publicKeys!,
masterFingerprint: Uint8List(0));
masterFingerprint: Uint8List.fromList([0, 0, 0, 0]));
final originalPsbt = await signPsbt(
base64.encode(transaction.asPsbtV0()), getUtxoWithPrivateKeys());
if (tx.shouldCommitUR()) {
tx.unsignedPsbt = transaction.asPsbtV0();
return tx;
}
final originalPsbt =
await signPsbt(base64.encode(transaction.asPsbtV0()), getUtxoWithPrivateKeys());
tx.commitOverride = () async {
final sender = await payjoinManager.initSender(
payjoinUri, originalPsbt, int.parse(tx.feeRate));
final sender =
await payjoinManager.initSender(payjoinUri!, originalPsbt, int.parse(tx.feeRate));
payjoinManager.spawnNewSender(
sender: sender, pjUrl: payjoinUri, amount: BigInt.from(tx.amount));
};
@@ -404,6 +418,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
feeRate: "",
network: network,
hasChange: true,
isViewOnly: false,
).commit();
}
@@ -429,6 +444,44 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
return base64Encode(psbt.asPsbtV0());
}
Future<void> commitPsbtUR(List<String> urCodes) async {
if (urCodes.isEmpty) throw Exception("No QR code got scanned");
bool isUr = urCodes.any((str) {
return str.startsWith("ur:psbt/");
});
if (isUr) {
final ur = URDecoder();
for (final inp in urCodes) {
ur.receivePart(inp);
}
final result = (ur.result as UR);
final cbor = result.cbor;
final cborDecoder = CBORDecoder(cbor);
final out = cborDecoder.decodeBytes();
final bytes = out.$1;
final base64psbt = base64Encode(bytes);
final psbt = PsbtV2()..deserializeV0(base64Decode(base64psbt));
// psbt.finalize();
final finalized = base64Encode(psbt.serialize());
await commitPsbt(finalized);
} else {
final btcTx = BtcTransaction.fromRaw(urCodes.first);
return PendingBitcoinTransaction(
btcTx,
type,
electrumClient: electrumClient,
amount: 0,
fee: 0,
feeRate: "",
network: network,
hasChange: true,
isViewOnly: false,
).commit();
}
}
@override
Future<String> signMessage(String message, {String? address = null}) async {
if (walletInfo.isHardwareWallet) {
@@ -54,6 +54,17 @@ class BitcoinRestoreWalletFromWIFCredentials extends WalletCredentials {
final String wif;
}
class BitcoinWalletFromKeysCredentials extends WalletCredentials {
BitcoinWalletFromKeysCredentials({
required String name,
required String password,
required this.xpub,
WalletInfo? walletInfo,
}) : super(name: name, password: password, walletInfo: walletInfo);
final String xpub;
}
class BitcoinRestoreWalletFromHardware extends WalletCredentials {
BitcoinRestoreWalletFromHardware({
required String name,
+9 -1
View File
@@ -1,7 +1,15 @@
class BitcoinWalletKeys {
const BitcoinWalletKeys({required this.wif, required this.privateKey, required this.publicKey});
const BitcoinWalletKeys({required this.wif, required this.privateKey, required this.publicKey, required this.xpub});
final String wif;
final String privateKey;
final String publicKey;
final String xpub;
Map<String, String> toJson() => {
'wif': wif,
'privateKey': privateKey,
'publicKey': publicKey,
'xpub': xpub
};
}
+20 -4
View File
@@ -20,7 +20,7 @@ import 'package:bip39/bip39.dart' as bip39;
class BitcoinWalletService extends WalletService<
BitcoinNewWalletCredentials,
BitcoinRestoreWalletFromSeedCredentials,
BitcoinRestoreWalletFromWIFCredentials,
BitcoinWalletFromKeysCredentials,
BitcoinRestoreWalletFromHardware> {
BitcoinWalletService(this.walletInfoSource, this.unspentCoinsInfoSource,
this.payjoinSessionSource, this.isDirect);
@@ -165,9 +165,25 @@ class BitcoinWalletService extends WalletService<
}
@override
Future<BitcoinWallet> restoreFromKeys(BitcoinRestoreWalletFromWIFCredentials credentials,
{bool? isTestnet}) async =>
throw UnimplementedError();
Future<BitcoinWallet> restoreFromKeys(BitcoinWalletFromKeysCredentials credentials,
{bool? isTestnet}) async {
final network = isTestnet == true ? BitcoinNetwork.testnet : BitcoinNetwork.mainnet;
credentials.walletInfo?.network = network.value;
final wallet = await BitcoinWallet(
password: credentials.password!,
xpub: credentials.xpub,
walletInfo: credentials.walletInfo!,
unspentCoinsInfo: unspentCoinsInfoSource,
networkParam: network,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
payjoinBox: payjoinSessionSource,
);
await wallet.save();
await wallet.init();
return wallet;
}
@override
Future<BitcoinWallet> restoreFromSeed(BitcoinRestoreWalletFromSeedCredentials credentials,
+19 -9
View File
@@ -203,15 +203,25 @@ class ElectrumClient {
return [];
});
Future<Map<String, dynamic>> getBalance(String scriptHash) =>
call(method: 'blockchain.scripthash.get_balance', params: [scriptHash])
.then((dynamic result) {
if (result is Map<String, dynamic>) {
return result;
}
return <String, dynamic>{};
});
Future<Map<String, dynamic>> getBalance(String scriptHash, {bool throwOnError = false}) async {
try {
final result = await call(method: 'blockchain.scripthash.get_balance', params: [scriptHash]);
if (result is Map<String, dynamic>) {
return result;
}
if (throwOnError) {
throw Exception('Invalid response format for getBalance');
}
return <String, dynamic>{};
} catch (e) {
if (throwOnError) {
rethrow;
}
return <String, dynamic>{};
}
}
Future<List<Map<String, dynamic>>> getHistory(String scriptHash) =>
call(method: 'blockchain.scripthash.get_history', params: [scriptHash])
+90 -22
View File
@@ -42,6 +42,7 @@ import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_keys_file.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:cw_core/unspent_coin_type.dart';
import 'package:cw_core/output_info.dart';
import 'package:flutter/foundation.dart';
import 'package:hive/hive.dart';
import 'package:ledger_flutter_plus/ledger_flutter_plus.dart' as ledger;
@@ -216,7 +217,7 @@ abstract class ElectrumWalletBase
@override
bool isTestnet;
bool get hasSilentPaymentsScanning => type == WalletType.bitcoin;
bool get hasSilentPaymentsScanning => type == WalletType.bitcoin && keys.privateKey.isNotEmpty;
@observable
bool nodeSupportsSilentPayments = true;
@@ -282,11 +283,26 @@ abstract class ElectrumWalletBase
}
@override
BitcoinWalletKeys get keys => BitcoinWalletKeys(
wif: WifEncoder.encode(hd.privateKey.raw, netVer: network.wifNetVer),
privateKey: hd.privateKey.toHex(),
publicKey: hd.publicKey.toHex(),
);
BitcoinWalletKeys get keys {
String? wif;
String? privateKey;
String? publicKey;
try {
wif = WifEncoder.encode(hd.privateKey.raw, netVer: network.wifNetVer);
} catch (_) {}
try {
privateKey = hd.privateKey.toHex();
} catch (_) {}
try {
publicKey = hd.publicKey.toHex();
} catch (_) {}
return BitcoinWalletKeys(
wif: wif ?? '',
privateKey: privateKey ?? '',
publicKey: publicKey ?? '',
xpub: xpub,
);
}
String _password;
List<BitcoinUnspent> unspentCoins;
@@ -672,7 +688,7 @@ abstract class ElectrumWalletBase
);
spendsSilentPayment = true;
isSilentPayment = true;
} else if (!isHardwareWallet) {
} else if (!isHardwareWallet && keys.privateKey.isNotEmpty) {
privkey =
generateECPrivate(hd: hd, index: utx.bitcoinAddressRecord.index, network: network);
}
@@ -780,7 +796,11 @@ abstract class ElectrumWalletBase
}
if (outputs.length == 1) {
outputs[0] = BitcoinOutput(address: outputs.last.address, value: BigInt.from(amount));
outputs[0] = BitcoinOutput(
address: outputs.last.address,
value: BigInt.from(amount),
isSilentPayment: hasSilentPayment,
);
}
return EstimatedTxResult(
@@ -888,7 +908,17 @@ abstract class ElectrumWalletBase
);
updatedOutputs.clear();
updatedOutputs.addAll(temp);
for (int i = 0; i < temp.length; i++) {
final output = temp[i];
final oldOutput = outputs[i];
updatedOutputs.add(BitcoinOutput(
address: output.address,
value: output.value,
isSilentPayment: oldOutput.isSilentPayment,
isChange: output.isChange,
));
}
if (fee == 0) {
throw BitcoinTransactionNoFeeException();
@@ -1088,6 +1118,14 @@ abstract class ElectrumWalletBase
);
}
for (final output in updatedOutputs) {
// TODO: get from server
// if (output.isSilentPayment && output.value.toInt() > silentPaymentsMin) {
if (output.isSilentPayment && output.value.toInt() <= 1000) {
throw BitcoinTransactionNoDustException();
}
}
if (walletInfo.isHardwareWallet) {
final transaction = await buildHardwareWalletTransaction(
utxos: estimatedTx.utxos,
@@ -1098,6 +1136,7 @@ abstract class ElectrumWalletBase
memo: estimatedTx.memo,
outputOrdering: BitcoinOrdering.none,
enableRBF: true,
cwOutputs: transactionCredentials.outputs,
);
return PendingBitcoinTransaction(
@@ -1110,7 +1149,8 @@ abstract class ElectrumWalletBase
network: network,
hasChange: estimatedTx.hasChange,
isSendAll: estimatedTx.isSendAll,
hasTaprootInputs: false, // ToDo: (Konsti) Support Taproot
hasTaprootInputs: false, // ToDo: (Konsti) Support Taproot,
isViewOnly: false,
)..addListener((transaction) async {
transactionHistory.addOne(transaction);
await updateBalance();
@@ -1144,6 +1184,7 @@ abstract class ElectrumWalletBase
bool hasTaprootInputs = false;
final transaction = txb.buildTransaction((txDigest, utxo, publicKey, sighash) {
if (keys.privateKey.isEmpty) return "";
String error = "Cannot find private key.";
ECPrivateInfo? key;
@@ -1181,18 +1222,21 @@ abstract class ElectrumWalletBase
}
});
return PendingBitcoinTransaction(transaction, type,
electrumClient: electrumClient,
amount: estimatedTx.amount,
fee: estimatedTx.fee,
feeRate: feeRateInt.toString(),
network: network,
hasChange: estimatedTx.hasChange,
isSendAll: estimatedTx.isSendAll,
hasTaprootInputs: hasTaprootInputs,
utxos: estimatedTx.utxos,
publicKeys: estimatedTx.publicKeys)
..addListener((transaction) async {
return PendingBitcoinTransaction(
transaction,
type,
electrumClient: electrumClient,
amount: estimatedTx.amount,
fee: estimatedTx.fee,
feeRate: feeRateInt.toString(),
network: network,
hasChange: estimatedTx.hasChange,
isSendAll: estimatedTx.isSendAll,
hasTaprootInputs: hasTaprootInputs,
utxos: estimatedTx.utxos,
publicKeys: estimatedTx.publicKeys,
isViewOnly: keys.privateKey.isEmpty,
)..addListener((transaction) async {
transactionHistory.addOne(transaction);
if (estimatedTx.spendsSilentPayment) {
transactionHistory.transactions.values.forEach((tx) {
@@ -1220,6 +1264,7 @@ abstract class ElectrumWalletBase
required BigInt fee,
required BasedUtxoNetwork network,
required List<UtxoWithAddress> utxos,
required List<OutputInfo> cwOutputs,
required Map<String, PublicKeyWithDerivationPath> publicKeys,
String? memo,
bool enableRBF = false,
@@ -1357,6 +1402,8 @@ abstract class ElectrumWalletBase
@action
@override
Future<void> rescan({required int height, bool? doSingleScan}) async {
if (keys.privateKey.isEmpty) return;
silentPaymentsScanningActive = true;
_setListeners(height, doSingleScan: doSingleScan);
}
@@ -1853,6 +1900,7 @@ abstract class ElectrumWalletBase
network: network,
hasChange: changeOutputs.isNotEmpty,
feeRate: newFee.toString(),
isViewOnly: keys.privateKey.isEmpty,
)..addListener((transaction) async {
transactionHistory.transactions.values.forEach((tx) {
if (tx.id == hash) {
@@ -2300,6 +2348,26 @@ abstract class ElectrumWalletBase
await save();
}
@override
Future<bool> checkNodeHealth() async {
try {
final addresses = walletAddresses.allAddresses
.where((address) => RegexUtils.addressTypeFromStr(address.address, network) is! MwebAddress)
.toList();
if (addresses.isEmpty) {
return false;
}
final firstAddress = addresses.first;
final sh = firstAddress.getScriptHash(network);
await electrumClient.getBalance(sh, throwOnError: true);
return true;
} catch (e) {
return false;
}
}
@override
void setExceptionHandler(void Function(FlutterErrorDetails) onError) => _onError = onError;
+2
View File
@@ -38,6 +38,7 @@ import 'package:cw_bitcoin/litecoin_wallet_addresses.dart';
import 'package:cw_core/transaction_priority.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_keys_file.dart';
import 'package:cw_core/output_info.dart';
import 'package:flutter/foundation.dart';
import 'package:grpc/grpc.dart';
import 'package:hive/hive.dart';
@@ -1383,6 +1384,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
required BigInt fee,
required BasedUtxoNetwork network,
required List<UtxoWithAddress> utxos,
required List<OutputInfo> cwOutputs,
required Map<String, PublicKeyWithDerivationPath> publicKeys,
String? memo,
bool enableRBF = false,
@@ -1,3 +1,7 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:bbqrdart/bbqrdart.dart';
import 'package:cw_bitcoin/electrum_wallet.dart';
import 'package:grpc/grpc.dart';
import 'package:cw_bitcoin/exceptions.dart';
@@ -11,6 +15,9 @@ import 'package:cw_core/transaction_direction.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:cw_mweb/cw_mweb.dart';
import 'package:cw_mweb/mwebd.pb.dart';
import 'package:ur/cbor_lite.dart';
import 'package:ur/ur.dart';
import 'package:ur/ur_encoder.dart';
class PendingBitcoinTransaction with PendingTransaction {
PendingBitcoinTransaction(
@@ -28,6 +35,8 @@ class PendingBitcoinTransaction with PendingTransaction {
this.utxos = const [],
this.publicKeys,
this.commitOverride,
this.unsignedPsbt,
required this.isViewOnly,
}) : _listeners = <void Function(ElectrumTransactionInfo transaction)>[];
final WalletType type;
@@ -40,6 +49,7 @@ class PendingBitcoinTransaction with PendingTransaction {
final bool isSendAll;
final bool hasChange;
final bool hasTaprootInputs;
final bool isViewOnly;
List<UtxoWithAddress> utxos;
bool isMweb;
String? changeAddressOverride;
@@ -49,6 +59,8 @@ class PendingBitcoinTransaction with PendingTransaction {
final Map<String, PublicKeyWithDerivationPath>? publicKeys;
Future<void> Function()? commitOverride;
Uint8List? unsignedPsbt;
@override
String get id => idOverride ?? _tx.txId();
@@ -89,9 +101,11 @@ class PendingBitcoinTransaction with PendingTransaction {
try {
final change = _tx.outputs.firstWhere((out) => out.isChange);
if (changeAddressOverride != null) {
return PendingChange(changeAddressOverride!, BtcUtils.fromSatoshi(change.amount));
return PendingChange(
changeAddressOverride!, BtcUtils.fromSatoshi(change.amount));
}
return PendingChange(change.scriptPubKey.toAddress(), BtcUtils.fromSatoshi(change.amount));
return PendingChange(
change.scriptPubKey.toAddress(), BtcUtils.fromSatoshi(change.amount));
} catch (_) {
return null;
}
@@ -140,12 +154,14 @@ class PendingBitcoinTransaction with PendingTransaction {
Future<void> _ltcCommit() async {
try {
final resp = await CwMweb.broadcast(BroadcastRequest(rawTx: BytesUtils.fromHexString(hex)));
final resp = await CwMweb.broadcast(
BroadcastRequest(rawTx: BytesUtils.fromHexString(hex)));
idOverride = resp.txid;
} on GrpcError catch (e) {
throw BitcoinTransactionCommitFailed(errorMessage: e.message);
} catch (e) {
throw BitcoinTransactionCommitFailed(errorMessage: "Unknown error: ${e.toString()}");
throw BitcoinTransactionCommitFailed(
errorMessage: "Unknown error: ${e.toString()}");
}
}
@@ -164,7 +180,8 @@ class PendingBitcoinTransaction with PendingTransaction {
_listeners.forEach((listener) => listener(transactionInfo()));
}
void addListener(void Function(ElectrumTransactionInfo transaction) listener) =>
void addListener(
void Function(ElectrumTransactionInfo transaction) listener) =>
_listeners.add(listener);
ElectrumTransactionInfo transactionInfo() => ElectrumTransactionInfo(type,
@@ -179,9 +196,40 @@ class PendingBitcoinTransaction with PendingTransaction {
inputAddresses: _tx.inputs.map((input) => input.txId).toList(),
outputAddresses: outputAddresses,
fee: fee);
@override
Future<String?> commitUR() {
throw UnimplementedError();
bool shouldCommitUR() => isViewOnly;
@override
Future<Map<String, String>> commitUR() {
var sourceBytes = unsignedPsbt!;
var cborEncoder = CBOREncoder();
cborEncoder.encodeBytes(sourceBytes);
var ur = UR("psbt", cborEncoder.getBytes());
var urLegacy = UR("crypto-psbt", cborEncoder.getBytes());
// var ur = UR("psbt", Uint8List.fromList(List.generate(64*1024, (int x) => x % 256)));
var encoded = UREncoder(ur, 120);
var encodedLegacy = UREncoder(urLegacy, 120);
List<String> values = [];
List<String> valuesLegacy = [];
while (!encoded.isComplete) {
values.add(encoded.nextPart());
valuesLegacy.add(encodedLegacy.nextPart());
}
final bbqrObj = BBQRPsbt.fromUint8List(sourceBytes);
List<String> bbqr = [
bbqrObj.asString(),
];
while (!bbqrObj.isDone) {
bbqrObj.next();
bbqr.add(bbqrObj.asString());
}
return Future.value({
"PSBT (bcur)": values.join("\n"),
"PSBT (bbqr)": bbqr.join("\n"),
"PSBT (bcur legacy)": valuesLegacy.join("\n"),
});
}
}
+31 -2
View File
@@ -1,7 +1,9 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:convert/convert.dart';
import 'package:cw_core/output_info.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:ledger_bitcoin/psbt.dart';
@@ -9,7 +11,7 @@ class PSBTTransactionBuild {
final PsbtV2 psbt = PsbtV2();
PSBTTransactionBuild(
{required List<PSBTReadyUtxoWithAddress> inputs, required List<BitcoinBaseOutput> outputs, bool enableRBF = true}) {
{required List<PSBTReadyUtxoWithAddress> inputs, required List<BitcoinBaseOutput> outputs, required List<OutputInfo> cwOutputs, bool enableRBF = true}) {
psbt.setGlobalTxVersion(2);
psbt.setGlobalInputCount(inputs.length);
psbt.setGlobalOutputCount(outputs.length);
@@ -24,7 +26,7 @@ class PSBTTransactionBuild {
psbt.setInputPreviousTxId(i, Uint8List.fromList(hex.decode(input.utxo.txHash).reversed.toList()));
psbt.setInputOutputIndex(i, input.utxo.vout);
psbt.setInputSequence(i, enableRBF ? 0x1 : 0xffffffff);
if (input.utxo.isSegwit()) {
setInputSegwit(i, input);
@@ -43,6 +45,33 @@ class PSBTTransactionBuild {
if (output is BitcoinOutput) {
psbt.setOutputScript(i, Uint8List.fromList(output.address.toScriptPubKey().toBytes()));
psbt.setOutputAmount(i, output.value.toInt());
if (cwOutputs.isNotEmpty) {
try {
final cwOutput = cwOutputs
.where((e) => [e.address, e.extractedAddress]
.map((e) => e?.toLowerCase())
.contains(output.address.toAddress().toLowerCase()))
.firstOrNull;
if (cwOutput != null) {
final bip353Name = utf8.encode(cwOutput.extra['bip353_name'] as String);
final bip353Proof = base64.decode(cwOutput.extra['bip353_proof'] as String);
if (bip353Name.length > 255) {
printV('BIP353 name is too long, skipping');
continue;
}
final proof = Uint8List.fromList([
bip353Name.length,
...bip353Name,
...bip353Proof,
]);
psbt.setOutputDNSSECProof(i, proof);
}
} catch (e) {
printV('Error setting DNSSEC proof: $e');
}
}
}
}
}
+21 -3
View File
@@ -46,6 +46,15 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.11.0"
bbqrdart:
dependency: "direct main"
description:
path: "."
ref: b0f3f72911221bafed0ab77359fccd64bc79d524
resolved-ref: b0f3f72911221bafed0ab77359fccd64bc79d524
url: "https://github.com/mrcyjanek/bbqrdart"
source: git
version: "1.0.0"
bech32:
dependency: "direct main"
description:
@@ -588,8 +597,8 @@ packages:
dependency: "direct main"
description:
path: "packages/ledger-bitcoin"
ref: trunk
resolved-ref: e93254f3ff3f996fb91f65a1e7ceffb9f510b4c8
ref: e8ab1a98493dfdd2277edde07a66b8d378ca70f6
resolved-ref: e8ab1a98493dfdd2277edde07a66b8d378ca70f6
url: "https://github.com/cake-tech/ledger-flutter-plus-plugins"
source: git
version: "0.0.3"
@@ -1101,6 +1110,15 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.3.0"
ur:
dependency: "direct main"
description:
path: "."
ref: "5738f70d0ec3d50977ac3dd01fed62939600238b"
resolved-ref: "5738f70d0ec3d50977ac3dd01fed62939600238b"
url: "https://github.com/bukata-sa/bc-ur-dart"
source: git
version: "0.1.0"
vector_math:
dependency: transitive
description:
@@ -1182,5 +1200,5 @@ packages:
source: hosted
version: "2.2.2"
sdks:
dart: ">=3.6.0 <4.0.0"
dart: ">=3.6.2 <4.0.0"
flutter: ">=3.27.0"
+9 -1
View File
@@ -49,7 +49,7 @@ dependencies:
git:
url: https://github.com/cake-tech/ledger-flutter-plus-plugins
path: packages/ledger-bitcoin
ref: trunk
ref: e8ab1a98493dfdd2277edde07a66b8d378ca70f6
ledger_litecoin:
git:
url: https://github.com/cake-tech/ledger-flutter-plus-plugins
@@ -58,6 +58,14 @@ dependencies:
git:
url: https://github.com/sneurlax/socks_socket
ref: e6232c53c1595469931ababa878759a067c02e94
ur:
git:
url: https://github.com/bukata-sa/bc-ur-dart
ref: 5738f70d0ec3d50977ac3dd01fed62939600238b
bbqrdart:
git:
url: https://github.com/mrcyjanek/bbqrdart
ref: b0f3f72911221bafed0ab77359fccd64bc79d524
dev_dependencies:
flutter_test:
@@ -89,7 +89,7 @@ class PendingBitcoinCashTransaction with PendingTransaction {
isReplaced: false,
);
@override
Future<String?> commitUR() {
Future<Map<String, String>> commitUR() {
throw UnimplementedError();
}
}
+5
View File
@@ -113,6 +113,8 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
CryptoCurrency.flip,
CryptoCurrency.deuro,
CryptoCurrency.usdtbsc,
CryptoCurrency.ndeps,
CryptoCurrency.deps,
];
static const havenCurrencies = [
@@ -235,6 +237,9 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
static const flip = CryptoCurrency(title: 'FLIP', tag: 'ETH', fullName: 'Chainflip', raw: 97, name: 'flip', iconPath: 'assets/images/flip_icon.png', decimals: 18);
static const deuro = CryptoCurrency(title: 'DEURO', tag: 'ETH', fullName: 'Decentralized Euro', raw: 98, name: 'deuro', iconPath: 'assets/images/deuro_icon.png', decimals: 18);
static const usdtbsc = CryptoCurrency(title: 'USDT', tag: 'BSC', fullName: 'USDT Binance coin', raw: 99, name: 'usdtbsc', iconPath: 'assets/images/usdtbsc_icon.png', decimals: 18);
static const ndeps = CryptoCurrency(title: 'NDEPS', tag: 'ETH', fullName: 'Native Decentralized Euro Protocol Share', raw: 100, name: 'ndeps', iconPath: 'assets/images/ndeps_icon.png', decimals: 18);
static const deps = CryptoCurrency(title: 'DEPS', tag: 'ETH', fullName: 'Decentralized Euro Protocol Share', raw: 101, name: 'deps', iconPath: 'assets/images/deps_icon.png', decimals: 18);
static final Map<int, CryptoCurrency> _rawCurrencyMap =
[...all, ...havenCurrencies].fold<Map<int, CryptoCurrency>>(<int, CryptoCurrency>{}, (acc, item) {
+6 -1
View File
@@ -26,6 +26,7 @@ class Node extends HiveObject with Keyable {
this.trusted = false,
this.socksProxyAddress,
this.path = '',
this.isEnabledForAutoSwitching = false,
String? uri,
WalletType? type,
}) {
@@ -44,7 +45,8 @@ class Node extends HiveObject with Keyable {
password = map['password'] as String?,
useSSL = map['useSSL'] as bool?,
trusted = map['trusted'] as bool? ?? false,
socksProxyAddress = map['socksProxyPort'] as String?;
socksProxyAddress = map['socksProxyPort'] as String?,
isEnabledForAutoSwitching = map['isEnabledForAutoSwitching'] as bool? ?? false;
static const typeId = NODE_TYPE_ID;
static const boxName = 'Nodes';
@@ -82,6 +84,9 @@ class Node extends HiveObject with Keyable {
@HiveField(10)
bool? supportsMweb;
@HiveField(11, defaultValue: false)
bool isEnabledForAutoSwitching;
bool get isSSL => useSSL ?? false;
bool get useSocksProxy => socksProxyAddress == null ? false : socksProxyAddress!.isNotEmpty;
+3 -1
View File
@@ -8,7 +8,8 @@ class OutputInfo {
this.fiatAmount,
this.note,
this.extractedAddress,
this.memo});
this.memo,
this.extra = const {}});
final String? fiatAmount;
final String? cryptoAmount;
@@ -19,4 +20,5 @@ class OutputInfo {
final bool isParsedAddress;
final int? formattedCryptoAmount;
final String? memo;
final Map<String, dynamic> extra;
}
+1 -1
View File
@@ -18,5 +18,5 @@ mixin PendingTransaction {
bool shouldCommitUR() => false;
Future<void> commit();
Future<String?> commitUR();
Future<Map<String, String>> commitUR();
}
+5
View File
@@ -112,4 +112,9 @@ abstract class WalletBase<BalanceType extends Balance, HistoryType extends Trans
bool isTestnet = false;
bool canSend() => true;
/// This is used to check if the current node is healthy by making a lightweight RPC call
/// Each wallet implementation should override this to make a single, efficient call
/// Returns true if the node is healthy, false otherwise
Future<bool> checkNodeHealth();
}
+4 -1
View File
@@ -454,7 +454,7 @@ class Libwallet {
return res.payload;
}
Future<Map> balance(String walletName) async {
Future<Map> balance(String walletName, {bool throwOnError = false}) async {
if (_closed) throw StateError('Closed');
final completer = Completer<Object?>.sync();
final id = _idCounter++;
@@ -468,6 +468,9 @@ class Libwallet {
try {
return jsonDecode(res.payload);
} catch (_) {
if (throwOnError) {
rethrow;
}
return {};
}
}
+1 -1
View File
@@ -36,7 +36,7 @@ class DecredPendingTransaction with PendingTransaction {
}
@override
Future<String?> commitUR() {
Future<Map<String, String>> commitUR() {
throw UnimplementedError();
}
}
+3 -1
View File
@@ -26,7 +26,6 @@ import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/transaction_priority.dart';
import 'package:cw_core/pending_transaction.dart';
import 'package:cw_core/pathForWallet.dart';
import 'package:cw_core/sync_status.dart';
import 'package:cw_core/node.dart';
import 'package:cw_core/unspent_coins_info.dart';
@@ -594,6 +593,9 @@ abstract class DecredWalletBase
);
}
@override
Future<bool> checkNodeHealth() async => await checkSync();
@override
void setExceptionHandler(void Function(FlutterErrorDetails) onError) => onError;
@@ -304,6 +304,20 @@ class DefaultEthereumErc20Tokens {
decimal: 18,
enabled: false,
),
Erc20Token(
name: "Native Decentralized Euro Protocol Share",
symbol: "NDEPS",
contractAddress: "0xc71104001A3CCDA1BEf1177d765831Bd1bfE8eE6",
decimal: 18,
enabled: false,
),
Erc20Token(
name: "Decentralized Euro Protocol Share",
symbol: "DEPS",
contractAddress: "0x103747924e74708139a9400e4ab4bea79fffa380",
decimal: 18,
enabled: false,
),
];
List<Erc20Token> get initialErc20Tokens => _defaultTokens.map((token) {
+4 -1
View File
@@ -65,10 +65,13 @@ abstract class EVMChainClient {
// });
}
Future<EtherAmount> getBalance(EthereumAddress address) async {
Future<EtherAmount> getBalance(EthereumAddress address, {bool throwOnError = false}) async {
try {
return await _client!.getBalance(address);
} catch (_) {
if (throwOnError) {
rethrow;
}
return EtherAmount.zero();
}
}
+39 -3
View File
@@ -164,6 +164,36 @@ abstract class EVMChainWalletBase
EncryptionFileUtils encryptionFileUtils,
);
@override
Future<bool> checkNodeHealth() async {
try {
// Check native balance
await _client.getBalance(_evmChainPrivateKey.address, throwOnError: true);
// Check USDC token balance
String usdcContractAddress;
if (_client.chainId == 1) {
// Ethereum mainnet
usdcContractAddress = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48";
} else if (_client.chainId == 137) {
// Polygon mainnet
usdcContractAddress = "0x2791bca1f2de4661ed88a30c99a7a9449aa84174";
} else {
// If we are not on Ethereum or Polygon, we skip ERC20 token check
return true;
}
await _client.fetchERC20Balances(
_evmChainPrivateKey.address,
usdcContractAddress,
);
return true;
} catch (e) {
return false;
}
}
//! Common Methods across child classes
String idFor(String name, WalletType type) => '${walletTypeToString(type).toLowerCase()}_$name';
@@ -379,15 +409,21 @@ abstract class EVMChainWalletBase
Future<void> startSync() async {
try {
syncStatus = AttemptingSyncStatus();
// Verify node health before attempting to sync
final isHealthy = await checkNodeHealth();
if (!isHealthy) {
syncStatus = FailedSyncStatus();
return;
}
await _updateBalance();
await _updateTransactions();
await _updateEstimatedGasFeeParams();
_updateFeesTimer ??= Timer.periodic(const Duration(seconds: 30), (timer) async {
await _updateEstimatedGasFeeParams();
});
syncStatus = SyncedSyncStatus();
} catch (e) {
syncStatus = FailedSyncStatus();
@@ -825,7 +861,7 @@ abstract class EVMChainWalletBase
_transactionsUpdateTimer!.cancel();
}
_transactionsUpdateTimer = Timer.periodic(const Duration(seconds: 15), (_) {
_transactionsUpdateTimer = Timer.periodic(const Duration(seconds: 20), (_) {
_updateTransactions();
_updateBalance();
});
+3 -11
View File
@@ -1,6 +1,5 @@
import 'dart:convert';
import 'dart:math';
import 'package:intl/intl.dart';
import 'package:cw_core/format_fixed.dart';
import 'package:cw_core/balance.dart';
@@ -12,17 +11,10 @@ class EVMChainERC20Balance extends Balance {
final int exponent;
@override
String get formattedAdditionalBalance => _balance();
String get formattedAdditionalBalance => formatFixed(balance, exponent, fractionalDigits: 12);
@override
String get formattedAvailableBalance => _balance();
String _balance() {
NumberFormat formatter = NumberFormat('0.00##########', 'en_US');
double numBalance = (balance / BigInt.from(10).pow(exponent)).toDouble();
String formattedBalance = formatter.format(numBalance);
return formattedBalance;
}
String get formattedAvailableBalance => formatFixed(balance, exponent, fractionalDigits: 12);
String toJSON() => json.encode({
'balanceInWei': balance.toString(),
@@ -53,7 +53,7 @@ class PendingEVMChainTransaction with PendingTransaction {
}
@override
Future<String?> commitUR() {
Future<Map<String, String>> commitUR() {
throw UnimplementedError();
}
}
+2 -1
View File
@@ -34,7 +34,8 @@ void enableLedgerExchange(LedgerConnection connection) {
api.MoneroWallet.setDeviceReceivedData(
result.cast<UnsignedChar>(), response.length);
api.MoneroFree().free(result.cast());
malloc.free(result);
// api.MoneroFree().free(result.cast());
}
callable = NativeCallable<LedgerCallback>.listener(callback);
+32 -5
View File
@@ -184,6 +184,24 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
@override
Future<void>? updateBalance() => null;
@override
Future<bool> checkNodeHealth() async {
try {
// Check if the wallet is currently connected to the daemon
final isConnected = monero_wallet.isConnectedSync();
if (!isConnected) {
return false; // It's not connected to daemon
}
// Check to get current node height to ensure daemon is responsive
final nodeHeight = await monero_wallet.getNodeHeight();
return nodeHeight > 0;
} catch (e) {
return false;
}
}
@override
Future<void> close({bool shouldCleanup = false}) async {
if (isHardwareWallet) {
@@ -336,14 +354,23 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
return retStatus;
}
String exportOutputsUR(bool all) {
final str = currentWallet!.exportOutputsUR(all: all);
final status = currentWallet!.status();
Map<String, String> exportOutputsUR() {
final str = currentWallet!.exportOutputsUR(all: false);
int status = currentWallet!.status();
if (status != 0) {
final err = currentWallet!.errorString();
throw MoneroTransactionCreationException("unable to export UR: $err");
throw MoneroTransactionCreationException("unable to export outputs: $err");
}
return str;
final strAll = currentWallet!.exportOutputsUR(all: true);
status = currentWallet!.status();
if (status != 0) {
final err = currentWallet!.errorString();
throw MoneroTransactionCreationException("unable to export outputs: $err");
}
return {
"Outputs (partial)": str,
"Outputs (all)": strAll,
};
}
bool needExportOutputs(int amount) {
@@ -42,6 +42,7 @@ class PendingMoneroTransaction with PendingTransaction {
String get feeFormattedValue => AmountConverter.amountIntToString(
CryptoCurrency.xmr, pendingTransactionDescription.fee);
@override
bool shouldCommitUR() => isViewOnly;
@override
@@ -67,7 +68,7 @@ class PendingMoneroTransaction with PendingTransaction {
}
@override
Future<String?> commitUR() async {
Future<Map<String, String>> commitUR() async {
try {
final ret = await monero_transaction_history.commitTransactionFromPointerAddress(
address: pendingTransactionDescription.pointerAddress,
@@ -77,7 +78,10 @@ class PendingMoneroTransaction with PendingTransaction {
await Future.delayed(const Duration(milliseconds: 250));
await wallet.fetchTransactions();
}());
return ret;
if (ret == null) return {};
return {
"xmr-txsigned": ret,
};
} catch (e) {
final message = e.toString();
+2 -2
View File
@@ -573,8 +573,8 @@ packages:
dependency: "direct main"
description:
path: "impls/monero.dart"
ref: a27fbcb24d91143715ed930a05aaa4d853fba1f2
resolved-ref: a27fbcb24d91143715ed930a05aaa4d853fba1f2
ref: b576312e4d466569cd03482b61c597b39a9f4dc3
resolved-ref: b576312e4d466569cd03482b61c597b39a9f4dc3
url: "https://github.com/mrcyjanek/monero_c"
source: git
version: "0.0.0"
+1 -1
View File
@@ -26,7 +26,7 @@ dependencies:
monero:
git:
url: https://github.com/mrcyjanek/monero_c
ref: a27fbcb24d91143715ed930a05aaa4d853fba1f2
ref: b576312e4d466569cd03482b61c597b39a9f4dc3
path: impls/monero.dart
mutex: ^3.1.0
ledger_flutter_plus: ^1.4.1
+4 -1
View File
@@ -92,7 +92,7 @@ class NanoClient {
return NanoBalance(currentBalance: cur, receivableBalance: rec);
}
Future<AccountInfoResponse?> getAccountInfo(String address) async {
Future<AccountInfoResponse?> getAccountInfo(String address, {bool throwOnError = false}) async {
try {
final response = await ProxyWrapper().post(
clearnetUri: _node!.uri,
@@ -110,6 +110,9 @@ class NanoClient {
return AccountInfoResponse.fromJson(data);
} catch (e) {
printV("error while getting account info $e");
if (throwOnError) {
rethrow;
}
return null;
}
}
+11
View File
@@ -518,8 +518,19 @@ abstract class NanoWalletBase
return _client.getN2Reps();
}
@override
Future<void>? updateBalance() async => await _updateBalance();
@override
Future<bool> checkNodeHealth() async {
try {
await _client.getAccountInfo(_publicAddress!, throwOnError: true);
return true;
} catch (_) {
return false;
}
}
@override
Future<void> renameWalletFiles(String newWalletName) async {
final currentWalletPath = await pathForWallet(name: walletInfo.name, type: type);
+1 -1
View File
@@ -42,7 +42,7 @@ class PendingNanoTransaction with PendingTransaction {
}
@override
Future<String?> commitUR() {
Future<Map<String, String>> commitUR() {
throw UnimplementedError();
}
}
@@ -44,7 +44,7 @@ class PendingSolanaTransaction with PendingTransaction {
String get id => '';
@override
Future<String?> commitUR() {
Future<Map<String, String>> commitUR() {
throw UnimplementedError();
}
}
+33 -23
View File
@@ -51,7 +51,7 @@ class SolanaWalletClient {
}
}
Future<double> getBalance(String walletAddress) async {
Future<double> getBalance(String walletAddress, {bool throwOnError = false}) async {
try {
final balance = await _provider!.requestWithContext(
SolanaRPCGetBalance(
@@ -65,6 +65,9 @@ class SolanaWalletClient {
return solBalance;
} catch (_) {
if (throwOnError) {
rethrow;
}
return 0.0;
}
}
@@ -88,31 +91,38 @@ class SolanaWalletClient {
}
Future<SolanaBalance?> getSplTokenBalance(
String mintAddress, String walletAddress) async {
// Fetch the token accounts (a token can have multiple accounts for various uses)
final tokenAccounts = await getSPLTokenAccounts(mintAddress, walletAddress);
String mintAddress, String walletAddress, {bool throwOnError = false}) async {
try {
// Fetch the token accounts (a token can have multiple accounts for various uses)
final tokenAccounts = await getSPLTokenAccounts(mintAddress, walletAddress);
// Handle scenario where there is no token account
if (tokenAccounts == null || tokenAccounts.isEmpty) {
// Handle scenario where there is no token account
if (tokenAccounts == null || tokenAccounts.isEmpty) {
return null;
}
// Sum the balances of all accounts with the specified mint address
double totalBalance = 0.0;
for (var tokenAccount in tokenAccounts) {
final tokenAmountResult = await _provider!.request(
SolanaRPCGetTokenAccountBalance(account: tokenAccount.pubkey),
);
final balance = tokenAmountResult.uiAmountString;
final balanceAsDouble = double.tryParse(balance ?? '0.0') ?? 0.0;
totalBalance += balanceAsDouble;
}
return SolanaBalance(totalBalance);
} catch (_) {
if (throwOnError) {
rethrow;
}
return null;
}
// Sum the balances of all accounts with the specified mint address
double totalBalance = 0.0;
for (var tokenAccount in tokenAccounts) {
final tokenAmountResult = await _provider!.request(
SolanaRPCGetTokenAccountBalance(account: tokenAccount.pubkey),
);
final balance = tokenAmountResult.uiAmountString;
final balanceAsDouble = double.tryParse(balance ?? '0.0') ?? 0.0;
totalBalance += balanceAsDouble;
}
return SolanaBalance(totalBalance);
}
Future<double> getFeeForMessage(String message, Commitment commitment) async {
+25 -1
View File
@@ -377,6 +377,13 @@ abstract class SolanaWalletBase
try {
syncStatus = AttemptingSyncStatus();
// Verify node health before attempting to sync
final isHealthy = await checkNodeHealth();
if (!isHealthy) {
syncStatus = FailedSyncStatus();
return;
}
await Future.wait([
_updateBalance(),
_updateNativeSOLTransactions(),
@@ -477,6 +484,22 @@ abstract class SolanaWalletBase
@override
Future<void>? updateBalance() async => await _updateBalance();
@override
Future<bool> checkNodeHealth() async {
try {
// Check native balance
await _client.getBalance(solanaAddress, throwOnError: true);
// Check USDC token balance
const usdcMintAddress = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
await _client.getSplTokenBalance(usdcMintAddress, solanaAddress, throwOnError: true);
return true;
} catch (e) {
return false;
}
}
List<SPLToken> get splTokenCurrencies => splTokensBox.values.toList();
void addInitialTokens() {
@@ -554,6 +577,7 @@ abstract class SolanaWalletBase
_updateBalance();
_updateNativeSOLTransactions();
_updateSPLTokenTransactions();
_getEstimatedFees();
});
}
@@ -564,7 +588,7 @@ abstract class SolanaWalletBase
// Sign the message bytes with the wallet's private key
final signature = (_solanaPrivateKey.sign(messageBytes));
return Base58Encoder.encode(signature);
}
+1 -1
View File
@@ -35,7 +35,7 @@ class PendingTronTransaction with PendingTransaction {
String get id => '';
@override
Future<String?> commitUR() {
Future<Map<String, String>> commitUR() {
throw UnimplementedError();
}
}
+8 -2
View File
@@ -102,12 +102,15 @@ class TronClient {
}
}
Future<BigInt> getBalance(TronAddress address) async {
Future<BigInt> getBalance(TronAddress address, {bool throwOnError = false}) async {
try {
final accountDetails = await _provider!.request(TronRequestGetAccount(address: address));
return accountDetails?.balance ?? BigInt.zero;
} catch (_) {
if (throwOnError) {
rethrow;
}
return BigInt.zero;
}
}
@@ -476,7 +479,7 @@ class TronClient {
}
}
Future<TronBalance> fetchTronTokenBalances(String userAddress, String contractAddress) async {
Future<TronBalance> fetchTronTokenBalances(String userAddress, String contractAddress, {bool throwOnError = false}) async {
try {
final ownerAddress = TronAddress(userAddress);
@@ -499,6 +502,9 @@ class TronClient {
return TronBalance(outputResult);
} catch (_) {
if (throwOnError) {
rethrow;
}
return TronBalance(BigInt.zero);
}
}
+23
View File
@@ -276,6 +276,13 @@ abstract class TronWalletBase
Future<void> startSync() async {
try {
syncStatus = AttemptingSyncStatus();
// Verify node health before attempting to sync
final isHealthy = await checkNodeHealth();
if (!isHealthy) {
syncStatus = FailedSyncStatus();
return;
}
await _updateBalance();
await fetchTransactions();
fetchTrc20ExcludedTransactions();
@@ -505,6 +512,22 @@ abstract class TronWalletBase
@override
Future<void>? updateBalance() async => await _updateBalance();
@override
Future<bool> checkNodeHealth() async {
try {
// Check native balance
await _client.getBalance(_tronPublicKey.toAddress(), throwOnError: true);
// Check USDT token balance
const usdtContractAddress = "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t";
await _client.fetchTronTokenBalances(_tronAddress, usdtContractAddress, throwOnError: true);
return true;
} catch (e) {
return false;
}
}
List<TronToken> get tronTokenCurrencies => tronTokensBox.values.toList();
Future<void> addTronToken(TronToken token) async {
@@ -55,7 +55,7 @@ class PendingWowneroTransaction with PendingTransaction {
}
@override
Future<String?> commitUR() {
Future<Map<String, String>> commitUR() {
throw UnimplementedError();
}
}
+18
View File
@@ -174,6 +174,24 @@ abstract class WowneroWalletBase
@override
Future<void>? updateBalance() => null;
@override
Future<bool> checkNodeHealth() async {
try {
// Check if the wallet is currently connected to the daemon
final isConnected = wownero_wallet.isConnectedSync();
if (!isConnected) {
return false; // It's not connected to daemon
}
// Check to get current node height to ensure daemon is responsive
final nodeHeight = await wownero_wallet.getNodeHeight();
return nodeHeight > 0;
} catch (e) {
return false;
}
}
@override
Future<void> close({bool shouldCleanup = false}) async {
_listener?.stop();
+2 -2
View File
@@ -493,8 +493,8 @@ packages:
dependency: "direct main"
description:
path: "impls/monero.dart"
ref: a27fbcb24d91143715ed930a05aaa4d853fba1f2
resolved-ref: a27fbcb24d91143715ed930a05aaa4d853fba1f2
ref: b576312e4d466569cd03482b61c597b39a9f4dc3
resolved-ref: b576312e4d466569cd03482b61c597b39a9f4dc3
url: "https://github.com/mrcyjanek/monero_c"
source: git
version: "0.0.0"
+1 -1
View File
@@ -24,7 +24,7 @@ dependencies:
monero:
git:
url: https://github.com/mrcyjanek/monero_c
ref: a27fbcb24d91143715ed930a05aaa4d853fba1f2 # monero_c hash
ref: b576312e4d466569cd03482b61c597b39a9f4dc3 # monero_c hash
path: impls/monero.dart
mutex: ^3.1.0
@@ -49,7 +49,7 @@ class PendingZanoTransaction with PendingTransaction {
}
@override
Future<String?> commitUR() {
Future<Map<String, String>> commitUR() {
throw UnimplementedError();
}
}
+11
View File
@@ -411,6 +411,17 @@ abstract class ZanoWalletBase
@override
Future<void>? updateBalance() => null;
@override
Future<bool> checkNodeHealth() async {
try {
final status = await getWalletStatus();
return status.isDaemonConnected;
} catch (_) {
return false;
}
}
Future<void> updateTransactions() async {
try {
if (_isTransactionUpdating) {
+2 -2
View File
@@ -498,8 +498,8 @@ packages:
dependency: "direct main"
description:
path: "impls/monero.dart"
ref: a27fbcb24d91143715ed930a05aaa4d853fba1f2
resolved-ref: a27fbcb24d91143715ed930a05aaa4d853fba1f2
ref: b576312e4d466569cd03482b61c597b39a9f4dc3
resolved-ref: b576312e4d466569cd03482b61c597b39a9f4dc3
url: "https://github.com/mrcyjanek/monero_c"
source: git
version: "0.0.0"
+1 -1
View File
@@ -25,7 +25,7 @@ dependencies:
monero:
git:
url: https://github.com/mrcyjanek/monero_c
ref: a27fbcb24d91143715ed930a05aaa4d853fba1f2 # monero_c hash
ref: b576312e4d466569cd03482b61c597b39a9f4dc3 # monero_c hash
path: impls/monero.dart
dev_dependencies:
flutter_test:
+2 -2
View File
@@ -18,8 +18,8 @@ In order to build the latest version of Cake Wallet, simply run the following:
git clone --branch main https://github.com/cake-tech/cake_wallet.git
# NOTE: Replace `main` with the latest release tag available at https://github.com/cake-tech/cake_wallet/releases/latest.
cd cake_wallet
# docker build -t ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.0-go1.24.1-ruststablenightly . # Uncomment to build the docker image yourself instead of pulling it from the registry
docker run -v$(pwd):$(pwd) -w $(pwd) -i --rm ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.0-go1.24.1-ruststablenightly bash -x << EOF
# docker build -t ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.4-ndkr28-go1.24.1-ruststablenightly . # Uncomment to build the docker image yourself instead of pulling it from the registry
docker run -v$(pwd):$(pwd) -w $(pwd) -i --rm ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.4-ndkr28-go1.24.1-ruststablenightly bash -x << EOF
set -x -e
pushd scripts/android
source ./app_env.sh cakewallet
+4 -4
View File
@@ -7,7 +7,7 @@ The following are the system requirements to build Cake Wallet for your iOS devi
```txt
macOS 15.3.1
Xcode 16.2
Flutter 3.27.0
Flutter 3.27.4
```
NOTE: Newer versions of macOS and Xcode may also work, but have not been confirmed to work by the Cake team.
@@ -43,9 +43,9 @@ To enable iOS build support for Xcode, perform the following:
### 3. Installing Flutter
Install Flutter, specifically version `3.27.0` by following the [official docs](https://docs.flutter.dev/get-started/install/macos/desktop?tab=download).
Install Flutter, specifically version `3.27.4` by following the [official docs](https://docs.flutter.dev/get-started/install/macos/desktop?tab=download).
NOTE: as `3.27.0` is not the latest version, you'll need to download it from <https://docs.flutter.dev/release/archive> instead of the link in the docs above.
NOTE: as `3.27.4` is not the latest version, you'll need to download it from <https://docs.flutter.dev/release/archive> instead of the link in the docs above.
### 4. Installing Rust
@@ -65,7 +65,7 @@ The output of this command should appear like this, indicating successful instal
```zsh
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 3.27.0, on macOS 15.x.x)
[✓] Flutter (Channel stable, 3.27.4, on macOS 15.x.x)
[✓] Xcode - develop for iOS and macOS (Xcode 16.2)
```
+2 -2
View File
@@ -20,8 +20,8 @@ In order to build the latest version of Cake Wallet, simply run the following:
git clone --branch main https://github.com/cake-tech/cake_wallet.git
# NOTE: Replace `main` with the latest release tag available at https://github.com/cake-tech/cake_wallet/releases/latest.
cd cake_wallet
# docker build -t ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.0-go1.24.1-ruststablenightly . # Uncomment to build the docker image yourself instead of pulling it from the registry
docker run --privileged -v$(pwd):$(pwd) -w $(pwd) -i --rm ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.0-go1.24.1-ruststablenightly bash -x << EOF
# docker build -t ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.4-ndkr28-go1.24.1-ruststablenightly . # Uncomment to build the docker image yourself instead of pulling it from the registry
docker run --privileged -v$(pwd):$(pwd) -w $(pwd) -i --rm ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.4-ndkr28-go1.24.1-ruststablenightly bash -x << EOF
set -x -e
pushd scripts
./gen_android_manifest.sh
+4 -4
View File
@@ -7,7 +7,7 @@ The following are the system requirements to build Cake Wallet for your macOS de
```txt
macOS 15.3.1
Xcode 16.2
Flutter 3.27.0
Flutter 3.27.4
```
### 1. Installing dependencies
@@ -34,9 +34,9 @@ sudo xcodebuild -runFirstLaunch
### 3. Installing Flutter
Install Flutter, specifically version `3.27.0` by following the [official docs](https://docs.flutter.dev/get-started/install/macos/desktop?tab=download).
Install Flutter, specifically version `3.27.4` by following the [official docs](https://docs.flutter.dev/get-started/install/macos/desktop?tab=download).
NOTE: as `3.27.0` is not the latest version, you'll need to download it from <https://docs.flutter.dev/release/archive> instead of the link in the docs above.
NOTE: as `3.27.4` is not the latest version, you'll need to download it from <https://docs.flutter.dev/release/archive> instead of the link in the docs above.
### 4. Installing Rust
@@ -56,7 +56,7 @@ The output of this command should appear like this, indicating successful instal
```zsh
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 3.27.0, on macOS 15.x.x)
[✓] Flutter (Channel stable, 3.27.4, on macOS 15.x.x)
...
[✓] Xcode - develop for iOS and macOS (Xcode 16.2)
...
+3 -3
View File
@@ -6,18 +6,18 @@ The following are the system requirements to build Cake Wallet for your Windows
```txt
Windows 10 or later (64-bit), x86-64 based
Flutter 3.27.0
Flutter 3.27.4
```
### 1. Installing Flutter
Install Flutter, specifically version `3.27.0` by following the [official docs](https://docs.flutter.dev/get-started/install/windows).
Install Flutter, specifically version `3.27.4` by following the [official docs](https://docs.flutter.dev/get-started/install/windows).
In order for Flutter to function, you'll also need to enable Developer Mode:
Start Menu > search for "Run" > type `ms-settings:developers`, and turn on Developer Mode.
NOTE: as `3.27.0` is not the latest version, you'll need to download it from <https://docs.flutter.dev/release/archive> instead of the link in the docs above.
NOTE: as `3.27.4` is not the latest version, you'll need to download it from <https://docs.flutter.dev/release/archive> instead of the link in the docs above.
### 2. Install Development Tools
+25 -7
View File
@@ -18,6 +18,14 @@ class CWBitcoin extends Bitcoin {
passphrase: passphrase,
);
@override
WalletCredentials createBitcoinWalletFromKeys({
required String name,
required String password,
required String xpub,
}) =>
BitcoinWalletFromKeysCredentials(name: name, password: password, xpub: xpub);
@override
WalletCredentials createBitcoinRestoreWalletFromWIFCredentials(
{required String name,
@@ -62,11 +70,7 @@ class CWBitcoin extends Bitcoin {
final bitcoinWallet = wallet as ElectrumWallet;
final keys = bitcoinWallet.keys;
return <String, String>{
'wif': keys.wif,
'privateKey': keys.privateKey,
'publicKey': keys.publicKey
};
return bitcoinWallet.keys.toJson();
}
@override
@@ -126,7 +130,8 @@ class CWBitcoin extends Bitcoin {
extractedAddress: out.extractedAddress,
isParsedAddress: out.isParsedAddress,
formattedCryptoAmount: out.formattedCryptoAmount,
memo: out.memo))
memo: out.memo,
extra: out.extra))
.toList(),
priority: priority as BitcoinTransactionPriority,
feeRate: bitcoinFeeRate,
@@ -275,7 +280,14 @@ class CWBitcoin extends Bitcoin {
}
@override
List<ReceivePageOption> getBitcoinReceivePageOptions() => BitcoinReceivePageOption.all;
List<ReceivePageOption> getBitcoinReceivePageOptions(Object wallet) {
final bitcoinWallet = wallet as ElectrumWallet;
final keys = bitcoinWallet.keys;
if (keys.privateKey.isEmpty) {
return BitcoinReceivePageOption.allViewOnly;
}
return BitcoinReceivePageOption.all;
}
@override
List<ReceivePageOption> getLitecoinReceivePageOptions() {
@@ -727,6 +739,12 @@ class CWBitcoin extends Bitcoin {
}
}
@override
Future<void> commitPsbtUR(Object wallet, List<String> urCodes) {
final _wallet = wallet as BitcoinWalletBase;
return _wallet.commitPsbtUR(urCodes);
}
@override
String getPayjoinEndpoint(Object wallet) {
final _wallet = wallet as ElectrumWallet;
+126
View File
@@ -0,0 +1,126 @@
import 'dart:async';
import 'package:cw_core/node.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:cake_wallet/store/app_store.dart';
import 'package:cake_wallet/store/settings_store.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:hive/hive.dart';
class NodeSwitchingService {
NodeSwitchingService({
required this.appStore,
required this.settingsStore,
required this.nodeSource,
});
static const int _healthCheckIntervalSeconds = 30;
Timer? _healthCheckTimer;
bool _isSwitching = false;
bool get isSwitching => _isSwitching;
final AppStore appStore;
final SettingsStore settingsStore;
final Box<Node> nodeSource;
// Track used nodes to cycle through all trusted nodes
final Map<WalletType, List<dynamic>> _usedNodeKeys = {};
void startHealthCheckTimer() {
_healthCheckTimer?.cancel();
_healthCheckTimer = Timer.periodic(
Duration(seconds: _healthCheckIntervalSeconds),
(_) => performHealthCheck(),
);
performHealthCheck();
}
void stopHealthCheckTimer() {
_healthCheckTimer?.cancel();
_healthCheckTimer = null;
}
Future<void> performHealthCheck() async {
if (appStore.wallet == null) return;
if (!settingsStore.enableAutomaticNodeSwitching) return;
if (_isSwitching) return;
try {
final isHealthy = await appStore.wallet!.checkNodeHealth();
if (!isHealthy) {
await _switchToNextTrustedNode();
}
} catch (e) {
printV('Error during health check: $e');
}
}
/// Switch to the next available trusted node
Future<void> _switchToNextTrustedNode() async {
_isSwitching = true;
try {
final walletType = appStore.wallet!.type;
final currentNode = settingsStore.getCurrentNode(walletType);
// Get all trusted nodes for this wallet type
final trustedNodes = nodeSource.values
.where((node) => node.type == walletType && node.isEnabledForAutoSwitching)
.toList();
if (trustedNodes.isEmpty) {
printV('No trusted nodes available for switching');
return;
}
// Initialize used nodes list for this wallet type if it does not exist
_usedNodeKeys.putIfAbsent(walletType, () => []);
// Add current node to used list if not already there
if (!_usedNodeKeys[walletType]!.contains(currentNode.key)) {
_usedNodeKeys[walletType]!.add(currentNode.key);
}
// Get next unused trusted node from the list
Node? nextNode;
for (final node in trustedNodes) {
if (!_usedNodeKeys[walletType]!.contains(node.key)) {
nextNode = node;
break;
}
}
// If all trusted nodes have been used, reset the list and start over
if (nextNode == null) {
printV('All trusted nodes have been tried, resetting and starting over');
_usedNodeKeys[walletType]!.clear();
nextNode = trustedNodes.first;
}
// Add the next node to used list
_usedNodeKeys[walletType]!.add(nextNode.key);
printV('Switching from ${currentNode.uriRaw} to ${nextNode.uriRaw}');
printV('Used nodes for ${walletType}: ${_usedNodeKeys[walletType]}');
// Update the current node in settings
settingsStore.nodes[walletType] = nextNode;
// Connect the wallet to the new node
await appStore.wallet!.connectToNode(node: nextNode);
await appStore.wallet!.startSync();
printV('Successfully switched to node: ${nextNode.uriRaw}');
} catch (e) {
printV('Error switching to next trusted node: $e');
} finally {
_isSwitching = false;
}
}
}
+8 -1
View File
@@ -23,6 +23,7 @@ import 'package:cake_wallet/core/totp_request_details.dart';
import 'package:cake_wallet/core/wallet_creation_service.dart';
import 'package:cake_wallet/core/wallet_loading_service.dart';
import 'package:cake_wallet/core/yat_service.dart';
import 'package:cake_wallet/core/node_switching_service.dart';
import 'package:cake_wallet/entities/biometric_auth.dart';
import 'package:cake_wallet/entities/contact.dart';
import 'package:cake_wallet/entities/contact_record.dart';
@@ -977,7 +978,7 @@ Future<void> setup({
getIt.registerFactory(() => AnimatedURModel(getIt.get<AppStore>()));
getIt.registerFactoryParam<AnimatedURPage, String, void>((String urQr, _) =>
getIt.registerFactoryParam<AnimatedURPage, Map<String, String>, void>((Map<String, String> urQr, _) =>
AnimatedURPage(getIt.get<AnimatedURModel>(), urQr: urQr));
getIt.registerFactoryParam<ContactViewModel, ContactRecord?, void>(
@@ -1516,5 +1517,11 @@ Future<void> setup({
getIt.registerFactory(() => DEuroSavingsPage(getIt<DEuroViewModel>()));
getIt.registerLazySingleton(() => NodeSwitchingService(
appStore: getIt.get<AppStore>(),
settingsStore: getIt.get<SettingsStore>(),
nodeSource: _nodeSource,
));
_isSetupFinished = true;
}
+25 -1
View File
@@ -1,3 +1,6 @@
import 'dart:convert';
import 'dart:isolate';
import 'package:basic_utils/basic_utils.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/widgets/alert_with_picker_option.dart';
@@ -5,6 +8,7 @@ import 'package:cake_wallet/src/widgets/picker.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:dnssec_proof/dnssec_proof.dart';
import 'package:flutter/material.dart';
class Bip353Record {
@@ -22,6 +26,19 @@ class Bip353Record {
'address': 'On-Chain Address',
};
static Future<String?> fetchDnsProof(String bip353Name) async {
if (bip353Name.startsWith('₿')) {
bip353Name = bip353Name.substring(1);
}
final parts = bip353Name.split('@');
if (parts.length != 2) return null;
final userPart = parts[0];
final domainPart = parts[1];
final bip353Domain = '$userPart.user._bitcoin-payment.$domainPart.';
final proof = await Isolate.run(() => DnsProver.getTxtProof(bip353Domain));
return base64.encode(proof);
}
static Future<Map<String, String>?> fetchUriByCryptoCurrency(
String bip353Name, String asset) async {
try {
@@ -30,7 +47,10 @@ class Bip353Record {
if (parts.length != 2) return null;
final userPart = parts[0];
String userPart = parts[0];
if (userPart.startsWith('₿')) {
userPart = userPart.substring(1);
}
final domainPart = parts[1];
// 2. Construct the correct subdomain: "user._bitcoin-payment.domain"
@@ -42,6 +62,10 @@ class Bip353Record {
RRecordType.TXT,
dnssec: true,
);
final proof = await fetchDnsProof(bip353Name);
if (proof == null) {
throw Exception('DNSSEC proof not found');
}
if (txtRecords == null) return null;
+50 -3
View File
@@ -510,7 +510,10 @@ Future<void> defaultSettingsMigration(
providerName: "SwapTrade",
enabled: true,
);
break;
break;
case 50:
migrateExistingNodesToUseAutoSwitching(nodes: nodes, powNodes: powNodes);
break;
default:
break;
}
@@ -1088,7 +1091,7 @@ Future<void> checkCurrentNodes(
if (currentBitcoinElectrumServer == null) {
final cakeWalletElectrum =
Node(uri: cakeWalletBitcoinElectrumUri, type: WalletType.bitcoin, useSSL: false);
Node(uri: cakeWalletBitcoinElectrumUri, type: WalletType.bitcoin, useSSL: false, isEnabledForAutoSwitching: true);
await nodeSource.add(cakeWalletElectrum);
final cakeWalletElectrumTestnet =
Node(uri: publicBitcoinTestnetElectrumUri, type: WalletType.bitcoin, useSSL: false);
@@ -1188,7 +1191,7 @@ Future<void> resetBitcoinElectrumServer(
if (cakeWalletNode == null) {
cakeWalletNode =
Node(uri: cakeWalletBitcoinElectrumUri, type: WalletType.bitcoin, useSSL: false);
Node(uri: cakeWalletBitcoinElectrumUri, type: WalletType.bitcoin, useSSL: false, isEnabledForAutoSwitching: true);
// final cakeWalletElectrumTestnet =
// Node(uri: publicBitcoinTestnetElectrumUri, type: WalletType.bitcoin, useSSL: false);
// await nodeSource.add(cakeWalletElectrumTestnet);
@@ -1272,3 +1275,47 @@ Future<void> removeMoneroWorld(
);
}
}
Future<void> migrateExistingNodesToUseAutoSwitching(
{required Box<Node> nodes, required Box<Node> powNodes}) async {
final listOfDefaultNodesWithAutoSwitching = [
'bitcoincash.stackwallet.com:50002',
'bch.aftrek.org:50002',
'btc-electrum.cakewallet.com:50002',
'fulcrum.sethforprivacy.com:50002',
'default-spv-nodes',
'dcrd.sethforprivacy.com:9108',
'ethereum-rpc.publicnode.com',
'eth.nownodes.io',
'ltc-electrum.cakewallet.com:50002',
'litecoin.stackwallet.com:20063',
'nano.nownodes.io',
'rpc.nano.to',
'node.nautilus.io',
'rpc.nano.to',
'workers.perish.co',
'worker.nanoriver.cc',
'xmr-node.cakewallet.com:18081',
'node.sethforprivacy.com:443',
'nodes.hashvault.pro:18081',
'polygon-bor-rpc.publicnode.com',
'matic.nownodes.io',
'api.mainnet-beta.solana.com:443',
'solana-rpc.publicnode.com:443',
'solana-mainnet.core.chainstack.com',
'api.trongrid.io',
'trx.nownodes.io',
'node3.monerodevs.org:34568',
'node2.monerodevs.org:34568',
'37.27.100.59:10500',
'zano.cakewallet.com:11211',
'electrum.cakewallet.com:50002',
];
for (var node in [...nodes.values.toList(), ...powNodes.values.toList()]) {
if (listOfDefaultNodesWithAutoSwitching.contains(node.uriRaw)) {
node.isEnabledForAutoSwitching = true;
await node.save();
}
}
}
+7 -1
View File
@@ -344,7 +344,13 @@ class AddressResolver {
if (bip353AddressMap != null && bip353AddressMap.isNotEmpty) {
final chosenAddress = await Bip353Record.pickBip353AddressChoice(context, text, bip353AddressMap);
if (chosenAddress != null) {
return ParsedAddress.fetchBip353AddressAddress(address: chosenAddress, name: text);
try {
final dnsProof = await Bip353Record.fetchDnsProof(text);
return ParsedAddress.fetchBip353AddressAddress(address: chosenAddress, name: text, dnsProof: dnsProof);
} catch (e) {
printV('Bip353Record.fetchBip353AddressAddress error: $e');
return ParsedAddress.fetchBip353AddressAddress(address: chosenAddress, name: text);
}
}
}
+4
View File
@@ -26,6 +26,7 @@ class ParsedAddress {
this.profileImageUrl = '',
this.profileName = '',
this.parseFrom = ParseFrom.notParsed,
this.bip353DnsProof,
});
factory ParsedAddress.fetchEmojiAddress({
@@ -59,11 +60,13 @@ class ParsedAddress {
factory ParsedAddress.fetchBip353AddressAddress ({
required String address,
required String name,
String? dnsProof,
}) {
return ParsedAddress(
addresses: [address],
name: name,
parseFrom: ParseFrom.bip353,
bip353DnsProof: dnsProof,
);
}
@@ -178,4 +181,5 @@ class ParsedAddress {
final String profileImageUrl;
final String profileName;
final ParseFrom parseFrom;
final String? bip353DnsProof;
}
+1
View File
@@ -112,4 +112,5 @@ class PreferencesKey {
'${PreferencesKey.walletConnectPairingTopicsList}_${publicKey}';
static String backgroundSyncLastTrigger(String walletId) => 'background_sync_last_trigger_${walletId}';
static const backgroundSyncNotificationsEnabled = 'background_sync_notifications_enabled';
static const enableAutomaticNodeSwitching = 'enable_automatic_node_switching';
}
+4 -1
View File
@@ -6,6 +6,7 @@ import 'package:cake_wallet/app_scroll_behavior.dart';
import 'package:cake_wallet/buy/order.dart';
import 'package:cake_wallet/core/auth_service.dart';
import 'package:cake_wallet/core/background_sync.dart';
import 'package:cake_wallet/core/node_switching_service.dart';
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/entities/contact.dart';
import 'package:cake_wallet/entities/default_settings_migration.dart';
@@ -249,7 +250,7 @@ Future<void> initializeAppConfigs({bool loadWallet = true}) async {
payjoinSessionSource: payjoinSessionSource,
anonpayInvoiceInfo: anonpayInvoiceInfo,
havenSeedStore: havenSeedStore,
initialMigrationVersion: 49,
initialMigrationVersion: 50,
);
}
@@ -327,6 +328,7 @@ class AppState extends State<App> with SingleTickerProviderStateMixin {
final authService = getIt.get<AuthService>();
final linkViewModel = getIt.get<LinkViewModel>();
final tradeMonitor = getIt.get<TradeMonitor>();
final nodeSwitchingService = getIt.get<NodeSwitchingService>();
final settingsStore = appStore.settingsStore;
final statusBarColor = Colors.transparent;
final authenticationStore = getIt.get<AuthenticationStore>();
@@ -351,6 +353,7 @@ class AppState extends State<App> with SingleTickerProviderStateMixin {
authService: authService,
linkViewModel: linkViewModel,
tradeMonitor: tradeMonitor,
nodeSwitchingService: nodeSwitchingService,
child: ThemeProvider(
themeStore: appStore.themeStore,
materialAppBuilder: (context, theme, darkTheme, themeMode) => MaterialApp(
+2 -2
View File
@@ -399,9 +399,9 @@ class CWMonero extends Monero {
}
@override
String exportOutputsUR(Object wallet, bool all) {
Map<String, String> exportOutputsUR(Object wallet) {
final moneroWallet = wallet as MoneroWallet;
return moneroWallet.exportOutputsUR(all);
return moneroWallet.exportOutputsUR();
}
@override
+3
View File
@@ -14,6 +14,7 @@ import 'package:cake_wallet/store/app_store.dart';
import 'package:cake_wallet/store/settings_store.dart';
import 'package:cake_wallet/store/authentication_store.dart';
import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart';
import 'package:cake_wallet/core/node_switching_service.dart';
Future<void> bootstrapOffline() async {
final authenticationStore = getIt.get<AuthenticationStore>();
@@ -40,4 +41,6 @@ void bootstrapOnline(GlobalKey<NavigatorState> navigatorKey, {required bool load
startCurrentFiatApiModeChangeReaction(appStore, settingsStore, fiatConversionStore);
startOnCurrentNodeChangeReaction(appStore);
startFiatRateUpdate(appStore, settingsStore, fiatConversionStore);
getIt.get<NodeSwitchingService>().startHealthCheckTimer();
}
@@ -197,9 +197,9 @@ class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
Padding(
padding: EdgeInsets.only(left: 20, right: 20, bottom: 20),
child: Text(
Platform.isIOS
? S.of(context).connect_your_hardware_wallet_ios
: S.of(context).connect_your_hardware_wallet,
Platform.isAndroid
? S.of(context).connect_your_hardware_wallet
: S.of(context).connect_your_hardware_wallet_ios,
style: Theme.of(context)
.textTheme.titleMedium,
textAlign: TextAlign.center,
@@ -174,8 +174,12 @@ class ExchangeTradeState extends State<ExchangeTradeForm> {
onPressed: () async {
Navigator.of(context).pushNamed(Routes.exchangeTradeExternalSendPage);
},
color: Theme.of(context).colorScheme.surfaceContainer,
textColor: Theme.of(context).colorScheme.onSecondaryContainer,
color: widget.exchangeTradeViewModel.isSendable
? Theme.of(context).colorScheme.surfaceContainer
: Theme.of(context).colorScheme.primary,
textColor: widget.exchangeTradeViewModel.isSendable
? Theme.of(context).colorScheme.onSecondaryContainer
: Theme.of(context).colorScheme.onPrimary,
),
SizedBox(height: 16),
Observer(
@@ -183,18 +187,19 @@ class ExchangeTradeState extends State<ExchangeTradeForm> {
final trade = widget.exchangeTradeViewModel.trade;
final sendingState = widget.exchangeTradeViewModel.sendViewModel.state;
return widget.exchangeTradeViewModel.isSendable &&
!(sendingState is TransactionCommitted)
? LoadingPrimaryButton(
key: ValueKey('exchange_trade_page_send_from_cake_button_key'),
isDisabled: trade.inputAddress == null || trade.inputAddress!.isEmpty,
isLoading: sendingState is IsExecutingState,
onPressed: () => widget.exchangeTradeViewModel.confirmSending(),
text: S.current.send_from_cake_wallet,
color: Theme.of(context).colorScheme.primary,
textColor: Theme.of(context).colorScheme.onPrimary,
)
: Offstage();
return Offstage(
offstage: !(widget.exchangeTradeViewModel.isSendable &&
!(sendingState is TransactionCommitted)),
child: LoadingPrimaryButton(
key: ValueKey('exchange_trade_page_send_from_cake_button_key'),
isDisabled: trade.inputAddress == null || trade.inputAddress!.isEmpty,
isLoading: sendingState is IsExecutingState,
onPressed: () => widget.exchangeTradeViewModel.confirmSending(),
text: S.current.send_from_cake_wallet,
color: Theme.of(context).colorScheme.primary,
textColor: Theme.of(context).colorScheme.onPrimary,
),
);
},
),
],
@@ -9,7 +9,9 @@ import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart';
import 'package:cake_wallet/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart';
import 'package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart';
import 'package:cake_wallet/src/widgets/bottom_sheet/tooltip_bottom_sheet.dart';
import 'package:cake_wallet/src/widgets/gradient_background.dart';
import 'package:cake_wallet/utils/responsive_layout_util.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/integrations/deuro_view_model.dart';
import 'package:cake_wallet/view_model/send/send_view_model_state.dart';
@@ -37,86 +39,71 @@ class DEuroSavingsPage extends BasePage {
@override
String get title => S.current.deuro_savings;
Widget trailing(BuildContext context) => MergeSemantics(
child: SizedBox(
height: 37,
width: 37,
child: ButtonTheme(
minWidth: double.minPositive,
child: Semantics(
label: "Refresh",
child: TextButton(
style: TextButton.styleFrom(
foregroundColor: Theme.of(context).colorScheme.onSurface,
overlayColor: WidgetStateColor.resolveWith((states) => Colors.transparent),
),
onPressed: _dEuroViewModel.reloadSavingsUserData,
child: Icon(
Icons.refresh,
color: pageIconColor(context),
size: 20,
),
),
),
),
),
);
@override
Widget body(BuildContext context) {
WidgetsBinding.instance.addPostFrameCallback((_) => _setReactions(context, _dEuroViewModel));
return Container(
width: double.infinity,
child: Column(
children: <Widget>[
Observer(
builder: (_) => SavingsCard(
isDarkTheme: currentTheme.isDark,
interestRate: "${_dEuroViewModel.interestRate}%",
savingsBalance: _dEuroViewModel.savingsBalance,
fiatSavingsBalance: _dEuroViewModel.fiatSavingsBalance,
currency: CryptoCurrency.deuro,
fiatCurrency: _dEuroViewModel.isFiatDisabled ? null : _dEuroViewModel.fiat,
onAddSavingsPressed: () => _onSavingsAdd(context),
onRemoveSavingsPressed: () => _onSavingsRemove(context),
onApproveSavingsPressed: _dEuroViewModel.prepareApproval,
onTooltipPressed: () => _onSavingsTooltipPressed(context),
isEnabled: _dEuroViewModel.isEnabled,
isLoading: _dEuroViewModel.isLoading,
),
),
Observer(
builder: (_) => InterestCardWidget(
isDarkTheme: currentTheme.isDark,
title: S.of(context).deuro_savings_collect_interest,
fiatAccruedInterest: _dEuroViewModel.fiatAccruedInterest,
fiatCurrency: _dEuroViewModel.isFiatDisabled ? null : _dEuroViewModel.fiat,
accruedInterest: _dEuroViewModel.accruedInterest,
onCollectInterest: _dEuroViewModel.prepareCollectInterest,
onReinvestInterest: _dEuroViewModel.prepareReinvestInterest,
onTooltipPressed: () => _onInterestTooltipPressed(context),
isEnabled: _dEuroViewModel.isEnabled,
),
),
Spacer(),
SafeArea(
child: Padding(
padding: EdgeInsets.only(bottom: 30),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
InfoChip(
label: S.of(context).deuro_about_deuro,
icon: CupertinoIcons.info,
onPressed: () => _showWelcomeTooltip(context),
return RefreshIndicator(
displacement: responsiveLayoutUtil.screenHeight * 0.1,
onRefresh: _dEuroViewModel.reloadSavingsUserData,
child: CustomScrollView(
slivers: <Widget>[
SliverFillRemaining(
child: SizedBox(
width: double.infinity,
child: Column(
children: <Widget>[
Observer(
builder: (_) => SavingsCard(
isDarkTheme: currentTheme.isDark,
interestRate: "${_dEuroViewModel.interestRateFormated}%",
savingsBalance: _dEuroViewModel.savingsBalanceFormated,
fiatSavingsBalance: _dEuroViewModel.fiatSavingsBalanceFormated,
currency: CryptoCurrency.deuro,
fiatCurrency: _dEuroViewModel.isFiatDisabled ? null : _dEuroViewModel.fiat,
onAddSavingsPressed: () => _onSavingsAdd(context),
onRemoveSavingsPressed: () => _onSavingsRemove(context),
onApproveSavingsPressed: _dEuroViewModel.prepareApproval,
onTooltipPressed: () => _onSavingsTooltipPressed(context),
isEnabled: _dEuroViewModel.isEnabled,
isLoading: _dEuroViewModel.isLoading,
),
),
Observer(
builder: (_) => InterestCardWidget(
isDarkTheme: currentTheme.isDark,
title: S.of(context).deuro_savings_collect_interest,
fiatAccruedInterest: _dEuroViewModel.fiatAccruedInterestFormated,
fiatCurrency: _dEuroViewModel.isFiatDisabled ? null : _dEuroViewModel.fiat,
accruedInterest: _dEuroViewModel.accruedInterestFormated,
onCollectInterest: _onCollectInterest,
onReinvestInterest: _onReinvestInterest,
onTooltipPressed: () => _onInterestTooltipPressed(context),
isEnabled: _dEuroViewModel.isSavingsActionsEnabled,
),
),
Spacer(),
SafeArea(
child: Padding(
padding: EdgeInsets.only(bottom: 30),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
InfoChip(
label: S.of(context).deuro_about_deuro,
icon: CupertinoIcons.info,
onPressed: () => _showWelcomeTooltip(context),
),
SizedBox(width: 10),
InfoChip(
label: S.of(context).website,
icon: CupertinoIcons.globe,
onPressed: () => launchUrlString("https://deuro.com/"),
)
],
),
),
),
SizedBox(width: 10),
InfoChip(
label: S.of(context).website,
icon: CupertinoIcons.globe,
onPressed: () => launchUrlString("https://deuro.com/"),
)
],
),
),
@@ -126,14 +113,36 @@ class DEuroSavingsPage extends BasePage {
);
}
bool _editSheetIsOpen = false;
Future<void> _onSavingsAdd(BuildContext context) async {
if (_editSheetIsOpen) return;
_editSheetIsOpen = true;
final amount = await _showEditBottomSheet(context, isAdding: true);
if (amount != null) _dEuroViewModel.prepareSavingsEdit(amount, true);
_editSheetIsOpen = false;
}
Future<void> _onSavingsRemove(BuildContext context) async {
if (_editSheetIsOpen) return;
_editSheetIsOpen = true;
final amount = await _showEditBottomSheet(context, isAdding: false);
if (amount != null) _dEuroViewModel.prepareSavingsEdit(amount, false);
_editSheetIsOpen = false;
}
Future<void> _onReinvestInterest() async {
if (_editSheetIsOpen) return;
_editSheetIsOpen = true;
await _dEuroViewModel.prepareReinvestInterest();
_editSheetIsOpen = false;
}
Future<void> _onCollectInterest() async {
if (_editSheetIsOpen) return;
_editSheetIsOpen = true;
await _dEuroViewModel.prepareCollectInterest();
_editSheetIsOpen = false;
}
bool _isReactionsSet = false;
@@ -175,10 +184,10 @@ class DEuroSavingsPage extends BasePage {
currency: CryptoCurrency.deuro,
amount: S.of(bottomSheetContext).send_amount,
amountValue: _dEuroViewModel.actionType == DEuroActionType.reinvest
? _dEuroViewModel.accruedInterest
? _dEuroViewModel.accruedInterestFormated
: tx.amountFormatted,
fiatAmountValue: _dEuroViewModel.actionType == DEuroActionType.reinvest
? _dEuroViewModel.fiatAccruedInterest
? _dEuroViewModel.fiatAccruedInterestFormated
: _dEuroViewModel.pendingTransactionFiatAmountFormatted,
fee: S.of(bottomSheetContext).send_estimated_fee,
feeValue: tx.feeFormatted,
@@ -301,11 +310,11 @@ class DEuroSavingsPage extends BasePage {
showModalBottomSheet<void>(
context: context,
builder: (BuildContext bottomSheetContext) => InfoBottomSheet(
builder: (BuildContext bottomSheetContext) => TooltipSheet(
currentTheme: currentTheme,
titleText: title,
titleIconPath: CryptoCurrency.deuro.iconPath,
content: content,
tooltip: content,
footerType: FooterType.doubleActionButton,
doubleActionRightButtonText: S.of(context).close,
rightActionButtonKey: ValueKey('deuro_page_tooltip_dialog_${key}_ok_button_key'),
@@ -358,7 +367,8 @@ class DEuroSavingsPage extends BasePage {
content: S.of(context).deuro_tooltip_no_eth,
singleActionButtonKey: ValueKey('deuro_page_tooltip_dialog_no_eth_ok_button_key'),
singleActionButtonText: S.of(context).close,
onSingleActionButtonPressed: () => Navigator.of(bottomSheetContext).pop(), footerType: FooterType.singleActionButton,
onSingleActionButtonPressed: () => Navigator.of(bottomSheetContext).pop(),
footerType: FooterType.singleActionButton,
),
);
}
@@ -375,7 +385,9 @@ class DEuroSavingsPage extends BasePage {
balanceTitle: isAdding
? S.of(context).deuro_savings_available_to_add
: S.of(context).deuro_savings_available_to_remove,
balance: isAdding ? _dEuroViewModel.accountBalance : _dEuroViewModel.savingsBalance,
balance: isAdding
? _dEuroViewModel.accountBalanceFormated
: _dEuroViewModel.savingsBalanceFormated,
footerType: FooterType.none,
maxHeight: MediaQuery.of(context).size.height * 0.8,
),
@@ -3,6 +3,7 @@ import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/integrations/deuro/widgets/numpad.dart';
import 'package:cake_wallet/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cw_core/parse_fixed.dart';
import 'package:flutter/material.dart';
class SavingsEditSheet extends BaseBottomSheet {
@@ -14,7 +15,8 @@ class SavingsEditSheet extends BaseBottomSheet {
super.titleIconPath,
this.balance,
this.balanceTitle,
required super.footerType, required super.maxHeight,
required super.footerType,
required super.maxHeight,
});
@override
@@ -53,8 +55,35 @@ class _SavingsEditBodyState extends State<_SavingsEditBody> {
}
String amount = '0';
bool isValid = false;
final FocusNode _numpadFocusNode = FocusNode();
void _onPressedAll() => setState(() {
amount = widget.balance!;
isValid = _validate();
});
void _onNumberPressed(int i) => setState(() {
amount = amount == '0' ? i.toString() : '${amount}${i}';
isValid = _validate();
});
void _onDeletePressed() => setState(() {
amount = amount.length > 1 ? amount.substring(0, amount.length - 1) : '0';
isValid = _validate();
});
void _onDecimalPressed() => setState(() {
amount = '${amount.replaceAll('.', '')}.';
isValid = _validate();
});
bool _validate() {
final amountBigInt = parseFixed(amount, 18);
final balanceBigInt = parseFixed(widget.balance!, 18);
return balanceBigInt >= amountBigInt && amountBigInt > BigInt.zero;
}
@override
Widget build(BuildContext context) => SafeArea(
child: Column(children: [
@@ -80,18 +109,14 @@ class _SavingsEditBodyState extends State<_SavingsEditBody> {
AssetBalanceRow(
title: widget.balanceTitle!,
amount: widget.balance!,
onAllPressed: () => setState(() => amount = widget.balance!),
onAllPressed: _onPressedAll,
),
],
NumberPad(
focusNode: _numpadFocusNode,
onNumberPressed: (i) => setState(
() => amount = amount == '0' ? i.toString() : '${amount}${i}',
),
onDeletePressed: () => setState(
() => amount = amount.length > 1 ? amount.substring(0, amount.length - 1) : '0',
),
onDecimalPressed: () => setState(() => amount = '${amount.replaceAll('.', '')}.'),
onNumberPressed: _onNumberPressed,
onDeletePressed: _onDeletePressed,
onDecimalPressed: _onDecimalPressed,
),
Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
@@ -101,7 +126,7 @@ class _SavingsEditBodyState extends State<_SavingsEditBody> {
color: Theme.of(context).colorScheme.primary,
textColor: Theme.of(context).colorScheme.onPrimary,
isLoading: false,
isDisabled: false,
isDisabled: !isValid,
),
)
]),
@@ -32,6 +32,7 @@ class NodeForm extends StatelessWidget {
..setLogin((editingNode!.login ?? ''))
..setSSL((editingNode!.isSSL))
..setTrusted((editingNode!.trusted))
..setIsEnabledForAutoSwitching((editingNode!.isEnabledForAutoSwitching))
..setSocksProxy((editingNode!.useSocksProxy))
..setSocksProxyAddress((editingNode!.socksProxyAddress ?? ''));
}
@@ -153,6 +154,25 @@ class NodeForm extends StatelessWidget {
),
),
SizedBox(height: 10.0),
Padding(
padding: EdgeInsets.only(top: 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
children: [
Observer(
builder: (_) => StandardCheckbox(
value: nodeViewModel.isEnabledForAutoSwitching,
borderColor: Theme.of(context).colorScheme.surfaceContainerHighest,
iconColor: Theme.of(context).colorScheme.primary,
onChanged: (value) => nodeViewModel.isEnabledForAutoSwitching = value,
caption: S.current.enable_for_auto_switching,
),
),
],
),
),
SizedBox(height: 10.0),
if (nodeViewModel.hasAuthCredentials) ...[
Row(
children: <Widget>[

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