Merge remote-tracking branch 'origin/main' into CW-453-silent-payments

This commit is contained in:
Rafael Saes 2024-04-01 18:07:49 -03:00
commit 7197a7bc71
151 changed files with 3083 additions and 1393 deletions

View file

@ -143,7 +143,7 @@ jobs:
echo "const chatwootWebsiteToken = '${{ secrets.CHATWOOT_WEBSITE_TOKEN }}';" >> lib/.secrets.g.dart
echo "const exolixApiKey = '${{ secrets.EXOLIX_API_KEY }}';" >> lib/.secrets.g.dart
echo "const robinhoodApplicationId = '${{ secrets.ROBINHOOD_APPLICATION_ID }}';" >> lib/.secrets.g.dart
echo "const robinhoodCIdApiSecret = '${{ secrets.ROBINHOOD_CID_CLIENT_SECRET }}';" >> lib/.secrets.g.dart
echo "const exchangeHelperApiKey = '${{ secrets.ROBINHOOD_CID_CLIENT_SECRET }}';" >> lib/.secrets.g.dart
echo "const walletConnectProjectId = '${{ secrets.WALLET_CONNECT_PROJECT_ID }}';" >> lib/.secrets.g.dart
echo "const moralisApiKey = '${{ secrets.MORALIS_API_KEY }}';" >> lib/.secrets.g.dart
echo "const polygonScanApiKey = '${{ secrets.POLYGON_SCAN_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart

Binary file not shown.

Before

Width:  |  Height:  |  Size: 471 B

View file

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="Capa_1"
x="0px"
y="0px"
width="510px"
height="510px"
viewBox="0 0 510 510"
style="enable-background:new 0 0 510 510;"
xml:space="preserve"
inkscape:version="0.92.3 (2405546, 2018-03-11)"
sodipodi:docname="notification_logo_tilt_white.svg"><metadata
id="metadata42"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs40" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1090"
id="namedview38"
showgrid="false"
inkscape:zoom="0.4627451"
inkscape:cx="-849.27966"
inkscape:cy="255"
inkscape:window-x="-12"
inkscape:window-y="58"
inkscape:window-maximized="1"
inkscape:current-layer="notifications"
inkscape:object-paths="true" /><g
id="g3"
transform="matrix(0.87658593,0,0,0.87658593,31.470588,31.470588)"><g
id="notifications"
transform="rotate(-20,255,255)"><path
d="m 233.56017,502.19654 c 28.05,0 51,-22.95 51,-51 h -102 c 0,28.05 22.95,51 51,51 z m 165.75,-153 v -140.25 c 0,-79.05 -53.55,-142.8 -127.5,-160.65 v -17.85 c 0,-20.4 -17.85,-38.2499996 -38.25,-38.2499996 -20.4,0 -38.25,17.8499996 -38.25,38.2499996 v 17.85 c -73.95,17.85 -127.499999,81.6 -127.499999,160.65 v 140.25 l -51,51 v 25.5 H 450.31017 v -25.5 z"
id="path6"
inkscape:connector-curvature="0"
style="opacity:1;fill:#ffffff;fill-opacity:0;stroke:#000000;stroke-width:19.39342117;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /></g></g><g
id="g8" /><g
id="g10" /><g
id="g12" /><g
id="g14" /><g
id="g16" /><g
id="g18" /><g
id="g20" /><g
id="g22" /><g
id="g24" /><g
id="g26" /><g
id="g28" /><g
id="g30" /><g
id="g32" /><g
id="g34" /><g
id="g36" /></svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
assets/images/thorchain.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View file

@ -1,2 +1,2 @@
New themes
Bug fixes and enhancements
Exchange flow enhancements and fixes
Generic enhancements and bug fixes

View file

@ -1,6 +1,6 @@
Add Solana wallet
Support ALL Bitcoin address types (Legacy, Segwit (both variants), Taproot)
Enhance Sending/Receiving flow for Bitcoin
Improve fee calculations in Bitcoin
New themes
Bug fixes and enhancements
Exchange flow enhancements and fixes
Add MoonPay to Buy options
Add THORChain to Exchange providers
Improve Bitcoin fee calculations
Fixes and enhancements for Solana
Generic enhancements and bug fixes

View file

@ -3,6 +3,9 @@ import 'package:bitcoin_base/bitcoin_base.dart' as bitcoin;
List<int> addressToOutputScript(String address, bitcoin.BasedUtxoNetwork network) {
try {
if (network == bitcoin.BitcoinCashNetwork.mainnet) {
return bitcoin.BitcoinCashAddress(address).baseAddress.toScriptPubKey().toBytes();
}
return bitcoin.addressToOutputScript(address: address, network: network);
} catch (err) {
print(err);

View file

@ -1,5 +1,4 @@
import 'dart:convert';
import 'package:bitbox/bitbox.dart' as bitbox;
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:cw_bitcoin/script_hash.dart' as sh;
@ -49,8 +48,6 @@ abstract class BaseBitcoinAddressRecord {
int get hashCode => address.hashCode;
String get cashAddr => bitbox.Address.toCashAddress(address);
BitcoinAddressType type;
String toJSON();
@ -87,9 +84,7 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord {
.firstWhere((type) => type.toString() == decoded['type'] as String)
: SegwitAddresType.p2wpkh,
scriptHash: decoded['scriptHash'] as String?,
network: (decoded['network'] as String?) == null
? network
: BasedUtxoNetwork.fromName(decoded['network'] as String),
network: network,
);
}
@ -111,7 +106,6 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord {
'balance': balance,
'type': type.toString(),
'scriptHash': scriptHash,
'network': network?.value,
});
}

View file

@ -1,4 +1,8 @@
class BitcoinCommitTransactionException implements Exception {
String errorMessage;
BitcoinCommitTransactionException(this.errorMessage);
@override
String toString() => 'Transaction commit is failed.';
}
String toString() => errorMessage;
}

View file

@ -1,4 +0,0 @@
class BitcoinTransactionNoInputsException implements Exception {
@override
String toString() => 'Not enough inputs available. Please select more under Coin Control';
}

View file

@ -1,10 +0,0 @@
import 'package:cw_core/crypto_currency.dart';
class BitcoinTransactionWrongBalanceException implements Exception {
BitcoinTransactionWrongBalanceException(this.currency);
final CryptoCurrency currency;
@override
String toString() => 'You do not have enough ${currency.title} to send this amount.';
}

View file

@ -61,7 +61,8 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
network: network == BitcoinNetwork.testnet ? bitcoin.testnet : bitcoin.bitcoin,
),
);
hasSilentPaymentsScanning = addressPageType == SilentPaymentsAddresType.p2sp.toString();
hasSilentPaymentsScanning = true;
autorun((_) {
this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress;
@ -114,8 +115,10 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
required Box<UnspentCoinsInfo> unspentCoinsInfo,
required String password,
}) async {
final snp = await ElectrumWalletSnapshot.load(name, walletInfo.type, password,
walletInfo.network != null ? BasedUtxoNetwork.fromName(walletInfo.network!) : null);
final network = walletInfo.network != null
? BasedUtxoNetwork.fromName(walletInfo.network!)
: BitcoinNetwork.mainnet;
final snp = await ElectrumWalletSnapshot.load(name, walletInfo.type, password, network);
final seedBytes = await mnemonicToSeedBytes(snp.mnemonic);
return BitcoinWallet(
@ -131,7 +134,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
initialRegularAddressIndex: snp.regularAddressIndex,
initialChangeAddressIndex: snp.changeAddressIndex,
addressPageType: snp.addressPageType,
networkParam: snp.network,
networkParam: network,
);
}
}

View file

@ -7,10 +7,9 @@ import 'package:cw_bitcoin/bitcoin_amount_format.dart';
import 'package:cw_bitcoin/script_hash.dart';
import 'package:flutter/foundation.dart';
import 'package:rxdart/rxdart.dart';
import 'package:http/http.dart' as http;
String jsonrpcparams(List<Object> params) {
final _params = params?.map((val) => '"${val.toString()}"')?.join(',');
final _params = params.map((val) => '"${val.toString()}"').join(',');
return '[$_params]';
}
@ -34,6 +33,7 @@ class ElectrumClient {
: _id = 0,
_isConnected = false,
_tasks = {},
_errors = {},
unterminatedString = '';
static const connectionTimeout = Duration(seconds: 300);
@ -44,6 +44,7 @@ class ElectrumClient {
void Function(bool)? onConnectionStatusChange;
int _id;
final Map<String, SocketTask> _tasks;
final Map<String, String> _errors;
bool _isConnected;
Timer? _aliveTimer;
String unterminatedString;
@ -60,7 +61,7 @@ class ElectrumClient {
await socket?.close();
} catch (_) {}
socket = await Socket.connect(host, port, timeout: connectionTimeout);
socket = await SecureSocket.connect(host, port, timeout: connectionTimeout);
_setIsConnected(true);
socket!.listen((Uint8List event) {
@ -247,30 +248,20 @@ class ElectrumClient {
});
Future<String> broadcastTransaction(
{required String transactionRaw, BasedUtxoNetwork? network}) async {
if (network == BitcoinNetwork.testnet) {
return http
.post(Uri(scheme: 'https', host: 'blockstream.info', path: '/testnet/api/tx'),
headers: <String, String>{'Content-Type': 'application/json; charset=utf-8'},
body: transactionRaw)
.then((http.Response response) {
if (response.statusCode == 200) {
return response.body;
{required String transactionRaw,
BasedUtxoNetwork? network,
Function(int)? idCallback}) async =>
call(
method: 'blockchain.transaction.broadcast',
params: [transactionRaw],
idCallback: idCallback)
.then((dynamic result) {
if (result is String) {
return result;
}
throw Exception('Failed to broadcast transaction: ${response.body}');
return '';
});
}
return call(method: 'blockchain.transaction.broadcast', params: [transactionRaw])
.then((dynamic result) {
if (result is String) {
return result;
}
return '';
});
}
Future<Map<String, dynamic>> getMerkle({required String hash, required int height}) async =>
await call(method: 'blockchain.transaction.get_merkle', params: [hash, height])
@ -387,10 +378,12 @@ class ElectrumClient {
}
}
Future<dynamic> call({required String method, List<Object> params = const []}) async {
Future<dynamic> call(
{required String method, List<Object> params = const [], Function(int)? idCallback}) async {
final completer = Completer<dynamic>();
_id += 1;
final id = _id;
idCallback?.call(id);
_registryTask(id, completer);
socket!.write(jsonrpc(method: method, id: id, params: params));
@ -472,6 +465,23 @@ class ElectrumClient {
final id = response['id'] as String?;
final result = response['result'];
try {
final error = response['error'] as Map<String, dynamic>?;
if (error != null) {
final errorMessage = error['message'] as String?;
if (errorMessage != null) {
_errors[id!] = errorMessage;
}
}
} catch (_) {}
try {
final error = response['error'] as String?;
if (error != null) {
_errors[id!] = error;
}
} catch (_) {}
if (method is String) {
_methodHandler(method: method, request: response);
return;
@ -481,6 +491,8 @@ class ElectrumClient {
_finish(id, result);
}
}
String getErrorMessage(int id) => _errors[id.toString()] ?? '';
}
// FIXME: move me

View file

@ -9,10 +9,9 @@ import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
import 'package:blockchain_utils/blockchain_utils.dart';
import 'package:collection/collection.dart';
import 'package:cw_bitcoin/bitcoin_address_record.dart';
import 'package:cw_bitcoin/bitcoin_amount_format.dart';
import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart';
import 'package:cw_bitcoin/bitcoin_transaction_no_inputs_exception.dart';
import 'package:cw_bitcoin/bitcoin_transaction_priority.dart';
import 'package:cw_bitcoin/bitcoin_transaction_wrong_balance_exception.dart';
import 'package:cw_bitcoin/bitcoin_unspent.dart';
import 'package:cw_bitcoin/bitcoin_wallet_keys.dart';
import 'package:cw_bitcoin/electrum.dart';
@ -20,6 +19,7 @@ import 'package:cw_bitcoin/electrum_balance.dart';
import 'package:cw_bitcoin/electrum_transaction_history.dart';
import 'package:cw_bitcoin/electrum_transaction_info.dart';
import 'package:cw_bitcoin/electrum_wallet_addresses.dart';
import 'package:cw_bitcoin/exceptions.dart';
import 'package:cw_bitcoin/litecoin_network.dart';
import 'package:cw_bitcoin/pending_bitcoin_transaction.dart';
import 'package:cw_bitcoin/script_hash.dart';
@ -83,11 +83,7 @@ abstract class ElectrumWalletBase
}
: {}),
this.unspentCoinsInfo = unspentCoinsInfo,
this.network = networkType == bitcoin.bitcoin
? BitcoinNetwork.mainnet
: networkType == litecoinNetwork
? LitecoinNetwork.mainnet
: BitcoinNetwork.testnet,
this.network = _getNetwork(networkType, currency),
this.isTestnet = networkType == bitcoin.testnet,
super(walletInfo) {
this.electrumClient = electrumClient ?? ElectrumClient();
@ -317,18 +313,21 @@ abstract class ElectrumWalletBase
@override
Future<void> connectToNode({required Node node}) => _electrumConnect(node);
Future<EstimatedTxResult> _estimateTxFeeAndInputsToUse(
int credentialsAmount,
bool sendAll,
List<BitcoinBaseAddress> outputAddresses,
List<BitcoinOutput> outputs,
BitcoinTransactionCredentials transactionCredentials,
{int? inputsCount,
bool? hasSilentPayment}) async {
final utxos = <UtxoWithAddress>[];
int _getDustAmount() {
return 546;
}
var leftAmount = credentialsAmount;
var allInputsAmount = 0;
bool _isBelowDust(int amount) => amount <= _getDustAmount() && network != BitcoinNetwork.testnet;
Future<EstimatedTxResult> estimateSendAllTx(
List<BitcoinOutput> outputs,
int feeRate, {
String? memo,
int credentialsAmount = 0,
bool hasSilentPayment = false,
}) async {
final utxos = <UtxoWithAddress>[];
int allInputsAmount = 0;
List<Outpoint> vinOutpoints = [];
List<ECPrivateInfo> inputPrivKeyInfos = [];
@ -340,27 +339,28 @@ abstract class ElectrumWalletBase
if (utx.isSending) {
allInputsAmount += utx.value;
leftAmount = leftAmount - utx.value;
final address = _addressTypeFromStr(utx.address, network);
final address = addressTypeFromStr(utx.address, network);
ECPrivate? privkey;
bool? isSilentPayment = false;
if (utx.bitcoinAddressRecord is BitcoinSilentPaymentAddressRecord) {
privkey = walletAddresses.silentAddress!.b_spend.tweakAdd(
BigintUtils.fromBytes(BytesUtils.fromHexString(
(utx.bitcoinAddressRecord as BitcoinSilentPaymentAddressRecord)
.silentPaymentTweak!)),
BigintUtils.fromBytes(
BytesUtils.fromHexString(
(utx.bitcoinAddressRecord as BitcoinSilentPaymentAddressRecord).silentPaymentTweak!,
),
),
);
spendsSilentPayment = true;
isSilentPayment = true;
} else {
privkey = generateECPrivate(
hd: utx.bitcoinAddressRecord.isHidden
? walletAddresses.sideHd
: walletAddresses.mainHd,
index: utx.bitcoinAddressRecord.index,
network: network);
hd: utx.bitcoinAddressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd,
index: utx.bitcoinAddressRecord.index,
network: network,
);
}
inputPrivKeyInfos.add(ECPrivateInfo(privkey, address.type == SegwitAddresType.p2tr));
@ -376,15 +376,12 @@ abstract class ElectrumWalletBase
scriptType: _getScriptType(address),
isSilentPayment: isSilentPayment,
),
ownerDetails:
UtxoAddressDetails(publicKey: privkey.getPublic().toHex(), address: address),
ownerDetails: UtxoAddressDetails(
publicKey: privkey.getPublic().toHex(),
address: address,
),
),
);
bool amountIsAcquired = !sendAll && leftAmount <= 0;
if ((inputsCount == null && amountIsAcquired) || inputsCount == i + 1) {
break;
}
}
}
@ -392,15 +389,21 @@ abstract class ElectrumWalletBase
throw BitcoinTransactionNoInputsException();
}
var changeValue = allInputsAmount - credentialsAmount;
if (!sendAll) {
if (changeValue > 0) {
final changeAddress = await walletAddresses.getChangeAddress();
final address = _addressTypeFromStr(changeAddress, network);
outputAddresses.add(address);
outputs.add(BitcoinOutput(address: address, value: BigInt.from(changeValue)));
}
int estimatedSize;
if (network is BitcoinCashNetwork) {
estimatedSize = ForkedTransactionBuilder.estimateTransactionSize(
utxos: utxos,
outputs: outputs,
network: network as BitcoinCashNetwork,
memo: memo,
);
} else {
estimatedSize = BitcoinTransactionBuilder.estimateTransactionSize(
utxos: utxos,
outputs: outputs,
network: network,
memo: memo,
);
}
if (hasSilentPayment == true) {
@ -442,119 +445,322 @@ abstract class ElectrumWalletBase
}
}
final estimatedSize = BitcoinTransactionBuilder.estimateTransactionSize(
utxos: utxos, outputs: outputs, network: network);
final fee = transactionCredentials.feeRate != null
? feeAmountWithFeeRate(transactionCredentials.feeRate!, 0, 0, size: estimatedSize)
: feeAmountForPriority(transactionCredentials.priority!, 0, 0, size: estimatedSize);
int fee = feeAmountWithFeeRate(feeRate, 0, 0, size: estimatedSize);
if (fee == 0) {
throw BitcoinTransactionWrongBalanceException(currency);
throw BitcoinTransactionNoFeeException();
}
var amount = credentialsAmount;
// Here, when sending all, the output amount equals to the input value - fee to fully spend every input on the transaction and have no amount left for change
int amount = allInputsAmount - fee;
final lastOutput = outputs.last;
if (!sendAll) {
if (changeValue > fee) {
// Here, lastOutput is change, deduct the fee from it
outputs[outputs.length - 1] =
BitcoinOutput(address: lastOutput.address, value: lastOutput.value - BigInt.from(fee));
// Attempting to send less than the dust limit
if (_isBelowDust(amount)) {
throw BitcoinTransactionNoDustException();
}
if (credentialsAmount > 0) {
final amountLeftForFee = amount - credentialsAmount;
if (amountLeftForFee > 0 && _isBelowDust(amountLeftForFee)) {
amount -= amountLeftForFee;
fee += amountLeftForFee;
}
}
outputs[outputs.length - 1] =
BitcoinOutput(address: outputs.last.address, value: BigInt.from(amount));
return EstimatedTxResult(
utxos: utxos,
inputPrivKeyInfos: inputPrivKeyInfos,
fee: fee,
amount: amount,
isSendAll: true,
hasChange: false,
memo: memo,
spendsSilentPayment: spendsSilentPayment,
);
}
Future<EstimatedTxResult> estimateTxForAmount(
int credentialsAmount,
List<BitcoinOutput> outputs,
int feeRate, {
int? inputsCount,
String? memo,
bool hasSilentPayment = false,
}) async {
final utxos = <UtxoWithAddress>[];
List<ECPrivateInfo> inputPrivKeyInfos = [];
int allInputsAmount = 0;
bool spendsSilentPayment = false;
int leftAmount = credentialsAmount;
final sendingCoins = unspentCoins.where((utx) => utx.isSending).toList();
for (int i = 0; i < sendingCoins.length; i++) {
final utx = sendingCoins[i];
allInputsAmount += utx.value;
leftAmount = leftAmount - utx.value;
final address = addressTypeFromStr(utx.address, network);
ECPrivate? privkey;
bool? isSilentPayment = false;
if (utx.bitcoinAddressRecord is BitcoinSilentPaymentAddressRecord) {
privkey = walletAddresses.silentAddress!.b_spend.tweakAdd(
BigintUtils.fromBytes(
BytesUtils.fromHexString(
(utx.bitcoinAddressRecord as BitcoinSilentPaymentAddressRecord).silentPaymentTweak!,
),
),
);
spendsSilentPayment = true;
isSilentPayment = true;
} else {
privkey = generateECPrivate(
hd: utx.bitcoinAddressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd,
index: utx.bitcoinAddressRecord.index,
network: network,
);
}
utxos.add(
UtxoWithAddress(
utxo: BitcoinUtxo(
txHash: utx.hash,
value: BigInt.from(utx.value),
vout: utx.vout,
scriptType: _getScriptType(address),
isSilentPayment: isSilentPayment,
),
ownerDetails: UtxoAddressDetails(
publicKey: privkey.getPublic().toHex(),
address: address,
),
),
);
bool amountIsAcquired = leftAmount <= 0;
if ((inputsCount == null && amountIsAcquired) || inputsCount == i + 1) {
break;
}
}
if (utxos.isEmpty) {
throw BitcoinTransactionNoInputsException();
}
final spendingAllCoins = sendingCoins.length == utxos.length;
// How much is being spent - how much is being sent
int amountLeftForChangeAndFee = allInputsAmount - credentialsAmount;
if (amountLeftForChangeAndFee <= 0) {
throw BitcoinTransactionWrongBalanceException();
}
final changeAddress = await walletAddresses.getChangeAddress();
final address = addressTypeFromStr(changeAddress, network);
outputs.add(BitcoinOutput(
address: address,
value: BigInt.from(amountLeftForChangeAndFee),
));
int estimatedSize;
if (network is BitcoinCashNetwork) {
estimatedSize = ForkedTransactionBuilder.estimateTransactionSize(
utxos: utxos,
outputs: outputs,
network: network as BitcoinCashNetwork,
memo: memo,
);
} else {
// Here, if sendAll, the output amount equals to the input value - fee to fully spend every input on the transaction and have no amount for change
amount = allInputsAmount - fee;
estimatedSize = BitcoinTransactionBuilder.estimateTransactionSize(
utxos: utxos,
outputs: outputs,
network: network,
memo: memo,
);
}
int fee = feeAmountWithFeeRate(feeRate, 0, 0, size: estimatedSize);
if (fee == 0) {
throw BitcoinTransactionNoFeeException();
}
int amount = credentialsAmount;
final lastOutput = outputs.last;
final amountLeftForChange = amountLeftForChangeAndFee - fee;
if (!_isBelowDust(amountLeftForChange)) {
// Here, lastOutput already is change, return the amount left without the fee to the user's address.
outputs[outputs.length - 1] =
BitcoinOutput(address: lastOutput.address, value: BigInt.from(amount));
BitcoinOutput(address: lastOutput.address, value: BigInt.from(amountLeftForChange));
} else {
// If has change that is lower than dust, will end up with tx rejected by network rules, so estimate again without the added change
outputs.removeLast();
// Still has inputs to spend before failing
if (!spendingAllCoins) {
return estimateTxForAmount(
credentialsAmount,
outputs,
feeRate,
inputsCount: utxos.length + 1,
memo: memo,
);
}
final estimatedSendAll = await estimateSendAllTx(
outputs,
feeRate,
memo: memo,
);
if (estimatedSendAll.amount == credentialsAmount) {
return estimatedSendAll;
}
// Estimate to user how much is needed to send to cover the fee
final maxAmountWithReturningChange = allInputsAmount - _getDustAmount() - fee - 1;
throw BitcoinTransactionNoDustOnChangeException(
bitcoinAmountToString(amount: maxAmountWithReturningChange),
bitcoinAmountToString(amount: estimatedSendAll.amount),
);
}
// Attempting to send less than the dust limit
if (_isBelowDust(amount)) {
throw BitcoinTransactionNoDustException();
}
final totalAmount = amount + fee;
if (totalAmount > balance[currency]!.confirmed) {
throw BitcoinTransactionWrongBalanceException(currency);
throw BitcoinTransactionWrongBalanceException();
}
if (totalAmount > allInputsAmount) {
if (unspentCoins.where((utx) => utx.isSending).length == utxos.length) {
throw BitcoinTransactionWrongBalanceException(currency);
if (spendingAllCoins) {
throw BitcoinTransactionWrongBalanceException();
} else {
if (changeValue > fee) {
outputAddresses.removeLast();
if (amountLeftForChangeAndFee > fee) {
outputs.removeLast();
}
return _estimateTxFeeAndInputsToUse(
credentialsAmount, sendAll, outputAddresses, outputs, transactionCredentials,
inputsCount: utxos.length + 1, hasSilentPayment: hasSilentPayment);
return estimateTxForAmount(
credentialsAmount,
outputs,
feeRate,
inputsCount: utxos.length + 1,
memo: memo,
hasSilentPayment: hasSilentPayment,
);
}
}
return EstimatedTxResult(
utxos: utxos,
inputPrivKeyInfos: inputPrivKeyInfos,
fee: fee,
amount: amount,
spendsSilentPayment: spendsSilentPayment);
utxos: utxos,
inputPrivKeyInfos: inputPrivKeyInfos,
fee: fee,
amount: amount,
hasChange: true,
isSendAll: false,
memo: memo,
spendsSilentPayment: spendsSilentPayment,
);
}
@override
Future<PendingTransaction> createTransaction(Object credentials) async {
try {
final outputs = <BitcoinOutput>[];
final outputAddresses = <BitcoinBaseAddress>[];
final transactionCredentials = credentials as BitcoinTransactionCredentials;
final hasMultiDestination = transactionCredentials.outputs.length > 1;
final sendAll = !hasMultiDestination && transactionCredentials.outputs.first.sendAll;
final memo = transactionCredentials.outputs.first.memo;
var credentialsAmount = 0;
int credentialsAmount = 0;
bool hasSilentPayment = false;
for (final out in transactionCredentials.outputs) {
final outputAddress = out.isParsedAddress ? out.extractedAddress! : out.address;
final address = _addressTypeFromStr(outputAddress, network);
final outputAmount = out.formattedCryptoAmount!;
if (!sendAll && _isBelowDust(outputAmount)) {
throw BitcoinTransactionNoDustException();
}
if (hasMultiDestination) {
if (out.sendAll) {
throw BitcoinTransactionWrongBalanceException();
}
}
credentialsAmount += outputAmount;
final address =
addressTypeFromStr(out.isParsedAddress ? out.extractedAddress! : out.address, network);
if (address is SilentPaymentAddress) {
hasSilentPayment = true;
}
outputAddresses.add(address);
if (hasMultiDestination) {
if (out.sendAll || out.formattedCryptoAmount! <= 0) {
throw BitcoinTransactionWrongBalanceException(currency);
}
final outputAmount = out.formattedCryptoAmount!;
credentialsAmount += outputAmount;
outputs.add(BitcoinOutput(address: address, value: BigInt.from(outputAmount)));
if (sendAll) {
// The value will be changed after estimating the Tx size and deducting the fee from the total to be sent
outputs.add(BitcoinOutput(address: address, value: BigInt.from(0)));
} else {
if (!sendAll) {
final outputAmount = out.formattedCryptoAmount!;
credentialsAmount += outputAmount;
outputs.add(BitcoinOutput(address: address, value: BigInt.from(outputAmount)));
} else {
// The value will be changed after estimating the Tx size and deducting the fee from the total
outputs.add(BitcoinOutput(address: address, value: BigInt.from(0)));
}
outputs.add(BitcoinOutput(address: address, value: BigInt.from(outputAmount)));
}
}
final estimatedTx = await _estimateTxFeeAndInputsToUse(
credentialsAmount,
sendAll,
outputAddresses,
outputs,
transactionCredentials,
hasSilentPayment: hasSilentPayment,
);
final feeRateInt = transactionCredentials.feeRate != null
? transactionCredentials.feeRate!
: feeRate(transactionCredentials.priority!);
final txb = BitcoinTransactionBuilder(
EstimatedTxResult estimatedTx;
if (sendAll) {
estimatedTx = await estimateSendAllTx(
outputs,
feeRateInt,
memo: memo,
credentialsAmount: credentialsAmount,
hasSilentPayment: hasSilentPayment,
);
} else {
estimatedTx = await estimateTxForAmount(
credentialsAmount,
outputs,
feeRateInt,
memo: memo,
hasSilentPayment: hasSilentPayment,
);
}
BasedBitcoinTransacationBuilder txb;
if (network is BitcoinCashNetwork) {
txb = ForkedTransactionBuilder(
utxos: estimatedTx.utxos,
outputs: outputs,
fee: BigInt.from(estimatedTx.fee),
network: network);
network: network,
memo: estimatedTx.memo,
outputOrdering: BitcoinOrdering.none,
);
} else {
txb = BitcoinTransactionBuilder(
utxos: estimatedTx.utxos,
outputs: outputs,
fee: BigInt.from(estimatedTx.fee),
network: network,
memo: estimatedTx.memo,
outputOrdering: BitcoinOrdering.none,
);
}
bool hasTaprootInputs = false;
final transaction = txb.buildTransaction((txDigest, utxo, publicKey, sighash) {
final key = estimatedTx.inputPrivKeyInfos
@ -565,6 +771,7 @@ abstract class ElectrumWalletBase
}
if (utxo.utxo.isP2tr()) {
hasTaprootInputs = true;
return key.privkey.signTapRoot(
txDigest,
sighash: sighash,
@ -575,12 +782,18 @@ abstract class ElectrumWalletBase
}
});
return PendingBitcoinTransaction(transaction, type,
electrumClient: electrumClient,
amount: estimatedTx.amount,
fee: estimatedTx.fee,
network: network)
..addListener((transaction) async {
return PendingBitcoinTransaction(
transaction,
type,
electrumClient: electrumClient,
amount: estimatedTx.amount,
fee: estimatedTx.fee,
feeRate: feeRateInt.toString(),
network: network,
hasChange: estimatedTx.hasChange,
isSendAll: estimatedTx.isSendAll,
hasTaprootInputs: hasTaprootInputs,
)..addListener((transaction) async {
transactionHistory.addOne(transaction);
if (estimatedTx.spendsSilentPayment) {
transactionHistory.transactions.values.forEach((tx) {
@ -608,7 +821,6 @@ abstract class ElectrumWalletBase
'balance': balance[currency]?.toJSON(),
'silent_addresses': walletAddresses.silentAddresses.map((addr) => addr.toJSON()).toList(),
'silent_address_index': walletAddresses.currentSilentAddressIndex.toString(),
'network_type': network == BitcoinNetwork.testnet ? 'testnet' : 'mainnet',
});
int feeRate(TransactionPriority priority) {
@ -623,7 +835,7 @@ abstract class ElectrumWalletBase
}
}
int feeAmountForPriority(BitcoinTransactionPriority priority, int inputsCount, int outputsCount,
int feeAmountForPriority(TransactionPriority priority, int inputsCount, int outputsCount,
{int? size}) =>
feeRate(priority) * (size ?? estimatedTransactionSize(inputsCount, outputsCount));
@ -731,12 +943,14 @@ abstract class ElectrumWalletBase
Future<void> updateUnspent() async {
List<BitcoinUnspent> updatedUnspentCoins = [];
// Update unspents stored from scanned silent payment transactions
transactionHistory.transactions.values.forEach((tx) {
if (tx.unspents != null) {
updatedUnspentCoins.addAll(tx.unspents!);
}
});
if (hasSilentPaymentsScanning) {
// Update unspents stored from scanned silent payment transactions
transactionHistory.transactions.values.forEach((tx) {
if (tx.unspents != null) {
updatedUnspentCoins.addAll(tx.unspents!);
}
});
}
final addressesSet = walletAddresses.allAddresses.map((addr) => addr.address).toSet();
@ -1038,20 +1252,22 @@ abstract class ElectrumWalletBase
var totalConfirmed = 0;
var totalUnconfirmed = 0;
// Add values from unspent coins that are not fetched by the address list
// i.e. scanned silent payments
unspentCoinsInfo.values.forEach((info) {
unspentCoins.forEach((element) {
if (element.hash == info.hash &&
element.bitcoinAddressRecord.address == info.address &&
element.value == info.value) {
if (info.isFrozen) totalFrozen += element.value;
if (element.bitcoinAddressRecord is BitcoinSilentPaymentAddressRecord) {
totalConfirmed += element.value;
if (hasSilentPaymentsScanning) {
// Add values from unspent coins that are not fetched by the address list
// i.e. scanned silent payments
unspentCoinsInfo.values.forEach((info) {
unspentCoins.forEach((element) {
if (element.hash == info.hash &&
element.bitcoinAddressRecord.address == info.address &&
element.value == info.value) {
if (info.isFrozen) totalFrozen += element.value;
if (element.bitcoinAddressRecord is BitcoinSilentPaymentAddressRecord) {
totalConfirmed += element.value;
}
}
}
});
});
});
}
final balances = await Future.wait(balanceFutures);
@ -1075,17 +1291,20 @@ abstract class ElectrumWalletBase
Future<void> updateBalance() async {
balance[currency] = await _fetchBalances();
// Update balance stored from scanned silent payment transactions
try {
transactionHistory.transactions.values.forEach((tx) {
if (tx.unspents != null) {
balance[currency]!.confirmed += tx.unspents!
.where((unspent) => unspent.bitcoinAddressRecord is BitcoinSilentPaymentAddressRecord)
.map((e) => e.value)
.reduce((value, element) => value + element);
}
});
} catch (_) {}
if (hasSilentPaymentsScanning) {
// Update balance stored from scanned silent payment transactions
try {
transactionHistory.transactions.values.forEach((tx) {
if (tx.unspents != null) {
balance[currency]!.confirmed += tx.unspents!
.where(
(unspent) => unspent.bitcoinAddressRecord is BitcoinSilentPaymentAddressRecord)
.map((e) => e.value)
.reduce((value, element) => value + element);
}
});
} catch (_) {}
}
await save();
}
@ -1118,6 +1337,22 @@ abstract class ElectrumWalletBase
currentChainTip = await electrumClient.getCurrentBlockChainTip();
if (currentChainTip != null) walletInfo.restoreHeight = currentChainTip!;
}
static BasedUtxoNetwork _getNetwork(bitcoin.NetworkType networkType, CryptoCurrency? currency) {
if (networkType == bitcoin.bitcoin && currency == CryptoCurrency.bch) {
return BitcoinCashNetwork.mainnet;
}
if (networkType == litecoinNetwork) {
return LitecoinNetwork.mainnet;
}
if (networkType == bitcoin.testnet) {
return BitcoinNetwork.testnet;
}
return BitcoinNetwork.mainnet;
}
}
class ScanData {
@ -1347,21 +1582,37 @@ Future<void> startRefresh(ScanData scanData) async {
}
class EstimatedTxResult {
EstimatedTxResult(
{required this.utxos,
required this.inputPrivKeyInfos,
required this.fee,
required this.amount,
required this.spendsSilentPayment});
EstimatedTxResult({
required this.utxos,
required this.inputPrivKeyInfos,
required this.fee,
required this.amount,
required this.hasChange,
required this.isSendAll,
this.memo,
required this.spendsSilentPayment,
});
final List<UtxoWithAddress> utxos;
final List<ECPrivateInfo> inputPrivKeyInfos;
final int fee;
final int amount;
final bool spendsSilentPayment;
final bool hasChange;
final bool isSendAll;
final String? memo;
}
BitcoinBaseAddress _addressTypeFromStr(String address, BasedUtxoNetwork network) {
BitcoinBaseAddress addressTypeFromStr(String address, BasedUtxoNetwork network) {
if (network is BitcoinCashNetwork) {
if (!address.startsWith("bitcoincash:") &&
(address.startsWith("q") || address.startsWith("p"))) {
address = "bitcoincash:$address";
}
return BitcoinCashAddress(address).baseAddress;
}
if (P2pkhAddress.regex.hasMatch(address)) {
return P2pkhAddress.fromAddress(address: address, network: network);
} else if (P2shAddress.regex.hasMatch(address)) {

View file

@ -1,6 +1,5 @@
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
import 'package:bitbox/bitbox.dart' as bitbox;
import 'package:blockchain_utils/blockchain_utils.dart';
import 'package:cw_bitcoin/bitcoin_address_record.dart';
import 'package:cw_core/wallet_addresses.dart';
@ -32,6 +31,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
List<BitcoinSilentPaymentAddressRecord>? initialSilentAddresses,
int initialSilentAddressIndex = 1,
bitcoin.HDWallet? masterHd,
BitcoinAddressType? initialAddressPageType,
}) : _addresses = ObservableList<BitcoinAddressRecord>.of((initialAddresses ?? []).toSet()),
addressesByReceiveType =
ObservableList<BaseBitcoinAddressRecord>.of((<BitcoinAddressRecord>[]).toSet()),
@ -43,9 +43,10 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
.toSet()),
currentReceiveAddressIndexByType = initialRegularAddressIndex ?? {},
currentChangeAddressIndexByType = initialChangeAddressIndex ?? {},
_addressPageType = walletInfo.addressPageType != null
? BitcoinAddressType.fromValue(walletInfo.addressPageType!)
: SegwitAddresType.p2wpkh,
_addressPageType = initialAddressPageType ??
(walletInfo.addressPageType != null
? BitcoinAddressType.fromValue(walletInfo.addressPageType!)
: SegwitAddresType.p2wpkh),
silentAddresses = ObservableList<BitcoinSilentPaymentAddressRecord>.of(
(initialSilentAddresses ?? []).toSet()),
currentSilentAddressIndex = initialSilentAddressIndex,
@ -75,9 +76,6 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
static const defaultChangeAddressesCount = 17;
static const gap = 20;
static String toCashAddr(String address) => bitbox.Address.toCashAddress(address);
static String toLegacy(String address) => bitbox.Address.toLegacyAddress(address);
final ObservableList<BitcoinAddressRecord> _addresses;
late ObservableList<BaseBitcoinAddressRecord> addressesByReceiveType;
final ObservableList<BitcoinAddressRecord> receiveAddresses;
@ -91,7 +89,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
SilentPaymentOwner? silentAddress;
@observable
BitcoinAddressType _addressPageType = SegwitAddresType.p2wpkh;
late BitcoinAddressType _addressPageType;
@computed
BitcoinAddressType get addressPageType => _addressPageType;
@ -115,7 +113,8 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
String receiveAddress;
final typeMatchingReceiveAddresses = receiveAddresses.where(_isAddressPageTypeMatch);
final typeMatchingReceiveAddresses =
receiveAddresses.where(_isAddressPageTypeMatch).where((addr) => !addr.isUsed);
if ((isEnabledAutoGenerateSubaddress && receiveAddresses.isEmpty) ||
typeMatchingReceiveAddresses.isEmpty) {
@ -132,7 +131,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
}
}
return walletInfo.type == WalletType.bitcoinCash ? toCashAddr(receiveAddress) : receiveAddress;
return receiveAddress;
}
@observable
@ -152,9 +151,6 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
return;
}
if (addr.startsWith('bitcoincash:')) {
addr = toLegacy(addr);
}
final addressRecord = _addresses.firstWhere((addressRecord) => addressRecord.address == addr);
previousAddressRecord = addressRecord;
@ -204,11 +200,17 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
@override
Future<void> init() async {
await _generateInitialAddresses();
await _generateInitialAddresses(type: P2pkhAddressType.p2pkh);
await _generateInitialAddresses(type: P2shAddressType.p2wpkhInP2sh);
await _generateInitialAddresses(type: SegwitAddresType.p2tr);
await _generateInitialAddresses(type: SegwitAddresType.p2wsh);
if (walletInfo.type == WalletType.bitcoinCash) {
await _generateInitialAddresses(type: P2pkhAddressType.p2pkh);
} else if (walletInfo.type == WalletType.litecoin) {
await _generateInitialAddresses();
} else if (walletInfo.type == WalletType.bitcoin) {
await _generateInitialAddresses();
await _generateInitialAddresses(type: P2pkhAddressType.p2pkh);
await _generateInitialAddresses(type: P2shAddressType.p2wpkhInP2sh);
await _generateInitialAddresses(type: SegwitAddresType.p2tr);
await _generateInitialAddresses(type: SegwitAddresType.p2wsh);
}
updateAddressesByMatch();
updateReceiveAddresses();
updateChangeAddresses();
@ -308,6 +310,11 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
try {
addressesMap.clear();
addressesMap[address] = '';
allAddressesMap.clear();
_addresses.forEach((addressRecord) {
allAddressesMap[addressRecord.address] = addressRecord.name;
});
await saveAddressesInBox();
} catch (e) {
print(e.toString());
@ -316,9 +323,6 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
@action
void updateAddress(String address, String label) {
if (address.startsWith('bitcoincash:')) {
address = toLegacy(address);
}
final addressRecord =
_addresses.firstWhere((addressRecord) => addressRecord.address == address);
addressRecord.setNewName(label);
@ -354,7 +358,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
addressRecord.isHidden &&
!addressRecord.isUsed &&
// TODO: feature to change change address type. For now fixed to p2wpkh, the cheapest type
addressRecord.type == SegwitAddresType.p2wpkh);
(walletInfo.type != WalletType.bitcoin || addressRecord.type == SegwitAddresType.p2wpkh));
changeAddresses.addAll(newAddresses);
}

View file

@ -18,15 +18,13 @@ class ElectrumWalletSnapshot {
required this.regularAddressIndex,
required this.changeAddressIndex,
required this.addressPageType,
required this.network,
required this.silentAddressIndex,
});
final String name;
final String password;
final WalletType type;
final String addressPageType;
final BasedUtxoNetwork network;
final String? addressPageType;
String mnemonic;
List<BitcoinAddressRecord> addresses;
@ -37,7 +35,7 @@ class ElectrumWalletSnapshot {
int silentAddressIndex;
static Future<ElectrumWalletSnapshot> load(
String name, WalletType type, String password, BasedUtxoNetwork? network) async {
String name, WalletType type, String password, BasedUtxoNetwork network) async {
final path = await pathForWallet(name: name, type: type);
final jsonSource = await read(path: path, password: password);
final data = json.decode(jsonSource) as Map;
@ -87,8 +85,7 @@ class ElectrumWalletSnapshot {
balance: balance,
regularAddressIndex: regularAddressIndexByType,
changeAddressIndex: changeAddressIndexByType,
addressPageType: data['address_page_type'] as String? ?? SegwitAddresType.p2wpkh.toString(),
network: data['network_type'] == 'testnet' ? BitcoinNetwork.testnet : BitcoinNetwork.mainnet,
addressPageType: data['address_page_type'] as String?,
silentAddressIndex: silentAddressIndex,
);
}

View file

@ -0,0 +1,27 @@
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/exceptions.dart';
class BitcoinTransactionWrongBalanceException extends TransactionWrongBalanceException {
BitcoinTransactionWrongBalanceException() : super(CryptoCurrency.btc);
}
class BitcoinTransactionNoInputsException extends TransactionNoInputsException {}
class BitcoinTransactionNoFeeException extends TransactionNoFeeException {}
class BitcoinTransactionNoDustException extends TransactionNoDustException {}
class BitcoinTransactionNoDustOnChangeException extends TransactionNoDustOnChangeException {
BitcoinTransactionNoDustOnChangeException(super.max, super.min);
}
class BitcoinTransactionCommitFailed extends TransactionCommitFailed {}
class BitcoinTransactionCommitFailedDustChange extends TransactionCommitFailedDustChange {}
class BitcoinTransactionCommitFailedDustOutput extends TransactionCommitFailedDustOutput {}
class BitcoinTransactionCommitFailedDustOutputSendAll
extends TransactionCommitFailedDustOutputSendAll {}
class BitcoinTransactionCommitFailedVoutNegative extends TransactionCommitFailedVoutNegative {}

View file

@ -1,4 +1,4 @@
import 'package:cw_bitcoin/bitcoin_commit_transaction_exception.dart';
import 'package:cw_bitcoin/exceptions.dart';
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:cw_core/pending_transaction.dart';
import 'package:cw_bitcoin/electrum.dart';
@ -8,16 +8,29 @@ import 'package:cw_core/transaction_direction.dart';
import 'package:cw_core/wallet_type.dart';
class PendingBitcoinTransaction with PendingTransaction {
PendingBitcoinTransaction(this._tx, this.type,
{required this.electrumClient, required this.amount, required this.fee, this.network})
: _listeners = <void Function(ElectrumTransactionInfo transaction)>[];
PendingBitcoinTransaction(
this._tx,
this.type, {
required this.electrumClient,
required this.amount,
required this.fee,
required this.feeRate,
this.network,
required this.hasChange,
required this.isSendAll,
this.hasTaprootInputs = false,
}) : _listeners = <void Function(ElectrumTransactionInfo transaction)>[];
final WalletType type;
final BtcTransaction _tx;
final ElectrumClient electrumClient;
final int amount;
final int fee;
final String feeRate;
final BasedUtxoNetwork? network;
final bool hasChange;
final bool isSendAll;
final bool hasTaprootInputs;
@override
String get id => _tx.txId();
@ -31,14 +44,37 @@ class PendingBitcoinTransaction with PendingTransaction {
@override
String get feeFormatted => bitcoinAmountToString(amount: fee);
@override
int? get outputCount => _tx.outputs.length;
final List<void Function(ElectrumTransactionInfo transaction)> _listeners;
@override
Future<void> commit() async {
final result = await electrumClient.broadcastTransaction(transactionRaw: hex, network: network);
int? callId;
final result = await electrumClient.broadcastTransaction(
transactionRaw: hex, network: network, idCallback: (id) => callId = id);
if (result.isEmpty) {
throw BitcoinCommitTransactionException();
if (callId != null) {
final error = electrumClient.getErrorMessage(callId!);
if (error.contains("dust")) {
if (hasChange) {
throw BitcoinTransactionCommitFailedDustChange();
} else if (!isSendAll) {
throw BitcoinTransactionCommitFailedDustOutput();
} else {
throw BitcoinTransactionCommitFailedDustOutputSendAll();
}
}
if (error.contains("bad-txns-vout-negative")) {
throw BitcoinTransactionCommitFailedVoutNegative();
}
}
throw BitcoinTransactionCommitFailed();
}
_listeners.forEach((listener) => listener(transactionInfo()));

View file

@ -1,8 +1,9 @@
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:crypto/crypto.dart';
import 'package:cw_bitcoin/address_to_output_script.dart';
import 'package:bitcoin_base/bitcoin_base.dart' as bitcoin;
String scriptHash(String address, {required BasedUtxoNetwork network}) {
final outputScript = addressToOutputScript(address: address, network: network);
String scriptHash(String address, {required bitcoin.BasedUtxoNetwork network}) {
final outputScript = addressToOutputScript(address, network);
final parts = sha256.convert(outputScript).toString().split('');
var res = '';

View file

@ -79,7 +79,7 @@ packages:
dependency: "direct main"
description:
path: "."
ref: cake-update-v2
ref: cake-update-v3
resolved-ref: fb4c0a0b6cf24628ddad7d3cdc58e4c918eff714
url: "https://github.com/cake-tech/bitcoin_base"
source: git

View file

@ -33,7 +33,7 @@ dependencies:
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

View file

@ -4,15 +4,10 @@ import 'package:bitbox/bitbox.dart' as bitbox;
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
import 'package:cw_bitcoin/bitcoin_address_record.dart';
import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart';
import 'package:cw_bitcoin/bitcoin_transaction_no_inputs_exception.dart';
import 'package:cw_bitcoin/bitcoin_transaction_priority.dart';
import 'package:cw_bitcoin/bitcoin_transaction_wrong_balance_exception.dart';
import 'package:cw_bitcoin/bitcoin_unspent.dart';
import 'package:cw_bitcoin/electrum_balance.dart';
import 'package:cw_bitcoin/electrum_wallet.dart';
import 'package:cw_bitcoin/electrum_wallet_snapshot.dart';
import 'package:cw_bitcoin_cash/src/pending_bitcoin_cash_transaction.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/transaction_priority.dart';
import 'package:cw_core/unspent_coins_info.dart';
@ -34,7 +29,7 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
required WalletInfo walletInfo,
required Box<UnspentCoinsInfo> unspentCoinsInfo,
required Uint8List seedBytes,
String? addressPageType,
BitcoinAddressType? addressPageType,
List<BitcoinAddressRecord>? initialAddresses,
ElectrumBalance? initialBalance,
Map<String, int>? initialRegularAddressIndex,
@ -57,6 +52,7 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
mainHd: hd,
sideHd: bitcoin.HDWallet.fromSeed(seedBytes).derivePath("m/44'/145'/0'/1"),
network: network,
initialAddressPageType: addressPageType,
);
autorun((_) {
this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress;
@ -83,7 +79,7 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
seedBytes: await Mnemonic.toSeed(mnemonic),
initialRegularAddressIndex: initialRegularAddressIndex,
initialChangeAddressIndex: initialChangeAddressIndex,
addressPageType: addressPageType,
addressPageType: P2pkhAddressType.p2pkh,
);
}
@ -100,193 +96,37 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
password: password,
walletInfo: walletInfo,
unspentCoinsInfo: unspentCoinsInfo,
initialAddresses: snp.addresses,
initialAddresses: snp.addresses.map((addr) {
try {
BitcoinCashAddress(addr.address);
return BitcoinAddressRecord(
addr.address,
index: addr.index,
isHidden: addr.isHidden,
type: P2pkhAddressType.p2pkh,
network: BitcoinCashNetwork.mainnet,
);
} catch (_) {
return BitcoinAddressRecord(
AddressUtils.getCashAddrFormat(addr.address),
index: addr.index,
isHidden: addr.isHidden,
type: P2pkhAddressType.p2pkh,
network: BitcoinCashNetwork.mainnet,
);
}
}).toList(),
initialBalance: snp.balance,
seedBytes: await Mnemonic.toSeed(snp.mnemonic),
initialRegularAddressIndex: snp.regularAddressIndex,
initialChangeAddressIndex: snp.changeAddressIndex,
addressPageType: snp.addressPageType,
addressPageType: P2pkhAddressType.p2pkh,
);
}
@override
Future<PendingBitcoinCashTransaction> createTransaction(Object credentials) async {
const minAmount = 546;
final transactionCredentials = credentials as BitcoinTransactionCredentials;
final inputs = <BitcoinUnspent>[];
final outputs = transactionCredentials.outputs;
final hasMultiDestination = outputs.length > 1;
var allInputsAmount = 0;
if (unspentCoins.isEmpty) await updateUnspent();
for (final utx in unspentCoins) {
if (utx.isSending) {
allInputsAmount += utx.value;
inputs.add(utx);
}
}
if (inputs.isEmpty) throw BitcoinTransactionNoInputsException();
final allAmountFee = transactionCredentials.feeRate != null
? feeAmountWithFeeRate(transactionCredentials.feeRate!, inputs.length, outputs.length)
: feeAmountForPriority(transactionCredentials.priority!, inputs.length, outputs.length);
final allAmount = allInputsAmount - allAmountFee;
var credentialsAmount = 0;
var amount = 0;
var fee = 0;
if (hasMultiDestination) {
if (outputs.any((item) => item.sendAll || item.formattedCryptoAmount! <= 0)) {
throw BitcoinTransactionWrongBalanceException(currency);
}
credentialsAmount = outputs.fold(0, (acc, value) => acc + value.formattedCryptoAmount!);
if (allAmount - credentialsAmount < minAmount) {
throw BitcoinTransactionWrongBalanceException(currency);
}
amount = credentialsAmount;
if (transactionCredentials.feeRate != null) {
fee = calculateEstimatedFeeWithFeeRate(transactionCredentials.feeRate!, amount,
outputsCount: outputs.length + 1);
} else {
fee = calculateEstimatedFee(transactionCredentials.priority, amount,
outputsCount: outputs.length + 1);
}
} else {
final output = outputs.first;
credentialsAmount = !output.sendAll ? output.formattedCryptoAmount! : 0;
if (credentialsAmount > allAmount) {
throw BitcoinTransactionWrongBalanceException(currency);
}
amount = output.sendAll || allAmount - credentialsAmount < minAmount
? allAmount
: credentialsAmount;
if (output.sendAll || amount == allAmount) {
fee = allAmountFee;
} else if (transactionCredentials.feeRate != null) {
fee = calculateEstimatedFeeWithFeeRate(transactionCredentials.feeRate!, amount);
} else {
fee = calculateEstimatedFee(transactionCredentials.priority, amount);
}
}
if (fee == 0) {
throw BitcoinTransactionWrongBalanceException(currency);
}
final totalAmount = amount + fee;
if (totalAmount > balance[currency]!.confirmed || totalAmount > allInputsAmount) {
throw BitcoinTransactionWrongBalanceException(currency);
}
final txb = bitbox.Bitbox.transactionBuilder(testnet: false);
final changeAddress = await walletAddresses.getChangeAddress();
var leftAmount = totalAmount;
var totalInputAmount = 0;
inputs.clear();
for (final utx in unspentCoins) {
if (utx.isSending) {
leftAmount = leftAmount - utx.value;
totalInputAmount += utx.value;
inputs.add(utx);
if (leftAmount <= 0) {
break;
}
}
}
if (inputs.isEmpty) throw BitcoinTransactionNoInputsException();
if (amount <= 0 || totalInputAmount < totalAmount) {
throw BitcoinTransactionWrongBalanceException(currency);
}
inputs.forEach((input) {
txb.addInput(input.hash, input.vout);
});
final String bchPrefix = "bitcoincash:";
outputs.forEach((item) {
final outputAmount = hasMultiDestination ? item.formattedCryptoAmount : amount;
String outputAddress = item.isParsedAddress ? item.extractedAddress! : item.address;
if (!outputAddress.startsWith(bchPrefix)) {
outputAddress = "$bchPrefix$outputAddress";
}
bool isP2sh = outputAddress.startsWith("p", bchPrefix.length);
if (isP2sh) {
final p2sh = P2shAddress.fromAddress(
address: outputAddress,
network: BitcoinCashNetwork.mainnet,
);
txb.addOutput(Uint8List.fromList(p2sh.toScriptPubKey().toBytes()), outputAmount!);
return;
}
txb.addOutput(outputAddress, outputAmount!);
});
final estimatedSize = bitbox.BitcoinCash.getByteCount(inputs.length, outputs.length + 1);
var feeAmount = 0;
if (transactionCredentials.feeRate != null) {
feeAmount = transactionCredentials.feeRate! * estimatedSize;
} else {
feeAmount = feeRate(transactionCredentials.priority!) * estimatedSize;
}
final changeValue = totalInputAmount - amount - feeAmount;
if (changeValue > minAmount) {
txb.addOutput(changeAddress, changeValue);
}
for (var i = 0; i < inputs.length; i++) {
final input = inputs[i];
final keyPair = generateKeyPair(
hd: input.bitcoinAddressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd,
index: input.bitcoinAddressRecord.index);
txb.sign(i, keyPair, input.value);
}
// Build the transaction
final tx = txb.build();
return PendingBitcoinCashTransaction(tx, type,
electrumClient: electrumClient, amount: amount, fee: fee);
}
bitbox.ECPair generateKeyPair({required bitcoin.HDWallet hd, required int index}) =>
bitbox.ECPair.fromWIF(hd.derive(index).wif!);
@override
int feeAmountForPriority(BitcoinTransactionPriority priority, int inputsCount, int outputsCount,
{int? size}) =>
feeRate(priority) * bitbox.BitcoinCash.getByteCount(inputsCount, outputsCount);
int feeAmountWithFeeRate(int feeRate, int inputsCount, int outputsCount, {int? size}) =>
feeRate * bitbox.BitcoinCash.getByteCount(inputsCount, outputsCount);
int calculateEstimatedFeeWithFeeRate(int feeRate, int? amount, {int? outputsCount, int? size}) {
int inputsCount = 0;
int totalValue = 0;

View file

@ -18,6 +18,7 @@ abstract class BitcoinCashWalletAddressesBase extends ElectrumWalletAddresses wi
super.initialAddresses,
super.initialRegularAddressIndex,
super.initialChangeAddressIndex,
super.initialAddressPageType,
}) : super(walletInfo);
@override

View file

@ -1,4 +1,4 @@
import 'package:cw_bitcoin/bitcoin_commit_transaction_exception.dart';
import 'package:cw_bitcoin/exceptions.dart';
import 'package:bitbox/bitbox.dart' as bitbox;
import 'package:cw_core/pending_transaction.dart';
import 'package:cw_bitcoin/electrum.dart';
@ -11,7 +11,9 @@ class PendingBitcoinCashTransaction with PendingTransaction {
PendingBitcoinCashTransaction(this._tx, this.type,
{required this.electrumClient,
required this.amount,
required this.fee})
required this.fee,
required this.hasChange,
required this.isSendAll})
: _listeners = <void Function(ElectrumTransactionInfo transaction)>[];
final WalletType type;
@ -19,6 +21,8 @@ class PendingBitcoinCashTransaction with PendingTransaction {
final ElectrumClient electrumClient;
final int amount;
final int fee;
final bool hasChange;
final bool isSendAll;
@override
String get id => _tx.getId();
@ -36,18 +40,36 @@ class PendingBitcoinCashTransaction with PendingTransaction {
@override
Future<void> commit() async {
final result =
await electrumClient.broadcastTransaction(transactionRaw: _tx.toHex());
int? callId;
final result = await electrumClient.broadcastTransaction(
transactionRaw: hex, idCallback: (id) => callId = id);
if (result.isEmpty) {
throw BitcoinCommitTransactionException();
if (callId != null) {
final error = electrumClient.getErrorMessage(callId!);
if (error.contains("dust")) {
if (hasChange) {
throw BitcoinTransactionCommitFailedDustChange();
} else if (!isSendAll) {
throw BitcoinTransactionCommitFailedDustOutput();
} else {
throw BitcoinTransactionCommitFailedDustOutputSendAll();
}
}
if (error.contains("bad-txns-vout-negative")) {
throw BitcoinTransactionCommitFailedVoutNegative();
}
}
throw BitcoinTransactionCommitFailed();
}
_listeners?.forEach((listener) => listener(transactionInfo()));
_listeners.forEach((listener) => listener(transactionInfo()));
}
void addListener(
void Function(ElectrumTransactionInfo transaction) listener) =>
void addListener(void Function(ElectrumTransactionInfo transaction) listener) =>
_listeners.add(listener);
ElectrumTransactionInfo transactionInfo() => ElectrumTransactionInfo(type,

View file

@ -32,7 +32,7 @@ dependencies:
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

View file

@ -38,6 +38,8 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
CryptoCurrency.trx,
CryptoCurrency.usdt,
CryptoCurrency.usdterc20,
CryptoCurrency.sol,
CryptoCurrency.maticpoly,
CryptoCurrency.xlm,
CryptoCurrency.xrp,
CryptoCurrency.xhv,
@ -50,7 +52,6 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
CryptoCurrency.usdttrc20,
CryptoCurrency.hbar,
CryptoCurrency.sc,
CryptoCurrency.sol,
CryptoCurrency.usdc,
CryptoCurrency.usdcsol,
CryptoCurrency.zaddr,
@ -61,7 +62,6 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
CryptoCurrency.dcr,
CryptoCurrency.kmd,
CryptoCurrency.mana,
CryptoCurrency.maticpoly,
CryptoCurrency.matic,
CryptoCurrency.mkr,
CryptoCurrency.near,

View file

@ -0,0 +1,30 @@
import 'package:cw_core/crypto_currency.dart';
class TransactionWrongBalanceException implements Exception {
TransactionWrongBalanceException(this.currency);
final CryptoCurrency currency;
}
class TransactionNoInputsException implements Exception {}
class TransactionNoFeeException implements Exception {}
class TransactionNoDustException implements Exception {}
class TransactionNoDustOnChangeException implements Exception {
TransactionNoDustOnChangeException(this.max, this.min);
final String max;
final String min;
}
class TransactionCommitFailed implements Exception {}
class TransactionCommitFailedDustChange implements Exception {}
class TransactionCommitFailedDustOutput implements Exception {}
class TransactionCommitFailedDustOutputSendAll implements Exception {}
class TransactionCommitFailedVoutNegative implements Exception {}

View file

@ -1,7 +1,4 @@
import 'package:cw_core/transaction_priority.dart';
import 'package:cw_core/wallet_type.dart';
//import 'package:cake_wallet/generated/i18n.dart';
import 'package:cw_core/enumerable_item.dart';
class MoneroTransactionPriority extends TransactionPriority {
const MoneroTransactionPriority({required String title, required int raw})
@ -12,21 +9,20 @@ class MoneroTransactionPriority extends TransactionPriority {
MoneroTransactionPriority.automatic,
MoneroTransactionPriority.medium,
MoneroTransactionPriority.fast,
MoneroTransactionPriority.fastest
MoneroTransactionPriority.fastest,
];
static const slow = MoneroTransactionPriority(title: 'Slow', raw: 0);
static const automatic = MoneroTransactionPriority(title: 'Automatic', raw: 1);
static const automatic = MoneroTransactionPriority(title: 'Automatic', raw: 0);
static const slow = MoneroTransactionPriority(title: 'Slow', raw: 1);
static const medium = MoneroTransactionPriority(title: 'Medium', raw: 2);
static const fast = MoneroTransactionPriority(title: 'Fast', raw: 3);
static const fastest = MoneroTransactionPriority(title: 'Fastest', raw: 4);
static const standard = slow;
static MoneroTransactionPriority deserialize({required int raw}) {
switch (raw) {
case 0:
return slow;
case 1:
return automatic;
case 1:
return slow;
case 2:
return medium;
case 3:

View file

@ -7,7 +7,8 @@ class OutputInfo {
this.formattedCryptoAmount,
this.fiatAmount,
this.note,
this.extractedAddress,});
this.extractedAddress,
this.memo});
final String? fiatAmount;
final String? cryptoAmount;
@ -17,4 +18,5 @@ class OutputInfo {
final bool sendAll;
final bool isParsedAddress;
final int? formattedCryptoAmount;
final String? memo;
}

View file

@ -2,7 +2,9 @@ mixin PendingTransaction {
String get id;
String get amountFormatted;
String get feeFormatted;
String? feeRate;
String get hex;
int? get outputCount => null;
Future<void> commit();
}
}

View file

@ -3,8 +3,9 @@ import 'package:cw_core/wallet_info.dart';
abstract class WalletAddresses {
WalletAddresses(this.walletInfo)
: addressesMap = {},
addressInfos = {};
: addressesMap = {},
allAddressesMap = {},
addressInfos = {};
final WalletInfo walletInfo;
@ -15,6 +16,7 @@ abstract class WalletAddresses {
set address(String address);
Map<String, String> addressesMap;
Map<String, String> allAddressesMap;
Map<int, List<AddressInfo>> addressInfos;
@ -39,5 +41,5 @@ abstract class WalletAddresses {
}
}
bool containsAddress(String address) => addressesMap.containsKey(address);
bool containsAddress(String address) => allAddressesMap.containsKey(address);
}

View file

@ -41,4 +41,29 @@ class EthereumClient extends EVMChainClient {
return [];
}
}
@override
Future<List<EVMChainTransactionModel>> fetchInternalTransactions(String address) async {
try {
final response = await httpClient.get(Uri.https("api.etherscan.io", "/api", {
"module": "account",
"action": "txlistinternal",
"address": address,
"apikey": secrets.etherScanApiKey,
}));
final jsonResponse = json.decode(response.body) as Map<String, dynamic>;
if (response.statusCode >= 200 && response.statusCode < 300 && jsonResponse['status'] != 0) {
return (jsonResponse['result'] as List)
.map((e) => EVMChainTransactionModel.fromJson(e as Map<String, dynamic>, 'ETH'))
.toList();
}
return [];
} catch (e) {
log(e.toString());
return [];
}
}
}

View file

@ -14,6 +14,7 @@ import 'package:flutter/services.dart';
import 'package:http/http.dart';
import 'package:erc20/erc20.dart';
import 'package:web3dart/web3dart.dart';
import 'package:hex/hex.dart' as hex;
abstract class EVMChainClient {
final httpClient = Client();
@ -26,6 +27,8 @@ abstract class EVMChainClient {
Future<List<EVMChainTransactionModel>> fetchTransactions(String address,
{String? contractAddress});
Future<List<EVMChainTransactionModel>> fetchInternalTransactions(String address);
Uint8List prepareSignedTransactionForSending(Uint8List signedTransaction);
//! Common methods across all child classes
@ -79,12 +82,13 @@ abstract class EVMChainClient {
Future<PendingEVMChainTransaction> signTransaction({
required EthPrivateKey privateKey,
required String toAddress,
required String amount,
required BigInt amount,
required int gas,
required EVMChainTransactionPriority priority,
required CryptoCurrency currency,
required int exponent,
String? contractAddress,
String? data,
}) async {
assert(currency == CryptoCurrency.eth ||
currency == CryptoCurrency.maticpoly ||
@ -99,7 +103,8 @@ abstract class EVMChainClient {
from: privateKey.address,
to: EthereumAddress.fromHex(toAddress),
maxPriorityFeePerGas: EtherAmount.fromInt(EtherUnit.gwei, priority.tip),
amount: isEVMCompatibleChain ? EtherAmount.inWei(BigInt.parse(amount)) : EtherAmount.zero(),
amount: isEVMCompatibleChain ? EtherAmount.inWei(amount) : EtherAmount.zero(),
data: data != null ? hexToBytes(data) : null,
);
final signedTransaction =
@ -119,7 +124,7 @@ abstract class EVMChainClient {
_sendTransaction = () async {
await erc20.transfer(
EthereumAddress.fromHex(toAddress),
BigInt.parse(amount),
amount,
credentials: privateKey,
transaction: transaction,
);
@ -128,7 +133,7 @@ abstract class EVMChainClient {
return PendingEVMChainTransaction(
signedTransaction: signedTransaction,
amount: amount,
amount: amount.toString(),
fee: BigInt.from(gas) * (await price).getInWei,
sendTransaction: _sendTransaction,
exponent: exponent,
@ -140,12 +145,14 @@ abstract class EVMChainClient {
required EthereumAddress to,
required EtherAmount amount,
EtherAmount? maxPriorityFeePerGas,
Uint8List? data,
}) {
return Transaction(
from: from,
to: to,
maxPriorityFeePerGas: maxPriorityFeePerGas,
value: amount,
data: data,
);
}
@ -222,6 +229,10 @@ abstract class EVMChainClient {
}
}
Uint8List hexToBytes(String hexString) {
return Uint8List.fromList(hex.HEX.decode(hexString.startsWith('0x') ? hexString.substring(2) : hexString));
}
void stop() {
_client?.dispose();
}

View file

@ -9,3 +9,14 @@ class EVMChainTransactionCreationException implements Exception {
@override
String toString() => exceptionMessage;
}
class EVMChainTransactionFeesException implements Exception {
final String exceptionMessage;
EVMChainTransactionFeesException()
: exceptionMessage = 'Current balance is less than the estimated fees for this transaction.';
@override
String toString() => exceptionMessage;
}

View file

@ -32,15 +32,15 @@ class EVMChainTransactionModel {
factory EVMChainTransactionModel.fromJson(Map<String, dynamic> json, String defaultSymbol) =>
EVMChainTransactionModel(
date: DateTime.fromMillisecondsSinceEpoch(int.parse(json["timeStamp"]) * 1000),
hash: json["hash"],
from: json["from"],
to: json["to"],
amount: BigInt.parse(json["value"]),
gasUsed: int.parse(json["gasUsed"]),
gasPrice: BigInt.parse(json["gasPrice"]),
contractAddress: json["contractAddress"],
confirmations: int.parse(json["confirmations"]),
blockNumber: int.parse(json["blockNumber"]),
hash: json["hash"] ?? "",
from: json["from"] ?? "",
to: json["to"] ?? "",
amount: BigInt.parse(json["value"] ?? "0"),
gasUsed: int.parse(json["gasUsed"] ?? "0"),
gasPrice: BigInt.parse(json["gasPrice"] ?? "0"),
contractAddress: json["contractAddress"] ?? "",
confirmations: int.parse(json["confirmations"] ?? "0"),
blockNumber: int.parse(json["blockNumber"] ?? "0"),
tokenSymbol: json["tokenSymbol"] ?? defaultSymbol,
tokenDecimal: int.tryParse(json["tokenDecimal"] ?? ""),
isError: json["isError"] == "1",

View file

@ -224,10 +224,17 @@ abstract class EVMChainWalletBase
final outputs = _credentials.outputs;
final hasMultiDestination = outputs.length > 1;
final String? opReturnMemo = outputs.first.memo;
String? hexOpReturnMemo;
if (opReturnMemo != null) {
hexOpReturnMemo = '0x${opReturnMemo.codeUnits.map((char) => char.toRadixString(16).padLeft(2, '0')).join()}';
}
final CryptoCurrency transactionCurrency =
balance.keys.firstWhere((element) => element.title == _credentials.currency.title);
final _erc20Balance = balance[transactionCurrency]!;
final erc20Balance = balance[transactionCurrency]!;
BigInt totalAmount = BigInt.zero;
int exponent = transactionCurrency is Erc20Token ? transactionCurrency.decimal : 18;
num amountToEVMChainMultiplier = pow(10, exponent);
@ -242,7 +249,7 @@ abstract class EVMChainWalletBase
outputs.fold(0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0)));
totalAmount = BigInt.from(totalOriginalAmount * amountToEVMChainMultiplier);
if (_erc20Balance.balance < totalAmount) {
if (erc20Balance.balance < totalAmount) {
throw EVMChainTransactionCreationException(transactionCurrency);
}
} else {
@ -251,18 +258,27 @@ abstract class EVMChainWalletBase
// then no need to subtract the fees from the amount if send all
final BigInt allAmount;
if (transactionCurrency is Erc20Token) {
allAmount = _erc20Balance.balance;
allAmount = erc20Balance.balance;
} else {
allAmount = _erc20Balance.balance -
BigInt.from(calculateEstimatedFee(_credentials.priority!, null));
}
final totalOriginalAmount =
EVMChainFormatter.parseEVMChainAmountToDouble(output.formattedCryptoAmount ?? 0);
totalAmount = output.sendAll
? allAmount
: BigInt.from(totalOriginalAmount * amountToEVMChainMultiplier);
final estimatedFee = BigInt.from(calculateEstimatedFee(_credentials.priority!, null));
if (_erc20Balance.balance < totalAmount) {
if (estimatedFee > erc20Balance.balance) {
throw EVMChainTransactionFeesException();
}
allAmount = erc20Balance.balance - estimatedFee;
}
if (output.sendAll) {
totalAmount = allAmount;
} else {
final totalOriginalAmount =
EVMChainFormatter.parseEVMChainAmountToDouble(output.formattedCryptoAmount ?? 0);
totalAmount = BigInt.from(totalOriginalAmount * amountToEVMChainMultiplier);
}
if (erc20Balance.balance < totalAmount) {
throw EVMChainTransactionCreationException(transactionCurrency);
}
}
@ -272,13 +288,14 @@ abstract class EVMChainWalletBase
toAddress: _credentials.outputs.first.isParsedAddress
? _credentials.outputs.first.extractedAddress!
: _credentials.outputs.first.address,
amount: totalAmount.toString(),
amount: totalAmount,
gas: _estimatedGas!,
priority: _credentials.priority!,
currency: transactionCurrency,
exponent: exponent,
contractAddress:
transactionCurrency is Erc20Token ? transactionCurrency.contractAddress : null,
data: hexOpReturnMemo,
);
return pendingEVMChainTransaction;
@ -310,6 +327,7 @@ abstract class EVMChainWalletBase
Future<Map<String, EVMChainTransactionInfo>> fetchTransactions() async {
final address = _evmChainPrivateKey.address.hex;
final transactions = await _client.fetchTransactions(address);
final internalTransactions = await _client.fetchInternalTransactions(address);
final List<Future<List<EVMChainTransactionModel>>> erc20TokensTransactions = [];
@ -324,6 +342,7 @@ abstract class EVMChainWalletBase
final tokensTransaction = await Future.wait(erc20TokensTransactions);
transactions.addAll(tokensTransaction.expand((element) => element));
transactions.addAll(internalTransactions);
final Map<String, EVMChainTransactionInfo> result = {};
@ -484,7 +503,7 @@ abstract class EVMChainWalletBase
_transactionsUpdateTimer!.cancel();
}
_transactionsUpdateTimer = Timer.periodic(const Duration(seconds: 10), (_) {
_transactionsUpdateTimer = Timer.periodic(const Duration(seconds: 15), (_) {
_updateTransactions();
_updateBalance();
});

View file

@ -3,6 +3,7 @@ import 'dart:typed_data';
import 'package:cw_core/pending_transaction.dart';
import 'package:web3dart/crypto.dart';
import 'package:hex/hex.dart' as Hex;
class PendingEVMChainTransaction with PendingTransaction {
final Function sendTransaction;
@ -38,5 +39,12 @@ class PendingEVMChainTransaction with PendingTransaction {
String get hex => bytesToHex(signedTransaction, include0x: true);
@override
String get id => '';
String get id {
final String eip1559Hex = '0x02${hex.substring(2)}';
final Uint8List bytes = Uint8List.fromList(Hex.HEX.decode(eip1559Hex.substring(2)));
var txid = keccak256(bytes);
return '0x${Hex.HEX.encode(txid)}';
}
}

View file

@ -282,9 +282,7 @@ abstract class MoneroWalletBase
pendingTransactionDescription = await transaction_history.createTransaction(
address: address!,
amount: amount,
priorityRaw: _credentials.priority == MoneroTransactionPriority.automatic
? MoneroTransactionPriority.medium.serialize()
: _credentials.priority.serialize(),
priorityRaw: _credentials.priority.serialize(),
accountIndex: walletAddresses.account!.id,
preferredInputs: inputs);
}
@ -576,13 +574,7 @@ abstract class MoneroWalletBase
int height = 0;
try {
height = _getHeightByDate(walletInfo.date);
} catch (e, s) {
onError?.call(FlutterErrorDetails(
exception: e,
stack: s,
library: this.runtimeType.toString(),
));
}
} catch (_) {}
monero_wallet.setRecoveringFromSeed(isRecovery: true);
monero_wallet.setRefreshFromBlockHeight(height: height);

View file

@ -13,6 +13,7 @@ class PolygonClient extends EVMChainClient {
required EthereumAddress to,
required EtherAmount amount,
EtherAmount? maxPriorityFeePerGas,
Uint8List? data,
}) {
return Transaction(
from: from,
@ -54,4 +55,28 @@ class PolygonClient extends EVMChainClient {
return [];
}
}
@override
Future<List<EVMChainTransactionModel>> fetchInternalTransactions(String address) async {
try {
final response = await httpClient.get(Uri.https("api.polygonscan.io", "/api", {
"module": "account",
"action": "txlistinternal",
"address": address,
"apikey": secrets.polygonScanApiKey,
}));
final jsonResponse = json.decode(response.body) as Map<String, dynamic>;
if (response.statusCode >= 200 && response.statusCode < 300 && jsonResponse['status'] != 0) {
return (jsonResponse['result'] as List)
.map((e) => EVMChainTransactionModel.fromJson(e as Map<String, dynamic>, 'MATIC'))
.toList();
}
return [];
} catch (_) {
return [];
}
}
}

View file

@ -96,16 +96,30 @@ class SolanaWalletClient {
return SolanaBalance(totalBalance);
}
Future<double> getGasForMessage(String message) async {
Future<double> getFeeForMessage(String message, Commitment commitment) async {
try {
final gasPrice = await _client!.rpcClient.getFeeForMessage(message) ?? 0;
final fee = gasPrice / lamportsPerSol;
final feeForMessage =
await _client!.rpcClient.getFeeForMessage(message, commitment: commitment);
final fee = (feeForMessage ?? 0.0) / lamportsPerSol;
return fee;
} catch (_) {
return 0;
return 0.0;
}
}
Future<double> getEstimatedFee(Ed25519HDKeyPair ownerKeypair) async {
const commitment = Commitment.confirmed;
final message =
_getMessageForNativeTransaction(ownerKeypair, ownerKeypair.address, lamportsPerSol);
final recentBlockhash = await _getRecentBlockhash(commitment);
final estimatedFee =
_getFeeFromCompiledMessage(message, ownerKeypair.publicKey, recentBlockhash, commitment);
return estimatedFee;
}
/// Load the Address's transactions into the account
Future<List<SolanaTransactionModel>> fetchTransactions(
Ed25519HDPublicKey publicKey, {
@ -257,24 +271,15 @@ class SolanaWalletClient {
Future<PendingSolanaTransaction> signSolanaTransaction({
required String tokenTitle,
required int tokenDecimals,
String? tokenMint,
required double inputAmount,
required String destinationAddress,
required Ed25519HDKeyPair ownerKeypair,
required bool isSendAll,
String? tokenMint,
List<String> references = const [],
}) async {
const commitment = Commitment.confirmed;
final latestBlockhash =
await _client!.rpcClient.getLatestBlockhash(commitment: commitment).value;
final recentBlockhash = RecentBlockhash(
blockhash: latestBlockhash.blockhash,
feeCalculator: const FeeCalculator(
lamportsPerSignature: 500,
),
);
if (tokenTitle == CryptoCurrency.sol.title) {
final pendingNativeTokenTransaction = await _signNativeTokenTransaction(
tokenTitle: tokenTitle,
@ -282,8 +287,8 @@ class SolanaWalletClient {
inputAmount: inputAmount,
destinationAddress: destinationAddress,
ownerKeypair: ownerKeypair,
recentBlockhash: recentBlockhash,
commitment: commitment,
isSendAll: isSendAll,
);
return pendingNativeTokenTransaction;
} else {
@ -294,25 +299,29 @@ class SolanaWalletClient {
inputAmount: inputAmount,
destinationAddress: destinationAddress,
ownerKeypair: ownerKeypair,
recentBlockhash: recentBlockhash,
commitment: commitment,
);
return pendingSPLTokenTransaction;
}
}
Future<PendingSolanaTransaction> _signNativeTokenTransaction({
required String tokenTitle,
required int tokenDecimals,
required double inputAmount,
required String destinationAddress,
required Ed25519HDKeyPair ownerKeypair,
required RecentBlockhash recentBlockhash,
required Commitment commitment,
}) async {
// Convert SOL to lamport
int lamports = (inputAmount * lamportsPerSol).toInt();
Future<RecentBlockhash> _getRecentBlockhash(Commitment commitment) async {
final latestBlockhash =
await _client!.rpcClient.getLatestBlockhash(commitment: commitment).value;
final recentBlockhash = RecentBlockhash(
blockhash: latestBlockhash.blockhash,
feeCalculator: const FeeCalculator(lamportsPerSignature: 500),
);
return recentBlockhash;
}
Message _getMessageForNativeTransaction(
Ed25519HDKeyPair ownerKeypair,
String destinationAddress,
int lamports,
) {
final instructions = [
SystemInstruction.transfer(
fundingAccount: ownerKeypair.publicKey,
@ -322,21 +331,75 @@ class SolanaWalletClient {
];
final message = Message(instructions: instructions);
return message;
}
Future<double> _getFeeFromCompiledMessage(
Message message,
Ed25519HDPublicKey feePayer,
RecentBlockhash recentBlockhash,
Commitment commitment,
) async {
final compile = message.compile(
recentBlockhash: recentBlockhash.blockhash,
feePayer: feePayer,
);
final base64Message = base64Encode(compile.toByteArray().toList());
final fee = await getFeeForMessage(base64Message, commitment);
return fee;
}
Future<PendingSolanaTransaction> _signNativeTokenTransaction({
required String tokenTitle,
required int tokenDecimals,
required double inputAmount,
required String destinationAddress,
required Ed25519HDKeyPair ownerKeypair,
required Commitment commitment,
required bool isSendAll,
}) async {
// Convert SOL to lamport
int lamports = (inputAmount * lamportsPerSol).toInt();
Message message = _getMessageForNativeTransaction(ownerKeypair, destinationAddress, lamports);
final signers = [ownerKeypair];
final signedTx = await _signTransactionInternal(
message: message,
signers: signers,
commitment: commitment,
recentBlockhash: recentBlockhash,
);
RecentBlockhash recentBlockhash = await _getRecentBlockhash(commitment);
final fee = await _getFeeFromCompiledMessage(
message,
recentBlockhash,
signers.first.publicKey,
recentBlockhash,
commitment,
);
SignedTx signedTx;
if (isSendAll) {
final feeInLamports = (fee * lamportsPerSol).toInt();
final updatedLamports = lamports - feeInLamports;
final updatedMessage =
_getMessageForNativeTransaction(ownerKeypair, destinationAddress, updatedLamports);
signedTx = await _signTransactionInternal(
message: updatedMessage,
signers: signers,
commitment: commitment,
recentBlockhash: recentBlockhash,
);
} else {
signedTx = await _signTransactionInternal(
message: message,
signers: signers,
commitment: commitment,
recentBlockhash: recentBlockhash,
);
}
sendTx() async => await sendTransaction(
signedTransaction: signedTx,
commitment: commitment,
@ -360,7 +423,6 @@ class SolanaWalletClient {
required double inputAmount,
required String destinationAddress,
required Ed25519HDKeyPair ownerKeypair,
required RecentBlockhash recentBlockhash,
required Commitment commitment,
}) async {
final destinationOwner = Ed25519HDPublicKey.fromBase58(destinationAddress);
@ -408,8 +470,18 @@ class SolanaWalletClient {
);
final message = Message(instructions: [instruction]);
final signers = [ownerKeypair];
RecentBlockhash recentBlockhash = await _getRecentBlockhash(commitment);
final fee = await _getFeeFromCompiledMessage(
message,
signers.first.publicKey,
recentBlockhash,
commitment,
);
final signedTx = await _signTransactionInternal(
message: message,
signers: signers,
@ -417,12 +489,6 @@ class SolanaWalletClient {
recentBlockhash: recentBlockhash,
);
final fee = await _getFeeFromCompiledMessage(
message,
recentBlockhash,
signers.first.publicKey,
);
sendTx() async => await sendTransaction(
signedTransaction: signedTx,
commitment: commitment,
@ -438,19 +504,6 @@ class SolanaWalletClient {
return pendingTransaction;
}
Future<double> _getFeeFromCompiledMessage(
Message message, RecentBlockhash recentBlockhash, Ed25519HDPublicKey feePayer) async {
final compile = message.compile(
recentBlockhash: recentBlockhash.blockhash,
feePayer: feePayer,
);
final base64Message = base64Encode(compile.toByteArray().toList());
final fee = await getGasForMessage(base64Message);
return fee;
}
Future<SignedTx> _signTransactionInternal({
required Message message,
required List<Ed25519HDKeyPair> signers,
@ -466,13 +519,18 @@ class SolanaWalletClient {
required SignedTx signedTransaction,
required Commitment commitment,
}) async {
final signature = await _client!.rpcClient.sendTransaction(
signedTransaction.encode(),
preflightCommitment: commitment,
);
try {
final signature = await _client!.rpcClient.sendTransaction(
signedTransaction.encode(),
preflightCommitment: commitment,
);
_client!.waitForSignatureStatus(signature, status: commitment);
_client!.waitForSignatureStatus(signature, status: commitment);
return signature;
return signature;
} catch (e) {
print('Error while sending transaction: ${e.toString()}');
throw Exception(e);
}
}
}

View file

@ -1,6 +1,5 @@
import 'dart:async';
import 'dart:convert';
import 'dart:developer';
import 'dart:io';
import 'package:cw_core/cake_hive.dart';
import 'package:cw_core/crypto_currency.dart';
@ -30,7 +29,6 @@ import 'package:mobx/mobx.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:solana/metaplex.dart' as metaplex;
import 'package:solana/solana.dart';
import 'package:web3dart/crypto.dart';
part 'solana_wallet.g.dart';
@ -77,6 +75,9 @@ abstract class SolanaWalletBase
late SolanaWalletClient _client;
@observable
double? estimatedFee;
Timer? _transactionsUpdateTimer;
late final Box<SPLToken> splTokensBox;
@ -134,7 +135,7 @@ abstract class SolanaWalletBase
assert(mnemonic != null || privateKey != null);
if (privateKey != null) {
final privateKeyBytes = hexToBytes(privateKey);
final privateKeyBytes = HEX.decode(privateKey);
return await Wallet.fromPrivateKeyBytes(privateKey: privateKeyBytes);
}
@ -173,6 +174,14 @@ abstract class SolanaWalletBase
}
}
Future<void> _getEstimatedFees() async {
try {
estimatedFee = await _client.getEstimatedFee(_walletKeyPair!);
} catch (e) {
estimatedFee = 0.0;
}
}
@override
Future<PendingTransaction> createTransaction(Object credentials) async {
final solCredentials = credentials as SolanaTransactionCredentials;
@ -190,6 +199,8 @@ abstract class SolanaWalletBase
double totalAmount = 0.0;
bool isSendAll = false;
if (hasMultiDestination) {
if (outputs.any((item) => item.sendAll || (item.formattedCryptoAmount ?? 0) <= 0)) {
throw SolanaTransactionWrongBalanceException(transactionCurrency);
@ -206,9 +217,15 @@ abstract class SolanaWalletBase
} else {
final output = outputs.first;
final totalOriginalAmount = double.parse(output.cryptoAmount ?? '0.0');
isSendAll = output.sendAll;
totalAmount = output.sendAll ? walletBalanceForCurrency : totalOriginalAmount;
if (isSendAll) {
totalAmount = walletBalanceForCurrency;
} else {
final totalOriginalAmount = double.parse(output.cryptoAmount ?? '0.0');
totalAmount = totalOriginalAmount;
}
if (walletBalanceForCurrency < totalAmount) {
throw SolanaTransactionWrongBalanceException(transactionCurrency);
@ -230,6 +247,7 @@ abstract class SolanaWalletBase
destinationAddress: solCredentials.outputs.first.isParsedAddress
? solCredentials.outputs.first.extractedAddress!
: solCredentials.outputs.first.address,
isSendAll: isSendAll,
);
return pendingSolanaTransaction;
@ -271,7 +289,10 @@ abstract class SolanaWalletBase
Future<void> _updateSPLTokenTransactions() async {
List<SolanaTransactionModel> splTokenTransactions = [];
for (var token in balance.keys) {
// Make a copy of keys to avoid concurrent modification
var tokenKeys = List<CryptoCurrency>.from(balance.keys);
for (var token in tokenKeys) {
if (token is SPLToken) {
final tokenTxs = await _client.getSPLTokenTransfers(
token.mintAddress,
@ -328,6 +349,7 @@ abstract class SolanaWalletBase
_updateBalance(),
_updateNativeSOLTransactions(),
_updateSPLTokenTransactions(),
_getEstimatedFees(),
]);
syncStatus = SyncedSyncStatus();
@ -435,18 +457,22 @@ abstract class SolanaWalletBase
final mintPublicKey = Ed25519HDPublicKey.fromBase58(mintAddress);
// Fetch token's metadata account
final token = await solanaClient!.rpcClient.getMetadata(mint: mintPublicKey);
try {
final token = await solanaClient!.rpcClient.getMetadata(mint: mintPublicKey);
if (token == null) {
if (token == null) {
return null;
}
return SPLToken.fromMetadata(
name: token.name,
mint: token.mint,
symbol: token.symbol,
mintAddress: mintAddress,
);
} catch (e) {
return null;
}
return SPLToken.fromMetadata(
name: token.name,
mint: token.mint,
symbol: token.symbol,
mintAddress: mintAddress,
);
}
@override
@ -477,9 +503,9 @@ abstract class SolanaWalletBase
}
_transactionsUpdateTimer = Timer.periodic(const Duration(seconds: 20), (_) {
_updateSPLTokenTransactions();
_updateNativeSOLTransactions();
_updateBalance();
_updateNativeSOLTransactions();
_updateSPLTokenTransactions();
});
}
@ -491,7 +517,7 @@ abstract class SolanaWalletBase
final signature = await _walletKeyPair!.sign(messageBytes);
// Convert the signature to a hexadecimal string
final hex = bytesToHex(signature.bytes);
final hex = HEX.encode(signature.bytes);
return hex;
}

View file

@ -32,6 +32,7 @@ class SolanaWalletService extends WalletService<SolanaNewWalletCredentials,
await wallet.init();
wallet.addInitialTokens();
await wallet.save();
return wallet;
}
@ -46,16 +47,31 @@ class SolanaWalletService extends WalletService<SolanaNewWalletCredentials,
Future<SolanaWallet> openWallet(String name, String password) async {
final walletInfo =
walletInfoSource.values.firstWhere((info) => info.id == WalletBase.idFor(name, getType()));
final wallet = await SolanaWalletBase.open(
name: name,
password: password,
walletInfo: walletInfo,
);
await wallet.init();
await wallet.save();
try {
final wallet = await SolanaWalletBase.open(
name: name,
password: password,
walletInfo: walletInfo,
);
return wallet;
await wallet.init();
await wallet.save();
saveBackup(name);
return wallet;
} catch (_) {
await restoreWalletFilesFromBackup(name);
final wallet = await SolanaWalletBase.open(
name: name,
password: password,
walletInfo: walletInfo,
);
await wallet.init();
await wallet.save();
return wallet;
}
}
@override
@ -110,6 +126,7 @@ class SolanaWalletService extends WalletService<SolanaNewWalletCredentials,
password: password, name: currentName, walletInfo: currentWalletInfo);
await currentWallet.renameWalletFiles(newName);
await saveBackup(newName);
final newWalletInfo = currentWalletInfo;
newWalletInfo.id = WalletBase.idFor(newName, getType());

View file

@ -19,7 +19,6 @@ dependencies:
bip39: ^1.0.6
mobx: ^2.3.0+1
shared_preferences: ^2.0.15
web3dart: ^2.7.1
bip32: ^2.0.0
hex: ^0.2.0
@ -34,4 +33,4 @@ dev_dependencies:
flutter:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
# - images/a_dot_ham.jpeg

View file

@ -58,16 +58,16 @@ post_install do |installer|
'PERMISSION_CONTACTS=0',
## dart: PermissionGroup.camera
'PERMISSION_CAMERA=0',
'PERMISSION_CAMERA=1',
## dart: PermissionGroup.microphone
'PERMISSION_MICROPHONE=0',
'PERMISSION_MICROPHONE=1',
## dart: PermissionGroup.speech
'PERMISSION_SPEECH_RECOGNIZER=0',
## dart: PermissionGroup.photos
'PERMISSION_PHOTOS=0',
'PERMISSION_PHOTOS=1',
## dart: [PermissionGroup.location, PermissionGroup.locationAlways, PermissionGroup.locationWhenInUse]
'PERMISSION_LOCATION=0',

View file

@ -277,7 +277,7 @@ SPEC CHECKSUMS:
flutter_inappwebview_ios: 97215cf7d4677db55df76782dbd2930c5e1c1ea0
flutter_mailer: 2ef5a67087bc8c6c4cefd04a178bf1ae2c94cd83
flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be
fluttertoast: eb263d302cc92e04176c053d2385237e9f43fad0
fluttertoast: 48c57db1b71b0ce9e6bba9f31c940ff4b001293c
in_app_review: 318597b3a06c22bb46dc454d56828c85f444f99d
local_auth_ios: 1ba1475238daa33a6ffa2a29242558437be435ac
MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb
@ -300,6 +300,6 @@ SPEC CHECKSUMS:
wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47
workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6
PODFILE CHECKSUM: fcb1b8418441a35b438585c9dd8374e722e6c6ca
PODFILE CHECKSUM: a2fe518be61cdbdc5b0e2da085ab543d556af2d3
COCOAPODS: 1.12.1
COCOAPODS: 1.15.2

View file

@ -85,7 +85,8 @@ class CWBitcoin extends Bitcoin {
sendAll: out.sendAll,
extractedAddress: out.extractedAddress,
isParsedAddress: out.isParsedAddress,
formattedCryptoAmount: out.formattedCryptoAmount))
formattedCryptoAmount: out.formattedCryptoAmount,
memo: out.memo))
.toList(),
priority: priority as BitcoinTransactionPriority,
feeRate: feeRate);
@ -113,13 +114,46 @@ class CWBitcoin extends Bitcoin {
.map((BaseBitcoinAddressRecord addr) => ElectrumSubAddress(
id: addr.index,
name: addr.name,
address: electrumWallet.type == WalletType.bitcoinCash ? addr.cashAddr : addr.address,
address: addr.address,
txCount: addr.txCount,
balance: addr.balance,
isChange: addr.isHidden))
.toList();
}
@override
Future<int> estimateFakeSendAllTxAmount(Object wallet, TransactionPriority priority) async {
try {
final sk = ECPrivate.random();
final electrumWallet = wallet as ElectrumWallet;
if (wallet.type == WalletType.bitcoinCash) {
final p2pkhAddr = sk.getPublic().toP2pkhAddress();
final estimatedTx = await electrumWallet.estimateSendAllTx(
[BitcoinOutput(address: p2pkhAddr, value: BigInt.zero)],
getFeeRate(wallet, priority as BitcoinCashTransactionPriority),
);
return estimatedTx.amount;
}
final p2shAddr = sk.getPublic().toP2pkhInP2sh();
final estimatedTx = await electrumWallet.estimateSendAllTx(
[BitcoinOutput(address: p2shAddr, value: BigInt.zero)],
getFeeRate(
wallet,
wallet.type == WalletType.litecoin
? priority as LitecoinTransactionPriority
: priority as BitcoinTransactionPriority,
),
);
return estimatedTx.amount;
} catch (_) {
return 0;
}
}
@override
String getAddress(Object wallet) {
final bitcoinWallet = wallet as ElectrumWallet;
@ -212,6 +246,11 @@ class CWBitcoin extends Bitcoin {
}
}
@override
bool hasTaprootInput(PendingTransaction pendingTransaction) {
return (pendingTransaction as PendingBitcoinTransaction).hasTaprootInputs;
}
List<BitcoinSilentPaymentAddressRecord> getSilentAddresses(Object wallet) {
final bitcoinWallet = wallet as ElectrumWallet;
return bitcoinWallet.walletAddresses.silentAddresses;

View file

@ -1,4 +1,13 @@
import 'dart:convert';
import 'package:cake_wallet/.secrets.g.dart' as secrets;
import 'package:cake_wallet/buy/buy_amount.dart';
import 'package:cake_wallet/buy/buy_exception.dart';
import 'package:cake_wallet/buy/buy_provider.dart';
import 'package:cake_wallet/buy/buy_provider_description.dart';
import 'package:cake_wallet/buy/order.dart';
import 'package:cake_wallet/exchange/trade_state.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/palette.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
@ -6,34 +15,31 @@ import 'package:cake_wallet/store/settings_store.dart';
import 'package:cake_wallet/themes/theme_base.dart';
import 'package:cake_wallet/utils/device_info.dart';
import 'package:crypto/crypto.dart';
import 'package:cake_wallet/buy/buy_exception.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart';
import 'package:cake_wallet/buy/buy_amount.dart';
import 'package:cake_wallet/buy/buy_provider.dart';
import 'package:cake_wallet/buy/buy_provider_description.dart';
import 'package:cake_wallet/buy/order.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:cake_wallet/exchange/trade_state.dart';
import 'package:cake_wallet/.secrets.g.dart' as secrets;
import 'package:cw_core/crypto_currency.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart';
import 'package:url_launcher/url_launcher.dart';
class MoonPaySellProvider extends BuyProvider {
MoonPaySellProvider({
class MoonPayProvider extends BuyProvider {
MoonPayProvider({
required SettingsStore settingsStore,
required WalletBase wallet,
bool isTestEnvironment = false,
}) : baseUrl = isTestEnvironment ? _baseTestUrl : _baseProductUrl,
}) : baseSellUrl = isTestEnvironment ? _baseSellTestUrl : _baseSellProductUrl,
baseBuyUrl = isTestEnvironment ? _baseBuyTestUrl : _baseBuyProductUrl,
this._settingsStore = settingsStore,
super(wallet: wallet, isTestEnvironment: isTestEnvironment);
final SettingsStore _settingsStore;
static const _baseTestUrl = 'sell-sandbox.moonpay.com';
static const _baseProductUrl = 'sell.moonpay.com';
static const _baseSellTestUrl = 'sell-sandbox.moonpay.com';
static const _baseSellProductUrl = 'sell.moonpay.com';
static const _baseBuyTestUrl = 'buy-staging.moonpay.com';
static const _baseBuyProductUrl = 'buy.moonpay.com';
static const _cIdBaseUrl = 'exchange-helper.cakewallet.com';
static const _apiUrl = 'https://api.moonpay.com';
@override
String get providerDescription =>
@ -60,146 +66,121 @@ class MoonPaySellProvider extends BuyProvider {
static String get _apiKey => secrets.moonPayApiKey;
static String get _secretKey => secrets.moonPaySecretKey;
final String baseUrl;
final String baseBuyUrl;
final String baseSellUrl;
Future<Uri> requestMoonPayUrl({
String get currencyCode => walletTypeToCryptoCurrency(wallet.type).title.toLowerCase();
String get trackUrl => baseBuyUrl + '/transaction_receipt?transactionId=';
static String get _exchangeHelperApiKey => secrets.exchangeHelperApiKey;
Future<String> getMoonpaySignature(String query) async {
final uri = Uri.https(_cIdBaseUrl, "/api/moonpay");
final response = await post(
uri,
headers: {
'Content-Type': 'application/json',
'x-api-key': _exchangeHelperApiKey,
},
body: json.encode({'query': query}),
);
if (response.statusCode == 200) {
return (jsonDecode(response.body) as Map<String, dynamic>)['signature'] as String;
} else {
throw Exception(
'Provider currently unavailable. Status: ${response.statusCode} ${response.body}');
}
}
Future<Uri> requestSellMoonPayUrl({
required CryptoCurrency currency,
required String refundWalletAddress,
required SettingsStore settingsStore,
}) async {
final customParams = {
final params = {
'theme': themeToMoonPayTheme(settingsStore.currentTheme),
'language': settingsStore.languageCode,
'colorCode': settingsStore.currentTheme.type == ThemeType.dark
? '#${Palette.blueCraiola.value.toRadixString(16).substring(2, 8)}'
: '#${Palette.moderateSlateBlue.value.toRadixString(16).substring(2, 8)}',
'defaultCurrencyCode': _normalizeCurrency(currency),
'refundWalletAddress': refundWalletAddress,
};
final originalUri = Uri.https(
baseUrl,
'',
<String, dynamic>{
'apiKey': _apiKey,
'defaultBaseCurrencyCode': currency.toString().toLowerCase(),
'refundWalletAddress': refundWalletAddress,
}..addAll(customParams),
);
if (_apiKey.isNotEmpty) {
params['apiKey'] = _apiKey;
}
final messageBytes = utf8.encode('?${originalUri.query}');
final key = utf8.encode(_secretKey);
final hmac = Hmac(sha256, key);
final digest = hmac.convert(messageBytes);
final signature = base64.encode(digest.bytes);
final originalUri = Uri.https(
baseSellUrl,
'',
params,
);
if (isTestEnvironment) {
return originalUri;
}
final signature = await getMoonpaySignature('?${originalUri.query}');
final query = Map<String, dynamic>.from(originalUri.queryParameters);
query['signature'] = signature;
final signedUri = originalUri.replace(queryParameters: query);
return signedUri;
}
@override
Future<void> launchProvider(BuildContext context, bool? isBuyAction) async {
try {
final uri = await requestMoonPayUrl(
currency: wallet.currency,
refundWalletAddress: wallet.walletAddresses.address,
settingsStore: _settingsStore,
);
if (await canLaunchUrl(uri)) {
if (DeviceInfo.instance.isMobile) {
Navigator.of(context).pushNamed(Routes.webViewPage, arguments: ['MoonPay', uri]);
} else {
await launchUrl(uri, mode: LaunchMode.externalApplication);
}
} else {
throw Exception('Could not launch URL');
}
} catch (e) {
await showDialog<void>(
context: context,
builder: (BuildContext context) {
return AlertWithOneAction(
alertTitle: 'MoonPay',
alertContent: 'The MoonPay service is currently unavailable: $e',
buttonText: S.of(context).ok,
buttonAction: () => Navigator.of(context).pop(),
);
},
);
}
}
}
class MoonPayBuyProvider extends BuyProvider {
MoonPayBuyProvider({required WalletBase wallet, bool isTestEnvironment = false})
: baseUrl = isTestEnvironment ? _baseTestUrl : _baseProductUrl,
super(wallet: wallet, isTestEnvironment: isTestEnvironment);
static const _baseTestUrl = 'https://buy-staging.moonpay.com';
static const _baseProductUrl = 'https://buy.moonpay.com';
static const _apiUrl = 'https://api.moonpay.com';
// BUY:
static const _currenciesSuffix = '/v3/currencies';
static const _quoteSuffix = '/buy_quote';
static const _transactionsSuffix = '/v1/transactions';
static const _ipAddressSuffix = '/v4/ip_address';
static const _apiKey = secrets.moonPayApiKey;
static const _secretKey = secrets.moonPaySecretKey;
@override
String get title => 'MoonPay';
Future<Uri> requestBuyMoonPayUrl({
required CryptoCurrency currency,
required SettingsStore settingsStore,
required String walletAddress,
String? amount,
}) async {
final params = {
'theme': themeToMoonPayTheme(settingsStore.currentTheme),
'language': settingsStore.languageCode,
'colorCode': settingsStore.currentTheme.type == ThemeType.dark
? '#${Palette.blueCraiola.value.toRadixString(16).substring(2, 8)}'
: '#${Palette.moderateSlateBlue.value.toRadixString(16).substring(2, 8)}',
'defaultCurrencyCode': _normalizeCurrency(currency),
'baseCurrencyCode': _normalizeCurrency(currency),
'baseCurrencyAmount': amount ?? '0',
'currencyCode': currencyCode,
'walletAddress': walletAddress,
'lockAmount': 'true',
'showAllCurrencies': 'false',
'showWalletAddressForm': 'false',
'enabledPaymentMethods':
'credit_debit_card,apple_pay,google_pay,samsung_pay,sepa_bank_transfer,gbp_bank_transfer,gbp_open_banking_payment',
};
@override
String get providerDescription =>
'MoonPay offers a fast and simple way to buy and sell cryptocurrencies';
if (_apiKey.isNotEmpty) {
params['apiKey'] = _apiKey;
}
@override
String get lightIcon => 'assets/images/moonpay_light.png';
final originalUri = Uri.https(
baseBuyUrl,
'',
params,
);
@override
String get darkIcon => 'assets/images/moonpay_dark.png';
if (isTestEnvironment) {
return originalUri;
}
String get currencyCode => walletTypeToCryptoCurrency(wallet.type).title.toLowerCase();
String get trackUrl => baseUrl + '/transaction_receipt?transactionId=';
String baseUrl;
Future<String> requestUrl(String amount, String sourceCurrency) async {
final enabledPaymentMethods = 'credit_debit_card%2Capple_pay%2Cgoogle_pay%2Csamsung_pay'
'%2Csepa_bank_transfer%2Cgbp_bank_transfer%2Cgbp_open_banking_payment';
final suffix = '?apiKey=' +
_apiKey +
'&currencyCode=' +
currencyCode +
'&enabledPaymentMethods=' +
enabledPaymentMethods +
'&walletAddress=' +
wallet.walletAddresses.address +
'&baseCurrencyCode=' +
sourceCurrency.toLowerCase() +
'&baseCurrencyAmount=' +
amount +
'&lockAmount=true' +
'&showAllCurrencies=false' +
'&showWalletAddressForm=false';
final originalUrl = baseUrl + suffix;
final messageBytes = utf8.encode(suffix);
final key = utf8.encode(_secretKey);
final hmac = Hmac(sha256, key);
final digest = hmac.convert(messageBytes);
final signature = base64.encode(digest.bytes);
final urlWithSignature = originalUrl + '&signature=${Uri.encodeComponent(signature)}';
return isTestEnvironment ? originalUrl : urlWithSignature;
final signature = await getMoonpaySignature('?${originalUri.query}');
final query = Map<String, dynamic>.from(originalUri.queryParameters);
query['signature'] = signature;
final signedUri = originalUri.replace(queryParameters: query);
return signedUri;
}
Future<BuyAmount> calculateAmount(String amount, String sourceCurrency) async {
@ -274,6 +255,52 @@ class MoonPayBuyProvider extends BuyProvider {
}
@override
Future<void> launchProvider(BuildContext context, bool? isBuyAction) =>
throw UnimplementedError();
Future<void> launchProvider(BuildContext context, bool? isBuyAction) async {
// try {
late final Uri uri;
if (isBuyAction ?? true) {
uri = await requestBuyMoonPayUrl(
currency: wallet.currency,
walletAddress: wallet.walletAddresses.address,
settingsStore: _settingsStore,
);
} else {
uri = await requestSellMoonPayUrl(
currency: wallet.currency,
refundWalletAddress: wallet.walletAddresses.address,
settingsStore: _settingsStore,
);
}
if (await canLaunchUrl(uri)) {
if (DeviceInfo.instance.isMobile) {
Navigator.of(context).pushNamed(Routes.webViewPage, arguments: ['MoonPay', uri]);
} else {
await launchUrl(uri, mode: LaunchMode.externalApplication);
}
} else {
throw Exception('Could not launch URL');
}
// } catch (e) {
// await showDialog<void>(
// context: context,
// builder: (BuildContext context) {
// return AlertWithOneAction(
// alertTitle: 'MoonPay',
// alertContent: 'The MoonPay service is currently unavailable: $e',
// buttonText: S.of(context).ok,
// buttonAction: () => Navigator.of(context).pop(),
// );
// },
// );
// }
}
String _normalizeCurrency(CryptoCurrency currency) {
if (currency == CryptoCurrency.maticpoly) {
return "MATIC_POLYGON";
}
return currency.toString().toLowerCase();
}
}

View file

@ -32,11 +32,12 @@ class RobinhoodBuyProvider extends BuyProvider {
String get _applicationId => secrets.robinhoodApplicationId;
String get _apiSecret => secrets.robinhoodCIdApiSecret;
String get _apiSecret => secrets.exchangeHelperApiKey;
String getSignature(String message) {
switch (wallet.type) {
case WalletType.ethereum:
case WalletType.polygon:
return wallet.signMessage(message);
case WalletType.litecoin:
case WalletType.bitcoin:

View file

@ -274,8 +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]|^)((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)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:
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]|\$)'

View file

@ -34,6 +34,10 @@ class AmountValidator extends TextValidator {
late final DecimalAmountValidator decimalAmountValidator;
String? call(String? value) {
if (value == null || value.isEmpty) {
return S.current.error_text_amount;
}
//* Validate for Text(length, symbols, decimals etc)
final textValidation = symbolsAmountValidator(value) ?? decimalAmountValidator(value);

View file

@ -1,5 +1,7 @@
import 'dart:async';
import 'dart:io';
import 'package:cake_wallet/core/secure_storage.dart';
import 'package:cake_wallet/core/totp_request_details.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/auth/auth_page.dart';
@ -64,7 +66,7 @@ class AuthService with Store {
Future<bool> authenticate(String pin) async {
final key = generateStoreKeyFor(key: SecretStoreKey.pinCodePassword);
final encodedPin = await secureStorage.read(key: key);
final encodedPin = await readSecureStorage(secureStorage, key);
final decodedPin = decodedPinCode(pin: encodedPin!);
return decodedPin == pin;
@ -76,7 +78,8 @@ class AuthService with Store {
}
Future<bool> requireAuth() async {
final timestamp = int.tryParse(await secureStorage.read(key: SecureKey.lastAuthTimeMilliseconds) ?? '0');
final timestamp =
int.tryParse(await secureStorage.read(key: SecureKey.lastAuthTimeMilliseconds) ?? '0');
final duration = _durationToRequireAuth(timestamp ?? 0);
final requiredPinInterval = settingsStore.pinTimeOutDuration;

View file

@ -1,3 +1,4 @@
import 'package:cake_wallet/core/secure_storage.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:cake_wallet/entities/secret_store_key.dart';
import 'package:cake_wallet/entities/encrypt.dart';
@ -10,7 +11,7 @@ class KeyService {
Future<String> getWalletPassword({required String walletName}) async {
final key = generateStoreKeyFor(
key: SecretStoreKey.moneroWalletPassword, walletName: walletName);
final encodedPassword = await _secureStorage.read(key: key);
final encodedPassword = await readSecureStorage(_secureStorage, key);
return decodeWalletPassword(password: encodedPassword!);
}

View file

@ -0,0 +1,27 @@
import 'dart:async';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
// For now, we can create a utility function to handle this.
//
// However, we could look into abstracting the entire FlutterSecureStorage package
// so the app doesn't depend on the package directly but an absraction.
// It'll make these kind of modifications to read/write come from a single point.
Future<String?> readSecureStorage(FlutterSecureStorage secureStorage, String key) async {
String? result;
const maxWait = Duration(seconds: 3);
const checkInterval = Duration(milliseconds: 200);
DateTime start = DateTime.now();
while (result == null && DateTime.now().difference(start) < maxWait) {
result = await secureStorage.read(key: key);
if (result != null) {
break;
}
await Future.delayed(checkInterval);
}
return result;
}

View file

@ -198,6 +198,7 @@ import 'package:cake_wallet/view_model/wallet_list/wallet_list_view_model.dart';
import 'package:cake_wallet/view_model/wallet_restore_view_model.dart';
import 'package:cake_wallet/view_model/wallet_seed_view_model.dart';
import 'package:cake_wallet/view_model/exchange/exchange_view_model.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:get_it/get_it.dart';
import 'package:hive/hive.dart';
@ -811,8 +812,11 @@ Future<void> setup({
getIt
.registerFactory<DFXBuyProvider>(() => DFXBuyProvider(wallet: getIt.get<AppStore>().wallet!));
getIt.registerFactory<MoonPaySellProvider>(() => MoonPaySellProvider(
settingsStore: getIt.get<AppStore>().settingsStore, wallet: getIt.get<AppStore>().wallet!));
getIt.registerFactory<MoonPayProvider>(() => MoonPayProvider(
settingsStore: getIt.get<AppStore>().settingsStore,
wallet: getIt.get<AppStore>().wallet!,
isTestEnvironment: kDebugMode,
));
getIt.registerFactory<OnRamperBuyProvider>(() => OnRamperBuyProvider(
getIt.get<AppStore>().settingsStore,

View file

@ -4,6 +4,7 @@ import 'package:cake_wallet/core/wallet_loading_service.dart';
import 'package:cake_wallet/entities/preferences_key.dart';
import 'package:cake_wallet/store/settings_store.dart';
import 'package:cake_wallet/utils/device_info.dart';
import 'package:cake_wallet/utils/feature_flag.dart';
import 'package:cake_wallet/view_model/settings/sync_mode.dart';
import 'package:cake_wallet/view_model/wallet_list/wallet_list_item.dart';
import 'package:cake_wallet/view_model/wallet_list/wallet_list_view_model.dart';
@ -107,7 +108,7 @@ class BackgroundTasks {
final SyncMode syncMode = settingsStore.currentSyncMode;
final bool syncAll = settingsStore.currentSyncAll;
if (syncMode.type == SyncType.disabled) {
if (syncMode.type == SyncType.disabled || !FeatureFlag.isBackgroundSyncEnabled) {
cancelSyncTask();
return;
}

View file

@ -10,6 +10,7 @@ class BiometricAuth {
return await _localAuth.authenticate(
localizedReason: S.current.biometric_auth_reason,
options: AuthenticationOptions(
biometricOnly: true,
useErrorDialogs: true,
stickyAuth: false));
} on PlatformException catch (e) {

View file

@ -35,6 +35,7 @@ const cakeWalletBitcoinCashDefaultNodeUri = 'bitcoincash.stackwallet.com:50002';
const nanoDefaultNodeUri = 'rpc.nano.to';
const nanoDefaultPowNodeUri = 'rpc.nano.to';
const solanaDefaultNodeUri = 'rpc.ankr.com';
const newCakeWalletBitcoinUri = 'btc-electrum.cakewallet.com:50002';
Future<void> defaultSettingsMigration(
{required int version,
@ -201,6 +202,15 @@ Future<void> defaultSettingsMigration(
await changeSolanaCurrentNodeToDefault(
sharedPreferences: sharedPreferences, nodes: nodes);
break;
case 28:
await _updateMoneroPriority(sharedPreferences);
break;
case 29:
await changeDefaultBitcoinNode(nodes, sharedPreferences);
break;
default:
break;
}
@ -215,6 +225,18 @@ Future<void> defaultSettingsMigration(
await sharedPreferences.setInt(PreferencesKey.currentDefaultSettingsMigrationVersion, version);
}
Future<void> _updateMoneroPriority(SharedPreferences sharedPreferences) async {
final currentPriority =
await sharedPreferences.getInt(PreferencesKey.moneroTransactionPriority) ??
monero!.getDefaultTransactionPriority().serialize();
// was set to automatic but automatic should be 0
if (currentPriority == 1) {
sharedPreferences.setInt(PreferencesKey.moneroTransactionPriority,
monero!.getDefaultTransactionPriority().serialize()); // 0
}
}
Future<void> _validateWalletInfoBoxData(Box<WalletInfo> walletInfoSource) async {
try {
final root = await getApplicationDocumentsDirectory();
@ -687,6 +709,26 @@ Future<void> changeDefaultMoneroNode(
}
}
Future<void> changeDefaultBitcoinNode(
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 newCakeWalletBitcoinNode = Node(uri: newCakeWalletBitcoinUri, type: WalletType.bitcoin);
await nodeSource.add(newCakeWalletBitcoinNode);
if (needToReplaceCurrentBitcoinNode) {
await sharedPreferences.setInt(
PreferencesKey.currentBitcoinElectrumSererIdKey, newCakeWalletBitcoinNode.key as int);
}
}
Future<void> checkCurrentNodes(
Box<Node> nodeSource, Box<Node> powNodeSource, SharedPreferences sharedPreferences) async {
final currentMoneroNodeId = sharedPreferences.getInt(PreferencesKey.currentNodeIdKey);

View file

@ -20,6 +20,7 @@ class PreferencesKey {
static const isAppSecureKey = 'is_app_secure';
static const disableBuyKey = 'disable_buy';
static const disableSellKey = 'disable_sell';
static const disableBulletinKey = 'disable_bulletin';
static const defaultBuyProvider = 'default_buy_provider';
static const walletListOrder = 'wallet_list_order';
static const walletListAscending = 'wallet_list_ascending';

View file

@ -11,7 +11,7 @@ enum ProviderType {
robinhood,
dfx,
onramper,
moonpaySell,
moonpay,
}
extension ProviderTypeName on ProviderType {
@ -25,7 +25,7 @@ extension ProviderTypeName on ProviderType {
return 'DFX Connect';
case ProviderType.onramper:
return 'Onramper';
case ProviderType.moonpaySell:
case ProviderType.moonpay:
return 'MoonPay';
}
}
@ -40,7 +40,7 @@ extension ProviderTypeName on ProviderType {
return 'dfx_connect_provider';
case ProviderType.onramper:
return 'onramper_provider';
case ProviderType.moonpaySell:
case ProviderType.moonpay:
return 'moonpay_provider';
}
}
@ -55,18 +55,18 @@ class ProvidersHelper {
case WalletType.monero:
return [ProviderType.askEachTime, ProviderType.onramper, ProviderType.dfx];
case WalletType.bitcoin:
case WalletType.polygon:
case WalletType.ethereum:
return [
ProviderType.askEachTime,
ProviderType.onramper,
ProviderType.dfx,
ProviderType.robinhood,
ProviderType.moonpay,
];
case WalletType.litecoin:
case WalletType.bitcoinCash:
return [ProviderType.askEachTime, ProviderType.onramper, ProviderType.robinhood];
case WalletType.polygon:
return [ProviderType.askEachTime, ProviderType.onramper, ProviderType.dfx];
return [ProviderType.askEachTime, ProviderType.onramper, ProviderType.robinhood, ProviderType.moonpay];
case WalletType.solana:
return [ProviderType.askEachTime, ProviderType.onramper, ProviderType.robinhood];
case WalletType.none:
@ -79,23 +79,22 @@ class ProvidersHelper {
switch (walletType) {
case WalletType.bitcoin:
case WalletType.ethereum:
case WalletType.polygon:
return [
ProviderType.askEachTime,
ProviderType.onramper,
ProviderType.moonpaySell,
ProviderType.moonpay,
ProviderType.dfx,
];
case WalletType.litecoin:
case WalletType.bitcoinCash:
return [ProviderType.askEachTime, ProviderType.moonpaySell];
case WalletType.polygon:
return [ProviderType.askEachTime, ProviderType.onramper, ProviderType.dfx];
return [ProviderType.askEachTime, ProviderType.moonpay];
case WalletType.solana:
return [
ProviderType.askEachTime,
ProviderType.onramper,
ProviderType.robinhood,
ProviderType.moonpaySell,
ProviderType.moonpay,
];
case WalletType.monero:
case WalletType.nano:
@ -114,10 +113,10 @@ class ProvidersHelper {
return getIt.get<DFXBuyProvider>();
case ProviderType.onramper:
return getIt.get<OnRamperBuyProvider>();
case ProviderType.moonpay:
return getIt.get<MoonPayProvider>();
case ProviderType.askEachTime:
return null;
case ProviderType.moonpaySell:
return getIt.get<MoonPaySellProvider>();
}
}
}

View file

@ -76,7 +76,8 @@ class CWEthereum extends Ethereum {
sendAll: out.sendAll,
extractedAddress: out.extractedAddress,
isParsedAddress: out.isParsedAddress,
formattedCryptoAmount: out.formattedCryptoAmount))
formattedCryptoAmount: out.formattedCryptoAmount,
memo: out.memo))
.toList(),
priority: priority as EVMChainTransactionPriority,
currency: currency,

View file

@ -22,6 +22,8 @@ class ExchangeProviderDescription extends EnumerableItem<int> with Serializable<
ExchangeProviderDescription(title: 'Trocador', raw: 5, image: 'assets/images/trocador.png');
static const exolix =
ExchangeProviderDescription(title: 'Exolix', raw: 6, image: 'assets/images/exolix.png');
static const thorChain =
ExchangeProviderDescription(title: 'ThorChain' , raw: 8, image: 'assets/images/thorchain.png');
static const all = ExchangeProviderDescription(title: 'All trades', raw: 7, image: '');
@ -41,6 +43,8 @@ class ExchangeProviderDescription extends EnumerableItem<int> with Serializable<
return trocador;
case 6:
return exolix;
case 8:
return thorChain;
case 7:
return all;
default:

View file

@ -133,7 +133,11 @@ class ChangeNowExchangeProvider extends ExchangeProvider {
}
@override
Future<Trade> createTrade({required TradeRequest request, required bool isFixedRateMode}) async {
Future<Trade> createTrade({
required TradeRequest request,
required bool isFixedRateMode,
required bool isSendAll,
}) async {
final distributionPath = await DistributionInfo.instance.getDistributionPath();
final formattedAppVersion = int.tryParse(_settingsStore.appVersion.replaceAll('.', '')) ?? 0;
final payload = {
@ -202,7 +206,8 @@ class ChangeNowExchangeProvider extends ExchangeProvider {
createdAt: DateTime.now(),
amount: responseJSON['fromAmount']?.toString() ?? request.fromAmount,
state: TradeState.created,
payoutAddress: payoutAddress);
payoutAddress: payoutAddress,
isSendAll: isSendAll);
}
@override

View file

@ -28,7 +28,8 @@ abstract class ExchangeProvider {
Future<Limits> fetchLimits(
{required CryptoCurrency from, required CryptoCurrency to, required bool isFixedRateMode});
Future<Trade> createTrade({required TradeRequest request, required bool isFixedRateMode});
Future<Trade> createTrade(
{required TradeRequest request, required bool isFixedRateMode, required bool isSendAll});
Future<Trade> findTradeById({required String id});

View file

@ -66,6 +66,7 @@ class ExolixExchangeProvider extends ExchangeProvider {
final params = <String, String>{
'rateType': _getRateType(isFixedRateMode),
'amount': '1',
'apiToken': apiKey,
};
if (isFixedRateMode) {
params['coinFrom'] = _normalizeCurrency(to);
@ -129,7 +130,11 @@ class ExolixExchangeProvider extends ExchangeProvider {
}
@override
Future<Trade> createTrade({required TradeRequest request, required bool isFixedRateMode}) async {
Future<Trade> createTrade({
required TradeRequest request,
required bool isFixedRateMode,
required bool isSendAll,
}) async {
final headers = {'Content-Type': 'application/json'};
final body = {
'coinFrom': _normalizeCurrency(request.fromCurrency),
@ -179,7 +184,8 @@ class ExolixExchangeProvider extends ExchangeProvider {
createdAt: DateTime.now(),
amount: amount,
state: TradeState.created,
payoutAddress: payoutAddress);
payoutAddress: payoutAddress,
isSendAll: isSendAll);
}
@override

View file

@ -144,7 +144,11 @@ class SideShiftExchangeProvider extends ExchangeProvider {
}
@override
Future<Trade> createTrade({required TradeRequest request, required bool isFixedRateMode}) async {
Future<Trade> createTrade({
required TradeRequest request,
required bool isFixedRateMode,
required bool isSendAll,
}) async {
String url = '';
final body = {
'affiliateId': affiliateId,
@ -197,6 +201,7 @@ class SideShiftExchangeProvider extends ExchangeProvider {
amount: depositAmount ?? request.fromAmount,
payoutAddress: settleAddress,
createdAt: DateTime.now(),
isSendAll: isSendAll,
);
}

View file

@ -117,7 +117,11 @@ class SimpleSwapExchangeProvider extends ExchangeProvider {
}
@override
Future<Trade> createTrade({required TradeRequest request, required bool isFixedRateMode}) async {
Future<Trade> createTrade({
required TradeRequest request,
required bool isFixedRateMode,
required bool isSendAll,
}) async {
final headers = {'Content-Type': 'application/json'};
final params = {'api_key': apiKey};
final body = <String, dynamic>{
@ -162,6 +166,7 @@ class SimpleSwapExchangeProvider extends ExchangeProvider {
amount: request.fromAmount,
payoutAddress: payoutAddress,
createdAt: DateTime.now(),
isSendAll: isSendAll,
);
}

View file

@ -0,0 +1,255 @@
import 'dart:convert';
import 'package:cake_wallet/exchange/exchange_provider_description.dart';
import 'package:cake_wallet/exchange/limits.dart';
import 'package:cake_wallet/exchange/provider/exchange_provider.dart';
import 'package:cake_wallet/exchange/trade.dart';
import 'package:cake_wallet/exchange/trade_request.dart';
import 'package:cake_wallet/exchange/trade_state.dart';
import 'package:cake_wallet/exchange/utils/currency_pairs_utils.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:hive/hive.dart';
import 'package:http/http.dart' as http;
class ThorChainExchangeProvider extends ExchangeProvider {
ThorChainExchangeProvider({required this.tradesStore})
: super(pairList: supportedPairs(_notSupported));
static final List<CryptoCurrency> _notSupported = [
...(CryptoCurrency.all
.where((element) => ![
CryptoCurrency.btc,
CryptoCurrency.eth,
CryptoCurrency.ltc,
CryptoCurrency.bch,
CryptoCurrency.aave,
CryptoCurrency.dai,
CryptoCurrency.gusd,
CryptoCurrency.usdc,
CryptoCurrency.usdterc20,
CryptoCurrency.wbtc,
].contains(element))
.toList())
];
static final isRefundAddressSupported = [CryptoCurrency.eth];
static const _baseURL = 'thornode.ninerealms.com';
static const _quotePath = '/thorchain/quote/swap';
static const _txInfoPath = '/thorchain/tx/status/';
static const _affiliateName = 'cakewallet';
static const _affiliateBps = '175';
final Box<Trade> tradesStore;
@override
String get title => 'THORChain';
@override
bool get isAvailable => true;
@override
bool get isEnabled => true;
@override
bool get supportsFixedRate => false;
@override
ExchangeProviderDescription get description => ExchangeProviderDescription.thorChain;
@override
Future<bool> checkIsAvailable() async => true;
@override
Future<double> fetchRate(
{required CryptoCurrency from,
required CryptoCurrency to,
required double amount,
required bool isFixedRateMode,
required bool isReceiveAmount}) async {
try {
if (amount == 0) return 0.0;
final params = {
'from_asset': _normalizeCurrency(from),
'to_asset': _normalizeCurrency(to),
'amount': _doubleToThorChainString(amount),
'affiliate': _affiliateName,
'affiliate_bps': _affiliateBps
};
final responseJSON = await _getSwapQuote(params);
final expectedAmountOut = responseJSON['expected_amount_out'] as String? ?? '0.0';
return _thorChainAmountToDouble(expectedAmountOut) / amount;
} catch (e) {
print(e.toString());
return 0.0;
}
}
@override
Future<Limits> fetchLimits(
{required CryptoCurrency from,
required CryptoCurrency to,
required bool isFixedRateMode}) async {
final params = {
'from_asset': _normalizeCurrency(from),
'to_asset': _normalizeCurrency(to),
'amount': _doubleToThorChainString(1),
'affiliate': _affiliateName,
'affiliate_bps': _affiliateBps
};
final responseJSON = await _getSwapQuote(params);
final minAmountIn = responseJSON['recommended_min_amount_in'] as String? ?? '0.0';
return Limits(min: _thorChainAmountToDouble(minAmountIn));
}
@override
Future<Trade> createTrade({
required TradeRequest request,
required bool isFixedRateMode,
required bool isSendAll,
}) async {
String formattedToAddress = request.toAddress.startsWith('bitcoincash:')
? request.toAddress.replaceFirst('bitcoincash:', '')
: request.toAddress;
final formattedFromAmount = double.parse(request.fromAmount);
final params = {
'from_asset': _normalizeCurrency(request.fromCurrency),
'to_asset': _normalizeCurrency(request.toCurrency),
'amount': _doubleToThorChainString(formattedFromAmount),
'destination': formattedToAddress,
'affiliate': _affiliateName,
'affiliate_bps': _affiliateBps,
'refund_address':
isRefundAddressSupported.contains(request.fromCurrency) ? request.refundAddress : '',
};
final responseJSON = await _getSwapQuote(params);
final inputAddress = responseJSON['inbound_address'] as String?;
final memo = responseJSON['memo'] as String?;
return Trade(
id: '',
from: request.fromCurrency,
to: request.toCurrency,
provider: description,
inputAddress: inputAddress,
createdAt: DateTime.now(),
amount: request.fromAmount,
state: TradeState.notFound,
payoutAddress: request.toAddress,
memo: memo,
isSendAll: isSendAll);
}
@override
Future<Trade> findTradeById({required String id}) async {
if (id.isEmpty) throw Exception('Trade id is empty');
final formattedId = id.startsWith('0x') ? id.substring(2) : id;
final uri = Uri.https(_baseURL, '$_txInfoPath$formattedId');
final response = await http.get(uri);
if (response.statusCode == 404) {
throw Exception('Trade not found for id: $formattedId');
} else if (response.statusCode != 200) {
throw Exception('Unexpected HTTP status: ${response.statusCode}');
}
final responseJSON = json.decode(response.body);
final Map<String, dynamic> stagesJson = responseJSON['stages'] as Map<String, dynamic>;
final inboundObservedStarted = stagesJson['inbound_observed']?['started'] as bool? ?? true;
if (!inboundObservedStarted) {
throw Exception('Trade has not started for id: $formattedId');
}
final currentState = _updateStateBasedOnStages(stagesJson) ?? TradeState.notFound;
final tx = responseJSON['tx'];
final String fromAddress = tx['from_address'] as String? ?? '';
final String toAddress = tx['to_address'] as String? ?? '';
final List<dynamic> coins = tx['coins'] as List<dynamic>;
final String? memo = tx['memo'] as String?;
final parts = memo?.split(':') ?? [];
final String toChain = parts.length > 1 ? parts[1].split('.')[0] : '';
final String toAsset = parts.length > 1 && parts[1].split('.').length > 1
? parts[1].split('.')[1].split('-')[0]
: '';
final formattedToChain = CryptoCurrency.fromString(toChain);
final toAssetWithChain = CryptoCurrency.fromString(toAsset, walletCurrency: formattedToChain);
final plannedOutTxs = responseJSON['planned_out_txs'] as List<dynamic>?;
final isRefund = plannedOutTxs?.any((tx) => tx['refund'] == true) ?? false;
return Trade(
id: id,
from: CryptoCurrency.fromString(tx['chain'] as String? ?? ''),
to: toAssetWithChain,
provider: description,
inputAddress: fromAddress,
payoutAddress: toAddress,
amount: coins.first['amount'] as String? ?? '0.0',
state: currentState,
memo: memo,
isRefund: isRefund,
);
}
Future<Map<String, dynamic>> _getSwapQuote(Map<String, String> params) async {
Uri uri = Uri.https(_baseURL, _quotePath, params);
final response = await http.get(uri);
if (response.statusCode != 200) {
throw Exception('Unexpected HTTP status: ${response.statusCode}');
}
if (response.body.contains('error')) {
throw Exception('Unexpected response: ${response.body}');
}
return json.decode(response.body) as Map<String, dynamic>;
}
String _normalizeCurrency(CryptoCurrency currency) {
final networkTitle = currency.tag == 'ETH' ? 'ETH' : currency.title;
return '$networkTitle.${currency.title}';
}
String _doubleToThorChainString(double amount) => (amount * 1e8).toInt().toString();
double _thorChainAmountToDouble(String amount) => double.parse(amount) / 1e8;
TradeState? _updateStateBasedOnStages(Map<String, dynamic> stages) {
TradeState? currentState;
if (stages['inbound_observed']['completed'] as bool? ?? false) {
currentState = TradeState.confirmation;
}
if (stages['inbound_confirmation_counted']['completed'] as bool? ?? false) {
currentState = TradeState.confirmed;
}
if (stages['inbound_finalised']['completed'] as bool? ?? false) {
currentState = TradeState.processing;
}
if (stages['swap_finalised']['completed'] as bool? ?? false) {
currentState = TradeState.traded;
}
if (stages['outbound_signed']['completed'] as bool? ?? false) {
currentState = TradeState.success;
}
return currentState;
}
}

View file

@ -13,7 +13,8 @@ import 'package:http/http.dart';
class TrocadorExchangeProvider extends ExchangeProvider {
TrocadorExchangeProvider({this.useTorOnly = false, this.providerStates = const {}})
: _lastUsedRateId = '', _provider = [],
: _lastUsedRateId = '',
_provider = [],
super(pairList: supportedPairs(_notSupported));
bool useTorOnly;
@ -23,7 +24,7 @@ class TrocadorExchangeProvider extends ExchangeProvider {
'Swapter',
'StealthEx',
'Simpleswap',
'Swapuz'
'Swapuz',
'ChangeNow',
'Changehero',
'FixedFloat',
@ -144,8 +145,11 @@ class TrocadorExchangeProvider extends ExchangeProvider {
}
@override
Future<Trade> createTrade({required TradeRequest request, required bool isFixedRateMode}) async {
Future<Trade> createTrade({
required TradeRequest request,
required bool isFixedRateMode,
required bool isSendAll,
}) async {
final params = {
'api_key': apiKey,
'ticker_from': _normalizeCurrency(request.fromCurrency),
@ -172,7 +176,6 @@ class TrocadorExchangeProvider extends ExchangeProvider {
params['id'] = _lastUsedRateId;
}
String firstAvailableProvider = '';
for (var provider in _provider) {
@ -225,7 +228,8 @@ class TrocadorExchangeProvider extends ExchangeProvider {
providerName: providerName,
createdAt: DateTime.tryParse(date)?.toLocal(),
amount: responseJSON['amount_from']?.toString() ?? request.fromAmount,
payoutAddress: payoutAddress);
payoutAddress: payoutAddress,
isSendAll: isSendAll);
}
@override

View file

@ -27,7 +27,11 @@ class Trade extends HiveObject {
this.password,
this.providerId,
this.providerName,
this.fromWalletAddress
this.fromWalletAddress,
this.memo,
this.txId,
this.isRefund,
this.isSendAll,
}) {
if (provider != null) providerRaw = provider.raw;
@ -105,6 +109,18 @@ class Trade extends HiveObject {
@HiveField(17)
String? fromWalletAddress;
@HiveField(18)
String? memo;
@HiveField(19)
String? txId;
@HiveField(20)
bool? isRefund;
@HiveField(21)
bool? isSendAll;
static Trade fromMap(Map<String, Object?> map) {
return Trade(
id: map['id'] as String,
@ -115,8 +131,11 @@ class Trade extends HiveObject {
map['date'] != null ? DateTime.fromMillisecondsSinceEpoch(map['date'] as int) : null,
amount: map['amount'] as String,
walletId: map['wallet_id'] as String,
fromWalletAddress: map['from_wallet_address'] as String?
);
fromWalletAddress: map['from_wallet_address'] as String?,
memo: map['memo'] as String?,
txId: map['tx_id'] as String?,
isRefund: map['isRefund'] as bool?,
isSendAll: map['isSendAll'] as bool?);
}
Map<String, dynamic> toMap() {
@ -128,7 +147,11 @@ class Trade extends HiveObject {
'date': createdAt != null ? createdAt!.millisecondsSinceEpoch : null,
'amount': amount,
'wallet_id': walletId,
'from_wallet_address': fromWalletAddress
'from_wallet_address': fromWalletAddress,
'memo': memo,
'tx_id': txId,
'isRefund': isRefund,
'isSendAll': isSendAll,
};
}

View file

@ -41,6 +41,8 @@ class TradeState extends EnumerableItem<String> with Serializable<String> {
static const success = TradeState(raw: 'success', title: 'Success');
static TradeState deserialize({required String raw}) {
switch (raw) {
case 'NOT_FOUND':
return notFound;
case 'pending':
return pending;
case 'confirming':
@ -98,6 +100,7 @@ class TradeState extends EnumerableItem<String> with Serializable<String> {
case 'sending':
return sending;
case 'success':
case 'done':
return success;
default:
throw Exception('Unexpected token: $raw in TradeState deserialize');

View file

@ -163,7 +163,7 @@ Future<void> initializeAppConfigs() async {
transactionDescriptions: transactionDescriptions,
secureStorage: secureStorage,
anonpayInvoiceInfo: anonpayInvoiceInfo,
initialMigrationVersion: 27);
initialMigrationVersion: 29);
}
Future<void> initialSetup(

View file

@ -74,8 +74,22 @@ class CWSolana extends Solana {
}
@override
Future<void> addSPLToken(WalletBase wallet, CryptoCurrency token) async =>
await (wallet as SolanaWallet).addSPLToken(token as SPLToken);
Future<void> addSPLToken(
WalletBase wallet,
CryptoCurrency token,
String contractAddress,
) async {
final splToken = SPLToken(
name: token.name,
symbol: token.title,
mintAddress: contractAddress,
decimal: token.decimals,
mint: token.name.toUpperCase(),
enabled: token.enabled,
);
await (wallet as SolanaWallet).addSPLToken(splToken);
}
@override
Future<void> deleteSPLToken(WalletBase wallet, CryptoCurrency token) async =>
@ -115,4 +129,9 @@ class CWSolana extends Solana {
return null;
}
@override
double? getEstimateFees(WalletBase wallet) {
return (wallet as SolanaWallet).estimatedFee;
}
}

View file

@ -1,6 +1,7 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/widgets/option_tile.dart';
import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
import 'package:cake_wallet/themes/extensions/option_tile_theme.dart';
import 'package:cake_wallet/themes/extensions/transaction_trade_theme.dart';
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
@ -25,45 +26,46 @@ class BuySellOptionsPage extends BasePage {
? dashboardViewModel.availableBuyProviders
: dashboardViewModel.availableSellProviders;
return Container(
child: Center(
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: 330),
child: Column(
children: [
...availableProviders.map((provider) {
final icon = Image.asset(
isLightMode ? provider.lightIcon : provider.darkIcon,
height: 40,
width: 40,
);
return ScrollableWithBottomSection(
content: Container(
child: Center(
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: 330),
child: Column(
children: [
...availableProviders.map((provider) {
final icon = Image.asset(
isLightMode ? provider.lightIcon : provider.darkIcon,
height: 40,
width: 40,
);
return Padding(
padding: EdgeInsets.only(top: 24),
child: OptionTile(
image: icon,
title: provider.toString(),
description: provider.providerDescription,
onPressed: () => provider.launchProvider(context, isBuyAction),
),
);
}).toList(),
Spacer(),
Padding(
padding: EdgeInsets.fromLTRB(24, 24, 24, 32),
child: Text(
isBuyAction
? S.of(context).select_buy_provider_notice
: S.of(context).select_sell_provider_notice,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.normal,
color: Theme.of(context).extension<TransactionTradeTheme>()!.detailsTitlesColor,
),
),
),
],
return Padding(
padding: EdgeInsets.only(top: 24),
child: OptionTile(
image: icon,
title: provider.toString(),
description: provider.providerDescription,
onPressed: () => provider.launchProvider(context, isBuyAction),
),
);
}).toList(),
],
),
),
),
),
bottomSection: Padding(
padding: EdgeInsets.fromLTRB(24, 24, 24, 32),
child: Text(
isBuyAction
? S.of(context).select_buy_provider_notice
: S.of(context).select_sell_provider_notice,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.normal,
color: Theme.of(context).extension<TransactionTradeTheme>()!.detailsTitlesColor,
),
),
),

View file

@ -60,7 +60,7 @@ class BuyWebViewPageBodyState extends State<BuyWebViewPageBody> {
_saveOrder(keyword: 'completed', splitSymbol: '/');
}
if (widget.buyViewModel.selectedProvider is MoonPayBuyProvider) {
if (widget.buyViewModel.selectedProvider is MoonPayProvider) {
_saveOrder(keyword: 'transactionId', splitSymbol: '=');
}
}

View file

@ -103,7 +103,16 @@ class _DashboardPageView extends BasePage {
Widget get endDrawer => MenuWidget(dashboardViewModel);
@override
Widget leading(BuildContext context) => ServicesUpdatesWidget(dashboardViewModel.getServicesStatus());
Widget leading(BuildContext context) {
return Observer(
builder: (context) {
if (dashboardViewModel.isEnabledBulletinAction) {
return ServicesUpdatesWidget(dashboardViewModel.getServicesStatus());
}
return const SizedBox();
},
);
}
@override
Widget middle(BuildContext context) {

View file

@ -7,13 +7,15 @@ class SideMenuItem extends StatelessWidget {
required this.onTap,
this.imagePath,
this.icon,
this.widget,
this.isSelected = false,
}) : assert((icon != null && imagePath == null) || (icon == null && imagePath != null));
}) : assert(widget != null || icon != null || imagePath != null);
final void Function() onTap;
final String? imagePath;
final IconData? icon;
final bool isSelected;
final Widget? widget;
Color _setColor(BuildContext context) {
if (isSelected) {
@ -28,18 +30,7 @@ class SideMenuItem extends StatelessWidget {
return InkWell(
child: Padding(
padding: EdgeInsets.all(20),
child: icon != null
? Icon(
icon,
color: _setColor(context),
)
: Image.asset(
imagePath ?? '',
fit: BoxFit.cover,
height: 30,
width: 30,
color: _setColor(context),
),
child: widget ?? _getIcon(context),
),
onTap: () => onTap.call(),
highlightColor: Colors.transparent,
@ -48,4 +39,19 @@ class SideMenuItem extends StatelessWidget {
splashColor: Colors.transparent,
);
}
Widget _getIcon(BuildContext context) {
return icon != null
? Icon(
icon,
color: _setColor(context),
)
: Image.asset(
imagePath ?? '',
fit: BoxFit.cover,
height: 30,
width: 30,
color: _setColor(context),
);
}
}

View file

@ -9,6 +9,7 @@ import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/desktop_sideba
import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart';
import 'package:cake_wallet/src/screens/dashboard/widgets/sync_indicator.dart';
import 'package:cake_wallet/src/screens/wallet_connect/widgets/modals/bottom_sheet_listener.dart';
import 'package:cake_wallet/src/widgets/services_updates_widget.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:flutter/cupertino.dart';
@ -105,12 +106,18 @@ class DesktopSidebarWrapper extends BasePage {
? selectedIconPath
: unselectedIconPath,
),
SideMenuItem(
widget: ServicesUpdatesWidget(dashboardViewModel.getServicesStatus()),
isSelected: desktopSidebarViewModel.currentPage == SidebarItem.status,
onTap: () {},
),
],
bottomItems: [
SideMenuItem(
imagePath: 'assets/images/support_icon.png',
isSelected: desktopSidebarViewModel.currentPage == SidebarItem.support,
onTap: () => desktopSidebarViewModel.onPageChange(SidebarItem.support)),
imagePath: 'assets/images/support_icon.png',
isSelected: desktopSidebarViewModel.currentPage == SidebarItem.support,
onTap: () => desktopSidebarViewModel.onPageChange(SidebarItem.support),
),
SideMenuItem(
imagePath: 'assets/images/settings_outline.png',
isSelected: desktopSidebarViewModel.currentPage == SidebarItem.settings,

View file

@ -195,12 +195,14 @@ class _EditTokenPageBodyState extends State<EditTokenPageBody> {
onPressed: () async {
if (_formKey.currentState!.validate() &&
(!_showDisclaimer || _disclaimerChecked)) {
await widget.homeSettingsViewModel.addToken(Erc20Token(
name: _tokenNameController.text,
symbol: _tokenSymbolController.text,
await widget.homeSettingsViewModel.addToken(
token: CryptoCurrency(
name: _tokenNameController.text,
title: _tokenSymbolController.text.toUpperCase(),
decimals: int.parse(_tokenDecimalController.text),
),
contractAddress: _contractAddressController.text,
decimal: int.parse(_tokenDecimalController.text),
));
);
if (context.mounted) {
Navigator.pop(context);
}

View file

@ -9,7 +9,7 @@ class FilterTile extends StatelessWidget {
Widget build(BuildContext context) {
return Container(
width: double.infinity,
padding: EdgeInsets.symmetric(vertical: 8.0, horizontal: 24.0),
padding: EdgeInsets.symmetric(vertical: 6.0, horizontal: 24.0),
child: child,
);
}

View file

@ -20,6 +20,7 @@ class SyncIndicatorIcon extends StatelessWidget {
static const String created = 'created';
static const String fetching = 'fetching';
static const String finished = 'finished';
static const String success = 'success';
@override
Widget build(BuildContext context) {
@ -45,6 +46,7 @@ class SyncIndicatorIcon extends StatelessWidget {
indicatorColor = Colors.red;
break;
case finished:
case success:
indicatorColor = PaletteDark.brightGreen;
break;
default:

View file

@ -34,7 +34,9 @@ class TradeRow extends StatelessWidget {
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
_getPoweredImage(provider)!,
ClipRRect(
borderRadius: BorderRadius.circular(50),
child: Image.asset(provider.image, width: 36, height: 36)),
SizedBox(width: 12),
Expanded(
child: Column(
@ -69,38 +71,4 @@ class TradeRow extends StatelessWidget {
),
));
}
Widget? _getPoweredImage(ExchangeProviderDescription provider) {
Widget? image;
switch (provider) {
case ExchangeProviderDescription.xmrto:
image = Image.asset('assets/images/xmrto.png', height: 36, width: 36);
break;
case ExchangeProviderDescription.changeNow:
image = Image.asset('assets/images/changenow.png', height: 36, width: 36);
break;
case ExchangeProviderDescription.morphToken:
image = Image.asset('assets/images/morph.png', height: 36, width: 36);
break;
case ExchangeProviderDescription.sideShift:
image = Image.asset('assets/images/sideshift.png', width: 36, height: 36);
break;
case ExchangeProviderDescription.simpleSwap:
image = Image.asset('assets/images/simpleSwap.png', width: 36, height: 36);
break;
case ExchangeProviderDescription.trocador:
image = ClipRRect(
borderRadius: BorderRadius.circular(50),
child: Image.asset('assets/images/trocador.png', width: 36, height: 36));
break;
case ExchangeProviderDescription.exolix:
image = Image.asset('assets/images/exolix.png', width: 36, height: 36);
break;
default:
image = null;
}
return image;
}
}

View file

@ -1,3 +1,5 @@
import 'package:cake_wallet/exchange/exchange_provider_description.dart';
import 'package:cake_wallet/exchange/provider/thorchain_exchange.provider.dart';
import 'package:cake_wallet/themes/extensions/exchange_page_theme.dart';
import 'package:cake_wallet/themes/extensions/keyboard_theme.dart';
import 'package:cake_wallet/core/auth_service.dart';
@ -60,7 +62,7 @@ class ExchangePage extends BasePage {
final _receiveAmountFocus = FocusNode();
final _receiveAddressFocus = FocusNode();
final _receiveAmountDebounce = Debounce(Duration(milliseconds: 500));
final _depositAmountDebounce = Debounce(Duration(milliseconds: 500));
Debounce _depositAmountDebounce = Debounce(Duration(milliseconds: 500));
var _isReactionsSet = false;
final arrowBottomPurple = Image.asset(
@ -184,7 +186,13 @@ class ExchangePage extends BasePage {
StandardCheckbox(
value: exchangeViewModel.isFixedRateMode,
caption: S.of(context).fixed_rate,
onChanged: (value) => exchangeViewModel.isFixedRateMode = value,
onChanged: (value) {
if (value) {
exchangeViewModel.enableFixedRateMode();
} else {
exchangeViewModel.isFixedRateMode = false;
}
},
),
],
)),
@ -384,7 +392,7 @@ class ExchangePage extends BasePage {
(CryptoCurrency currency) => _onCurrencyChange(currency, exchangeViewModel, depositKey));
reaction((_) => exchangeViewModel.depositAmount, (String amount) {
if (depositKey.currentState!.amountController.text != amount) {
if (depositKey.currentState!.amountController.text != amount && amount != S.of(context).all) {
depositKey.currentState!.amountController.text = amount;
}
});
@ -431,7 +439,9 @@ class ExchangePage extends BasePage {
}
if (state is TradeIsCreatedSuccessfully) {
exchangeViewModel.reset();
Navigator.of(context).pushNamed(Routes.exchangeConfirm);
(exchangeViewModel.tradesStore.trade?.provider == ExchangeProviderDescription.thorChain)
? Navigator.of(context).pushReplacementNamed(Routes.exchangeTrade)
: Navigator.of(context).pushReplacementNamed(Routes.exchangeConfirm);
}
});
@ -467,7 +477,16 @@ class ExchangePage extends BasePage {
.addListener(() => exchangeViewModel.depositAddress = depositAddressController.text);
depositAmountController.addListener(() {
if (depositAmountController.text != exchangeViewModel.depositAmount) {
if (depositAmountController.text != exchangeViewModel.depositAmount &&
depositAmountController.text != S.of(context).all) {
exchangeViewModel.isSendAllEnabled = false;
final isThorChain = exchangeViewModel.selectedProviders
.any((provider) => provider is ThorChainExchangeProvider);
_depositAmountDebounce = isThorChain
? Debounce(Duration(milliseconds: 1000))
: Debounce(Duration(milliseconds: 500));
_depositAmountDebounce.run(() {
exchangeViewModel.changeDepositAmount(amount: depositAmountController.text);
exchangeViewModel.isReceiveAmountEntered = false;
@ -515,7 +534,7 @@ class ExchangePage extends BasePage {
_receiveAmountFocus.addListener(() {
if (_receiveAmountFocus.hasFocus) {
exchangeViewModel.isFixedRateMode = true;
exchangeViewModel.enableFixedRateMode();
}
// exchangeViewModel.changeReceiveAmount(amount: receiveAmountController.text);
});
@ -589,8 +608,9 @@ class ExchangePage extends BasePage {
onDispose: disposeBestRateSync,
hasAllAmount: exchangeViewModel.hasAllAmount,
allAmount: exchangeViewModel.hasAllAmount
? () => exchangeViewModel.calculateDepositAllAmount()
? () => exchangeViewModel.enableSendAllAmount()
: null,
isAllAmountEnabled: exchangeViewModel.isSendAllEnabled,
amountFocusNode: _depositAmountFocus,
addressFocusNode: _depositAddressFocus,
key: depositKey,
@ -626,10 +646,12 @@ class ExchangePage extends BasePage {
},
imageArrow: arrowBottomPurple,
currencyButtonColor: Colors.transparent,
addressButtonsColor: Theme.of(context).extension<SendPageTheme>()!.textFieldButtonColor,
borderColor: Theme.of(context).extension<ExchangePageTheme>()!.textFieldBorderTopPanelColor,
addressButtonsColor:
Theme.of(context).extension<SendPageTheme>()!.textFieldButtonColor,
borderColor:
Theme.of(context).extension<ExchangePageTheme>()!.textFieldBorderTopPanelColor,
currencyValueValidator: (value) {
return !exchangeViewModel.isFixedRateMode
return !exchangeViewModel.isFixedRateMode && value != S.of(context).all
? AmountValidator(
isAutovalidate: true,
currency: exchangeViewModel.depositCurrency,
@ -673,8 +695,10 @@ class ExchangePage extends BasePage {
exchangeViewModel.changeReceiveCurrency(currency: currency),
imageArrow: arrowBottomCakeGreen,
currencyButtonColor: Colors.transparent,
addressButtonsColor: Theme.of(context).extension<SendPageTheme>()!.textFieldButtonColor,
borderColor: Theme.of(context).extension<ExchangePageTheme>()!.textFieldBorderBottomPanelColor,
addressButtonsColor:
Theme.of(context).extension<SendPageTheme>()!.textFieldButtonColor,
borderColor:
Theme.of(context).extension<ExchangePageTheme>()!.textFieldBorderBottomPanelColor,
currencyValueValidator: (value) {
return exchangeViewModel.isFixedRateMode
? AmountValidator(

View file

@ -56,17 +56,14 @@ class ExchangeTemplatePage extends BasePage {
height: 8,
);
final depositWalletName =
exchangeViewModel.depositCurrency == CryptoCurrency.xmr
final depositWalletName = exchangeViewModel.depositCurrency == CryptoCurrency.xmr
? exchangeViewModel.wallet.name
: null;
final receiveWalletName =
exchangeViewModel.receiveCurrency == CryptoCurrency.xmr
final receiveWalletName = exchangeViewModel.receiveCurrency == CryptoCurrency.xmr
? exchangeViewModel.wallet.name
: null;
WidgetsBinding.instance
.addPostFrameCallback((_) => _setReactions(context, exchangeViewModel));
WidgetsBinding.instance.addPostFrameCallback((_) => _setReactions(context, exchangeViewModel));
return KeyboardActions(
disableScroll: true,
@ -76,128 +73,125 @@ class ExchangeTemplatePage extends BasePage {
nextFocus: false,
actions: [
KeyboardActionsItem(
focusNode: _depositAmountFocus,
toolbarButtons: [(_) => KeyboardDoneButton()]),
focusNode: _depositAmountFocus, toolbarButtons: [(_) => KeyboardDoneButton()]),
KeyboardActionsItem(
focusNode: _receiveAmountFocus,
toolbarButtons: [(_) => KeyboardDoneButton()])
focusNode: _receiveAmountFocus, toolbarButtons: [(_) => KeyboardDoneButton()])
]),
child: Container(
color: Theme.of(context).colorScheme.background,
child: Form(
key: _formKey,
child: ScrollableWithBottomSection(
contentPadding: EdgeInsets.only(bottom: 24),
content: Container(
padding: EdgeInsets.only(bottom: 32),
decoration: BoxDecoration(
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(24),
bottomRight: Radius.circular(24)
),
gradient: LinearGradient(
colors: [
Theme.of(context).extension<ExchangePageTheme>()!.firstGradientBottomPanelColor,
Theme.of(context).extension<ExchangePageTheme>()!.secondGradientBottomPanelColor,
],
stops: [0.35, 1.0],
begin: Alignment.topLeft,
end: Alignment.bottomRight),
),
child: FocusTraversalGroup(
policy: OrderedTraversalPolicy(),
child: Column(
children: <Widget>[
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(24),
bottomRight: Radius.circular(24)
color: Theme.of(context).colorScheme.background,
child: Form(
key: _formKey,
child: ScrollableWithBottomSection(
contentPadding: EdgeInsets.only(bottom: 24),
content: Container(
padding: EdgeInsets.only(bottom: 32),
decoration: BoxDecoration(
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(24), bottomRight: Radius.circular(24)),
gradient: LinearGradient(colors: [
Theme.of(context)
.extension<ExchangePageTheme>()!
.firstGradientBottomPanelColor,
Theme.of(context)
.extension<ExchangePageTheme>()!
.secondGradientBottomPanelColor,
], stops: [
0.35,
1.0
], begin: Alignment.topLeft, end: Alignment.bottomRight),
),
child: FocusTraversalGroup(
policy: OrderedTraversalPolicy(),
child: Column(
children: <Widget>[
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(24),
bottomRight: Radius.circular(24)),
gradient: LinearGradient(colors: [
Theme.of(context)
.extension<ExchangePageTheme>()!
.firstGradientTopPanelColor,
Theme.of(context)
.extension<ExchangePageTheme>()!
.secondGradientTopPanelColor,
], begin: Alignment.topLeft, end: Alignment.bottomRight),
),
padding: EdgeInsets.fromLTRB(24, 100, 24, 32),
child: Observer(
builder: (_) => ExchangeCard(
amountFocusNode: _depositAmountFocus,
key: depositKey,
title: S.of(context).you_will_send,
initialCurrency: exchangeViewModel.depositCurrency,
initialWalletName: depositWalletName ?? '',
initialAddress: exchangeViewModel.depositCurrency ==
exchangeViewModel.wallet.currency
? exchangeViewModel.wallet.walletAddresses.address
: exchangeViewModel.depositAddress,
initialIsAmountEditable: true,
initialIsAddressEditable: exchangeViewModel.isDepositAddressEnabled,
isAmountEstimated: false,
hasRefundAddress: true,
isMoneroWallet: exchangeViewModel.isMoneroWallet,
currencies: CryptoCurrency.all,
onCurrencySelected: (currency) =>
exchangeViewModel.changeDepositCurrency(currency: currency),
imageArrow: arrowBottomPurple,
currencyButtonColor: Colors.transparent,
addressButtonsColor: Theme.of(context)
.extension<ExchangePageTheme>()!
.textFieldButtonColor,
borderColor: Theme.of(context)
.extension<ExchangePageTheme>()!
.textFieldBorderBottomPanelColor,
currencyValueValidator:
AmountValidator(currency: exchangeViewModel.depositCurrency),
//addressTextFieldValidator: AddressValidator(
// type: exchangeViewModel.depositCurrency),
),
),
),
gradient: LinearGradient(
colors: [
Theme.of(context).extension<ExchangePageTheme>()!.firstGradientTopPanelColor,
Theme.of(context).extension<ExchangePageTheme>()!.secondGradientTopPanelColor,
],
begin: Alignment.topLeft,
end: Alignment.bottomRight),
),
padding: EdgeInsets.fromLTRB(24, 100, 24, 32),
child: Observer(
builder: (_) => ExchangeCard(
amountFocusNode: _depositAmountFocus,
key: depositKey,
title: S.of(context).you_will_send,
initialCurrency:
exchangeViewModel.depositCurrency,
initialWalletName: depositWalletName ?? '',
initialAddress: exchangeViewModel
.depositCurrency ==
exchangeViewModel.wallet.currency
? exchangeViewModel.wallet.walletAddresses.address
: exchangeViewModel.depositAddress,
initialIsAmountEditable: true,
initialIsAddressEditable: exchangeViewModel
.isDepositAddressEnabled,
isAmountEstimated: false,
hasRefundAddress: true,
isMoneroWallet: exchangeViewModel.isMoneroWallet,
currencies: CryptoCurrency.all,
onCurrencySelected: (currency) =>
exchangeViewModel.changeDepositCurrency(
currency: currency),
imageArrow: arrowBottomPurple,
currencyButtonColor: Colors.transparent,
addressButtonsColor:
Theme.of(context).extension<ExchangePageTheme>()!.textFieldButtonColor,
borderColor: Theme.of(context).extension<ExchangePageTheme>()!.textFieldBorderBottomPanelColor,
currencyValueValidator: AmountValidator(
currency: exchangeViewModel.depositCurrency),
//addressTextFieldValidator: AddressValidator(
// type: exchangeViewModel.depositCurrency),
),
),
Padding(
padding: EdgeInsets.only(top: 29, left: 24, right: 24),
child: Observer(
builder: (_) => ExchangeCard(
amountFocusNode: _receiveAmountFocus,
key: receiveKey,
title: S.of(context).you_will_get,
initialCurrency: exchangeViewModel.receiveCurrency,
initialWalletName: receiveWalletName ?? '',
initialAddress: exchangeViewModel.receiveCurrency ==
exchangeViewModel.wallet.currency
? exchangeViewModel.wallet.walletAddresses.address
: exchangeViewModel.receiveAddress,
initialIsAmountEditable: false,
isAmountEstimated: true,
isMoneroWallet: exchangeViewModel.isMoneroWallet,
currencies: exchangeViewModel.receiveCurrencies,
onCurrencySelected: (currency) => exchangeViewModel
.changeReceiveCurrency(currency: currency),
imageArrow: arrowBottomCakeGreen,
currencyButtonColor: Colors.transparent,
addressButtonsColor: Theme.of(context)
.extension<ExchangePageTheme>()!
.textFieldButtonColor,
borderColor: Theme.of(context)
.extension<ExchangePageTheme>()!
.textFieldBorderBottomPanelColor,
currencyValueValidator: AmountValidator(
currency: exchangeViewModel.receiveCurrency),
//addressTextFieldValidator: AddressValidator(
// type: exchangeViewModel.receiveCurrency),
)),
)
],
),
Padding(
padding: EdgeInsets.only(top: 29, left: 24, right: 24),
child: Observer(
builder: (_) => ExchangeCard(
amountFocusNode: _receiveAmountFocus,
key: receiveKey,
title: S.of(context).you_will_get,
initialCurrency:
exchangeViewModel.receiveCurrency,
initialWalletName: receiveWalletName ?? '',
initialAddress:
exchangeViewModel.receiveCurrency ==
exchangeViewModel.wallet.currency
? exchangeViewModel.wallet.walletAddresses.address
: exchangeViewModel.receiveAddress,
initialIsAmountEditable: false,
isAmountEstimated: true,
isMoneroWallet: exchangeViewModel.isMoneroWallet,
currencies: exchangeViewModel.receiveCurrencies,
onCurrencySelected: (currency) =>
exchangeViewModel.changeReceiveCurrency(
currency: currency),
imageArrow: arrowBottomCakeGreen,
currencyButtonColor: Colors.transparent,
addressButtonsColor:
Theme.of(context).extension<ExchangePageTheme>()!.textFieldButtonColor,
borderColor: Theme.of(context).extension<ExchangePageTheme>()!.textFieldBorderBottomPanelColor,
currencyValueValidator: AmountValidator(
currency: exchangeViewModel.receiveCurrency),
//addressTextFieldValidator: AddressValidator(
// type: exchangeViewModel.receiveCurrency),
)),
)
],
),
),
),
),
bottomSectionPadding:
EdgeInsets.only(left: 24, right: 24, bottom: 24),
bottomSection: Column(children: <Widget>[
bottomSectionPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24),
bottomSection: Column(children: <Widget>[
Padding(
padding: EdgeInsets.only(bottom: 15),
child: Observer(
@ -217,36 +211,31 @@ class ExchangeTemplatePage extends BasePage {
),
),
PrimaryButton(
onPressed: () {
if (_formKey.currentState != null && _formKey.currentState!.validate()) {
exchangeViewModel.addTemplate(
amount: exchangeViewModel.depositAmount,
depositCurrency:
exchangeViewModel.depositCurrency.name,
depositCurrencyTitle: exchangeViewModel
.depositCurrency.title + ' ${exchangeViewModel.depositCurrency.tag ?? ''}',
receiveCurrency:
exchangeViewModel.receiveCurrency.name,
receiveCurrencyTitle: exchangeViewModel
.receiveCurrency.title + ' ${exchangeViewModel.receiveCurrency.tag ?? ''}',
provider: exchangeViewModel.provider.toString(),
depositAddress: exchangeViewModel.depositAddress,
receiveAddress: exchangeViewModel.receiveAddress);
exchangeViewModel.updateTemplate();
Navigator.of(context).pop();
}
},
text: S.of(context).save,
color: Theme.of(context).primaryColor,
textColor: Colors.white),
]),
))
)
);
onPressed: () {
if (_formKey.currentState != null && _formKey.currentState!.validate()) {
exchangeViewModel.addTemplate(
amount: exchangeViewModel.depositAmount,
depositCurrency: exchangeViewModel.depositCurrency.name,
depositCurrencyTitle: exchangeViewModel.depositCurrency.title +
' ${exchangeViewModel.depositCurrency.tag ?? ''}',
receiveCurrency: exchangeViewModel.receiveCurrency.name,
receiveCurrencyTitle: exchangeViewModel.receiveCurrency.title +
' ${exchangeViewModel.receiveCurrency.tag ?? ''}',
provider: exchangeViewModel.provider.toString(),
depositAddress: exchangeViewModel.depositAddress,
receiveAddress: exchangeViewModel.receiveAddress);
exchangeViewModel.updateTemplate();
Navigator.of(context).pop();
}
},
text: S.of(context).save,
color: Theme.of(context).primaryColor,
textColor: Colors.white),
]),
))));
}
void _setReactions(
BuildContext context, ExchangeViewModel exchangeViewModel) {
void _setReactions(BuildContext context, ExchangeViewModel exchangeViewModel) {
if (_isReactionsSet) {
return;
}
@ -272,33 +261,27 @@ class ExchangeTemplatePage extends BasePage {
// key.currentState.changeLimits(min: min, max: max);
// }
_onCurrencyChange(
exchangeViewModel.receiveCurrency, exchangeViewModel, receiveKey);
_onCurrencyChange(
exchangeViewModel.depositCurrency, exchangeViewModel, depositKey);
_onCurrencyChange(exchangeViewModel.receiveCurrency, exchangeViewModel, receiveKey);
_onCurrencyChange(exchangeViewModel.depositCurrency, exchangeViewModel, depositKey);
reaction(
(_) => exchangeViewModel.wallet.name,
(String _) => _onWalletNameChange(
exchangeViewModel, exchangeViewModel.receiveCurrency, receiveKey));
(_) => exchangeViewModel.wallet.name,
(String _) =>
_onWalletNameChange(exchangeViewModel, exchangeViewModel.receiveCurrency, receiveKey));
reaction(
(_) => exchangeViewModel.wallet.name,
(String _) => _onWalletNameChange(
exchangeViewModel, exchangeViewModel.depositCurrency, depositKey));
(_) => exchangeViewModel.wallet.name,
(String _) =>
_onWalletNameChange(exchangeViewModel, exchangeViewModel.depositCurrency, depositKey));
reaction(
(_) => exchangeViewModel.receiveCurrency,
(CryptoCurrency currency) =>
_onCurrencyChange(currency, exchangeViewModel, receiveKey));
reaction((_) => exchangeViewModel.receiveCurrency,
(CryptoCurrency currency) => _onCurrencyChange(currency, exchangeViewModel, receiveKey));
reaction(
(_) => exchangeViewModel.depositCurrency,
(CryptoCurrency currency) =>
_onCurrencyChange(currency, exchangeViewModel, depositKey));
reaction((_) => exchangeViewModel.depositCurrency,
(CryptoCurrency currency) => _onCurrencyChange(currency, exchangeViewModel, depositKey));
reaction((_) => exchangeViewModel.depositAmount, (String amount) {
if (depositKey.currentState!.amountController.text != amount) {
if (depositKey.currentState!.amountController.text != amount && amount != S.of(context).all) {
depositKey.currentState!.amountController.text = amount;
}
});
@ -309,10 +292,9 @@ class ExchangeTemplatePage extends BasePage {
}
});
reaction((_) => exchangeViewModel.isDepositAddressEnabled,
(bool isEnabled) {
depositKey.currentState!.isAddressEditable(isEditable: isEnabled);
});
reaction((_) => exchangeViewModel.isDepositAddressEnabled, (bool isEnabled) {
depositKey.currentState!.isAddressEditable(isEditable: isEnabled);
});
reaction((_) => exchangeViewModel.receiveAmount, (String amount) {
if (receiveKey.currentState!.amountController.text != amount) {
@ -353,30 +335,28 @@ class ExchangeTemplatePage extends BasePage {
receiveKey.currentState.changeLimits(min: null, max: null);
});*/
depositAddressController.addListener(
() => exchangeViewModel.depositAddress = depositAddressController.text);
depositAddressController
.addListener(() => exchangeViewModel.depositAddress = depositAddressController.text);
depositAmountController.addListener(() {
if (depositAmountController.text != exchangeViewModel.depositAmount) {
exchangeViewModel.changeDepositAmount(
amount: depositAmountController.text);
if (depositAmountController.text != exchangeViewModel.depositAmount &&
exchangeViewModel.depositAmount != S.of(context).all) {
exchangeViewModel.changeDepositAmount(amount: depositAmountController.text);
exchangeViewModel.isReceiveAmountEntered = false;
}
});
receiveAddressController.addListener(
() => exchangeViewModel.receiveAddress = receiveAddressController.text);
receiveAddressController
.addListener(() => exchangeViewModel.receiveAddress = receiveAddressController.text);
receiveAmountController.addListener(() {
if (receiveAmountController.text != exchangeViewModel.receiveAmount) {
exchangeViewModel.changeReceiveAmount(
amount: receiveAmountController.text);
exchangeViewModel.changeReceiveAmount(amount: receiveAmountController.text);
exchangeViewModel.isReceiveAmountEntered = true;
}
});
reaction((_) => exchangeViewModel.wallet.walletAddresses.address,
(String address) {
reaction((_) => exchangeViewModel.wallet.walletAddresses.address, (String address) {
if (exchangeViewModel.depositCurrency == CryptoCurrency.xmr) {
depositKey.currentState!.changeAddress(address: address);
}
@ -389,29 +369,26 @@ class ExchangeTemplatePage extends BasePage {
_isReactionsSet = true;
}
void _onCurrencyChange(CryptoCurrency currency,
ExchangeViewModel exchangeViewModel, GlobalKey<ExchangeCardState> key) {
void _onCurrencyChange(CryptoCurrency currency, ExchangeViewModel exchangeViewModel,
GlobalKey<ExchangeCardState> key) {
final isCurrentTypeWallet = currency == exchangeViewModel.wallet.currency;
key.currentState!.changeSelectedCurrency(currency);
key.currentState!.changeWalletName(
isCurrentTypeWallet ? exchangeViewModel.wallet.name : '');
key.currentState!.changeWalletName(isCurrentTypeWallet ? exchangeViewModel.wallet.name : '');
key.currentState!.changeAddress(
address: isCurrentTypeWallet
? exchangeViewModel.wallet.walletAddresses.address : '');
address: isCurrentTypeWallet ? exchangeViewModel.wallet.walletAddresses.address : '');
key.currentState!.changeAmount(amount: '');
}
void _onWalletNameChange(ExchangeViewModel exchangeViewModel,
CryptoCurrency currency, GlobalKey<ExchangeCardState> key) {
void _onWalletNameChange(ExchangeViewModel exchangeViewModel, CryptoCurrency currency,
GlobalKey<ExchangeCardState> key) {
final isCurrentTypeWallet = currency == exchangeViewModel.wallet.currency;
if (isCurrentTypeWallet) {
key.currentState!.changeWalletName(exchangeViewModel.wallet.name);
key.currentState!.addressController.text =
exchangeViewModel.wallet.walletAddresses.address;
key.currentState!.addressController.text = exchangeViewModel.wallet.walletAddresses.address;
} else if (key.currentState!.addressController.text ==
exchangeViewModel.wallet.walletAddresses.address) {
key.currentState!.changeWalletName('');

View file

@ -1,3 +1,4 @@
import 'package:cake_wallet/core/amount_validator.dart';
import 'package:cake_wallet/entities/contact_base.dart';
import 'package:cake_wallet/themes/extensions/qr_code_theme.dart';
import 'package:cake_wallet/routes.dart';
@ -37,6 +38,7 @@ class ExchangeCard extends StatefulWidget {
this.addressButtonsColor = Colors.transparent,
this.borderColor = Colors.transparent,
this.hasAllAmount = false,
this.isAllAmountEnabled = false,
this.amountFocusNode,
this.addressFocusNode,
this.allAmount,
@ -62,9 +64,11 @@ class ExchangeCard extends StatefulWidget {
final Color borderColor;
final FormFieldValidator<String>? currencyValueValidator;
final FormFieldValidator<String>? addressTextFieldValidator;
final FormFieldValidator<String> allAmountValidator = AllAmountValidator();
final FocusNode? amountFocusNode;
final FocusNode? addressFocusNode;
final bool hasAllAmount;
final bool isAllAmountEnabled;
final VoidCallback? allAmount;
final void Function(BuildContext context)? onPushPasteButton;
final void Function(BuildContext context)? onPushAddressBookButton;
@ -76,15 +80,15 @@ class ExchangeCard extends StatefulWidget {
class ExchangeCardState extends State<ExchangeCard> {
ExchangeCardState()
: _title = '',
_min = '',
_max = '',
_isAmountEditable = false,
_isAddressEditable = false,
_walletName = '',
_selectedCurrency = CryptoCurrency.btc,
_isAmountEstimated = false,
_isMoneroWallet = false;
: _title = '',
_min = '',
_max = '',
_isAmountEditable = false,
_isAddressEditable = false,
_walletName = '',
_selectedCurrency = CryptoCurrency.btc,
_isAmountEstimated = false,
_isMoneroWallet = false;
final addressController = TextEditingController();
final amountController = TextEditingController();
@ -160,6 +164,12 @@ class ExchangeCardState extends State<ExchangeCard> {
@override
Widget build(BuildContext context) {
if (widget.isAllAmountEnabled) {
WidgetsBinding.instance.addPostFrameCallback((_) {
amountController.text = S.of(context).all;
});
}
final copyImage = Image.asset('assets/images/copy_content.png',
height: 16,
width: 16,
@ -168,8 +178,7 @@ class ExchangeCardState extends State<ExchangeCard> {
return Container(
width: double.infinity,
color: Colors.transparent,
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: <
Widget>[
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
@ -202,40 +211,38 @@ class ExchangeCardState extends State<ExchangeCard> {
),
Text(_selectedCurrency.toString(),
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 16,
color: Colors.white))
fontWeight: FontWeight.w600, fontSize: 16, color: Colors.white))
]),
),
),
_selectedCurrency.tag != null ? Padding(
padding: const EdgeInsets.only(right:3.0),
child: Container(
height: 32,
decoration: BoxDecoration(
color: widget.addressButtonsColor ??
Theme.of(context).extension<SendPageTheme>()!.textFieldButtonColor,
borderRadius:
BorderRadius.all(Radius.circular(6))),
child: Center(
child: Padding(
padding: const EdgeInsets.all(6.0),
child: Text(_selectedCurrency.tag!,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Theme.of(context).extension<SendPageTheme>()!.textFieldButtonIconColor)),
if (_selectedCurrency.tag != null)
Padding(
padding: const EdgeInsets.only(right: 3.0),
child: Container(
height: 32,
decoration: BoxDecoration(
color: widget.addressButtonsColor ??
Theme.of(context).extension<SendPageTheme>()!.textFieldButtonColor,
borderRadius: BorderRadius.all(Radius.circular(6))),
child: Center(
child: Padding(
padding: const EdgeInsets.all(6.0),
child: Text(_selectedCurrency.tag!,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Theme.of(context)
.extension<SendPageTheme>()!
.textFieldButtonIconColor)),
),
),
),
),
) : Container(),
Padding(
padding: const EdgeInsets.only(right: 4.0),
child: Text(':',
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 16,
color: Colors.white)),
fontWeight: FontWeight.w600, fontSize: 16, color: Colors.white)),
),
Expanded(
child: Row(
@ -249,26 +256,25 @@ class ExchangeCardState extends State<ExchangeCard> {
controller: amountController,
enabled: _isAmountEditable,
textAlign: TextAlign.left,
keyboardType: TextInputType.numberWithOptions(
signed: false, decimal: true),
keyboardType:
TextInputType.numberWithOptions(signed: false, decimal: true),
inputFormatters: [
FilteringTextInputFormatter.deny(
RegExp('[\\-|\\ ]'))
FilteringTextInputFormatter.deny(RegExp('[\\-|\\ ]'))
],
hintText: '0.0000',
borderColor: Colors.transparent,
//widget.borderColor,
textStyle: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.white),
fontSize: 16, fontWeight: FontWeight.w600, color: Colors.white),
placeholderTextStyle: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Theme.of(context).extension<ExchangePageTheme>()!.hintTextColor),
color: Theme.of(context)
.extension<ExchangePageTheme>()!
.hintTextColor),
validator: _isAmountEditable
? widget.currencyValueValidator
: null),
? widget.currencyValueValidator
: null),
),
),
if (widget.hasAllAmount)
@ -276,9 +282,10 @@ class ExchangeCardState extends State<ExchangeCard> {
height: 32,
width: 32,
decoration: BoxDecoration(
color: Theme.of(context).extension<SendPageTheme>()!.textFieldButtonColor,
borderRadius:
BorderRadius.all(Radius.circular(6))),
color: Theme.of(context)
.extension<SendPageTheme>()!
.textFieldButtonColor,
borderRadius: BorderRadius.all(Radius.circular(6))),
child: InkWell(
onTap: () => widget.allAmount?.call(),
child: Center(
@ -287,7 +294,9 @@ class ExchangeCardState extends State<ExchangeCard> {
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Theme.of(context).extension<SendPageTheme>()!.textFieldButtonIconColor)),
color: Theme.of(context)
.extension<SendPageTheme>()!
.textFieldButtonIconColor)),
),
),
)
@ -296,39 +305,30 @@ class ExchangeCardState extends State<ExchangeCard> {
),
],
)),
Divider(
height: 1,
color: Theme.of(context).extension<SendPageTheme>()!.textFieldHintColor),
Divider(height: 1, color: Theme.of(context).extension<SendPageTheme>()!.textFieldHintColor),
Padding(
padding: EdgeInsets.only(top: 5),
child: Container(
height: 15,
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
_min != null
? Text(
S
.of(context)
.min_value(_min ?? '', _selectedCurrency.toString()),
style: TextStyle(
fontSize: 10,
height: 1.2,
color: Theme.of(context).extension<ExchangePageTheme>()!.hintTextColor),
)
: Offstage(),
_min != null ? SizedBox(width: 10) : Offstage(),
_max != null
? Text(
S
.of(context)
.max_value(_max ?? '', _selectedCurrency.toString()),
style: TextStyle(
fontSize: 10,
height: 1.2,
color: Theme.of(context).extension<ExchangePageTheme>()!.hintTextColor))
: Offstage(),
])),
child: Row(mainAxisAlignment: MainAxisAlignment.start, children: <Widget>[
_min != null
? Text(
S.of(context).min_value(_min ?? '', _selectedCurrency.toString()),
style: TextStyle(
fontSize: 10,
height: 1.2,
color: Theme.of(context).extension<ExchangePageTheme>()!.hintTextColor),
)
: Offstage(),
_min != null ? SizedBox(width: 10) : Offstage(),
_max != null
? Text(S.of(context).max_value(_max ?? '', _selectedCurrency.toString()),
style: TextStyle(
fontSize: 10,
height: 1.2,
color: Theme.of(context).extension<ExchangePageTheme>()!.hintTextColor))
: Offstage(),
])),
),
!_isAddressEditable && widget.hasRefundAddress
? Padding(
@ -343,7 +343,7 @@ class ExchangeCardState extends State<ExchangeCard> {
: Offstage(),
_isAddressEditable
? FocusTraversalOrder(
order: NumericFocusOrder(2),
order: NumericFocusOrder(2),
child: Padding(
padding: EdgeInsets.only(top: 20),
child: AddressTextField(
@ -352,27 +352,23 @@ class ExchangeCardState extends State<ExchangeCard> {
onURIScanned: (uri) {
final paymentRequest = PaymentRequest.fromUri(uri);
addressController.text = paymentRequest.address;
if (amountController.text.isNotEmpty) {
_showAmountPopup(context, paymentRequest);
return;
}
widget.amountFocusNode?.requestFocus();
amountController.text = paymentRequest.amount;
amountController.text = paymentRequest.amount;
},
placeholder: widget.hasRefundAddress
? S.of(context).refund_address
: null,
placeholder: widget.hasRefundAddress ? S.of(context).refund_address : null,
options: [
AddressTextFieldOption.paste,
AddressTextFieldOption.qrCode,
AddressTextFieldOption.addressBook,
],
isBorderExist: false,
textStyle: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.white),
textStyle:
TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Colors.white),
hintStyle: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
@ -381,27 +377,22 @@ class ExchangeCardState extends State<ExchangeCard> {
validator: widget.addressTextFieldValidator,
onPushPasteButton: widget.onPushPasteButton,
onPushAddressBookButton: widget.onPushAddressBookButton,
selectedCurrency: _selectedCurrency
),
selectedCurrency: _selectedCurrency),
),
)
)
: Padding(
padding: EdgeInsets.only(top: 10),
child: Builder(
builder: (context) => Stack(children: <Widget>[
FocusTraversalOrder(
order: NumericFocusOrder(3),
child: BaseTextFormField(
controller: addressController,
borderColor: Colors.transparent,
suffixIcon:
SizedBox(width: _isMoneroWallet ? 80 : 36),
textStyle: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.white),
validator: widget.addressTextFieldValidator),
FocusTraversalOrder(
order: NumericFocusOrder(3),
child: BaseTextFormField(
controller: addressController,
borderColor: Colors.transparent,
suffixIcon: SizedBox(width: _isMoneroWallet ? 80 : 36),
textStyle: TextStyle(
fontSize: 16, fontWeight: FontWeight.w600, color: Colors.white),
validator: widget.addressTextFieldValidator),
),
Positioned(
top: 2,
@ -421,33 +412,28 @@ class ExchangeCardState extends State<ExchangeCard> {
child: InkWell(
onTap: () async {
final contact =
await Navigator.of(context)
.pushNamed(
await Navigator.of(context).pushNamed(
Routes.pickerAddressBook,
arguments: widget.initialCurrency,
);
if (contact is ContactBase &&
contact.address != null) {
if (contact is ContactBase) {
setState(() =>
addressController.text =
contact.address);
widget.onPushAddressBookButton
?.call(context);
addressController.text = contact.address);
widget.onPushAddressBookButton?.call(context);
}
},
child: Container(
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
color: widget
.addressButtonsColor,
color: widget.addressButtonsColor,
borderRadius:
BorderRadius.all(
Radius.circular(
6))),
BorderRadius.all(Radius.circular(6))),
child: Image.asset(
'assets/images/open_book.png',
color: Theme.of(context).extension<SendPageTheme>()!.textFieldButtonIconColor,
color: Theme.of(context)
.extension<SendPageTheme>()!
.textFieldButtonIconColor,
)),
),
)),
@ -462,18 +448,13 @@ class ExchangeCardState extends State<ExchangeCard> {
label: S.of(context).copy_address,
child: InkWell(
onTap: () {
Clipboard.setData(ClipboardData(
text: addressController
.text));
Clipboard.setData(
ClipboardData(text: addressController.text));
showBar<void>(
context,
S
.of(context)
.copied_to_clipboard);
context, S.of(context).copied_to_clipboard);
},
child: Container(
padding: EdgeInsets.fromLTRB(
8, 8, 0, 8),
padding: EdgeInsets.fromLTRB(8, 8, 0, 8),
color: Colors.transparent,
child: copyImage),
),
@ -504,17 +485,16 @@ class ExchangeCardState extends State<ExchangeCard> {
context: context,
builder: (dialogContext) {
return AlertWithTwoActions(
alertTitle: S.of(context).overwrite_amount,
alertContent: S.of(context).qr_payment_amount,
rightButtonText: S.of(context).ok,
leftButtonText: S.of(context).cancel,
alertTitle: S.of(dialogContext).overwrite_amount,
alertContent: S.of(dialogContext).qr_payment_amount,
rightButtonText: S.of(dialogContext).ok,
leftButtonText: S.of(dialogContext).cancel,
actionRightButton: () {
widget.amountFocusNode?.requestFocus();
amountController.text = paymentRequest.amount;
Navigator.of(context).pop();
Navigator.of(dialogContext).pop();
},
actionLeftButton: () => Navigator.of(dialogContext).pop());
}
);
});
}
}

View file

@ -262,6 +262,7 @@ class ExchangeTradeState extends State<ExchangeTradeForm> {
fee: S.of(popupContext).send_fee,
feeValue: widget.exchangeTradeViewModel.sendViewModel
.pendingTransaction!.feeFormatted,
feeRate: widget.exchangeTradeViewModel.sendViewModel.pendingTransaction!.feeRate,
rightButtonText: S.of(popupContext).send,
leftButtonText: S.of(popupContext).cancel,
actionRightButton: () async {

View file

@ -100,6 +100,12 @@ class _AdvancedPrivacySettingsBodyState extends State<AdvancedPrivacySettingsBod
Observer(builder: (_) {
return Column(
children: [
SettingsSwitcherCell(
title: S.current.disable_bulletin,
value: widget.privacySettingsViewModel.disableBulletin,
onValueChange: (BuildContext _, bool value) {
widget.privacySettingsViewModel.setDisableBulletin(value);
}),
SettingsSwitcherCell(
title: S.current.add_custom_node,
value: widget.privacySettingsViewModel.addCustomNode,

View file

@ -5,6 +5,7 @@ import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/reactions/wallet_connect.dart';
import 'package:cake_wallet/utils/device_info.dart';
import 'package:cake_wallet/utils/payment_request.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:flutter/material.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/auth/auth_page.dart';
@ -12,6 +13,7 @@ import 'package:cake_wallet/store/app_store.dart';
import 'package:cake_wallet/store/authentication_store.dart';
import 'package:cake_wallet/entities/qr_scanner.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:mobx/mobx.dart';
import 'package:uni_links/uni_links.dart';
import 'package:cake_wallet/src/screens/setup_2fa/setup_2fa_enter_code_page.dart';
@ -49,6 +51,7 @@ class RootState extends State<Root> with WidgetsBindingObserver {
bool _requestAuth;
StreamSubscription<Uri?>? stream;
ReactionDisposer? _walletReactionDisposer;
Uri? launchUri;
@override
@ -72,6 +75,7 @@ class RootState extends State<Root> with WidgetsBindingObserver {
@override
void dispose() {
stream?.cancel();
_walletReactionDisposer?.call();
super.dispose();
}
@ -169,10 +173,20 @@ class RootState extends State<Root> with WidgetsBindingObserver {
);
});
} else if (_isValidPaymentUri()) {
widget.navigatorKey.currentState?.pushNamed(
Routes.send,
arguments: PaymentRequest.fromUri(launchUri),
);
if (widget.authenticationStore.state == AuthenticationState.uninitialized) {
launchUri = null;
} else {
if (widget.appStore.wallet == null) {
waitForWalletInstance(context, launchUri!);
launchUri = null;
} else {
widget.navigatorKey.currentState?.pushNamed(
Routes.send,
arguments: PaymentRequest.fromUri(launchUri),
);
launchUri = null;
}
}
launchUri = null;
} else if (isWalletConnectLink) {
if (isEVMCompatibleChain(widget.appStore.wallet!.type)) {
@ -233,4 +247,24 @@ class RootState extends State<Root> with WidgetsBindingObserver {
fontSize: 16.0,
);
}
void waitForWalletInstance(BuildContext context, Uri tempLaunchUri) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (context.mounted) {
_walletReactionDisposer = reaction(
(_) => widget.appStore.wallet,
(WalletBase? wallet) {
if (wallet != null) {
widget.navigatorKey.currentState?.pushNamed(
Routes.send,
arguments: PaymentRequest.fromUri(tempLaunchUri),
);
_walletReactionDisposer?.call();
_walletReactionDisposer = null;
}
},
);
}
});
}
}

View file

@ -1,4 +1,5 @@
import 'package:cake_wallet/core/auth_service.dart';
import 'package:cake_wallet/entities/contact_record.dart';
import 'package:cake_wallet/entities/fiat_currency.dart';
import 'package:cake_wallet/entities/template.dart';
import 'package:cake_wallet/reactions/wallet_connect.dart';
@ -48,6 +49,7 @@ class SendPage extends BasePage {
final PaymentRequest? initialPaymentRequest;
bool _effectsInstalled = false;
ContactRecord? newContactAddress;
@override
String get title => S.current.send;
@ -424,6 +426,7 @@ class SendPage extends BasePage {
fee: isEVMCompatibleChain(sendViewModel.walletType)
? S.of(_dialogContext).send_estimated_fee
: S.of(_dialogContext).send_fee,
feeRate: sendViewModel.pendingTransaction!.feeRate,
feeValue: sendViewModel.pendingTransaction!.feeFormatted,
feeFiatAmount: sendViewModel.pendingTransactionFeeFiatAmountFormatted,
outputs: sendViewModel.outputs,
@ -443,22 +446,50 @@ class SendPage extends BasePage {
}
if (state is TransactionCommitted) {
String alertContent;
if (sendViewModel.walletType == WalletType.solana) {
alertContent =
'${S.of(_dialogContext).send_success(sendViewModel.selectedCryptoCurrency.toString())}. ${S.of(_dialogContext).waitFewSecondForTxUpdate}';
newContactAddress =
newContactAddress ?? sendViewModel.newContactAddress();
final successMessage = S.of(_dialogContext).send_success(
sendViewModel.selectedCryptoCurrency.toString());
final waitMessage = sendViewModel.walletType == WalletType.solana
? '. ${S.of(_dialogContext).waitFewSecondForTxUpdate}' : '';
final newContactMessage = newContactAddress != null
? '\n${S.of(context).add_contact_to_address_book}' : '';
final alertContent =
"$successMessage$waitMessage$newContactMessage";
if (newContactAddress != null) {
return AlertWithTwoActions(
alertTitle: '',
alertContent: alertContent,
rightButtonText: S.of(_dialogContext).add_contact,
leftButtonText: S.of(_dialogContext).ignor,
actionRightButton: () {
Navigator.of(_dialogContext).pop();
RequestReviewHandler.requestReview();
Navigator.of(context).pushNamed(
Routes.addressBookAddContact,
arguments: newContactAddress);
newContactAddress = null;
},
actionLeftButton: () {
Navigator.of(_dialogContext).pop();
RequestReviewHandler.requestReview();
newContactAddress = null;
});
} else {
alertContent = S.of(_dialogContext).send_success(
sendViewModel.selectedCryptoCurrency.toString());
return AlertWithOneAction(
alertTitle: '',
alertContent: alertContent,
buttonText: S.of(_dialogContext).ok,
buttonAction: () {
Navigator.of(_dialogContext).pop();
RequestReviewHandler.requestReview();
});
}
return AlertWithOneAction(
alertTitle: '',
alertContent: alertContent,
buttonText: S.of(_dialogContext).ok,
buttonAction: () {
Navigator.of(_dialogContext).pop();
RequestReviewHandler.requestReview();
});
}
return Offstage();

View file

@ -1,7 +1,5 @@
import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart';
import 'package:cake_wallet/themes/extensions/keyboard_theme.dart';
import 'package:cake_wallet/themes/extensions/seed_widget_theme.dart';
import 'package:cake_wallet/utils/payment_request.dart';
import 'package:cake_wallet/src/widgets/trail_button.dart';
import 'package:cake_wallet/view_model/send/template_view_model.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
@ -11,7 +9,6 @@ import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/view_model/send/send_template_view_model.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
import 'package:cake_wallet/src/screens/send/widgets/prefix_currency_icon_widget.dart';
import 'package:cake_wallet/themes/extensions/send_page_theme.dart';
import 'package:cake_wallet/src/screens/send/widgets/send_template_card.dart';
import 'package:smooth_page_indicator/smooth_page_indicator.dart';
@ -97,8 +94,13 @@ class SendTemplatePage extends BasePage {
radius: 6.0,
dotWidth: 6.0,
dotHeight: 6.0,
dotColor: Theme.of(context).extension<SendPageTheme>()!.indicatorDotColor,
activeDotColor: Theme.of(context).extension<DashboardPageTheme>()!.indicatorDotTheme.activeIndicatorColor))
dotColor: Theme.of(context)
.extension<SendPageTheme>()!
.indicatorDotColor,
activeDotColor: Theme.of(context)
.extension<DashboardPageTheme>()!
.indicatorDotTheme
.activeIndicatorColor))
: Offstage();
},
),

View file

@ -16,6 +16,7 @@ class ConfirmSendingAlert extends BaseAlertDialog {
required this.amountValue,
required this.fiatAmountValue,
required this.fee,
this.feeRate,
required this.feeValue,
required this.feeFiatAmount,
required this.outputs,
@ -36,6 +37,7 @@ class ConfirmSendingAlert extends BaseAlertDialog {
final String amountValue;
final String fiatAmountValue;
final String fee;
final String? feeRate;
final String feeValue;
final String feeFiatAmount;
final List<Output> outputs;
@ -90,6 +92,7 @@ class ConfirmSendingAlert extends BaseAlertDialog {
amountValue: amountValue,
fiatAmountValue: fiatAmountValue,
fee: fee,
feeRate: feeRate,
feeValue: feeValue,
feeFiatAmount: feeFiatAmount,
outputs: outputs);
@ -103,6 +106,7 @@ class ConfirmSendingAlertContent extends StatefulWidget {
required this.amountValue,
required this.fiatAmountValue,
required this.fee,
this.feeRate,
required this.feeValue,
required this.feeFiatAmount,
required this.outputs});
@ -113,6 +117,7 @@ class ConfirmSendingAlertContent extends StatefulWidget {
final String amountValue;
final String fiatAmountValue;
final String fee;
final String? feeRate;
final String feeValue;
final String feeFiatAmount;
final List<Output> outputs;
@ -125,6 +130,7 @@ class ConfirmSendingAlertContent extends StatefulWidget {
amountValue: amountValue,
fiatAmountValue: fiatAmountValue,
fee: fee,
feeRate: feeRate,
feeValue: feeValue,
feeFiatAmount: feeFiatAmount,
outputs: outputs);
@ -138,6 +144,7 @@ class ConfirmSendingAlertContentState extends State<ConfirmSendingAlertContent>
required this.amountValue,
required this.fiatAmountValue,
required this.fee,
this.feeRate,
required this.feeValue,
required this.feeFiatAmount,
required this.outputs})
@ -153,6 +160,7 @@ class ConfirmSendingAlertContentState extends State<ConfirmSendingAlertContent>
final String amountValue;
final String fiatAmountValue;
final String fee;
final String? feeRate;
final String feeValue;
final String feeFiatAmount;
final List<Output> outputs;
@ -183,7 +191,7 @@ class ConfirmSendingAlertContentState extends State<ConfirmSendingAlertContent>
return Stack(alignment: Alignment.center, clipBehavior: Clip.none, children: [
Container(
height: 200,
height: feeRate != null ? 250 : 200,
child: SingleChildScrollView(
controller: controller,
child: Column(
@ -311,6 +319,36 @@ class ConfirmSendingAlertContentState extends State<ConfirmSendingAlertContent>
)
],
)),
if (feeRate != null && feeRate!.isNotEmpty)
Padding(
padding: EdgeInsets.only(top: 16),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
S.current.send_estimated_fee,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.normal,
fontFamily: 'Lato',
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
decoration: TextDecoration.none,
),
),
Text(
"$feeRate sat/byte",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
fontFamily: 'Lato',
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
decoration: TextDecoration.none,
),
)
],
)),
Padding(
padding: EdgeInsets.only(top: 16),
child: Column(

View file

@ -81,15 +81,17 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
if (initialPaymentRequest != null &&
sendViewModel.walletCurrencyName != initialPaymentRequest!.scheme.toLowerCase()) {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
showPopUp<void>(
context: context,
builder: (BuildContext context) {
return AlertWithOneAction(
alertTitle: S.of(context).error,
alertContent: S.of(context).unmatched_currencies,
buttonText: S.of(context).ok,
buttonAction: () => Navigator.of(context).pop());
});
if (context.mounted) {
showPopUp<void>(
context: context,
builder: (BuildContext context) {
return AlertWithOneAction(
alertTitle: S.of(context).error,
alertContent: S.of(context).unmatched_currencies,
buttonText: S.of(context).ok,
buttonAction: () => Navigator.of(context).pop());
});
}
});
}
}
@ -322,7 +324,7 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
? sendViewModel.allAmountValidator
: sendViewModel.amountValidator,
),
if (!sendViewModel.isBatchSending && sendViewModel.shouldDisplaySendALL)
if (!sendViewModel.isBatchSending)
Positioned(
top: 2,
right: 0,
@ -454,7 +456,9 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
if (sendViewModel.hasFees)
Observer(
builder: (_) => GestureDetector(
onTap: () => _setTransactionPriority(context),
onTap: sendViewModel.hasFeesPriority
? () => _setTransactionPriority(context)
: () {},
child: Container(
padding: EdgeInsets.only(top: 24),
child: Row(

View file

@ -15,7 +15,6 @@ import 'package:flutter/material.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/widgets/standard_list.dart';
import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
@ -43,7 +42,7 @@ class ConnectionSyncPage extends BasePage {
title: S.current.rescan,
handler: (context) => Navigator.of(context).pushNamed(Routes.rescan),
),
if (DeviceInfo.instance.isMobile) ...[
if (DeviceInfo.instance.isMobile && FeatureFlag.isBackgroundSyncEnabled) ...[
Observer(builder: (context) {
return SettingsPickerCell<SyncMode>(
title: S.current.background_sync_mode,

View file

@ -80,6 +80,12 @@ class PrivacyPage extends BasePage {
onValueChange: (BuildContext _, bool value) {
_privacySettingsViewModel.setDisableSell(value);
}),
SettingsSwitcherCell(
title: S.current.disable_bulletin,
value: _privacySettingsViewModel.disableBulletin,
onValueChange: (BuildContext _, bool value) {
_privacySettingsViewModel.setDisableBulletin(value);
}),
if (_privacySettingsViewModel.canUseEtherscan)
SettingsSwitcherCell(
title: S.current.etherscan_history,

View file

@ -46,9 +46,6 @@ class UnspentCoinsListFormState extends State<UnspentCoinsListForm> {
itemBuilder: (_, int index) {
return Observer(builder: (_) {
final item = unspentCoinsListViewModel.items[index];
final address = unspentCoinsListViewModel.wallet.type == WalletType.bitcoinCash
? bitcoinCash!.getCashAddrFormat(item.address)
: item.address;
return GestureDetector(
onTap: () => Navigator.of(context).pushNamed(Routes.unspentCoinsDetails,
@ -56,7 +53,7 @@ class UnspentCoinsListFormState extends State<UnspentCoinsListForm> {
child: UnspentCoinsListItem(
note: item.note,
amount: item.amount,
address: address,
address: item.address,
isSending: item.isSending,
isFrozen: item.isFrozen,
isChange: item.isChange,

View file

@ -27,10 +27,12 @@ class UnspentCoinsListItem extends StatelessWidget {
Widget build(BuildContext context) {
final unselectedItemColor = Theme.of(context).cardColor;
final selectedItemColor = Theme.of(context).primaryColor;
final itemColor = isSending ? selectedItemColor : unselectedItemColor;
final amountColor =
isSending ? Colors.white : Theme.of(context).extension<CakeTextTheme>()!.buttonTextColor;
final itemColor = isSending
? selectedItemColor
: unselectedItemColor;
final amountColor = isSending
? Colors.white
: Theme.of(context).extension<CakeTextTheme>()!.buttonTextColor;
final addressColor = isSending
? Colors.white.withOpacity(0.5)
: Theme.of(context).extension<CakeTextTheme>()!.buttonSecondaryTextColor;
@ -85,7 +87,7 @@ class UnspentCoinsListItem extends StatelessWidget {
child: Text(
S.of(context).frozen,
style: TextStyle(
color: amountColor, fontSize: 7, fontWeight: FontWeight.w600),
color: Colors.black, fontSize: 7, fontWeight: FontWeight.w600),
)),
],
),

View file

@ -6,20 +6,28 @@ import 'package:cake_wallet/src/widgets/service_status_tile.dart';
import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart';
import 'package:cake_wallet/themes/extensions/wallet_list_theme.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:url_launcher/url_launcher.dart';
class ServicesUpdatesWidget extends StatelessWidget {
class ServicesUpdatesWidget extends StatefulWidget {
final Future<ServicesResponse> servicesResponse;
const ServicesUpdatesWidget(this.servicesResponse, {super.key});
@override
State<ServicesUpdatesWidget> createState() => _ServicesUpdatesWidgetState();
}
class _ServicesUpdatesWidgetState extends State<ServicesUpdatesWidget> {
bool wasOpened = false;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: FutureBuilder<ServicesResponse>(
future: servicesResponse,
future: widget.servicesResponse,
builder: (context, state) {
return InkWell(
onTap: state.hasData
@ -29,6 +37,8 @@ class ServicesUpdatesWidget extends StatelessWidget {
.get<SharedPreferences>()
.setString(PreferencesKey.serviceStatusShaKey, state.data!.currentSha);
setState(() => wasOpened = true);
showModalBottomSheet(
context: context,
shape: RoundedRectangleBorder(
@ -96,15 +106,16 @@ class ServicesUpdatesWidget extends StatelessWidget {
: null,
child: Stack(
children: [
Image.asset(
"assets/images/notification_icon.png",
SvgPicture.asset(
"assets/images/notification_icon.svg",
color: Theme.of(context).extension<DashboardPageTheme>()!.pageTitleTextColor,
width: 30,
),
if (state.hasData && state.data!.hasUpdates)
if (state.hasData && state.data!.hasUpdates && !wasOpened)
Container(
height: 7,
width: 7,
margin: EdgeInsetsDirectional.only(start: 8),
margin: EdgeInsetsDirectional.only(start: 15),
decoration: BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,

View file

@ -3,18 +3,20 @@ import 'package:cake_wallet/view_model/dashboard/trade_list_item.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:mobx/mobx.dart';
part'trade_filter_store.g.dart';
part 'trade_filter_store.g.dart';
class TradeFilterStore = TradeFilterStoreBase with _$TradeFilterStore;
abstract class TradeFilterStoreBase with Store {
TradeFilterStoreBase() : displayXMRTO = true,
TradeFilterStoreBase()
: displayXMRTO = true,
displayChangeNow = true,
displaySideShift = true,
displayMorphToken = true,
displaySimpleSwap = true,
displayTrocador = true,
displayExolix = true;
displayExolix = true,
displayThorChain = true;
@observable
bool displayXMRTO;
@ -37,8 +39,17 @@ abstract class TradeFilterStoreBase with Store {
@observable
bool displayExolix;
@observable
bool displayThorChain;
@computed
bool get displayAllTrades => displayChangeNow && displaySideShift && displaySimpleSwap && displayTrocador && displayExolix;
bool get displayAllTrades =>
displayChangeNow &&
displaySideShift &&
displaySimpleSwap &&
displayTrocador &&
displayExolix &&
displayThorChain;
@action
void toggleDisplayExchange(ExchangeProviderDescription provider) {
@ -64,6 +75,9 @@ abstract class TradeFilterStoreBase with Store {
case ExchangeProviderDescription.exolix:
displayExolix = !displayExolix;
break;
case ExchangeProviderDescription.thorChain:
displayThorChain = !displayThorChain;
break;
case ExchangeProviderDescription.all:
if (displayAllTrades) {
displayChangeNow = false;
@ -73,6 +87,7 @@ abstract class TradeFilterStoreBase with Store {
displaySimpleSwap = false;
displayTrocador = false;
displayExolix = false;
displayThorChain = false;
} else {
displayChangeNow = true;
displaySideShift = true;
@ -81,6 +96,7 @@ abstract class TradeFilterStoreBase with Store {
displaySimpleSwap = true;
displayTrocador = true;
displayExolix = true;
displayThorChain = true;
}
break;
}
@ -96,16 +112,13 @@ abstract class TradeFilterStoreBase with Store {
? _trades
.where((item) =>
(displayXMRTO && item.trade.provider == ExchangeProviderDescription.xmrto) ||
(displaySideShift &&
item.trade.provider == ExchangeProviderDescription.sideShift) ||
(displayChangeNow &&
item.trade.provider == ExchangeProviderDescription.changeNow) ||
(displayMorphToken &&
item.trade.provider == ExchangeProviderDescription.morphToken) ||
(displaySimpleSwap &&
item.trade.provider == ExchangeProviderDescription.simpleSwap) ||
(displaySideShift && item.trade.provider == ExchangeProviderDescription.sideShift) ||
(displayChangeNow && item.trade.provider == ExchangeProviderDescription.changeNow) ||
(displayMorphToken && item.trade.provider == ExchangeProviderDescription.morphToken) ||
(displaySimpleSwap && item.trade.provider == ExchangeProviderDescription.simpleSwap) ||
(displayTrocador && item.trade.provider == ExchangeProviderDescription.trocador) ||
(displayExolix && item.trade.provider == ExchangeProviderDescription.exolix))
(displayExolix && item.trade.provider == ExchangeProviderDescription.exolix) ||
(displayThorChain && item.trade.provider == ExchangeProviderDescription.thorChain))
.toList()
: _trades;
}

View file

@ -59,6 +59,7 @@ abstract class SettingsStoreBase with Store {
required bool initialAppSecure,
required bool initialDisableBuy,
required bool initialDisableSell,
required bool initialDisableBulletin,
required WalletListOrderType initialWalletListOrder,
required bool initialWalletListAscending,
required FiatApiMode initialFiatMode,
@ -130,6 +131,7 @@ abstract class SettingsStoreBase with Store {
isAppSecure = initialAppSecure,
disableBuy = initialDisableBuy,
disableSell = initialDisableSell,
disableBulletin = initialDisableBulletin,
walletListOrder = initialWalletListOrder,
walletListAscending = initialWalletListAscending,
shouldShowMarketPlaceInDashboard = initialShouldShowMarketPlaceInDashboard,
@ -291,6 +293,11 @@ abstract class SettingsStoreBase with Store {
(bool disableSell) =>
sharedPreferences.setBool(PreferencesKey.disableSellKey, disableSell));
reaction(
(_) => disableBulletin,
(bool disableBulletin) =>
sharedPreferences.setBool(PreferencesKey.disableBulletinKey, disableBulletin));
reaction(
(_) => walletListOrder,
(WalletListOrderType walletListOrder) =>
@ -553,6 +560,9 @@ abstract class SettingsStoreBase with Store {
@observable
bool disableSell;
@observable
bool disableBulletin;
@observable
WalletListOrderType walletListOrder;
@ -777,6 +787,7 @@ abstract class SettingsStoreBase with Store {
final isAppSecure = sharedPreferences.getBool(PreferencesKey.isAppSecureKey) ?? false;
final disableBuy = sharedPreferences.getBool(PreferencesKey.disableBuyKey) ?? false;
final disableSell = sharedPreferences.getBool(PreferencesKey.disableSellKey) ?? false;
final disableBulletin = sharedPreferences.getBool(PreferencesKey.disableBulletinKey) ?? false;
final walletListOrder =
WalletListOrderType.values[sharedPreferences.getInt(PreferencesKey.walletListOrder) ?? 0];
final walletListAscending =
@ -1029,6 +1040,7 @@ abstract class SettingsStoreBase with Store {
initialAppSecure: isAppSecure,
initialDisableBuy: disableBuy,
initialDisableSell: disableSell,
initialDisableBulletin: disableBulletin,
initialWalletListOrder: walletListOrder,
initialWalletListAscending: walletListAscending,
initialFiatMode: currentFiatApiMode,
@ -1147,6 +1159,7 @@ abstract class SettingsStoreBase with Store {
isAppSecure = sharedPreferences.getBool(PreferencesKey.isAppSecureKey) ?? isAppSecure;
disableBuy = sharedPreferences.getBool(PreferencesKey.disableBuyKey) ?? disableBuy;
disableSell = sharedPreferences.getBool(PreferencesKey.disableSellKey) ?? disableSell;
disableBulletin = sharedPreferences.getBool(PreferencesKey.disableBulletinKey) ?? disableBulletin;
walletListOrder =
WalletListOrderType.values[sharedPreferences.getInt(PreferencesKey.walletListOrder) ?? 0];
walletListAscending = sharedPreferences.getBool(PreferencesKey.walletListAscending) ?? true;

View file

@ -1,5 +1,6 @@
class FeatureFlag {
static const bool isCakePayEnabled = false;
static const bool isExolixEnabled = false;
static const bool isExolixEnabled = true;
static const bool isInAppTorEnabled = false;
static const bool isBackgroundSyncEnabled = false;
}

View file

@ -20,6 +20,9 @@ abstract class AdvancedPrivacySettingsViewModelBase with Store {
@computed
FiatApiMode get fiatApiMode => _settingsStore.fiatApiMode;
@computed
bool get disableBulletin => _settingsStore.disableBulletin;
@observable
bool _addCustomNode = false;
@ -64,6 +67,9 @@ abstract class AdvancedPrivacySettingsViewModelBase with Store {
@action
void setExchangeApiMode(ExchangeApiMode value) => _settingsStore.exchangeStatus = value;
@action
void setDisableBulletin(bool value) => _settingsStore.disableBulletin = value;
@action
void toggleAddCustomNode() => _addCustomNode = !_addCustomNode;

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