diff --git a/.github/assets/Logo_CakeWallet.png b/.github/assets/Logo_CakeWallet.png
new file mode 100644
index 000000000..459a6b37c
Binary files /dev/null and b/.github/assets/Logo_CakeWallet.png differ
diff --git a/.github/assets/NOTICE.txt b/.github/assets/NOTICE.txt
new file mode 100644
index 000000000..9719639a1
--- /dev/null
+++ b/.github/assets/NOTICE.txt
@@ -0,0 +1,48 @@
+Notice for linux-badge.svg:
+
+1:
+This is the Linux-penguin again...
+
+Originally drewn by Larry Ewing (http://www.isc.tamu.edu/~lewing/)
+(with the GIMP) the Linux Logo has been vectorized by me (Simon Budig,
+http://www.home.unix-ag.org/simon/).
+
+This happened quite some time ago with Corel Draw 4. But luckily
+meanwhile there are tools available to handle vector graphics with
+Linux. Bernhard Herzog (bernhard@users.sourceforge.net) deserves kudos
+for creating Sketch (http://sketch.sourceforge.net), a powerful free
+tool for creating vector graphics. He converted the Corel Draw file to
+the Sketch native format. Since I am unable to maintain the Corel Draw
+file any longer, the Sketch version now is the "official" one.
+
+Anja Gerwinski (anja@gerwinski.de) has created an alternate version of
+the penguin (penguin-variant.sk) with a thinner mouth line and slightly
+altered gradients. It also features a nifty drop shadow.
+
+The third bird (penguin-flat.sk) is a version reduced to three colors
+(black/white/yellow) for e.g. silk screen printing. I made this version
+for a mug, available at the friendly folks at
+http://www.kernelconcepts.de/ - they do good stuff, mail Petra
+(pinguin@kernelconcepts.de) if you need something special or don't
+understand the german :-)
+
+These drawings are copyrighted by Larry Ewing and Simon Budig
+(penguin-variant.sk also by Anja Gerwinski), redistribution is free but
+has to include this README/Copyright notice.
+
+The use of these drawings is free. However I am happy about a sample of
+your mug/t-shirt/whatever with this penguin on it...
+
+Have fun
+ Simon Budig
+
+
+Simon.Budig@unix-ag.org
+http://www.home.unix-ag.org/simon/
+
+Simon Budig
+Am Hardtkoeppel 2
+D-61279 Graevenwiesbach
+
+2:
+Attribution: lewing@isc.tamu.edu Larry Ewing and The GIMP
\ No newline at end of file
diff --git a/.github/assets/app-store-badge.svg b/.github/assets/app-store-badge.svg
new file mode 100755
index 000000000..072b425a1
--- /dev/null
+++ b/.github/assets/app-store-badge.svg
@@ -0,0 +1,46 @@
+
+ Download_on_the_App_Store_Badge_US-UK_RGB_blk_4SVG_092917
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.github/assets/devices.png b/.github/assets/devices.png
new file mode 100644
index 000000000..7bdccc5b5
Binary files /dev/null and b/.github/assets/devices.png differ
diff --git a/.github/assets/f-droid-badge.png b/.github/assets/f-droid-badge.png
new file mode 100644
index 000000000..2c9521de1
Binary files /dev/null and b/.github/assets/f-droid-badge.png differ
diff --git a/.github/assets/google-play-badge.png b/.github/assets/google-play-badge.png
new file mode 100644
index 000000000..9667c568d
Binary files /dev/null and b/.github/assets/google-play-badge.png differ
diff --git a/.github/assets/linux-badge.svg b/.github/assets/linux-badge.svg
new file mode 100755
index 000000000..8416e1bb1
--- /dev/null
+++ b/.github/assets/linux-badge.svg
@@ -0,0 +1,1071 @@
+
+linux-badge GET IT ON Linux linux-badge
diff --git a/.github/assets/mac-store-badge.svg b/.github/assets/mac-store-badge.svg
new file mode 100755
index 000000000..c36a76a5a
--- /dev/null
+++ b/.github/assets/mac-store-badge.svg
@@ -0,0 +1,51 @@
+
+ Download_on_the_Mac_App_Store_Badge_US-UK_RGB_blk_092917
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.github/workflows/pr_test_build.yml b/.github/workflows/pr_test_build.yml
index ddc8869f0..dc231df42 100644
--- a/.github/workflows/pr_test_build.yml
+++ b/.github/workflows/pr_test_build.yml
@@ -139,7 +139,9 @@ jobs:
echo "const anonPayReferralCode = '${{ secrets.ANON_PAY_REFERRAL_CODE }}';" >> lib/.secrets.g.dart
echo "const fiatApiKey = '${{ secrets.FIAT_API_KEY }}';" >> lib/.secrets.g.dart
echo "const payfuraApiKey = '${{ secrets.PAYFURA_API_KEY }}';" >> lib/.secrets.g.dart
+ echo "const ankrApiKey = '${{ secrets.ANKR_API_KEY }}';" >> lib/.secrets.g.dart
echo "const etherScanApiKey = '${{ secrets.ETHER_SCAN_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart
+ echo "const moralisApiKey = '${{ secrets.MORALIS_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart
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
diff --git a/README.md b/README.md
index 7b739f980..7823734fb 100644
--- a/README.md
+++ b/README.md
@@ -1,15 +1,35 @@
-# Cake Wallet for Mobile and Desktop
+
-## Open Source Multi-Currency Wallet
+
-## Links
+
-* Website: https://cakewallet.com
-* App Store (iOS / MacOS): https://cakewallet.com/ios
-* Google Play: https://cakewallet.com/gp
-* F-Droid: https://fdroid.cakelabs.com
-* APK: https://github.com/cake-tech/cake_wallet/releases
-* Linux: https://github.com/cake-tech/cake_wallet/releases
+![devices](.github/assets/devices.png)
+
+
+
+[
](https://apps.apple.com/us/app/cake-wallet/id1334702542?platform=iphone)
+[
](https://play.google.com/store/apps/details?id=com.cakewallet.cake_wallet)
+[
](https://fdroid.cakelabs.com)
+[
](https://apps.apple.com/us/app/cake-wallet/id1334702542?platform=mac)
+[
](https://github.com/cake-tech/cake_wallet/releases)
+
+
+
+# Cake Wallet
+
+Cake Wallet is an open source, non-custodial, and private multi-currency crypto wallet for Android, iOS, macOS, and Linux.
+
+Cake Wallet includes support for several cryptocurrencies, including:
+* Monero (XMR)
+* Bitcoin (BTC)
+* Ethereum (ETH)
+* Litecoin (LTC)
+* Bitcoin Cash (BCH)
+* Polygon (MATIC)
+* Solana (SOL)
+* Nano (XNO)
+* Haven (XHV)
## Features
diff --git a/assets/images/thorchain.png b/assets/images/thorchain.png
new file mode 100644
index 000000000..674b60f82
Binary files /dev/null and b/assets/images/thorchain.png differ
diff --git a/assets/text/Monerocom_Release_Notes.txt b/assets/text/Monerocom_Release_Notes.txt
index 90fcd2a75..e6aab2dda 100644
--- a/assets/text/Monerocom_Release_Notes.txt
+++ b/assets/text/Monerocom_Release_Notes.txt
@@ -1,4 +1,2 @@
-Monero enhancements
-In-App live status page for the app services
-Add Exolix exchange provider
-Bug fixes and enhancements
\ No newline at end of file
+Exchange flow enhancements and fixes
+Generic enhancements and bug fixes
\ No newline at end of file
diff --git a/assets/text/Release_Notes.txt b/assets/text/Release_Notes.txt
index 83e18c18e..b32cd539d 100644
--- a/assets/text/Release_Notes.txt
+++ b/assets/text/Release_Notes.txt
@@ -1 +1,6 @@
-Bug fixes and enhancements
\ No newline at end of file
+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
\ No newline at end of file
diff --git a/cw_bitcoin/lib/bitcoin_commit_transaction_exception.dart b/cw_bitcoin/lib/bitcoin_commit_transaction_exception.dart
index 3e21bae81..7bf488f3f 100644
--- a/cw_bitcoin/lib/bitcoin_commit_transaction_exception.dart
+++ b/cw_bitcoin/lib/bitcoin_commit_transaction_exception.dart
@@ -1,4 +1,8 @@
class BitcoinCommitTransactionException implements Exception {
+ String errorMessage;
+ BitcoinCommitTransactionException(this.errorMessage);
+
@override
- String toString() => 'Transaction commit is failed.';
-}
\ No newline at end of file
+ String toString() => errorMessage;
+}
+
diff --git a/cw_bitcoin/lib/bitcoin_transaction_credentials.dart b/cw_bitcoin/lib/bitcoin_transaction_credentials.dart
index bd8f1763c..bda7c39ae 100644
--- a/cw_bitcoin/lib/bitcoin_transaction_credentials.dart
+++ b/cw_bitcoin/lib/bitcoin_transaction_credentials.dart
@@ -2,7 +2,8 @@ import 'package:cw_bitcoin/bitcoin_transaction_priority.dart';
import 'package:cw_core/output_info.dart';
class BitcoinTransactionCredentials {
- BitcoinTransactionCredentials(this.outputs, {required this.priority, this.feeRate});
+ BitcoinTransactionCredentials(this.outputs,
+ {required this.priority, this.feeRate});
final List outputs;
final BitcoinTransactionPriority? priority;
diff --git a/cw_bitcoin/lib/bitcoin_transaction_no_inputs_exception.dart b/cw_bitcoin/lib/bitcoin_transaction_no_inputs_exception.dart
deleted file mode 100644
index fac7e93c4..000000000
--- a/cw_bitcoin/lib/bitcoin_transaction_no_inputs_exception.dart
+++ /dev/null
@@ -1,4 +0,0 @@
-class BitcoinTransactionNoInputsException implements Exception {
- @override
- String toString() => 'Not enough inputs available. Please select more under Coin Control';
-}
diff --git a/cw_bitcoin/lib/bitcoin_transaction_priority.dart b/cw_bitcoin/lib/bitcoin_transaction_priority.dart
index 10953a2e0..d51775368 100644
--- a/cw_bitcoin/lib/bitcoin_transaction_priority.dart
+++ b/cw_bitcoin/lib/bitcoin_transaction_priority.dart
@@ -4,13 +4,15 @@ class BitcoinTransactionPriority extends TransactionPriority {
const BitcoinTransactionPriority({required String title, required int raw})
: super(title: title, raw: raw);
- static const List all = [fast, medium, slow];
+ static const List all = [fast, medium, slow, custom];
static const BitcoinTransactionPriority slow =
BitcoinTransactionPriority(title: 'Slow', raw: 0);
static const BitcoinTransactionPriority medium =
BitcoinTransactionPriority(title: 'Medium', raw: 1);
static const BitcoinTransactionPriority fast =
BitcoinTransactionPriority(title: 'Fast', raw: 2);
+ static const BitcoinTransactionPriority custom =
+ BitcoinTransactionPriority(title: 'Custom', raw: 3);
static BitcoinTransactionPriority deserialize({required int raw}) {
switch (raw) {
@@ -20,6 +22,8 @@ class BitcoinTransactionPriority extends TransactionPriority {
return medium;
case 2:
return fast;
+ case 3:
+ return custom;
default:
throw Exception('Unexpected token: $raw for BitcoinTransactionPriority deserialize');
}
@@ -39,7 +43,10 @@ class BitcoinTransactionPriority extends TransactionPriority {
label = 'Medium'; // S.current.transaction_priority_medium;
break;
case BitcoinTransactionPriority.fast:
- label = 'Fast'; // S.current.transaction_priority_fast;
+ label = 'Fast';
+ break; // S.current.transaction_priority_fast;
+ case BitcoinTransactionPriority.custom:
+ label = 'Custom';
break;
default:
break;
@@ -48,7 +55,10 @@ class BitcoinTransactionPriority extends TransactionPriority {
return label;
}
- String labelWithRate(int rate) => '${toString()} ($rate ${units}/byte)';
+ String labelWithRate(int rate, int? customRate) {
+ final rateValue = this == custom ? customRate ??= 0 : rate;
+ return '${toString()} ($rateValue ${units}/byte)';
+ }
}
class LitecoinTransactionPriority extends BitcoinTransactionPriority {
diff --git a/cw_bitcoin/lib/bitcoin_transaction_wrong_balance_exception.dart b/cw_bitcoin/lib/bitcoin_transaction_wrong_balance_exception.dart
deleted file mode 100644
index 3f379bea0..000000000
--- a/cw_bitcoin/lib/bitcoin_transaction_wrong_balance_exception.dart
+++ /dev/null
@@ -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.';
-}
\ No newline at end of file
diff --git a/cw_bitcoin/lib/electrum.dart b/cw_bitcoin/lib/electrum.dart
index 51a53e285..0553170cc 100644
--- a/cw_bitcoin/lib/electrum.dart
+++ b/cw_bitcoin/lib/electrum.dart
@@ -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 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: 5);
@@ -44,6 +44,7 @@ class ElectrumClient {
void Function(bool)? onConnectionStatusChange;
int _id;
final Map _tasks;
+ final Map _errors;
bool _isConnected;
Timer? _aliveTimer;
String unterminatedString;
@@ -243,30 +244,20 @@ class ElectrumClient {
});
Future 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: {'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> getMerkle({required String hash, required int height}) async =>
await call(method: 'blockchain.transaction.get_merkle', params: [hash, height])
@@ -371,10 +362,12 @@ class ElectrumClient {
}
}
- Future call({required String method, List params = const []}) async {
+ Future call(
+ {required String method, List params = const [], Function(int)? idCallback}) async {
final completer = Completer();
_id += 1;
final id = _id;
+ idCallback?.call(id);
_registryTask(id, completer);
socket!.write(jsonrpc(method: method, id: id, params: params));
@@ -456,6 +449,23 @@ class ElectrumClient {
final id = response['id'] as String?;
final result = response['result'];
+ try {
+ final error = response['error'] as Map?;
+ 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;
@@ -465,6 +475,8 @@ class ElectrumClient {
_finish(id, result);
}
}
+
+ String getErrorMessage(int id) => _errors[id.toString()] ?? '';
}
// FIXME: move me
diff --git a/cw_bitcoin/lib/electrum_transaction_info.dart b/cw_bitcoin/lib/electrum_transaction_info.dart
index cfea0e089..f980bd884 100644
--- a/cw_bitcoin/lib/electrum_transaction_info.dart
+++ b/cw_bitcoin/lib/electrum_transaction_info.dart
@@ -11,12 +11,11 @@ import 'package:cw_core/wallet_type.dart';
class ElectrumTransactionBundle {
ElectrumTransactionBundle(this.originalTransaction,
- {required this.ins, required this.confirmations, this.time, required this.height});
+ {required this.ins, required this.confirmations, this.time});
final BtcTransaction originalTransaction;
final List ins;
final int? time;
final int confirmations;
- final int height;
}
class ElectrumTransactionInfo extends TransactionInfo {
@@ -25,6 +24,8 @@ class ElectrumTransactionInfo extends TransactionInfo {
required int height,
required int amount,
int? fee,
+ List? inputAddresses,
+ List? outputAddresses,
required TransactionDirection direction,
required bool isPending,
required DateTime date,
@@ -32,6 +33,8 @@ class ElectrumTransactionInfo extends TransactionInfo {
this.id = id;
this.height = height;
this.amount = amount;
+ this.inputAddresses = inputAddresses;
+ this.outputAddresses = outputAddresses;
this.fee = fee;
this.direction = direction;
this.date = date;
@@ -100,6 +103,8 @@ class ElectrumTransactionInfo extends TransactionInfo {
var amount = 0;
var inputAmount = 0;
var totalOutAmount = 0;
+ List inputAddresses = [];
+ List outputAddresses = [];
for (var i = 0; i < bundle.originalTransaction.inputs.length; i++) {
final input = bundle.originalTransaction.inputs[i];
@@ -108,6 +113,7 @@ class ElectrumTransactionInfo extends TransactionInfo {
inputAmount += outTransaction.amount.toInt();
if (addresses.contains(addressFromOutputScript(outTransaction.scriptPubKey, network))) {
direction = TransactionDirection.outgoing;
+ inputAddresses.add(addressFromOutputScript(outTransaction.scriptPubKey, network));
}
}
@@ -115,6 +121,7 @@ class ElectrumTransactionInfo extends TransactionInfo {
for (final out in bundle.originalTransaction.outputs) {
totalOutAmount += out.amount.toInt();
final addressExists = addresses.contains(addressFromOutputScript(out.scriptPubKey, network));
+ outputAddresses.add(addressFromOutputScript(out.scriptPubKey, network));
if (addressExists) {
receivedAmounts.add(out.amount.toInt());
@@ -137,6 +144,8 @@ class ElectrumTransactionInfo extends TransactionInfo {
id: bundle.originalTransaction.txId(),
height: height,
isPending: bundle.confirmations == 0,
+ inputAddresses: inputAddresses,
+ outputAddresses: outputAddresses,
fee: fee,
direction: direction,
amount: amount,
@@ -187,6 +196,8 @@ class ElectrumTransactionInfo extends TransactionInfo {
direction: parseTransactionDirectionFromInt(data['direction'] as int),
date: DateTime.fromMillisecondsSinceEpoch(data['date'] as int),
isPending: data['isPending'] as bool,
+ inputAddresses: data['inputAddresses'] as List,
+ outputAddresses: data['outputAddresses'] as List,
confirmations: data['confirmations'] as int);
}
@@ -218,6 +229,8 @@ class ElectrumTransactionInfo extends TransactionInfo {
direction: direction,
date: date,
isPending: isPending,
+ inputAddresses: inputAddresses,
+ outputAddresses: outputAddresses,
confirmations: info.confirmations);
}
@@ -231,6 +244,8 @@ class ElectrumTransactionInfo extends TransactionInfo {
m['isPending'] = isPending;
m['confirmations'] = confirmations;
m['fee'] = fee;
+ m['inputAddresses'] = inputAddresses;
+ m['outputAddresses'] = outputAddresses;
return m;
}
}
diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart
index 86fbd6dbe..5bed6a449 100644
--- a/cw_bitcoin/lib/electrum_wallet.dart
+++ b/cw_bitcoin/lib/electrum_wallet.dart
@@ -7,11 +7,11 @@ import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
import 'package:bitcoin_base/bitcoin_base.dart' as bitcoin_base;
import 'package:collection/collection.dart';
+import 'package:cw_bitcoin/address_from_output.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';
@@ -19,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';
@@ -188,26 +189,25 @@ abstract class ElectrumWalletBase
}
}
- Future estimateTxFeeAndInputsToUse(
- int credentialsAmount,
- bool sendAll,
- List outputAddresses,
- List outputs,
- int? feeRate,
- BitcoinTransactionPriority? priority,
- {int? inputsCount}) async {
+ int get _dustAmount => 546;
+
+ bool _isBelowDust(int amount) => amount <= _dustAmount && network != BitcoinNetwork.testnet;
+
+ Future estimateSendAllTx(
+ List outputs,
+ int feeRate, {
+ String? memo,
+ int credentialsAmount = 0,
+ }) async {
final utxos = [];
List privateKeys = [];
-
- var leftAmount = credentialsAmount;
- var allInputsAmount = 0;
+ int allInputsAmount = 0;
for (int i = 0; i < unspentCoins.length; i++) {
final utx = unspentCoins[i];
if (utx.isSending) {
allInputsAmount += utx.value;
- leftAmount = leftAmount - utx.value;
final address = addressTypeFromStr(utx.address, network);
final privkey = generateECPrivate(
@@ -225,15 +225,12 @@ abstract class ElectrumWalletBase
vout: utx.vout,
scriptType: _getScriptType(address),
),
- 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;
- }
}
}
@@ -241,120 +238,314 @@ 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,
+ );
}
- final estimatedSize = BitcoinTransactionBuilder.estimateTransactionSize(
- utxos: utxos, outputs: outputs, network: network);
-
- int fee = feeRate != null
- ? feeAmountWithFeeRate(feeRate, 0, 0, size: estimatedSize)
- : feeAmountForPriority(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,
+ privateKeys: privateKeys,
+ fee: fee,
+ amount: amount,
+ isSendAll: true,
+ hasChange: false,
+ memo: memo,
+ );
+ }
+
+ Future estimateTxForAmount(
+ int credentialsAmount,
+ List outputs,
+ int feeRate, {
+ int? inputsCount,
+ String? memo,
+ }) async {
+ final utxos = [];
+ List privateKeys = [];
+ int allInputsAmount = 0;
+
+ 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);
+ final privkey = generateECPrivate(
+ hd: utx.bitcoinAddressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd,
+ index: utx.bitcoinAddressRecord.index,
+ network: network);
+
+ privateKeys.add(privkey);
+
+ utxos.add(
+ UtxoWithAddress(
+ utxo: BitcoinUtxo(
+ txHash: utx.hash,
+ value: BigInt.from(utx.value),
+ vout: utx.vout,
+ scriptType: _getScriptType(address),
+ ),
+ 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 - _dustAmount - 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, feeRate, priority,
- inputsCount: utxos.length + 1);
+ return estimateTxForAmount(
+ credentialsAmount,
+ outputs,
+ feeRate,
+ inputsCount: utxos.length + 1,
+ memo: memo,
+ );
}
}
- return EstimatedTxResult(utxos: utxos, privateKeys: privateKeys, fee: fee, amount: amount);
+ return EstimatedTxResult(
+ utxos: utxos,
+ privateKeys: privateKeys,
+ fee: fee,
+ amount: amount,
+ hasChange: true,
+ isSendAll: false,
+ memo: memo,
+ );
}
@override
Future createTransaction(Object credentials) async {
try {
final outputs = [];
- final outputAddresses = [];
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;
for (final out in transactionCredentials.outputs) {
- final outputAddress = out.isParsedAddress ? out.extractedAddress! : out.address;
- final address = addressTypeFromStr(outputAddress, network);
+ final outputAmount = out.formattedCryptoAmount!;
- outputAddresses.add(address);
+ if (!sendAll && _isBelowDust(outputAmount)) {
+ throw BitcoinTransactionNoDustException();
+ }
if (hasMultiDestination) {
- if (out.sendAll || out.formattedCryptoAmount! <= 0) {
- throw BitcoinTransactionWrongBalanceException(currency);
+ if (out.sendAll) {
+ throw BitcoinTransactionWrongBalanceException();
}
+ }
- final outputAmount = out.formattedCryptoAmount!;
- credentialsAmount += outputAmount;
+ credentialsAmount += outputAmount;
- outputs.add(BitcoinOutput(address: address, value: BigInt.from(outputAmount)));
+ final address =
+ addressTypeFromStr(out.isParsedAddress ? out.extractedAddress! : out.address, network);
+
+ 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.feeRate,
- transactionCredentials.priority,
- );
+ 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,
+ );
+ } else {
+ estimatedTx = await estimateTxForAmount(
+ credentialsAmount,
+ outputs,
+ feeRateInt,
+ memo: memo,
+ );
+ }
+
+ 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,
+ enableRBF: true,
+ );
+ } else {
+ txb = BitcoinTransactionBuilder(
+ utxos: estimatedTx.utxos,
+ outputs: outputs,
+ fee: BigInt.from(estimatedTx.fee),
+ network: network,
+ memo: estimatedTx.memo,
+ outputOrdering: BitcoinOrdering.none,
+ enableRBF: true,
+ );
+ }
+
+ bool hasTaprootInputs = false;
final transaction = txb.buildTransaction((txDigest, utxo, publicKey, sighash) {
final key = estimatedTx.privateKeys
@@ -365,18 +556,25 @@ abstract class ElectrumWalletBase
}
if (utxo.utxo.isP2tr()) {
+ hasTaprootInputs = true;
return key.signTapRoot(txDigest, sighash: sighash);
} else {
return key.signInput(txDigest, sigHash: sighash);
}
});
- 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);
await updateBalance();
});
@@ -408,7 +606,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));
@@ -597,8 +795,180 @@ abstract class ElectrumWalletBase
}
}
- Future getTransactionExpanded(
- {required String hash, required int height}) async {
+ Future canReplaceByFee(String hash) async {
+ final verboseTransaction = await electrumClient.getTransactionRaw(hash: hash);
+ final confirmations = verboseTransaction['confirmations'] as int? ?? 0;
+ final transactionHex = verboseTransaction['hex'] as String?;
+
+ if (confirmations > 0) return false;
+
+ if (transactionHex == null) {
+ return false;
+ }
+
+ final original = bitcoin.Transaction.fromHex(transactionHex);
+
+ return original.ins
+ .any((element) => element.sequence != null && element.sequence! < 4294967293);
+ }
+
+ Future isChangeSufficientForFee(String txId, int newFee) async {
+ final bundle = await getTransactionExpanded(hash: txId);
+ final outputs = bundle.originalTransaction.outputs;
+
+ final changeAddresses = walletAddresses.allAddresses.where((element) => element.isHidden);
+
+ // look for a change address in the outputs
+ final changeOutput = outputs.firstWhereOrNull((output) => changeAddresses.any(
+ (element) => element.address == addressFromOutputScript(output.scriptPubKey, network)));
+
+ var allInputsAmount = 0;
+
+ for (int i = 0; i < bundle.originalTransaction.inputs.length; i++) {
+ final input = bundle.originalTransaction.inputs[i];
+ final inputTransaction = bundle.ins[i];
+ final vout = input.txIndex;
+ final outTransaction = inputTransaction.outputs[vout];
+ allInputsAmount += outTransaction.amount.toInt();
+ }
+
+ int totalOutAmount = bundle.originalTransaction.outputs
+ .fold(0, (previousValue, element) => previousValue + element.amount.toInt());
+
+ var currentFee = allInputsAmount - totalOutAmount;
+
+ int remainingFee = (newFee - currentFee > 0) ? newFee - currentFee : newFee;
+
+ return changeOutput != null && changeOutput.amount.toInt() - remainingFee >= 0;
+ }
+
+ Future replaceByFee(String hash, int newFee) async {
+ try {
+ final bundle = await getTransactionExpanded(hash: hash);
+
+ final utxos = [];
+ List privateKeys = [];
+
+ var allInputsAmount = 0;
+
+ // Add inputs
+ for (var i = 0; i < bundle.originalTransaction.inputs.length; i++) {
+ final input = bundle.originalTransaction.inputs[i];
+ final inputTransaction = bundle.ins[i];
+ final vout = input.txIndex;
+ final outTransaction = inputTransaction.outputs[vout];
+ final address = addressFromOutputScript(outTransaction.scriptPubKey, network);
+ allInputsAmount += outTransaction.amount.toInt();
+
+ final addressRecord =
+ walletAddresses.allAddresses.firstWhere((element) => element.address == address);
+
+ final btcAddress = addressTypeFromStr(addressRecord.address, network);
+ final privkey = generateECPrivate(
+ hd: addressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd,
+ index: addressRecord.index,
+ network: network);
+
+ privateKeys.add(privkey);
+
+ utxos.add(
+ UtxoWithAddress(
+ utxo: BitcoinUtxo(
+ txHash: input.txId,
+ value: outTransaction.amount,
+ vout: vout,
+ scriptType: _getScriptType(btcAddress),
+ ),
+ ownerDetails:
+ UtxoAddressDetails(publicKey: privkey.getPublic().toHex(), address: btcAddress),
+ ),
+ );
+ }
+
+ int totalOutAmount = bundle.originalTransaction.outputs
+ .fold(0, (previousValue, element) => previousValue + element.amount.toInt());
+
+ var currentFee = allInputsAmount - totalOutAmount;
+ int remainingFee = newFee - currentFee;
+
+ final outputs = [];
+
+ // Add outputs and deduct the fees from it
+ for (int i = bundle.originalTransaction.outputs.length - 1; i >= 0; i--) {
+ final out = bundle.originalTransaction.outputs[i];
+ final address = addressFromOutputScript(out.scriptPubKey, network);
+ final btcAddress = addressTypeFromStr(address, network);
+
+ int newAmount;
+ if (out.amount.toInt() >= remainingFee) {
+ newAmount = out.amount.toInt() - remainingFee;
+ remainingFee = 0;
+
+ // if new amount of output is less than dust amount, then don't add this output as well
+ if (newAmount <= _dustAmount) {
+ continue;
+ }
+ } else {
+ remainingFee -= out.amount.toInt();
+ continue;
+ }
+
+ outputs.add(BitcoinOutput(address: btcAddress, value: BigInt.from(newAmount)));
+ }
+
+ final changeAddresses = walletAddresses.allAddresses.where((element) => element.isHidden);
+
+ // look for a change address in the outputs
+ final changeOutput = outputs.firstWhereOrNull((output) =>
+ changeAddresses.any((element) => element.address == output.address.toAddress(network)));
+
+ // deduct the change amount from the output amount
+ if (changeOutput != null) {
+ totalOutAmount -= changeOutput.value.toInt();
+ }
+
+ final txb = BitcoinTransactionBuilder(
+ utxos: utxos,
+ outputs: outputs,
+ fee: BigInt.from(newFee),
+ network: network,
+ enableRBF: true,
+ );
+
+ final transaction = txb.buildTransaction((txDigest, utxo, publicKey, sighash) {
+ final key =
+ privateKeys.firstWhereOrNull((element) => element.getPublic().toHex() == publicKey);
+
+ if (key == null) {
+ throw Exception("Cannot find private key");
+ }
+
+ if (utxo.utxo.isP2tr()) {
+ return key.signTapRoot(txDigest, sighash: sighash);
+ } else {
+ return key.signInput(txDigest, sigHash: sighash);
+ }
+ });
+
+ return PendingBitcoinTransaction(
+ transaction,
+ type,
+ electrumClient: electrumClient,
+ amount: totalOutAmount,
+ fee: newFee,
+ network: network,
+ hasChange: changeOutput != null,
+ feeRate: newFee.toString(),
+ )..addListener((transaction) async {
+ transactionHistory.addOne(transaction);
+ await updateBalance();
+ });
+ } catch (e) {
+ throw e;
+ }
+ }
+
+ Future getTransactionExpanded({required String hash}) async {
String transactionHex;
int? time;
int confirmations = 0;
@@ -629,8 +999,12 @@ abstract class ElectrumWalletBase
ins.add(tx);
}
- return ElectrumTransactionBundle(original,
- ins: ins, time: time, confirmations: confirmations, height: height);
+ return ElectrumTransactionBundle(
+ original,
+ ins: ins,
+ time: time,
+ confirmations: confirmations,
+ );
}
Future fetchTransactionInfo(
@@ -640,7 +1014,7 @@ abstract class ElectrumWalletBase
bool? retryOnFailure}) async {
try {
return ElectrumTransactionInfo.fromElectrumBundle(
- await getTransactionExpanded(hash: hash, height: height), walletInfo.type, network,
+ await getTransactionExpanded(hash: hash), walletInfo.type, network,
addresses: myAddresses, height: height);
} catch (e) {
if (e is FormatException && retryOnFailure == true) {
@@ -888,16 +1262,35 @@ class EstimateTxParams {
}
class EstimatedTxResult {
- EstimatedTxResult(
- {required this.utxos, required this.privateKeys, required this.fee, required this.amount});
+ EstimatedTxResult({
+ required this.utxos,
+ required this.privateKeys,
+ required this.fee,
+ required this.amount,
+ required this.hasChange,
+ required this.isSendAll,
+ this.memo,
+ });
final List utxos;
final List privateKeys;
final int fee;
final int amount;
+ final bool hasChange;
+ final bool isSendAll;
+ final String? memo;
}
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)) {
diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart
index 69d0a6385..ac2397561 100644
--- a/cw_bitcoin/lib/electrum_wallet_addresses.dart
+++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart
@@ -77,7 +77,8 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
String get address {
String receiveAddress;
- final typeMatchingReceiveAddresses = receiveAddresses.where(_isAddressPageTypeMatch);
+ final typeMatchingReceiveAddresses =
+ receiveAddresses.where(_isAddressPageTypeMatch).where((addr) => !addr.isUsed);
if ((isEnabledAutoGenerateSubaddress && receiveAddresses.isEmpty) ||
typeMatchingReceiveAddresses.isEmpty) {
@@ -220,8 +221,11 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
Future updateAddressesInBox() async {
try {
addressesMap.clear();
+ addressesMap[address] = '';
+
+ allAddressesMap.clear();
_addresses.forEach((addressRecord) {
- addressesMap[addressRecord.address] = addressRecord.name;
+ allAddressesMap[addressRecord.address] = addressRecord.name;
});
await saveAddressesInBox();
} catch (e) {
diff --git a/cw_bitcoin/lib/exceptions.dart b/cw_bitcoin/lib/exceptions.dart
new file mode 100644
index 000000000..4b03eb922
--- /dev/null
+++ b/cw_bitcoin/lib/exceptions.dart
@@ -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 {}
diff --git a/cw_bitcoin/lib/pending_bitcoin_transaction.dart b/cw_bitcoin/lib/pending_bitcoin_transaction.dart
index fa413febd..529ac61da 100644
--- a/cw_bitcoin/lib/pending_bitcoin_transaction.dart
+++ b/cw_bitcoin/lib/pending_bitcoin_transaction.dart
@@ -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 = [];
+ PendingBitcoinTransaction(
+ this._tx,
+ this.type, {
+ required this.electrumClient,
+ required this.amount,
+ required this.fee,
+ required this.feeRate,
+ this.network,
+ required this.hasChange,
+ this.isSendAll = false,
+ this.hasTaprootInputs = false,
+ }) : _listeners = [];
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 _listeners;
@override
Future 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()));
diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock
index b39dcae07..3d828243c 100644
--- a/cw_bitcoin/pubspec.lock
+++ b/cw_bitcoin/pubspec.lock
@@ -70,8 +70,8 @@ packages:
dependency: "direct main"
description:
path: "."
- ref: master
- resolved-ref: ea65073efbaf395a5557e8cd7bd72f195cd7eb11
+ ref: Add-Support-For-OP-Return-data
+ resolved-ref: "57b78afb85bd2c30d3cdb9f7884f3878a62be442"
url: "https://github.com/cake-tech/bitbox-flutter.git"
source: git
version: "1.0.1"
diff --git a/cw_bitcoin/pubspec.yaml b/cw_bitcoin/pubspec.yaml
index bcbb55e11..632a3140a 100644
--- a/cw_bitcoin/pubspec.yaml
+++ b/cw_bitcoin/pubspec.yaml
@@ -26,7 +26,7 @@ dependencies:
bitbox:
git:
url: https://github.com/cake-tech/bitbox-flutter.git
- ref: master
+ ref: Add-Support-For-OP-Return-data
rxdart: ^0.27.5
unorm_dart: ^0.2.0
cryptography: ^2.0.5
diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart
index f5835e728..1f04e5624 100644
--- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart
+++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart
@@ -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';
@@ -130,184 +125,9 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
);
}
- @override
- Future createTransaction(Object credentials) async {
- const minAmount = 546;
- final transactionCredentials = credentials as BitcoinTransactionCredentials;
- final inputs = [];
- 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;
diff --git a/cw_bitcoin_cash/lib/src/pending_bitcoin_cash_transaction.dart b/cw_bitcoin_cash/lib/src/pending_bitcoin_cash_transaction.dart
index d5ac36ce2..da4710a8b 100644
--- a/cw_bitcoin_cash/lib/src/pending_bitcoin_cash_transaction.dart
+++ b/cw_bitcoin_cash/lib/src/pending_bitcoin_cash_transaction.dart
@@ -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 = [];
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 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,
diff --git a/cw_bitcoin_cash/pubspec.yaml b/cw_bitcoin_cash/pubspec.yaml
index 7130b3c58..37827f1ba 100644
--- a/cw_bitcoin_cash/pubspec.yaml
+++ b/cw_bitcoin_cash/pubspec.yaml
@@ -28,7 +28,7 @@ dependencies:
bitbox:
git:
url: https://github.com/cake-tech/bitbox-flutter.git
- ref: master
+ ref: Add-Support-For-OP-Return-data
bitcoin_base:
git:
url: https://github.com/cake-tech/bitcoin_base.git
diff --git a/cw_core/lib/crypto_currency.dart b/cw_core/lib/crypto_currency.dart
index 9cebce10a..f1c1cd8ae 100644
--- a/cw_core/lib/crypto_currency.dart
+++ b/cw_core/lib/crypto_currency.dart
@@ -38,6 +38,8 @@ class CryptoCurrency extends EnumerableItem with Serializable 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 with Serializable implemen
CryptoCurrency.usdttrc20,
CryptoCurrency.hbar,
CryptoCurrency.sc,
- CryptoCurrency.sol,
CryptoCurrency.usdc,
CryptoCurrency.usdcsol,
CryptoCurrency.zaddr,
@@ -61,7 +62,6 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen
CryptoCurrency.dcr,
CryptoCurrency.kmd,
CryptoCurrency.mana,
- CryptoCurrency.maticpoly,
CryptoCurrency.matic,
CryptoCurrency.mkr,
CryptoCurrency.near,
diff --git a/cw_core/lib/exceptions.dart b/cw_core/lib/exceptions.dart
new file mode 100644
index 000000000..848ac40e6
--- /dev/null
+++ b/cw_core/lib/exceptions.dart
@@ -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 {}
diff --git a/cw_core/lib/output_info.dart b/cw_core/lib/output_info.dart
index e2b1201a8..9e3ac4ffc 100644
--- a/cw_core/lib/output_info.dart
+++ b/cw_core/lib/output_info.dart
@@ -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;
}
\ No newline at end of file
diff --git a/cw_core/lib/pending_transaction.dart b/cw_core/lib/pending_transaction.dart
index cc5686fc9..642db9c2c 100644
--- a/cw_core/lib/pending_transaction.dart
+++ b/cw_core/lib/pending_transaction.dart
@@ -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 commit();
-}
\ No newline at end of file
+}
diff --git a/cw_core/lib/transaction_info.dart b/cw_core/lib/transaction_info.dart
index 7624b147f..992582ff8 100644
--- a/cw_core/lib/transaction_info.dart
+++ b/cw_core/lib/transaction_info.dart
@@ -16,6 +16,8 @@ abstract class TransactionInfo extends Object with Keyable {
void changeFiatAmount(String amount);
String? to;
String? from;
+ List? inputAddresses;
+ List? outputAddresses;
@override
dynamic get keyIndex => id;
diff --git a/cw_core/lib/wallet_addresses.dart b/cw_core/lib/wallet_addresses.dart
index d8c84c80c..a2a2a50a3 100644
--- a/cw_core/lib/wallet_addresses.dart
+++ b/cw_core/lib/wallet_addresses.dart
@@ -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 addressesMap;
+ Map allAddressesMap;
Map> addressInfos;
@@ -39,5 +41,5 @@ abstract class WalletAddresses {
}
}
- bool containsAddress(String address) => addressesMap.containsKey(address);
+ bool containsAddress(String address) => allAddressesMap.containsKey(address);
}
diff --git a/cw_core/lib/wallet_base.dart b/cw_core/lib/wallet_base.dart
index 49f1bdc94..037a26d38 100644
--- a/cw_core/lib/wallet_base.dart
+++ b/cw_core/lib/wallet_base.dart
@@ -67,6 +67,7 @@ abstract class WalletBase> 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;
+
+ if (response.statusCode >= 200 && response.statusCode < 300 && jsonResponse['status'] != 0) {
+ return (jsonResponse['result'] as List)
+ .map((e) => EVMChainTransactionModel.fromJson(e as Map, 'ETH'))
+ .toList();
+ }
+
+ return [];
+ } catch (e) {
+ log(e.toString());
+ return [];
+ }
+ }
}
diff --git a/cw_evm/lib/evm_chain_client.dart b/cw_evm/lib/evm_chain_client.dart
index de5b3874a..eebbe4f4f 100644
--- a/cw_evm/lib/evm_chain_client.dart
+++ b/cw_evm/lib/evm_chain_client.dart
@@ -1,4 +1,5 @@
import 'dart:async';
+import 'dart:convert';
import 'dart:developer';
import 'package:cw_core/node.dart';
@@ -9,11 +10,13 @@ import 'package:cw_evm/evm_erc20_balance.dart';
import 'package:cw_evm/evm_chain_transaction_model.dart';
import 'package:cw_evm/pending_evm_chain_transaction.dart';
import 'package:cw_evm/evm_chain_transaction_priority.dart';
+import 'package:cw_evm/.secrets.g.dart' as secrets;
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 +29,8 @@ abstract class EVMChainClient {
Future> fetchTransactions(String address,
{String? contractAddress});
+ Future> fetchInternalTransactions(String address);
+
Uint8List prepareSignedTransactionForSending(Uint8List signedTransaction);
//! Common methods across all child classes
@@ -79,12 +84,13 @@ abstract class EVMChainClient {
Future 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 +105,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 +126,7 @@ abstract class EVMChainClient {
_sendTransaction = () async {
await erc20.transfer(
EthereumAddress.fromHex(toAddress),
- BigInt.parse(amount),
+ amount,
credentials: privateKey,
transaction: transaction,
);
@@ -128,7 +135,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 +147,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,
);
}
@@ -204,24 +213,63 @@ abstract class EVMChainClient {
return EVMChainERC20Balance(balance, exponent: exponent);
}
- Future getErc20Token(String contractAddress) async {
+ Future getErc20Token(String contractAddress, String chainName) async {
try {
- final erc20 = ERC20(address: EthereumAddress.fromHex(contractAddress), client: _client!);
- final name = await erc20.name();
- final symbol = await erc20.symbol();
- final decimal = await erc20.decimals();
+ final uri = Uri.https(
+ 'deep-index.moralis.io',
+ '/api/v2.2/erc20/metadata',
+ {
+ "chain": chainName,
+ "addresses": contractAddress,
+ },
+ );
+
+ final response = await httpClient.get(
+ uri,
+ headers: {
+ "Accept": "application/json",
+ "X-API-Key": secrets.moralisApiKey,
+ },
+ );
+
+ final decodedResponse = jsonDecode(response.body)[0] as Map;
+
+ final name = decodedResponse['name'] ?? '';
+ final symbol = decodedResponse['symbol'] ?? '';
+ final decimal = decodedResponse['decimals'] ?? '0';
+ final iconPath = decodedResponse['logo'] ?? '';
return Erc20Token(
name: name,
symbol: symbol,
contractAddress: contractAddress,
- decimal: decimal.toInt(),
+ decimal: int.tryParse(decimal) ?? 0,
+ iconPath: iconPath,
);
} catch (e) {
+ try {
+ final erc20 = ERC20(address: EthereumAddress.fromHex(contractAddress), client: _client!);
+ final name = await erc20.name();
+ final symbol = await erc20.symbol();
+ final decimal = await erc20.decimals();
+
+ return Erc20Token(
+ name: name,
+ symbol: symbol,
+ contractAddress: contractAddress,
+ decimal: decimal.toInt(),
+ );
+ } catch (_) {}
+
return null;
}
}
+ Uint8List hexToBytes(String hexString) {
+ return Uint8List.fromList(
+ hex.HEX.decode(hexString.startsWith('0x') ? hexString.substring(2) : hexString));
+ }
+
void stop() {
_client?.dispose();
}
diff --git a/cw_evm/lib/evm_chain_exceptions.dart b/cw_evm/lib/evm_chain_exceptions.dart
index 1c09ecf6d..8aa371b19 100644
--- a/cw_evm/lib/evm_chain_exceptions.dart
+++ b/cw_evm/lib/evm_chain_exceptions.dart
@@ -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;
+}
diff --git a/cw_evm/lib/evm_chain_transaction_model.dart b/cw_evm/lib/evm_chain_transaction_model.dart
index a328a2d6d..dfdeab8f5 100644
--- a/cw_evm/lib/evm_chain_transaction_model.dart
+++ b/cw_evm/lib/evm_chain_transaction_model.dart
@@ -32,15 +32,15 @@ class EVMChainTransactionModel {
factory EVMChainTransactionModel.fromJson(Map 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",
diff --git a/cw_evm/lib/evm_chain_wallet.dart b/cw_evm/lib/evm_chain_wallet.dart
index 0fb282960..4193e590a 100644
--- a/cw_evm/lib/evm_chain_wallet.dart
+++ b/cw_evm/lib/evm_chain_wallet.dart
@@ -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> fetchTransactions() async {
final address = _evmChainPrivateKey.address.hex;
final transactions = await _client.fetchTransactions(address);
+ final internalTransactions = await _client.fetchInternalTransactions(address);
final List>> 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 result = {};
@@ -420,11 +439,16 @@ abstract class EVMChainWalletBase
Future addErc20Token(Erc20Token token) async {
String? iconPath;
- try {
- iconPath = CryptoCurrency.all
- .firstWhere((element) => element.title.toUpperCase() == token.symbol.toUpperCase())
- .iconPath;
- } catch (_) {}
+
+ if (token.iconPath == null || token.iconPath!.isEmpty) {
+ try {
+ iconPath = CryptoCurrency.all
+ .firstWhere((element) => element.title.toUpperCase() == token.symbol.toUpperCase())
+ .iconPath;
+ } catch (_) {}
+ } else {
+ iconPath = token.iconPath;
+ }
final newToken = createNewErc20TokenObject(token, iconPath);
@@ -447,8 +471,8 @@ abstract class EVMChainWalletBase
_updateBalance();
}
- Future getErc20Token(String contractAddress) async =>
- await _client.getErc20Token(contractAddress);
+ Future getErc20Token(String contractAddress, String chainName) async =>
+ await _client.getErc20Token(contractAddress, chainName);
void _onNewTransaction() {
_updateBalance();
@@ -484,7 +508,7 @@ abstract class EVMChainWalletBase
_transactionsUpdateTimer!.cancel();
}
- _transactionsUpdateTimer = Timer.periodic(const Duration(seconds: 10), (_) {
+ _transactionsUpdateTimer = Timer.periodic(const Duration(seconds: 15), (_) {
_updateTransactions();
_updateBalance();
});
diff --git a/cw_evm/lib/pending_evm_chain_transaction.dart b/cw_evm/lib/pending_evm_chain_transaction.dart
index 8129de728..0b367da68 100644
--- a/cw_evm/lib/pending_evm_chain_transaction.dart
+++ b/cw_evm/lib/pending_evm_chain_transaction.dart
@@ -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)}';
+ }
}
diff --git a/cw_polygon/lib/polygon_client.dart b/cw_polygon/lib/polygon_client.dart
index 055b42f87..d55ee2269 100644
--- a/cw_polygon/lib/polygon_client.dart
+++ b/cw_polygon/lib/polygon_client.dart
@@ -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> 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;
+
+ if (response.statusCode >= 200 && response.statusCode < 300 && jsonResponse['status'] != 0) {
+ return (jsonResponse['result'] as List)
+ .map((e) => EVMChainTransactionModel.fromJson(e as Map, 'MATIC'))
+ .toList();
+ }
+
+ return [];
+ } catch (_) {
+ return [];
+ }
+ }
}
diff --git a/cw_solana/lib/solana_client.dart b/cw_solana/lib/solana_client.dart
index ea4a9161a..6ed8cab29 100644
--- a/cw_solana/lib/solana_client.dart
+++ b/cw_solana/lib/solana_client.dart
@@ -96,16 +96,30 @@ class SolanaWalletClient {
return SolanaBalance(totalBalance);
}
- Future getGasForMessage(String message) async {
+ Future 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 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> fetchTransactions(
Ed25519HDPublicKey publicKey, {
@@ -257,24 +271,15 @@ class SolanaWalletClient {
Future signSolanaTransaction({
required String tokenTitle,
required int tokenDecimals,
- String? tokenMint,
required double inputAmount,
required String destinationAddress,
required Ed25519HDKeyPair ownerKeypair,
+ required bool isSendAll,
+ String? tokenMint,
List 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 _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 _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 _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 _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 _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 _signTransactionInternal({
required Message message,
required List signers,
@@ -466,13 +519,35 @@ 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);
+ }
+ }
+
+ Future getIconImageFromTokenUri(String uri) async {
+ try {
+ final response = await httpClient.get(Uri.parse(uri));
+
+ final jsonResponse = json.decode(response.body) as Map;
+
+ if (response.statusCode >= 200 && response.statusCode < 300) {
+ return jsonResponse['image'];
+ } else {
+ return null;
+ }
+ } catch (e) {
+ print('Error occurred while fetching token image: \n${e.toString()}');
+ return null;
+ }
}
}
diff --git a/cw_solana/lib/solana_wallet.dart b/cw_solana/lib/solana_wallet.dart
index de4d70674..ad58c4293 100644
--- a/cw_solana/lib/solana_wallet.dart
+++ b/cw_solana/lib/solana_wallet.dart
@@ -75,6 +75,9 @@ abstract class SolanaWalletBase
late SolanaWalletClient _client;
+ @observable
+ double? estimatedFee;
+
Timer? _transactionsUpdateTimer;
late final Box splTokensBox;
@@ -171,6 +174,14 @@ abstract class SolanaWalletBase
}
}
+ Future _getEstimatedFees() async {
+ try {
+ estimatedFee = await _client.getEstimatedFee(_walletKeyPair!);
+ } catch (e) {
+ estimatedFee = 0.0;
+ }
+ }
+
@override
Future createTransaction(Object credentials) async {
final solCredentials = credentials as SolanaTransactionCredentials;
@@ -188,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);
@@ -204,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);
@@ -228,6 +247,7 @@ abstract class SolanaWalletBase
destinationAddress: solCredentials.outputs.first.isParsedAddress
? solCredentials.outputs.first.extractedAddress!
: solCredentials.outputs.first.address,
+ isSendAll: isSendAll,
);
return pendingSolanaTransaction;
@@ -269,7 +289,10 @@ abstract class SolanaWalletBase
Future _updateSPLTokenTransactions() async {
List splTokenTransactions = [];
- for (var token in balance.keys) {
+ // Make a copy of keys to avoid concurrent modification
+ var tokenKeys = List.from(balance.keys);
+
+ for (var token in tokenKeys) {
if (token is SPLToken) {
final tokenTxs = await _client.getSPLTokenTransfers(
token.mintAddress,
@@ -326,6 +349,7 @@ abstract class SolanaWalletBase
_updateBalance(),
_updateNativeSOLTransactions(),
_updateSPLTokenTransactions(),
+ _getEstimatedFees(),
]);
syncStatus = SyncedSyncStatus();
@@ -433,18 +457,28 @@ 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;
+ }
+
+ String? iconPath;
+ try {
+ iconPath = await _client.getIconImageFromTokenUri(token.uri);
+ } catch (_) {}
+
+ return SPLToken.fromMetadata(
+ name: token.name,
+ mint: token.mint,
+ symbol: token.symbol,
+ mintAddress: mintAddress,
+ iconPath: iconPath,
+ );
+ } catch (e) {
return null;
}
-
- return SPLToken.fromMetadata(
- name: token.name,
- mint: token.mint,
- symbol: token.symbol,
- mintAddress: mintAddress,
- );
}
@override
@@ -475,9 +509,9 @@ abstract class SolanaWalletBase
}
_transactionsUpdateTimer = Timer.periodic(const Duration(seconds: 20), (_) {
- _updateSPLTokenTransactions();
- _updateNativeSOLTransactions();
_updateBalance();
+ _updateNativeSOLTransactions();
+ _updateSPLTokenTransactions();
});
}
diff --git a/cw_solana/lib/solana_wallet_service.dart b/cw_solana/lib/solana_wallet_service.dart
index b3ff22e7e..83370ff73 100644
--- a/cw_solana/lib/solana_wallet_service.dart
+++ b/cw_solana/lib/solana_wallet_service.dart
@@ -32,6 +32,7 @@ class SolanaWalletService extends WalletService 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 outputs,
- {required TransactionPriority priority, int? feeRate}) =>
- BitcoinTransactionCredentials(
- outputs
- .map((out) => OutputInfo(
- fiatAmount: out.fiatAmount,
- cryptoAmount: out.cryptoAmount,
- address: out.address,
- note: out.note,
- sendAll: out.sendAll,
- extractedAddress: out.extractedAddress,
- isParsedAddress: out.isParsedAddress,
- formattedCryptoAmount: out.formattedCryptoAmount))
- .toList(),
- priority: priority as BitcoinTransactionPriority,
- feeRate: feeRate);
+ {required TransactionPriority priority, int? feeRate}) {
+ final bitcoinFeeRate =
+ priority == BitcoinTransactionPriority.custom && feeRate != null ? feeRate : null;
+ return BitcoinTransactionCredentials(
+ outputs
+ .map((out) => OutputInfo(
+ fiatAmount: out.fiatAmount,
+ cryptoAmount: out.cryptoAmount,
+ address: out.address,
+ note: out.note,
+ sendAll: out.sendAll,
+ extractedAddress: out.extractedAddress,
+ isParsedAddress: out.isParsedAddress,
+ formattedCryptoAmount: out.formattedCryptoAmount,
+ memo: out.memo))
+ .toList(),
+ priority: priority as BitcoinTransactionPriority,
+ feeRate: bitcoinFeeRate
+ );
+ }
@override
Object createBitcoinTransactionCredentialsRaw(List outputs,
@@ -122,23 +127,30 @@ class CWBitcoin extends Bitcoin {
@override
Future estimateFakeSendAllTxAmount(Object wallet, TransactionPriority priority) async {
- final electrumWallet = wallet as ElectrumWallet;
- final sk = ECPrivate.random();
-
- final p2shAddr = sk.getPublic().toP2pkhInP2sh();
- final p2wpkhAddr = sk.getPublic().toP2wpkhAddress();
try {
- final estimatedTx = await electrumWallet.estimateTxFeeAndInputsToUse(
- 0,
- true,
- // Deposit address + change address
- [p2shAddr, p2wpkhAddr],
- [
- BitcoinOutput(address: p2shAddr, value: BigInt.zero),
- BitcoinOutput(address: p2wpkhAddr, value: BigInt.zero)
- ],
- null,
- priority as BitcoinTransactionPriority);
+ 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 (_) {
@@ -164,8 +176,9 @@ class CWBitcoin extends Bitcoin {
int formatterStringDoubleToBitcoinAmount(String amount) => stringDoubleToBitcoinAmount(amount);
@override
- String bitcoinTransactionPriorityWithLabel(TransactionPriority priority, int rate) =>
- (priority as BitcoinTransactionPriority).labelWithRate(rate);
+ String bitcoinTransactionPriorityWithLabel(TransactionPriority priority, int rate,
+ {int? customRate}) =>
+ (priority as BitcoinTransactionPriority).labelWithRate(rate, customRate);
@override
List getUnspents(Object wallet) {
@@ -191,6 +204,9 @@ class CWBitcoin extends Bitcoin {
@override
TransactionPriority getBitcoinTransactionPriorityMedium() => BitcoinTransactionPriority.medium;
+ @override
+ TransactionPriority getBitcoinTransactionPriorityCustom() => BitcoinTransactionPriority.custom;
+
@override
TransactionPriority getLitecoinTransactionPriorityMedium() => LitecoinTransactionPriority.medium;
@@ -231,4 +247,48 @@ class CWBitcoin extends Bitcoin {
return SegwitAddresType.p2wpkh;
}
}
+
+ @override
+ bool hasTaprootInput(PendingTransaction pendingTransaction) {
+ return (pendingTransaction as PendingBitcoinTransaction).hasTaprootInputs;
+ }
+
+ @override
+ Future replaceByFee(
+ Object wallet, String transactionHash, String fee) async {
+ final bitcoinWallet = wallet as ElectrumWallet;
+ return await bitcoinWallet.replaceByFee(transactionHash, int.parse(fee));
+ }
+
+ @override
+ Future canReplaceByFee(Object wallet, String transactionHash) async {
+ final bitcoinWallet = wallet as ElectrumWallet;
+ return bitcoinWallet.canReplaceByFee(transactionHash);
+ }
+
+ @override
+ Future isChangeSufficientForFee(Object wallet, String txId, String newFee) async {
+ final bitcoinWallet = wallet as ElectrumWallet;
+ return bitcoinWallet.isChangeSufficientForFee(txId, int.parse(newFee));
+ }
+
+ @override
+ int getFeeAmountForPriority(
+ Object wallet, TransactionPriority priority, int inputsCount, int outputsCount,
+ {int? size}) {
+ final bitcoinWallet = wallet as ElectrumWallet;
+ return bitcoinWallet.feeAmountForPriority(
+ priority as BitcoinTransactionPriority, inputsCount, outputsCount);
+ }
+
+ @override
+ int getFeeAmountWithFeeRate(Object wallet, int feeRate, int inputsCount, int outputsCount,
+ {int? size}) {
+ final bitcoinWallet = wallet as ElectrumWallet;
+ return bitcoinWallet.feeAmountWithFeeRate(
+ feeRate,
+ inputsCount,
+ outputsCount,
+ );
+ }
}
diff --git a/lib/buy/moonpay/moonpay_provider.dart b/lib/buy/moonpay/moonpay_provider.dart
index 02bdedaec..fea8fdabd 100644
--- a/lib/buy/moonpay/moonpay_provider.dart
+++ b/lib/buy/moonpay/moonpay_provider.dart
@@ -22,20 +22,24 @@ 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 =>
@@ -62,8 +66,14 @@ class MoonPaySellProvider extends BuyProvider {
static String get _apiKey => secrets.moonPayApiKey;
+ final String baseBuyUrl;
+ final String baseSellUrl;
+
+ String get currencyCode => walletTypeToCryptoCurrency(wallet.type).title.toLowerCase();
+
+ String get trackUrl => baseBuyUrl + '/transaction_receipt?transactionId=';
+
static String get _exchangeHelperApiKey => secrets.exchangeHelperApiKey;
- final String baseUrl;
Future getMoonpaySignature(String query) async {
final uri = Uri.https(_cIdBaseUrl, "/api/moonpay");
@@ -85,147 +95,92 @@ class MoonPaySellProvider extends BuyProvider {
}
}
- Future requestMoonPayUrl({
+ Future 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,
- '',
- {
- 'apiKey': _apiKey,
- 'defaultBaseCurrencyCode': _normalizeCurrency(currency),
- 'refundWalletAddress': refundWalletAddress,
- }..addAll(customParams),
- );
+ if (_apiKey.isNotEmpty) {
+ params['apiKey'] = _apiKey;
+ }
- final signature = await getMoonpaySignature('?${originalUri.query}');
+ final originalUri = Uri.https(
+ baseSellUrl,
+ '',
+ params,
+ );
if (isTestEnvironment) {
return originalUri;
}
+ final signature = await getMoonpaySignature('?${originalUri.query}');
+
final query = Map.from(originalUri.queryParameters);
query['signature'] = signature;
final signedUri = originalUri.replace(queryParameters: query);
return signedUri;
}
- @override
- Future 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(
- 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();
- }
-}
-
-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 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': 'false',
+ '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 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.from(originalUri.queryParameters);
+ query['signature'] = signature;
+ final signedUri = originalUri.replace(queryParameters: query);
+ return signedUri;
}
Future calculateAmount(String amount, String sourceCurrency) async {
@@ -300,6 +255,52 @@ class MoonPayBuyProvider extends BuyProvider {
}
@override
- Future launchProvider(BuildContext context, bool? isBuyAction) =>
- throw UnimplementedError();
+ Future 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(
+ 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();
+ }
}
diff --git a/lib/core/amount_validator.dart b/lib/core/amount_validator.dart
index fb5214d54..38983dfb2 100644
--- a/lib/core/amount_validator.dart
+++ b/lib/core/amount_validator.dart
@@ -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);
diff --git a/lib/core/execution_state.dart b/lib/core/execution_state.dart
index 18dc81030..6bc906010 100644
--- a/lib/core/execution_state.dart
+++ b/lib/core/execution_state.dart
@@ -14,4 +14,13 @@ class FailureState extends ExecutionState {
FailureState(this.error);
final String error;
+}
+
+class AwaitingConfirmationState extends ExecutionState {
+ AwaitingConfirmationState({this.title, this.message, this.onConfirm, this.onCancel});
+
+ final String? title;
+ final String? message;
+ final Function()? onConfirm;
+ final Function()? onCancel;
}
\ No newline at end of file
diff --git a/lib/core/wallet_connect/chain_service/solana/solana_chain_id.dart b/lib/core/wallet_connect/chain_service/solana/solana_chain_id.dart
index bdc8a7d20..ed80a4f3f 100644
--- a/lib/core/wallet_connect/chain_service/solana/solana_chain_id.dart
+++ b/lib/core/wallet_connect/chain_service/solana/solana_chain_id.dart
@@ -2,8 +2,8 @@ import 'solana_chain_service.dart';
enum SolanaChainId {
mainnet,
- testnet,
- devnet,
+ // testnet,
+ // devnet,
}
extension SolanaChainIdX on SolanaChainId {
@@ -13,13 +13,16 @@ extension SolanaChainIdX on SolanaChainId {
switch (this) {
case SolanaChainId.mainnet:
name = '4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZ';
+ // solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp
break;
- case SolanaChainId.testnet:
- name = '8E9rvCKLFQia2Y35HXjjpWzj8weVo44K';
- break;
- case SolanaChainId.devnet:
- name = '';
- break;
+ // case SolanaChainId.devnet:
+ // name = '8E9rvCKLFQia2Y35HXjjpWzj8weVo44K';
+ // // solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1
+ // break;
+ // case SolanaChainId.testnet:
+ // name = '';
+ // // solana:4uhcVJyU9pJkvQyS88uRDiswHXSCkY3z
+ // break;
}
return '${SolanaChainServiceImpl.namespace}:$name';
diff --git a/lib/core/wallet_connect/chain_service/solana/solana_chain_service.dart b/lib/core/wallet_connect/chain_service/solana/solana_chain_service.dart
index f5c696be6..efbf9df74 100644
--- a/lib/core/wallet_connect/chain_service/solana/solana_chain_service.dart
+++ b/lib/core/wallet_connect/chain_service/solana/solana_chain_service.dart
@@ -43,7 +43,7 @@ class SolanaChainServiceImpl implements ChainService {
SolanaClient(
rpcUrl: rpcUrl,
websocketUrl: Uri.parse(webSocketUrl),
- timeout: const Duration(minutes: 2),
+ timeout: const Duration(minutes: 5),
) {
for (final String event in getEvents()) {
wallet.registerEventEmitter(chainId: getChainId(), event: event);
@@ -72,7 +72,7 @@ class SolanaChainServiceImpl implements ChainService {
@override
List getEvents() {
- return [''];
+ return ['chainChanged', 'accountsChanged'];
}
Future requestAuthorization(String? text) async {
@@ -100,8 +100,7 @@ class SolanaChainServiceImpl implements ChainService {
Future solanaSignTransaction(String topic, dynamic parameters) async {
log('received solana sign transaction request $parameters');
- final solanaSignTx =
- SolanaSignTransaction.fromJson(parameters as Map);
+ final solanaSignTx = SolanaSignTransaction.fromJson(parameters as Map);
final String? authError = await requestAuthorization('Confirm request to sign transaction?');
@@ -122,10 +121,13 @@ class SolanaChainServiceImpl implements ChainService {
return '';
}
- String signature = sign.signatures.first.toBase58();
+ String signature = await solanaClient.sendAndConfirmTransaction(
+ message: message,
+ signers: [ownerKeyPair!],
+ commitment: Commitment.confirmed,
+ );
print(signature);
- print(signature.runtimeType);
bottomSheetService.queueBottomSheet(
isModalDismissible: true,
diff --git a/lib/core/wallet_connect/web3wallet_service.dart b/lib/core/wallet_connect/web3wallet_service.dart
index 4c71abe48..66ccb2d76 100644
--- a/lib/core/wallet_connect/web3wallet_service.dart
+++ b/lib/core/wallet_connect/web3wallet_service.dart
@@ -133,13 +133,27 @@ abstract class Web3WalletServiceBase with Store {
if (appStore.wallet!.type == WalletType.solana) {
for (final cId in SolanaChainId.values) {
final node = appStore.settingsStore.getCurrentNode(appStore.wallet!.type);
- final rpcUri = node.uri;
- final webSocketUri = 'wss://${node.uriRaw}/ws${node.uri.path}';
+
+ Uri? rpcUri;
+ String webSocketUrl;
+ bool isModifiedNodeUri = false;
+
+ if (node.uriRaw == 'rpc.ankr.com') {
+ isModifiedNodeUri = true;
+
+ //A better way to handle this instead of adding this to the general secrets?
+ String ankrApiKey = secrets.ankrApiKey;
+
+ rpcUri = Uri.https(node.uriRaw, '/solana/$ankrApiKey');
+ webSocketUrl = 'wss://${node.uriRaw}/solana/ws/$ankrApiKey';
+ } else {
+ webSocketUrl = 'wss://${node.uriRaw}';
+ }
SolanaChainServiceImpl(
reference: cId,
- rpcUrl: rpcUri,
- webSocketUrl: webSocketUri,
+ rpcUrl: isModifiedNodeUri ? rpcUri! : node.uri,
+ webSocketUrl: webSocketUrl,
wcKeyService: walletKeyService,
bottomSheetService: _bottomSheetHandler,
wallet: _web3Wallet,
diff --git a/lib/di.dart b/lib/di.dart
index 782c0f1f4..5262a01e6 100644
--- a/lib/di.dart
+++ b/lib/di.dart
@@ -13,6 +13,7 @@ import 'package:cake_wallet/core/yat_service.dart';
import 'package:cake_wallet/entities/background_tasks.dart';
import 'package:cake_wallet/entities/exchange_api_mode.dart';
import 'package:cake_wallet/entities/parse_address_from_domain.dart';
+import 'package:cake_wallet/src/screens/transaction_details/rbf_details_page.dart';
import 'package:cw_core/receive_page_option.dart';
import 'package:cake_wallet/ethereum/ethereum.dart';
import 'package:cake_wallet/nano/nano.dart';
@@ -198,6 +199,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';
@@ -806,8 +808,11 @@ Future setup({
getIt
.registerFactory(() => DFXBuyProvider(wallet: getIt.get().wallet!));
- getIt.registerFactory(() => MoonPaySellProvider(
- settingsStore: getIt.get().settingsStore, wallet: getIt.get().wallet!));
+ getIt.registerFactory(() => MoonPayProvider(
+ settingsStore: getIt.get().settingsStore,
+ wallet: getIt.get().wallet!,
+ isTestEnvironment: kDebugMode,
+ ));
getIt.registerFactory(() => OnRamperBuyProvider(
getIt.get().settingsStore,
@@ -910,7 +915,8 @@ Future setup({
transactionInfo: transactionInfo,
transactionDescriptionBox: _transactionDescriptionBox,
wallet: wallet,
- settingsStore: getIt.get());
+ settingsStore: getIt.get(),
+ sendViewModel: getIt.get());
});
getIt.registerFactoryParam(
@@ -1133,6 +1139,11 @@ Future setup({
getIt.registerFactory(() => IoniaAccountCardsPage(getIt.get()));
+ getIt.registerFactoryParam(
+ (TransactionInfo transactionInfo, _) => RBFDetailsPage(
+ transactionDetailsViewModel:
+ getIt.get(param1: transactionInfo)));
+
getIt.registerFactory(() => AnonPayApi(
useTorOnly: getIt.get().exchangeStatus == ExchangeApiMode.torOnly,
wallet: getIt.get().wallet!));
diff --git a/lib/entities/background_tasks.dart b/lib/entities/background_tasks.dart
index ce1e2f6d8..5db42381e 100644
--- a/lib/entities/background_tasks.dart
+++ b/lib/entities/background_tasks.dart
@@ -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;
}
diff --git a/lib/entities/biometric_auth.dart b/lib/entities/biometric_auth.dart
index a0afc070a..febbfa469 100644
--- a/lib/entities/biometric_auth.dart
+++ b/lib/entities/biometric_auth.dart
@@ -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) {
diff --git a/lib/entities/default_settings_migration.dart b/lib/entities/default_settings_migration.dart
index 8ce38e4c4..9a2db56af 100644
--- a/lib/entities/default_settings_migration.dart
+++ b/lib/entities/default_settings_migration.dart
@@ -1,6 +1,7 @@
import 'dart:io' show Directory, File, Platform;
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/entities/exchange_api_mode.dart';
+import 'package:cake_wallet/entities/fiat_api_mode.dart';
import 'package:cw_core/pathForWallet.dart';
import 'package:cake_wallet/entities/secret_store_key.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
@@ -211,6 +212,10 @@ Future defaultSettingsMigration(
await changeDefaultBitcoinNode(nodes, sharedPreferences);
break;
+ case 30:
+ await disableServiceStatusFiatDisabled(sharedPreferences);
+ break;
+
default:
break;
}
@@ -225,6 +230,18 @@ Future defaultSettingsMigration(
await sharedPreferences.setInt(PreferencesKey.currentDefaultSettingsMigrationVersion, version);
}
+Future disableServiceStatusFiatDisabled(SharedPreferences sharedPreferences) async {
+ final currentFiat =
+ await sharedPreferences.getInt(PreferencesKey.currentFiatApiModeKey) ?? -1;
+ if (currentFiat == -1 || currentFiat == FiatApiMode.enabled.raw) {
+ return;
+ }
+
+ if (currentFiat == FiatApiMode.disabled.raw || currentFiat == FiatApiMode.torOnly.raw) {
+ await sharedPreferences.setBool(PreferencesKey.disableBulletinKey, true);
+ }
+}
+
Future _updateMoneroPriority(SharedPreferences sharedPreferences) async {
final currentPriority =
await sharedPreferences.getInt(PreferencesKey.moneroTransactionPriority) ??
diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart
index 5c22455d2..ba6d6ef4f 100644
--- a/lib/entities/preferences_key.dart
+++ b/lib/entities/preferences_key.dart
@@ -42,6 +42,7 @@ class PreferencesKey {
static const ethereumTransactionPriority = 'current_fee_priority_ethereum';
static const polygonTransactionPriority = 'current_fee_priority_polygon';
static const bitcoinCashTransactionPriority = 'current_fee_priority_bitcoin_cash';
+ static const customBitcoinFeeRate = 'custom_electrum_fee_rate';
static const shouldShowReceiveWarning = 'should_show_receive_warning';
static const shouldShowYatPopup = 'should_show_yat_popup';
static const moneroWalletPasswordUpdateV1Base = 'monero_wallet_update_v1';
diff --git a/lib/entities/provider_types.dart b/lib/entities/provider_types.dart
index f3993e129..701781cc2 100644
--- a/lib/entities/provider_types.dart
+++ b/lib/entities/provider_types.dart
@@ -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';
}
}
@@ -62,10 +62,11 @@ class ProvidersHelper {
ProviderType.onramper,
ProviderType.dfx,
ProviderType.robinhood,
+ ProviderType.moonpay,
];
case WalletType.litecoin:
case WalletType.bitcoinCash:
- return [ProviderType.askEachTime, ProviderType.onramper, ProviderType.robinhood];
+ return [ProviderType.askEachTime, ProviderType.onramper, ProviderType.robinhood, ProviderType.moonpay];
case WalletType.solana:
return [ProviderType.askEachTime, ProviderType.onramper, ProviderType.robinhood];
case WalletType.none:
@@ -82,18 +83,18 @@ class ProvidersHelper {
return [
ProviderType.askEachTime,
ProviderType.onramper,
- ProviderType.moonpaySell,
+ ProviderType.moonpay,
ProviderType.dfx,
];
case WalletType.litecoin:
case WalletType.bitcoinCash:
- return [ProviderType.askEachTime, ProviderType.moonpaySell];
+ 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:
@@ -112,10 +113,10 @@ class ProvidersHelper {
return getIt.get();
case ProviderType.onramper:
return getIt.get();
+ case ProviderType.moonpay:
+ return getIt.get();
case ProviderType.askEachTime:
return null;
- case ProviderType.moonpaySell:
- return getIt.get();
}
}
}
diff --git a/lib/ethereum/cw_ethereum.dart b/lib/ethereum/cw_ethereum.dart
index 6e658788e..13fe3aafd 100644
--- a/lib/ethereum/cw_ethereum.dart
+++ b/lib/ethereum/cw_ethereum.dart
@@ -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,
@@ -130,7 +131,7 @@ class CWEthereum extends Ethereum {
@override
Future getErc20Token(WalletBase wallet, String contractAddress) async {
final ethereumWallet = wallet as EthereumWallet;
- return await ethereumWallet.getErc20Token(contractAddress);
+ return await ethereumWallet.getErc20Token(contractAddress, 'eth');
}
@override
diff --git a/lib/exchange/exchange_provider_description.dart b/lib/exchange/exchange_provider_description.dart
index abfac3a6b..4d9691035 100644
--- a/lib/exchange/exchange_provider_description.dart
+++ b/lib/exchange/exchange_provider_description.dart
@@ -22,6 +22,8 @@ class ExchangeProviderDescription extends EnumerableItem 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 with Serializable<
return trocador;
case 6:
return exolix;
+ case 8:
+ return thorChain;
case 7:
return all;
default:
diff --git a/lib/exchange/provider/changenow_exchange_provider.dart b/lib/exchange/provider/changenow_exchange_provider.dart
index c4a96bc5b..42f8634fb 100644
--- a/lib/exchange/provider/changenow_exchange_provider.dart
+++ b/lib/exchange/provider/changenow_exchange_provider.dart
@@ -133,7 +133,11 @@ class ChangeNowExchangeProvider extends ExchangeProvider {
}
@override
- Future createTrade({required TradeRequest request, required bool isFixedRateMode}) async {
+ Future 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
diff --git a/lib/exchange/provider/exchange_provider.dart b/lib/exchange/provider/exchange_provider.dart
index d1f69689d..a91a7ac9d 100644
--- a/lib/exchange/provider/exchange_provider.dart
+++ b/lib/exchange/provider/exchange_provider.dart
@@ -28,7 +28,8 @@ abstract class ExchangeProvider {
Future fetchLimits(
{required CryptoCurrency from, required CryptoCurrency to, required bool isFixedRateMode});
- Future createTrade({required TradeRequest request, required bool isFixedRateMode});
+ Future createTrade(
+ {required TradeRequest request, required bool isFixedRateMode, required bool isSendAll});
Future findTradeById({required String id});
diff --git a/lib/exchange/provider/exolix_exchange_provider.dart b/lib/exchange/provider/exolix_exchange_provider.dart
index 9374439f3..db11a8f58 100644
--- a/lib/exchange/provider/exolix_exchange_provider.dart
+++ b/lib/exchange/provider/exolix_exchange_provider.dart
@@ -130,7 +130,11 @@ class ExolixExchangeProvider extends ExchangeProvider {
}
@override
- Future createTrade({required TradeRequest request, required bool isFixedRateMode}) async {
+ Future createTrade({
+ required TradeRequest request,
+ required bool isFixedRateMode,
+ required bool isSendAll,
+ }) async {
final headers = {'Content-Type': 'application/json'};
final body = {
'coinFrom': _normalizeCurrency(request.fromCurrency),
@@ -180,7 +184,8 @@ class ExolixExchangeProvider extends ExchangeProvider {
createdAt: DateTime.now(),
amount: amount,
state: TradeState.created,
- payoutAddress: payoutAddress);
+ payoutAddress: payoutAddress,
+ isSendAll: isSendAll);
}
@override
diff --git a/lib/exchange/provider/sideshift_exchange_provider.dart b/lib/exchange/provider/sideshift_exchange_provider.dart
index 261aeedf3..1be4f8045 100644
--- a/lib/exchange/provider/sideshift_exchange_provider.dart
+++ b/lib/exchange/provider/sideshift_exchange_provider.dart
@@ -144,7 +144,11 @@ class SideShiftExchangeProvider extends ExchangeProvider {
}
@override
- Future createTrade({required TradeRequest request, required bool isFixedRateMode}) async {
+ Future 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,
);
}
diff --git a/lib/exchange/provider/simpleswap_exchange_provider.dart b/lib/exchange/provider/simpleswap_exchange_provider.dart
index 5c162a995..df83cf491 100644
--- a/lib/exchange/provider/simpleswap_exchange_provider.dart
+++ b/lib/exchange/provider/simpleswap_exchange_provider.dart
@@ -117,7 +117,11 @@ class SimpleSwapExchangeProvider extends ExchangeProvider {
}
@override
- Future createTrade({required TradeRequest request, required bool isFixedRateMode}) async {
+ Future createTrade({
+ required TradeRequest request,
+ required bool isFixedRateMode,
+ required bool isSendAll,
+ }) async {
final headers = {'Content-Type': 'application/json'};
final params = {'api_key': apiKey};
final body = {
@@ -162,6 +166,7 @@ class SimpleSwapExchangeProvider extends ExchangeProvider {
amount: request.fromAmount,
payoutAddress: payoutAddress,
createdAt: DateTime.now(),
+ isSendAll: isSendAll,
);
}
diff --git a/lib/exchange/provider/thorchain_exchange.provider.dart b/lib/exchange/provider/thorchain_exchange.provider.dart
new file mode 100644
index 000000000..32dce7db8
--- /dev/null
+++ b/lib/exchange/provider/thorchain_exchange.provider.dart
@@ -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 _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 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 checkIsAvailable() async => true;
+
+ @override
+ Future 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 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 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 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 stagesJson = responseJSON['stages'] as Map;
+
+ 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 coins = tx['coins'] as List;
+ 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?;
+ 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> _getSwapQuote(Map 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 _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 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;
+ }
+}
diff --git a/lib/exchange/provider/trocador_exchange_provider.dart b/lib/exchange/provider/trocador_exchange_provider.dart
index faa4cc060..326573016 100644
--- a/lib/exchange/provider/trocador_exchange_provider.dart
+++ b/lib/exchange/provider/trocador_exchange_provider.dart
@@ -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 createTrade({required TradeRequest request, required bool isFixedRateMode}) async {
-
+ Future 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
diff --git a/lib/exchange/trade.dart b/lib/exchange/trade.dart
index 4eb48c248..6cc3fddbe 100644
--- a/lib/exchange/trade.dart
+++ b/lib/exchange/trade.dart
@@ -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 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 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,
};
}
diff --git a/lib/exchange/trade_state.dart b/lib/exchange/trade_state.dart
index ed56d9845..2c58a96f4 100644
--- a/lib/exchange/trade_state.dart
+++ b/lib/exchange/trade_state.dart
@@ -41,6 +41,8 @@ class TradeState extends EnumerableItem with Serializable {
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 with Serializable {
case 'sending':
return sending;
case 'success':
+ case 'done':
return success;
default:
throw Exception('Unexpected token: $raw in TradeState deserialize');
diff --git a/lib/main.dart b/lib/main.dart
index db505f15a..6868348f6 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -149,25 +149,26 @@ Future initializeAppConfigs() async {
final unspentCoinsInfoSource = await CakeHive.openBox(UnspentCoinsInfo.boxName);
await initialSetup(
- sharedPreferences: await SharedPreferences.getInstance(),
- nodes: nodes,
- powNodes: powNodes,
- walletInfoSource: walletInfoSource,
- contactSource: contacts,
- tradesSource: trades,
- ordersSource: orders,
- unspentCoinsInfoSource: unspentCoinsInfoSource,
- // fiatConvertationService: fiatConvertationService,
- templates: templates,
- exchangeTemplates: exchangeTemplates,
- transactionDescriptions: transactionDescriptions,
- secureStorage: secureStorage,
- anonpayInvoiceInfo: anonpayInvoiceInfo,
- initialMigrationVersion: 29);
+ sharedPreferences: await SharedPreferences.getInstance(),
+ nodes: nodes,
+ powNodes: powNodes,
+ walletInfoSource: walletInfoSource,
+ contactSource: contacts,
+ tradesSource: trades,
+ ordersSource: orders,
+ unspentCoinsInfoSource: unspentCoinsInfoSource,
+ // fiatConvertationService: fiatConvertationService,
+ templates: templates,
+ exchangeTemplates: exchangeTemplates,
+ transactionDescriptions: transactionDescriptions,
+ secureStorage: secureStorage,
+ anonpayInvoiceInfo: anonpayInvoiceInfo,
+ initialMigrationVersion: 30,
+ );
}
Future initialSetup(
- {required SharedPreferences sharedPreferences,
+ {required SharedPreferences sharedPreferences,
required Box nodes,
required Box powNodes,
required Box walletInfoSource,
diff --git a/lib/polygon/cw_polygon.dart b/lib/polygon/cw_polygon.dart
index 0ee7457eb..9f0f9a1bf 100644
--- a/lib/polygon/cw_polygon.dart
+++ b/lib/polygon/cw_polygon.dart
@@ -129,7 +129,7 @@ class CWPolygon extends Polygon {
@override
Future getErc20Token(WalletBase wallet, String contractAddress) async {
final polygonWallet = wallet as PolygonWallet;
- return await polygonWallet.getErc20Token(contractAddress);
+ return await polygonWallet.getErc20Token(contractAddress, 'polygon');
}
@override
diff --git a/lib/reactions/wallet_connect.dart b/lib/reactions/wallet_connect.dart
index f4487123e..ca908bc65 100644
--- a/lib/reactions/wallet_connect.dart
+++ b/lib/reactions/wallet_connect.dart
@@ -16,6 +16,7 @@ bool isWalletConnectCompatibleChain(WalletType walletType) {
switch (walletType) {
case WalletType.polygon:
case WalletType.ethereum:
+ case WalletType.solana:
return true;
default:
return false;
diff --git a/lib/router.dart b/lib/router.dart
index ef7b7f31e..9f5dfb838 100644
--- a/lib/router.dart
+++ b/lib/router.dart
@@ -54,6 +54,7 @@ import 'package:cake_wallet/src/screens/setup_2fa/setup_2fa_enter_code_page.dart
import 'package:cake_wallet/src/screens/support/support_page.dart';
import 'package:cake_wallet/src/screens/support_chat/support_chat_page.dart';
import 'package:cake_wallet/src/screens/support_other_links/support_other_links_page.dart';
+import 'package:cake_wallet/src/screens/transaction_details/rbf_details_page.dart';
import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_details_page.dart';
import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_list_page.dart';
import 'package:cake_wallet/src/screens/wallet_connect/wc_connections_listing_view.dart';
@@ -253,6 +254,12 @@ Route createRoute(RouteSettings settings) {
builder: (_) =>
getIt.get(param1: settings.arguments as TransactionInfo));
+ case Routes.bumpFeePage:
+ return CupertinoPageRoute(
+ fullscreenDialog: true,
+ builder: (_) =>
+ getIt.get(param1: settings.arguments as TransactionInfo));
+
case Routes.newSubaddress:
return CupertinoPageRoute(
builder: (_) => getIt.get(param1: settings.arguments));
diff --git a/lib/routes.dart b/lib/routes.dart
index 7ad5c70bc..9c4e21651 100644
--- a/lib/routes.dart
+++ b/lib/routes.dart
@@ -12,6 +12,7 @@ class Routes {
static const dashboard = '/dashboard';
static const send = '/send';
static const transactionDetails = '/transaction_info';
+ static const bumpFeePage = '/bump_fee_page';
static const receive = '/receive';
static const newSubaddress = '/new_subaddress';
static const walletEdit = '/walletEdit';
diff --git a/lib/solana/cw_solana.dart b/lib/solana/cw_solana.dart
index 9f9d81e5f..6f4b17309 100644
--- a/lib/solana/cw_solana.dart
+++ b/lib/solana/cw_solana.dart
@@ -74,8 +74,23 @@ class CWSolana extends Solana {
}
@override
- Future addSPLToken(WalletBase wallet, CryptoCurrency token) async =>
- await (wallet as SolanaWallet).addSPLToken(token as SPLToken);
+ Future 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,
+ iconPath: token.iconPath,
+ );
+
+ await (wallet as SolanaWallet).addSPLToken(splToken);
+ }
@override
Future deleteSPLToken(WalletBase wallet, CryptoCurrency token) async =>
@@ -115,4 +130,9 @@ class CWSolana extends Solana {
return null;
}
+
+ @override
+ double? getEstimateFees(WalletBase wallet) {
+ return (wallet as SolanaWallet).estimatedFee;
+ }
}
diff --git a/lib/src/screens/buy/buy_options_page.dart b/lib/src/screens/buy/buy_options_page.dart
index 50f041d2e..38f3ed968 100644
--- a/lib/src/screens/buy/buy_options_page.dart
+++ b/lib/src/screens/buy/buy_options_page.dart
@@ -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()!.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()!.detailsTitlesColor,
),
),
),
diff --git a/lib/src/screens/buy/buy_webview_page.dart b/lib/src/screens/buy/buy_webview_page.dart
index 829bff3d9..ad6970861 100644
--- a/lib/src/screens/buy/buy_webview_page.dart
+++ b/lib/src/screens/buy/buy_webview_page.dart
@@ -60,7 +60,7 @@ class BuyWebViewPageBodyState extends State {
_saveOrder(keyword: 'completed', splitSymbol: '/');
}
- if (widget.buyViewModel.selectedProvider is MoonPayBuyProvider) {
+ if (widget.buyViewModel.selectedProvider is MoonPayProvider) {
_saveOrder(keyword: 'transactionId', splitSymbol: '=');
}
}
diff --git a/lib/src/screens/dashboard/edit_token_page.dart b/lib/src/screens/dashboard/edit_token_page.dart
index 720a8cc14..59f7de9e5 100644
--- a/lib/src/screens/dashboard/edit_token_page.dart
+++ b/lib/src/screens/dashboard/edit_token_page.dart
@@ -59,6 +59,7 @@ class _EditTokenPageBodyState extends State {
final TextEditingController _tokenNameController = TextEditingController();
final TextEditingController _tokenSymbolController = TextEditingController();
final TextEditingController _tokenDecimalController = TextEditingController();
+ final TextEditingController _tokenIconPathController = TextEditingController();
final FocusNode _contractAddressFocusNode = FocusNode();
final FocusNode _tokenNameFocusNode = FocusNode();
@@ -83,6 +84,7 @@ class _EditTokenPageBodyState extends State {
_tokenNameController.text = widget.token!.name;
_tokenSymbolController.text = widget.token!.title;
_tokenDecimalController.text = widget.token!.decimals.toString();
+ _tokenIconPathController.text = widget.token?.iconPath ?? '';
}
if (widget.initialContractAddress != null) {
@@ -195,12 +197,15 @@ class _EditTokenPageBodyState extends State {
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),
+ iconPath: _tokenIconPathController.text,
+ ),
contractAddress: _contractAddressController.text,
- decimal: int.parse(_tokenDecimalController.text),
- ));
+ );
if (context.mounted) {
Navigator.pop(context);
}
@@ -226,6 +231,8 @@ class _EditTokenPageBodyState extends State {
if (token != null) {
if (_tokenNameController.text.isEmpty) _tokenNameController.text = token.name;
if (_tokenSymbolController.text.isEmpty) _tokenSymbolController.text = token.title;
+ if (_tokenIconPathController.text.isEmpty)
+ _tokenIconPathController.text = token.iconPath ?? '';
if (_tokenDecimalController.text.isEmpty)
_tokenDecimalController.text = token.decimals.toString();
}
@@ -303,10 +310,15 @@ class _EditTokenPageBodyState extends State {
if (text?.isEmpty ?? true) {
return S.of(context).field_required;
}
+
if (int.tryParse(text!) == null) {
return S.of(context).invalid_input;
}
+ if (int.tryParse(text) == 0) {
+ return S.current.decimals_cannot_be_zero;
+ }
+
return null;
},
),
diff --git a/lib/src/screens/dashboard/home_settings_page.dart b/lib/src/screens/dashboard/home_settings_page.dart
index e841423c1..aa6bb12c0 100644
--- a/lib/src/screens/dashboard/home_settings_page.dart
+++ b/lib/src/screens/dashboard/home_settings_page.dart
@@ -129,25 +129,29 @@ class HomeSettingsPage extends BasePage {
'token': token,
});
},
- leading: CakeImageWidget(
- imageUrl: token.iconPath,
- height: 40,
- width: 40,
- displayOnError: Container(
- height: 30.0,
- width: 30.0,
- child: Center(
- child: Text(
- token.title.substring(0, min(token.title.length, 2)),
- style: TextStyle(fontSize: 11),
- ),
- ),
- decoration: BoxDecoration(
- shape: BoxShape.circle,
- color: Colors.grey.shade400,
+ leading: Container(
+ clipBehavior: Clip.hardEdge,
+ decoration: BoxDecoration(shape: BoxShape.circle),
+ child: CakeImageWidget(
+ imageUrl: token.iconPath,
+ height: 40,
+ width: 40,
+ displayOnError: Container(
+ height: 30.0,
+ width: 30.0,
+ child: Center(
+ child: Text(
+ token.title.substring(0, min(token.title.length, 2)),
+ style: TextStyle(fontSize: 11),
),
+ ),
+ decoration: BoxDecoration(
+ shape: BoxShape.circle,
+ color: Colors.grey.shade400,
+ ),
+ ),
),
- ),
+ ),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(30),
diff --git a/lib/src/screens/dashboard/pages/balance_page.dart b/lib/src/screens/dashboard/pages/balance_page.dart
index bb3ec70dc..0b7596469 100644
--- a/lib/src/screens/dashboard/pages/balance_page.dart
+++ b/lib/src/screens/dashboard/pages/balance_page.dart
@@ -323,7 +323,7 @@ class BalanceRowWidget extends StatelessWidget {
style: TextStyle(
fontSize: 16,
fontFamily: 'Lato',
- fontWeight: FontWeight.w500,
+ fontWeight: FontWeight.w500,
color: Theme.of(context).extension()!.textColor,
height: 1)),
],
@@ -334,24 +334,28 @@ class BalanceRowWidget extends StatelessWidget {
child: Center(
child: Column(
children: [
- CakeImageWidget(
- imageUrl: currency.iconPath,
- height: 40,
- width: 40,
- displayOnError: Container(
- height: 30.0,
- width: 30.0,
- child: Center(
- child: Text(
- currency.title.substring(0, min(currency.title.length, 2)),
- style: TextStyle(fontSize: 11),
- ),
- ),
- decoration: BoxDecoration(
- shape: BoxShape.circle,
- color: Colors.grey.shade400,
+ Container(
+ clipBehavior: Clip.antiAlias,
+ decoration: BoxDecoration(shape: BoxShape.circle),
+ child: CakeImageWidget(
+ imageUrl: currency.iconPath,
+ height: 40,
+ width: 40,
+ displayOnError: Container(
+ height: 30.0,
+ width: 30.0,
+ child: Center(
+ child: Text(
+ currency.title.substring(0, min(currency.title.length, 2)),
+ style: TextStyle(fontSize: 11),
),
),
+ decoration: BoxDecoration(
+ shape: BoxShape.circle,
+ color: Colors.grey.shade400,
+ ),
+ ),
+ ),
),
const SizedBox(height: 10),
Text(
@@ -410,9 +414,7 @@ class BalanceRowWidget extends StatelessWidget {
fontSize: 20,
fontFamily: 'Lato',
fontWeight: FontWeight.w400,
- color: Theme.of(context)
- .extension()!
- .balanceAmountColor,
+ color: Theme.of(context).extension()!.balanceAmountColor,
height: 1,
),
maxLines: 1,
diff --git a/lib/src/screens/dashboard/widgets/filter_tile.dart b/lib/src/screens/dashboard/widgets/filter_tile.dart
index 3be96073a..d2f824806 100644
--- a/lib/src/screens/dashboard/widgets/filter_tile.dart
+++ b/lib/src/screens/dashboard/widgets/filter_tile.dart
@@ -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,
);
}
diff --git a/lib/src/screens/dashboard/widgets/sync_indicator_icon.dart b/lib/src/screens/dashboard/widgets/sync_indicator_icon.dart
index 11bde6dfa..21133a438 100644
--- a/lib/src/screens/dashboard/widgets/sync_indicator_icon.dart
+++ b/lib/src/screens/dashboard/widgets/sync_indicator_icon.dart
@@ -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:
diff --git a/lib/src/screens/dashboard/widgets/trade_row.dart b/lib/src/screens/dashboard/widgets/trade_row.dart
index 7f570b98e..caccb8047 100644
--- a/lib/src/screens/dashboard/widgets/trade_row.dart
+++ b/lib/src/screens/dashboard/widgets/trade_row.dart
@@ -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;
- }
}
diff --git a/lib/src/screens/exchange/exchange_page.dart b/lib/src/screens/exchange/exchange_page.dart
index 1a5ab24e6..d9e119038 100644
--- a/lib/src/screens/exchange/exchange_page.dart
+++ b/lib/src/screens/exchange/exchange_page.dart
@@ -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;
+ }
+ },
),
],
)),
@@ -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);
}
});
@@ -470,6 +480,13 @@ class ExchangePage extends BasePage {
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;
@@ -517,7 +534,7 @@ class ExchangePage extends BasePage {
_receiveAmountFocus.addListener(() {
if (_receiveAmountFocus.hasFocus) {
- exchangeViewModel.isFixedRateMode = true;
+ exchangeViewModel.enableFixedRateMode();
}
// exchangeViewModel.changeReceiveAmount(amount: receiveAmountController.text);
});
diff --git a/lib/src/screens/exchange/widgets/exchange_card.dart b/lib/src/screens/exchange/widgets/exchange_card.dart
index 8fa809de9..760b0c137 100644
--- a/lib/src/screens/exchange/widgets/exchange_card.dart
+++ b/lib/src/screens/exchange/widgets/exchange_card.dart
@@ -485,14 +485,14 @@ class ExchangeCardState extends State {
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());
});
diff --git a/lib/src/screens/exchange_trade/exchange_trade_page.dart b/lib/src/screens/exchange_trade/exchange_trade_page.dart
index c4dcae32c..4d3334f9f 100644
--- a/lib/src/screens/exchange_trade/exchange_trade_page.dart
+++ b/lib/src/screens/exchange_trade/exchange_trade_page.dart
@@ -262,6 +262,7 @@ class ExchangeTradeState extends State {
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 {
diff --git a/lib/src/screens/send/send_page.dart b/lib/src/screens/send/send_page.dart
index a4c095739..970bb31f2 100644
--- a/lib/src/screens/send/send_page.dart
+++ b/lib/src/screens/send/send_page.dart
@@ -100,7 +100,10 @@ class SendPage extends BasePage {
AppBarStyle get appBarStyle => AppBarStyle.transparent;
double _sendCardHeight(BuildContext context) {
- final double initialHeight = sendViewModel.hasCoinControl ? 500 : 465;
+ double initialHeight = 450;
+ if (sendViewModel.hasCoinControl) {
+ initialHeight += 35;
+ }
if (!responsiveLayoutUtil.shouldRenderMobileUI) {
return initialHeight - 66;
@@ -190,7 +193,7 @@ class SendPage extends BasePage {
},
)),
Padding(
- padding: EdgeInsets.only(top: 10, left: 24, right: 24, bottom: 10),
+ padding: EdgeInsets.only(left: 24, right: 24, bottom: 10),
child: Container(
height: 10,
child: Observer(
@@ -426,6 +429,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,
@@ -455,7 +459,7 @@ class SendPage extends BasePage {
? '. ${S.of(_dialogContext).waitFewSecondForTxUpdate}' : '';
final newContactMessage = newContactAddress != null
- ? '\n${S.of(context).add_contact_to_address_book}' : '';
+ ? '\n${S.of(_dialogContext).add_contact_to_address_book}' : '';
final alertContent =
"$successMessage$waitMessage$newContactMessage";
diff --git a/lib/src/screens/send/widgets/confirm_sending_alert.dart b/lib/src/screens/send/widgets/confirm_sending_alert.dart
index 87d00ce0f..ce711ce8b 100644
--- a/lib/src/screens/send/widgets/confirm_sending_alert.dart
+++ b/lib/src/screens/send/widgets/confirm_sending_alert.dart
@@ -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 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 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
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
final String amountValue;
final String fiatAmountValue;
final String fee;
+ final String? feeRate;
final String feeValue;
final String feeFiatAmount;
final List outputs;
@@ -183,7 +191,7 @@ class ConfirmSendingAlertContentState extends State
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
)
],
)),
+ if (feeRate != null && feeRate!.isNotEmpty)
+ Padding(
+ padding: EdgeInsets.only(top: 16),
+ child: Row(
+ mainAxisSize: MainAxisSize.max,
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ S.current.send_estimated_fee,
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.normal,
+ fontFamily: 'Lato',
+ color: Theme.of(context).extension()!.titleColor,
+ decoration: TextDecoration.none,
+ ),
+ ),
+ Text(
+ "$feeRate sat/byte",
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.w600,
+ fontFamily: 'Lato',
+ color: Theme.of(context).extension()!.titleColor,
+ decoration: TextDecoration.none,
+ ),
+ )
+ ],
+ )),
Padding(
padding: EdgeInsets.only(top: 16),
child: Column(
diff --git a/lib/src/screens/send/widgets/send_card.dart b/lib/src/screens/send/widgets/send_card.dart
index 3f5714be9..7c2bfedd0 100644
--- a/lib/src/screens/send/widgets/send_card.dart
+++ b/lib/src/screens/send/widgets/send_card.dart
@@ -1,16 +1,17 @@
-import 'package:cake_wallet/themes/extensions/keyboard_theme.dart';
import 'package:cake_wallet/entities/priority_for_wallet_type.dart';
+import 'package:cake_wallet/src/widgets/picker.dart';
+import 'package:cake_wallet/themes/extensions/keyboard_theme.dart';
import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/utils/payment_request.dart';
import 'package:cake_wallet/utils/responsive_layout_util.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/currency.dart';
-import 'package:cw_core/transaction_priority.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/widgets/keyboard_done_button.dart';
-import 'package:cake_wallet/src/widgets/picker.dart';
import 'package:cake_wallet/view_model/send/output.dart';
+import 'package:cw_core/transaction_priority.dart';
+import 'package:cw_core/wallet_type.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
@@ -323,8 +324,7 @@ class SendCardState extends State with AutomaticKeepAliveClientMixin with AutomaticKeepAliveClientMixin GestureDetector(
- onTap: () => _setTransactionPriority(context),
+ onTap: sendViewModel.hasFeesPriority
+ ? () => pickTransactionPriority(context)
+ : () {},
child: Container(
padding: EdgeInsets.only(top: 24),
child: Row(
@@ -668,22 +670,41 @@ class SendCardState extends State with AutomaticKeepAliveClientMixin _setTransactionPriority(BuildContext context) async {
+ Future pickTransactionPriority(BuildContext context) async {
final items = priorityForWalletType(sendViewModel.walletType);
final selectedItem = items.indexOf(sendViewModel.transactionPriority);
+ final customItemIndex = sendViewModel.getCustomPriorityIndex(items);
+ final isBitcoinWallet = sendViewModel.walletType == WalletType.bitcoin;
+ double? customFeeRate = isBitcoinWallet ? sendViewModel.customBitcoinFeeRate.toDouble() : null;
await showPopUp(
context: context,
- builder: (_) => Picker(
- items: items,
- displayItem: sendViewModel.displayFeeRate,
- selectedAtIndex: selectedItem,
- title: S.of(context).please_select,
- mainAxisAlignment: MainAxisAlignment.center,
- onItemSelected: (TransactionPriority priority) =>
- sendViewModel.setTransactionPriority(priority),
- ),
+ builder: (BuildContext context) {
+ int selectedIdx = selectedItem;
+ return StatefulBuilder(
+ builder: (BuildContext context, StateSetter setState) {
+ return Picker(
+ items: items,
+ displayItem: (TransactionPriority priority) =>
+ sendViewModel.displayFeeRate(priority, customFeeRate?.round()),
+ selectedAtIndex: selectedIdx,
+ customItemIndex: customItemIndex,
+ title: S.of(context).please_select,
+ headerEnabled: !isBitcoinWallet,
+ closeOnItemSelected: !isBitcoinWallet,
+ mainAxisAlignment: MainAxisAlignment.center,
+ sliderValue: customFeeRate,
+ onSliderChanged: (double newValue) => setState(() => customFeeRate = newValue),
+ onItemSelected: (TransactionPriority priority) {
+ sendViewModel.setTransactionPriority(priority);
+ setState(() => selectedIdx = items.indexOf(priority));
+ },
+ );
+ },
+ );
+ },
);
+ if (isBitcoinWallet) sendViewModel.customBitcoinFeeRate = customFeeRate!.round();
}
void _presentPicker(BuildContext context) {
diff --git a/lib/src/screens/settings/connection_sync_page.dart b/lib/src/screens/settings/connection_sync_page.dart
index cc04944b3..8c4da4cc5 100644
--- a/lib/src/screens/settings/connection_sync_page.dart
+++ b/lib/src/screens/settings/connection_sync_page.dart
@@ -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