diff --git a/.github/workflows/pr_test_build.yml b/.github/workflows/pr_test_build.yml
index 23902f110..69c632967 100644
--- a/.github/workflows/pr_test_build.yml
+++ b/.github/workflows/pr_test_build.yml
@@ -151,6 +151,7 @@ jobs:
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
echo "const ankrApiKey = '${{ secrets.ANKR_API_KEY }}';" >> cw_solana/lib/.secrets.g.dart
+ echo "const nano2ApiKey = '${{ secrets.NANO2_API_KEY }}';" >> cw_nano/lib/.secrets.g.dart
echo "const tronGridApiKey = '${{ secrets.TRON_GRID_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart
- name: Rename app
diff --git a/.gitignore b/.gitignore
index f1e5b6da3..24b7291f8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -94,6 +94,7 @@ android/app/key.jks
**/tool/.evm-secrets-config.json
**/tool/.ethereum-secrets-config.json
**/tool/.solana-secrets-config.json
+**/tool/.nano-secrets-config.json
**/tool/.tron-secrets-config.json
**/lib/.secrets.g.dart
**/cw_evm/lib/.secrets.g.dart
diff --git a/android/app/src/main/AndroidManifestBase.xml b/android/app/src/main/AndroidManifestBase.xml
index 23207d629..57462099c 100644
--- a/android/app/src/main/AndroidManifestBase.xml
+++ b/android/app/src/main/AndroidManifestBase.xml
@@ -91,6 +91,13 @@
+
+
+
+
+
+
+
with Serializable implemen
element.tag == walletCurrency?.tag));
} catch (_) {}
+ // search by fullName if not found by title:
+ try {
+ return CryptoCurrency.all.firstWhere((element) => element.fullName?.toLowerCase() == name);
+ } catch (_) {}
+
if (CryptoCurrency._nameCurrencyMap[name.toLowerCase()] == null) {
final s = 'Unexpected token: $name for CryptoCurrency fromString';
throw ArgumentError.value(name, 'name', s);
}
+
return CryptoCurrency._nameCurrencyMap[name.toLowerCase()]!;
}
diff --git a/cw_core/lib/window_size.dart b/cw_core/lib/window_size.dart
new file mode 100644
index 000000000..a0f192f66
--- /dev/null
+++ b/cw_core/lib/window_size.dart
@@ -0,0 +1,22 @@
+import 'dart:io';
+
+import 'package:flutter/services.dart';
+
+const MethodChannel _channel = MethodChannel('com.cake_wallet/native_utils');
+
+Future setDefaultMinimumWindowSize() async {
+ if (!Platform.isMacOS) return;
+
+ try {
+ final result = await _channel.invokeMethod(
+ 'setMinWindowSize',
+ {'width': 500, 'height': 700},
+ ) as bool;
+
+ if (!result) {
+ print("Failed to set minimum window size.");
+ }
+ } on PlatformException catch (e) {
+ print("Failed to set minimum window size: '${e.message}'.");
+ }
+}
diff --git a/cw_nano/lib/banano_balance.dart b/cw_nano/lib/banano_balance.dart
index b904a35cb..d766077fc 100644
--- a/cw_nano/lib/banano_balance.dart
+++ b/cw_nano/lib/banano_balance.dart
@@ -1,12 +1,28 @@
import 'package:cw_core/balance.dart';
import 'package:nanoutil/nanoutil.dart';
+BigInt stringAmountToBigIntBanano(String amount) {
+ return BigInt.parse(NanoAmounts.getAmountAsRaw(amount, NanoAmounts.rawPerBanano));
+}
+
class BananoBalance extends Balance {
final BigInt currentBalance;
final BigInt receivableBalance;
BananoBalance({required this.currentBalance, required this.receivableBalance}) : super(0, 0);
+ BananoBalance.fromFormattedString(
+ {required String formattedCurrentBalance, required String formattedReceivableBalance})
+ : currentBalance = stringAmountToBigIntBanano(formattedCurrentBalance),
+ receivableBalance = stringAmountToBigIntBanano(formattedReceivableBalance),
+ super(0, 0);
+
+ BananoBalance.fromRawString(
+ {required String currentBalance, required String receivableBalance})
+ : currentBalance = BigInt.parse(currentBalance),
+ receivableBalance = BigInt.parse(receivableBalance),
+ super(0, 0);
+
@override
String get formattedAvailableBalance {
return NanoAmounts.getRawAsUsableString(currentBalance.toString(), NanoAmounts.rawPerBanano);
diff --git a/cw_nano/lib/nano_balance.dart b/cw_nano/lib/nano_balance.dart
index 8b8c93b33..691b3a32d 100644
--- a/cw_nano/lib/nano_balance.dart
+++ b/cw_nano/lib/nano_balance.dart
@@ -1,7 +1,7 @@
import 'package:cw_core/balance.dart';
import 'package:nanoutil/nanoutil.dart';
-BigInt stringAmountToBigInt(String amount) {
+BigInt stringAmountToBigIntNano(String amount) {
return BigInt.parse(NanoAmounts.getAmountAsRaw(amount, NanoAmounts.rawPerNano));
}
@@ -13,8 +13,8 @@ class NanoBalance extends Balance {
NanoBalance.fromFormattedString(
{required String formattedCurrentBalance, required String formattedReceivableBalance})
- : currentBalance = stringAmountToBigInt(formattedCurrentBalance),
- receivableBalance = stringAmountToBigInt(formattedReceivableBalance),
+ : currentBalance = stringAmountToBigIntNano(formattedCurrentBalance),
+ receivableBalance = stringAmountToBigIntNano(formattedReceivableBalance),
super(0, 0);
NanoBalance.fromRawString(
diff --git a/cw_nano/lib/nano_client.dart b/cw_nano/lib/nano_client.dart
index 267ac888f..99782e47c 100644
--- a/cw_nano/lib/nano_client.dart
+++ b/cw_nano/lib/nano_client.dart
@@ -10,6 +10,7 @@ import 'package:http/http.dart' as http;
import 'package:cw_core/node.dart';
import 'package:nanoutil/nanoutil.dart';
import 'package:shared_preferences/shared_preferences.dart';
+import 'package:cw_nano/.secrets.g.dart' as secrets;
class NanoClient {
static const Map CAKE_HEADERS = {
@@ -52,10 +53,19 @@ class NanoClient {
}
}
+ Map getHeaders() {
+ if (_node!.uri == "https://rpc.nano.to") {
+ return CAKE_HEADERS..addAll({
+ "key": secrets.nano2ApiKey,
+ });
+ }
+ return CAKE_HEADERS;
+ }
+
Future getBalance(String address) async {
final response = await http.post(
_node!.uri,
- headers: CAKE_HEADERS,
+ headers: getHeaders(),
body: jsonEncode(
{
"action": "account_balance",
@@ -82,7 +92,7 @@ class NanoClient {
try {
final response = await http.post(
_node!.uri,
- headers: CAKE_HEADERS,
+ headers: getHeaders(),
body: jsonEncode(
{
"action": "account_info",
@@ -94,7 +104,7 @@ class NanoClient {
final data = await jsonDecode(response.body);
return AccountInfoResponse.fromJson(data as Map);
} catch (e) {
- print("error while getting account info");
+ print("error while getting account info $e");
return null;
}
}
@@ -170,7 +180,7 @@ class NanoClient {
Future requestWork(String hash) async {
final response = await http.post(
_powNode!.uri,
- headers: CAKE_HEADERS,
+ headers: getHeaders(),
body: json.encode(
{
"action": "work_generate",
@@ -213,7 +223,7 @@ class NanoClient {
final processResponse = await http.post(
_node!.uri,
- headers: CAKE_HEADERS,
+ headers: getHeaders(),
body: processBody,
);
@@ -412,7 +422,7 @@ class NanoClient {
});
final processResponse = await http.post(
_node!.uri,
- headers: CAKE_HEADERS,
+ headers: getHeaders(),
body: processBody,
);
@@ -428,7 +438,7 @@ class NanoClient {
required String privateKey,
}) async {
final receivableResponse = await http.post(_node!.uri,
- headers: CAKE_HEADERS,
+ headers: getHeaders(),
body: jsonEncode({
"action": "receivable",
"account": destinationAddress,
@@ -476,7 +486,7 @@ class NanoClient {
Future> fetchTransactions(String address) async {
try {
final response = await http.post(_node!.uri,
- headers: CAKE_HEADERS,
+ headers: getHeaders(),
body: jsonEncode({
"action": "account_history",
"account": address,
diff --git a/cw_tron/lib/tron_client.dart b/cw_tron/lib/tron_client.dart
index f03a8abce..73812f14c 100644
--- a/cw_tron/lib/tron_client.dart
+++ b/cw_tron/lib/tron_client.dart
@@ -367,7 +367,7 @@ class TronClient {
) async {
// This is introduce to server as a limit in cases where feeLimit is 0
// The transaction signing will fail if the feeLimit is explicitly 0.
- int defaultFeeLimit = 100000;
+ int defaultFeeLimit = 269000;
final block = await _provider!.request(TronRequestGetNowBlock());
// Create the transfer contract
@@ -401,8 +401,9 @@ class TronClient {
final tronBalanceInt = tronBalance.toInt();
if (feeLimit > tronBalanceInt) {
+ final feeInTrx = TronHelper.fromSun(BigInt.parse(feeLimit.toString()));
throw Exception(
- 'You don\'t have enough TRX to cover the transaction fee for this transaction. Kindly top up.',
+ 'You don\'t have enough TRX to cover the transaction fee for this transaction. Kindly top up.\nTransaction fee: $feeInTrx TRX',
);
}
@@ -442,6 +443,9 @@ class TronClient {
if (!request.isSuccess) {
log("Tron TRC20 error: ${request.error} \n ${request.respose}");
+ throw Exception(
+ 'An error occured while creating the transfer request. Please try again.',
+ );
}
final feeLimit = await getFeeLimit(
@@ -454,8 +458,9 @@ class TronClient {
final tronBalanceInt = tronBalance.toInt();
if (feeLimit > tronBalanceInt) {
+ final feeInTrx = TronHelper.fromSun(BigInt.parse(feeLimit.toString()));
throw Exception(
- 'You don\'t have enough TRX to cover the transaction fee for this transaction. Kindly top up.',
+ 'You don\'t have enough TRX to cover the transaction fee for this transaction. Kindly top up. Transaction fee: $feeInTrx TRX',
);
}
diff --git a/ios/Runner/InfoBase.plist b/ios/Runner/InfoBase.plist
index 02365bda7..83e60b542 100644
--- a/ios/Runner/InfoBase.plist
+++ b/ios/Runner/InfoBase.plist
@@ -140,6 +140,16 @@
nano-wallet
+
+ CFBundleTypeRole
+ Viewer
+ CFBundleURLName
+ nano-gpt
+ CFBundleURLSchemes
+
+ nano-gpt
+
+
CFBundleTypeRole
Editor
diff --git a/lib/di.dart b/lib/di.dart
index 4a8539e7d..a013f72a2 100644
--- a/lib/di.dart
+++ b/lib/di.dart
@@ -28,6 +28,7 @@ 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/dashboard/sign_page.dart';
import 'package:cake_wallet/src/screens/receive/address_list_page.dart';
+import 'package:cake_wallet/view_model/link_view_model.dart';
import 'package:cake_wallet/tron/tron.dart';
import 'package:cake_wallet/src/screens/transaction_details/rbf_details_page.dart';
import 'package:cake_wallet/view_model/dashboard/sign_view_model.dart';
@@ -271,6 +272,7 @@ Future setup({
required Box unspentCoinsInfoSource,
required Box anonpayInvoiceInfoSource,
required FlutterSecureStorage secureStorage,
+ required GlobalKey navigatorKey,
}) async {
_walletInfoSource = walletInfoSource;
_nodeSource = nodeSource;
@@ -432,68 +434,89 @@ Future setup({
),
);
- getIt.registerFactory(() {
- return AuthPage(getIt.get(),
+ getIt.registerLazySingleton(() {
+ return LinkViewModel(
+ appStore: getIt.get(),
+ settingsStore: getIt.get(),
+ authenticationStore: getIt.get(),
+ navigatorKey: navigatorKey,
+ );
+ });
+
+ getIt.registerFactory(instanceName: 'login', () {
+ return AuthPage(getIt.get(), closable: false,
onAuthenticationFinished: (isAuthenticated, AuthPageState authPageState) {
if (!isAuthenticated) {
return;
- } else {
- final authStore = getIt.get();
- final appStore = getIt.get();
- final useTotp = appStore.settingsStore.useTOTP2FA;
- final shouldUseTotp2FAToAccessWallets =
- appStore.settingsStore.shouldRequireTOTP2FAForAccessingWallet;
- if (useTotp && shouldUseTotp2FAToAccessWallets) {
- authPageState.close(
- route: Routes.totpAuthCodePage,
- arguments: TotpAuthArgumentsModel(
- isForSetup: false,
- isClosable: false,
- onTotpAuthenticationFinished: (bool isAuthenticatedSuccessfully,
- TotpAuthCodePageState totpAuthPageState) async {
- if (!isAuthenticatedSuccessfully) {
- return;
- }
- if (appStore.wallet != null) {
- authStore.allowed();
- return;
- }
-
- totpAuthPageState.changeProcessText('Loading the wallet');
-
- if (loginError != null) {
- totpAuthPageState.changeProcessText('ERROR: ${loginError.toString()}');
- }
-
- ReactionDisposer? _reaction;
- _reaction = reaction((_) => appStore.wallet, (Object? _) {
- _reaction?.reaction.dispose();
- authStore.allowed();
- });
- },
- ),
- );
- } else {
- if (appStore.wallet != null) {
- authStore.allowed();
- return;
- }
-
- authPageState.changeProcessText('Loading the wallet');
-
- if (loginError != null) {
- authPageState.changeProcessText('ERROR: ${loginError.toString()}');
- }
-
- ReactionDisposer? _reaction;
- _reaction = reaction((_) => appStore.wallet, (Object? _) {
- _reaction?.reaction.dispose();
- authStore.allowed();
- });
- }
}
- }, closable: false);
- }, instanceName: 'login');
+ final authStore = getIt.get();
+ final appStore = getIt.get();
+ final useTotp = appStore.settingsStore.useTOTP2FA;
+ final shouldUseTotp2FAToAccessWallets =
+ appStore.settingsStore.shouldRequireTOTP2FAForAccessingWallet;
+ if (useTotp && shouldUseTotp2FAToAccessWallets) {
+ authPageState.close(
+ route: Routes.totpAuthCodePage,
+ arguments: TotpAuthArgumentsModel(
+ isForSetup: false,
+ isClosable: false,
+ onTotpAuthenticationFinished:
+ (bool isAuthenticatedSuccessfully, TotpAuthCodePageState totpAuthPageState) async {
+ if (!isAuthenticatedSuccessfully) {
+ return;
+ }
+ if (appStore.wallet != null) {
+ authStore.allowed();
+ return;
+ }
+
+ totpAuthPageState.changeProcessText('Loading the wallet');
+
+ if (loginError != null) {
+ totpAuthPageState.changeProcessText('ERROR: ${loginError.toString()}');
+ }
+
+ ReactionDisposer? _reaction;
+ _reaction = reaction((_) => appStore.wallet, (Object? _) {
+ _reaction?.reaction.dispose();
+ authStore.allowed();
+ });
+ },
+ ),
+ );
+ } else {
+ // wallet is already loaded:
+ if (appStore.wallet != null) {
+ // goes to the dashboard:
+ authStore.allowed();
+ // trigger any deep links:
+ final linkViewModel = getIt.get();
+ if (linkViewModel.currentLink != null) {
+ linkViewModel.handleLink();
+ }
+ return;
+ }
+
+ // load the wallet:
+
+ authPageState.changeProcessText('Loading the wallet');
+
+ if (loginError != null) {
+ authPageState.changeProcessText('ERROR: ${loginError.toString()}');
+ }
+
+ ReactionDisposer? _reaction;
+ _reaction = reaction((_) => appStore.wallet, (Object? _) {
+ _reaction?.reaction.dispose();
+ authStore.allowed();
+ final linkViewModel = getIt.get();
+ if (linkViewModel.currentLink != null) {
+ linkViewModel.handleLink();
+ }
+ });
+ }
+ });
+ });
getIt.registerSingleton(BottomSheetServiceImpl());
@@ -854,8 +877,10 @@ Future setup({
tradesStore: getIt.get(),
sendViewModel: getIt.get()));
- getIt.registerFactory(
- () => ExchangePage(getIt.get(), getIt.get()));
+ getIt.registerFactoryParam(
+ (PaymentRequest? paymentRequest, __) {
+ return ExchangePage(getIt.get(), getIt.get(), paymentRequest);
+ });
getIt.registerFactory(() => ExchangeConfirmPage(tradesStore: getIt.get()));
diff --git a/lib/entities/parse_address_from_domain.dart b/lib/entities/parse_address_from_domain.dart
index f729e6392..409724c6e 100644
--- a/lib/entities/parse_address_from_domain.dart
+++ b/lib/entities/parse_address_from_domain.dart
@@ -5,6 +5,7 @@ import 'package:cake_wallet/entities/openalias_record.dart';
import 'package:cake_wallet/entities/parsed_address.dart';
import 'package:cake_wallet/entities/unstoppable_domain_address.dart';
import 'package:cake_wallet/entities/emoji_string_extension.dart';
+import 'package:cake_wallet/exchange/provider/thorchain_exchange.provider.dart';
import 'package:cake_wallet/mastodon/mastodon_api.dart';
import 'package:cake_wallet/nostr/nostr_api.dart';
import 'package:cake_wallet/store/settings_store.dart';
@@ -71,8 +72,8 @@ class AddressResolver {
return emailRegex.hasMatch(address);
}
- // TODO: refactor this to take Crypto currency instead of ticker, or at least pass in the tag as well
- Future resolve(BuildContext context, String text, String ticker) async {
+ Future resolve(BuildContext context, String text, CryptoCurrency currency) async {
+ final ticker = currency.title;
try {
if (text.startsWith('@') && !text.substring(1).contains('@')) {
if (settingsStore.lookupsTwitter) {
@@ -116,8 +117,7 @@ class AddressResolver {
await MastodonAPI.lookupUserByUserName(userName: userName, apiHost: hostName);
if (mastodonUser != null) {
- String? addressFromBio = extractAddressByType(
- raw: mastodonUser.note, type: CryptoCurrency.fromString(ticker));
+ String? addressFromBio = extractAddressByType(raw: mastodonUser.note, type: currency);
if (addressFromBio != null) {
return ParsedAddress.fetchMastodonAddress(
@@ -131,8 +131,8 @@ class AddressResolver {
if (pinnedPosts.isNotEmpty) {
final userPinnedPostsText = pinnedPosts.map((item) => item.content).join('\n');
- String? addressFromPinnedPost = extractAddressByType(
- raw: userPinnedPostsText, type: CryptoCurrency.fromString(ticker));
+ String? addressFromPinnedPost =
+ extractAddressByType(raw: userPinnedPostsText, type: currency);
if (addressFromPinnedPost != null) {
return ParsedAddress.fetchMastodonAddress(
@@ -162,6 +162,16 @@ class AddressResolver {
}
}
}
+
+ final thorChainAddress = await ThorChainExchangeProvider.lookupAddressByName(text);
+ if (thorChainAddress != null) {
+ String? address =
+ thorChainAddress[ticker] ?? (ticker == 'RUNE' ? thorChainAddress['THOR'] : null);
+ if (address != null) {
+ return ParsedAddress.thorChainAddress(address: address, name: text);
+ }
+ }
+
final formattedName = OpenaliasRecord.formatDomainName(text);
final domainParts = formattedName.split('.');
final name = domainParts.last;
@@ -204,7 +214,7 @@ class AddressResolver {
if (nostrUserData != null) {
String? addressFromBio = extractAddressByType(
- raw: nostrUserData.about, type: CryptoCurrency.fromString(ticker));
+ raw: nostrUserData.about, type: currency);
if (addressFromBio != null) {
return ParsedAddress.nostrAddress(
address: addressFromBio,
diff --git a/lib/entities/parsed_address.dart b/lib/entities/parsed_address.dart
index fc8ab2440..cfd69acbe 100644
--- a/lib/entities/parsed_address.dart
+++ b/lib/entities/parsed_address.dart
@@ -11,7 +11,8 @@ enum ParseFrom {
ens,
contact,
mastodon,
- nostr
+ nostr,
+ thorChain
}
class ParsedAddress {
@@ -133,6 +134,14 @@ class ParsedAddress {
);
}
+ factory ParsedAddress.thorChainAddress({required String address, required String name}) {
+ return ParsedAddress(
+ addresses: [address],
+ name: name,
+ parseFrom: ParseFrom.thorChain,
+ );
+ }
+
final List addresses;
final String name;
final String description;
diff --git a/lib/exchange/provider/thorchain_exchange.provider.dart b/lib/exchange/provider/thorchain_exchange.provider.dart
index 32dce7db8..826e203f3 100644
--- a/lib/exchange/provider/thorchain_exchange.provider.dart
+++ b/lib/exchange/provider/thorchain_exchange.provider.dart
@@ -34,11 +34,13 @@ class ThorChainExchangeProvider extends ExchangeProvider {
static final isRefundAddressSupported = [CryptoCurrency.eth];
- static const _baseURL = 'thornode.ninerealms.com';
+ static const _baseNodeURL = 'thornode.ninerealms.com';
+ static const _baseURL = 'midgard.ninerealms.com';
static const _quotePath = '/thorchain/quote/swap';
static const _txInfoPath = '/thorchain/tx/status/';
static const _affiliateName = 'cakewallet';
static const _affiliateBps = '175';
+ static const _nameLookUpPath= 'v2/thorname/lookup/';
final Box tradesStore;
@@ -154,7 +156,7 @@ class ThorChainExchangeProvider extends ExchangeProvider {
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 uri = Uri.https(_baseNodeURL, '$_txInfoPath$formattedId');
final response = await http.get(uri);
if (response.statusCode == 404) {
@@ -206,8 +208,35 @@ class ThorChainExchangeProvider extends ExchangeProvider {
);
}
+ static Future