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 282941365..dc231df42 100644
--- a/.github/workflows/pr_test_build.yml
+++ b/.github/workflows/pr_test_build.yml
@@ -139,11 +139,13 @@ 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
- echo "const robinhoodCIdApiSecret = '${{ secrets.ROBINHOOD_CID_CLIENT_SECRET }}';" >> lib/.secrets.g.dart
+ echo "const exchangeHelperApiKey = '${{ secrets.ROBINHOOD_CID_CLIENT_SECRET }}';" >> lib/.secrets.g.dart
echo "const walletConnectProjectId = '${{ secrets.WALLET_CONNECT_PROJECT_ID }}';" >> lib/.secrets.g.dart
echo "const moralisApiKey = '${{ secrets.MORALIS_API_KEY }}';" >> lib/.secrets.g.dart
echo "const polygonScanApiKey = '${{ secrets.POLYGON_SCAN_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart
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
+
+
+
+
+[
](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/SECURITY.md b/SECURITY.md
new file mode 100644
index 000000000..a1b489b76
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,12 @@
+# Security Policy
+
+## Reporting a Vulnerability
+
+If you need to report a vulnerability, please either:
+
+* Open a security advisory: https://github.com/cake-tech/cake_wallet/security/advisories/new
+* Send an email to `dev@cakewallet.com` with details on the vulnerability
+
+## Supported Versions
+
+As we don't maintain prevoius versions of the app, only the latest release for each platform is supported and any updates will bump the version number.
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/nano_node_list.yml b/assets/nano_node_list.yml
index 63b4baec1..2e4d1ec3c 100644
--- a/assets/nano_node_list.yml
+++ b/assets/nano_node_list.yml
@@ -3,4 +3,26 @@
useSSL: true
is_default: true
-
- uri: node.perish.co:9076
\ No newline at end of file
+ uri: node.nautilus.io
+ path: /api
+ useSSL: true
+-
+ uri: app.natrium.io
+ path: /api
+ useSSL: true
+-
+ uri: rainstorm.city
+ path: /api
+ useSSL: true
+-
+ uri: node.somenano.com
+ path: /proxy
+ useSSL: true
+-
+ uri: nanoslo.0x.no
+ path: /proxy
+ useSSL: true
+-
+ uri: www.bitrequest.app
+ port: 8020
+ useSSL: true
\ No newline at end of file
diff --git a/assets/text/Monerocom_Release_Notes.txt b/assets/text/Monerocom_Release_Notes.txt
index 90fcd2a75..09092a8df 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
+UI enhancements
+Bug fixes
\ No newline at end of file
diff --git a/assets/text/Release_Notes.txt b/assets/text/Release_Notes.txt
index d8d4ed830..69a5145c9 100644
--- a/assets/text/Release_Notes.txt
+++ b/assets/text/Release_Notes.txt
@@ -1,5 +1,7 @@
-Monero enhancements
-Bitcoin support different address types (Taproot, Segwit P2WPKH/P2WSH, Legacy)
-In-App live status page for the app services
-Add Exolix exchange provider
-Bug fixes and enhancements
\ No newline at end of file
+Add Replace-By-Fee to boost pending Bitcoin transactions
+Enable WalletConnect for Solana
+WalletConnect Enhancements
+Enhancements for ERC-20 tokens and Solana tokens
+Enhancements for Nano wallet
+UI enhancements
+Bug fixes
\ No newline at end of file
diff --git a/cw_bitcoin/lib/address_to_output_script.dart b/cw_bitcoin/lib/address_to_output_script.dart
index 6ae50132b..892f7a0d6 100644
--- a/cw_bitcoin/lib/address_to_output_script.dart
+++ b/cw_bitcoin/lib/address_to_output_script.dart
@@ -3,6 +3,9 @@ import 'package:bitcoin_base/bitcoin_base.dart' as bitcoin;
List addressToOutputScript(String address, bitcoin.BasedUtxoNetwork network) {
try {
+ if (network == bitcoin.BitcoinCashNetwork.mainnet) {
+ return bitcoin.BitcoinCashAddress(address).baseAddress.toScriptPubKey().toBytes();
+ }
return bitcoin.addressToOutputScript(address: address, network: network);
} catch (err) {
print(err);
diff --git a/cw_bitcoin/lib/bitcoin_address_record.dart b/cw_bitcoin/lib/bitcoin_address_record.dart
index d8d908230..d1c3b6a61 100644
--- a/cw_bitcoin/lib/bitcoin_address_record.dart
+++ b/cw_bitcoin/lib/bitcoin_address_record.dart
@@ -1,5 +1,4 @@
import 'dart:convert';
-import 'package:bitbox/bitbox.dart' as bitbox;
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:cw_bitcoin/script_hash.dart' as sh;
@@ -20,10 +19,9 @@ class BitcoinAddressRecord {
_balance = balance,
_name = name,
_isUsed = isUsed,
- scriptHash =
- scriptHash ?? (network != null ? sh.scriptHash(address, network: network) : null);
+ scriptHash = scriptHash ?? sh.scriptHash(address, network: network);
- factory BitcoinAddressRecord.fromJSON(String jsonSource, BasedUtxoNetwork? network) {
+ factory BitcoinAddressRecord.fromJSON(String jsonSource, BasedUtxoNetwork network) {
final decoded = json.decode(jsonSource) as Map;
return BitcoinAddressRecord(
@@ -39,9 +37,7 @@ class BitcoinAddressRecord {
.firstWhere((type) => type.toString() == decoded['type'] as String)
: SegwitAddresType.p2wpkh,
scriptHash: decoded['scriptHash'] as String?,
- network: (decoded['network'] as String?) == null
- ? network
- : BasedUtxoNetwork.fromName(decoded['network'] as String),
+ network: network,
);
}
@@ -56,7 +52,7 @@ class BitcoinAddressRecord {
String _name;
bool _isUsed;
String? scriptHash;
- BasedUtxoNetwork? network;
+ BasedUtxoNetwork network;
int get txCount => _txCount;
@@ -76,8 +72,6 @@ class BitcoinAddressRecord {
@override
int get hashCode => address.hashCode;
- String get cashAddr => bitbox.Address.toCashAddress(address);
-
BitcoinAddressType type;
String updateScriptHash(BasedUtxoNetwork network) {
@@ -95,6 +89,5 @@ class BitcoinAddressRecord {
'balance': balance,
'type': type.toString(),
'scriptHash': scriptHash,
- 'network': network?.value,
});
}
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/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart
index 5152668bf..092e9d663 100644
--- a/cw_bitcoin/lib/bitcoin_wallet.dart
+++ b/cw_bitcoin/lib/bitcoin_wallet.dart
@@ -115,8 +115,10 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
required Box unspentCoinsInfo,
required String password,
}) async {
- final snp = await ElectrumWalletSnapshot.load(name, walletInfo.type, password,
- walletInfo.network != null ? BasedUtxoNetwork.fromName(walletInfo.network!) : null);
+ final network = walletInfo.network != null
+ ? BasedUtxoNetwork.fromName(walletInfo.network!)
+ : BitcoinNetwork.mainnet;
+ final snp = await ElectrumWalletSnapshot.load(name, walletInfo.type, password, network);
walletInfo.derivationInfo ??= DerivationInfo(
derivationType: snp.derivationType ?? DerivationType.electrum2,
@@ -149,7 +151,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
initialRegularAddressIndex: snp.regularAddressIndex,
initialChangeAddressIndex: snp.changeAddressIndex,
addressPageType: snp.addressPageType,
- networkParam: snp.network,
+ networkParam: network,
);
}
}
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 c3f40a235..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';
@@ -75,11 +76,7 @@ abstract class ElectrumWalletBase
}
: {}),
this.unspentCoinsInfo = unspentCoinsInfo,
- this.network = networkType == bitcoin.bitcoin
- ? BitcoinNetwork.mainnet
- : networkType == litecoinNetwork
- ? LitecoinNetwork.mainnet
- : BitcoinNetwork.testnet,
+ this.network = _getNetwork(networkType, currency),
this.isTestnet = networkType == bitcoin.testnet,
super(walletInfo) {
this.electrumClient = electrumClient ?? ElectrumClient();
@@ -192,27 +189,27 @@ abstract class ElectrumWalletBase
}
}
- Future _estimateTxFeeAndInputsToUse(
- int credentialsAmount,
- bool sendAll,
- List outputAddresses,
- List outputs,
- BitcoinTransactionCredentials transactionCredentials,
- {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 address = addressTypeFromStr(utx.address, network);
final privkey = generateECPrivate(
hd: utx.bitcoinAddressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd,
index: utx.bitcoinAddressRecord.index,
@@ -228,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;
- }
}
}
@@ -244,114 +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);
-
- final fee = transactionCredentials.feeRate != null
- ? feeAmountWithFeeRate(transactionCredentials.feeRate!, 0, 0, size: estimatedSize)
- : feeAmountForPriority(transactionCredentials.priority!, 0, 0, size: estimatedSize);
+ int fee = feeAmountWithFeeRate(feeRate, 0, 0, size: estimatedSize);
if (fee == 0) {
- throw BitcoinTransactionWrongBalanceException(currency);
+ throw BitcoinTransactionNoFeeException();
}
- var amount = credentialsAmount;
+ // Here, when sending all, the output amount equals to the input value - fee to fully spend every input on the transaction and have no amount left for change
+ int amount = allInputsAmount - fee;
- final lastOutput = outputs.last;
- if (!sendAll) {
- if (changeValue > fee) {
- // Here, lastOutput is change, deduct the fee from it
- outputs[outputs.length - 1] =
- BitcoinOutput(address: lastOutput.address, value: lastOutput.value - BigInt.from(fee));
+ // Attempting to send less than the dust limit
+ if (_isBelowDust(amount)) {
+ throw BitcoinTransactionNoDustException();
+ }
+
+ if (credentialsAmount > 0) {
+ final amountLeftForFee = amount - credentialsAmount;
+ if (amountLeftForFee > 0 && _isBelowDust(amountLeftForFee)) {
+ amount -= amountLeftForFee;
+ fee += amountLeftForFee;
}
+ }
+
+ outputs[outputs.length - 1] =
+ BitcoinOutput(address: outputs.last.address, value: BigInt.from(amount));
+
+ return EstimatedTxResult(
+ utxos: utxos,
+ 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, transactionCredentials,
- 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);
+ 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
@@ -362,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();
});
@@ -391,7 +592,6 @@ abstract class ElectrumWalletBase
? SegwitAddresType.p2wpkh.toString()
: walletInfo.addressPageType.toString(),
'balance': balance[currency]?.toJSON(),
- 'network_type': network == BitcoinNetwork.testnet ? 'testnet' : 'mainnet',
});
int feeRate(TransactionPriority priority) {
@@ -406,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));
@@ -595,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;
@@ -627,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(
@@ -638,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) {
@@ -852,6 +1228,22 @@ abstract class ElectrumWalletBase
final HD = index == null ? hd : hd.derive(index);
return base64Encode(HD.signMessage(message));
}
+
+ static BasedUtxoNetwork _getNetwork(bitcoin.NetworkType networkType, CryptoCurrency? currency) {
+ if (networkType == bitcoin.bitcoin && currency == CryptoCurrency.bch) {
+ return BitcoinCashNetwork.mainnet;
+ }
+
+ if (networkType == litecoinNetwork) {
+ return LitecoinNetwork.mainnet;
+ }
+
+ if (networkType == bitcoin.testnet) {
+ return BitcoinNetwork.testnet;
+ }
+
+ return BitcoinNetwork.mainnet;
+ }
}
class EstimateTxParams {
@@ -870,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) {
+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 5880f5a19..c43d4988a 100644
--- a/cw_bitcoin/lib/electrum_wallet_addresses.dart
+++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart
@@ -1,6 +1,5 @@
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
-import 'package:bitbox/bitbox.dart' as bitbox;
import 'package:cw_bitcoin/bitcoin_address_record.dart';
import 'package:cw_bitcoin/electrum.dart';
import 'package:cw_core/wallet_addresses.dart';
@@ -30,6 +29,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
List? initialAddresses,
Map? initialRegularAddressIndex,
Map? initialChangeAddressIndex,
+ BitcoinAddressType? initialAddressPageType,
}) : _addresses = ObservableList.of((initialAddresses ?? []).toSet()),
addressesByReceiveType =
ObservableList.of(([]).toSet()),
@@ -41,9 +41,10 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
.toSet()),
currentReceiveAddressIndexByType = initialRegularAddressIndex ?? {},
currentChangeAddressIndexByType = initialChangeAddressIndex ?? {},
- _addressPageType = walletInfo.addressPageType != null
- ? BitcoinAddressType.fromValue(walletInfo.addressPageType!)
- : SegwitAddresType.p2wpkh,
+ _addressPageType = initialAddressPageType ??
+ (walletInfo.addressPageType != null
+ ? BitcoinAddressType.fromValue(walletInfo.addressPageType!)
+ : SegwitAddresType.p2wpkh),
super(walletInfo) {
updateAddressesByMatch();
}
@@ -52,10 +53,6 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
static const defaultChangeAddressesCount = 17;
static const gap = 20;
- static String toCashAddr(String address) => bitbox.Address.toCashAddress(address);
-
- static String toLegacy(String address) => bitbox.Address.toLegacyAddress(address);
-
final ObservableList _addresses;
// Matched by addressPageType
late ObservableList addressesByReceiveType;
@@ -67,7 +64,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
final bitcoin.HDWallet sideHd;
@observable
- BitcoinAddressType _addressPageType = SegwitAddresType.p2wpkh;
+ late BitcoinAddressType _addressPageType;
@computed
BitcoinAddressType get addressPageType => _addressPageType;
@@ -80,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) {
@@ -97,7 +95,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
}
}
- return walletInfo.type == WalletType.bitcoinCash ? toCashAddr(receiveAddress) : receiveAddress;
+ return receiveAddress;
}
@observable
@@ -105,9 +103,6 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
@override
set address(String addr) {
- if (addr.startsWith('bitcoincash:')) {
- addr = toLegacy(addr);
- }
final addressRecord = _addresses.firstWhere((addressRecord) => addressRecord.address == addr);
previousAddressRecord = addressRecord;
@@ -155,11 +150,17 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
@override
Future init() async {
- await _generateInitialAddresses();
- await _generateInitialAddresses(type: P2pkhAddressType.p2pkh);
- await _generateInitialAddresses(type: P2shAddressType.p2wpkhInP2sh);
- await _generateInitialAddresses(type: SegwitAddresType.p2tr);
- await _generateInitialAddresses(type: SegwitAddresType.p2wsh);
+ if (walletInfo.type == WalletType.bitcoinCash) {
+ await _generateInitialAddresses(type: P2pkhAddressType.p2pkh);
+ } else if (walletInfo.type == WalletType.litecoin) {
+ await _generateInitialAddresses();
+ } else if (walletInfo.type == WalletType.bitcoin) {
+ await _generateInitialAddresses();
+ await _generateInitialAddresses(type: P2pkhAddressType.p2pkh);
+ await _generateInitialAddresses(type: P2shAddressType.p2wpkhInP2sh);
+ await _generateInitialAddresses(type: SegwitAddresType.p2tr);
+ await _generateInitialAddresses(type: SegwitAddresType.p2wsh);
+ }
updateAddressesByMatch();
updateReceiveAddresses();
updateChangeAddresses();
@@ -221,6 +222,11 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
try {
addressesMap.clear();
addressesMap[address] = '';
+
+ allAddressesMap.clear();
+ _addresses.forEach((addressRecord) {
+ allAddressesMap[addressRecord.address] = addressRecord.name;
+ });
await saveAddressesInBox();
} catch (e) {
print(e.toString());
@@ -229,15 +235,14 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
@action
void updateAddress(String address, String label) {
- if (address.startsWith('bitcoincash:')) {
- address = toLegacy(address);
- }
final addressRecord =
_addresses.firstWhere((addressRecord) => addressRecord.address == address);
addressRecord.setNewName(label);
final index = _addresses.indexOf(addressRecord);
_addresses.remove(addressRecord);
_addresses.insert(index, addressRecord);
+
+ updateAddressesByMatch();
}
@action
@@ -261,7 +266,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
addressRecord.isHidden &&
!addressRecord.isUsed &&
// TODO: feature to change change address type. For now fixed to p2wpkh, the cheapest type
- addressRecord.type == SegwitAddresType.p2wpkh);
+ (walletInfo.type != WalletType.bitcoin || addressRecord.type == SegwitAddresType.p2wpkh));
changeAddresses.addAll(newAddresses);
}
diff --git a/cw_bitcoin/lib/electrum_wallet_snapshot.dart b/cw_bitcoin/lib/electrum_wallet_snapshot.dart
index 7d9e5a1e2..5f1b90e6d 100644
--- a/cw_bitcoin/lib/electrum_wallet_snapshot.dart
+++ b/cw_bitcoin/lib/electrum_wallet_snapshot.dart
@@ -18,7 +18,6 @@ class ElectrumWalletSnapshot {
required this.regularAddressIndex,
required this.changeAddressIndex,
required this.addressPageType,
- required this.network,
this.derivationType,
this.derivationPath,
});
@@ -26,8 +25,7 @@ class ElectrumWalletSnapshot {
final String name;
final String password;
final WalletType type;
- final String addressPageType;
- final BasedUtxoNetwork network;
+ final String? addressPageType;
String mnemonic;
List addresses;
@@ -38,7 +36,7 @@ class ElectrumWalletSnapshot {
String? derivationPath;
static Future load(
- String name, WalletType type, String password, BasedUtxoNetwork? network) async {
+ String name, WalletType type, String password, BasedUtxoNetwork network) async {
final path = await pathForWallet(name: name, type: type);
final jsonSource = await read(path: path, password: password);
final data = json.decode(jsonSource) as Map;
@@ -80,8 +78,7 @@ class ElectrumWalletSnapshot {
balance: balance,
regularAddressIndex: regularAddressIndexByType,
changeAddressIndex: changeAddressIndexByType,
- addressPageType: data['address_page_type'] as String? ?? SegwitAddresType.p2wpkh.toString(),
- network: data['network_type'] == 'testnet' ? BitcoinNetwork.testnet : BitcoinNetwork.mainnet,
+ addressPageType: data['address_page_type'] as String?,
derivationType: derivationType,
derivationPath: derivationPath,
);
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/lib/script_hash.dart b/cw_bitcoin/lib/script_hash.dart
index 620d3d28a..2130fcbbe 100644
--- a/cw_bitcoin/lib/script_hash.dart
+++ b/cw_bitcoin/lib/script_hash.dart
@@ -1,8 +1,9 @@
-import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:crypto/crypto.dart';
+import 'package:cw_bitcoin/address_to_output_script.dart';
+import 'package:bitcoin_base/bitcoin_base.dart' as bitcoin;
-String scriptHash(String address, {required BasedUtxoNetwork network}) {
- final outputScript = addressToOutputScript(address: address, network: network);
+String scriptHash(String address, {required bitcoin.BasedUtxoNetwork network}) {
+ final outputScript = addressToOutputScript(address, network);
final parts = sha256.convert(outputScript).toString().split('');
var res = '';
diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock
index 25e6f269d..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"
@@ -79,11 +79,11 @@ packages:
dependency: "direct main"
description:
path: "."
- ref: cake-update-v1
- resolved-ref: "9611e9db77e92a8434e918cdfb620068f6fcb1aa"
+ ref: cake-update-v2
+ resolved-ref: "3fd81d238b990bb767fc7a4fdd5053a22a142e2e"
url: "https://github.com/cake-tech/bitcoin_base.git"
source: git
- version: "4.0.0"
+ version: "4.2.0"
bitcoin_flutter:
dependency: "direct main"
description:
@@ -97,10 +97,10 @@ packages:
dependency: "direct main"
description:
name: blockchain_utils
- sha256: "9701dfaa74caad4daae1785f1ec4445cf7fb94e45620bc3a4aca1b9b281dc6c9"
+ sha256: "38ef5f4a22441ac4370aed9071dc71c460acffc37c79b344533f67d15f24c13c"
url: "https://pub.dev"
source: hosted
- version: "1.6.0"
+ version: "2.1.1"
boolean_selector:
dependency: transitive
description:
diff --git a/cw_bitcoin/pubspec.yaml b/cw_bitcoin/pubspec.yaml
index 847b77773..632a3140a 100644
--- a/cw_bitcoin/pubspec.yaml
+++ b/cw_bitcoin/pubspec.yaml
@@ -26,15 +26,15 @@ 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
bitcoin_base:
git:
url: https://github.com/cake-tech/bitcoin_base.git
- ref: cake-update-v1
- blockchain_utils: ^1.6.0
+ ref: cake-update-v2
+ blockchain_utils: ^2.1.1
dev_dependencies:
flutter_test:
diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart
index 3c40cf9e9..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';
@@ -34,7 +29,7 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
required WalletInfo walletInfo,
required Box unspentCoinsInfo,
required Uint8List seedBytes,
- String? addressPageType,
+ BitcoinAddressType? addressPageType,
List? initialAddresses,
ElectrumBalance? initialBalance,
Map? initialRegularAddressIndex,
@@ -58,6 +53,7 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
mainHd: hd,
sideHd: bitcoin.HDWallet.fromSeed(seedBytes).derivePath("m/44'/145'/0'/1"),
network: network,
+ initialAddressPageType: addressPageType,
);
autorun((_) {
this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress;
@@ -84,7 +80,7 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
seedBytes: await Mnemonic.toSeed(mnemonic),
initialRegularAddressIndex: initialRegularAddressIndex,
initialChangeAddressIndex: initialChangeAddressIndex,
- addressPageType: addressPageType,
+ addressPageType: P2pkhAddressType.p2pkh,
);
}
@@ -101,193 +97,37 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
password: password,
walletInfo: walletInfo,
unspentCoinsInfo: unspentCoinsInfo,
- initialAddresses: snp.addresses,
+ initialAddresses: snp.addresses.map((addr) {
+ try {
+ BitcoinCashAddress(addr.address);
+ return BitcoinAddressRecord(
+ addr.address,
+ index: addr.index,
+ isHidden: addr.isHidden,
+ type: P2pkhAddressType.p2pkh,
+ network: BitcoinCashNetwork.mainnet,
+ );
+ } catch (_) {
+ return BitcoinAddressRecord(
+ AddressUtils.getCashAddrFormat(addr.address),
+ index: addr.index,
+ isHidden: addr.isHidden,
+ type: P2pkhAddressType.p2pkh,
+ network: BitcoinCashNetwork.mainnet,
+ );
+ }
+ }).toList(),
initialBalance: snp.balance,
seedBytes: await Mnemonic.toSeed(snp.mnemonic),
initialRegularAddressIndex: snp.regularAddressIndex,
initialChangeAddressIndex: snp.changeAddressIndex,
- addressPageType: snp.addressPageType,
+ addressPageType: P2pkhAddressType.p2pkh,
);
}
- @override
- Future 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/bitcoin_cash_wallet_addresses.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart
index 8291ce2a5..3164651f3 100644
--- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart
+++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart
@@ -19,6 +19,7 @@ abstract class BitcoinCashWalletAddressesBase extends ElectrumWalletAddresses wi
super.initialAddresses,
super.initialRegularAddressIndex,
super.initialChangeAddressIndex,
+ super.initialAddressPageType,
}) : super(walletInfo);
@override
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 9c098c0ff..37827f1ba 100644
--- a/cw_bitcoin_cash/pubspec.yaml
+++ b/cw_bitcoin_cash/pubspec.yaml
@@ -28,11 +28,11 @@ 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
- ref: cake-update-v1
+ ref: cake-update-v2
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/n2_node.dart b/cw_core/lib/n2_node.dart
new file mode 100644
index 000000000..a2eb6e4d3
--- /dev/null
+++ b/cw_core/lib/n2_node.dart
@@ -0,0 +1,31 @@
+class N2Node {
+ N2Node({
+ this.weight,
+ this.uptime,
+ this.score,
+ this.account,
+ this.alias,
+ });
+
+ String? uptime;
+ double? weight;
+ int? score;
+ String? account;
+ String? alias;
+
+ factory N2Node.fromJson(Map json) => N2Node(
+ weight: double.tryParse((json['weight'] as num?).toString()),
+ uptime: json['uptime'] as String?,
+ score: json['score'] as int?,
+ account: json['rep_address'] as String?,
+ alias: json['alias'] as String?,
+ );
+
+ Map toJson() => {
+ 'uptime': uptime,
+ 'weight': weight,
+ 'score': score,
+ 'rep_address': account,
+ 'alias': alias,
+ };
+}
diff --git a/cw_core/lib/node.dart b/cw_core/lib/node.dart
index 585bc3c38..d7e91d692 100644
--- a/cw_core/lib/node.dart
+++ b/cw_core/lib/node.dart
@@ -21,6 +21,7 @@ class Node extends HiveObject with Keyable {
this.trusted = false,
this.socksProxyAddress,
String? uri,
+ String? path,
WalletType? type,
}) {
if (uri != null) {
@@ -29,10 +30,14 @@ class Node extends HiveObject with Keyable {
if (type != null) {
this.type = type;
}
+ if (path != null) {
+ this.path = path;
+ }
}
Node.fromMap(Map map)
: uriRaw = map['uri'] as String? ?? '',
+ path = map['path'] as String? ?? '',
login = map['login'] as String?,
password = map['password'] as String?,
useSSL = map['useSSL'] as bool?,
@@ -63,6 +68,9 @@ class Node extends HiveObject with Keyable {
@HiveField(6)
String? socksProxyAddress;
+ @HiveField(7, defaultValue: '')
+ String? path;
+
bool get isSSL => useSSL ?? false;
bool get useSocksProxy => socksProxyAddress == null ? false : socksProxyAddress!.isNotEmpty;
@@ -79,9 +87,9 @@ class Node extends HiveObject with Keyable {
case WalletType.nano:
case WalletType.banano:
if (isSSL) {
- return Uri.https(uriRaw, '');
+ return Uri.https(uriRaw, path ?? '');
} else {
- return Uri.http(uriRaw, '');
+ return Uri.http(uriRaw, path ?? '');
}
case WalletType.ethereum:
case WalletType.polygon:
@@ -103,7 +111,8 @@ class Node extends HiveObject with Keyable {
other.typeRaw == typeRaw &&
other.useSSL == useSSL &&
other.trusted == trusted &&
- other.socksProxyAddress == socksProxyAddress);
+ other.socksProxyAddress == socksProxyAddress &&
+ other.path == path);
@override
int get hashCode =>
@@ -113,7 +122,8 @@ class Node extends HiveObject with Keyable {
typeRaw.hashCode ^
useSSL.hashCode ^
trusted.hashCode ^
- socksProxyAddress.hashCode;
+ socksProxyAddress.hashCode ^
+ path.hashCode;
@override
dynamic get keyIndex {
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..e987b5d0e 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,6 @@ abstract class WalletAddresses {
}
}
- bool containsAddress(String address) => addressesMap.containsKey(address);
+ bool containsAddress(String address) =>
+ addressesMap.containsKey(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_nano/lib/nano_client.dart b/cw_nano/lib/nano_client.dart
index 661fbcab8..064a0bdee 100644
--- a/cw_nano/lib/nano_client.dart
+++ b/cw_nano/lib/nano_client.dart
@@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:convert';
import 'package:cw_core/nano_account_info_response.dart';
+import 'package:cw_core/n2_node.dart';
import 'package:cw_nano/nano_balance.dart';
import 'package:cw_nano/nano_transaction_model.dart';
import 'package:http/http.dart' as http;
@@ -16,6 +17,8 @@ class NanoClient {
"nano-app": "cake-wallet"
};
+ static const String N2_REPS_ENDPOINT = "https://rpc.nano.to";
+
NanoClient() {
SharedPreferences.getInstance().then((value) => prefs = value);
}
@@ -418,7 +421,7 @@ class NanoClient {
body: jsonEncode({
"action": "account_history",
"account": address,
- "count": "250", // TODO: pick a number
+ "count": "100",
// "raw": true,
}));
final data = await jsonDecode(response.body);
@@ -434,4 +437,37 @@ class NanoClient {
return [];
}
}
+
+ Future> getN2Reps() async {
+ final response = await http.post(
+ Uri.parse(N2_REPS_ENDPOINT),
+ headers: CAKE_HEADERS,
+ body: jsonEncode({"action": "reps"}),
+ );
+ try {
+ final List nodes = (json.decode(response.body) as List)
+ .map((dynamic e) => N2Node.fromJson(e as Map))
+ .toList();
+ return nodes;
+ } catch (error) {
+ return [];
+ }
+ }
+
+ Future getRepScore(String rep) async {
+ final response = await http.post(
+ Uri.parse(N2_REPS_ENDPOINT),
+ headers: CAKE_HEADERS,
+ body: jsonEncode({
+ "action": "rep_info",
+ "account": rep,
+ }),
+ );
+ try {
+ final N2Node node = N2Node.fromJson(json.decode(response.body) as Map);
+ return node.score ?? 100;
+ } catch (error) {
+ return 100;
+ }
+ }
}
diff --git a/cw_nano/lib/nano_wallet.dart b/cw_nano/lib/nano_wallet.dart
index 611356173..5efe3006d 100644
--- a/cw_nano/lib/nano_wallet.dart
+++ b/cw_nano/lib/nano_wallet.dart
@@ -13,6 +13,7 @@ import 'package:cw_core/transaction_priority.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_nano/file.dart';
import 'package:cw_core/nano_account.dart';
+import 'package:cw_core/n2_node.dart';
import 'package:cw_nano/nano_balance.dart';
import 'package:cw_nano/nano_client.dart';
import 'package:cw_nano/nano_transaction_credentials.dart';
@@ -65,9 +66,11 @@ abstract class NanoWalletBase
String? _privateKey;
String? _publicAddress;
String? _hexSeed;
+ Timer? _receiveTimer;
String? _representativeAddress;
- Timer? _receiveTimer;
+ int repScore = 100;
+ bool get isRepOk => repScore >= 90;
late final NanoClient _client;
bool _isTransactionUpdating;
@@ -375,7 +378,7 @@ abstract class NanoWalletBase
final data = json.decode(jsonSource) as Map;
final mnemonic = data['mnemonic'] as String;
-
+
final balance = NanoBalance.fromRawString(
currentBalance: data['currentBalance'] as String? ?? "0",
receivableBalance: data['receivableBalance'] as String? ?? "0",
@@ -432,6 +435,8 @@ abstract class NanoWalletBase
_representativeAddress = await _client.getRepFromPrefs();
throw Exception("Failed to get representative address $e");
}
+
+ repScore = await _client.getRepScore(_representativeAddress!);
}
Future regenerateAddress() async {
@@ -468,6 +473,10 @@ abstract class NanoWalletBase
}
}
+ Future> getN2Reps() async {
+ return _client.getN2Reps();
+ }
+
Future? updateBalance() async => await _updateBalance();
@override
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 3476d76cd..ad58c4293 100644
--- a/cw_solana/lib/solana_wallet.dart
+++ b/cw_solana/lib/solana_wallet.dart
@@ -1,6 +1,5 @@
import 'dart:async';
import 'dart:convert';
-import 'dart:developer';
import 'dart:io';
import 'package:cw_core/cake_hive.dart';
import 'package:cw_core/crypto_currency.dart';
@@ -30,7 +29,6 @@ import 'package:mobx/mobx.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:solana/metaplex.dart' as metaplex;
import 'package:solana/solana.dart';
-import 'package:web3dart/crypto.dart';
part 'solana_wallet.g.dart';
@@ -77,6 +75,9 @@ abstract class SolanaWalletBase
late SolanaWalletClient _client;
+ @observable
+ double? estimatedFee;
+
Timer? _transactionsUpdateTimer;
late final Box splTokensBox;
@@ -134,7 +135,7 @@ abstract class SolanaWalletBase
assert(mnemonic != null || privateKey != null);
if (privateKey != null) {
- final privateKeyBytes = hexToBytes(privateKey);
+ final privateKeyBytes = HEX.decode(privateKey);
return await Wallet.fromPrivateKeyBytes(privateKey: privateKeyBytes);
}
@@ -173,6 +174,14 @@ abstract class SolanaWalletBase
}
}
+ Future _getEstimatedFees() async {
+ try {
+ estimatedFee = await _client.getEstimatedFee(_walletKeyPair!);
+ } catch (e) {
+ estimatedFee = 0.0;
+ }
+ }
+
@override
Future createTransaction(Object credentials) async {
final solCredentials = credentials as SolanaTransactionCredentials;
@@ -190,6 +199,8 @@ abstract class SolanaWalletBase
double totalAmount = 0.0;
+ bool isSendAll = false;
+
if (hasMultiDestination) {
if (outputs.any((item) => item.sendAll || (item.formattedCryptoAmount ?? 0) <= 0)) {
throw SolanaTransactionWrongBalanceException(transactionCurrency);
@@ -206,9 +217,15 @@ abstract class SolanaWalletBase
} else {
final output = outputs.first;
- final totalOriginalAmount = double.parse(output.cryptoAmount ?? '0.0');
+ isSendAll = output.sendAll;
- totalAmount = output.sendAll ? walletBalanceForCurrency : totalOriginalAmount;
+ if (isSendAll) {
+ totalAmount = walletBalanceForCurrency;
+ } else {
+ final totalOriginalAmount = double.parse(output.cryptoAmount ?? '0.0');
+
+ totalAmount = totalOriginalAmount;
+ }
if (walletBalanceForCurrency < totalAmount) {
throw SolanaTransactionWrongBalanceException(transactionCurrency);
@@ -230,6 +247,7 @@ abstract class SolanaWalletBase
destinationAddress: solCredentials.outputs.first.isParsedAddress
? solCredentials.outputs.first.extractedAddress!
: solCredentials.outputs.first.address,
+ isSendAll: isSendAll,
);
return pendingSolanaTransaction;
@@ -271,7 +289,10 @@ abstract class SolanaWalletBase
Future _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,
@@ -328,6 +349,7 @@ abstract class SolanaWalletBase
_updateBalance(),
_updateNativeSOLTransactions(),
_updateSPLTokenTransactions(),
+ _getEstimatedFees(),
]);
syncStatus = SyncedSyncStatus();
@@ -435,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
@@ -477,9 +509,9 @@ abstract class SolanaWalletBase
}
_transactionsUpdateTimer = Timer.periodic(const Duration(seconds: 20), (_) {
- _updateSPLTokenTransactions();
- _updateNativeSOLTransactions();
_updateBalance();
+ _updateNativeSOLTransactions();
+ _updateSPLTokenTransactions();
});
}
@@ -491,7 +523,7 @@ abstract class SolanaWalletBase
final signature = await _walletKeyPair!.sign(messageBytes);
// Convert the signature to a hexadecimal string
- final hex = bytesToHex(signature.bytes);
+ final hex = HEX.encode(signature.bytes);
return hex;
}
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,13 +126,46 @@ class CWBitcoin extends Bitcoin {
.map((BitcoinAddressRecord addr) => ElectrumSubAddress(
id: addr.index,
name: addr.name,
- address: electrumWallet.type == WalletType.bitcoinCash ? addr.cashAddr : addr.address,
+ address: addr.address,
txCount: addr.txCount,
balance: addr.balance,
isChange: addr.isHidden))
.toList();
}
+ @override
+ Future estimateFakeSendAllTxAmount(Object wallet, TransactionPriority priority) async {
+ try {
+ final sk = ECPrivate.random();
+ final electrumWallet = wallet as ElectrumWallet;
+
+ if (wallet.type == WalletType.bitcoinCash) {
+ final p2pkhAddr = sk.getPublic().toP2pkhAddress();
+ final estimatedTx = await electrumWallet.estimateSendAllTx(
+ [BitcoinOutput(address: p2pkhAddr, value: BigInt.zero)],
+ getFeeRate(wallet, priority as BitcoinCashTransactionPriority),
+ );
+
+ return estimatedTx.amount;
+ }
+
+ final p2shAddr = sk.getPublic().toP2pkhInP2sh();
+ final estimatedTx = await electrumWallet.estimateSendAllTx(
+ [BitcoinOutput(address: p2shAddr, value: BigInt.zero)],
+ getFeeRate(
+ wallet,
+ wallet.type == WalletType.litecoin
+ ? priority as LitecoinTransactionPriority
+ : priority as BitcoinTransactionPriority,
+ ),
+ );
+
+ return estimatedTx.amount;
+ } catch (_) {
+ return 0;
+ }
+ }
+
@override
String getAddress(Object wallet) {
final bitcoinWallet = wallet as ElectrumWallet;
@@ -147,8 +184,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) {
@@ -174,6 +212,9 @@ class CWBitcoin extends Bitcoin {
@override
TransactionPriority getBitcoinTransactionPriorityMedium() => BitcoinTransactionPriority.medium;
+ @override
+ TransactionPriority getBitcoinTransactionPriorityCustom() => BitcoinTransactionPriority.custom;
+
@override
TransactionPriority getLitecoinTransactionPriorityMedium() => LitecoinTransactionPriority.medium;
@@ -312,4 +353,48 @@ class CWBitcoin extends Bitcoin {
return list;
}
+
+ @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 0ccb73e1c..fea8fdabd 100644
--- a/lib/buy/moonpay/moonpay_provider.dart
+++ b/lib/buy/moonpay/moonpay_provider.dart
@@ -1,4 +1,13 @@
import 'dart:convert';
+
+import 'package:cake_wallet/.secrets.g.dart' as secrets;
+import 'package:cake_wallet/buy/buy_amount.dart';
+import 'package:cake_wallet/buy/buy_exception.dart';
+import 'package:cake_wallet/buy/buy_provider.dart';
+import 'package:cake_wallet/buy/buy_provider_description.dart';
+import 'package:cake_wallet/buy/order.dart';
+import 'package:cake_wallet/exchange/trade_state.dart';
+import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/palette.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
@@ -6,34 +15,31 @@ import 'package:cake_wallet/store/settings_store.dart';
import 'package:cake_wallet/themes/theme_base.dart';
import 'package:cake_wallet/utils/device_info.dart';
import 'package:crypto/crypto.dart';
-import 'package:cake_wallet/buy/buy_exception.dart';
-import 'package:cake_wallet/generated/i18n.dart';
-import 'package:flutter/material.dart';
-import 'package:http/http.dart';
-import 'package:cake_wallet/buy/buy_amount.dart';
-import 'package:cake_wallet/buy/buy_provider.dart';
-import 'package:cake_wallet/buy/buy_provider_description.dart';
-import 'package:cake_wallet/buy/order.dart';
+import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_type.dart';
-import 'package:cake_wallet/exchange/trade_state.dart';
-import 'package:cake_wallet/.secrets.g.dart' as secrets;
-import 'package:cw_core/crypto_currency.dart';
+import 'package:flutter/material.dart';
+import 'package:http/http.dart';
import 'package:url_launcher/url_launcher.dart';
-class MoonPaySellProvider extends BuyProvider {
- MoonPaySellProvider({
+class MoonPayProvider extends BuyProvider {
+ MoonPayProvider({
required SettingsStore settingsStore,
required WalletBase wallet,
bool isTestEnvironment = false,
- }) : baseUrl = isTestEnvironment ? _baseTestUrl : _baseProductUrl,
+ }) : baseSellUrl = isTestEnvironment ? _baseSellTestUrl : _baseSellProductUrl,
+ baseBuyUrl = isTestEnvironment ? _baseBuyTestUrl : _baseBuyProductUrl,
this._settingsStore = settingsStore,
super(wallet: wallet, isTestEnvironment: isTestEnvironment);
final SettingsStore _settingsStore;
- static const _baseTestUrl = 'sell-sandbox.moonpay.com';
- static const _baseProductUrl = 'sell.moonpay.com';
+ static const _baseSellTestUrl = 'sell-sandbox.moonpay.com';
+ static const _baseSellProductUrl = 'sell.moonpay.com';
+ static const _baseBuyTestUrl = 'buy-staging.moonpay.com';
+ static const _baseBuyProductUrl = 'buy.moonpay.com';
+ static const _cIdBaseUrl = 'exchange-helper.cakewallet.com';
+ static const _apiUrl = 'https://api.moonpay.com';
@override
String get providerDescription =>
@@ -60,146 +66,121 @@ class MoonPaySellProvider extends BuyProvider {
static String get _apiKey => secrets.moonPayApiKey;
- static String get _secretKey => secrets.moonPaySecretKey;
- final String baseUrl;
+ final String baseBuyUrl;
+ final String baseSellUrl;
- Future requestMoonPayUrl({
+ String get currencyCode => walletTypeToCryptoCurrency(wallet.type).title.toLowerCase();
+
+ String get trackUrl => baseBuyUrl + '/transaction_receipt?transactionId=';
+
+ static String get _exchangeHelperApiKey => secrets.exchangeHelperApiKey;
+
+ Future getMoonpaySignature(String query) async {
+ final uri = Uri.https(_cIdBaseUrl, "/api/moonpay");
+
+ final response = await post(
+ uri,
+ headers: {
+ 'Content-Type': 'application/json',
+ 'x-api-key': _exchangeHelperApiKey,
+ },
+ body: json.encode({'query': query}),
+ );
+
+ if (response.statusCode == 200) {
+ return (jsonDecode(response.body) as Map)['signature'] as String;
+ } else {
+ throw Exception(
+ 'Provider currently unavailable. Status: ${response.statusCode} ${response.body}');
+ }
+ }
+
+ 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': currency.toString().toLowerCase(),
- 'refundWalletAddress': refundWalletAddress,
- }..addAll(customParams),
- );
+ if (_apiKey.isNotEmpty) {
+ params['apiKey'] = _apiKey;
+ }
- final messageBytes = utf8.encode('?${originalUri.query}');
- final key = utf8.encode(_secretKey);
- final hmac = Hmac(sha256, key);
- final digest = hmac.convert(messageBytes);
- final signature = base64.encode(digest.bytes);
+ final originalUri = Uri.https(
+ baseSellUrl,
+ '',
+ params,
+ );
if (isTestEnvironment) {
return originalUri;
}
+ final signature = await getMoonpaySignature('?${originalUri.query}');
+
final query = Map.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(),
- );
- },
- );
- }
- }
-}
-
-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 {
@@ -274,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/buy/robinhood/robinhood_buy_provider.dart b/lib/buy/robinhood/robinhood_buy_provider.dart
index 47c3ab1ea..7610e51f3 100644
--- a/lib/buy/robinhood/robinhood_buy_provider.dart
+++ b/lib/buy/robinhood/robinhood_buy_provider.dart
@@ -32,11 +32,12 @@ class RobinhoodBuyProvider extends BuyProvider {
String get _applicationId => secrets.robinhoodApplicationId;
- String get _apiSecret => secrets.robinhoodCIdApiSecret;
+ String get _apiSecret => secrets.exchangeHelperApiKey;
String getSignature(String message) {
switch (wallet.type) {
case WalletType.ethereum:
+ case WalletType.polygon:
return wallet.signMessage(message);
case WalletType.litecoin:
case WalletType.bitcoin:
diff --git a/lib/core/address_validator.dart b/lib/core/address_validator.dart
index ad2c761a3..967cf9bf0 100644
--- a/lib/core/address_validator.dart
+++ b/lib/core/address_validator.dart
@@ -274,7 +274,7 @@ class AddressValidator extends TextValidator {
'|([^0-9a-zA-Z]|^)([23][a-km-zA-HJ-NP-Z1-9]{25,34})([^0-9a-zA-Z]|\$)' //P2shAddress type
'|([^0-9a-zA-Z]|^)((bc|tb)1q[ac-hj-np-z02-9]{25,39})([^0-9a-zA-Z]|\$)' //P2wpkhAddress type
'|([^0-9a-zA-Z]|^)((bc|tb)1q[ac-hj-np-z02-9]{40,80})([^0-9a-zA-Z]|\$)' //P2wshAddress type
- '|([^0-9a-zA-Z]|^)((bc|tb)1p([ac-hj-np-z02-9]{39}|[ac-hj-np-z02-9]{59}|[ac-hj-np-z02-9]{8,89}))([^0-9a-zA-Z]|\$)'; //P2trAddress type
+ '|([^0-9a-zA-Z]|^)((bc|tb)1p([ac-hj-np-z02-9]{39}|[ac-hj-np-z02-9]{59}|[ac-hj-np-z02-9]{8,89}))([^0-9a-zA-Z]|\$)'; //P2trAddress type
case CryptoCurrency.ltc:
return '([^0-9a-zA-Z]|^)^L[a-zA-Z0-9]{26,33}([^0-9a-zA-Z]|\$)'
'|([^0-9a-zA-Z]|^)[LM][a-km-zA-HJ-NP-Z1-9]{26,33}([^0-9a-zA-Z]|\$)'
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/auth_service.dart b/lib/core/auth_service.dart
index a99aef31d..48610784c 100644
--- a/lib/core/auth_service.dart
+++ b/lib/core/auth_service.dart
@@ -1,5 +1,7 @@
+import 'dart:async';
import 'dart:io';
+import 'package:cake_wallet/core/secure_storage.dart';
import 'package:cake_wallet/core/totp_request_details.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/auth/auth_page.dart';
@@ -64,7 +66,7 @@ class AuthService with Store {
Future authenticate(String pin) async {
final key = generateStoreKeyFor(key: SecretStoreKey.pinCodePassword);
- final encodedPin = await secureStorage.read(key: key);
+ final encodedPin = await readSecureStorage(secureStorage, key);
final decodedPin = decodedPinCode(pin: encodedPin!);
return decodedPin == pin;
@@ -76,7 +78,8 @@ class AuthService with Store {
}
Future requireAuth() async {
- final timestamp = int.tryParse(await secureStorage.read(key: SecureKey.lastAuthTimeMilliseconds) ?? '0');
+ final timestamp =
+ int.tryParse(await secureStorage.read(key: SecureKey.lastAuthTimeMilliseconds) ?? '0');
final duration = _durationToRequireAuth(timestamp ?? 0);
final requiredPinInterval = settingsStore.pinTimeOutDuration;
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/key_service.dart b/lib/core/key_service.dart
index fce254ea2..f829c22b5 100644
--- a/lib/core/key_service.dart
+++ b/lib/core/key_service.dart
@@ -1,3 +1,4 @@
+import 'package:cake_wallet/core/secure_storage.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:cake_wallet/entities/secret_store_key.dart';
import 'package:cake_wallet/entities/encrypt.dart';
@@ -10,7 +11,7 @@ class KeyService {
Future getWalletPassword({required String walletName}) async {
final key = generateStoreKeyFor(
key: SecretStoreKey.moneroWalletPassword, walletName: walletName);
- final encodedPassword = await _secureStorage.read(key: key);
+ final encodedPassword = await readSecureStorage(_secureStorage, key);
return decodeWalletPassword(password: encodedPassword!);
}
diff --git a/lib/core/node_address_validator.dart b/lib/core/node_address_validator.dart
index 0e034dabc..c1fe4ba91 100644
--- a/lib/core/node_address_validator.dart
+++ b/lib/core/node_address_validator.dart
@@ -8,3 +8,8 @@ class NodeAddressValidator extends TextValidator {
pattern:
'^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\$|^[0-9a-zA-Z.\-]+\$');
}
+
+class NodePathValidator extends TextValidator {
+ NodePathValidator()
+ : super(errorMessage: S.current.error_text_node_address, pattern: '^([/0-9a-zA-Z.\-]+)?\$');
+}
diff --git a/lib/core/secure_storage.dart b/lib/core/secure_storage.dart
new file mode 100644
index 000000000..4d9334a10
--- /dev/null
+++ b/lib/core/secure_storage.dart
@@ -0,0 +1,27 @@
+import 'dart:async';
+import 'package:flutter_secure_storage/flutter_secure_storage.dart';
+// For now, we can create a utility function to handle this.
+//
+// However, we could look into abstracting the entire FlutterSecureStorage package
+// so the app doesn't depend on the package directly but an absraction.
+// It'll make these kind of modifications to read/write come from a single point.
+
+Future readSecureStorage(FlutterSecureStorage secureStorage, String key) async {
+ String? result;
+ const maxWait = Duration(seconds: 3);
+ const checkInterval = Duration(milliseconds: 200);
+
+ DateTime start = DateTime.now();
+
+ while (result == null && DateTime.now().difference(start) < maxWait) {
+ result = await secureStorage.read(key: key);
+
+ if (result != null) {
+ break;
+ }
+
+ await Future.delayed(checkInterval);
+ }
+
+ return result;
+}
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..adb516817 100644
--- a/lib/core/wallet_connect/web3wallet_service.dart
+++ b/lib/core/wallet_connect/web3wallet_service.dart
@@ -1,10 +1,12 @@
import 'dart:async';
+import 'dart:convert';
import 'dart:developer';
import 'dart:typed_data';
import 'package:cake_wallet/core/wallet_connect/chain_service/eth/evm_chain_id.dart';
import 'package:cake_wallet/core/wallet_connect/chain_service/eth/evm_chain_service.dart';
import 'package:cake_wallet/core/wallet_connect/wallet_connect_key_service.dart';
+import 'package:cake_wallet/entities/preferences_key.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/core/wallet_connect/models/auth_request_model.dart';
import 'package:cake_wallet/core/wallet_connect/models/chain_key_model.dart';
@@ -19,6 +21,7 @@ import 'package:cw_core/wallet_type.dart';
import 'package:eth_sig_util/eth_sig_util.dart';
import 'package:flutter/material.dart';
import 'package:mobx/mobx.dart';
+import 'package:shared_preferences/shared_preferences.dart';
import 'package:walletconnect_flutter_v2/walletconnect_flutter_v2.dart';
import 'chain_service/solana/solana_chain_id.dart';
@@ -32,6 +35,7 @@ class Web3WalletService = Web3WalletServiceBase with _$Web3WalletService;
abstract class Web3WalletServiceBase with Store {
final AppStore appStore;
+ final SharedPreferences sharedPreferences;
final BottomSheetService _bottomSheetHandler;
final WalletConnectKeyService walletKeyService;
@@ -52,7 +56,8 @@ abstract class Web3WalletServiceBase with Store {
@observable
ObservableList auth;
- Web3WalletServiceBase(this._bottomSheetHandler, this.walletKeyService, this.appStore)
+ Web3WalletServiceBase(
+ this._bottomSheetHandler, this.walletKeyService, this.appStore, this.sharedPreferences)
: pairings = ObservableList(),
sessions = ObservableList(),
auth = ObservableList(),
@@ -133,13 +138,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,
@@ -177,13 +196,6 @@ abstract class Web3WalletServiceBase with Store {
_refreshPairings();
}
- @action
- void _refreshPairings() {
- pairings.clear();
- final allPairings = _web3Wallet.pairings.getAll();
- pairings.addAll(allPairings);
- }
-
Future _onSessionProposalError(SessionProposalErrorEvent? args) async {
log(args.toString());
}
@@ -246,14 +258,37 @@ abstract class Web3WalletServiceBase with Store {
}
}
+ @action
+ void _refreshPairings() {
+ print('Refreshing pairings');
+ pairings.clear();
+
+ final allPairings = _web3Wallet.pairings.getAll();
+
+ final keyForWallet = getKeyForStoringTopicsForWallet();
+
+ final currentTopicsForWallet = getPairingTopicsForWallet(keyForWallet);
+
+ final filteredPairings =
+ allPairings.where((pairing) => currentTopicsForWallet.contains(pairing.topic)).toList();
+
+ pairings.addAll(filteredPairings);
+ }
+
void _onPairingCreate(PairingEvent? args) {
log('Pairing Create Event: $args');
}
@action
- void _onSessionConnect(SessionConnect? args) {
+ Future _onSessionConnect(SessionConnect? args) async {
if (args != null) {
+ log('Session Connected $args');
+
+ await savePairingTopicToLocalStorage(args.session.pairingTopic);
+
sessions.add(args.session);
+
+ _refreshPairings();
}
}
@@ -321,4 +356,53 @@ abstract class Web3WalletServiceBase with Store {
List getSessionsForPairingInfo(PairingInfo pairing) {
return sessions.where((element) => element.pairingTopic == pairing.topic).toList();
}
+
+ String getKeyForStoringTopicsForWallet() {
+ List chainKeys = walletKeyService.getKeysForChain(appStore.wallet!);
+
+ final keyForPairingTopic =
+ PreferencesKey.walletConnectPairingTopicsListForWallet(chainKeys.first.publicKey);
+
+ return keyForPairingTopic;
+ }
+
+ List getPairingTopicsForWallet(String key) {
+ // Get the JSON-encoded string from shared preferences
+ final jsonString = sharedPreferences.getString(key);
+
+ // If the string is null, return an empty list
+ if (jsonString == null) {
+ return [];
+ }
+
+ // Decode the JSON string to a list of strings
+ final List jsonList = jsonDecode(jsonString) as List;
+
+ // Cast each item to a string
+ return jsonList.map((item) => item as String).toList();
+ }
+
+ Future savePairingTopicToLocalStorage(String pairingTopic) async {
+ // Get key specific to the current wallet
+ final key = getKeyForStoringTopicsForWallet();
+
+ // Get all pairing topics attached to this key
+ final pairingTopicsForWallet = getPairingTopicsForWallet(key);
+
+ print(pairingTopicsForWallet);
+
+ bool isPairingTopicAlreadySaved = pairingTopicsForWallet.contains(pairingTopic);
+ print('Is Pairing Topic Saved: $isPairingTopicAlreadySaved');
+
+ if (!isPairingTopicAlreadySaved) {
+ // Update the list with the most recent pairing topic
+ pairingTopicsForWallet.add(pairingTopic);
+
+ // Convert the list of updated pairing topics to a JSON-encoded string
+ final jsonString = jsonEncode(pairingTopicsForWallet);
+
+ // Save the encoded string to shared preferences
+ await sharedPreferences.setString(key, jsonString);
+ }
+ }
}
diff --git a/lib/di.dart b/lib/di.dart
index 782c0f1f4..d78da638c 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';
@@ -491,6 +493,7 @@ Future setup({
getIt.get(),
getIt.get(),
appStore,
+ getIt.get()
);
web3WalletService.create();
return web3WalletService;
@@ -806,8 +809,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 +916,8 @@ Future setup({
transactionInfo: transactionInfo,
transactionDescriptionBox: _transactionDescriptionBox,
wallet: wallet,
- settingsStore: getIt.get());
+ settingsStore: getIt.get(),
+ sendViewModel: getIt.get());
});
getIt.registerFactoryParam(
@@ -1133,6 +1140,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 9e7491d57..2f6a7afc2 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,10 +212,17 @@ Future defaultSettingsMigration(
await changeDefaultBitcoinNode(nodes, sharedPreferences);
break;
- case 31:
- await updateBtcNanoWalletInfos(walletInfoSource);
+ case 30:
+ await disableServiceStatusFiatDisabled(sharedPreferences);
break;
+ case 31:
+ await updateNanoNodeList(nodes: nodes);
+ break;
+
+ case 32:
+ await updateBtcNanoWalletInfos(walletInfoSource);
+
default:
break;
}
@@ -229,6 +237,44 @@ Future defaultSettingsMigration(
await sharedPreferences.setInt(PreferencesKey.currentDefaultSettingsMigrationVersion, version);
}
+Future updateNanoNodeList({required Box nodes}) async {
+ final nodeList = await loadDefaultNanoNodes();
+ var listOfNewEndpoints = [
+ "app.natrium.io",
+ "rainstorm.city",
+ "node.somenano.com",
+ "nanoslo.0x.no",
+ "www.bitrequest.app",
+ ];
+ // add new nodes:
+ for (final node in nodeList) {
+ if (listOfNewEndpoints.contains(node.uriRaw)) {
+ await nodes.add(node);
+ }
+ }
+
+ // update the nautilus node:
+ final nautilusNode =
+ nodes.values.firstWhereOrNull((element) => element.uriRaw == "node.perish.co");
+ if (nautilusNode != null) {
+ nautilusNode.uriRaw = "node.nautilus.io";
+ nautilusNode.path = "/api";
+ nautilusNode.useSSL = true;
+ await nautilusNode.save();
+ }
+}
+
+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 1808be97c..f512d6b72 100644
--- a/lib/entities/preferences_key.dart
+++ b/lib/entities/preferences_key.dart
@@ -20,6 +20,7 @@ class PreferencesKey {
static const isAppSecureKey = 'is_app_secure';
static const disableBuyKey = 'disable_buy';
static const disableSellKey = 'disable_sell';
+ static const disableBulletinKey = 'disable_bulletin';
static const defaultBuyProvider = 'default_buy_provider';
static const walletListOrder = 'wallet_list_order';
static const walletListAscending = 'wallet_list_ascending';
@@ -41,8 +42,10 @@ 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 shouldShowRepWarning = 'should_show_rep_warning';
static const moneroWalletPasswordUpdateV1Base = 'monero_wallet_update_v1';
static const syncModeKey = 'sync_mode';
static const syncAllKey = 'sync_all';
@@ -73,4 +76,7 @@ class PreferencesKey {
static const shouldShowMarketPlaceInDashboard = 'should_show_marketplace_in_dashboard';
static const isNewInstall = 'is_new_install';
static const serviceStatusShaKey = 'service_status_sha_key';
+ static const walletConnectPairingTopicsList = 'wallet_connect_pairing_topics_list';
+ static String walletConnectPairingTopicsListForWallet(String publicKey) =>
+ '${PreferencesKey.walletConnectPairingTopicsList}_${publicKey}';
}
diff --git a/lib/entities/provider_types.dart b/lib/entities/provider_types.dart
index f9c2f1a82..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';
}
}
@@ -55,18 +55,18 @@ class ProvidersHelper {
case WalletType.monero:
return [ProviderType.askEachTime, ProviderType.onramper, ProviderType.dfx];
case WalletType.bitcoin:
+ case WalletType.polygon:
case WalletType.ethereum:
return [
ProviderType.askEachTime,
ProviderType.onramper,
ProviderType.dfx,
ProviderType.robinhood,
+ ProviderType.moonpay,
];
case WalletType.litecoin:
case WalletType.bitcoinCash:
- return [ProviderType.askEachTime, ProviderType.onramper, ProviderType.robinhood];
- case WalletType.polygon:
- return [ProviderType.askEachTime, ProviderType.onramper, ProviderType.dfx];
+ return [ProviderType.askEachTime, ProviderType.onramper, ProviderType.robinhood, ProviderType.moonpay];
case WalletType.solana:
return [ProviderType.askEachTime, ProviderType.onramper, ProviderType.robinhood];
case WalletType.none:
@@ -79,23 +79,22 @@ class ProvidersHelper {
switch (walletType) {
case WalletType.bitcoin:
case WalletType.ethereum:
+ case WalletType.polygon:
return [
ProviderType.askEachTime,
ProviderType.onramper,
- ProviderType.moonpaySell,
+ ProviderType.moonpay,
ProviderType.dfx,
];
case WalletType.litecoin:
case WalletType.bitcoinCash:
- return [ProviderType.askEachTime, ProviderType.moonpaySell];
- case WalletType.polygon:
- return [ProviderType.askEachTime, ProviderType.onramper, ProviderType.dfx];
+ return [ProviderType.askEachTime, ProviderType.moonpay];
case WalletType.solana:
return [
ProviderType.askEachTime,
ProviderType.onramper,
ProviderType.robinhood,
- ProviderType.moonpaySell,
+ ProviderType.moonpay,
];
case WalletType.monero:
case WalletType.nano:
@@ -114,10 +113,10 @@ class ProvidersHelper {
return getIt.get();
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..688bf15c9 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',
@@ -31,7 +32,17 @@ class TrocadorExchangeProvider extends ExchangeProvider {
'Exolix',
'Godex',
'Exch',
- 'CoinCraddle'
+ 'CoinCraddle',
+ 'Alfacash',
+ 'LocalMonero',
+ 'XChange',
+ 'NeroSwap',
+ 'Changee',
+ 'BitcoinVN',
+ 'EasyBit',
+ 'WizardSwap',
+ 'Quantex',
+ 'SwapSpace',
];
static const List _notSupported = [
@@ -144,8 +155,11 @@ class TrocadorExchangeProvider extends ExchangeProvider {
}
@override
- Future createTrade({required TradeRequest request, required bool isFixedRateMode}) async {
-
+ Future