Merge remote-tracking branch 'origin/main'
|
Before Width: | Height: | Size: 156 KiB After Width: | Height: | Size: 351 KiB |
@@ -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
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -94,7 +94,7 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
ndkVersion "27.0.12077973"
|
||||
ndkVersion "28.2.13676358"
|
||||
}
|
||||
|
||||
flutter {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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
|
||||
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 143 KiB |
|
After Width: | Height: | Size: 149 KiB |
|
After Width: | Height: | Size: 157 KiB |
|
After Width: | Height: | Size: 128 KiB |
|
After Width: | Height: | Size: 166 KiB |
|
After Width: | Height: | Size: 155 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 143 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 103 KiB |
|
After Width: | Height: | Size: 88 KiB |
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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,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
|
||||
@@ -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,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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,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,
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -18,5 +18,5 @@ mixin PendingTransaction {
|
||||
bool shouldCommitUR() => false;
|
||||
|
||||
Future<void> commit();
|
||||
Future<String?> commitUR();
|
||||
Future<Map<String, String>> commitUR();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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 {};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ class DecredPendingTransaction with PendingTransaction {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String?> commitUR() {
|
||||
Future<Map<String, String>> commitUR() {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ class PendingTronTransaction with PendingTransaction {
|
||||
String get id => '';
|
||||
|
||||
@override
|
||||
Future<String?> commitUR() {
|
||||
Future<Map<String, String>> commitUR() {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
...
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>[
|
||||
|
||||