mirror of
https://github.com/cake-tech/cake_wallet.git
synced 2025-03-12 09:32:33 +00:00
Merge remote-tracking branch 'origin/main' into CW-453-silent-payments
This commit is contained in:
commit
7197a7bc71
151 changed files with 3083 additions and 1393 deletions
2
.github/workflows/pr_test_build.yml
vendored
2
.github/workflows/pr_test_build.yml
vendored
|
@ -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 |
69
assets/images/notification_icon.svg
Normal file
69
assets/images/notification_icon.svg
Normal 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
BIN
assets/images/thorchain.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.3 KiB |
|
@ -1,2 +1,2 @@
|
|||
New themes
|
||||
Bug fixes and enhancements
|
||||
Exchange flow enhancements and fixes
|
||||
Generic enhancements and bug fixes
|
|
@ -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
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
class BitcoinCommitTransactionException implements Exception {
|
||||
String errorMessage;
|
||||
BitcoinCommitTransactionException(this.errorMessage);
|
||||
|
||||
@override
|
||||
String toString() => 'Transaction commit is failed.';
|
||||
}
|
||||
String toString() => errorMessage;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
class BitcoinTransactionNoInputsException implements Exception {
|
||||
@override
|
||||
String toString() => 'Not enough inputs available. Please select more under Coin Control';
|
||||
}
|
|
@ -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.';
|
||||
}
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
|
27
cw_bitcoin/lib/exceptions.dart
Normal file
27
cw_bitcoin/lib/exceptions.dart
Normal 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 {}
|
|
@ -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()));
|
||||
|
|
|
@ -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 = '';
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -18,6 +18,7 @@ abstract class BitcoinCashWalletAddressesBase extends ElectrumWalletAddresses wi
|
|||
super.initialAddresses,
|
||||
super.initialRegularAddressIndex,
|
||||
super.initialChangeAddressIndex,
|
||||
super.initialAddressPageType,
|
||||
}) : super(walletInfo);
|
||||
|
||||
@override
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
30
cw_core/lib/exceptions.dart
Normal file
30
cw_core/lib/exceptions.dart
Normal 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 {}
|
|
@ -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:
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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)}';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 +
|
||||
'¤cyCode=' +
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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]|\$)'
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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!);
|
||||
}
|
||||
|
||||
|
|
27
lib/core/secure_storage.dart
Normal file
27
lib/core/secure_storage.dart
Normal 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;
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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});
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
255
lib/exchange/provider/thorchain_exchange.provider.dart
Normal file
255
lib/exchange/provider/thorchain_exchange.provider.dart
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -163,7 +163,7 @@ Future<void> initializeAppConfigs() async {
|
|||
transactionDescriptions: transactionDescriptions,
|
||||
secureStorage: secureStorage,
|
||||
anonpayInvoiceInfo: anonpayInvoiceInfo,
|
||||
initialMigrationVersion: 27);
|
||||
initialMigrationVersion: 29);
|
||||
}
|
||||
|
||||
Future<void> initialSetup(
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -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: '=');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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('');
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
},
|
||||
),
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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),
|
||||
)),
|
||||
],
|
||||
),
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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
Loading…
Reference in a new issue