Merge branch 'main' into CW-621-Open-external-sites-in-default-browser

This commit is contained in:
Serhii 2024-05-31 14:50:28 +03:00
commit 6b1dd1f780
133 changed files with 4195 additions and 1449 deletions

View file

@ -42,7 +42,7 @@ jobs:
- name: Flutter action - name: Flutter action
uses: subosito/flutter-action@v1 uses: subosito/flutter-action@v1
with: with:
flutter-version: "3.19.5" flutter-version: "3.19.6"
channel: stable channel: stable
- name: Install package dependencies - name: Install package dependencies

65
assets/images/cards.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 158 KiB

BIN
assets/images/tbtc.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

View file

@ -1,3 +1,2 @@
Add Tron wallet Bitcoin Silent Payments
Hardware wallets enhancements Bug fixes and generic enhancements
Bug fixes

View file

@ -3,8 +3,8 @@ import 'dart:convert';
import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:cw_bitcoin/script_hash.dart' as sh; import 'package:cw_bitcoin/script_hash.dart' as sh;
class BitcoinAddressRecord { abstract class BaseBitcoinAddressRecord {
BitcoinAddressRecord( BaseBitcoinAddressRecord(
this.address, { this.address, {
required this.index, required this.index,
this.isHidden = false, this.isHidden = false,
@ -13,15 +13,62 @@ class BitcoinAddressRecord {
String name = '', String name = '',
bool isUsed = false, bool isUsed = false,
required this.type, required this.type,
String? scriptHash,
required this.network, required this.network,
}) : _txCount = txCount, }) : _txCount = txCount,
_balance = balance, _balance = balance,
_name = name, _name = name,
_isUsed = isUsed, _isUsed = isUsed;
scriptHash = scriptHash ?? sh.scriptHash(address, network: network);
factory BitcoinAddressRecord.fromJSON(String jsonSource, BasedUtxoNetwork network) { @override
bool operator ==(Object o) => o is BaseBitcoinAddressRecord && address == o.address;
final String address;
bool isHidden;
final int index;
int _txCount;
int _balance;
String _name;
bool _isUsed;
BasedUtxoNetwork? network;
int get txCount => _txCount;
String get name => _name;
int get balance => _balance;
set txCount(int value) => _txCount = value;
set balance(int value) => _balance = value;
bool get isUsed => _isUsed;
void setAsUsed() => _isUsed = true;
void setNewName(String label) => _name = label;
int get hashCode => address.hashCode;
BitcoinAddressType type;
String toJSON();
}
class BitcoinAddressRecord extends BaseBitcoinAddressRecord {
BitcoinAddressRecord(
super.address, {
required super.index,
super.isHidden = false,
super.txCount = 0,
super.balance = 0,
super.name = '',
super.isUsed = false,
required super.type,
String? scriptHash,
required super.network,
}) : scriptHash =
scriptHash ?? (network != null ? sh.scriptHash(address, network: network) : null);
factory BitcoinAddressRecord.fromJSON(String jsonSource, {BasedUtxoNetwork? network}) {
final decoded = json.decode(jsonSource) as Map; final decoded = json.decode(jsonSource) as Map;
return BitcoinAddressRecord( return BitcoinAddressRecord(
@ -41,44 +88,15 @@ class BitcoinAddressRecord {
); );
} }
@override
bool operator ==(Object o) => o is BitcoinAddressRecord && address == o.address;
final String address;
bool isHidden;
final int index;
int _txCount;
int _balance;
String _name;
bool _isUsed;
String? scriptHash; String? scriptHash;
BasedUtxoNetwork network;
int get txCount => _txCount; String getScriptHash(BasedUtxoNetwork network) {
if (scriptHash != null) return scriptHash!;
String get name => _name;
int get balance => _balance;
set txCount(int value) => _txCount = value;
set balance(int value) => _balance = value;
bool get isUsed => _isUsed;
void setAsUsed() => _isUsed = true;
void setNewName(String label) => _name = label;
@override
int get hashCode => address.hashCode;
BitcoinAddressType type;
String updateScriptHash(BasedUtxoNetwork network) {
scriptHash = sh.scriptHash(address, network: network); scriptHash = sh.scriptHash(address, network: network);
return scriptHash!; return scriptHash!;
} }
@override
String toJSON() => json.encode({ String toJSON() => json.encode({
'address': address, 'address': address,
'index': index, 'index': index,
@ -91,3 +109,57 @@ class BitcoinAddressRecord {
'scriptHash': scriptHash, 'scriptHash': scriptHash,
}); });
} }
class BitcoinSilentPaymentAddressRecord extends BaseBitcoinAddressRecord {
BitcoinSilentPaymentAddressRecord(
super.address, {
required super.index,
super.isHidden = false,
super.txCount = 0,
super.balance = 0,
super.name = '',
super.isUsed = false,
required this.silentPaymentTweak,
required super.network,
required super.type,
}) : super();
factory BitcoinSilentPaymentAddressRecord.fromJSON(String jsonSource,
{BasedUtxoNetwork? network}) {
final decoded = json.decode(jsonSource) as Map;
return BitcoinSilentPaymentAddressRecord(
decoded['address'] as String,
index: decoded['index'] as int,
isHidden: decoded['isHidden'] as bool? ?? false,
isUsed: decoded['isUsed'] as bool? ?? false,
txCount: decoded['txCount'] as int? ?? 0,
name: decoded['name'] as String? ?? '',
balance: decoded['balance'] as int? ?? 0,
network: (decoded['network'] as String?) == null
? network
: BasedUtxoNetwork.fromName(decoded['network'] as String),
silentPaymentTweak: decoded['silent_payment_tweak'] as String?,
type: decoded['type'] != null && decoded['type'] != ''
? BitcoinAddressType.values
.firstWhere((type) => type.toString() == decoded['type'] as String)
: SilentPaymentsAddresType.p2sp,
);
}
final String? silentPaymentTweak;
@override
String toJSON() => json.encode({
'address': address,
'index': index,
'isHidden': isHidden,
'isUsed': isUsed,
'txCount': txCount,
'name': name,
'balance': balance,
'type': type.toString(),
'network': network?.value,
'silent_payment_tweak': silentPaymentTweak,
});
}

View file

@ -8,6 +8,8 @@ class BitcoinReceivePageOption implements ReceivePageOption {
static const p2wsh = BitcoinReceivePageOption._('Segwit (P2WSH)'); static const p2wsh = BitcoinReceivePageOption._('Segwit (P2WSH)');
static const p2pkh = BitcoinReceivePageOption._('Legacy (P2PKH)'); static const p2pkh = BitcoinReceivePageOption._('Legacy (P2PKH)');
static const silent_payments = BitcoinReceivePageOption._('Silent Payments');
const BitcoinReceivePageOption._(this.value); const BitcoinReceivePageOption._(this.value);
final String value; final String value;
@ -17,6 +19,7 @@ class BitcoinReceivePageOption implements ReceivePageOption {
} }
static const all = [ static const all = [
BitcoinReceivePageOption.silent_payments,
BitcoinReceivePageOption.p2wpkh, BitcoinReceivePageOption.p2wpkh,
BitcoinReceivePageOption.p2tr, BitcoinReceivePageOption.p2tr,
BitcoinReceivePageOption.p2wsh, BitcoinReceivePageOption.p2wsh,
@ -24,6 +27,24 @@ class BitcoinReceivePageOption implements ReceivePageOption {
BitcoinReceivePageOption.p2pkh BitcoinReceivePageOption.p2pkh
]; ];
BitcoinAddressType toType() {
switch (this) {
case BitcoinReceivePageOption.p2tr:
return SegwitAddresType.p2tr;
case BitcoinReceivePageOption.p2wsh:
return SegwitAddresType.p2wsh;
case BitcoinReceivePageOption.p2pkh:
return P2pkhAddressType.p2pkh;
case BitcoinReceivePageOption.p2sh:
return P2shAddressType.p2wpkhInP2sh;
case BitcoinReceivePageOption.silent_payments:
return SilentPaymentsAddresType.p2sp;
case BitcoinReceivePageOption.p2wpkh:
default:
return SegwitAddresType.p2wpkh;
}
}
factory BitcoinReceivePageOption.fromType(BitcoinAddressType type) { factory BitcoinReceivePageOption.fromType(BitcoinAddressType type) {
switch (type) { switch (type) {
case SegwitAddresType.p2tr: case SegwitAddresType.p2tr:
@ -34,6 +55,8 @@ class BitcoinReceivePageOption implements ReceivePageOption {
return BitcoinReceivePageOption.p2pkh; return BitcoinReceivePageOption.p2pkh;
case P2shAddressType.p2wpkhInP2sh: case P2shAddressType.p2wpkhInP2sh:
return BitcoinReceivePageOption.p2sh; return BitcoinReceivePageOption.p2sh;
case SilentPaymentsAddresType.p2sp:
return BitcoinReceivePageOption.silent_payments;
case SegwitAddresType.p2wpkh: case SegwitAddresType.p2wpkh:
default: default:
return BitcoinReceivePageOption.p2wpkh; return BitcoinReceivePageOption.p2wpkh;

View file

@ -2,13 +2,66 @@ import 'package:cw_bitcoin/bitcoin_address_record.dart';
import 'package:cw_core/unspent_transaction_output.dart'; import 'package:cw_core/unspent_transaction_output.dart';
class BitcoinUnspent extends Unspent { class BitcoinUnspent extends Unspent {
BitcoinUnspent(BitcoinAddressRecord addressRecord, String hash, int value, int vout) BitcoinUnspent(BaseBitcoinAddressRecord addressRecord, String hash, int value, int vout)
: bitcoinAddressRecord = addressRecord, : bitcoinAddressRecord = addressRecord,
super(addressRecord.address, hash, value, vout, null); super(addressRecord.address, hash, value, vout, null);
factory BitcoinUnspent.fromJSON(BitcoinAddressRecord address, Map<String, dynamic> json) => factory BitcoinUnspent.fromJSON(BaseBitcoinAddressRecord? address, Map<String, dynamic> json) =>
BitcoinUnspent( BitcoinUnspent(
address, json['tx_hash'] as String, json['value'] as int, json['tx_pos'] as int); address ?? BitcoinAddressRecord.fromJSON(json['address_record'].toString()),
json['tx_hash'] as String,
json['value'] as int,
json['tx_pos'] as int,
);
final BitcoinAddressRecord bitcoinAddressRecord; Map<String, dynamic> toJson() {
final json = <String, dynamic>{
'address_record': bitcoinAddressRecord.toJSON(),
'tx_hash': hash,
'value': value,
'tx_pos': vout,
};
return json;
}
final BaseBitcoinAddressRecord bitcoinAddressRecord;
}
class BitcoinSilentPaymentsUnspent extends BitcoinUnspent {
BitcoinSilentPaymentsUnspent(
BitcoinSilentPaymentAddressRecord addressRecord,
String hash,
int value,
int vout, {
required this.silentPaymentTweak,
required this.silentPaymentLabel,
}) : super(addressRecord, hash, value, vout);
@override
factory BitcoinSilentPaymentsUnspent.fromJSON(
BitcoinSilentPaymentAddressRecord? address, Map<String, dynamic> json) =>
BitcoinSilentPaymentsUnspent(
address ?? BitcoinSilentPaymentAddressRecord.fromJSON(json['address_record'].toString()),
json['tx_hash'] as String,
json['value'] as int,
json['tx_pos'] as int,
silentPaymentTweak: json['silent_payment_tweak'] as String?,
silentPaymentLabel: json['silent_payment_label'] as String?,
);
@override
Map<String, dynamic> toJson() {
final json = <String, dynamic>{
'address_record': bitcoinAddressRecord.toJSON(),
'tx_hash': hash,
'value': value,
'tx_pos': vout,
'silent_payment_tweak': silentPaymentTweak,
'silent_payment_label': silentPaymentLabel,
};
return json;
}
String? silentPaymentTweak;
String? silentPaymentLabel;
} }

View file

@ -39,22 +39,28 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
Map<String, int>? initialRegularAddressIndex, Map<String, int>? initialRegularAddressIndex,
Map<String, int>? initialChangeAddressIndex, Map<String, int>? initialChangeAddressIndex,
String? passphrase, String? passphrase,
List<BitcoinSilentPaymentAddressRecord>? initialSilentAddresses,
int initialSilentAddressIndex = 0,
bool? alwaysScan,
}) : super( }) : super(
mnemonic: mnemonic, mnemonic: mnemonic,
passphrase: passphrase, passphrase: passphrase,
xpub: xpub, xpub: xpub,
password: password, password: password,
walletInfo: walletInfo, walletInfo: walletInfo,
unspentCoinsInfo: unspentCoinsInfo, unspentCoinsInfo: unspentCoinsInfo,
networkType: networkParam == null networkType: networkParam == null
? bitcoin.bitcoin
: networkParam == BitcoinNetwork.mainnet
? bitcoin.bitcoin ? bitcoin.bitcoin
: bitcoin.testnet, : networkParam == BitcoinNetwork.mainnet
initialAddresses: initialAddresses, ? bitcoin.bitcoin
initialBalance: initialBalance, : bitcoin.testnet,
seedBytes: seedBytes, initialAddresses: initialAddresses,
currency: CryptoCurrency.btc) { initialBalance: initialBalance,
seedBytes: seedBytes,
currency:
networkParam == BitcoinNetwork.testnet ? CryptoCurrency.tbtc : CryptoCurrency.btc,
alwaysScan: alwaysScan,
) {
// in a standard BIP44 wallet, mainHd derivation path = m/84'/0'/0'/0 (account 0, index unspecified here) // in a standard BIP44 wallet, mainHd derivation path = m/84'/0'/0'/0 (account 0, index unspecified here)
// the sideHd derivation path = m/84'/0'/0'/1 (account 1, index unspecified here) // the sideHd derivation path = m/84'/0'/0'/1 (account 1, index unspecified here)
// String derivationPath = walletInfo.derivationInfo!.derivationPath!; // String derivationPath = walletInfo.derivationInfo!.derivationPath!;
@ -62,14 +68,18 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
// final hd = bitcoin.HDWallet.fromSeed(seedBytes, network: networkType); // final hd = bitcoin.HDWallet.fromSeed(seedBytes, network: networkType);
walletAddresses = BitcoinWalletAddresses( walletAddresses = BitcoinWalletAddresses(
walletInfo, walletInfo,
electrumClient: electrumClient,
initialAddresses: initialAddresses, initialAddresses: initialAddresses,
initialRegularAddressIndex: initialRegularAddressIndex, initialRegularAddressIndex: initialRegularAddressIndex,
initialChangeAddressIndex: initialChangeAddressIndex, initialChangeAddressIndex: initialChangeAddressIndex,
initialSilentAddresses: initialSilentAddresses,
initialSilentAddressIndex: initialSilentAddressIndex,
mainHd: hd, mainHd: hd,
sideHd: accountHD.derive(1), sideHd: accountHD.derive(1),
network: networkParam ?? network, network: networkParam ?? network,
masterHd:
seedBytes != null ? bitcoin.HDWallet.fromSeed(seedBytes, network: networkType) : null,
); );
autorun((_) { autorun((_) {
this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress; this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress;
}); });
@ -84,9 +94,11 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
String? addressPageType, String? addressPageType,
BasedUtxoNetwork? network, BasedUtxoNetwork? network,
List<BitcoinAddressRecord>? initialAddresses, List<BitcoinAddressRecord>? initialAddresses,
List<BitcoinSilentPaymentAddressRecord>? initialSilentAddresses,
ElectrumBalance? initialBalance, ElectrumBalance? initialBalance,
Map<String, int>? initialRegularAddressIndex, Map<String, int>? initialRegularAddressIndex,
Map<String, int>? initialChangeAddressIndex, Map<String, int>? initialChangeAddressIndex,
int initialSilentAddressIndex = 0,
}) async { }) async {
late Uint8List seedBytes; late Uint8List seedBytes;
@ -109,6 +121,8 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
walletInfo: walletInfo, walletInfo: walletInfo,
unspentCoinsInfo: unspentCoinsInfo, unspentCoinsInfo: unspentCoinsInfo,
initialAddresses: initialAddresses, initialAddresses: initialAddresses,
initialSilentAddresses: initialSilentAddresses,
initialSilentAddressIndex: initialSilentAddressIndex,
initialBalance: initialBalance, initialBalance: initialBalance,
seedBytes: seedBytes, seedBytes: seedBytes,
initialRegularAddressIndex: initialRegularAddressIndex, initialRegularAddressIndex: initialRegularAddressIndex,
@ -123,6 +137,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
required WalletInfo walletInfo, required WalletInfo walletInfo,
required Box<UnspentCoinsInfo> unspentCoinsInfo, required Box<UnspentCoinsInfo> unspentCoinsInfo,
required String password, required String password,
required bool alwaysScan,
}) async { }) async {
final network = walletInfo.network != null final network = walletInfo.network != null
? BasedUtxoNetwork.fromName(walletInfo.network!) ? BasedUtxoNetwork.fromName(walletInfo.network!)
@ -163,12 +178,15 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
walletInfo: walletInfo, walletInfo: walletInfo,
unspentCoinsInfo: unspentCoinsInfo, unspentCoinsInfo: unspentCoinsInfo,
initialAddresses: snp.addresses, initialAddresses: snp.addresses,
initialSilentAddresses: snp.silentAddresses,
initialSilentAddressIndex: snp.silentAddressIndex,
initialBalance: snp.balance, initialBalance: snp.balance,
seedBytes: seedBytes, seedBytes: seedBytes,
initialRegularAddressIndex: snp.regularAddressIndex, initialRegularAddressIndex: snp.regularAddressIndex,
initialChangeAddressIndex: snp.changeAddressIndex, initialChangeAddressIndex: snp.changeAddressIndex,
addressPageType: snp.addressPageType, addressPageType: snp.addressPageType,
networkParam: network, networkParam: network,
alwaysScan: alwaysScan,
); );
} }
@ -179,7 +197,8 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
void setLedger(Ledger setLedger, LedgerDevice setLedgerDevice) { void setLedger(Ledger setLedger, LedgerDevice setLedgerDevice) {
_ledger = setLedger; _ledger = setLedger;
_ledgerDevice = setLedgerDevice; _ledgerDevice = setLedgerDevice;
_bitcoinLedgerApp = BitcoinLedgerApp(_ledger!, derivationPath: walletInfo.derivationInfo!.derivationPath!); _bitcoinLedgerApp =
BitcoinLedgerApp(_ledger!, derivationPath: walletInfo.derivationInfo!.derivationPath!);
} }
@override @override
@ -202,16 +221,17 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
final publicKeyAndDerivationPath = publicKeys[utxo.ownerDetails.address.pubKeyHash()]!; final publicKeyAndDerivationPath = publicKeys[utxo.ownerDetails.address.pubKeyHash()]!;
psbtReadyInputs.add(PSBTReadyUtxoWithAddress( psbtReadyInputs.add(PSBTReadyUtxoWithAddress(
utxo: utxo.utxo, utxo: utxo.utxo,
rawTx: rawTx, rawTx: rawTx,
ownerDetails: utxo.ownerDetails, ownerDetails: utxo.ownerDetails,
ownerDerivationPath: publicKeyAndDerivationPath.derivationPath, ownerDerivationPath: publicKeyAndDerivationPath.derivationPath,
ownerMasterFingerprint: masterFingerprint, ownerMasterFingerprint: masterFingerprint,
ownerPublicKey: publicKeyAndDerivationPath.publicKey, ownerPublicKey: publicKeyAndDerivationPath.publicKey,
)); ));
} }
final psbt = PSBTTransactionBuild(inputs: psbtReadyInputs, outputs: outputs, enableRBF: enableRBF); final psbt =
PSBTTransactionBuild(inputs: psbtReadyInputs, outputs: outputs, enableRBF: enableRBF);
final rawHex = await _bitcoinLedgerApp!.signPsbt(_ledgerDevice!, psbt: psbt.psbt); final rawHex = await _bitcoinLedgerApp!.signPsbt(_ledgerDevice!, psbt: psbt.psbt);
return BtcTransaction.fromRaw(hex.encode(rawHex)); return BtcTransaction.fromRaw(hex.encode(rawHex));

View file

@ -15,10 +15,12 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S
required super.mainHd, required super.mainHd,
required super.sideHd, required super.sideHd,
required super.network, required super.network,
required super.electrumClient,
super.initialAddresses, super.initialAddresses,
super.initialRegularAddressIndex, super.initialRegularAddressIndex,
super.initialChangeAddressIndex, super.initialChangeAddressIndex,
super.initialSilentAddresses,
super.initialSilentAddressIndex = 0,
super.masterHd,
}) : super(walletInfo); }) : super(walletInfo);
@override @override

View file

@ -19,10 +19,11 @@ class BitcoinWalletService extends WalletService<
BitcoinRestoreWalletFromSeedCredentials, BitcoinRestoreWalletFromSeedCredentials,
BitcoinRestoreWalletFromWIFCredentials, BitcoinRestoreWalletFromWIFCredentials,
BitcoinRestoreWalletFromHardware> { BitcoinRestoreWalletFromHardware> {
BitcoinWalletService(this.walletInfoSource, this.unspentCoinsInfoSource); BitcoinWalletService(this.walletInfoSource, this.unspentCoinsInfoSource, this.alwaysScan);
final Box<WalletInfo> walletInfoSource; final Box<WalletInfo> walletInfoSource;
final Box<UnspentCoinsInfo> unspentCoinsInfoSource; final Box<UnspentCoinsInfo> unspentCoinsInfoSource;
final bool alwaysScan;
@override @override
WalletType getType() => WalletType.bitcoin; WalletType getType() => WalletType.bitcoin;
@ -55,20 +56,24 @@ class BitcoinWalletService extends WalletService<
.firstWhereOrNull((info) => info.id == WalletBase.idFor(name, getType()))!; .firstWhereOrNull((info) => info.id == WalletBase.idFor(name, getType()))!;
try { try {
final wallet = await BitcoinWalletBase.open( final wallet = await BitcoinWalletBase.open(
password: password, password: password,
name: name, name: name,
walletInfo: walletInfo, walletInfo: walletInfo,
unspentCoinsInfo: unspentCoinsInfoSource); unspentCoinsInfo: unspentCoinsInfoSource,
alwaysScan: alwaysScan,
);
await wallet.init(); await wallet.init();
saveBackup(name); saveBackup(name);
return wallet; return wallet;
} catch (_) { } catch (_) {
await restoreWalletFilesFromBackup(name); await restoreWalletFilesFromBackup(name);
final wallet = await BitcoinWalletBase.open( final wallet = await BitcoinWalletBase.open(
password: password, password: password,
name: name, name: name,
walletInfo: walletInfo, walletInfo: walletInfo,
unspentCoinsInfo: unspentCoinsInfoSource); unspentCoinsInfo: unspentCoinsInfoSource,
alwaysScan: alwaysScan,
);
await wallet.init(); await wallet.init();
return wallet; return wallet;
} }
@ -87,10 +92,12 @@ class BitcoinWalletService extends WalletService<
final currentWalletInfo = walletInfoSource.values final currentWalletInfo = walletInfoSource.values
.firstWhereOrNull((info) => info.id == WalletBase.idFor(currentName, getType()))!; .firstWhereOrNull((info) => info.id == WalletBase.idFor(currentName, getType()))!;
final currentWallet = await BitcoinWalletBase.open( final currentWallet = await BitcoinWalletBase.open(
password: password, password: password,
name: currentName, name: currentName,
walletInfo: currentWalletInfo, walletInfo: currentWalletInfo,
unspentCoinsInfo: unspentCoinsInfoSource); unspentCoinsInfo: unspentCoinsInfoSource,
alwaysScan: alwaysScan,
);
await currentWallet.renameWalletFiles(newName); await currentWallet.renameWalletFiles(newName);
await saveBackup(newName); await saveBackup(newName);
@ -105,12 +112,13 @@ class BitcoinWalletService extends WalletService<
@override @override
Future<BitcoinWallet> restoreFromHardwareWallet(BitcoinRestoreWalletFromHardware credentials, Future<BitcoinWallet> restoreFromHardwareWallet(BitcoinRestoreWalletFromHardware credentials,
{bool? isTestnet}) async { {bool? isTestnet}) async {
final network = isTestnet == true ? BitcoinNetwork.testnet : BitcoinNetwork.mainnet; final network = isTestnet == true ? BitcoinNetwork.testnet : BitcoinNetwork.mainnet;
credentials.walletInfo?.network = network.value; credentials.walletInfo?.network = network.value;
credentials.walletInfo?.derivationInfo?.derivationPath = credentials.hwAccountData.derivationPath; credentials.walletInfo?.derivationInfo?.derivationPath =
credentials.hwAccountData.derivationPath;
final wallet = await BitcoinWallet(password: credentials.password!, final wallet = await BitcoinWallet(
password: credentials.password!,
xpub: credentials.hwAccountData.xpub, xpub: credentials.hwAccountData.xpub,
walletInfo: credentials.walletInfo!, walletInfo: credentials.walletInfo!,
unspentCoinsInfo: unspentCoinsInfoSource, unspentCoinsInfo: unspentCoinsInfoSource,
@ -123,7 +131,7 @@ class BitcoinWalletService extends WalletService<
@override @override
Future<BitcoinWallet> restoreFromKeys(BitcoinRestoreWalletFromWIFCredentials credentials, Future<BitcoinWallet> restoreFromKeys(BitcoinRestoreWalletFromWIFCredentials credentials,
{bool? isTestnet}) async => {bool? isTestnet}) async =>
throw UnimplementedError(); throw UnimplementedError();
@override @override

View file

@ -41,23 +41,35 @@ class ElectrumClient {
bool get isConnected => _isConnected; bool get isConnected => _isConnected;
Socket? socket; Socket? socket;
void Function(bool)? onConnectionStatusChange; void Function(bool?)? onConnectionStatusChange;
int _id; int _id;
final Map<String, SocketTask> _tasks; final Map<String, SocketTask> _tasks;
Map<String, SocketTask> get tasks => _tasks;
final Map<String, String> _errors; final Map<String, String> _errors;
bool _isConnected; bool _isConnected;
Timer? _aliveTimer; Timer? _aliveTimer;
String unterminatedString; String unterminatedString;
Future<void> connectToUri(Uri uri) async => await connect(host: uri.host, port: uri.port); Uri? uri;
bool? useSSL;
Future<void> connect({required String host, required int port}) async { Future<void> connectToUri(Uri uri, {bool? useSSL}) async {
this.uri = uri;
this.useSSL = useSSL;
await connect(host: uri.host, port: uri.port, useSSL: useSSL);
}
Future<void> connect({required String host, required int port, bool? useSSL}) async {
try { try {
await socket?.close(); await socket?.close();
} catch (_) {} } catch (_) {}
socket = await SecureSocket.connect(host, port, if (useSSL == false) {
timeout: connectionTimeout, onBadCertificate: (_) => true); socket = await Socket.connect(host, port, timeout: connectionTimeout);
} else {
socket = await SecureSocket.connect(host, port,
timeout: connectionTimeout, onBadCertificate: (_) => true);
}
_setIsConnected(true); _setIsConnected(true);
socket!.listen((Uint8List event) { socket!.listen((Uint8List event) {
@ -79,7 +91,7 @@ class ElectrumClient {
_setIsConnected(false); _setIsConnected(false);
}, onDone: () { }, onDone: () {
unterminatedString = ''; unterminatedString = '';
_setIsConnected(false); _setIsConnected(null);
}); });
keepAlive(); keepAlive();
} }
@ -134,11 +146,12 @@ class ElectrumClient {
await callWithTimeout(method: 'server.ping'); await callWithTimeout(method: 'server.ping');
_setIsConnected(true); _setIsConnected(true);
} on RequestFailedTimeoutException catch (_) { } on RequestFailedTimeoutException catch (_) {
_setIsConnected(false); _setIsConnected(null);
} }
} }
Future<List<String>> version() => call(method: 'server.version').then((dynamic result) { Future<List<String>> version() =>
call(method: 'server.version', params: ["", "1.4"]).then((dynamic result) {
if (result is List) { if (result is List) {
return result.map((dynamic val) => val.toString()).toList(); return result.map((dynamic val) => val.toString()).toList();
} }
@ -266,6 +279,18 @@ class ElectrumClient {
Future<Map<String, dynamic>> getHeader({required int height}) async => Future<Map<String, dynamic>> getHeader({required int height}) async =>
await call(method: 'blockchain.block.get_header', params: [height]) as Map<String, dynamic>; await call(method: 'blockchain.block.get_header', params: [height]) as Map<String, dynamic>;
BehaviorSubject<Object>? tweaksSubscribe({required int height, required int count}) {
_id += 1;
return subscribe<Object>(
id: 'blockchain.tweaks.subscribe:${height + count}',
method: 'blockchain.tweaks.subscribe',
params: [height, count, false],
);
}
Future<dynamic> getTweaks({required int height}) async =>
await callWithTimeout(method: 'blockchain.tweaks.subscribe', params: [height, 1, false]);
Future<double> estimatefee({required int p}) => Future<double> estimatefee({required int p}) =>
call(method: 'blockchain.estimatefee', params: [p]).then((dynamic result) { call(method: 'blockchain.estimatefee', params: [p]).then((dynamic result) {
if (result is double) { if (result is double) {
@ -308,9 +333,6 @@ class ElectrumClient {
}); });
Future<List<int>> feeRates({BasedUtxoNetwork? network}) async { Future<List<int>> feeRates({BasedUtxoNetwork? network}) async {
if (network == BitcoinNetwork.testnet) {
return [1, 1, 1];
}
try { try {
final topDoubleString = await estimatefee(p: 1); final topDoubleString = await estimatefee(p: 1);
final middleDoubleString = await estimatefee(p: 5); final middleDoubleString = await estimatefee(p: 5);
@ -332,7 +354,7 @@ class ElectrumClient {
// "hex": "00000020890208a0ae3a3892aa047c5468725846577cfcd9b512b50000000000000000005dc2b02f2d297a9064ee103036c14d678f9afc7e3d9409cf53fd58b82e938e8ecbeca05a2d2103188ce804c4" // "hex": "00000020890208a0ae3a3892aa047c5468725846577cfcd9b512b50000000000000000005dc2b02f2d297a9064ee103036c14d678f9afc7e3d9409cf53fd58b82e938e8ecbeca05a2d2103188ce804c4"
// } // }
Future<int?> getCurrentBlockChainTip() => Future<int?> getCurrentBlockChainTip() =>
call(method: 'blockchain.headers.subscribe').then((result) { callWithTimeout(method: 'blockchain.headers.subscribe').then((result) {
if (result is Map<String, dynamic>) { if (result is Map<String, dynamic>) {
return result["height"] as int; return result["height"] as int;
} }
@ -340,6 +362,12 @@ class ElectrumClient {
return null; return null;
}); });
BehaviorSubject<Object>? chainTipSubscribe() {
_id += 1;
return subscribe<Object>(
id: 'blockchain.headers.subscribe', method: 'blockchain.headers.subscribe');
}
BehaviorSubject<Object>? scripthashUpdate(String scripthash) { BehaviorSubject<Object>? scripthashUpdate(String scripthash) {
_id += 1; _id += 1;
return subscribe<Object>( return subscribe<Object>(
@ -396,7 +424,9 @@ class ElectrumClient {
Future<void> close() async { Future<void> close() async {
_aliveTimer?.cancel(); _aliveTimer?.cancel();
await socket?.close(); try {
await socket?.close();
} catch (_) {}
onConnectionStatusChange = null; onConnectionStatusChange = null;
} }
@ -431,17 +461,25 @@ class ElectrumClient {
_tasks[id]?.subject?.add(params.last); _tasks[id]?.subject?.add(params.last);
break; break;
case 'blockchain.headers.subscribe':
final params = request['params'] as List<dynamic>;
_tasks[method]?.subject?.add(params.last);
break;
case 'blockchain.tweaks.subscribe':
final params = request['params'] as List<dynamic>;
_tasks[_tasks.keys.first]?.subject?.add(params.last);
break;
default: default:
break; break;
} }
} }
void _setIsConnected(bool isConnected) { void _setIsConnected(bool? isConnected) {
if (_isConnected != isConnected) { if (_isConnected != isConnected) {
onConnectionStatusChange?.call(isConnected); onConnectionStatusChange?.call(isConnected);
} }
_isConnected = isConnected; _isConnected = isConnected ?? false;
} }
void _handleResponse(Map<String, dynamic> response) { void _handleResponse(Map<String, dynamic> response) {

View file

@ -3,8 +3,11 @@ import 'package:cw_bitcoin/bitcoin_amount_format.dart';
import 'package:cw_core/balance.dart'; import 'package:cw_core/balance.dart';
class ElectrumBalance extends Balance { class ElectrumBalance extends Balance {
const ElectrumBalance({required this.confirmed, required this.unconfirmed, required this.frozen}) ElectrumBalance({
: super(confirmed, unconfirmed); required this.confirmed,
required this.unconfirmed,
required this.frozen,
}) : super(confirmed, unconfirmed);
static ElectrumBalance? fromJSON(String? jsonSource) { static ElectrumBalance? fromJSON(String? jsonSource) {
if (jsonSource == null) { if (jsonSource == null) {
@ -19,8 +22,8 @@ class ElectrumBalance extends Balance {
frozen: decoded['frozen'] as int? ?? 0); frozen: decoded['frozen'] as int? ?? 0);
} }
final int confirmed; int confirmed;
final int unconfirmed; int unconfirmed;
final int frozen; final int frozen;
@override @override

View file

@ -11,13 +11,11 @@ part 'electrum_transaction_history.g.dart';
const transactionsHistoryFileName = 'transactions.json'; const transactionsHistoryFileName = 'transactions.json';
class ElectrumTransactionHistory = ElectrumTransactionHistoryBase class ElectrumTransactionHistory = ElectrumTransactionHistoryBase with _$ElectrumTransactionHistory;
with _$ElectrumTransactionHistory;
abstract class ElectrumTransactionHistoryBase abstract class ElectrumTransactionHistoryBase
extends TransactionHistoryBase<ElectrumTransactionInfo> with Store { extends TransactionHistoryBase<ElectrumTransactionInfo> with Store {
ElectrumTransactionHistoryBase( ElectrumTransactionHistoryBase({required this.walletInfo, required String password})
{required this.walletInfo, required String password})
: _password = password, : _password = password,
_height = 0 { _height = 0 {
transactions = ObservableMap<String, ElectrumTransactionInfo>(); transactions = ObservableMap<String, ElectrumTransactionInfo>();
@ -30,8 +28,7 @@ abstract class ElectrumTransactionHistoryBase
Future<void> init() async => await _load(); Future<void> init() async => await _load();
@override @override
void addOne(ElectrumTransactionInfo transaction) => void addOne(ElectrumTransactionInfo transaction) => transactions[transaction.id] = transaction;
transactions[transaction.id] = transaction;
@override @override
void addMany(Map<String, ElectrumTransactionInfo> transactions) => void addMany(Map<String, ElectrumTransactionInfo> transactions) =>
@ -40,11 +37,13 @@ abstract class ElectrumTransactionHistoryBase
@override @override
Future<void> save() async { Future<void> save() async {
try { try {
final dirPath = final dirPath = await pathForWalletDir(name: walletInfo.name, type: walletInfo.type);
await pathForWalletDir(name: walletInfo.name, type: walletInfo.type);
final path = '$dirPath/$transactionsHistoryFileName'; final path = '$dirPath/$transactionsHistoryFileName';
final data = final txjson = {};
json.encode({'height': _height, 'transactions': transactions}); for (final tx in transactions.entries) {
txjson[tx.key] = tx.value.toJson();
}
final data = json.encode({'height': _height, 'transactions': txjson});
await writeData(path: path, password: _password, data: data); await writeData(path: path, password: _password, data: data);
} catch (e) { } catch (e) {
print('Error while save bitcoin transaction history: ${e.toString()}'); print('Error while save bitcoin transaction history: ${e.toString()}');
@ -57,8 +56,7 @@ abstract class ElectrumTransactionHistoryBase
} }
Future<Map<String, dynamic>> _read() async { Future<Map<String, dynamic>> _read() async {
final dirPath = final dirPath = await pathForWalletDir(name: walletInfo.name, type: walletInfo.type);
await pathForWalletDir(name: walletInfo.name, type: walletInfo.type);
final path = '$dirPath/$transactionsHistoryFileName'; final path = '$dirPath/$transactionsHistoryFileName';
final content = await read(path: path, password: _password); final content = await read(path: path, password: _password);
return json.decode(content) as Map<String, dynamic>; return json.decode(content) as Map<String, dynamic>;
@ -84,7 +82,5 @@ abstract class ElectrumTransactionHistoryBase
} }
} }
void _update(ElectrumTransactionInfo transaction) => void _update(ElectrumTransactionInfo transaction) => transactions[transaction.id] = transaction;
transactions[transaction.id] = transaction;
} }

View file

@ -1,9 +1,8 @@
import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
import 'package:bitcoin_flutter/src/payments/index.dart' show PaymentData;
import 'package:cw_bitcoin/address_from_output.dart'; import 'package:cw_bitcoin/address_from_output.dart';
import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart';
import 'package:cw_bitcoin/bitcoin_amount_format.dart'; import 'package:cw_bitcoin/bitcoin_amount_format.dart';
import 'package:cw_bitcoin/bitcoin_unspent.dart';
import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/transaction_direction.dart';
import 'package:cw_core/transaction_info.dart'; import 'package:cw_core/transaction_info.dart';
import 'package:cw_core/format_amount.dart'; import 'package:cw_core/format_amount.dart';
@ -19,6 +18,8 @@ class ElectrumTransactionBundle {
} }
class ElectrumTransactionInfo extends TransactionInfo { class ElectrumTransactionInfo extends TransactionInfo {
List<BitcoinSilentPaymentsUnspent>? unspents;
ElectrumTransactionInfo(this.type, ElectrumTransactionInfo(this.type,
{required String id, {required String id,
required int height, required int height,
@ -29,7 +30,9 @@ class ElectrumTransactionInfo extends TransactionInfo {
required TransactionDirection direction, required TransactionDirection direction,
required bool isPending, required bool isPending,
required DateTime date, required DateTime date,
required int confirmations}) { required int confirmations,
String? to,
this.unspents}) {
this.id = id; this.id = id;
this.height = height; this.height = height;
this.amount = amount; this.amount = amount;
@ -40,6 +43,7 @@ class ElectrumTransactionInfo extends TransactionInfo {
this.date = date; this.date = date;
this.isPending = isPending; this.isPending = isPending;
this.confirmations = confirmations; this.confirmations = confirmations;
this.to = to;
} }
factory ElectrumTransactionInfo.fromElectrumVerbose(Map<String, Object> obj, WalletType type, factory ElectrumTransactionInfo.fromElectrumVerbose(Map<String, Object> obj, WalletType type,
@ -153,52 +157,31 @@ class ElectrumTransactionInfo extends TransactionInfo {
confirmations: bundle.confirmations); confirmations: bundle.confirmations);
} }
factory ElectrumTransactionInfo.fromHexAndHeader(WalletType type, String hex,
{List<String>? addresses, required int height, int? timestamp, required int confirmations}) {
final tx = bitcoin.Transaction.fromHex(hex);
var exist = false;
var amount = 0;
if (addresses != null) {
tx.outs.forEach((out) {
try {
final p2pkh =
bitcoin.P2PKH(data: PaymentData(output: out.script), network: bitcoin.bitcoin);
exist = addresses.contains(p2pkh.data.address);
if (exist) {
amount += out.value!;
}
} catch (_) {}
});
}
final date =
timestamp != null ? DateTime.fromMillisecondsSinceEpoch(timestamp * 1000) : DateTime.now();
return ElectrumTransactionInfo(type,
id: tx.getId(),
height: height,
isPending: false,
fee: null,
direction: TransactionDirection.incoming,
amount: amount,
date: date,
confirmations: confirmations);
}
factory ElectrumTransactionInfo.fromJson(Map<String, dynamic> data, WalletType type) { factory ElectrumTransactionInfo.fromJson(Map<String, dynamic> data, WalletType type) {
return ElectrumTransactionInfo(type, final inputAddresses = data['inputAddresses'] as List<dynamic>? ?? [];
id: data['id'] as String, final outputAddresses = data['outputAddresses'] as List<dynamic>? ?? [];
height: data['height'] as int, final unspents = data['unspents'] as List<dynamic>? ?? [];
amount: data['amount'] as int,
fee: data['fee'] as int, return ElectrumTransactionInfo(
direction: parseTransactionDirectionFromInt(data['direction'] as int), type,
date: DateTime.fromMillisecondsSinceEpoch(data['date'] as int), id: data['id'] as String,
isPending: data['isPending'] as bool, height: data['height'] as int,
inputAddresses: data['inputAddresses'] as List<String>, amount: data['amount'] as int,
outputAddresses: data['outputAddresses'] as List<String>, fee: data['fee'] as int,
confirmations: data['confirmations'] as int); direction: parseTransactionDirectionFromInt(data['direction'] as int),
date: DateTime.fromMillisecondsSinceEpoch(data['date'] as int),
isPending: data['isPending'] as bool,
confirmations: data['confirmations'] as int,
inputAddresses:
inputAddresses.isEmpty ? [] : inputAddresses.map((e) => e.toString()).toList(),
outputAddresses:
outputAddresses.isEmpty ? [] : outputAddresses.map((e) => e.toString()).toList(),
to: data['to'] as String?,
unspents: unspents
.map((unspent) =>
BitcoinSilentPaymentsUnspent.fromJSON(null, unspent as Map<String, dynamic>))
.toList(),
);
} }
final WalletType type; final WalletType type;
@ -244,8 +227,14 @@ class ElectrumTransactionInfo extends TransactionInfo {
m['isPending'] = isPending; m['isPending'] = isPending;
m['confirmations'] = confirmations; m['confirmations'] = confirmations;
m['fee'] = fee; m['fee'] = fee;
m['to'] = to;
m['unspents'] = unspents?.map((e) => e.toJson()).toList() ?? [];
m['inputAddresses'] = inputAddresses; m['inputAddresses'] = inputAddresses;
m['outputAddresses'] = outputAddresses; m['outputAddresses'] = outputAddresses;
return m; return m;
} }
String toString() {
return 'ElectrumTransactionInfo(id: $id, height: $height, amount: $amount, fee: $fee, direction: $direction, date: $date, isPending: $isPending, confirmations: $confirmations, to: $to, unspent: $unspents)';
}
} }

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,7 @@
import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
import 'package:blockchain_utils/blockchain_utils.dart';
import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart';
import 'package:cw_bitcoin/electrum.dart';
import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_addresses.dart';
import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_type.dart'; import 'package:cw_core/wallet_type.dart';
@ -24,15 +24,17 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
WalletInfo walletInfo, { WalletInfo walletInfo, {
required this.mainHd, required this.mainHd,
required this.sideHd, required this.sideHd,
required this.electrumClient,
required this.network, required this.network,
List<BitcoinAddressRecord>? initialAddresses, List<BitcoinAddressRecord>? initialAddresses,
Map<String, int>? initialRegularAddressIndex, Map<String, int>? initialRegularAddressIndex,
Map<String, int>? initialChangeAddressIndex, Map<String, int>? initialChangeAddressIndex,
List<BitcoinSilentPaymentAddressRecord>? initialSilentAddresses,
int initialSilentAddressIndex = 0,
bitcoin.HDWallet? masterHd,
BitcoinAddressType? initialAddressPageType, BitcoinAddressType? initialAddressPageType,
}) : _addresses = ObservableList<BitcoinAddressRecord>.of((initialAddresses ?? []).toSet()), }) : _addresses = ObservableList<BitcoinAddressRecord>.of((initialAddresses ?? []).toSet()),
addressesByReceiveType = addressesByReceiveType =
ObservableList<BitcoinAddressRecord>.of((<BitcoinAddressRecord>[]).toSet()), ObservableList<BaseBitcoinAddressRecord>.of((<BitcoinAddressRecord>[]).toSet()),
receiveAddresses = ObservableList<BitcoinAddressRecord>.of((initialAddresses ?? []) receiveAddresses = ObservableList<BitcoinAddressRecord>.of((initialAddresses ?? [])
.where((addressRecord) => !addressRecord.isHidden && !addressRecord.isUsed) .where((addressRecord) => !addressRecord.isHidden && !addressRecord.isUsed)
.toSet()), .toSet()),
@ -45,7 +47,38 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
(walletInfo.addressPageType != null (walletInfo.addressPageType != null
? BitcoinAddressType.fromValue(walletInfo.addressPageType!) ? BitcoinAddressType.fromValue(walletInfo.addressPageType!)
: SegwitAddresType.p2wpkh), : SegwitAddresType.p2wpkh),
silentAddresses = ObservableList<BitcoinSilentPaymentAddressRecord>.of(
(initialSilentAddresses ?? []).toSet()),
currentSilentAddressIndex = initialSilentAddressIndex,
super(walletInfo) { super(walletInfo) {
if (masterHd != null) {
silentAddress = SilentPaymentOwner.fromPrivateKeys(
b_scan: ECPrivate.fromHex(masterHd.derivePath(SCAN_PATH).privKey!),
b_spend: ECPrivate.fromHex(masterHd.derivePath(SPEND_PATH).privKey!),
hrp: network == BitcoinNetwork.testnet ? 'tsp' : 'sp');
if (silentAddresses.length == 0) {
silentAddresses.add(BitcoinSilentPaymentAddressRecord(
silentAddress.toString(),
index: 0,
isHidden: false,
name: "",
silentPaymentTweak: null,
network: network,
type: SilentPaymentsAddresType.p2sp,
));
silentAddresses.add(BitcoinSilentPaymentAddressRecord(
silentAddress!.toLabeledSilentPaymentAddress(0).toString(),
index: 0,
isHidden: true,
name: "",
silentPaymentTweak: BytesUtils.toHexString(silentAddress!.generateLabel(0)),
network: network,
type: SilentPaymentsAddresType.p2sp,
));
}
}
updateAddressesByMatch(); updateAddressesByMatch();
} }
@ -54,27 +87,40 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
static const gap = 20; static const gap = 20;
final ObservableList<BitcoinAddressRecord> _addresses; final ObservableList<BitcoinAddressRecord> _addresses;
// Matched by addressPageType late ObservableList<BaseBitcoinAddressRecord> addressesByReceiveType;
late ObservableList<BitcoinAddressRecord> addressesByReceiveType;
final ObservableList<BitcoinAddressRecord> receiveAddresses; final ObservableList<BitcoinAddressRecord> receiveAddresses;
final ObservableList<BitcoinAddressRecord> changeAddresses; final ObservableList<BitcoinAddressRecord> changeAddresses;
final ElectrumClient electrumClient; final ObservableList<BitcoinSilentPaymentAddressRecord> silentAddresses;
final BasedUtxoNetwork network; final BasedUtxoNetwork network;
final bitcoin.HDWallet mainHd; final bitcoin.HDWallet mainHd;
final bitcoin.HDWallet sideHd; final bitcoin.HDWallet sideHd;
@observable
SilentPaymentOwner? silentAddress;
@observable @observable
late BitcoinAddressType _addressPageType; late BitcoinAddressType _addressPageType;
@computed @computed
BitcoinAddressType get addressPageType => _addressPageType; BitcoinAddressType get addressPageType => _addressPageType;
@observable
String? activeSilentAddress;
@computed @computed
List<BitcoinAddressRecord> get allAddresses => _addresses; List<BitcoinAddressRecord> get allAddresses => _addresses;
@override @override
@computed @computed
String get address { String get address {
if (addressPageType == SilentPaymentsAddresType.p2sp) {
if (activeSilentAddress != null) {
return activeSilentAddress!;
}
return silentAddress.toString();
}
String receiveAddress; String receiveAddress;
final typeMatchingReceiveAddresses = final typeMatchingReceiveAddresses =
@ -103,6 +149,18 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
@override @override
set address(String addr) { set address(String addr) {
if (addressPageType == SilentPaymentsAddresType.p2sp) {
final selected = silentAddresses.firstWhere((addressRecord) => addressRecord.address == addr);
if (selected.silentPaymentTweak != null && silentAddress != null) {
activeSilentAddress =
silentAddress!.toLabeledSilentPaymentAddress(selected.index).toString();
} else {
activeSilentAddress = silentAddress!.toString();
}
return;
}
final addressRecord = _addresses.firstWhere((addressRecord) => addressRecord.address == addr); final addressRecord = _addresses.firstWhere((addressRecord) => addressRecord.address == addr);
previousAddressRecord = addressRecord; previousAddressRecord = addressRecord;
@ -129,6 +187,8 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
void set currentChangeAddressIndex(int index) => void set currentChangeAddressIndex(int index) =>
currentChangeAddressIndexByType[_addressPageType.toString()] = index; currentChangeAddressIndexByType[_addressPageType.toString()] = index;
int currentSilentAddressIndex;
@observable @observable
BitcoinAddressRecord? previousAddressRecord; BitcoinAddressRecord? previousAddressRecord;
@ -196,7 +256,50 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
return address; return address;
} }
BitcoinAddressRecord generateNewAddress({String label = ''}) { Map<String, String> get labels {
final G = ECPublic.fromBytes(BigintUtils.toBytes(Curves.generatorSecp256k1.x, length: 32));
final labels = <String, String>{};
for (int i = 0; i < silentAddresses.length; i++) {
final silentAddressRecord = silentAddresses[i];
final silentPaymentTweak = silentAddressRecord.silentPaymentTweak;
if (silentPaymentTweak != null &&
SilentPaymentAddress.regex.hasMatch(silentAddressRecord.address)) {
labels[G
.tweakMul(BigintUtils.fromBytes(BytesUtils.fromHexString(silentPaymentTweak)))
.toHex()] = silentPaymentTweak;
}
}
return labels;
}
@action
BaseBitcoinAddressRecord generateNewAddress({String label = ''}) {
if (addressPageType == SilentPaymentsAddresType.p2sp && silentAddress != null) {
final currentSilentAddressIndex = silentAddresses
.where((addressRecord) => addressRecord.type != SegwitAddresType.p2tr)
.length -
1;
this.currentSilentAddressIndex = currentSilentAddressIndex;
final address = BitcoinSilentPaymentAddressRecord(
silentAddress!.toLabeledSilentPaymentAddress(currentSilentAddressIndex).toString(),
index: currentSilentAddressIndex,
isHidden: false,
name: label,
silentPaymentTweak:
BytesUtils.toHexString(silentAddress!.generateLabel(currentSilentAddressIndex)),
network: network,
type: SilentPaymentsAddresType.p2sp,
);
silentAddresses.add(address);
updateAddressesByMatch();
return address;
}
final newAddressIndex = addressesByReceiveType.fold( final newAddressIndex = addressesByReceiveType.fold(
0, (int acc, addressRecord) => addressRecord.isHidden == false ? acc + 1 : acc); 0, (int acc, addressRecord) => addressRecord.isHidden == false ? acc + 1 : acc);
@ -221,12 +324,70 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
Future<void> updateAddressesInBox() async { Future<void> updateAddressesInBox() async {
try { try {
addressesMap.clear(); addressesMap.clear();
addressesMap[address] = ''; addressesMap[address] = 'Active';
allAddressesMap.clear(); allAddressesMap.clear();
_addresses.forEach((addressRecord) { _addresses.forEach((addressRecord) {
allAddressesMap[addressRecord.address] = addressRecord.name; allAddressesMap[addressRecord.address] = addressRecord.name;
}); });
final lastP2wpkh = _addresses
.where((addressRecord) =>
_isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2wpkh))
.toList()
.last;
if (lastP2wpkh.address != address) {
addressesMap[lastP2wpkh.address] = 'P2WPKH';
} else {
addressesMap[address] = 'Active - P2WPKH';
}
final lastP2pkh = _addresses.firstWhere(
(addressRecord) => _isUnusedReceiveAddressByType(addressRecord, P2pkhAddressType.p2pkh));
if (lastP2pkh.address != address) {
addressesMap[lastP2pkh.address] = 'P2PKH';
} else {
addressesMap[address] = 'Active - P2PKH';
}
final lastP2sh = _addresses.firstWhere((addressRecord) =>
_isUnusedReceiveAddressByType(addressRecord, P2shAddressType.p2wpkhInP2sh));
if (lastP2sh.address != address) {
addressesMap[lastP2sh.address] = 'P2SH';
} else {
addressesMap[address] = 'Active - P2SH';
}
final lastP2tr = _addresses.firstWhere(
(addressRecord) => _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2tr));
if (lastP2tr.address != address) {
addressesMap[lastP2tr.address] = 'P2TR';
} else {
addressesMap[address] = 'Active - P2TR';
}
final lastP2wsh = _addresses.firstWhere(
(addressRecord) => _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2wsh));
if (lastP2wsh.address != address) {
addressesMap[lastP2wsh.address] = 'P2WSH';
} else {
addressesMap[address] = 'Active - P2WSH';
}
silentAddresses.forEach((addressRecord) {
if (addressRecord.type != SilentPaymentsAddresType.p2sp || addressRecord.isHidden) {
return;
}
if (addressRecord.address != address) {
addressesMap[addressRecord.address] = addressRecord.name.isEmpty
? "Silent Payments"
: "Silent Payments - " + addressRecord.name;
} else {
addressesMap[address] = 'Active - Silent Payments';
}
});
await saveAddressesInBox(); await saveAddressesInBox();
} catch (e) { } catch (e) {
print(e.toString()); print(e.toString());
@ -235,18 +396,41 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
@action @action
void updateAddress(String address, String label) { void updateAddress(String address, String label) {
final addressRecord = BaseBitcoinAddressRecord? foundAddress;
_addresses.firstWhere((addressRecord) => addressRecord.address == address); _addresses.forEach((addressRecord) {
addressRecord.setNewName(label); if (addressRecord.address == address) {
final index = _addresses.indexOf(addressRecord); foundAddress = addressRecord;
_addresses.remove(addressRecord); }
_addresses.insert(index, addressRecord); });
silentAddresses.forEach((addressRecord) {
if (addressRecord.address == address) {
foundAddress = addressRecord;
}
});
updateAddressesByMatch(); if (foundAddress != null) {
foundAddress!.setNewName(label);
if (foundAddress is BitcoinAddressRecord) {
final index = _addresses.indexOf(foundAddress);
_addresses.remove(foundAddress);
_addresses.insert(index, foundAddress as BitcoinAddressRecord);
} else {
final index = silentAddresses.indexOf(foundAddress as BitcoinSilentPaymentAddressRecord);
silentAddresses.remove(foundAddress);
silentAddresses.insert(index, foundAddress as BitcoinSilentPaymentAddressRecord);
}
}
} }
@action @action
void updateAddressesByMatch() { void updateAddressesByMatch() {
if (addressPageType == SilentPaymentsAddresType.p2sp) {
addressesByReceiveType.clear();
addressesByReceiveType.addAll(silentAddresses);
return;
}
addressesByReceiveType.clear(); addressesByReceiveType.clear();
addressesByReceiveType.addAll(_addresses.where(_isAddressPageTypeMatch).toList()); addressesByReceiveType.addAll(_addresses.where(_isAddressPageTypeMatch).toList());
} }
@ -272,7 +456,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
@action @action
Future<void> discoverAddresses(List<BitcoinAddressRecord> addressList, bool isHidden, Future<void> discoverAddresses(List<BitcoinAddressRecord> addressList, bool isHidden,
Future<String?> Function(BitcoinAddressRecord, Set<String>) getAddressHistory, Future<String?> Function(BitcoinAddressRecord) getAddressHistory,
{BitcoinAddressType type = SegwitAddresType.p2wpkh}) async { {BitcoinAddressType type = SegwitAddresType.p2wpkh}) async {
if (!isHidden) { if (!isHidden) {
_validateSideHdAddresses(addressList.toList()); _validateSideHdAddresses(addressList.toList());
@ -282,8 +466,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
startIndex: addressList.length, isHidden: isHidden, type: type); startIndex: addressList.length, isHidden: isHidden, type: type);
addAddresses(newAddresses); addAddresses(newAddresses);
final addressesWithHistory = await Future.wait(newAddresses final addressesWithHistory = await Future.wait(newAddresses.map(getAddressHistory));
.map((addr) => getAddressHistory(addr, _addresses.map((e) => e.address).toSet())));
final isLastAddressUsed = addressesWithHistory.last == addressList.last.address; final isLastAddressUsed = addressesWithHistory.last == addressList.last.address;
if (isLastAddressUsed) { if (isLastAddressUsed) {
@ -349,6 +532,15 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
updateAddressesByMatch(); updateAddressesByMatch();
} }
@action
void addSilentAddresses(Iterable<BitcoinSilentPaymentAddressRecord> addresses) {
final addressesSet = this.silentAddresses.toSet();
addressesSet.addAll(addresses);
this.silentAddresses.clear();
this.silentAddresses.addAll(addressesSet);
updateAddressesByMatch();
}
void _validateSideHdAddresses(List<BitcoinAddressRecord> addrWithTransactions) { void _validateSideHdAddresses(List<BitcoinAddressRecord> addrWithTransactions) {
addrWithTransactions.forEach((element) { addrWithTransactions.forEach((element) {
if (element.address != if (element.address !=
@ -371,4 +563,15 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
bitcoin.HDWallet _getHd(bool isHidden) => isHidden ? sideHd : mainHd; bitcoin.HDWallet _getHd(bool isHidden) => isHidden ? sideHd : mainHd;
bool _isAddressByType(BitcoinAddressRecord addr, BitcoinAddressType type) => addr.type == type; bool _isAddressByType(BitcoinAddressRecord addr, BitcoinAddressType type) => addr.type == type;
bool _isUnusedReceiveAddressByType(BitcoinAddressRecord addr, BitcoinAddressType type) =>
!addr.isHidden && !addr.isUsed && addr.type == type;
@action
void deleteSilentPaymentAddress(String address) {
final addressRecord = silentAddresses.firstWhere((addressRecord) =>
addressRecord.type == SilentPaymentsAddresType.p2sp && addressRecord.address == address);
silentAddresses.remove(addressRecord);
updateAddressesByMatch();
}
} }

View file

@ -19,6 +19,8 @@ class ElectrumWalletSnapshot {
required this.regularAddressIndex, required this.regularAddressIndex,
required this.changeAddressIndex, required this.changeAddressIndex,
required this.addressPageType, required this.addressPageType,
required this.silentAddresses,
required this.silentAddressIndex,
this.passphrase, this.passphrase,
this.derivationType, this.derivationType,
this.derivationPath, this.derivationPath,
@ -32,9 +34,11 @@ class ElectrumWalletSnapshot {
String? mnemonic; String? mnemonic;
String? xpub; String? xpub;
List<BitcoinAddressRecord> addresses; List<BitcoinAddressRecord> addresses;
List<BitcoinSilentPaymentAddressRecord> silentAddresses;
ElectrumBalance balance; ElectrumBalance balance;
Map<String, int> regularAddressIndex; Map<String, int> regularAddressIndex;
Map<String, int> changeAddressIndex; Map<String, int> changeAddressIndex;
int silentAddressIndex;
String? passphrase; String? passphrase;
DerivationType? derivationType; DerivationType? derivationType;
String? derivationPath; String? derivationPath;
@ -50,15 +54,23 @@ class ElectrumWalletSnapshot {
final passphrase = data['passphrase'] as String? ?? ''; final passphrase = data['passphrase'] as String? ?? '';
final addresses = addressesTmp final addresses = addressesTmp
.whereType<String>() .whereType<String>()
.map((addr) => BitcoinAddressRecord.fromJSON(addr, network)) .map((addr) => BitcoinAddressRecord.fromJSON(addr, network: network))
.toList(); .toList();
final balance = ElectrumBalance.fromJSON(data['balance'] as String) ??
final silentAddressesTmp = data['silent_addresses'] as List? ?? <Object>[];
final silentAddresses = silentAddressesTmp
.whereType<String>()
.map((addr) => BitcoinSilentPaymentAddressRecord.fromJSON(addr, network: network))
.toList();
final balance = ElectrumBalance.fromJSON(data['balance'] as String?) ??
ElectrumBalance(confirmed: 0, unconfirmed: 0, frozen: 0); ElectrumBalance(confirmed: 0, unconfirmed: 0, frozen: 0);
var regularAddressIndexByType = {SegwitAddresType.p2wpkh.toString(): 0}; var regularAddressIndexByType = {SegwitAddresType.p2wpkh.toString(): 0};
var changeAddressIndexByType = {SegwitAddresType.p2wpkh.toString(): 0}; var changeAddressIndexByType = {SegwitAddresType.p2wpkh.toString(): 0};
var silentAddressIndex = 0;
final derivationType = final derivationType = DerivationType
DerivationType.values[(data['derivationTypeIndex'] as int?) ?? DerivationType.electrum.index]; .values[(data['derivationTypeIndex'] as int?) ?? DerivationType.electrum.index];
final derivationPath = data['derivationPath'] as String? ?? "m/0'/0"; final derivationPath = data['derivationPath'] as String? ?? "m/0'/0";
try { try {
@ -69,6 +81,7 @@ class ElectrumWalletSnapshot {
SegwitAddresType.p2wpkh.toString(): SegwitAddresType.p2wpkh.toString():
int.parse(data['change_address_index'] as String? ?? '0') int.parse(data['change_address_index'] as String? ?? '0')
}; };
silentAddressIndex = int.parse(data['silent_address_index'] as String? ?? '0');
} catch (_) { } catch (_) {
try { try {
regularAddressIndexByType = data["account_index"] as Map<String, int>? ?? {}; regularAddressIndexByType = data["account_index"] as Map<String, int>? ?? {};
@ -90,6 +103,8 @@ class ElectrumWalletSnapshot {
addressPageType: data['address_page_type'] as String?, addressPageType: data['address_page_type'] as String?,
derivationType: derivationType, derivationType: derivationType,
derivationPath: derivationPath, derivationPath: derivationPath,
silentAddresses: silentAddresses,
silentAddressIndex: silentAddressIndex,
); );
} }
} }

View file

@ -2,7 +2,7 @@ import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/exceptions.dart'; import 'package:cw_core/exceptions.dart';
class BitcoinTransactionWrongBalanceException extends TransactionWrongBalanceException { class BitcoinTransactionWrongBalanceException extends TransactionWrongBalanceException {
BitcoinTransactionWrongBalanceException() : super(CryptoCurrency.btc); BitcoinTransactionWrongBalanceException({super.amount}) : super(CryptoCurrency.btc);
} }
class BitcoinTransactionNoInputsException extends TransactionNoInputsException {} class BitcoinTransactionNoInputsException extends TransactionNoInputsException {}
@ -27,3 +27,7 @@ class BitcoinTransactionCommitFailedDustOutputSendAll
extends TransactionCommitFailedDustOutputSendAll {} extends TransactionCommitFailedDustOutputSendAll {}
class BitcoinTransactionCommitFailedVoutNegative extends TransactionCommitFailedVoutNegative {} class BitcoinTransactionCommitFailedVoutNegative extends TransactionCommitFailedVoutNegative {}
class BitcoinTransactionCommitFailedBIP68Final extends TransactionCommitFailedBIP68Final {}
class BitcoinTransactionSilentPaymentsNotSupported extends TransactionInputNotSupported {}

View file

@ -44,7 +44,6 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
currency: CryptoCurrency.ltc) { currency: CryptoCurrency.ltc) {
walletAddresses = LitecoinWalletAddresses( walletAddresses = LitecoinWalletAddresses(
walletInfo, walletInfo,
electrumClient: electrumClient,
initialAddresses: initialAddresses, initialAddresses: initialAddresses,
initialRegularAddressIndex: initialRegularAddressIndex, initialRegularAddressIndex: initialRegularAddressIndex,
initialChangeAddressIndex: initialChangeAddressIndex, initialChangeAddressIndex: initialChangeAddressIndex,

View file

@ -15,7 +15,6 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with
required super.mainHd, required super.mainHd,
required super.sideHd, required super.sideHd,
required super.network, required super.network,
required super.electrumClient,
super.initialAddresses, super.initialAddresses,
super.initialRegularAddressIndex, super.initialRegularAddressIndex,
super.initialChangeAddressIndex, super.initialChangeAddressIndex,

View file

@ -73,6 +73,11 @@ class PendingBitcoinTransaction with PendingTransaction {
if (error.contains("bad-txns-vout-negative")) { if (error.contains("bad-txns-vout-negative")) {
throw BitcoinTransactionCommitFailedVoutNegative(); throw BitcoinTransactionCommitFailedVoutNegative();
} }
if (error.contains("non-BIP68-final")) {
throw BitcoinTransactionCommitFailedBIP68Final();
}
throw BitcoinTransactionCommitFailed(errorMessage: error); throw BitcoinTransactionCommitFailed(errorMessage: error);
} }

View file

@ -29,10 +29,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: asn1lib name: asn1lib
sha256: c9c85fedbe2188b95133cbe960e16f5f448860f7133330e272edbbca5893ddc6 sha256: "58082b3f0dca697204dbab0ef9ff208bfaea7767ea771076af9a343488428dda"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.5.2" version: "1.5.3"
async: async:
dependency: transitive dependency: transitive
description: description:
@ -79,11 +79,11 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
path: "." path: "."
ref: cake-update-v2 ref: cake-update-v3
resolved-ref: "01d844a5f5a520a31df5254e34169af4664aa769" resolved-ref: cc99eedb1d28ee9376dda0465ef72aa627ac6149
url: "https://github.com/cake-tech/bitcoin_base.git" url: "https://github.com/cake-tech/bitcoin_base"
source: git source: git
version: "4.2.0" version: "4.2.1"
bitcoin_flutter: bitcoin_flutter:
dependency: "direct main" dependency: "direct main"
description: description:
@ -96,11 +96,12 @@ packages:
blockchain_utils: blockchain_utils:
dependency: "direct main" dependency: "direct main"
description: description:
name: blockchain_utils path: "."
sha256: "38ef5f4a22441ac4370aed9071dc71c460acffc37c79b344533f67d15f24c13c" ref: cake-update-v1
url: "https://pub.dev" resolved-ref: cabd7e0e16c4da9920338c76eff3aeb8af0211f3
source: hosted url: "https://github.com/cake-tech/blockchain_utils"
version: "2.1.1" source: git
version: "2.1.2"
boolean_selector: boolean_selector:
dependency: transitive dependency: transitive
description: description:
@ -197,6 +198,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.3" version: "2.0.3"
cli_util:
dependency: transitive
description:
name: cli_util
sha256: c05b7406fdabc7a49a3929d4af76bcaccbbffcbcdcf185b082e1ae07da323d19
url: "https://pub.dev"
source: hosted
version: "0.4.1"
clock: clock:
dependency: transitive dependency: transitive
description: description:
@ -241,10 +250,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: cryptography name: cryptography
sha256: df156c5109286340817d21fa7b62f9140f17915077127dd70f8bd7a2a0997a35 sha256: d146b76d33d94548cf035233fbc2f4338c1242fa119013bead807d033fc4ae05
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.5.0" version: "2.7.0"
cw_core: cw_core:
dependency: "direct main" dependency: "direct main"
description: description:
@ -288,10 +297,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: ffi name: ffi
sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.0" version: "2.1.2"
ffigen:
dependency: transitive
description:
name: ffigen
sha256: d3e76c2ad48a4e7f93a29a162006f00eba46ce7c08194a77bb5c5e97d1b5ff0a
url: "https://pub.dev"
source: hosted
version: "8.0.2"
file: file:
dependency: transitive dependency: transitive
description: description:
@ -346,10 +363,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: functional_data name: functional_data
sha256: aefdec4365452283b2a7cf420a3169654d51d3e9553069a22d76680d7a9d7c3d sha256: "76d17dc707c40e552014f5a49c0afcc3f1e3f05e800cd6b7872940bfe41a5039"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.1" version: "1.2.0"
glob: glob:
dependency: transitive dependency: transitive
description: description:
@ -394,10 +411,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: http name: http
sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525" sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.0" version: "1.2.1"
http_multi_server: http_multi_server:
dependency: transitive dependency: transitive
description: description:
@ -442,10 +459,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: json_annotation name: json_annotation
sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.8.1" version: "4.9.0"
leak_tracker: leak_tracker:
dependency: transitive dependency: transitive
description: description:
@ -531,10 +548,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: mime name: mime
sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.4" version: "1.0.5"
mobx: mobx:
dependency: "direct main" dependency: "direct main"
description: description:
@ -579,26 +596,26 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: path_provider name: path_provider
sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.2" version: "2.1.3"
path_provider_android: path_provider_android:
dependency: transitive dependency: transitive
description: description:
name: path_provider_android name: path_provider_android
sha256: "477184d672607c0a3bf68fbbf601805f92ef79c82b64b4d6eb318cbca4c48668" sha256: a248d8146ee5983446bf03ed5ea8f6533129a12b11f12057ad1b4a67a2b3b41d
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.2" version: "2.2.4"
path_provider_foundation: path_provider_foundation:
dependency: transitive dependency: transitive
description: description:
name: path_provider_foundation name: path_provider_foundation
sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f" sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.2" version: "2.4.0"
path_provider_linux: path_provider_linux:
dependency: transitive dependency: transitive
description: description:
@ -643,10 +660,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: pointycastle name: pointycastle
sha256: "70fe966348fe08c34bf929582f1d8247d9d9408130723206472b4687227e4333" sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.8.0" version: "3.9.1"
pool: pool:
dependency: transitive dependency: transitive
description: description:
@ -687,6 +704,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.2.3" version: "1.2.3"
quiver:
dependency: transitive
description:
name: quiver
sha256: b1c1ac5ce6688d77f65f3375a9abb9319b3cb32486bdc7a1e0fdf004d7ba4e47
url: "https://pub.dev"
source: hosted
version: "3.2.1"
reactive_ble_mobile: reactive_ble_mobile:
dependency: transitive dependency: transitive
description: description:
@ -736,10 +761,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: socks5_proxy name: socks5_proxy
sha256: "1d21b5606169654bbf4cfb904e8e6ed897e9f763358709f87310c757096d909a" sha256: "045cbba84f6e2b01c1c77634a63e926352bf110ef5f07fc462c6d43bbd4b6a83"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.4" version: "1.0.5+dev.2"
source_gen: source_gen:
dependency: transitive dependency: transitive
description: description:
@ -764,6 +789,15 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.10.0" version: "1.10.0"
sp_scanner:
dependency: "direct main"
description:
path: "."
ref: "sp_v1.0.0"
resolved-ref: a9a4c6d051f37a15a3a52cc2a0094f24c68b62c5
url: "https://github.com/cake-tech/sp_scanner"
source: git
version: "0.0.1"
stack_trace: stack_trace:
dependency: transitive dependency: transitive
description: description:
@ -860,22 +894,30 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.0" version: "1.1.0"
web:
dependency: transitive
description:
name: web
sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27"
url: "https://pub.dev"
source: hosted
version: "0.5.1"
web_socket_channel: web_socket_channel:
dependency: transitive dependency: transitive
description: description:
name: web_socket_channel name: web_socket_channel
sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b sha256: "58c6666b342a38816b2e7e50ed0f1e261959630becd4c879c4f26bfa14aa5a42"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.0" version: "2.4.5"
win32: win32:
dependency: transitive dependency: transitive
description: description:
name: win32 name: win32
sha256: "350a11abd2d1d97e0cc7a28a81b781c08002aa2864d9e3f192ca0ffa18b06ed3" sha256: "0eaf06e3446824099858367950a813472af675116bf63f008a4c2a75ae13e9cb"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.0.9" version: "5.5.0"
xdg_directories: xdg_directories:
dependency: transitive dependency: transitive
description: description:
@ -892,6 +934,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.2" version: "3.1.2"
yaml_edit:
dependency: transitive
description:
name: yaml_edit
sha256: e9c1a3543d2da0db3e90270dbb1e4eebc985ee5e3ffe468d83224472b2194a5f
url: "https://pub.dev"
source: hosted
version: "2.2.1"
sdks: sdks:
dart: ">=3.2.0-0 <4.0.0" dart: ">=3.3.0 <4.0.0"
flutter: ">=3.10.0" flutter: ">=3.16.6"

View file

@ -32,13 +32,21 @@ dependencies:
cryptography: ^2.0.5 cryptography: ^2.0.5
bitcoin_base: bitcoin_base:
git: git:
url: https://github.com/cake-tech/bitcoin_base.git url: https://github.com/cake-tech/bitcoin_base
ref: cake-update-v2 ref: cake-update-v3
blockchain_utils: ^2.1.1 blockchain_utils:
git:
url: https://github.com/cake-tech/blockchain_utils
ref: cake-update-v1
ledger_flutter: ^1.0.1 ledger_flutter: ^1.0.1
ledger_bitcoin: ledger_bitcoin:
git: git:
url: https://github.com/cake-tech/ledger-bitcoin url: https://github.com/cake-tech/ledger-bitcoin
sp_scanner:
git:
url: https://github.com/cake-tech/sp_scanner
ref: sp_v2.0.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View file

@ -46,7 +46,6 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
currency: CryptoCurrency.bch) { currency: CryptoCurrency.bch) {
walletAddresses = BitcoinCashWalletAddresses( walletAddresses = BitcoinCashWalletAddresses(
walletInfo, walletInfo,
electrumClient: electrumClient,
initialAddresses: initialAddresses, initialAddresses: initialAddresses,
initialRegularAddressIndex: initialRegularAddressIndex, initialRegularAddressIndex: initialRegularAddressIndex,
initialChangeAddressIndex: initialChangeAddressIndex, initialChangeAddressIndex: initialChangeAddressIndex,

View file

@ -15,7 +15,6 @@ abstract class BitcoinCashWalletAddressesBase extends ElectrumWalletAddresses wi
required super.mainHd, required super.mainHd,
required super.sideHd, required super.sideHd,
required super.network, required super.network,
required super.electrumClient,
super.initialAddresses, super.initialAddresses,
super.initialRegularAddressIndex, super.initialRegularAddressIndex,
super.initialChangeAddressIndex, super.initialChangeAddressIndex,

View file

@ -31,10 +31,12 @@ dependencies:
ref: Add-Support-For-OP-Return-data ref: Add-Support-For-OP-Return-data
bitcoin_base: bitcoin_base:
git: git:
url: https://github.com/cake-tech/bitcoin_base.git url: https://github.com/cake-tech/bitcoin_base
ref: cake-update-v2 ref: cake-update-v3
blockchain_utils:
git:
url: https://github.com/cake-tech/blockchain_utils
ref: cake-update-v1
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View file

@ -104,6 +104,7 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
CryptoCurrency.digibyte, CryptoCurrency.digibyte,
CryptoCurrency.usdtSol, CryptoCurrency.usdtSol,
CryptoCurrency.usdcTrc20, CryptoCurrency.usdcTrc20,
CryptoCurrency.tbtc,
]; ];
static const havenCurrencies = [ static const havenCurrencies = [
@ -218,7 +219,8 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
static const kaspa = CryptoCurrency(title: 'KAS', fullName: 'Kaspa', raw: 89, name: 'kas', iconPath: 'assets/images/kaspa_icon.png', decimals: 8); static const kaspa = CryptoCurrency(title: 'KAS', fullName: 'Kaspa', raw: 89, name: 'kas', iconPath: 'assets/images/kaspa_icon.png', decimals: 8);
static const digibyte = CryptoCurrency(title: 'DGB', fullName: 'DigiByte', raw: 90, name: 'dgb', iconPath: 'assets/images/digibyte.png', decimals: 8); static const digibyte = CryptoCurrency(title: 'DGB', fullName: 'DigiByte', raw: 90, name: 'dgb', iconPath: 'assets/images/digibyte.png', decimals: 8);
static const usdtSol = CryptoCurrency(title: 'USDT', tag: 'SOL', fullName: 'USDT Tether', raw: 91, name: 'usdtsol', iconPath: 'assets/images/usdt_icon.png', decimals: 6); static const usdtSol = CryptoCurrency(title: 'USDT', tag: 'SOL', fullName: 'USDT Tether', raw: 91, name: 'usdtsol', iconPath: 'assets/images/usdt_icon.png', decimals: 6);
static const usdcTrc20 = CryptoCurrency(title: 'USDC', tag: 'TRX', fullName: 'USDC Coin', raw: 92, name: 'usdctrc20', iconPath: 'assets/images/usdc_icon.png', decimals: 6); static const usdcTrc20 = CryptoCurrency(title: 'USDC', tag: 'TRX', fullName: 'USDC Coin', raw: 92, name: 'usdctrc20', iconPath: 'assets/images/usdc_icon.png', decimals: 6);
static const tbtc = CryptoCurrency(title: 'tBTC', fullName: 'Testnet Bitcoin', raw: 93, name: 'tbtc', iconPath: 'assets/images/tbtc.png', decimals: 8);
static final Map<int, CryptoCurrency> _rawCurrencyMap = static final Map<int, CryptoCurrency> _rawCurrencyMap =
@ -253,7 +255,7 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
static CryptoCurrency fromString(String name, {CryptoCurrency? walletCurrency}) { static CryptoCurrency fromString(String name, {CryptoCurrency? walletCurrency}) {
try { try {
return CryptoCurrency.all.firstWhere((element) => return CryptoCurrency.all.firstWhere((element) =>
element.title.toLowerCase() == name && element.title.toLowerCase() == name.toLowerCase() &&
(element.tag == null || (element.tag == null ||
element.tag == walletCurrency?.title || element.tag == walletCurrency?.title ||
element.tag == walletCurrency?.tag)); element.tag == walletCurrency?.tag));

View file

@ -1,9 +1,12 @@
import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/wallet_type.dart'; import 'package:cw_core/wallet_type.dart';
CryptoCurrency currencyForWalletType(WalletType type) { CryptoCurrency currencyForWalletType(WalletType type, {bool? isTestnet}) {
switch (type) { switch (type) {
case WalletType.bitcoin: case WalletType.bitcoin:
if (isTestnet == true) {
return CryptoCurrency.tbtc;
}
return CryptoCurrency.btc; return CryptoCurrency.btc;
case WalletType.monero: case WalletType.monero:
return CryptoCurrency.xmr; return CryptoCurrency.xmr;

View file

@ -1,9 +1,10 @@
import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/crypto_currency.dart';
class TransactionWrongBalanceException implements Exception { class TransactionWrongBalanceException implements Exception {
TransactionWrongBalanceException(this.currency); TransactionWrongBalanceException(this.currency, {this.amount});
final CryptoCurrency currency; final CryptoCurrency currency;
final int? amount;
} }
class TransactionNoInputsException implements Exception {} class TransactionNoInputsException implements Exception {}
@ -32,3 +33,7 @@ class TransactionCommitFailedDustOutput implements Exception {}
class TransactionCommitFailedDustOutputSendAll implements Exception {} class TransactionCommitFailedDustOutputSendAll implements Exception {}
class TransactionCommitFailedVoutNegative implements Exception {} class TransactionCommitFailedVoutNegative implements Exception {}
class TransactionCommitFailedBIP68Final implements Exception {}
class TransactionInputNotSupported implements Exception {}

View file

@ -242,3 +242,57 @@ Future<int> getHavenCurrentHeight() async {
throw Exception('Failed to load current blockchain height'); throw Exception('Failed to load current blockchain height');
} }
} }
// Data taken from https://timechaincalendar.com/
const bitcoinDates = {
"2024-05": 841590,
"2024-04": 837182,
"2024-03": 832623,
"2024-02": 828319,
"2024-01": 823807,
"2023-12": 819206,
"2023-11": 814765,
"2023-10": 810098,
"2023-09": 805675,
"2023-08": 801140,
"2023-07": 796640,
"2023-06": 792330,
"2023-05": 787733,
"2023-04": 783403,
"2023-03": 778740,
"2023-02": 774525,
"2023-01": 769810,
};
int getBitcoinHeightByDate({required DateTime date}) {
String dateKey = '${date.year}-${date.month.toString().padLeft(2, '0')}';
final closestKey = bitcoinDates.keys
.firstWhere((key) => formatMapKey(key).isBefore(date), orElse: () => bitcoinDates.keys.last);
final beginningBlock = bitcoinDates[dateKey] ?? bitcoinDates[closestKey]!;
final startOfMonth = DateTime(date.year, date.month);
final daysDifference = date.difference(startOfMonth).inDays;
// approximately 6 blocks per hour, 24 hours per day
int estimatedBlocksSinceStartOfMonth = (daysDifference * 24 * 6);
return beginningBlock + estimatedBlocksSinceStartOfMonth;
}
DateTime getDateByBitcoinHeight(int height) {
final closestEntry = bitcoinDates.entries
.lastWhere((entry) => entry.value >= height, orElse: () => bitcoinDates.entries.first);
final beginningBlock = closestEntry.value;
final startOfMonth = formatMapKey(closestEntry.key);
final blocksDifference = height - beginningBlock;
final hoursDifference = blocksDifference / 5.5;
final estimatedDate = startOfMonth.add(Duration(hours: hoursDifference.ceil()));
if (estimatedDate.isAfter(DateTime.now())) {
return DateTime.now();
}
return estimatedDate;
}

View file

@ -244,8 +244,12 @@ class Node extends HiveObject with Keyable {
Future<bool> requestElectrumServer() async { Future<bool> requestElectrumServer() async {
try { try {
await SecureSocket.connect(uri.host, uri.port, if (useSSL == true) {
timeout: Duration(seconds: 5), onBadCertificate: (_) => true); await SecureSocket.connect(uri.host, uri.port,
timeout: Duration(seconds: 5), onBadCertificate: (_) => true);
} else {
await Socket.connect(uri.host, uri.port, timeout: Duration(seconds: 5));
}
return true; return true;
} catch (_) { } catch (_) {
return false; return false;

View file

@ -14,6 +14,16 @@ class SyncingSyncStatus extends SyncStatus {
@override @override
String toString() => '$blocksLeft'; String toString() => '$blocksLeft';
factory SyncingSyncStatus.fromHeightValues(int chainTip, int initialSyncHeight, int syncHeight) {
final track = chainTip - initialSyncHeight;
final diff = track - (chainTip - syncHeight);
final ptc = diff <= 0 ? 0.0 : diff / track;
final left = chainTip - syncHeight;
// sum 1 because if at the chain tip, will say "0 blocks left"
return SyncingSyncStatus(left + 1, ptc);
}
} }
class SyncedSyncStatus extends SyncStatus { class SyncedSyncStatus extends SyncStatus {
@ -21,6 +31,17 @@ class SyncedSyncStatus extends SyncStatus {
double progress() => 1.0; double progress() => 1.0;
} }
class SyncedTipSyncStatus extends SyncedSyncStatus {
SyncedTipSyncStatus(this.tip);
final int tip;
}
class SyncronizingSyncStatus extends SyncStatus {
@override
double progress() => 0.0;
}
class NotConnectedSyncStatus extends SyncStatus { class NotConnectedSyncStatus extends SyncStatus {
const NotConnectedSyncStatus(); const NotConnectedSyncStatus();
@ -33,10 +54,7 @@ class AttemptingSyncStatus extends SyncStatus {
double progress() => 0.0; double progress() => 0.0;
} }
class FailedSyncStatus extends SyncStatus { class FailedSyncStatus extends NotConnectedSyncStatus {}
@override
double progress() => 1.0;
}
class ConnectingSyncStatus extends SyncStatus { class ConnectingSyncStatus extends SyncStatus {
@override @override
@ -48,7 +66,14 @@ class ConnectedSyncStatus extends SyncStatus {
double progress() => 0.0; double progress() => 0.0;
} }
class LostConnectionSyncStatus extends SyncStatus { class UnsupportedSyncStatus extends NotConnectedSyncStatus {}
class TimedOutSyncStatus extends NotConnectedSyncStatus {
@override @override
double progress() => 1.0; String toString() => 'Timed out';
} }
class LostConnectionSyncStatus extends NotConnectedSyncStatus {
@override
String toString() => 'Reconnecting';
}

View file

@ -16,7 +16,8 @@ class UnspentCoinsInfo extends HiveObject {
required this.value, required this.value,
this.keyImage = null, this.keyImage = null,
this.isChange = false, this.isChange = false,
this.accountIndex = 0 this.accountIndex = 0,
this.isSilentPayment = false,
}); });
static const typeId = UNSPENT_COINS_INFO_TYPE_ID; static const typeId = UNSPENT_COINS_INFO_TYPE_ID;
@ -49,13 +50,16 @@ class UnspentCoinsInfo extends HiveObject {
@HiveField(8, defaultValue: null) @HiveField(8, defaultValue: null)
String? keyImage; String? keyImage;
@HiveField(9, defaultValue: false) @HiveField(9, defaultValue: false)
bool isChange; bool isChange;
@HiveField(10, defaultValue: 0) @HiveField(10, defaultValue: 0)
int accountIndex; int accountIndex;
@HiveField(11, defaultValue: false)
bool? isSilentPayment;
String get note => noteRaw ?? ''; String get note => noteRaw ?? '';
set note(String value) => noteRaw = value; set note(String value) => noteRaw = value;

View file

@ -17,5 +17,6 @@ class Unspent {
int? confirmations; int? confirmations;
String note; String note;
bool get isP2wpkh => address.startsWith('bc') || address.startsWith('ltc'); bool get isP2wpkh =>
address.startsWith('bc') || address.startsWith('tb') || address.startsWith('ltc');
} }

View file

@ -24,7 +24,7 @@ abstract class WalletBase<BalanceType extends Balance, HistoryType extends Trans
WalletType get type => walletInfo.type; WalletType get type => walletInfo.type;
CryptoCurrency get currency => currencyForWalletType(type); CryptoCurrency get currency => currencyForWalletType(type, isTestnet: isTestnet);
String get id => walletInfo.id; String get id => walletInfo.id;

View file

@ -66,21 +66,21 @@ class DerivationInfo extends HiveObject {
@HiveType(typeId: WalletInfo.typeId) @HiveType(typeId: WalletInfo.typeId)
class WalletInfo extends HiveObject { class WalletInfo extends HiveObject {
WalletInfo( WalletInfo(
this.id, this.id,
this.name, this.name,
this.type, this.type,
this.isRecovery, this.isRecovery,
this.restoreHeight, this.restoreHeight,
this.timestamp, this.timestamp,
this.dirPath, this.dirPath,
this.path, this.path,
this.address, this.address,
this.yatEid, this.yatEid,
this.yatLastUsedAddressRaw, this.yatLastUsedAddressRaw,
this.showIntroCakePayCard, this.showIntroCakePayCard,
this.derivationInfo, this.derivationInfo,
this.hardwareWalletType, this.hardwareWalletType,
): _yatLastUsedAddressController = StreamController<String>.broadcast(); ) : _yatLastUsedAddressController = StreamController<String>.broadcast();
factory WalletInfo.external({ factory WalletInfo.external({
required String id, required String id,
@ -207,4 +207,9 @@ class WalletInfo extends HiveObject {
Stream<String> get yatLastUsedAddressStream => _yatLastUsedAddressController.stream; Stream<String> get yatLastUsedAddressStream => _yatLastUsedAddressController.stream;
StreamController<String> _yatLastUsedAddressController; StreamController<String> _yatLastUsedAddressController;
Future<void> updateRestoreHeight(int height) async {
restoreHeight = height;
await save();
}
} }

View file

@ -173,11 +173,14 @@ String walletTypeToDisplayName(WalletType type) {
} }
} }
CryptoCurrency walletTypeToCryptoCurrency(WalletType type) { CryptoCurrency walletTypeToCryptoCurrency(WalletType type, {bool isTestnet = false}) {
switch (type) { switch (type) {
case WalletType.monero: case WalletType.monero:
return CryptoCurrency.xmr; return CryptoCurrency.xmr;
case WalletType.bitcoin: case WalletType.bitcoin:
if (isTestnet) {
return CryptoCurrency.tbtc;
}
return CryptoCurrency.btc; return CryptoCurrency.btc;
case WalletType.litecoin: case WalletType.litecoin:
return CryptoCurrency.ltc; return CryptoCurrency.ltc;

View file

@ -399,6 +399,7 @@ extern "C"
return false; return false;
} }
wallet->store(std::string(path));
change_current_wallet(wallet); change_current_wallet(wallet);
return true; return true;
} }
@ -464,6 +465,16 @@ extern "C"
return strdup(get_current_wallet()->address(account_index, address_index).c_str()); return strdup(get_current_wallet()->address(account_index, address_index).c_str());
} }
char *get_cache_attribute(char *name)
{
return strdup(get_current_wallet()->getCacheAttribute(std::string(name)).c_str());
}
bool set_cache_attribute(char *name, char *value)
{
get_current_wallet()->setCacheAttribute(std::string(name), std::string(value));
return true;
}
const char *seed() const char *seed()
{ {

View file

@ -154,3 +154,7 @@ typedef freeze_coin = Void Function(Int32 index);
typedef thaw_coin = Void Function(Int32 index); typedef thaw_coin = Void Function(Int32 index);
typedef sign_message = Pointer<Utf8> Function(Pointer<Utf8> message, Pointer<Utf8> address); typedef sign_message = Pointer<Utf8> Function(Pointer<Utf8> message, Pointer<Utf8> address);
typedef get_cache_attribute = Pointer<Utf8> Function(Pointer<Utf8> name);
typedef set_cache_attribute = Int8 Function(Pointer<Utf8> name, Pointer<Utf8> value);

View file

@ -154,3 +154,7 @@ typedef FreezeCoin = void Function(int);
typedef ThawCoin = void Function(int); typedef ThawCoin = void Function(int);
typedef SignMessage = Pointer<Utf8> Function(Pointer<Utf8>, Pointer<Utf8>); typedef SignMessage = Pointer<Utf8> Function(Pointer<Utf8>, Pointer<Utf8>);
typedef GetCacheAttribute = Pointer<Utf8> Function(Pointer<Utf8>);
typedef SetCacheAttribute = int Function(Pointer<Utf8>, Pointer<Utf8>);

View file

@ -1,26 +1,24 @@
import 'dart:async'; import 'dart:async';
import 'dart:ffi'; import 'dart:ffi';
import 'package:ffi/ffi.dart';
import 'package:cw_monero/api/structs/ut8_box.dart';
import 'package:cw_monero/api/convert_utf8_to_string.dart'; import 'package:cw_monero/api/convert_utf8_to_string.dart';
import 'package:cw_monero/api/signatures.dart';
import 'package:cw_monero/api/types.dart';
import 'package:cw_monero/api/monero_api.dart';
import 'package:cw_monero/api/exceptions/setup_wallet_exception.dart'; import 'package:cw_monero/api/exceptions/setup_wallet_exception.dart';
import 'package:cw_monero/api/monero_api.dart';
import 'package:cw_monero/api/signatures.dart';
import 'package:cw_monero/api/structs/ut8_box.dart';
import 'package:cw_monero/api/types.dart';
import 'package:ffi/ffi.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
int _boolToInt(bool value) => value ? 1 : 0; int _boolToInt(bool value) => value ? 1 : 0;
final getFileNameNative = moneroApi final getFileNameNative =
.lookup<NativeFunction<get_filename>>('get_filename') moneroApi.lookup<NativeFunction<get_filename>>('get_filename').asFunction<GetFilename>();
.asFunction<GetFilename>();
final getSeedNative = final getSeedNative = moneroApi.lookup<NativeFunction<get_seed>>('seed').asFunction<GetSeed>();
moneroApi.lookup<NativeFunction<get_seed>>('seed').asFunction<GetSeed>();
final getAddressNative = moneroApi final getAddressNative =
.lookup<NativeFunction<get_address>>('get_address') moneroApi.lookup<NativeFunction<get_address>>('get_address').asFunction<GetAddress>();
.asFunction<GetAddress>();
final getFullBalanceNative = moneroApi final getFullBalanceNative = moneroApi
.lookup<NativeFunction<get_full_balanace>>('get_full_balance') .lookup<NativeFunction<get_full_balanace>>('get_full_balance')
@ -38,41 +36,34 @@ final getNodeHeightNative = moneroApi
.lookup<NativeFunction<get_node_height>>('get_node_height') .lookup<NativeFunction<get_node_height>>('get_node_height')
.asFunction<GetNodeHeight>(); .asFunction<GetNodeHeight>();
final isConnectedNative = moneroApi final isConnectedNative =
.lookup<NativeFunction<is_connected>>('is_connected') moneroApi.lookup<NativeFunction<is_connected>>('is_connected').asFunction<IsConnected>();
.asFunction<IsConnected>();
final setupNodeNative = moneroApi final setupNodeNative =
.lookup<NativeFunction<setup_node>>('setup_node') moneroApi.lookup<NativeFunction<setup_node>>('setup_node').asFunction<SetupNode>();
.asFunction<SetupNode>();
final startRefreshNative = moneroApi final startRefreshNative =
.lookup<NativeFunction<start_refresh>>('start_refresh') moneroApi.lookup<NativeFunction<start_refresh>>('start_refresh').asFunction<StartRefresh>();
.asFunction<StartRefresh>();
final connecToNodeNative = moneroApi final connecToNodeNative = moneroApi
.lookup<NativeFunction<connect_to_node>>('connect_to_node') .lookup<NativeFunction<connect_to_node>>('connect_to_node')
.asFunction<ConnectToNode>(); .asFunction<ConnectToNode>();
final setRefreshFromBlockHeightNative = moneroApi final setRefreshFromBlockHeightNative = moneroApi
.lookup<NativeFunction<set_refresh_from_block_height>>( .lookup<NativeFunction<set_refresh_from_block_height>>('set_refresh_from_block_height')
'set_refresh_from_block_height')
.asFunction<SetRefreshFromBlockHeight>(); .asFunction<SetRefreshFromBlockHeight>();
final setRecoveringFromSeedNative = moneroApi final setRecoveringFromSeedNative = moneroApi
.lookup<NativeFunction<set_recovering_from_seed>>( .lookup<NativeFunction<set_recovering_from_seed>>('set_recovering_from_seed')
'set_recovering_from_seed')
.asFunction<SetRecoveringFromSeed>(); .asFunction<SetRecoveringFromSeed>();
final storeNative = final storeNative = moneroApi.lookup<NativeFunction<store_c>>('store').asFunction<Store>();
moneroApi.lookup<NativeFunction<store_c>>('store').asFunction<Store>();
final setPasswordNative = final setPasswordNative =
moneroApi.lookup<NativeFunction<set_password>>('set_password').asFunction<SetPassword>(); moneroApi.lookup<NativeFunction<set_password>>('set_password').asFunction<SetPassword>();
final setListenerNative = moneroApi final setListenerNative =
.lookup<NativeFunction<set_listener>>('set_listener') moneroApi.lookup<NativeFunction<set_listener>>('set_listener').asFunction<SetListener>();
.asFunction<SetListener>();
final getSyncingHeightNative = moneroApi final getSyncingHeightNative = moneroApi
.lookup<NativeFunction<get_syncing_height>>('get_syncing_height') .lookup<NativeFunction<get_syncing_height>>('get_syncing_height')
@ -83,8 +74,7 @@ final isNeededToRefreshNative = moneroApi
.asFunction<IsNeededToRefresh>(); .asFunction<IsNeededToRefresh>();
final isNewTransactionExistNative = moneroApi final isNewTransactionExistNative = moneroApi
.lookup<NativeFunction<is_new_transaction_exist>>( .lookup<NativeFunction<is_new_transaction_exist>>('is_new_transaction_exist')
'is_new_transaction_exist')
.asFunction<IsNewTransactionExist>(); .asFunction<IsNewTransactionExist>();
final getSecretViewKeyNative = moneroApi final getSecretViewKeyNative = moneroApi
@ -107,9 +97,8 @@ final closeCurrentWalletNative = moneroApi
.lookup<NativeFunction<close_current_wallet>>('close_current_wallet') .lookup<NativeFunction<close_current_wallet>>('close_current_wallet')
.asFunction<CloseCurrentWallet>(); .asFunction<CloseCurrentWallet>();
final onStartupNative = moneroApi final onStartupNative =
.lookup<NativeFunction<on_startup>>('on_startup') moneroApi.lookup<NativeFunction<on_startup>>('on_startup').asFunction<OnStartup>();
.asFunction<OnStartup>();
final rescanBlockchainAsyncNative = moneroApi final rescanBlockchainAsyncNative = moneroApi
.lookup<NativeFunction<rescan_blockchain>>('rescan_blockchain') .lookup<NativeFunction<rescan_blockchain>>('rescan_blockchain')
@ -123,13 +112,19 @@ final setTrustedDaemonNative = moneroApi
.lookup<NativeFunction<set_trusted_daemon>>('set_trusted_daemon') .lookup<NativeFunction<set_trusted_daemon>>('set_trusted_daemon')
.asFunction<SetTrustedDaemon>(); .asFunction<SetTrustedDaemon>();
final trustedDaemonNative = moneroApi final trustedDaemonNative =
.lookup<NativeFunction<trusted_daemon>>('trusted_daemon') moneroApi.lookup<NativeFunction<trusted_daemon>>('trusted_daemon').asFunction<TrustedDaemon>();
.asFunction<TrustedDaemon>();
final signMessageNative = moneroApi final signMessageNative =
.lookup<NativeFunction<sign_message>>('sign_message') moneroApi.lookup<NativeFunction<sign_message>>('sign_message').asFunction<SignMessage>();
.asFunction<SignMessage>();
final getCacheAttributeNative = moneroApi
.lookup<NativeFunction<get_cache_attribute>>('get_cache_attribute')
.asFunction<GetCacheAttribute>();
final setCacheAttributeNative = moneroApi
.lookup<NativeFunction<set_cache_attribute>>('set_cache_attribute')
.asFunction<SetCacheAttribute>();
int getSyncingHeight() => getSyncingHeightNative(); int getSyncingHeight() => getSyncingHeightNative();
@ -144,11 +139,9 @@ String getSeed() => convertUTF8ToString(pointer: getSeedNative());
String getAddress({int accountIndex = 0, int addressIndex = 0}) => String getAddress({int accountIndex = 0, int addressIndex = 0}) =>
convertUTF8ToString(pointer: getAddressNative(accountIndex, addressIndex)); convertUTF8ToString(pointer: getAddressNative(accountIndex, addressIndex));
int getFullBalance({int accountIndex = 0}) => int getFullBalance({int accountIndex = 0}) => getFullBalanceNative(accountIndex);
getFullBalanceNative(accountIndex);
int getUnlockedBalance({int accountIndex = 0}) => int getUnlockedBalance({int accountIndex = 0}) => getUnlockedBalanceNative(accountIndex);
getUnlockedBalanceNative(accountIndex);
int getCurrentHeight() => getCurrentHeightNative(); int getCurrentHeight() => getCurrentHeightNative();
@ -187,7 +180,7 @@ bool setupNodeSync(
passwordPointer, passwordPointer,
_boolToInt(useSSL), _boolToInt(useSSL),
_boolToInt(isLightWallet), _boolToInt(isLightWallet),
socksProxyAddressPointer, socksProxyAddressPointer,
errorMessagePointer) != errorMessagePointer) !=
0; 0;
@ -202,8 +195,7 @@ bool setupNodeSync(
} }
if (!isSetupNode) { if (!isSetupNode) {
throw SetupWalletException( throw SetupWalletException(message: convertUTF8ToString(pointer: errorMessagePointer));
message: convertUTF8ToString(pointer: errorMessagePointer));
} }
return isSetupNode; return isSetupNode;
@ -213,8 +205,7 @@ void startRefreshSync() => startRefreshNative();
Future<bool> connectToNode() async => connecToNodeNative() != 0; Future<bool> connectToNode() async => connecToNodeNative() != 0;
void setRefreshFromBlockHeight({required int height}) => void setRefreshFromBlockHeight({required int height}) => setRefreshFromBlockHeightNative(height);
setRefreshFromBlockHeightNative(height);
void setRecoveringFromSeed({required bool isRecovery}) => void setRecoveringFromSeed({required bool isRecovery}) =>
setRecoveringFromSeedNative(_boolToInt(isRecovery)); setRecoveringFromSeedNative(_boolToInt(isRecovery));
@ -230,7 +221,7 @@ void setPasswordSync(String password) {
final errorMessagePointer = calloc<Utf8Box>(); final errorMessagePointer = calloc<Utf8Box>();
final changed = setPasswordNative(passwordPointer, errorMessagePointer) != 0; final changed = setPasswordNative(passwordPointer, errorMessagePointer) != 0;
calloc.free(passwordPointer); calloc.free(passwordPointer);
if (!changed) { if (!changed) {
final message = errorMessagePointer.ref.getValue(); final message = errorMessagePointer.ref.getValue();
calloc.free(errorMessagePointer); calloc.free(errorMessagePointer);
@ -242,24 +233,19 @@ void setPasswordSync(String password) {
void closeCurrentWallet() => closeCurrentWalletNative(); void closeCurrentWallet() => closeCurrentWalletNative();
String getSecretViewKey() => String getSecretViewKey() => convertUTF8ToString(pointer: getSecretViewKeyNative());
convertUTF8ToString(pointer: getSecretViewKeyNative());
String getPublicViewKey() => String getPublicViewKey() => convertUTF8ToString(pointer: getPublicViewKeyNative());
convertUTF8ToString(pointer: getPublicViewKeyNative());
String getSecretSpendKey() => String getSecretSpendKey() => convertUTF8ToString(pointer: getSecretSpendKeyNative());
convertUTF8ToString(pointer: getSecretSpendKeyNative());
String getPublicSpendKey() => String getPublicSpendKey() => convertUTF8ToString(pointer: getPublicSpendKeyNative());
convertUTF8ToString(pointer: getPublicSpendKeyNative());
class SyncListener { class SyncListener {
SyncListener(this.onNewBlock, this.onNewTransaction) SyncListener(this.onNewBlock, this.onNewTransaction)
: _cachedBlockchainHeight = 0, : _cachedBlockchainHeight = 0,
_lastKnownBlockHeight = 0, _lastKnownBlockHeight = 0,
_initialSyncHeight = 0; _initialSyncHeight = 0;
void Function(int, int, double) onNewBlock; void Function(int, int, double) onNewBlock;
void Function() onNewTransaction; void Function() onNewTransaction;
@ -281,8 +267,7 @@ class SyncListener {
_cachedBlockchainHeight = 0; _cachedBlockchainHeight = 0;
_lastKnownBlockHeight = 0; _lastKnownBlockHeight = 0;
_initialSyncHeight = 0; _initialSyncHeight = 0;
_updateSyncInfoTimer ??= _updateSyncInfoTimer ??= Timer.periodic(Duration(milliseconds: 1200), (_) async {
Timer.periodic(Duration(milliseconds: 1200), (_) async {
if (isNewTransactionExist()) { if (isNewTransactionExist()) {
onNewTransaction(); onNewTransaction();
} }
@ -321,8 +306,8 @@ class SyncListener {
void stop() => _updateSyncInfoTimer?.cancel(); void stop() => _updateSyncInfoTimer?.cancel();
} }
SyncListener setListeners(void Function(int, int, double) onNewBlock, SyncListener setListeners(
void Function() onNewTransaction) { void Function(int, int, double) onNewBlock, void Function() onNewTransaction) {
final listener = SyncListener(onNewBlock, onNewTransaction); final listener = SyncListener(onNewBlock, onNewTransaction);
setListenerNative(); setListenerNative();
return listener; return listener;
@ -364,7 +349,7 @@ Future<void> setupNode(
bool isLightWallet = false}) => bool isLightWallet = false}) =>
compute<Map<String, Object?>, void>(_setupNodeSync, { compute<Map<String, Object?>, void>(_setupNodeSync, {
'address': address, 'address': address,
'login': login , 'login': login,
'password': password, 'password': password,
'useSSL': useSSL, 'useSSL': useSSL,
'isLightWallet': isLightWallet, 'isLightWallet': isLightWallet,
@ -397,3 +382,23 @@ String signMessage(String message, {String address = ""}) {
return signature; return signature;
} }
bool setCacheAttribute(String name, String value) {
final namePointer = name.toNativeUtf8();
final valuePointer = value.toNativeUtf8();
final isSet = setCacheAttributeNative(namePointer, valuePointer);
calloc.free(namePointer);
calloc.free(valuePointer);
return isSet == 1;
}
String getCacheAttribute(String name) {
final namePointer = name.toNativeUtf8();
final value = convertUTF8ToString(pointer: getCacheAttributeNative(namePointer));
calloc.free(namePointer);
return value;
}

View file

@ -1,147 +0,0 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
async:
dependency: transitive
description:
name: async
url: "https://pub.dartlang.org"
source: hosted
version: "2.5.0"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
characters:
dependency: transitive
description:
name: characters
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
charcode:
dependency: transitive
description:
name: charcode
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
clock:
dependency: transitive
description:
name: clock
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
collection:
dependency: transitive
description:
name: collection
url: "https://pub.dartlang.org"
source: hosted
version: "1.15.0"
fake_async:
dependency: transitive
description:
name: fake_async
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
matcher:
dependency: transitive
description:
name: matcher
url: "https://pub.dartlang.org"
source: hosted
version: "0.12.10"
meta:
dependency: transitive
description:
name: meta
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.0"
path:
dependency: transitive
description:
name: path
url: "https://pub.dartlang.org"
source: hosted
version: "1.8.0"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.99"
source_span:
dependency: transitive
description:
name: source_span
url: "https://pub.dartlang.org"
source: hosted
version: "1.8.0"
stack_trace:
dependency: transitive
description:
name: stack_trace
url: "https://pub.dartlang.org"
source: hosted
version: "1.10.0"
stream_channel:
dependency: transitive
description:
name: stream_channel
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
string_scanner:
dependency: transitive
description:
name: string_scanner
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
term_glyph:
dependency: transitive
description:
name: term_glyph
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
test_api:
dependency: transitive
description:
name: test_api
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.19"
typed_data:
dependency: transitive
description:
name: typed_data
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.0"
vector_math:
dependency: transitive
description:
name: vector_math
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
sdks:
dart: ">=2.12.0-0.0 <3.0.0"
flutter: ">=1.20.0"

View file

@ -19,30 +19,6 @@ class DefaultSPLTokens {
mint: 'usdcsol', mint: 'usdcsol',
enabled: true, enabled: true,
), ),
SPLToken(
name: 'Wrapped Ethereum (Sollet)',
symbol: 'soETH',
mintAddress: '2FPyTwcZLUg1MDrwsyoP4D6s1tM7hAkHYRjkNb5w6Pxk',
decimal: 6,
mint: 'soEth',
iconPath: 'assets/images/eth_icon.png',
),
SPLToken(
name: 'Wrapped SOL',
symbol: 'WSOL',
mintAddress: 'So11111111111111111111111111111111111111112',
decimal: 9,
mint: 'WSOL',
iconPath: 'assets/images/sol_icon.png',
),
SPLToken(
name: 'Wrapped Bitcoin (Sollet)',
symbol: 'BTC',
mintAddress: '9n4nbM75f5Ui33ZbPYXn59EwSgE8CGsHtAeTH5YFeJ9E',
decimal: 6,
mint: 'btcsol',
iconPath: 'assets/images/btc.png',
),
SPLToken( SPLToken(
name: 'Bonk', name: 'Bonk',
symbol: 'Bonk', symbol: 'Bonk',
@ -50,21 +26,7 @@ class DefaultSPLTokens {
decimal: 5, decimal: 5,
mint: 'Bonk', mint: 'Bonk',
iconPath: 'assets/images/bonk_icon.png', iconPath: 'assets/images/bonk_icon.png',
), enabled: true,
SPLToken(
name: 'Helium Network Token',
symbol: 'HNT',
mintAddress: 'hntyVP6YFm1Hg25TN9WGLqM12b8TQmcknKrdu1oxWux',
decimal: 8,
mint: 'hnt',
iconPath: 'assets/images/hnt_icon.png',
),
SPLToken(
name: 'Pyth Network',
symbol: 'PYTH',
mintAddress: 'HZ1JovNiVvGrGNiiYvEozEVgZ58xaU3RKwX8eACQBCt3',
decimal: 6,
mint: 'pyth',
), ),
SPLToken( SPLToken(
name: 'Raydium', name: 'Raydium',
@ -73,6 +35,51 @@ class DefaultSPLTokens {
decimal: 6, decimal: 6,
mint: 'ray', mint: 'ray',
iconPath: 'assets/images/ray_icon.png', iconPath: 'assets/images/ray_icon.png',
enabled: true,
),
SPLToken(
name: 'Wrapped Ethereum (Sollet)',
symbol: 'soETH',
mintAddress: '2FPyTwcZLUg1MDrwsyoP4D6s1tM7hAkHYRjkNb5w6Pxk',
decimal: 6,
mint: 'soEth',
iconPath: 'assets/images/eth_icon.png',
enabled: false,
),
SPLToken(
name: 'Wrapped SOL',
symbol: 'WSOL',
mintAddress: 'So11111111111111111111111111111111111111112',
decimal: 9,
mint: 'WSOL',
iconPath: 'assets/images/sol_icon.png',
enabled: false,
),
SPLToken(
name: 'Wrapped Bitcoin (Sollet)',
symbol: 'BTC',
mintAddress: '9n4nbM75f5Ui33ZbPYXn59EwSgE8CGsHtAeTH5YFeJ9E',
decimal: 6,
mint: 'btcsol',
iconPath: 'assets/images/btc.png',
enabled: false,
),
SPLToken(
name: 'Helium Network Token',
symbol: 'HNT',
mintAddress: 'hntyVP6YFm1Hg25TN9WGLqM12b8TQmcknKrdu1oxWux',
decimal: 8,
mint: 'hnt',
iconPath: 'assets/images/hnt_icon.png',
enabled: false,
),
SPLToken(
name: 'Pyth Network',
symbol: 'PYTH',
mintAddress: 'HZ1JovNiVvGrGNiiYvEozEVgZ58xaU3RKwX8eACQBCt3',
decimal: 6,
mint: 'pyth',
enabled: false,
), ),
SPLToken( SPLToken(
name: 'GMT', name: 'GMT',
@ -81,6 +88,7 @@ class DefaultSPLTokens {
decimal: 6, decimal: 6,
mint: 'ray', mint: 'ray',
iconPath: 'assets/images/gmt_icon.png', iconPath: 'assets/images/gmt_icon.png',
enabled: false,
), ),
SPLToken( SPLToken(
name: 'AvocadoCoin', name: 'AvocadoCoin',
@ -89,6 +97,7 @@ class DefaultSPLTokens {
decimal: 8, decimal: 8,
mint: 'avdo', mint: 'avdo',
iconPath: 'assets/images/avdo_icon.png', iconPath: 'assets/images/avdo_icon.png',
enabled: false,
), ),
]; ];

View file

@ -15,8 +15,8 @@ class SolanaBalance extends Balance {
String _balanceFormatted() { String _balanceFormatted() {
String stringBalance = balance.toString(); String stringBalance = balance.toString();
if (stringBalance.toString().length >= 6) { if (stringBalance.toString().length >= 12) {
stringBalance = stringBalance.substring(0, 6); stringBalance = stringBalance.substring(0, 12);
} }
return stringBalance; return stringBalance;
} }

View file

@ -15,8 +15,14 @@ dependencies:
path: ../cw_core path: ../cw_core
cw_evm: cw_evm:
path: ../cw_evm path: ../cw_evm
on_chain: ^3.0.1 on_chain:
blockchain_utils: ^2.1.1 git:
url: https://github.com/cake-tech/On_chain
ref: cake-update-v1
blockchain_utils:
git:
url: https://github.com/cake-tech/blockchain_utils
ref: cake-update-v1
mobx: ^2.3.0+1 mobx: ^2.3.0+1
bip39: ^1.0.6 bip39: ^1.0.6
hive: ^2.2.3 hive: ^2.2.3

View file

@ -142,27 +142,9 @@ Then we need to generate localization files.
`$ flutter packages pub run tool/generate_localization.dart` `$ flutter packages pub run tool/generate_localization.dart`
Lastly, we will generate mobx models for the project.
Generate mobx models for `cw_core`:
`cd cw_core && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..`
Generate mobx models for `cw_monero`:
`cd cw_monero && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..`
Generate mobx models for `cw_bitcoin`:
`cd cw_bitcoin && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..`
Generate mobx models for `cw_haven`:
`cd cw_haven && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..`
Finally build mobx models for the app: Finally build mobx models for the app:
`$ flutter packages pub run build_runner build --delete-conflicting-outputs` `$ ./model_generator.sh`
### 9. Build! ### 9. Build!

View file

@ -147,6 +147,8 @@ PODS:
- shared_preferences_foundation (0.0.1): - shared_preferences_foundation (0.0.1):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- sp_scanner (0.0.1):
- Flutter
- SwiftProtobuf (1.25.2) - SwiftProtobuf (1.25.2)
- SwiftyGif (5.4.4) - SwiftyGif (5.4.4)
- Toast (4.1.0) - Toast (4.1.0)
@ -188,6 +190,7 @@ DEPENDENCIES:
- sensitive_clipboard (from `.symlinks/plugins/sensitive_clipboard/ios`) - sensitive_clipboard (from `.symlinks/plugins/sensitive_clipboard/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sp_scanner (from `.symlinks/plugins/sp_scanner/ios`)
- uni_links (from `.symlinks/plugins/uni_links/ios`) - uni_links (from `.symlinks/plugins/uni_links/ios`)
- UnstoppableDomainsResolution (~> 4.0.0) - UnstoppableDomainsResolution (~> 4.0.0)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
@ -259,6 +262,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/share_plus/ios" :path: ".symlinks/plugins/share_plus/ios"
shared_preferences_foundation: shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin" :path: ".symlinks/plugins/shared_preferences_foundation/darwin"
sp_scanner:
:path: ".symlinks/plugins/sp_scanner/ios"
uni_links: uni_links:
:path: ".symlinks/plugins/uni_links/ios" :path: ".symlinks/plugins/uni_links/ios"
url_launcher_ios: url_launcher_ios:
@ -302,6 +307,7 @@ SPEC CHECKSUMS:
sensitive_clipboard: d4866e5d176581536c27bb1618642ee83adca986 sensitive_clipboard: d4866e5d176581536c27bb1618642ee83adca986
share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68 share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
sp_scanner: eaa617fa827396b967116b7f1f43549ca62e9a12
SwiftProtobuf: 407a385e97fd206c4fbe880cc84123989167e0d1 SwiftProtobuf: 407a385e97fd206c4fbe880cc84123989167e0d1
SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f
Toast: ec33c32b8688982cecc6348adeae667c1b9938da Toast: ec33c32b8688982cecc6348adeae667c1b9938da

View file

@ -121,20 +121,12 @@ class CWBitcoin extends Bitcoin {
priority: priority != null ? priority as BitcoinTransactionPriority : null, priority: priority != null ? priority as BitcoinTransactionPriority : null,
feeRate: feeRate); feeRate: feeRate);
@override
List<String> getAddresses(Object wallet) {
final bitcoinWallet = wallet as ElectrumWallet;
return bitcoinWallet.walletAddresses.addressesByReceiveType
.map((BitcoinAddressRecord addr) => addr.address)
.toList();
}
@override @override
@computed @computed
List<ElectrumSubAddress> getSubAddresses(Object wallet) { List<ElectrumSubAddress> getSubAddresses(Object wallet) {
final electrumWallet = wallet as ElectrumWallet; final electrumWallet = wallet as ElectrumWallet;
return electrumWallet.walletAddresses.addressesByReceiveType return electrumWallet.walletAddresses.addressesByReceiveType
.map((BitcoinAddressRecord addr) => ElectrumSubAddress( .map((BaseBitcoinAddressRecord addr) => ElectrumSubAddress(
id: addr.index, id: addr.index,
name: addr.name, name: addr.name,
address: addr.address, address: addr.address,
@ -207,12 +199,12 @@ class CWBitcoin extends Bitcoin {
Future<void> updateUnspents(Object wallet) async { Future<void> updateUnspents(Object wallet) async {
final bitcoinWallet = wallet as ElectrumWallet; final bitcoinWallet = wallet as ElectrumWallet;
await bitcoinWallet.updateUnspent(); await bitcoinWallet.updateAllUnspents();
} }
WalletService createBitcoinWalletService( WalletService createBitcoinWalletService(
Box<WalletInfo> walletInfoSource, Box<UnspentCoinsInfo> unspentCoinSource) { Box<WalletInfo> walletInfoSource, Box<UnspentCoinsInfo> unspentCoinSource, bool alwaysScan) {
return BitcoinWalletService(walletInfoSource, unspentCoinSource); return BitcoinWalletService(walletInfoSource, unspentCoinSource, alwaysScan);
} }
WalletService createLitecoinWalletService( WalletService createLitecoinWalletService(
@ -247,6 +239,12 @@ class CWBitcoin extends Bitcoin {
return BitcoinReceivePageOption.fromType(bitcoinWallet.walletAddresses.addressPageType); return BitcoinReceivePageOption.fromType(bitcoinWallet.walletAddresses.addressPageType);
} }
@override
bool hasSelectedSilentPayments(Object wallet) {
final bitcoinWallet = wallet as ElectrumWallet;
return bitcoinWallet.walletAddresses.addressPageType == SilentPaymentsAddresType.p2sp;
}
@override @override
List<ReceivePageOption> getBitcoinReceivePageOptions() => BitcoinReceivePageOption.all; List<ReceivePageOption> getBitcoinReceivePageOptions() => BitcoinReceivePageOption.all;
@ -465,4 +463,137 @@ class CWBitcoin extends Bitcoin {
throw err; throw err;
} }
} }
@override
List<ElectrumSubAddress> getSilentPaymentAddresses(Object wallet) {
final bitcoinWallet = wallet as ElectrumWallet;
return bitcoinWallet.walletAddresses.silentAddresses
.where((addr) => addr.type != SegwitAddresType.p2tr)
.map((addr) => ElectrumSubAddress(
id: addr.index,
name: addr.name,
address: addr.address,
txCount: addr.txCount,
balance: addr.balance,
isChange: addr.isHidden))
.toList();
}
@override
List<ElectrumSubAddress> getSilentPaymentReceivedAddresses(Object wallet) {
final bitcoinWallet = wallet as ElectrumWallet;
return bitcoinWallet.walletAddresses.silentAddresses
.where((addr) => addr.type == SegwitAddresType.p2tr)
.map((addr) => ElectrumSubAddress(
id: addr.index,
name: addr.name,
address: addr.address,
txCount: addr.txCount,
balance: addr.balance,
isChange: addr.isHidden))
.toList();
}
@override
bool isBitcoinReceivePageOption(ReceivePageOption option) {
return option is BitcoinReceivePageOption;
}
@override
BitcoinAddressType getOptionToType(ReceivePageOption option) {
return (option as BitcoinReceivePageOption).toType();
}
@override
@computed
bool getScanningActive(Object wallet) {
final bitcoinWallet = wallet as ElectrumWallet;
return bitcoinWallet.silentPaymentsScanningActive;
}
@override
Future<void> setScanningActive(Object wallet, bool active) async {
final bitcoinWallet = wallet as ElectrumWallet;
if (active && !(await getNodeIsElectrsSPEnabled(wallet))) {
final node = Node(
useSSL: false,
uri: 'electrs.cakewallet.com:${(wallet.network == BitcoinNetwork.testnet ? 50002 : 50001)}',
);
node.type = WalletType.bitcoin;
await bitcoinWallet.connectToNode(node: node);
}
bitcoinWallet.setSilentPaymentsScanning(active);
}
@override
bool isTestnet(Object wallet) {
final bitcoinWallet = wallet as ElectrumWallet;
return bitcoinWallet.isTestnet ?? false;
}
@override
int getHeightByDate({required DateTime date}) => getBitcoinHeightByDate(date: date);
@override
Future<void> rescan(Object wallet, {required int height, bool? doSingleScan}) async {
final bitcoinWallet = wallet as ElectrumWallet;
if (!(await getNodeIsElectrsSPEnabled(wallet))) {
final node = Node(
useSSL: false,
uri: 'electrs.cakewallet.com:${(wallet.network == BitcoinNetwork.testnet ? 50002 : 50001)}',
);
node.type = WalletType.bitcoin;
await bitcoinWallet.connectToNode(node: node);
}
bitcoinWallet.rescan(height: height, doSingleScan: doSingleScan);
}
Future<bool> getNodeIsElectrs(Object wallet) async {
final bitcoinWallet = wallet as ElectrumWallet;
final version = await bitcoinWallet.electrumClient.version();
if (version.isEmpty) {
return false;
}
final server = version[0];
if (server.toLowerCase().contains('electrs')) {
return true;
}
return false;
}
@override
Future<bool> getNodeIsElectrsSPEnabled(Object wallet) async {
if (!(await getNodeIsElectrs(wallet))) {
return false;
}
final bitcoinWallet = wallet as ElectrumWallet;
final tweaksResponse = await bitcoinWallet.electrumClient.getTweaks(height: 0);
if (tweaksResponse != null) {
return true;
}
return false;
}
@override
void deleteSilentPaymentAddress(Object wallet, String address) {
final bitcoinWallet = wallet as ElectrumWallet;
bitcoinWallet.walletAddresses.deleteSilentPaymentAddress(address);
}
@override
Future<void> updateFeeRates(Object wallet) async {
final bitcoinWallet = wallet as ElectrumWallet;
await bitcoinWallet.updateFeeRates();
}
} }

View file

@ -26,7 +26,7 @@ class AddressValidator extends TextValidator {
return '^[0-9a-zA-Z]{59}\$|^[0-9a-zA-Z]{92}\$|^[0-9a-zA-Z]{104}\$' return '^[0-9a-zA-Z]{59}\$|^[0-9a-zA-Z]{92}\$|^[0-9a-zA-Z]{104}\$'
'|^[0-9a-zA-Z]{105}\$|^addr1[0-9a-zA-Z]{98}\$'; '|^[0-9a-zA-Z]{105}\$|^addr1[0-9a-zA-Z]{98}\$';
case CryptoCurrency.btc: case CryptoCurrency.btc:
return '^${P2pkhAddress.regex.pattern}\$|^${P2shAddress.regex.pattern}\$|^${P2wpkhAddress.regex.pattern}\$|${P2trAddress.regex.pattern}\$|^${P2wshAddress.regex.pattern}\$'; return '^${P2pkhAddress.regex.pattern}\$|^${P2shAddress.regex.pattern}\$|^${P2wpkhAddress.regex.pattern}\$|${P2trAddress.regex.pattern}\$|^${P2wshAddress.regex.pattern}\$|^${SilentPaymentAddress.regex.pattern}\$';
case CryptoCurrency.nano: case CryptoCurrency.nano:
return '[0-9a-zA-Z_]'; return '[0-9a-zA-Z_]';
case CryptoCurrency.banano: case CryptoCurrency.banano:
@ -274,7 +274,9 @@ class AddressValidator extends TextValidator {
'|([^0-9a-zA-Z]|^)([23][a-km-zA-HJ-NP-Z1-9]{25,34})([^0-9a-zA-Z]|\$)' //P2shAddress type '|([^0-9a-zA-Z]|^)([23][a-km-zA-HJ-NP-Z1-9]{25,34})([^0-9a-zA-Z]|\$)' //P2shAddress type
'|([^0-9a-zA-Z]|^)((bc|tb)1q[ac-hj-np-z02-9]{25,39})([^0-9a-zA-Z]|\$)' //P2wpkhAddress type '|([^0-9a-zA-Z]|^)((bc|tb)1q[ac-hj-np-z02-9]{25,39})([^0-9a-zA-Z]|\$)' //P2wpkhAddress type
'|([^0-9a-zA-Z]|^)((bc|tb)1q[ac-hj-np-z02-9]{40,80})([^0-9a-zA-Z]|\$)' //P2wshAddress type '|([^0-9a-zA-Z]|^)((bc|tb)1q[ac-hj-np-z02-9]{40,80})([^0-9a-zA-Z]|\$)' //P2wshAddress type
'|([^0-9a-zA-Z]|^)((bc|tb)1p([ac-hj-np-z02-9]{39}|[ac-hj-np-z02-9]{59}|[ac-hj-np-z02-9]{8,89}))([^0-9a-zA-Z]|\$)'; //P2trAddress type '|([^0-9a-zA-Z]|^)((bc|tb)1p([ac-hj-np-z02-9]{39}|[ac-hj-np-z02-9]{59}|[ac-hj-np-z02-9]{8,89}))([^0-9a-zA-Z]|\$)' //P2trAddress type
'|${SilentPaymentAddress.regex.pattern}\$';
case CryptoCurrency.ltc: case CryptoCurrency.ltc:
return '([^0-9a-zA-Z]|^)^L[a-zA-Z0-9]{26,33}([^0-9a-zA-Z]|\$)' return '([^0-9a-zA-Z]|^)^L[a-zA-Z0-9]{26,33}([^0-9a-zA-Z]|\$)'
'|([^0-9a-zA-Z]|^)[LM][a-km-zA-HJ-NP-Z1-9]{26,33}([^0-9a-zA-Z]|\$)' '|([^0-9a-zA-Z]|^)[LM][a-km-zA-HJ-NP-Z1-9]{26,33}([^0-9a-zA-Z]|\$)'

View file

@ -3,7 +3,13 @@ import 'package:cw_core/sync_status.dart';
String syncStatusTitle(SyncStatus syncStatus) { String syncStatusTitle(SyncStatus syncStatus) {
if (syncStatus is SyncingSyncStatus) { if (syncStatus is SyncingSyncStatus) {
return S.current.Blocks_remaining('${syncStatus.blocksLeft}'); return syncStatus.blocksLeft == 1
? S.current.block_remaining
: S.current.Blocks_remaining('${syncStatus.blocksLeft}');
}
if (syncStatus is SyncedTipSyncStatus) {
return S.current.silent_payments_scanned_tip(syncStatus.tip.toString());
} }
if (syncStatus is SyncedSyncStatus) { if (syncStatus is SyncedSyncStatus) {
@ -34,5 +40,17 @@ String syncStatusTitle(SyncStatus syncStatus) {
return S.current.sync_status_failed_connect; return S.current.sync_status_failed_connect;
} }
if (syncStatus is UnsupportedSyncStatus) {
return S.current.sync_status_unsupported;
}
if (syncStatus is TimedOutSyncStatus) {
return S.current.sync_status_timed_out;
}
if (syncStatus is SyncronizingSyncStatus) {
return S.current.sync_status_syncronizing;
}
return ''; return '';
} }

View file

@ -58,7 +58,7 @@ class WalletCreationService {
checkIfExists(credentials.name); checkIfExists(credentials.name);
final password = generateWalletPassword(); final password = generateWalletPassword();
credentials.password = password; credentials.password = password;
if (type == WalletType.bitcoinCash || type == WalletType.ethereum) { if (_hasSeedPhraseLengthOption) {
credentials.seedPhraseLength = settingsStore.seedPhraseLength.value; credentials.seedPhraseLength = settingsStore.seedPhraseLength.value;
} }
await keyService.saveWalletPassword(password: password, walletName: credentials.name); await keyService.saveWalletPassword(password: password, walletName: credentials.name);
@ -72,6 +72,25 @@ class WalletCreationService {
return wallet; return wallet;
} }
bool get _hasSeedPhraseLengthOption {
switch (type) {
case WalletType.ethereum:
case WalletType.bitcoinCash:
case WalletType.polygon:
case WalletType.solana:
case WalletType.tron:
return true;
case WalletType.monero:
case WalletType.none:
case WalletType.bitcoin:
case WalletType.litecoin:
case WalletType.haven:
case WalletType.nano:
case WalletType.banano:
return false;
}
}
Future<WalletBase> restoreFromKeys(WalletCredentials credentials, {bool? isTestnet}) async { Future<WalletBase> restoreFromKeys(WalletCredentials credentials, {bool? isTestnet}) async {
checkIfExists(credentials.name); checkIfExists(credentials.name);
final password = generateWalletPassword(); final password = generateWalletPassword();

View file

@ -6,6 +6,7 @@ import 'package:cake_wallet/anypay/any_pay_payment_committed_info.dart';
import 'package:cake_wallet/anypay/anypay_api.dart'; import 'package:cake_wallet/anypay/anypay_api.dart';
import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart'; import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart';
import 'package:cake_wallet/buy/dfx/dfx_buy_provider.dart';
import 'package:cake_wallet/buy/moonpay/moonpay_provider.dart'; import 'package:cake_wallet/buy/moonpay/moonpay_provider.dart';
import 'package:cake_wallet/buy/onramper/onramper_buy_provider.dart'; import 'package:cake_wallet/buy/onramper/onramper_buy_provider.dart';
import 'package:cake_wallet/buy/order.dart'; import 'package:cake_wallet/buy/order.dart';
@ -14,6 +15,8 @@ import 'package:cake_wallet/buy/robinhood/robinhood_buy_provider.dart';
import 'package:cake_wallet/core/auth_service.dart'; import 'package:cake_wallet/core/auth_service.dart';
import 'package:cake_wallet/core/backup_service.dart'; import 'package:cake_wallet/core/backup_service.dart';
import 'package:cake_wallet/core/key_service.dart'; import 'package:cake_wallet/core/key_service.dart';
import 'package:cake_wallet/core/secure_storage.dart';
import 'package:cake_wallet/core/totp_request_details.dart';
import 'package:cake_wallet/core/wallet_connect/wallet_connect_key_service.dart'; import 'package:cake_wallet/core/wallet_connect/wallet_connect_key_service.dart';
import 'package:cake_wallet/core/wallet_connect/wc_bottom_sheet_service.dart'; import 'package:cake_wallet/core/wallet_connect/wc_bottom_sheet_service.dart';
import 'package:cake_wallet/core/wallet_connect/web3wallet_service.dart'; import 'package:cake_wallet/core/wallet_connect/web3wallet_service.dart';
@ -26,10 +29,6 @@ import 'package:cake_wallet/entities/contact.dart';
import 'package:cake_wallet/entities/contact_record.dart'; import 'package:cake_wallet/entities/contact_record.dart';
import 'package:cake_wallet/entities/exchange_api_mode.dart'; import 'package:cake_wallet/entities/exchange_api_mode.dart';
import 'package:cake_wallet/entities/parse_address_from_domain.dart'; import 'package:cake_wallet/entities/parse_address_from_domain.dart';
import 'package:cake_wallet/view_model/link_view_model.dart';
import 'package:cake_wallet/tron/tron.dart';
import 'package:cake_wallet/src/screens/transaction_details/rbf_details_page.dart';
import 'package:cw_core/receive_page_option.dart';
import 'package:cake_wallet/entities/qr_view_data.dart'; import 'package:cake_wallet/entities/qr_view_data.dart';
import 'package:cake_wallet/entities/template.dart'; import 'package:cake_wallet/entities/template.dart';
import 'package:cake_wallet/entities/transaction_description.dart'; import 'package:cake_wallet/entities/transaction_description.dart';
@ -105,12 +104,14 @@ import 'package:cake_wallet/src/screens/seed/wallet_seed_page.dart';
import 'package:cake_wallet/src/screens/send/send_page.dart'; import 'package:cake_wallet/src/screens/send/send_page.dart';
import 'package:cake_wallet/src/screens/send/send_template_page.dart'; import 'package:cake_wallet/src/screens/send/send_template_page.dart';
import 'package:cake_wallet/src/screens/settings/connection_sync_page.dart'; import 'package:cake_wallet/src/screens/settings/connection_sync_page.dart';
import 'package:cake_wallet/src/screens/settings/desktop_settings/desktop_settings_page.dart';
import 'package:cake_wallet/src/screens/settings/display_settings_page.dart'; import 'package:cake_wallet/src/screens/settings/display_settings_page.dart';
import 'package:cake_wallet/src/screens/settings/domain_lookups_page.dart'; import 'package:cake_wallet/src/screens/settings/domain_lookups_page.dart';
import 'package:cake_wallet/src/screens/settings/manage_nodes_page.dart'; import 'package:cake_wallet/src/screens/settings/manage_nodes_page.dart';
import 'package:cake_wallet/src/screens/settings/other_settings_page.dart'; import 'package:cake_wallet/src/screens/settings/other_settings_page.dart';
import 'package:cake_wallet/src/screens/settings/privacy_page.dart'; import 'package:cake_wallet/src/screens/settings/privacy_page.dart';
import 'package:cake_wallet/src/screens/settings/security_backup_page.dart'; import 'package:cake_wallet/src/screens/settings/security_backup_page.dart';
import 'package:cake_wallet/src/screens/settings/silent_payments_settings.dart';
import 'package:cake_wallet/src/screens/settings/tor_page.dart'; import 'package:cake_wallet/src/screens/settings/tor_page.dart';
import 'package:cake_wallet/src/screens/settings/trocador_providers_page.dart'; import 'package:cake_wallet/src/screens/settings/trocador_providers_page.dart';
import 'package:cake_wallet/src/screens/setup_2fa/modify_2fa_page.dart'; import 'package:cake_wallet/src/screens/setup_2fa/modify_2fa_page.dart';
@ -124,6 +125,7 @@ import 'package:cake_wallet/src/screens/support/support_page.dart';
import 'package:cake_wallet/src/screens/support_chat/support_chat_page.dart'; import 'package:cake_wallet/src/screens/support_chat/support_chat_page.dart';
import 'package:cake_wallet/src/screens/support_other_links/support_other_links_page.dart'; import 'package:cake_wallet/src/screens/support_other_links/support_other_links_page.dart';
import 'package:cake_wallet/src/screens/trade_details/trade_details_page.dart'; import 'package:cake_wallet/src/screens/trade_details/trade_details_page.dart';
import 'package:cake_wallet/src/screens/transaction_details/rbf_details_page.dart';
import 'package:cake_wallet/src/screens/transaction_details/transaction_details_page.dart'; import 'package:cake_wallet/src/screens/transaction_details/transaction_details_page.dart';
import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_details_page.dart'; import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_details_page.dart';
import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_list_page.dart'; import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_list_page.dart';
@ -147,6 +149,7 @@ import 'package:cake_wallet/store/templates/send_template_store.dart';
import 'package:cake_wallet/store/wallet_list_store.dart'; import 'package:cake_wallet/store/wallet_list_store.dart';
import 'package:cake_wallet/store/yat/yat_store.dart'; import 'package:cake_wallet/store/yat/yat_store.dart';
import 'package:cake_wallet/themes/theme_list.dart'; import 'package:cake_wallet/themes/theme_list.dart';
import 'package:cake_wallet/tron/tron.dart';
import 'package:cake_wallet/utils/device_info.dart'; import 'package:cake_wallet/utils/device_info.dart';
import 'package:cake_wallet/utils/payment_request.dart'; import 'package:cake_wallet/utils/payment_request.dart';
import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart';
@ -160,10 +163,10 @@ import 'package:cake_wallet/view_model/buy/buy_view_model.dart';
import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart'; import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart';
import 'package:cake_wallet/view_model/contact_list/contact_view_model.dart'; import 'package:cake_wallet/view_model/contact_list/contact_view_model.dart';
import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart';
import 'package:cake_wallet/view_model/dashboard/cake_features_view_model.dart';
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
import 'package:cake_wallet/view_model/dashboard/desktop_sidebar_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/desktop_sidebar_view_model.dart';
import 'package:cake_wallet/view_model/dashboard/home_settings_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/home_settings_view_model.dart';
import 'package:cake_wallet/view_model/dashboard/market_place_view_model.dart';
import 'package:cake_wallet/view_model/dashboard/nft_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/nft_view_model.dart';
import 'package:cake_wallet/view_model/dashboard/receive_option_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/receive_option_view_model.dart';
import 'package:cake_wallet/view_model/edit_backup_password_view_model.dart'; import 'package:cake_wallet/view_model/edit_backup_password_view_model.dart';
@ -179,6 +182,7 @@ import 'package:cake_wallet/view_model/ionia/ionia_gift_card_details_view_model.
import 'package:cake_wallet/view_model/ionia/ionia_gift_cards_list_view_model.dart'; import 'package:cake_wallet/view_model/ionia/ionia_gift_cards_list_view_model.dart';
import 'package:cake_wallet/view_model/ionia/ionia_payment_status_view_model.dart'; import 'package:cake_wallet/view_model/ionia/ionia_payment_status_view_model.dart';
import 'package:cake_wallet/view_model/ionia/ionia_purchase_merch_view_model.dart'; import 'package:cake_wallet/view_model/ionia/ionia_purchase_merch_view_model.dart';
import 'package:cake_wallet/view_model/link_view_model.dart';
import 'package:cake_wallet/view_model/monero_account_list/account_list_item.dart'; import 'package:cake_wallet/view_model/monero_account_list/account_list_item.dart';
import 'package:cake_wallet/view_model/monero_account_list/monero_account_edit_or_create_view_model.dart'; import 'package:cake_wallet/view_model/monero_account_list/monero_account_edit_or_create_view_model.dart';
import 'package:cake_wallet/view_model/monero_account_list/monero_account_list_view_model.dart'; import 'package:cake_wallet/view_model/monero_account_list/monero_account_list_view_model.dart';
@ -199,6 +203,7 @@ import 'package:cake_wallet/view_model/settings/display_settings_view_model.dart
import 'package:cake_wallet/view_model/settings/other_settings_view_model.dart'; import 'package:cake_wallet/view_model/settings/other_settings_view_model.dart';
import 'package:cake_wallet/view_model/settings/privacy_settings_view_model.dart'; import 'package:cake_wallet/view_model/settings/privacy_settings_view_model.dart';
import 'package:cake_wallet/view_model/settings/security_settings_view_model.dart'; import 'package:cake_wallet/view_model/settings/security_settings_view_model.dart';
import 'package:cake_wallet/view_model/settings/silent_payments_settings_view_model.dart';
import 'package:cake_wallet/view_model/settings/trocador_providers_view_model.dart'; import 'package:cake_wallet/view_model/settings/trocador_providers_view_model.dart';
import 'package:cake_wallet/view_model/setup_pin_code_view_model.dart'; import 'package:cake_wallet/view_model/setup_pin_code_view_model.dart';
import 'package:cake_wallet/view_model/support_view_model.dart'; import 'package:cake_wallet/view_model/support_view_model.dart';
@ -222,6 +227,7 @@ import 'package:cake_wallet/view_model/wallet_seed_view_model.dart';
import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/nano_account.dart'; import 'package:cw_core/nano_account.dart';
import 'package:cw_core/node.dart'; import 'package:cw_core/node.dart';
import 'package:cw_core/receive_page_option.dart';
import 'package:cw_core/transaction_info.dart'; import 'package:cw_core/transaction_info.dart';
import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/unspent_coins_info.dart';
import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_info.dart';
@ -233,11 +239,6 @@ import 'package:get_it/get_it.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:mobx/mobx.dart'; import 'package:mobx/mobx.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:cake_wallet/core/secure_storage.dart';
import 'buy/dfx/dfx_buy_provider.dart';
import 'core/totp_request_details.dart';
import 'src/screens/settings/desktop_settings/desktop_settings_page.dart';
final getIt = GetIt.instance; final getIt = GetIt.instance;
@ -745,6 +746,9 @@ Future<void> setup({
return DisplaySettingsViewModel(getIt.get<SettingsStore>()); return DisplaySettingsViewModel(getIt.get<SettingsStore>());
}); });
getIt.registerFactory(() =>
SilentPaymentsSettingsViewModel(getIt.get<SettingsStore>(), getIt.get<AppStore>().wallet!));
getIt.registerFactory(() { getIt.registerFactory(() {
return PrivacySettingsViewModel(getIt.get<SettingsStore>(), getIt.get<AppStore>().wallet!); return PrivacySettingsViewModel(getIt.get<SettingsStore>(), getIt.get<AppStore>().wallet!);
}); });
@ -795,8 +799,8 @@ Future<void> setup({
getIt.registerFactory(() => ConnectionSyncPage(getIt.get<DashboardViewModel>())); getIt.registerFactory(() => ConnectionSyncPage(getIt.get<DashboardViewModel>()));
getIt.registerFactory( getIt.registerFactory(() => SecurityBackupPage(getIt.get<SecuritySettingsViewModel>(),
() => SecurityBackupPage(getIt.get<SecuritySettingsViewModel>(), getIt.get<AuthService>())); getIt.get<AuthService>(), getIt.get<AppStore>().wallet!.isHardwareWallet));
getIt.registerFactory(() => PrivacyPage(getIt.get<PrivacySettingsViewModel>())); getIt.registerFactory(() => PrivacyPage(getIt.get<PrivacySettingsViewModel>()));
@ -806,6 +810,9 @@ Future<void> setup({
getIt.registerFactory(() => DisplaySettingsPage(getIt.get<DisplaySettingsViewModel>())); getIt.registerFactory(() => DisplaySettingsPage(getIt.get<DisplaySettingsViewModel>()));
getIt.registerFactory(
() => SilentPaymentsSettingsPage(getIt.get<SilentPaymentsSettingsViewModel>()));
getIt.registerFactory(() => OtherSettingsPage(getIt.get<OtherSettingsViewModel>())); getIt.registerFactory(() => OtherSettingsPage(getIt.get<OtherSettingsViewModel>()));
getIt.registerFactory(() => NanoChangeRepPage( getIt.registerFactory(() => NanoChangeRepPage(
@ -893,7 +900,11 @@ Future<void> setup({
case WalletType.monero: case WalletType.monero:
return monero!.createMoneroWalletService(_walletInfoSource, _unspentCoinsInfoSource); return monero!.createMoneroWalletService(_walletInfoSource, _unspentCoinsInfoSource);
case WalletType.bitcoin: case WalletType.bitcoin:
return bitcoin!.createBitcoinWalletService(_walletInfoSource, _unspentCoinsInfoSource); return bitcoin!.createBitcoinWalletService(
_walletInfoSource,
_unspentCoinsInfoSource,
getIt.get<SettingsStore>().silentPaymentsAlwaysScan,
);
case WalletType.litecoin: case WalletType.litecoin:
return bitcoin!.createLitecoinWalletService(_walletInfoSource, _unspentCoinsInfoSource); return bitcoin!.createLitecoinWalletService(_walletInfoSource, _unspentCoinsInfoSource);
case WalletType.ethereum: case WalletType.ethereum:
@ -1089,7 +1100,7 @@ Future<void> setup({
getIt.registerFactory(() => IoniaGiftCardsListViewModel(ioniaService: getIt.get<IoniaService>())); getIt.registerFactory(() => IoniaGiftCardsListViewModel(ioniaService: getIt.get<IoniaService>()));
getIt.registerFactory(() => MarketPlaceViewModel(getIt.get<IoniaService>())); getIt.registerFactory(() => CakeFeaturesViewModel(getIt.get<IoniaService>()));
getIt.registerFactory(() => IoniaAuthViewModel(ioniaService: getIt.get<IoniaService>())); getIt.registerFactory(() => IoniaAuthViewModel(ioniaService: getIt.get<IoniaService>()));

View file

@ -24,8 +24,9 @@ import 'package:collection/collection.dart';
const newCakeWalletMoneroUri = 'xmr-node.cakewallet.com:18081'; const newCakeWalletMoneroUri = 'xmr-node.cakewallet.com:18081';
const cakeWalletBitcoinElectrumUri = 'electrum.cakewallet.com:50002'; const cakeWalletBitcoinElectrumUri = 'electrum.cakewallet.com:50002';
const publicBitcoinTestnetElectrumAddress = 'electrum.blockstream.info'; const cakeWalletSilentPaymentsElectrsUri = 'electrs.cakewallet.com:50001';
const publicBitcoinTestnetElectrumPort = '60002'; const publicBitcoinTestnetElectrumAddress = 'electrs.cakewallet.com';
const publicBitcoinTestnetElectrumPort = '50002';
const publicBitcoinTestnetElectrumUri = const publicBitcoinTestnetElectrumUri =
'$publicBitcoinTestnetElectrumAddress:$publicBitcoinTestnetElectrumPort'; '$publicBitcoinTestnetElectrumAddress:$publicBitcoinTestnetElectrumPort';
const cakeWalletLitecoinElectrumUri = 'ltc-electrum.cakewallet.com:50002'; const cakeWalletLitecoinElectrumUri = 'ltc-electrum.cakewallet.com:50002';
@ -224,6 +225,9 @@ Future<void> defaultSettingsMigration(
await addTronNodeList(nodes: nodes); await addTronNodeList(nodes: nodes);
await changeTronCurrentNodeToDefault(sharedPreferences: sharedPreferences, nodes: nodes); await changeTronCurrentNodeToDefault(sharedPreferences: sharedPreferences, nodes: nodes);
break; break;
case 34:
await _addElectRsNode(nodes, sharedPreferences);
break;
default: default:
break; break;
} }
@ -790,7 +794,8 @@ Future<void> changeDefaultBitcoinNode(
final needToReplaceCurrentBitcoinNode = final needToReplaceCurrentBitcoinNode =
currentBitcoinNode.uri.toString().contains(cakeWalletBitcoinNodeUriPattern); currentBitcoinNode.uri.toString().contains(cakeWalletBitcoinNodeUriPattern);
final newCakeWalletBitcoinNode = Node(uri: newCakeWalletBitcoinUri, type: WalletType.bitcoin); final newCakeWalletBitcoinNode =
Node(uri: newCakeWalletBitcoinUri, type: WalletType.bitcoin, useSSL: false);
await nodeSource.add(newCakeWalletBitcoinNode); await nodeSource.add(newCakeWalletBitcoinNode);
@ -800,6 +805,26 @@ Future<void> changeDefaultBitcoinNode(
} }
} }
Future<void> _addElectRsNode(Box<Node> nodeSource, SharedPreferences sharedPreferences) async {
const cakeWalletBitcoinNodeUriPattern = '.cakewallet.com';
final currentBitcoinNodeId =
sharedPreferences.getInt(PreferencesKey.currentBitcoinElectrumSererIdKey);
final currentBitcoinNode =
nodeSource.values.firstWhere((node) => node.key == currentBitcoinNodeId);
final needToReplaceCurrentBitcoinNode =
currentBitcoinNode.uri.toString().contains(cakeWalletBitcoinNodeUriPattern);
final newElectRsBitcoinNode =
Node(uri: cakeWalletSilentPaymentsElectrsUri, type: WalletType.bitcoin, useSSL: false);
await nodeSource.add(newElectRsBitcoinNode);
if (needToReplaceCurrentBitcoinNode) {
await sharedPreferences.setInt(
PreferencesKey.currentBitcoinElectrumSererIdKey, newElectRsBitcoinNode.key as int);
}
}
Future<void> checkCurrentNodes( Future<void> checkCurrentNodes(
Box<Node> nodeSource, Box<Node> powNodeSource, SharedPreferences sharedPreferences) async { Box<Node> nodeSource, Box<Node> powNodeSource, SharedPreferences sharedPreferences) async {
final currentMoneroNodeId = sharedPreferences.getInt(PreferencesKey.currentNodeIdKey); final currentMoneroNodeId = sharedPreferences.getInt(PreferencesKey.currentNodeIdKey);
@ -845,14 +870,19 @@ Future<void> checkCurrentNodes(
} }
if (currentBitcoinElectrumServer == null) { if (currentBitcoinElectrumServer == null) {
final cakeWalletElectrum = Node(uri: cakeWalletBitcoinElectrumUri, type: WalletType.bitcoin); final cakeWalletElectrum =
Node(uri: cakeWalletBitcoinElectrumUri, type: WalletType.bitcoin, useSSL: false);
await nodeSource.add(cakeWalletElectrum); await nodeSource.add(cakeWalletElectrum);
final cakeWalletElectrumTestnet =
Node(uri: publicBitcoinTestnetElectrumUri, type: WalletType.bitcoin, useSSL: false);
await nodeSource.add(cakeWalletElectrumTestnet);
await sharedPreferences.setInt( await sharedPreferences.setInt(
PreferencesKey.currentBitcoinElectrumSererIdKey, cakeWalletElectrum.key as int); PreferencesKey.currentBitcoinElectrumSererIdKey, cakeWalletElectrum.key as int);
} }
if (currentLitecoinElectrumServer == null) { if (currentLitecoinElectrumServer == null) {
final cakeWalletElectrum = Node(uri: cakeWalletLitecoinElectrumUri, type: WalletType.litecoin); final cakeWalletElectrum =
Node(uri: cakeWalletLitecoinElectrumUri, type: WalletType.litecoin, useSSL: false);
await nodeSource.add(cakeWalletElectrum); await nodeSource.add(cakeWalletElectrum);
await sharedPreferences.setInt( await sharedPreferences.setInt(
PreferencesKey.currentLitecoinElectrumSererIdKey, cakeWalletElectrum.key as int); PreferencesKey.currentLitecoinElectrumSererIdKey, cakeWalletElectrum.key as int);
@ -887,7 +917,8 @@ Future<void> checkCurrentNodes(
} }
if (currentBitcoinCashNodeServer == null) { if (currentBitcoinCashNodeServer == null) {
final node = Node(uri: cakeWalletBitcoinCashDefaultNodeUri, type: WalletType.bitcoinCash); final node =
Node(uri: cakeWalletBitcoinCashDefaultNodeUri, type: WalletType.bitcoinCash, useSSL: false);
await nodeSource.add(node); await nodeSource.add(node);
await sharedPreferences.setInt(PreferencesKey.currentBitcoinCashNodeIdKey, node.key as int); await sharedPreferences.setInt(PreferencesKey.currentBitcoinCashNodeIdKey, node.key as int);
} }
@ -921,7 +952,11 @@ Future<void> resetBitcoinElectrumServer(
.firstWhereOrNull((node) => node.uriRaw.toString() == cakeWalletBitcoinElectrumUri); .firstWhereOrNull((node) => node.uriRaw.toString() == cakeWalletBitcoinElectrumUri);
if (cakeWalletNode == null) { if (cakeWalletNode == null) {
cakeWalletNode = Node(uri: cakeWalletBitcoinElectrumUri, type: WalletType.bitcoin); cakeWalletNode =
Node(uri: cakeWalletBitcoinElectrumUri, type: WalletType.bitcoin, useSSL: false);
// final cakeWalletElectrumTestnet =
// Node(uri: publicBitcoinTestnetElectrumUri, type: WalletType.bitcoin, useSSL: false);
// await nodeSource.add(cakeWalletElectrumTestnet);
await nodeSource.add(cakeWalletNode); await nodeSource.add(cakeWalletNode);
} }

View file

@ -44,6 +44,8 @@ class PreferencesKey {
static const polygonTransactionPriority = 'current_fee_priority_polygon'; static const polygonTransactionPriority = 'current_fee_priority_polygon';
static const bitcoinCashTransactionPriority = 'current_fee_priority_bitcoin_cash'; static const bitcoinCashTransactionPriority = 'current_fee_priority_bitcoin_cash';
static const customBitcoinFeeRate = 'custom_electrum_fee_rate'; static const customBitcoinFeeRate = 'custom_electrum_fee_rate';
static const silentPaymentsCardDisplay = 'silentPaymentsCardDisplay';
static const silentPaymentsAlwaysScan = 'silentPaymentsAlwaysScan';
static const shouldShowReceiveWarning = 'should_show_receive_warning'; static const shouldShowReceiveWarning = 'should_show_receive_warning';
static const shouldShowYatPopup = 'should_show_yat_popup'; static const shouldShowYatPopup = 'should_show_yat_popup';
static const shouldShowRepWarning = 'should_show_rep_warning'; static const shouldShowRepWarning = 'should_show_rep_warning';

View file

@ -202,7 +202,7 @@ Future<void> initializeAppConfigs() async {
transactionDescriptions: transactionDescriptions, transactionDescriptions: transactionDescriptions,
secureStorage: secureStorage, secureStorage: secureStorage,
anonpayInvoiceInfo: anonpayInvoiceInfo, anonpayInvoiceInfo: anonpayInvoiceInfo,
initialMigrationVersion: 33, initialMigrationVersion: 34,
); );
} }

View file

@ -3,15 +3,19 @@ import 'dart:async';
import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/sync_status.dart'; import 'package:cw_core/sync_status.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/store/settings_store.dart';
Timer? _checkConnectionTimer; Timer? _checkConnectionTimer;
void startCheckConnectionReaction( void startCheckConnectionReaction(WalletBase wallet, SettingsStore settingsStore,
WalletBase wallet, SettingsStore settingsStore,
{int timeInterval = 5}) { {int timeInterval = 5}) {
_checkConnectionTimer?.cancel(); _checkConnectionTimer?.cancel();
_checkConnectionTimer = _checkConnectionTimer = Timer.periodic(Duration(seconds: timeInterval), (_) async {
Timer.periodic(Duration(seconds: timeInterval), (_) async { if (wallet.type == WalletType.bitcoin && wallet.syncStatus is SyncingSyncStatus) {
return;
}
try { try {
final connectivityResult = await (Connectivity().checkConnectivity()); final connectivityResult = await (Connectivity().checkConnectivity());
@ -20,14 +24,11 @@ void startCheckConnectionReaction(
return; return;
} }
if (wallet.syncStatus is LostConnectionSyncStatus || if (wallet.syncStatus is LostConnectionSyncStatus || wallet.syncStatus is FailedSyncStatus) {
wallet.syncStatus is FailedSyncStatus) { final alive = await settingsStore.getCurrentNode(wallet.type).requestNode();
final alive =
await settingsStore.getCurrentNode(wallet.type).requestNode();
if (alive) { if (alive) {
await wallet.connectToNode( await wallet.connectToNode(node: settingsStore.getCurrentNode(wallet.type));
node: settingsStore.getCurrentNode(wallet.type));
} }
} }
} catch (e) { } catch (e) {

View file

@ -12,12 +12,10 @@ import 'package:wakelock_plus/wakelock_plus.dart';
ReactionDisposer? _onWalletSyncStatusChangeReaction; ReactionDisposer? _onWalletSyncStatusChangeReaction;
void startWalletSyncStatusChangeReaction( void startWalletSyncStatusChangeReaction(
WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo> wallet,
TransactionInfo> wallet,
FiatConversionStore fiatConversionStore) { FiatConversionStore fiatConversionStore) {
_onWalletSyncStatusChangeReaction?.reaction.dispose(); _onWalletSyncStatusChangeReaction?.reaction.dispose();
_onWalletSyncStatusChangeReaction = _onWalletSyncStatusChangeReaction = reaction((_) => wallet.syncStatus, (SyncStatus status) async {
reaction((_) => wallet.syncStatus, (SyncStatus status) async {
try { try {
if (status is ConnectedSyncStatus) { if (status is ConnectedSyncStatus) {
await wallet.startSync(); await wallet.startSync();
@ -32,7 +30,7 @@ void startWalletSyncStatusChangeReaction(
if (status is SyncedSyncStatus || status is FailedSyncStatus) { if (status is SyncedSyncStatus || status is FailedSyncStatus) {
await WakelockPlus.disable(); await WakelockPlus.disable();
} }
} catch(e) { } catch (e) {
print(e.toString()); print(e.toString());
} }
}); });

View file

@ -76,6 +76,7 @@ import 'package:cake_wallet/src/screens/settings/manage_nodes_page.dart';
import 'package:cake_wallet/src/screens/settings/other_settings_page.dart'; import 'package:cake_wallet/src/screens/settings/other_settings_page.dart';
import 'package:cake_wallet/src/screens/settings/privacy_page.dart'; import 'package:cake_wallet/src/screens/settings/privacy_page.dart';
import 'package:cake_wallet/src/screens/settings/security_backup_page.dart'; import 'package:cake_wallet/src/screens/settings/security_backup_page.dart';
import 'package:cake_wallet/src/screens/settings/silent_payments_settings.dart';
import 'package:cake_wallet/src/screens/settings/tor_page.dart'; import 'package:cake_wallet/src/screens/settings/tor_page.dart';
import 'package:cake_wallet/src/screens/settings/trocador_providers_page.dart'; import 'package:cake_wallet/src/screens/settings/trocador_providers_page.dart';
import 'package:cake_wallet/src/screens/setup_2fa/modify_2fa_page.dart'; import 'package:cake_wallet/src/screens/setup_2fa/modify_2fa_page.dart';
@ -366,6 +367,10 @@ Route<dynamic> createRoute(RouteSettings settings) {
param1: settings.arguments as OnAuthenticationFinished, param2: false), param1: settings.arguments as OnAuthenticationFinished, param2: false),
onWillPop: () async => false)); onWillPop: () async => false));
case Routes.silentPaymentsSettings:
return CupertinoPageRoute<void>(
fullscreenDialog: true, builder: (_) => getIt.get<SilentPaymentsSettingsPage>());
case Routes.connectionSync: case Routes.connectionSync:
return CupertinoPageRoute<void>( return CupertinoPageRoute<void>(
fullscreenDialog: true, builder: (_) => getIt.get<ConnectionSyncPage>()); fullscreenDialog: true, builder: (_) => getIt.get<ConnectionSyncPage>());

View file

@ -81,6 +81,7 @@ class Routes {
static const ioniaMoreOptionsPage = '/ionia_more_options_page'; static const ioniaMoreOptionsPage = '/ionia_more_options_page';
static const ioniaCustomRedeemPage = '/ionia_custom_redeem_page'; static const ioniaCustomRedeemPage = '/ionia_custom_redeem_page';
static const webViewPage = '/web_view_page'; static const webViewPage = '/web_view_page';
static const silentPaymentsSettings = '/silent_payments_settings';
static const connectionSync = '/connection_sync_page'; static const connectionSync = '/connection_sync_page';
static const securityBackupPage = '/security_and_backup_page'; static const securityBackupPage = '/security_and_backup_page';
static const privacyPage = '/privacy_page'; static const privacyPage = '/privacy_page';

View file

@ -46,6 +46,7 @@ class ContactPage extends BasePage {
final TextEditingController _nameController; final TextEditingController _nameController;
final TextEditingController _currencyTypeController; final TextEditingController _currencyTypeController;
final TextEditingController _addressController; final TextEditingController _addressController;
bool _isEffectsApplied = false;
@override @override
Widget body(BuildContext context) { Widget body(BuildContext context) {
@ -53,15 +54,7 @@ class ContactPage extends BasePage {
color: Theme.of(context).extension<TransactionTradeTheme>()!.detailsTitlesColor, color: Theme.of(context).extension<TransactionTradeTheme>()!.detailsTitlesColor,
height: 8); height: 8);
reaction((_) => contactViewModel.state, (ExecutionState state) { _setEffects(context);
if (state is FailureState) {
_onContactSavingFailure(context, state.error);
}
if (state is ExecutedSuccessfullyState) {
_onContactSavedSuccessfully(context);
}
});
return Observer( return Observer(
builder: (_) => ScrollableWithBottomSection( builder: (_) => ScrollableWithBottomSection(
@ -177,4 +170,22 @@ class ContactPage extends BasePage {
void _onContactSavedSuccessfully(BuildContext context) => void _onContactSavedSuccessfully(BuildContext context) =>
Navigator.of(context).pop(); Navigator.of(context).pop();
void _setEffects(BuildContext context) {
if (_isEffectsApplied) {
return;
}
_isEffectsApplied = true;
reaction((_) => contactViewModel.state, (ExecutionState state) {
if (state is FailureState) {
_onContactSavingFailure(context, state.error);
}
if (state is ExecutedSuccessfullyState) {
_onContactSavedSuccessfully(context);
}
});
}
} }

View file

@ -4,7 +4,7 @@ import 'package:cake_wallet/entities/preferences_key.dart';
import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/entities/main_actions.dart'; import 'package:cake_wallet/entities/main_actions.dart';
import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/desktop_sidebar_wrapper.dart'; import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/desktop_sidebar_wrapper.dart';
import 'package:cake_wallet/src/screens/dashboard/pages/market_place_page.dart'; import 'package:cake_wallet/src/screens/dashboard/pages/cake_features_page.dart';
import 'package:cake_wallet/src/screens/wallet_connect/widgets/modals/bottom_sheet_listener.dart'; import 'package:cake_wallet/src/screens/wallet_connect/widgets/modals/bottom_sheet_listener.dart';
import 'package:cake_wallet/src/widgets/gradient_background.dart'; import 'package:cake_wallet/src/widgets/gradient_background.dart';
import 'package:cake_wallet/src/widgets/services_updates_widget.dart'; import 'package:cake_wallet/src/widgets/services_updates_widget.dart';
@ -12,7 +12,7 @@ import 'package:cake_wallet/src/widgets/vulnerable_seeds_popup.dart';
import 'package:cake_wallet/themes/extensions/sync_indicator_theme.dart'; import 'package:cake_wallet/themes/extensions/sync_indicator_theme.dart';
import 'package:cake_wallet/utils/device_info.dart'; import 'package:cake_wallet/utils/device_info.dart';
import 'package:cake_wallet/utils/version_comparator.dart'; import 'package:cake_wallet/utils/version_comparator.dart';
import 'package:cake_wallet/view_model/dashboard/market_place_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/cake_features_view_model.dart';
import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/yat_emoji_id.dart'; import 'package:cake_wallet/src/screens/yat_emoji_id.dart';
@ -36,7 +36,7 @@ import 'package:cake_wallet/src/screens/release_notes/release_notes_screen.dart'
import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart'; import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart';
import 'package:cake_wallet/themes/extensions/balance_page_theme.dart'; import 'package:cake_wallet/themes/extensions/balance_page_theme.dart';
class DashboardPage extends StatelessWidget { class DashboardPage extends StatefulWidget {
DashboardPage({ DashboardPage({
required this.bottomSheetService, required this.bottomSheetService,
required this.balancePage, required this.balancePage,
@ -50,43 +50,71 @@ class DashboardPage extends StatelessWidget {
final WalletAddressListViewModel addressListViewModel; final WalletAddressListViewModel addressListViewModel;
@override @override
Widget build(BuildContext context) { State<DashboardPage> createState() => _DashboardPageState();
final screenHeight = MediaQuery.of(context).size.height; }
return Scaffold(
body: Builder(
builder: (_) {
final dashboardPageView = RefreshIndicator(
displacement: screenHeight * 0.1,
onRefresh: () async => await dashboardViewModel.refreshDashboard(),
child: SingleChildScrollView(
physics: AlwaysScrollableScrollPhysics(),
child: Container(
height: screenHeight,
child: _DashboardPageView(
balancePage: balancePage,
bottomSheetService: bottomSheetService,
dashboardViewModel: dashboardViewModel,
addressListViewModel: addressListViewModel,
),
),
),
);
if (DeviceInfo.instance.isDesktop) { class _DashboardPageState extends State<DashboardPage> {
if (responsiveLayoutUtil.screenWidth > @override
ResponsiveLayoutUtilBase.kDesktopMaxDashBoardWidthConstraint) { void initState() {
return getIt.get<DesktopSidebarWrapper>(); super.initState();
} else {
return dashboardPageView; bool isMobileLayout =
} responsiveLayoutUtil.screenWidth < ResponsiveLayoutUtilBase.kMobileThreshold;
} else if (responsiveLayoutUtil.shouldRenderMobileUI) {
return dashboardPageView; reaction((_) => responsiveLayoutUtil.screenWidth, (screenWidth) {
} else { // Check if it was previously in mobile layout, and now changing to desktop
return getIt.get<DesktopSidebarWrapper>(); if (isMobileLayout &&
} screenWidth > ResponsiveLayoutUtilBase.kDesktopMaxDashBoardWidthConstraint) {
}, setState(() {
isMobileLayout = false;
});
}
// Check if it was previously in desktop layout, and now changing to mobile
if (!isMobileLayout &&
screenWidth <= ResponsiveLayoutUtilBase.kDesktopMaxDashBoardWidthConstraint) {
setState(() {
isMobileLayout = true;
});
}
});
}
@override
Widget build(BuildContext context) {
Widget dashboardChild;
final dashboardPageView = RefreshIndicator(
displacement: responsiveLayoutUtil.screenHeight * 0.1,
onRefresh: () async => await widget.dashboardViewModel.refreshDashboard(),
child: SingleChildScrollView(
physics: AlwaysScrollableScrollPhysics(),
child: Container(
height: responsiveLayoutUtil.screenHeight,
child: _DashboardPageView(
balancePage: widget.balancePage,
bottomSheetService: widget.bottomSheetService,
dashboardViewModel: widget.dashboardViewModel,
addressListViewModel: widget.addressListViewModel,
),
),
), ),
); );
if (DeviceInfo.instance.isDesktop) {
if (responsiveLayoutUtil.screenWidth >
ResponsiveLayoutUtilBase.kDesktopMaxDashBoardWidthConstraint) {
dashboardChild = getIt.get<DesktopSidebarWrapper>();
} else {
dashboardChild = dashboardPageView;
}
} else if (responsiveLayoutUtil.shouldRenderMobileUI) {
dashboardChild = dashboardPageView;
} else {
dashboardChild = getIt.get<DesktopSidebarWrapper>();
}
return Scaffold(body: dashboardChild);
} }
} }
@ -302,10 +330,10 @@ class _DashboardPageView extends BasePage {
if (dashboardViewModel.shouldShowMarketPlaceInDashboard) { if (dashboardViewModel.shouldShowMarketPlaceInDashboard) {
pages.add( pages.add(
Semantics( Semantics(
label: S.of(context).market_place, label: 'Cake ${S.of(context).features}',
child: MarketPlacePage( child: CakeFeaturesPage(
dashboardViewModel: dashboardViewModel, dashboardViewModel: dashboardViewModel,
marketPlaceViewModel: getIt.get<MarketPlaceViewModel>(), cakeFeaturesViewModel: getIt.get<CakeFeaturesViewModel>(),
), ),
), ),
); );

View file

@ -1,9 +1,9 @@
import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/entities/main_actions.dart'; import 'package:cake_wallet/entities/main_actions.dart';
import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/desktop_action_button.dart'; import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/desktop_action_button.dart';
import 'package:cake_wallet/src/screens/dashboard/pages/market_place_page.dart'; import 'package:cake_wallet/src/screens/dashboard/pages/cake_features_page.dart';
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
import 'package:cake_wallet/view_model/dashboard/market_place_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/cake_features_view_model.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:flutter_mobx/flutter_mobx.dart';
@ -74,9 +74,9 @@ class DesktopDashboardActions extends StatelessWidget {
], ],
), ),
Expanded( Expanded(
child: MarketPlacePage( child: CakeFeaturesPage(
dashboardViewModel: dashboardViewModel, dashboardViewModel: dashboardViewModel,
marketPlaceViewModel: getIt.get<MarketPlaceViewModel>(), cakeFeaturesViewModel: getIt.get<CakeFeaturesViewModel>(),
), ),
), ),
], ],

View file

@ -30,6 +30,7 @@ class DesktopWalletSelectionDropDown extends StatefulWidget {
class _DesktopWalletSelectionDropDownState extends State<DesktopWalletSelectionDropDown> { class _DesktopWalletSelectionDropDownState extends State<DesktopWalletSelectionDropDown> {
final moneroIcon = Image.asset('assets/images/monero_logo.png', height: 24, width: 24); final moneroIcon = Image.asset('assets/images/monero_logo.png', height: 24, width: 24);
final bitcoinIcon = Image.asset('assets/images/bitcoin.png', height: 24, width: 24); final bitcoinIcon = Image.asset('assets/images/bitcoin.png', height: 24, width: 24);
final tBitcoinIcon = Image.asset('assets/images/tbtc.png', height: 24, width: 24);
final litecoinIcon = Image.asset('assets/images/litecoin_icon.png', height: 24, width: 24); final litecoinIcon = Image.asset('assets/images/litecoin_icon.png', height: 24, width: 24);
final havenIcon = Image.asset('assets/images/haven_logo.png', height: 24, width: 24); final havenIcon = Image.asset('assets/images/haven_logo.png', height: 24, width: 24);
final ethereumIcon = Image.asset('assets/images/eth_icon.png', height: 24, width: 24); final ethereumIcon = Image.asset('assets/images/eth_icon.png', height: 24, width: 24);
@ -68,8 +69,11 @@ class _DesktopWalletSelectionDropDownState extends State<DesktopWalletSelectionD
child: ConstrainedBox( child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: 500), constraints: BoxConstraints(maxWidth: 500),
child: DropDownItemWidget( child: DropDownItemWidget(
title: wallet.name, title: wallet.name,
image: wallet.isEnabled ? _imageFor(type: wallet.type) : nonWalletTypeIcon), image: wallet.isEnabled
? _imageFor(type: wallet.type, isTestnet: wallet.isTestnet)
: nonWalletTypeIcon,
),
), ),
onSelected: () => _onSelectedWallet(wallet), onSelected: () => _onSelectedWallet(wallet),
)) ))
@ -120,16 +124,16 @@ class _DesktopWalletSelectionDropDownState extends State<DesktopWalletSelectionD
WidgetsBinding.instance.addPostFrameCallback((_) async { WidgetsBinding.instance.addPostFrameCallback((_) async {
final confirmed = await showPopUp<bool>( final confirmed = await showPopUp<bool>(
context: context, context: context,
builder: (dialogContext) { builder: (dialogContext) {
return AlertWithTwoActions( return AlertWithTwoActions(
alertTitle: S.of(context).change_wallet_alert_title, alertTitle: S.of(context).change_wallet_alert_title,
alertContent: S.of(context).change_wallet_alert_content(selectedWallet.name), alertContent: S.of(context).change_wallet_alert_content(selectedWallet.name),
leftButtonText: S.of(context).cancel, leftButtonText: S.of(context).cancel,
rightButtonText: S.of(context).change, rightButtonText: S.of(context).change,
actionLeftButton: () => Navigator.of(dialogContext).pop(false), actionLeftButton: () => Navigator.of(dialogContext).pop(false),
actionRightButton: () => Navigator.of(dialogContext).pop(true)); actionRightButton: () => Navigator.of(dialogContext).pop(true));
}) ?? }) ??
false; false;
if (confirmed) { if (confirmed) {
@ -138,9 +142,12 @@ class _DesktopWalletSelectionDropDownState extends State<DesktopWalletSelectionD
}); });
} }
Image _imageFor({required WalletType type}) { Image _imageFor({required WalletType type, bool? isTestnet}) {
switch (type) { switch (type) {
case WalletType.bitcoin: case WalletType.bitcoin:
if (isTestnet == true) {
return tBitcoinIcon;
}
return bitcoinIcon; return bitcoinIcon;
case WalletType.monero: case WalletType.monero:
return moneroIcon; return moneroIcon;
@ -160,7 +167,7 @@ class _DesktopWalletSelectionDropDownState extends State<DesktopWalletSelectionD
return polygonIcon; return polygonIcon;
case WalletType.solana: case WalletType.solana:
return solanaIcon; return solanaIcon;
case WalletType.tron: case WalletType.tron:
return tronIcon; return tronIcon;
default: default:
return nonWalletTypeIcon; return nonWalletTypeIcon;
@ -168,24 +175,25 @@ class _DesktopWalletSelectionDropDownState extends State<DesktopWalletSelectionD
} }
Future<void> _loadWallet(WalletListItem wallet) async { Future<void> _loadWallet(WalletListItem wallet) async {
widget._authService.authenticateAction(context, widget._authService.authenticateAction(
onAuthSuccess: (isAuthenticatedSuccessfully) async { context,
if (!isAuthenticatedSuccessfully) { onAuthSuccess: (isAuthenticatedSuccessfully) async {
return; if (!isAuthenticatedSuccessfully) {
} return;
}
try { try {
if (context.mounted) { if (context.mounted) {
changeProcessText(S.of(context).wallet_list_loading_wallet(wallet.name)); changeProcessText(S.of(context).wallet_list_loading_wallet(wallet.name));
}
await widget.walletListViewModel.loadWallet(wallet);
hideProgressText();
setState(() {});
} catch (e) {
if (context.mounted) {
changeProcessText(S.of(context).wallet_list_failed_to_load(wallet.name, e.toString()));
}
} }
await widget.walletListViewModel.loadWallet(wallet);
hideProgressText();
setState(() {});
} catch (e) {
if (context.mounted) {
changeProcessText(S.of(context).wallet_list_failed_to_load(wallet.name, e.toString()));
}
}
}, },
conditionToDetermineIfToUse2FA: conditionToDetermineIfToUse2FA:
widget.walletListViewModel.shouldRequireTOTP2FAForAccessingWallet, widget.walletListViewModel.shouldRequireTOTP2FAForAccessingWallet,
@ -198,17 +206,16 @@ class _DesktopWalletSelectionDropDownState extends State<DesktopWalletSelectionD
context, context,
route: Routes.newWallet, route: Routes.newWallet,
arguments: widget.walletListViewModel.currentWalletType, arguments: widget.walletListViewModel.currentWalletType,
conditionToDetermineIfToUse2FA: widget conditionToDetermineIfToUse2FA:
.walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets, widget.walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets,
); );
} else { } else {
widget._authService.authenticateAction( widget._authService.authenticateAction(
context, context,
route: Routes.newWalletType, route: Routes.newWalletType,
conditionToDetermineIfToUse2FA: widget conditionToDetermineIfToUse2FA:
.walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets, widget.walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets,
); );
} }
} }

View file

@ -187,6 +187,11 @@ class AddressPage extends BasePage {
} }
reaction((_) => receiveOptionViewModel.selectedReceiveOption, (ReceivePageOption option) { reaction((_) => receiveOptionViewModel.selectedReceiveOption, (ReceivePageOption option) {
if (bitcoin!.isBitcoinReceivePageOption(option)) {
addressListViewModel.setAddressType(bitcoin!.getOptionToType(option));
return;
}
switch (option) { switch (option) {
case ReceivePageOption.anonPayInvoice: case ReceivePageOption.anonPayInvoice:
Navigator.pushNamed( Navigator.pushNamed(

View file

@ -1,15 +1,18 @@
import 'dart:math'; import 'dart:math';
import 'package:auto_size_text/auto_size_text.dart'; import 'package:auto_size_text/auto_size_text.dart';
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cake_wallet/reactions/wallet_connect.dart';
import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/dashboard/pages/nft_listing_page.dart'; import 'package:cake_wallet/src/screens/dashboard/pages/nft_listing_page.dart';
import 'package:cake_wallet/src/screens/dashboard/widgets/home_screen_account_widget.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/home_screen_account_widget.dart';
import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
import 'package:cake_wallet/src/screens/exchange_trade/information_page.dart'; import 'package:cake_wallet/src/screens/exchange_trade/information_page.dart';
import 'package:cake_wallet/src/widgets/dashboard_card_widget.dart'; import 'package:cake_wallet/src/widgets/dashboard_card_widget.dart';
import 'package:cake_wallet/src/widgets/introducing_card.dart'; import 'package:cake_wallet/src/widgets/introducing_card.dart';
import 'package:cake_wallet/src/widgets/standard_switch.dart';
import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/store/settings_store.dart';
import 'package:cake_wallet/themes/extensions/balance_page_theme.dart'; import 'package:cake_wallet/themes/extensions/balance_page_theme.dart';
import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart'; import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart';
@ -21,6 +24,7 @@ import 'package:cake_wallet/view_model/dashboard/nft_view_model.dart';
import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/crypto_currency.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:url_launcher/url_launcher.dart';
class BalancePage extends StatelessWidget { class BalancePage extends StatelessWidget {
BalancePage({ BalancePage({
@ -221,30 +225,136 @@ class CryptoBalanceWidget extends StatelessWidget {
itemBuilder: (__, index) { itemBuilder: (__, index) {
final balance = final balance =
dashboardViewModel.balanceViewModel.formattedBalances.elementAt(index); dashboardViewModel.balanceViewModel.formattedBalances.elementAt(index);
return BalanceRowWidget( return Observer(builder: (_) {
availableBalanceLabel: return BalanceRowWidget(
'${dashboardViewModel.balanceViewModel.availableBalanceLabel}', availableBalanceLabel:
availableBalance: balance.availableBalance, '${dashboardViewModel.balanceViewModel.availableBalanceLabel}',
availableFiatBalance: balance.fiatAvailableBalance, availableBalance: balance.availableBalance,
additionalBalanceLabel: availableFiatBalance: balance.fiatAvailableBalance,
'${dashboardViewModel.balanceViewModel.additionalBalanceLabel}', additionalBalanceLabel:
additionalBalance: balance.additionalBalance, '${dashboardViewModel.balanceViewModel.additionalBalanceLabel}',
additionalFiatBalance: balance.fiatAdditionalBalance, additionalBalance: balance.additionalBalance,
frozenBalance: balance.frozenBalance, additionalFiatBalance: balance.fiatAdditionalBalance,
frozenFiatBalance: balance.fiatFrozenBalance, frozenBalance: balance.frozenBalance,
currency: balance.asset, frozenFiatBalance: balance.fiatFrozenBalance,
hasAdditionalBalance: currency: balance.asset,
dashboardViewModel.balanceViewModel.hasAdditionalBalance, hasAdditionalBalance:
); dashboardViewModel.balanceViewModel.hasAdditionalBalance,
isTestnet: dashboardViewModel.isTestnet,
);
});
}, },
); );
}, },
) ),
Observer(builder: (context) {
return Column(
children: [
if (dashboardViewModel.showSilentPaymentsCard) ...[
SizedBox(height: 10),
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 8),
child: DashBoardRoundedCardWidget(
customBorder: 30,
title: S.of(context).silent_payments,
subTitle: S.of(context).enable_silent_payments_scanning,
hint: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => launchUrl(
Uri.parse(
"https://guides.cakewallet.com/docs/cryptos/bitcoin/#silent-payments"),
mode: LaunchMode.externalApplication,
),
child: Row(
children: [
Text(
S.of(context).what_is_silent_payments,
style: TextStyle(
fontSize: 12,
fontFamily: 'Lato',
fontWeight: FontWeight.w400,
color: Theme.of(context)
.extension<BalancePageTheme>()!
.labelTextColor,
height: 1,
),
softWrap: true,
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: Icon(Icons.help_outline,
size: 16,
color: Theme.of(context)
.extension<BalancePageTheme>()!
.labelTextColor),
)
],
),
),
Observer(
builder: (_) => StandardSwitch(
value: dashboardViewModel.silentPaymentsScanningActive,
onTaped: () => _toggleSilentPaymentsScanning(context),
),
)
],
),
],
),
onTap: () => _toggleSilentPaymentsScanning(context),
icon: Icon(
Icons.lock,
color:
Theme.of(context).extension<DashboardPageTheme>()!.pageTitleTextColor,
size: 50,
),
),
),
]
],
);
}),
], ],
), ),
), ),
); );
} }
Future<void> _toggleSilentPaymentsScanning(BuildContext context) async {
final isSilentPaymentsScanningActive = dashboardViewModel.silentPaymentsScanningActive;
final newValue = !isSilentPaymentsScanningActive;
dashboardViewModel.silentPaymentsScanningActive = newValue;
final needsToSwitch = !isSilentPaymentsScanningActive &&
await bitcoin!.getNodeIsElectrsSPEnabled(dashboardViewModel.wallet) == false;
if (needsToSwitch) {
return showPopUp<void>(
context: context,
builder: (BuildContext context) => AlertWithTwoActions(
alertTitle: S.of(context).change_current_node_title,
alertContent: S.of(context).confirm_silent_payments_switch_node,
rightButtonText: S.of(context).ok,
leftButtonText: S.of(context).cancel,
actionRightButton: () {
dashboardViewModel.setSilentPaymentsScanning(newValue);
Navigator.of(context).pop();
},
actionLeftButton: () {
dashboardViewModel.silentPaymentsScanningActive = isSilentPaymentsScanningActive;
Navigator.of(context).pop();
},
));
}
return dashboardViewModel.setSilentPaymentsScanning(newValue);
}
} }
class BalanceRowWidget extends StatelessWidget { class BalanceRowWidget extends StatelessWidget {
@ -259,6 +369,7 @@ class BalanceRowWidget extends StatelessWidget {
required this.frozenFiatBalance, required this.frozenFiatBalance,
required this.currency, required this.currency,
required this.hasAdditionalBalance, required this.hasAdditionalBalance,
required this.isTestnet,
super.key, super.key,
}); });
@ -272,6 +383,7 @@ class BalanceRowWidget extends StatelessWidget {
final String frozenFiatBalance; final String frozenFiatBalance;
final CryptoCurrency currency; final CryptoCurrency currency;
final bool hasAdditionalBalance; final bool hasAdditionalBalance;
final bool isTestnet;
// void _showBalanceDescription(BuildContext context) { // void _showBalanceDescription(BuildContext context) {
// showPopUp<void>( // showPopUp<void>(
@ -346,14 +458,24 @@ class BalanceRowWidget extends StatelessWidget {
maxLines: 1, maxLines: 1,
textAlign: TextAlign.start), textAlign: TextAlign.start),
SizedBox(height: 6), SizedBox(height: 6),
Text('${availableFiatBalance}', if (isTestnet)
textAlign: TextAlign.center, Text(S.current.testnet_coins_no_value,
style: TextStyle( textAlign: TextAlign.center,
fontSize: 16, style: TextStyle(
fontFamily: 'Lato', fontSize: 14,
fontWeight: FontWeight.w500, fontFamily: 'Lato',
color: Theme.of(context).extension<BalancePageTheme>()!.textColor, fontWeight: FontWeight.w400,
height: 1)), color: Theme.of(context).extension<BalancePageTheme>()!.textColor,
height: 1)),
if (!isTestnet)
Text('${availableFiatBalance}',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16,
fontFamily: 'Lato',
fontWeight: FontWeight.w500,
color: Theme.of(context).extension<BalancePageTheme>()!.textColor,
height: 1)),
], ],
), ),
), ),
@ -362,27 +484,23 @@ class BalanceRowWidget extends StatelessWidget {
child: Center( child: Center(
child: Column( child: Column(
children: [ children: [
Container( CakeImageWidget(
clipBehavior: Clip.antiAlias, imageUrl: currency.iconPath,
decoration: BoxDecoration(shape: BoxShape.circle), height: 40,
child: CakeImageWidget( width: 40,
imageUrl: currency.iconPath, displayOnError: Container(
height: 40, height: 30.0,
width: 40, width: 30.0,
displayOnError: Container( child: Center(
height: 30.0, child: Text(
width: 30.0, currency.title.substring(0, min(currency.title.length, 2)),
child: Center( style: TextStyle(fontSize: 11),
child: Text(
currency.title.substring(0, min(currency.title.length, 2)),
style: TextStyle(fontSize: 11),
),
),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.grey.shade400,
), ),
), ),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.grey.shade400,
),
), ),
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
@ -449,17 +567,18 @@ class BalanceRowWidget extends StatelessWidget {
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
SizedBox(height: 4), SizedBox(height: 4),
Text( if (!isTestnet)
frozenFiatBalance, Text(
textAlign: TextAlign.center, frozenFiatBalance,
style: TextStyle( textAlign: TextAlign.center,
fontSize: 12, style: TextStyle(
fontFamily: 'Lato', fontSize: 12,
fontWeight: FontWeight.w400, fontFamily: 'Lato',
color: Theme.of(context).extension<BalancePageTheme>()!.textColor, fontWeight: FontWeight.w400,
height: 1, color: Theme.of(context).extension<BalancePageTheme>()!.textColor,
height: 1,
),
), ),
),
], ],
), ),
), ),
@ -493,17 +612,18 @@ class BalanceRowWidget extends StatelessWidget {
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
SizedBox(height: 4), SizedBox(height: 4),
Text( if (!isTestnet)
'${additionalFiatBalance}', Text(
textAlign: TextAlign.center, '${additionalFiatBalance}',
style: TextStyle( textAlign: TextAlign.center,
fontSize: 12, style: TextStyle(
fontFamily: 'Lato', fontSize: 12,
fontWeight: FontWeight.w400, fontFamily: 'Lato',
color: Theme.of(context).extension<BalancePageTheme>()!.textColor, fontWeight: FontWeight.w400,
height: 1, color: Theme.of(context).extension<BalancePageTheme>()!.textColor,
height: 1,
),
), ),
),
], ],
), ),
], ],

View file

@ -1,23 +1,20 @@
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/src/widgets/dashboard_card_widget.dart'; import 'package:cake_wallet/src/widgets/dashboard_card_widget.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
import 'package:cake_wallet/view_model/dashboard/market_place_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/cake_features_view_model.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/generated/i18n.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart'; import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart';
import 'package:flutter_svg/flutter_svg.dart';
class MarketPlacePage extends StatelessWidget { class CakeFeaturesPage extends StatelessWidget {
MarketPlacePage({ CakeFeaturesPage({
required this.dashboardViewModel, required this.dashboardViewModel,
required this.marketPlaceViewModel, required this.cakeFeaturesViewModel,
}); });
final DashboardViewModel dashboardViewModel; final DashboardViewModel dashboardViewModel;
final MarketPlaceViewModel marketPlaceViewModel; final CakeFeaturesViewModel cakeFeaturesViewModel;
final _scrollController = ScrollController(); final _scrollController = ScrollController();
@override @override
@ -37,7 +34,7 @@ class MarketPlacePage extends StatelessWidget {
children: [ children: [
SizedBox(height: 50), SizedBox(height: 50),
Text( Text(
S.of(context).market_place, 'Cake ${S.of(context).features}',
style: TextStyle( style: TextStyle(
fontSize: 24, fontSize: 24,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
@ -59,15 +56,21 @@ class MarketPlacePage extends StatelessWidget {
// ), // ),
SizedBox(height: 20), SizedBox(height: 20),
DashBoardRoundedCardWidget( DashBoardRoundedCardWidget(
onTap: () => _launchUrl("buy.cakepay.com"),
title: S.of(context).cake_pay_web_cards_title, title: S.of(context).cake_pay_web_cards_title,
subTitle: S.of(context).cake_pay_web_cards_subtitle, subTitle: S.of(context).cake_pay_web_cards_subtitle,
onTap: () => _launchMarketPlaceUrl("buy.cakepay.com"), svgPicture: SvgPicture.asset(
'assets/images/cards.svg',
height: 125,
width: 125,
fit: BoxFit.cover,
),
), ),
const SizedBox(height: 20), SizedBox(height: 10),
DashBoardRoundedCardWidget( DashBoardRoundedCardWidget(
title: "NanoGPT", title: "NanoGPT",
subTitle: S.of(context).nanogpt_subtitle, subTitle: S.of(context).nanogpt_subtitle,
onTap: () => _launchMarketPlaceUrl("cake.nano-gpt.com"), onTap: () => _launchUrl("cake.nano-gpt.com"),
), ),
], ],
), ),
@ -79,41 +82,12 @@ class MarketPlacePage extends StatelessWidget {
); );
} }
void _launchMarketPlaceUrl(String url) async { void _launchUrl(String url) {
try { try {
launchUrl( launchUrl(
Uri.https(url), Uri.https(url),
mode: LaunchMode.externalApplication, mode: LaunchMode.externalApplication,
); );
} catch (e) { } catch (_) {}
print(e);
}
}
// TODO: Remove ionia flow/files if we will discard it
void _navigatorToGiftCardsPage(BuildContext context) {
final walletType = dashboardViewModel.type;
switch (walletType) {
case WalletType.haven:
showPopUp<void>(
context: context,
builder: (BuildContext context) {
return AlertWithOneAction(
alertTitle: S.of(context).error,
alertContent: S.of(context).gift_cards_unavailable,
buttonText: S.of(context).ok,
buttonAction: () => Navigator.of(context).pop());
});
break;
default:
marketPlaceViewModel.isIoniaUserAuthenticated().then((value) {
if (value) {
Navigator.pushNamed(context, Routes.ioniaManageCardsPage);
return;
}
Navigator.of(context).pushNamed(Routes.ioniaWelcomePage);
});
}
} }
} }

View file

@ -1,3 +1,4 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/widgets/setting_action_button.dart'; import 'package:cake_wallet/src/widgets/setting_action_button.dart';
import 'package:cake_wallet/src/widgets/setting_actions.dart'; import 'package:cake_wallet/src/widgets/setting_actions.dart';
import 'package:cake_wallet/themes/extensions/menu_theme.dart'; import 'package:cake_wallet/themes/extensions/menu_theme.dart';
@ -180,6 +181,11 @@ class MenuWidgetState extends State<MenuWidget> {
final item = SettingActions.all[index]; final item = SettingActions.all[index];
if (!widget.dashboardViewModel.hasSilentPayments &&
item.name(context) == S.of(context).silent_payments_settings) {
return Container();
}
final isLastTile = index == itemCount - 1; final isLastTile = index == itemCount - 1;
return SettingActionButton( return SettingActionButton(

View file

@ -10,8 +10,7 @@ import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/generated/i18n.dart';
class PresentReceiveOptionPicker extends StatelessWidget { class PresentReceiveOptionPicker extends StatelessWidget {
PresentReceiveOptionPicker( PresentReceiveOptionPicker({required this.receiveOptionViewModel, required this.color});
{required this.receiveOptionViewModel, required this.color});
final ReceiveOptionViewModel receiveOptionViewModel; final ReceiveOptionViewModel receiveOptionViewModel;
final Color color; final Color color;
@ -43,17 +42,17 @@ class PresentReceiveOptionPicker extends StatelessWidget {
Text( Text(
S.current.receive, S.current.receive,
style: TextStyle( style: TextStyle(
fontSize: 18.0, fontSize: 18.0, fontWeight: FontWeight.bold, fontFamily: 'Lato', color: color),
fontWeight: FontWeight.bold,
fontFamily: 'Lato',
color: color),
), ),
Observer( Observer(
builder: (_) => Text(receiveOptionViewModel.selectedReceiveOption.toString(), builder: (_) => Text(
style: TextStyle( receiveOptionViewModel.selectedReceiveOption
fontSize: 10.0, .toString()
fontWeight: FontWeight.w500, .replaceAll(RegExp(r'silent payments', caseSensitive: false),
color: color))) S.current.silent_payments)
.replaceAll(
RegExp(r'default', caseSensitive: false), S.current.string_default),
style: TextStyle(fontSize: 10.0, fontWeight: FontWeight.w500, color: color)))
], ],
), ),
SizedBox(width: 5), SizedBox(width: 5),
@ -73,65 +72,75 @@ class PresentReceiveOptionPicker extends StatelessWidget {
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
body: Stack( body: Stack(
alignment: AlignmentDirectional.center, alignment: AlignmentDirectional.center,
children:[ AlertBackground( children: [
child: Column( AlertBackground(
mainAxisSize: MainAxisSize.min, child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min,
children: [ mainAxisAlignment: MainAxisAlignment.center,
Spacer(), children: [
Container( Spacer(),
margin: EdgeInsets.symmetric(horizontal: 24), Container(
decoration: BoxDecoration( margin: EdgeInsets.symmetric(horizontal: 24),
borderRadius: BorderRadius.circular(30), decoration: BoxDecoration(
color: Theme.of(context).colorScheme.background, borderRadius: BorderRadius.circular(30),
), color: Theme.of(context).colorScheme.background,
child: Padding( ),
padding: const EdgeInsets.only(top: 24, bottom: 24), child: Padding(
child: (ListView.separated( padding: const EdgeInsets.only(top: 24, bottom: 24),
padding: EdgeInsets.zero, child: (ListView.separated(
shrinkWrap: true, padding: EdgeInsets.zero,
itemCount: receiveOptionViewModel.options.length, shrinkWrap: true,
itemBuilder: (_, index) { itemCount: receiveOptionViewModel.options.length,
final option = receiveOptionViewModel.options[index]; itemBuilder: (_, index) {
return InkWell( final option = receiveOptionViewModel.options[index];
onTap: () { return InkWell(
Navigator.pop(popUpContext); onTap: () {
Navigator.pop(popUpContext);
receiveOptionViewModel.selectReceiveOption(option); receiveOptionViewModel.selectReceiveOption(option);
}, },
child: Padding( child: Padding(
padding: const EdgeInsets.only(left: 24, right: 24), padding: const EdgeInsets.only(left: 24, right: 24),
child: Observer(builder: (_) { child: Observer(builder: (_) {
final value = receiveOptionViewModel.selectedReceiveOption; final value = receiveOptionViewModel.selectedReceiveOption;
return Row( return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text(option.toString(), Text(
textAlign: TextAlign.left, option
style: textSmall( .toString()
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor, .replaceAll(
).copyWith( RegExp(r'silent payments', caseSensitive: false),
fontWeight: S.current.silent_payments)
value == option ? FontWeight.w800 : FontWeight.w500, .replaceAll(RegExp(r'default', caseSensitive: false),
)), S.current.string_default),
RoundedCheckbox( textAlign: TextAlign.left,
value: value == option, style: textSmall(
) color: Theme.of(context)
], .extension<CakeTextTheme>()!
); .titleColor,
}), ).copyWith(
), fontWeight:
); value == option ? FontWeight.w800 : FontWeight.w500,
}, )),
separatorBuilder: (_, index) => SizedBox(height: 30), RoundedCheckbox(
)), value: value == option,
)
],
);
}),
),
);
},
separatorBuilder: (_, index) => SizedBox(height: 30),
)),
),
), ),
), Spacer()
Spacer() ],
], ),
), ),
),
AlertCloseButton(onTap: () => Navigator.of(popUpContext).pop(), bottom: 40) AlertCloseButton(onTap: () => Navigator.of(popUpContext).pop(), bottom: 40)
], ],
), ),

View file

@ -67,8 +67,7 @@ class ReceivePage extends BasePage {
@override @override
Widget Function(BuildContext, Widget) get rootWrapper => Widget Function(BuildContext, Widget) get rootWrapper =>
(BuildContext context, Widget scaffold) => (BuildContext context, Widget scaffold) => GradientBackground(scaffold: scaffold);
GradientBackground(scaffold: scaffold);
@override @override
Widget trailing(BuildContext context) { Widget trailing(BuildContext context) {
@ -99,115 +98,144 @@ class ReceivePage extends BasePage {
@override @override
Widget body(BuildContext context) { Widget body(BuildContext context) {
return KeyboardActions( return KeyboardActions(
config: KeyboardActionsConfig( config: KeyboardActionsConfig(
keyboardActionsPlatform: KeyboardActionsPlatform.IOS, keyboardActionsPlatform: KeyboardActionsPlatform.IOS,
keyboardBarColor: Theme.of(context).extension<KeyboardTheme>()!.keyboardBarColor, keyboardBarColor: Theme.of(context).extension<KeyboardTheme>()!.keyboardBarColor,
nextFocus: false, nextFocus: false,
actions: [ actions: [
KeyboardActionsItem( KeyboardActionsItem(
focusNode: _cryptoAmountFocus, focusNode: _cryptoAmountFocus,
toolbarButtons: [(_) => KeyboardDoneButton()], toolbarButtons: [(_) => KeyboardDoneButton()],
) )
]), ]),
child: SingleChildScrollView( child: SingleChildScrollView(
child: Column( child: Column(
children: <Widget>[ children: <Widget>[
Padding( Padding(
padding: EdgeInsets.fromLTRB(24, 50, 24, 24), padding: EdgeInsets.fromLTRB(24, 50, 24, 24),
child: QRWidget( child: QRWidget(
addressListViewModel: addressListViewModel, addressListViewModel: addressListViewModel,
formKey: _formKey, formKey: _formKey,
heroTag: _heroTag, heroTag: _heroTag,
amountTextFieldFocusNode: _cryptoAmountFocus, amountTextFieldFocusNode: _cryptoAmountFocus,
amountController: _amountController, amountController: _amountController,
isLight: currentTheme.type == ThemeType.light), isLight: currentTheme.type == ThemeType.light),
),
Observer(
builder: (_) => ListView.separated(
padding: EdgeInsets.all(0),
separatorBuilder: (context, _) => const HorizontalSectionDivider(),
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemCount: addressListViewModel.items.length,
itemBuilder: (context, index) {
final item = addressListViewModel.items[index];
Widget cell = Container();
if (item is WalletAccountListHeader) {
cell = HeaderTile(
showTrailingButton: true,
walletAddressListViewModel: addressListViewModel,
trailingButtonTap: () async {
if (addressListViewModel.type == WalletType.monero ||
addressListViewModel.type == WalletType.haven) {
await showPopUp<void>(
context: context,
builder: (_) => getIt.get<MoneroAccountListPage>());
} else {
await showPopUp<void>(
context: context,
builder: (_) => getIt.get<NanoAccountListPage>());
}
},
title: S.of(context).accounts,
trailingIcon: Icon(
Icons.arrow_forward_ios,
size: 14,
color: Theme.of(context).extension<ReceivePageTheme>()!.iconsColor,
));
}
if (item is WalletAddressListHeader) {
cell = HeaderTile(
title: S.of(context).addresses,
walletAddressListViewModel: addressListViewModel,
showTrailingButton: !addressListViewModel.isAutoGenerateSubaddressEnabled,
showSearchButton: true,
trailingButtonTap: () =>
Navigator.of(context).pushNamed(Routes.newSubaddress),
trailingIcon: Icon(
Icons.add,
size: 20,
color: Theme.of(context)
.extension<ReceivePageTheme>()!
.iconsColor,
));
}
if (item is WalletAddressListItem) {
cell = Observer(builder: (_) {
final isCurrent =
item.address == addressListViewModel.address.address;
final backgroundColor = isCurrent
? Theme.of(context).extension<ReceivePageTheme>()!.currentTileBackgroundColor
: Theme.of(context).extension<ReceivePageTheme>()!.tilesBackgroundColor;
final textColor = isCurrent
? Theme.of(context).extension<ReceivePageTheme>()!.currentTileTextColor
: Theme.of(context).extension<ReceivePageTheme>()!.tilesTextColor;
return AddressCell.fromItem(item,
isCurrent: isCurrent,
hasBalance: addressListViewModel.isElectrumWallet,
backgroundColor: backgroundColor,
textColor: textColor,
onTap: (_) => addressListViewModel.setAddress(item),
onEdit: () => Navigator.of(context)
.pushNamed(Routes.newSubaddress, arguments: item));
});
}
return index != 0
? cell
: ClipRRect(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(30),
topRight: Radius.circular(30)),
child: cell,
);
})),
],
), ),
)); Observer(
builder: (_) => ListView.separated(
padding: EdgeInsets.all(0),
separatorBuilder: (context, _) => const HorizontalSectionDivider(),
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemCount: addressListViewModel.items.length,
itemBuilder: (context, index) {
final item = addressListViewModel.items[index];
Widget cell = Container();
if (item is WalletAccountListHeader) {
cell = HeaderTile(
showTrailingButton: true,
walletAddressListViewModel: addressListViewModel,
trailingButtonTap: () async {
if (addressListViewModel.type == WalletType.monero ||
addressListViewModel.type == WalletType.haven) {
await showPopUp<void>(
context: context,
builder: (_) => getIt.get<MoneroAccountListPage>());
} else {
await showPopUp<void>(
context: context,
builder: (_) => getIt.get<NanoAccountListPage>());
}
},
title: S.of(context).accounts,
trailingIcon: Icon(
Icons.arrow_forward_ios,
size: 14,
color: Theme.of(context).extension<ReceivePageTheme>()!.iconsColor,
));
}
if (item is WalletAddressListHeader) {
final hasTitle = item.title != null;
cell = HeaderTile(
title: hasTitle ? item.title! : S.of(context).addresses,
walletAddressListViewModel: addressListViewModel,
showTrailingButton:
!addressListViewModel.isAutoGenerateSubaddressEnabled && !hasTitle,
showSearchButton: true,
trailingButtonTap: () =>
Navigator.of(context).pushNamed(Routes.newSubaddress),
trailingIcon: hasTitle
? null
: Icon(
Icons.add,
size: 20,
color:
Theme.of(context).extension<ReceivePageTheme>()!.iconsColor,
),
);
}
if (item is WalletAddressListItem) {
cell = Observer(builder: (_) {
final isCurrent = item.address == addressListViewModel.address.address;
final backgroundColor = isCurrent
? Theme.of(context)
.extension<ReceivePageTheme>()!
.currentTileBackgroundColor
: Theme.of(context)
.extension<ReceivePageTheme>()!
.tilesBackgroundColor;
final textColor = isCurrent
? Theme.of(context)
.extension<ReceivePageTheme>()!
.currentTileTextColor
: Theme.of(context).extension<ReceivePageTheme>()!.tilesTextColor;
return AddressCell.fromItem(
item,
isCurrent: isCurrent,
hasBalance: addressListViewModel.isElectrumWallet,
backgroundColor: backgroundColor,
textColor: textColor,
onTap: item.isOneTimeReceiveAddress == true
? null
: (_) => addressListViewModel.setAddress(item),
onEdit: item.isOneTimeReceiveAddress == true || item.isPrimary
? null
: () => Navigator.of(context)
.pushNamed(Routes.newSubaddress, arguments: item),
onDelete: !addressListViewModel.isSilentPayments || item.isPrimary
? null
: () => addressListViewModel.deleteAddress(item),
);
});
}
return index != 0
? cell
: ClipRRect(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(30), topRight: Radius.circular(30)),
child: cell,
);
})),
Padding(
padding: EdgeInsets.fromLTRB(24, 24, 24, 32),
child: Text(
addressListViewModel.isSilentPayments
? S.of(context).silent_payments_disclaimer
: S.of(context).electrum_address_disclaimer,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 15,
color: Theme.of(context).extension<BalancePageTheme>()!.labelTextColor)),
),
],
),
));
} }
} }

View file

@ -15,18 +15,22 @@ class AddressCell extends StatelessWidget {
required this.textColor, required this.textColor,
this.onTap, this.onTap,
this.onEdit, this.onEdit,
this.onDelete,
this.txCount, this.txCount,
this.balance, this.balance,
this.isChange = false, this.isChange = false,
this.hasBalance = false}); this.hasBalance = false});
factory AddressCell.fromItem(WalletAddressListItem item, factory AddressCell.fromItem(
{required bool isCurrent, WalletAddressListItem item, {
required Color backgroundColor, required bool isCurrent,
required Color textColor, required Color backgroundColor,
Function(String)? onTap, required Color textColor,
bool hasBalance = false, Function(String)? onTap,
Function()? onEdit}) => bool hasBalance = false,
Function()? onEdit,
Function()? onDelete,
}) =>
AddressCell( AddressCell(
address: item.address, address: item.address,
name: item.name ?? '', name: item.name ?? '',
@ -36,6 +40,7 @@ class AddressCell extends StatelessWidget {
textColor: textColor, textColor: textColor,
onTap: onTap, onTap: onTap,
onEdit: onEdit, onEdit: onEdit,
onDelete: onDelete,
txCount: item.txCount, txCount: item.txCount,
balance: item.balance, balance: item.balance,
isChange: item.isChange, isChange: item.isChange,
@ -49,6 +54,7 @@ class AddressCell extends StatelessWidget {
final Color textColor; final Color textColor;
final Function(String)? onTap; final Function(String)? onTap;
final Function()? onEdit; final Function()? onEdit;
final Function()? onDelete;
final int? txCount; final int? txCount;
final String? balance; final String? balance;
final bool isChange; final bool isChange;
@ -64,7 +70,8 @@ class AddressCell extends StatelessWidget {
} else { } else {
return formatIfCashAddr.substring(0, addressPreviewLength) + return formatIfCashAddr.substring(0, addressPreviewLength) +
'...' + '...' +
formatIfCashAddr.substring(formatIfCashAddr.length - addressPreviewLength, formatIfCashAddr.length); formatIfCashAddr.substring(
formatIfCashAddr.length - addressPreviewLength, formatIfCashAddr.length);
} }
} }
@ -139,7 +146,7 @@ class AddressCell extends StatelessWidget {
mainAxisSize: MainAxisSize.max, mainAxisSize: MainAxisSize.max,
children: [ children: [
Text( Text(
'Balance: $balance', '${S.of(context).balance}: $balance',
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
@ -180,7 +187,7 @@ class AddressCell extends StatelessWidget {
ActionPane _actionPane(BuildContext context) => ActionPane( ActionPane _actionPane(BuildContext context) => ActionPane(
motion: const ScrollMotion(), motion: const ScrollMotion(),
extentRatio: 0.3, extentRatio: onDelete != null ? 0.4 : 0.3,
children: [ children: [
SlidableAction( SlidableAction(
onPressed: (_) => onEdit?.call(), onPressed: (_) => onEdit?.call(),
@ -189,6 +196,14 @@ class AddressCell extends StatelessWidget {
icon: Icons.edit, icon: Icons.edit,
label: S.of(context).edit, label: S.of(context).edit,
), ),
if (onDelete != null)
SlidableAction(
onPressed: (_) => onDelete!.call(),
backgroundColor: Colors.red,
foregroundColor: Colors.white,
icon: Icons.delete,
label: S.of(context).delete,
),
], ],
); );
} }

View file

@ -1,3 +1,7 @@
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/main.dart';
import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:cake_wallet/view_model/rescan_view_model.dart'; import 'package:cake_wallet/view_model/rescan_view_model.dart';
@ -11,7 +15,8 @@ class RescanPage extends BasePage {
: _blockchainHeightWidgetKey = GlobalKey<BlockchainHeightState>(); : _blockchainHeightWidgetKey = GlobalKey<BlockchainHeightState>();
@override @override
String get title => S.current.rescan; String get title =>
_rescanViewModel.isSilentPaymentsScan ? S.current.silent_payments_scanning : S.current.rescan;
final GlobalKey<BlockchainHeightState> _blockchainHeightWidgetKey; final GlobalKey<BlockchainHeightState> _blockchainHeightWidgetKey;
final RescanViewModel _rescanViewModel; final RescanViewModel _rescanViewModel;
@ -19,20 +24,28 @@ class RescanPage extends BasePage {
Widget body(BuildContext context) { Widget body(BuildContext context) {
return Padding( return Padding(
padding: EdgeInsets.only(left: 24, right: 24, bottom: 24), padding: EdgeInsets.only(left: 24, right: 24, bottom: 24),
child: child: Column(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
Column(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Observer(
BlockchainHeightWidget(key: _blockchainHeightWidgetKey, builder: (_) => BlockchainHeightWidget(
onHeightOrDateEntered: (value) => key: _blockchainHeightWidgetKey,
_rescanViewModel.isButtonEnabled = value), onHeightOrDateEntered: (value) => _rescanViewModel.isButtonEnabled = value,
isSilentPaymentsScan: _rescanViewModel.isSilentPaymentsScan,
doSingleScan: _rescanViewModel.doSingleScan,
toggleSingleScan: () =>
_rescanViewModel.doSingleScan = !_rescanViewModel.doSingleScan,
)),
Observer( Observer(
builder: (_) => LoadingPrimaryButton( builder: (_) => LoadingPrimaryButton(
isLoading: isLoading: _rescanViewModel.state == RescanWalletState.rescaning,
_rescanViewModel.state == RescanWalletState.rescaning,
text: S.of(context).rescan, text: S.of(context).rescan,
onPressed: () async { onPressed: () async {
await _rescanViewModel.rescanCurrentWallet( if (_rescanViewModel.isSilentPaymentsScan) {
restoreHeight: return _toggleSilentPaymentsScanning(context);
_blockchainHeightWidgetKey.currentState!.height); }
_rescanViewModel.rescanCurrentWallet(
restoreHeight: _blockchainHeightWidgetKey.currentState!.height);
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
color: Theme.of(context).primaryColor, color: Theme.of(context).primaryColor,
@ -42,4 +55,32 @@ class RescanPage extends BasePage {
]), ]),
); );
} }
Future<void> _toggleSilentPaymentsScanning(BuildContext context) async {
final height = _blockchainHeightWidgetKey.currentState!.height;
Navigator.of(context).pop();
final needsToSwitch =
await bitcoin!.getNodeIsElectrsSPEnabled(_rescanViewModel.wallet) == false;
if (needsToSwitch) {
return showPopUp<void>(
context: navigatorKey.currentState!.context,
builder: (BuildContext _dialogContext) => AlertWithTwoActions(
alertTitle: S.of(_dialogContext).change_current_node_title,
alertContent: S.of(_dialogContext).confirm_silent_payments_switch_node,
rightButtonText: S.of(_dialogContext).ok,
leftButtonText: S.of(_dialogContext).cancel,
actionRightButton: () async {
Navigator.of(_dialogContext).pop();
_rescanViewModel.rescanCurrentWallet(restoreHeight: height);
},
actionLeftButton: () => Navigator.of(_dialogContext).pop(),
));
}
_rescanViewModel.rescanCurrentWallet(restoreHeight: height);
}
} }

View file

@ -1,3 +1,4 @@
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/core/auth_service.dart'; import 'package:cake_wallet/core/auth_service.dart';
import 'package:cake_wallet/entities/contact_record.dart'; import 'package:cake_wallet/entities/contact_record.dart';
import 'package:cake_wallet/core/execution_state.dart'; import 'package:cake_wallet/core/execution_state.dart';
@ -420,6 +421,10 @@ class SendPage extends BasePage {
return; return;
} }
if (sendViewModel.isElectrumWallet) {
bitcoin!.updateFeeRates(sendViewModel.wallet);
}
reaction((_) => sendViewModel.state, (ExecutionState state) { reaction((_) => sendViewModel.state, (ExecutionState state) {
if (dialogContext != null && dialogContext?.mounted == true) { if (dialogContext != null && dialogContext?.mounted == true) {
Navigator.of(dialogContext!).pop(); Navigator.of(dialogContext!).pop();

View file

@ -39,7 +39,9 @@ class ConnectionSyncPage extends BasePage {
), ),
if (dashboardViewModel.hasRescan) ...[ if (dashboardViewModel.hasRescan) ...[
SettingsCellWithArrow( SettingsCellWithArrow(
title: S.current.rescan, title: dashboardViewModel.hasSilentPayments
? S.current.silent_payments_scanning
: S.current.rescan,
handler: (context) => Navigator.of(context).pushNamed(Routes.rescan), handler: (context) => Navigator.of(context).pushNamed(Routes.rescan),
), ),
if (DeviceInfo.instance.isMobile && FeatureFlag.isBackgroundSyncEnabled) ...[ if (DeviceInfo.instance.isMobile && FeatureFlag.isBackgroundSyncEnabled) ...[

View file

@ -9,14 +9,13 @@ import 'package:cake_wallet/src/screens/pin_code/pin_code_widget.dart';
import 'package:cake_wallet/src/screens/settings/widgets/settings_cell_with_arrow.dart'; import 'package:cake_wallet/src/screens/settings/widgets/settings_cell_with_arrow.dart';
import 'package:cake_wallet/src/screens/settings/widgets/settings_picker_cell.dart'; import 'package:cake_wallet/src/screens/settings/widgets/settings_picker_cell.dart';
import 'package:cake_wallet/src/screens/settings/widgets/settings_switcher_cell.dart'; import 'package:cake_wallet/src/screens/settings/widgets/settings_switcher_cell.dart';
import 'package:cake_wallet/src/widgets/standard_list.dart';
import 'package:cake_wallet/utils/device_info.dart'; import 'package:cake_wallet/utils/device_info.dart';
import 'package:cake_wallet/view_model/settings/security_settings_view_model.dart'; import 'package:cake_wallet/view_model/settings/security_settings_view_model.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:flutter_mobx/flutter_mobx.dart';
class SecurityBackupPage extends BasePage { class SecurityBackupPage extends BasePage {
SecurityBackupPage(this._securitySettingsViewModel, this._authService); SecurityBackupPage(this._securitySettingsViewModel, this._authService, [this._isHardwareWallet = false]);
final AuthService _authService; final AuthService _authService;
@ -25,20 +24,23 @@ class SecurityBackupPage extends BasePage {
final SecuritySettingsViewModel _securitySettingsViewModel; final SecuritySettingsViewModel _securitySettingsViewModel;
final bool _isHardwareWallet;
@override @override
Widget body(BuildContext context) { Widget body(BuildContext context) {
return Container( return Container(
padding: EdgeInsets.only(top: 10), padding: EdgeInsets.only(top: 10),
child: Column(mainAxisSize: MainAxisSize.min, children: [ child: Column(mainAxisSize: MainAxisSize.min, children: [
SettingsCellWithArrow( if (!_isHardwareWallet)
title: S.current.show_keys, SettingsCellWithArrow(
handler: (_) => _authService.authenticateAction( title: S.current.show_keys,
context, handler: (_) => _authService.authenticateAction(
route: Routes.showKeys, context,
conditionToDetermineIfToUse2FA: _securitySettingsViewModel route: Routes.showKeys,
.shouldRequireTOTP2FAForAllSecurityAndBackupSettings, conditionToDetermineIfToUse2FA:
_securitySettingsViewModel.shouldRequireTOTP2FAForAllSecurityAndBackupSettings,
),
), ),
),
SettingsCellWithArrow( SettingsCellWithArrow(
title: S.current.create_backup, title: S.current.create_backup,
handler: (_) => _authService.authenticateAction( handler: (_) => _authService.authenticateAction(

View file

@ -0,0 +1,50 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/screens/settings/widgets/settings_cell_with_arrow.dart';
import 'package:cake_wallet/src/screens/settings/widgets/settings_switcher_cell.dart';
import 'package:cake_wallet/view_model/settings/silent_payments_settings_view_model.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
class SilentPaymentsSettingsPage extends BasePage {
SilentPaymentsSettingsPage(this._silentPaymentsSettingsViewModel);
@override
String get title => S.current.silent_payments_settings;
final SilentPaymentsSettingsViewModel _silentPaymentsSettingsViewModel;
@override
Widget body(BuildContext context) {
return SingleChildScrollView(
child: Observer(builder: (_) {
return Container(
padding: EdgeInsets.only(top: 10),
child: Column(
children: [
SettingsSwitcherCell(
title: S.current.silent_payments_display_card,
value: _silentPaymentsSettingsViewModel.silentPaymentsCardDisplay,
onValueChange: (_, bool value) {
_silentPaymentsSettingsViewModel.setSilentPaymentsCardDisplay(value);
},
),
SettingsSwitcherCell(
title: S.current.silent_payments_always_scan,
value: _silentPaymentsSettingsViewModel.silentPaymentsAlwaysScan,
onValueChange: (_, bool value) {
_silentPaymentsSettingsViewModel.setSilentPaymentsAlwaysScan(value);
},
),
SettingsCellWithArrow(
title: S.current.silent_payments_scanning,
handler: (BuildContext context) => Navigator.of(context).pushNamed(Routes.rescan),
),
],
),
);
}),
);
}
}

View file

@ -57,6 +57,7 @@ class UnspentCoinsListFormState extends State<UnspentCoinsListForm> {
isSending: item.isSending, isSending: item.isSending,
isFrozen: item.isFrozen, isFrozen: item.isFrozen,
isChange: item.isChange, isChange: item.isChange,
isSilentPayment: item.isSilentPayment,
onCheckBoxTap: item.isFrozen onCheckBoxTap: item.isFrozen
? null ? null
: () async { : () async {

View file

@ -12,6 +12,7 @@ class UnspentCoinsListItem extends StatelessWidget {
required this.isSending, required this.isSending,
required this.isFrozen, required this.isFrozen,
required this.isChange, required this.isChange,
required this.isSilentPayment,
this.onCheckBoxTap, this.onCheckBoxTap,
}); });
@ -21,18 +22,16 @@ class UnspentCoinsListItem extends StatelessWidget {
final bool isSending; final bool isSending;
final bool isFrozen; final bool isFrozen;
final bool isChange; final bool isChange;
final bool isSilentPayment;
final Function()? onCheckBoxTap; final Function()? onCheckBoxTap;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final unselectedItemColor = Theme.of(context).cardColor; final unselectedItemColor = Theme.of(context).cardColor;
final selectedItemColor = Theme.of(context).primaryColor; final selectedItemColor = Theme.of(context).primaryColor;
final itemColor = isSending final itemColor = isSending ? selectedItemColor : unselectedItemColor;
? selectedItemColor final amountColor =
: unselectedItemColor; isSending ? Colors.white : Theme.of(context).extension<CakeTextTheme>()!.buttonTextColor;
final amountColor = isSending
? Colors.white
: Theme.of(context).extension<CakeTextTheme>()!.buttonTextColor;
final addressColor = isSending final addressColor = isSending
? Colors.white.withOpacity(0.5) ? Colors.white.withOpacity(0.5)
: Theme.of(context).extension<CakeTextTheme>()!.buttonSecondaryTextColor; : Theme.of(context).extension<CakeTextTheme>()!.buttonSecondaryTextColor;
@ -121,6 +120,23 @@ class UnspentCoinsListItem extends StatelessWidget {
), ),
), ),
), ),
if (isSilentPayment)
Container(
height: 17,
padding: EdgeInsets.only(left: 6, right: 6),
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(8.5)),
color: Colors.white),
alignment: Alignment.center,
child: Text(
S.of(context).silent_payments,
style: TextStyle(
color: itemColor,
fontSize: 7,
fontWeight: FontWeight.w600,
),
),
),
], ],
), ),
), ),

View file

@ -96,6 +96,7 @@ class WalletListBody extends StatefulWidget {
class WalletListBodyState extends State<WalletListBody> { class WalletListBodyState extends State<WalletListBody> {
final moneroIcon = Image.asset('assets/images/monero_logo.png', height: 24, width: 24); final moneroIcon = Image.asset('assets/images/monero_logo.png', height: 24, width: 24);
final bitcoinIcon = Image.asset('assets/images/bitcoin.png', height: 24, width: 24); final bitcoinIcon = Image.asset('assets/images/bitcoin.png', height: 24, width: 24);
final tBitcoinIcon = Image.asset('assets/images/tbtc.png', height: 24, width: 24);
final litecoinIcon = Image.asset('assets/images/litecoin_icon.png', height: 24, width: 24); final litecoinIcon = Image.asset('assets/images/litecoin_icon.png', height: 24, width: 24);
final nonWalletTypeIcon = Image.asset('assets/images/close.png', height: 24, width: 24); final nonWalletTypeIcon = Image.asset('assets/images/close.png', height: 24, width: 24);
final havenIcon = Image.asset('assets/images/haven_logo.png', height: 24, width: 24); final havenIcon = Image.asset('assets/images/haven_logo.png', height: 24, width: 24);
@ -162,7 +163,10 @@ class WalletListBodyState extends State<WalletListBody> {
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[ children: <Widget>[
wallet.isEnabled wallet.isEnabled
? _imageFor(type: wallet.type) ? _imageFor(
type: wallet.type,
isTestnet: wallet.isTestnet,
)
: nonWalletTypeIcon, : nonWalletTypeIcon,
SizedBox(width: 10), SizedBox(width: 10),
Flexible( Flexible(
@ -297,9 +301,12 @@ class WalletListBodyState extends State<WalletListBody> {
); );
} }
Image _imageFor({required WalletType type}) { Image _imageFor({required WalletType type, bool? isTestnet}) {
switch (type) { switch (type) {
case WalletType.bitcoin: case WalletType.bitcoin:
if (isTestnet == true) {
return tBitcoinIcon;
}
return bitcoinIcon; return bitcoinIcon;
case WalletType.monero: case WalletType.monero:
return moneroIcon; return moneroIcon;

View file

@ -1,3 +1,5 @@
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/src/widgets/standard_switch.dart';
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
import 'package:cake_wallet/utils/date_picker.dart'; import 'package:cake_wallet/utils/date_picker.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -12,13 +14,19 @@ class BlockchainHeightWidget extends StatefulWidget {
this.onHeightChange, this.onHeightChange,
this.focusNode, this.focusNode,
this.onHeightOrDateEntered, this.onHeightOrDateEntered,
this.hasDatePicker = true}) this.hasDatePicker = true,
: super(key: key); this.isSilentPaymentsScan = false,
this.toggleSingleScan,
this.doSingleScan = false,
}) : super(key: key);
final Function(int)? onHeightChange; final Function(int)? onHeightChange;
final Function(bool)? onHeightOrDateEntered; final Function(bool)? onHeightOrDateEntered;
final FocusNode? focusNode; final FocusNode? focusNode;
final bool hasDatePicker; final bool hasDatePicker;
final bool isSilentPaymentsScan;
final bool doSingleScan;
final Function()? toggleSingleScan;
@override @override
State<StatefulWidget> createState() => BlockchainHeightState(); State<StatefulWidget> createState() => BlockchainHeightState();
@ -64,9 +72,10 @@ class BlockchainHeightState extends State<BlockchainHeightWidget> {
child: BaseTextFormField( child: BaseTextFormField(
focusNode: widget.focusNode, focusNode: widget.focusNode,
controller: restoreHeightController, controller: restoreHeightController,
keyboardType: TextInputType.numberWithOptions( keyboardType: TextInputType.numberWithOptions(signed: false, decimal: false),
signed: false, decimal: false), hintText: widget.isSilentPaymentsScan
hintText: S.of(context).widgets_restore_from_blockheight, ? S.of(context).silent_payments_scan_from_height
: S.of(context).widgets_restore_from_blockheight,
))) )))
], ],
), ),
@ -78,8 +87,7 @@ class BlockchainHeightState extends State<BlockchainHeightWidget> {
style: TextStyle( style: TextStyle(
fontSize: 16.0, fontSize: 16.0,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
color: color: Theme.of(context).extension<CakeTextTheme>()!.titleColor),
Theme.of(context).extension<CakeTextTheme>()!.titleColor),
), ),
), ),
Row( Row(
@ -91,22 +99,47 @@ class BlockchainHeightState extends State<BlockchainHeightWidget> {
child: IgnorePointer( child: IgnorePointer(
child: BaseTextFormField( child: BaseTextFormField(
controller: dateController, controller: dateController,
hintText: S.of(context).widgets_restore_from_date, hintText: widget.isSilentPaymentsScan
? S.of(context).silent_payments_scan_from_date
: S.of(context).widgets_restore_from_date,
)), )),
), ),
)) ))
], ],
), ),
if (widget.isSilentPaymentsScan)
Padding(
padding: EdgeInsets.only(top: 24),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
S.of(context).scan_one_block,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.normal,
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
),
),
Padding(
padding: const EdgeInsets.only(right: 8),
child: StandardSwitch(
value: widget.doSingleScan,
onTaped: () => widget.toggleSingleScan?.call(),
),
)
],
),
),
Padding( Padding(
padding: EdgeInsets.only(left: 40, right: 40, top: 24), padding: EdgeInsets.only(left: 40, right: 40, top: 24),
child: Text( child: Text(
S.of(context).restore_from_date_or_blockheight, widget.isSilentPaymentsScan
? S.of(context).silent_payments_scan_from_date_or_blockheight
: S.of(context).restore_from_date_or_blockheight,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12, fontWeight: FontWeight.normal, color: Theme.of(context).hintColor),
fontWeight: FontWeight.normal,
color: Theme.of(context).hintColor
),
), ),
) )
] ]
@ -123,7 +156,12 @@ class BlockchainHeightState extends State<BlockchainHeightWidget> {
lastDate: now); lastDate: now);
if (date != null) { if (date != null) {
final height = monero!.getHeightByDate(date: date); int height;
if (widget.isSilentPaymentsScan) {
height = bitcoin!.getHeightByDate(date: date);
} else {
height = monero!.getHeightByDate(date: date);
}
setState(() { setState(() {
dateController.text = DateFormat('yyyy-MM-dd').format(date); dateController.text = DateFormat('yyyy-MM-dd').format(date);
restoreHeightController.text = '$height'; restoreHeightController.text = '$height';

View file

@ -2,19 +2,28 @@ import 'package:cake_wallet/themes/extensions/balance_page_theme.dart';
import 'package:cake_wallet/themes/extensions/sync_indicator_theme.dart'; import 'package:cake_wallet/themes/extensions/sync_indicator_theme.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart'; import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart';
import 'package:flutter_svg/flutter_svg.dart';
class DashBoardRoundedCardWidget extends StatelessWidget { class DashBoardRoundedCardWidget extends StatelessWidget {
DashBoardRoundedCardWidget({ DashBoardRoundedCardWidget({
required this.onTap, required this.onTap,
required this.title, required this.title,
required this.subTitle, required this.subTitle,
this.hint,
this.svgPicture,
this.icon,
this.onClose, this.onClose,
this.customBorder,
}); });
final VoidCallback onTap; final VoidCallback onTap;
final VoidCallback? onClose; final VoidCallback? onClose;
final String title; final String title;
final String subTitle; final String subTitle;
final Widget? hint;
final SvgPicture? svgPicture;
final Icon? icon;
final double? customBorder;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -26,34 +35,56 @@ class DashBoardRoundedCardWidget extends StatelessWidget {
child: Stack( child: Stack(
children: [ children: [
Container( Container(
padding: EdgeInsets.fromLTRB(20, 20, 40, 20), padding: EdgeInsets.all(20),
width: double.infinity, width: double.infinity,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).extension<SyncIndicatorTheme>()!.syncedBackgroundColor, color: Theme.of(context).extension<SyncIndicatorTheme>()!.syncedBackgroundColor,
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(customBorder ?? 20),
border: Border.all( border: Border.all(
color: Theme.of(context).extension<BalancePageTheme>()!.cardBorderColor, color: Theme.of(context).extension<BalancePageTheme>()!.cardBorderColor,
), ),
), ),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Row(
title, crossAxisAlignment: CrossAxisAlignment.start,
style: TextStyle( children: [
color: Theme.of(context).extension<DashboardPageTheme>()!.cardTextColor, Expanded(
fontSize: 24, child: Column(
fontWeight: FontWeight.w900, crossAxisAlignment: CrossAxisAlignment.start,
), children: [
Text(
title,
style: TextStyle(
color:
Theme.of(context).extension<DashboardPageTheme>()!.cardTextColor,
fontSize: 24,
fontWeight: FontWeight.w900,
),
softWrap: true,
),
SizedBox(height: 5),
Text(
subTitle,
style: TextStyle(
color: Theme.of(context)
.extension<DashboardPageTheme>()!
.cardTextColor,
fontWeight: FontWeight.w500,
fontFamily: 'Lato'),
softWrap: true,
),
],
),
),
if (svgPicture != null) svgPicture!,
if (icon != null) icon!
],
), ),
SizedBox(height: 5), if (hint != null) ...[
Text( SizedBox(height: 10),
subTitle, hint!,
style: TextStyle( ]
color: Theme.of(context).extension<DashboardPageTheme>()!.cardTextColor,
fontWeight: FontWeight.w500,
fontFamily: 'Lato'),
)
], ],
), ),
), ),

View file

@ -17,6 +17,7 @@ class SettingActions {
connectionSettingAction, connectionSettingAction,
walletSettingAction, walletSettingAction,
addressBookSettingAction, addressBookSettingAction,
silentPaymentsSettingAction,
securityBackupSettingAction, securityBackupSettingAction,
privacySettingAction, privacySettingAction,
displaySettingAction, displaySettingAction,
@ -35,6 +36,15 @@ class SettingActions {
supportSettingAction, supportSettingAction,
]; ];
static SettingActions silentPaymentsSettingAction = SettingActions._(
name: (context) => S.of(context).silent_payments_settings,
image: 'assets/images/bitcoin_menu.png',
onTap: (BuildContext context) {
Navigator.pop(context);
Navigator.of(context).pushNamed(Routes.silentPaymentsSettings);
},
);
static SettingActions connectionSettingAction = SettingActions._( static SettingActions connectionSettingAction = SettingActions._(
name: (context) => S.of(context).connection_sync, name: (context) => S.of(context).connection_sync,
image: 'assets/images/nodes_menu.png', image: 'assets/images/nodes_menu.png',

View file

@ -1,5 +1,4 @@
import 'dart:io'; import 'dart:io';
import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart'; import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart';
import 'package:cake_wallet/core/secure_storage.dart'; import 'package:cake_wallet/core/secure_storage.dart';
@ -108,6 +107,8 @@ abstract class SettingsStoreBase with Store {
required this.lookupsOpenAlias, required this.lookupsOpenAlias,
required this.lookupsENS, required this.lookupsENS,
required this.customBitcoinFeeRate, required this.customBitcoinFeeRate,
required this.silentPaymentsCardDisplay,
required this.silentPaymentsAlwaysScan,
TransactionPriority? initialBitcoinTransactionPriority, TransactionPriority? initialBitcoinTransactionPriority,
TransactionPriority? initialMoneroTransactionPriority, TransactionPriority? initialMoneroTransactionPriority,
TransactionPriority? initialHavenTransactionPriority, TransactionPriority? initialHavenTransactionPriority,
@ -518,6 +519,16 @@ abstract class SettingsStoreBase with Store {
(int customBitcoinFeeRate) => (int customBitcoinFeeRate) =>
_sharedPreferences.setInt(PreferencesKey.customBitcoinFeeRate, customBitcoinFeeRate)); _sharedPreferences.setInt(PreferencesKey.customBitcoinFeeRate, customBitcoinFeeRate));
reaction((_) => silentPaymentsCardDisplay, (bool silentPaymentsCardDisplay) {
_sharedPreferences.setBool(
PreferencesKey.silentPaymentsCardDisplay, silentPaymentsCardDisplay);
});
reaction(
(_) => silentPaymentsAlwaysScan,
(bool silentPaymentsAlwaysScan) => _sharedPreferences.setBool(
PreferencesKey.silentPaymentsAlwaysScan, silentPaymentsAlwaysScan));
this.nodes.observe((change) { this.nodes.observe((change) {
if (change.newValue != null && change.key != null) { if (change.newValue != null && change.key != null) {
_saveCurrentNode(change.newValue!, change.key!); _saveCurrentNode(change.newValue!, change.key!);
@ -713,6 +724,12 @@ abstract class SettingsStoreBase with Store {
@observable @observable
int customBitcoinFeeRate; int customBitcoinFeeRate;
@observable
bool silentPaymentsCardDisplay;
@observable
bool silentPaymentsAlwaysScan;
final SecureStorage _secureStorage; final SecureStorage _secureStorage;
final SharedPreferences _sharedPreferences; final SharedPreferences _sharedPreferences;
final BackgroundTasks _backgroundTasks; final BackgroundTasks _backgroundTasks;
@ -859,6 +876,10 @@ abstract class SettingsStoreBase with Store {
final lookupsOpenAlias = sharedPreferences.getBool(PreferencesKey.lookupsOpenAlias) ?? true; final lookupsOpenAlias = sharedPreferences.getBool(PreferencesKey.lookupsOpenAlias) ?? true;
final lookupsENS = sharedPreferences.getBool(PreferencesKey.lookupsENS) ?? true; final lookupsENS = sharedPreferences.getBool(PreferencesKey.lookupsENS) ?? true;
final customBitcoinFeeRate = sharedPreferences.getInt(PreferencesKey.customBitcoinFeeRate) ?? 1; final customBitcoinFeeRate = sharedPreferences.getInt(PreferencesKey.customBitcoinFeeRate) ?? 1;
final silentPaymentsCardDisplay =
sharedPreferences.getBool(PreferencesKey.silentPaymentsCardDisplay) ?? true;
final silentPaymentsAlwaysScan =
sharedPreferences.getBool(PreferencesKey.silentPaymentsAlwaysScan) ?? false;
// If no value // If no value
if (pinLength == null || pinLength == 0) { if (pinLength == null || pinLength == 0) {
@ -1103,6 +1124,8 @@ abstract class SettingsStoreBase with Store {
lookupsOpenAlias: lookupsOpenAlias, lookupsOpenAlias: lookupsOpenAlias,
lookupsENS: lookupsENS, lookupsENS: lookupsENS,
customBitcoinFeeRate: customBitcoinFeeRate, customBitcoinFeeRate: customBitcoinFeeRate,
silentPaymentsCardDisplay: silentPaymentsCardDisplay,
silentPaymentsAlwaysScan: silentPaymentsAlwaysScan,
initialMoneroTransactionPriority: moneroTransactionPriority, initialMoneroTransactionPriority: moneroTransactionPriority,
initialBitcoinTransactionPriority: bitcoinTransactionPriority, initialBitcoinTransactionPriority: bitcoinTransactionPriority,
initialHavenTransactionPriority: havenTransactionPriority, initialHavenTransactionPriority: havenTransactionPriority,
@ -1242,6 +1265,10 @@ abstract class SettingsStoreBase with Store {
lookupsOpenAlias = sharedPreferences.getBool(PreferencesKey.lookupsOpenAlias) ?? true; lookupsOpenAlias = sharedPreferences.getBool(PreferencesKey.lookupsOpenAlias) ?? true;
lookupsENS = sharedPreferences.getBool(PreferencesKey.lookupsENS) ?? true; lookupsENS = sharedPreferences.getBool(PreferencesKey.lookupsENS) ?? true;
customBitcoinFeeRate = sharedPreferences.getInt(PreferencesKey.customBitcoinFeeRate) ?? 1; customBitcoinFeeRate = sharedPreferences.getInt(PreferencesKey.customBitcoinFeeRate) ?? 1;
silentPaymentsCardDisplay =
sharedPreferences.getBool(PreferencesKey.silentPaymentsCardDisplay) ?? true;
silentPaymentsAlwaysScan =
sharedPreferences.getBool(PreferencesKey.silentPaymentsAlwaysScan) ?? false;
final nodeId = sharedPreferences.getInt(PreferencesKey.currentNodeIdKey); final nodeId = sharedPreferences.getInt(PreferencesKey.currentNodeIdKey);
final bitcoinElectrumServerId = final bitcoinElectrumServerId =
sharedPreferences.getInt(PreferencesKey.currentBitcoinElectrumSererIdKey); sharedPreferences.getInt(PreferencesKey.currentBitcoinElectrumSererIdKey);

View file

@ -6,7 +6,7 @@ part 'responsive_layout_util.g.dart';
class _ResponsiveLayoutUtil = ResponsiveLayoutUtilBase with _$_ResponsiveLayoutUtil; class _ResponsiveLayoutUtil = ResponsiveLayoutUtilBase with _$_ResponsiveLayoutUtil;
abstract class ResponsiveLayoutUtilBase with Store, WidgetsBindingObserver { abstract class ResponsiveLayoutUtilBase with Store, WidgetsBindingObserver {
static const double _kMobileThreshold = 550; static const double kMobileThreshold = 550;
static const double kDesktopMaxWidthConstraint = 400; static const double kDesktopMaxWidthConstraint = 400;
static const double kDesktopMaxDashBoardWidthConstraint = 900; static const double kDesktopMaxDashBoardWidthConstraint = 900;
static const double kPopupWidth = 400; static const double kPopupWidth = 400;
@ -42,13 +42,13 @@ abstract class ResponsiveLayoutUtilBase with Store, WidgetsBindingObserver {
@computed @computed
bool get shouldRenderMobileUI { bool get shouldRenderMobileUI {
return (screenWidth <= _kMobileThreshold) || return (screenWidth <= kMobileThreshold) ||
(orientation == Orientation.portrait && screenWidth < screenHeight) || (orientation == Orientation.portrait && screenWidth < screenHeight) ||
(orientation == Orientation.landscape && screenWidth < screenHeight); (orientation == Orientation.landscape && screenWidth < screenHeight);
} }
bool get shouldRenderTabletUI { bool get shouldRenderTabletUI {
return screenWidth > _kMobileThreshold && screenWidth < kDesktopMaxDashBoardWidthConstraint; return screenWidth > kMobileThreshold && screenWidth < kDesktopMaxDashBoardWidthConstraint;
} }
} }

View file

@ -2,6 +2,7 @@ import 'dart:async';
import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart'; import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart';
import 'package:cake_wallet/entities/contact_base.dart'; import 'package:cake_wallet/entities/contact_base.dart';
import 'package:cake_wallet/entities/wallet_contact.dart'; import 'package:cake_wallet/entities/wallet_contact.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/store/settings_store.dart';
import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_type.dart'; import 'package:cw_core/wallet_type.dart';
@ -40,16 +41,19 @@ abstract class ContactListViewModelBase with Store {
}); });
} else if (info.addresses?.isNotEmpty == true) { } else if (info.addresses?.isNotEmpty == true) {
info.addresses!.forEach((address, label) { info.addresses!.forEach((address, label) {
if (label.isEmpty) {
return;
}
final name = _createName(info.name, label); final name = _createName(info.name, label);
walletContacts.add(WalletContact( walletContacts.add(WalletContact(
address, address,
name, name,
walletTypeToCryptoCurrency(info.type), walletTypeToCryptoCurrency(info.type,
isTestnet:
info.network == null ? false : info.network!.toLowerCase().contains("testnet")),
)); ));
// Only one contact address per wallet
return;
}); });
} else if (info.address != null) { } else {
walletContacts.add(WalletContact( walletContacts.add(WalletContact(
info.address, info.address,
info.name, info.name,
@ -64,7 +68,9 @@ abstract class ContactListViewModelBase with Store {
} }
String _createName(String walletName, String label) { String _createName(String walletName, String label) {
return label.isNotEmpty ? '$walletName ($label)' : walletName; return label.isNotEmpty
? '$walletName (${label.replaceAll(RegExp(r'active', caseSensitive: false), S.current.active).replaceAll(RegExp(r'silent payments', caseSensitive: false), S.current.silent_payments)})'
: walletName;
} }
final bool isAutoGenerateEnabled; final bool isAutoGenerateEnabled;

View file

@ -60,6 +60,9 @@ abstract class BalanceViewModelBase with Store {
@observable @observable
WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo> wallet; WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo> wallet;
@computed
bool get hasSilentPayments => wallet.type == WalletType.bitcoin;
@computed @computed
double get price { double get price {
final price = fiatConvertationStore.prices[appStore.wallet!.currency]; final price = fiatConvertationStore.prices[appStore.wallet!.currency];

View file

@ -0,0 +1,16 @@
import 'package:cake_wallet/ionia/ionia_service.dart';
import 'package:mobx/mobx.dart';
part 'cake_features_view_model.g.dart';
class CakeFeaturesViewModel = CakeFeaturesViewModelBase with _$CakeFeaturesViewModel;
abstract class CakeFeaturesViewModelBase with Store {
final IoniaService _ioniaService;
CakeFeaturesViewModelBase(this._ioniaService);
Future<bool> isIoniaUserAuthenticated() async {
return await _ioniaService.isLogined();
}
}

View file

@ -45,6 +45,7 @@ import 'package:cw_core/wallet_type.dart';
import 'package:eth_sig_util/util/utils.dart'; import 'package:eth_sig_util/util/utils.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:mobx/mobx.dart'; import 'package:mobx/mobx.dart';
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
@ -201,6 +202,14 @@ abstract class DashboardViewModelBase with Store {
return true; return true;
}); });
if (hasSilentPayments) {
silentPaymentsScanningActive = bitcoin!.getScanningActive(wallet);
reaction((_) => wallet.syncStatus, (SyncStatus syncStatus) {
silentPaymentsScanningActive = bitcoin!.getScanningActive(wallet);
});
}
} }
@observable @observable
@ -287,11 +296,36 @@ abstract class DashboardViewModelBase with Store {
@observable @observable
WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo> wallet; WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo> wallet;
bool get hasRescan => wallet.type == WalletType.monero || wallet.type == WalletType.haven; @computed
bool get isTestnet => wallet.type == WalletType.bitcoin && bitcoin!.isTestnet(wallet);
@computed
bool get hasRescan =>
wallet.type == WalletType.bitcoin ||
wallet.type == WalletType.monero ||
wallet.type == WalletType.haven;
@computed
bool get hasSilentPayments => wallet.type == WalletType.bitcoin;
@computed
bool get showSilentPaymentsCard => hasSilentPayments && settingsStore.silentPaymentsCardDisplay;
final KeyService keyService; final KeyService keyService;
final SharedPreferences sharedPreferences; final SharedPreferences sharedPreferences;
@observable
bool silentPaymentsScanningActive = false;
@action
void setSilentPaymentsScanning(bool active) {
silentPaymentsScanningActive = active;
if (hasSilentPayments) {
bitcoin!.setScanningActive(wallet, active);
}
}
BalanceViewModel balanceViewModel; BalanceViewModel balanceViewModel;
AppStore appStore; AppStore appStore;
@ -535,24 +569,29 @@ abstract class DashboardViewModelBase with Store {
Future<ServicesResponse> getServicesStatus() async { Future<ServicesResponse> getServicesStatus() async {
try { try {
final res = await http.get(Uri.parse("https://service-api.cakewallet.com/v1/active-notices")); if (isEnabledBulletinAction) {
final res = await http.get(Uri.parse("https://service-api.cakewallet.com/v1/active-notices"));
if (res.statusCode < 200 || res.statusCode >= 300) { if (res.statusCode < 200 || res.statusCode >= 300) {
throw res.body; throw res.body;
}
final oldSha = sharedPreferences.getString(PreferencesKey.serviceStatusShaKey);
final hash = await Cryptography.instance.sha256().hash(utf8.encode(res.body));
final currentSha = bytesToHex(hash.bytes);
final hasUpdates = oldSha != currentSha;
return ServicesResponse.fromJson(
json.decode(res.body) as Map<String, dynamic>,
hasUpdates,
currentSha,
);
} }
else {
final oldSha = sharedPreferences.getString(PreferencesKey.serviceStatusShaKey); return ServicesResponse([], false, '');
}
final hash = await Cryptography.instance.sha256().hash(utf8.encode(res.body));
final currentSha = bytesToHex(hash.bytes);
final hasUpdates = oldSha != currentSha;
return ServicesResponse.fromJson(
json.decode(res.body) as Map<String, dynamic>,
hasUpdates,
currentSha,
);
} catch (_) { } catch (_) {
return ServicesResponse([], false, ''); return ServicesResponse([], false, '');
} }

View file

@ -1,17 +0,0 @@
import 'package:cake_wallet/ionia/ionia_service.dart';
import 'package:mobx/mobx.dart';
part 'market_place_view_model.g.dart';
class MarketPlaceViewModel = MarketPlaceViewModelBase with _$MarketPlaceViewModel;
abstract class MarketPlaceViewModelBase with Store {
final IoniaService _ioniaService;
MarketPlaceViewModelBase(this._ioniaService);
Future<bool> isIoniaUserAuthenticated() async {
return await _ioniaService.isLogined();
}
}

View file

@ -154,11 +154,11 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
ChangeNowExchangeProvider(settingsStore: _settingsStore), ChangeNowExchangeProvider(settingsStore: _settingsStore),
SideShiftExchangeProvider(), SideShiftExchangeProvider(),
SimpleSwapExchangeProvider(), SimpleSwapExchangeProvider(),
TrocadorExchangeProvider(
useTorOnly: _useTorOnly, providerStates: _settingsStore.trocadorProviderStates),
ThorChainExchangeProvider(tradesStore: trades), ThorChainExchangeProvider(tradesStore: trades),
if (FeatureFlag.isExolixEnabled) ExolixExchangeProvider(), if (FeatureFlag.isExolixEnabled) ExolixExchangeProvider(),
QuantexExchangeProvider(), QuantexExchangeProvider(),
TrocadorExchangeProvider(
useTorOnly: _useTorOnly, providerStates: _settingsStore.trocadorProviderStates),
]; ];
@observable @observable

View file

@ -1,4 +1,6 @@
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:mobx/mobx.dart'; import 'package:mobx/mobx.dart';
part 'rescan_view_model.g.dart'; part 'rescan_view_model.g.dart';
@ -8,11 +10,12 @@ class RescanViewModel = RescanViewModelBase with _$RescanViewModel;
enum RescanWalletState { rescaning, none } enum RescanWalletState { rescaning, none }
abstract class RescanViewModelBase with Store { abstract class RescanViewModelBase with Store {
RescanViewModelBase(this._wallet) RescanViewModelBase(this.wallet)
: state = RescanWalletState.none, : state = RescanWalletState.none,
isButtonEnabled = false; isButtonEnabled = false,
doSingleScan = false;
final WalletBase _wallet; final WalletBase wallet;
@observable @observable
RescanWalletState state; RescanWalletState state;
@ -20,11 +23,21 @@ abstract class RescanViewModelBase with Store {
@observable @observable
bool isButtonEnabled; bool isButtonEnabled;
@observable
bool doSingleScan;
@computed
bool get isSilentPaymentsScan => wallet.type == WalletType.bitcoin;
@action @action
Future<void> rescanCurrentWallet({required int restoreHeight}) async { Future<void> rescanCurrentWallet({required int restoreHeight}) async {
state = RescanWalletState.rescaning; state = RescanWalletState.rescaning;
await _wallet.rescan(height: restoreHeight); if (wallet.type != WalletType.bitcoin) {
_wallet.transactionHistory.clear(); wallet.rescan(height: restoreHeight);
wallet.transactionHistory.clear();
} else {
bitcoin!.rescan(wallet, height: restoreHeight, doSingleScan: doSingleScan);
}
state = RescanWalletState.none; state = RescanWalletState.none;
} }
} }

View file

@ -139,8 +139,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
String get pendingTransactionFeeFiatAmount { String get pendingTransactionFeeFiatAmount {
try { try {
if (pendingTransaction != null) { if (pendingTransaction != null) {
final currency = final currency = pendingTransactionFeeCurrency(walletType);
isEVMCompatibleChain(walletType) ? wallet.currency : selectedCryptoCurrency;
final fiat = calculateFiatAmount( final fiat = calculateFiatAmount(
price: _fiatConversationStore.prices[currency]!, price: _fiatConversationStore.prices[currency]!,
cryptoAmount: pendingTransaction!.feeFormatted); cryptoAmount: pendingTransaction!.feeFormatted);
@ -153,6 +152,18 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
} }
} }
CryptoCurrency pendingTransactionFeeCurrency(WalletType type) {
switch (type) {
case WalletType.ethereum:
case WalletType.polygon:
case WalletType.tron:
case WalletType.solana:
return wallet.currency;
default:
return selectedCryptoCurrency;
}
}
FiatCurrency get fiat => _settingsStore.fiatCurrency; FiatCurrency get fiat => _settingsStore.fiatCurrency;
TransactionPriority get transactionPriority { TransactionPriority get transactionPriority {
@ -218,7 +229,10 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
isFiatDisabled ? '' : pendingTransactionFeeFiatAmount + ' ' + fiat.title; isFiatDisabled ? '' : pendingTransactionFeeFiatAmount + ' ' + fiat.title;
@computed @computed
bool get isReadyForSend => wallet.syncStatus is SyncedSyncStatus; bool get isReadyForSend =>
wallet.syncStatus is SyncedSyncStatus ||
// If silent payments scanning, can still send payments
(wallet.type == WalletType.bitcoin && wallet.syncStatus is SyncingSyncStatus);
@computed @computed
List<Template> get templates => sendTemplateViewModel.templates List<Template> get templates => sendTemplateViewModel.templates
@ -588,6 +602,10 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
walletType == WalletType.litecoin || walletType == WalletType.litecoin ||
walletType == WalletType.bitcoinCash) { walletType == WalletType.bitcoinCash) {
if (error is TransactionWrongBalanceException) { if (error is TransactionWrongBalanceException) {
if (error.amount != null)
return S.current
.tx_wrong_balance_with_amount_exception(currency.toString(), error.amount.toString());
return S.current.tx_wrong_balance_exception(currency.toString()); return S.current.tx_wrong_balance_exception(currency.toString());
} }
if (error is TransactionNoInputsException) { if (error is TransactionNoInputsException) {
@ -614,9 +632,15 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
if (error is TransactionCommitFailedVoutNegative) { if (error is TransactionCommitFailedVoutNegative) {
return S.current.tx_rejected_vout_negative; return S.current.tx_rejected_vout_negative;
} }
if (error is TransactionCommitFailedBIP68Final) {
return S.current.tx_rejected_bip68_final;
}
if (error is TransactionNoDustOnChangeException) { if (error is TransactionNoDustOnChangeException) {
return S.current.tx_commit_exception_no_dust_on_change(error.min, error.max); return S.current.tx_commit_exception_no_dust_on_change(error.min, error.max);
} }
if (error is TransactionInputNotSupported) {
return S.current.tx_invalid_input;
}
} }
return errorMessage; return errorMessage;

View file

@ -0,0 +1,33 @@
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/store/settings_store.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:mobx/mobx.dart';
part 'silent_payments_settings_view_model.g.dart';
class SilentPaymentsSettingsViewModel = SilentPaymentsSettingsViewModelBase
with _$SilentPaymentsSettingsViewModel;
abstract class SilentPaymentsSettingsViewModelBase with Store {
SilentPaymentsSettingsViewModelBase(this._settingsStore, this._wallet);
final SettingsStore _settingsStore;
final WalletBase _wallet;
@computed
bool get silentPaymentsCardDisplay => _settingsStore.silentPaymentsCardDisplay;
@computed
bool get silentPaymentsAlwaysScan => _settingsStore.silentPaymentsAlwaysScan;
@action
void setSilentPaymentsCardDisplay(bool value) {
_settingsStore.silentPaymentsCardDisplay = value;
}
@action
void setSilentPaymentsAlwaysScan(bool value) {
_settingsStore.silentPaymentsAlwaysScan = value;
if (value) bitcoin!.setScanningActive(_wallet, true);
}
}

View file

@ -15,7 +15,8 @@ abstract class UnspentCoinsItemBase with Store {
required this.isChange, required this.isChange,
required this.amountRaw, required this.amountRaw,
required this.vout, required this.vout,
required this.keyImage required this.keyImage,
required this.isSilentPayment,
}); });
@observable @observable
@ -47,4 +48,7 @@ abstract class UnspentCoinsItemBase with Store {
@observable @observable
String? keyImage; String? keyImage;
@observable
bool isSilentPayment;
} }

View file

@ -1,10 +1,12 @@
import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/monero/monero.dart';
import 'package:cake_wallet/utils/exception_handler.dart';
import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_item.dart'; import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_item.dart';
import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/unspent_coins_info.dart';
import 'package:cw_core/unspent_transaction_output.dart'; import 'package:cw_core/unspent_transaction_output.dart';
import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_type.dart'; import 'package:cw_core/wallet_type.dart';
import 'package:flutter/cupertino.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:mobx/mobx.dart'; import 'package:mobx/mobx.dart';
@ -86,22 +88,33 @@ abstract class UnspentCoinsListViewModelBase with Store {
@action @action
void _updateUnspentCoinsInfo() { void _updateUnspentCoinsInfo() {
_items.clear(); _items.clear();
_items.addAll(_getUnspents().map((elem) {
final info =
getUnspentCoinInfo(elem.hash, elem.address, elem.value, elem.vout, elem.keyImage);
return UnspentCoinsItem( List<UnspentCoinsItem> unspents = [];
address: elem.address, _getUnspents().forEach((elem) {
amount: '${formatAmountToString(elem.value)} ${wallet.currency.title}', try {
hash: elem.hash, final info =
isFrozen: info.isFrozen, getUnspentCoinInfo(elem.hash, elem.address, elem.value, elem.vout, elem.keyImage);
note: info.note,
isSending: info.isSending, unspents.add(UnspentCoinsItem(
amountRaw: elem.value, address: elem.address,
vout: elem.vout, amount: '${formatAmountToString(elem.value)} ${wallet.currency.title}',
keyImage: elem.keyImage, hash: elem.hash,
isChange: elem.isChange, isFrozen: info.isFrozen,
); note: info.note,
})); isSending: info.isSending,
amountRaw: elem.value,
vout: elem.vout,
keyImage: elem.keyImage,
isChange: elem.isChange,
isSilentPayment: info.isSilentPayment ?? false,
));
} catch (e, s) {
print(s);
print(e.toString());
ExceptionHandler.onError(FlutterErrorDetails(exception: e, stack: s));
}
});
_items.addAll(unspents);
} }
} }

View file

@ -1,3 +1,6 @@
import 'package:cake_wallet/utils/list_item.dart'; import 'package:cake_wallet/utils/list_item.dart';
class WalletAddressListHeader extends ListItem {} class WalletAddressListHeader extends ListItem {
final String? title;
WalletAddressListHeader({this.title});
}

View file

@ -8,8 +8,10 @@ class WalletAddressListItem extends ListItem {
this.name, this.name,
this.txCount, this.txCount,
this.balance, this.balance,
this.isChange = false}) this.isChange = false,
: super(); // Address that is only ever used once, shouldn't be used to receive funds, copy and paste, share etc
this.isOneTimeReceiveAddress = false,
}) : super();
final int? id; final int? id;
final bool isPrimary; final bool isPrimary;
@ -18,7 +20,8 @@ class WalletAddressListItem extends ListItem {
final int? txCount; final int? txCount;
final String? balance; final String? balance;
final bool isChange; final bool isChange;
final bool? isOneTimeReceiveAddress;
@override @override
String toString() => name ?? address; String toString() => name ?? address;
} }

View file

@ -334,20 +334,55 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
} }
if (isElectrumWallet) { if (isElectrumWallet) {
final addressItems = bitcoin!.getSubAddresses(wallet).map((subaddress) { if (bitcoin!.hasSelectedSilentPayments(wallet)) {
final isPrimary = subaddress.id == 0; final addressItems = bitcoin!.getSilentPaymentAddresses(wallet).map((address) {
final isPrimary = address.id == 0;
return WalletAddressListItem( return WalletAddressListItem(
id: subaddress.id, id: address.id,
isPrimary: isPrimary, isPrimary: isPrimary,
name: subaddress.name, name: address.name,
address: subaddress.address, address: address.address,
txCount: subaddress.txCount, txCount: address.txCount,
balance: AmountConverter.amountIntToString( balance: AmountConverter.amountIntToString(
walletTypeToCryptoCurrency(type), subaddress.balance), walletTypeToCryptoCurrency(type), address.balance),
isChange: subaddress.isChange); isChange: address.isChange,
}); );
addressList.addAll(addressItems); });
addressList.addAll(addressItems);
addressList.add(WalletAddressListHeader(title: S.current.received));
final receivedAddressItems =
bitcoin!.getSilentPaymentReceivedAddresses(wallet).map((address) {
return WalletAddressListItem(
id: address.id,
isPrimary: false,
name: address.name,
address: address.address,
txCount: address.txCount,
balance: AmountConverter.amountIntToString(
walletTypeToCryptoCurrency(type), address.balance),
isChange: address.isChange,
isOneTimeReceiveAddress: true,
);
});
addressList.addAll(receivedAddressItems);
} else {
final addressItems = bitcoin!.getSubAddresses(wallet).map((subaddress) {
final isPrimary = subaddress.id == 0;
return WalletAddressListItem(
id: subaddress.id,
isPrimary: isPrimary,
name: subaddress.name,
address: subaddress.address,
txCount: subaddress.txCount,
balance: AmountConverter.amountIntToString(
walletTypeToCryptoCurrency(type), subaddress.balance),
isChange: subaddress.isChange);
});
addressList.addAll(addressItems);
}
} }
if (wallet.type == WalletType.ethereum) { if (wallet.type == WalletType.ethereum) {
@ -416,9 +451,14 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
wallet.type == WalletType.litecoin || wallet.type == WalletType.litecoin ||
wallet.type == WalletType.bitcoinCash; wallet.type == WalletType.bitcoinCash;
@computed
bool get isSilentPayments =>
wallet.type == WalletType.bitcoin && bitcoin!.hasSelectedSilentPayments(wallet);
@computed @computed
bool get isAutoGenerateSubaddressEnabled => bool get isAutoGenerateSubaddressEnabled =>
_settingsStore.autoGenerateSubaddressStatus != AutoGenerateSubaddressStatus.disabled; _settingsStore.autoGenerateSubaddressStatus != AutoGenerateSubaddressStatus.disabled &&
!isSilentPayments;
List<ListItem> _baseItems; List<ListItem> _baseItems;
@ -478,4 +518,11 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
amount = ''; amount = '';
} }
} }
@action
void deleteAddress(ListItem item) {
if (wallet.type == WalletType.bitcoin && item is WalletAddressListItem) {
bitcoin!.deleteSilentPaymentAddress(wallet, item.address);
}
}
} }

View file

@ -7,6 +7,7 @@ class WalletListItem {
required this.key, required this.key,
this.isCurrent = false, this.isCurrent = false,
this.isEnabled = true, this.isEnabled = true,
this.isTestnet = false,
}); });
final String name; final String name;
@ -14,4 +15,5 @@ class WalletListItem {
final bool isCurrent; final bool isCurrent;
final dynamic key; final dynamic key;
final bool isEnabled; final bool isEnabled;
final bool isTestnet;
} }

View file

@ -61,6 +61,7 @@ abstract class WalletListViewModelBase with Store {
key: info.key, key: info.key,
isCurrent: info.name == _appStore.wallet?.name && info.type == _appStore.wallet?.type, isCurrent: info.name == _appStore.wallet?.name && info.type == _appStore.wallet?.type,
isEnabled: availableWalletTypes.contains(info.type), isEnabled: availableWalletTypes.contains(info.type),
isTestnet: info.network?.toLowerCase().contains('testnet') ?? false,
), ),
), ),
); );

View file

@ -47,6 +47,8 @@ PODS:
- shared_preferences_foundation (0.0.1): - shared_preferences_foundation (0.0.1):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- sp_scanner (0.0.1):
- FlutterMacOS
- url_launcher_macos (0.0.1): - url_launcher_macos (0.0.1):
- FlutterMacOS - FlutterMacOS
- wakelock_plus (0.0.1): - wakelock_plus (0.0.1):
@ -67,6 +69,7 @@ DEPENDENCIES:
- path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
- share_plus_macos (from `Flutter/ephemeral/.symlinks/plugins/share_plus_macos/macos`) - share_plus_macos (from `Flutter/ephemeral/.symlinks/plugins/share_plus_macos/macos`)
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
- sp_scanner (from `Flutter/ephemeral/.symlinks/plugins/sp_scanner/macos`)
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
- wakelock_plus (from `Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos`) - wakelock_plus (from `Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos`)
@ -104,6 +107,8 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/share_plus_macos/macos :path: Flutter/ephemeral/.symlinks/plugins/share_plus_macos/macos
shared_preferences_foundation: shared_preferences_foundation:
:path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin
sp_scanner:
:path: Flutter/ephemeral/.symlinks/plugins/sp_scanner/macos
url_launcher_macos: url_launcher_macos:
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
wakelock_plus: wakelock_plus:
@ -126,7 +131,8 @@ SPEC CHECKSUMS:
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
share_plus_macos: 853ee48e7dce06b633998ca0735d482dd671ade4 share_plus_macos: 853ee48e7dce06b633998ca0735d482dd671ade4
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95 sp_scanner: 269d96e0ec3173e69156be7239b95182be3b8303
url_launcher_macos: 5f437abeda8c85500ceb03f5c1938a8c5a705399
wakelock_plus: 4783562c9a43d209c458cb9b30692134af456269 wakelock_plus: 4783562c9a43d209c458cb9b30692134af456269
PODFILE CHECKSUM: 65ec1541137fb5b35d00490dec1bb48d4d9586bb PODFILE CHECKSUM: 65ec1541137fb5b35d00490dec1bb48d4d9586bb

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