Merge branch 'main' of https://github.com/cake-tech/cake_wallet into bitcoin-derivations

This commit is contained in:
fosse 2023-10-05 10:01:44 -04:00
commit e9cd76f534
123 changed files with 3653 additions and 766 deletions

View file

@ -2,11 +2,10 @@ name: PR Test Build
on:
pull_request:
branches: [ main ]
branches: [main]
jobs:
PR_test_build:
runs-on: ubuntu-20.04
env:
STORE_PASS: test@cake_wallet
@ -23,12 +22,12 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-java@v1
with:
java-version: '8.x'
java-version: "8.x"
- name: Flutter action
uses: subosito/flutter-action@v1
with:
flutter-version: '3.10.x'
flutter-version: "3.10.x"
channel: stable
- name: Install package dependencies
@ -132,6 +131,7 @@ jobs:
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 walletConnectProjectId = '${{ secrets.WALLET_CONNECT_PROJECT_ID }}';" >> lib/.secrets.g.dart
- name: Rename app
run: echo -e "id=com.cakewallet.test\nname=$GITHUB_HEAD_REF" > /opt/android/cake_wallet/android/app.properties
@ -141,18 +141,18 @@ jobs:
cd /opt/android/cake_wallet
flutter build apk --release
# - name: Push to App Center
# run: |
# echo 'Installing App Center CLI tools'
# npm install -g appcenter-cli
# echo "Publishing test to App Center"
# appcenter distribute release \
# --group "Testers" \
# --file "/opt/android/cake_wallet/build/app/outputs/apk/release/app-release.apk" \
# --release-notes ${GITHUB_HEAD_REF} \
# --app Cake-Labs/Cake-Wallet \
# --token ${{ secrets.APP_CENTER_TOKEN }} \
# --quiet
# - name: Push to App Center
# run: |
# echo 'Installing App Center CLI tools'
# npm install -g appcenter-cli
# echo "Publishing test to App Center"
# appcenter distribute release \
# --group "Testers" \
# --file "/opt/android/cake_wallet/build/app/outputs/apk/release/app-release.apk" \
# --release-notes ${GITHUB_HEAD_REF} \
# --app Cake-Labs/Cake-Wallet \
# --token ${{ secrets.APP_CENTER_TOKEN }} \
# --quiet
- name: Rename apk file
run: |
@ -172,6 +172,6 @@ jobs:
token: ${{ secrets.SLACK_APP_TOKEN }}
path: /opt/android/cake_wallet/build/app/outputs/apk/release/app-release.apk
channel: ${{ secrets.SLACK_APK_CHANNEL }}
title: '${{github.head_ref}}.apk'
title: "${{github.head_ref}}.apk"
filename: ${{github.head_ref}}.apk
initial_comment: ${{ github.event.head_commit.message }}

1
.gitignore vendored
View file

@ -124,6 +124,7 @@ lib/bitcoin/bitcoin.dart
lib/monero/monero.dart
lib/haven/haven.dart
lib/ethereum/ethereum.dart
lib/nano/nano.dart
ios/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_180.png
ios/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_120.png

View file

@ -25,10 +25,6 @@
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize"
android:exported="true">
<meta-data
android:name="io.flutter.embedding.android.SplashScreenDrawable"
android:resource="@drawable/launch_background"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<color android:color="#000000"/> <!-- Dark background color -->
</item>
</layer-list>

View file

@ -1,12 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
<item>
<color android:color="#FFFFFF"/> <!-- Light background color -->
</item>
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

View file

@ -5,4 +5,5 @@
-
uri: workers.perish.co
-
uri: worker.nanoriver.cc:443
uri: worker.nanoriver.cc
useSSL: true

View file

@ -1,3 +1,3 @@
Enhance Monero coin control
Add Filipino localization
Bug Fixes
Fix 2FA code issue
Bug fixes
Minor enhancements

View file

@ -1,5 +1,4 @@
New Buy Provider Robinhood
Fix sending Ethereum issue
Enhance Monero coin control
Add Filipino localization
Bug Fixes
Ethereum enhancements and bug fixes
Fix 2FA code issue
Bug fixes
Minor enhancements

View file

@ -7,4 +7,5 @@ cd cw_monero && flutter pub get && flutter packages pub run build_runner build -
cd cw_bitcoin && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
cd cw_haven && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
cd cw_ethereum && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
cd cw_nano && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
flutter packages pub run build_runner build --delete-conflicting-outputs

View file

@ -2304,4 +2304,4 @@ final englishWordlist = <String>[
'zero',
'zone',
'zoo'
];
];

View file

@ -128,4 +128,4 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
initialRegularAddressIndex: snp.regularAddressIndex,
initialChangeAddressIndex: snp.changeAddressIndex);
}
}
}

View file

@ -31,4 +31,4 @@ class BitcoinRestoreWalletFromWIFCredentials extends WalletCredentials {
: super(name: name, password: password, walletInfo: walletInfo);
final String wif;
}
}

View file

@ -67,4 +67,4 @@ class ElectrumWallletSnapshot {
derivationType: derivationType,
derivationPath: derivationPath);
}
}
}

View file

@ -0,0 +1,23 @@
class AccountInfoResponse {
String frontier;
int confirmationHeight;
String balance;
String representative;
String? address;
AccountInfoResponse({
required this.frontier,
required this.balance,
required this.representative,
required this.confirmationHeight,
});
factory AccountInfoResponse.fromJson(Map<String, dynamic> json) {
return AccountInfoResponse(
frontier: json['frontier'] as String,
representative: json['representative'] as String,
balance: json['balance'] as String,
confirmationHeight: int.parse(json['confirmation_height'] as String),
);
}
}

View file

@ -13,9 +13,7 @@ import 'package:cw_core/sync_status.dart';
import 'package:cw_core/node.dart';
import 'package:cw_core/wallet_type.dart';
abstract class WalletBase<
BalanceType extends Balance,
HistoryType extends TransactionHistoryBase,
abstract class WalletBase<BalanceType extends Balance, HistoryType extends TransactionHistoryBase,
TransactionType extends TransactionInfo> {
WalletBase(this.walletInfo);

View file

@ -17,6 +17,8 @@ enum DerivationType {
@HiveField(3)
bip39,
@HiveField(4)
electrum1,
@HiveField(5)
electrum2,
}
@ -34,8 +36,8 @@ class DerivationInfo {
String balance;
String address;
int height;
DerivationType derivationType;
String? derivationPath;
final DerivationType derivationType;
final String? derivationPath;
final String? script_type;
final String? description;
}

View file

@ -1,9 +1,11 @@
import 'package:cw_core/node.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_credentials.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_type.dart';
abstract class WalletService<N extends WalletCredentials,
RFS extends WalletCredentials, RFK extends WalletCredentials> {
abstract class WalletService<N extends WalletCredentials, RFS extends WalletCredentials,
RFK extends WalletCredentials> {
WalletType getType();
Future<WalletBase> create(N credentials);

View file

@ -208,6 +208,10 @@ I/flutter ( 4474): Gas Used: 53000
}
}
Web3Client? getWeb3Client() {
return _client;
}
// Future<int> _getDecimalPlacesForContract(DeployedContract contract) async {
// final String abi = await rootBundle.loadString("assets/abi_json/erc20_abi.json");
// final contractAbi = ContractAbi.fromJson(abi, "ERC20");

View file

@ -77,6 +77,8 @@ abstract class EthereumWalletBase
late final EthPrivateKey _ethPrivateKey;
EthPrivateKey get ethPrivateKey => _ethPrivateKey;
late EthereumClient _client;
int? _gasPrice;
@ -508,4 +510,6 @@ abstract class EthereumWalletBase
@override
String signMessage(String message, {String? address = null}) =>
bytesToHex(_ethPrivateKey.signPersonalMessageToUint8List(ascii.encode(message)));
Web3Client? getWeb3Client() => _client.getWeb3Client();
}

View file

@ -1,32 +1,13 @@
import 'package:cw_core/balance.dart';
import 'package:cw_core/currency.dart';
import 'package:cw_nano/nano_util.dart';
String rawToFormattedAmount(BigInt amount, Currency currency) {
return "";
}
BigInt stringAmountToBigInt(String amount) {
return BigInt.zero;
}
class BananoBalance extends Balance {
final BigInt currentBalance;
final BigInt receivableBalance;
late String formattedCurrentBalance;
late String formattedReceivableBalance;
BananoBalance({required this.currentBalance, required this.receivableBalance}) : super(0, 0) {
this.formattedCurrentBalance = "";
this.formattedReceivableBalance = "";
}
BananoBalance.fromString(
{required this.formattedCurrentBalance, required this.formattedReceivableBalance})
: currentBalance = stringAmountToBigInt(formattedCurrentBalance),
receivableBalance = stringAmountToBigInt(formattedReceivableBalance),
super(0, 0);
@override
String get formattedAvailableBalance {
return NanoUtil.getRawAsUsableString(currentBalance.toString(), NanoUtil.rawPerBanano);

View file

@ -1,6 +1,4 @@
import 'package:cw_core/balance.dart';
import 'package:cw_core/currency.dart';
import 'package:cw_core/monero_amount_format.dart';
import 'package:cw_nano/nano_util.dart';
BigInt stringAmountToBigInt(String amount) {

View file

@ -1,6 +1,7 @@
import 'dart:async';
import 'dart:convert';
import 'package:cw_core/nano_account_info_response.dart';
import 'package:cw_nano/nano_balance.dart';
import 'package:cw_nano/nano_transaction_model.dart';
import 'package:cw_nano/nano_util.dart';
@ -52,7 +53,7 @@ class NanoClient {
return NanoBalance(currentBalance: cur, receivableBalance: rec);
}
Future<dynamic> getAccountInfo(String address) async {
Future<AccountInfoResponse?> getAccountInfo(String address) async {
try {
final response = await http.post(
_node!.uri,
@ -66,10 +67,10 @@ class NanoClient {
),
);
final data = await jsonDecode(response.body);
return data;
return AccountInfoResponse.fromJson(data as Map<String, dynamic>);
} catch (e) {
print("error while getting account info");
rethrow;
return null;
}
}
@ -79,15 +80,19 @@ class NanoClient {
required String ourAddress,
}) async {
try {
final accountInfo = await getAccountInfo(ourAddress);
AccountInfoResponse? accountInfo = await getAccountInfo(ourAddress);
if (accountInfo == null) {
throw Exception("error while getting account info");
}
// construct the change block:
Map<String, String> changeBlock = {
"type": "state",
"account": ourAddress,
"previous": accountInfo["frontier"] as String,
"previous": accountInfo.frontier,
"representative": repAddress,
"balance": accountInfo["balance"] as String,
"balance": accountInfo.balance,
"link": "0000000000000000000000000000000000000000000000000000000000000000",
"link_as_account": "nano_1111111111111111111111111111111111111111111111111111hifc8npp",
};
@ -104,7 +109,7 @@ class NanoClient {
final String signature = NanoSignatures.signBlock(hash, privateKey);
// get PoW for the send block:
final String work = await requestWork(accountInfo["frontier"] as String);
final String work = await requestWork(accountInfo.frontier);
changeBlock["signature"] = signature;
changeBlock["work"] = work;
@ -116,8 +121,7 @@ class NanoClient {
}
Future<String> requestWork(String hash) async {
return http
.post(
final response = await http.post(
_powNode!.uri,
headers: {'Content-type': 'application/json'},
body: json.encode(
@ -126,18 +130,16 @@ class NanoClient {
"hash": hash,
},
),
)
.then((http.Response response) {
if (response.statusCode == 200) {
final Map<String, dynamic> decoded = json.decode(response.body) as Map<String, dynamic>;
if (decoded.containsKey("error")) {
throw Exception("Received error ${decoded["error"]}");
}
return decoded["work"] as String;
} else {
throw Exception("Received error ${response.statusCode}");
);
if (response.statusCode == 200) {
final Map<String, dynamic> decoded = json.decode(response.body) as Map<String, dynamic>;
if (decoded.containsKey("error")) {
throw Exception("Received error ${decoded["error"]}");
}
});
return decoded["work"] as String;
} else {
throw Exception("Received work error ${response.body}");
}
}
Future<String> send({
@ -197,24 +199,18 @@ class NanoClient {
}
// get the account info (we need the frontier and representative):
final headers = {"Content-Type": "application/json"};
final infoBody = jsonEncode({
"action": "account_info",
"representative": "true",
"account": publicAddress,
});
final infoResponse = await http.post(
_node!.uri,
headers: headers,
body: infoBody,
);
AccountInfoResponse? infoResponse = await getAccountInfo(publicAddress);
if (infoResponse == null) {
throw Exception(
"error while getting account info! (we probably don't have an open account yet)");
}
String frontier = jsonDecode(infoResponse.body)["frontier"].toString();
String frontier = infoResponse.frontier;
// override if provided:
if (previousHash != null) {
frontier = previousHash;
}
final String representative = jsonDecode(infoResponse.body)["representative"].toString();
final String representative = infoResponse.representative;
// link = destination address:
final String link = NanoAccounts.extractPublicKey(destinationAddress);
final String linkAsAccount = destinationAddress;
@ -270,48 +266,27 @@ class NanoClient {
// first check if the account is open:
// get the account info (we need the frontier and representative):
final infoBody = jsonEncode({
"action": "account_info",
"representative": "true",
"account": destinationAddress,
});
final infoResponse = await http.post(
_node!.uri,
headers: headers,
body: infoBody,
);
final infoData = jsonDecode(infoResponse.body);
AccountInfoResponse? infoData = await getAccountInfo(destinationAddress);
String? frontier;
String? representative;
if (infoData["error"] != null) {
if (infoData == null) {
// account is not open yet, we need to create an open block:
openBlock = true;
// we don't have a representative set yet:
representative = DEFAULT_REPRESENTATIVE;
// we don't have a frontier yet:
frontier = "0000000000000000000000000000000000000000000000000000000000000000";
} else {
frontier = infoData.frontier;
representative = infoData.representative;
}
// first get the account balance:
final balanceBody = jsonEncode({
"action": "account_balance",
"account": destinationAddress,
});
final balanceResponse = await http.post(
_node!.uri,
headers: headers,
body: balanceBody,
);
final balanceData = jsonDecode(balanceResponse.body);
final BigInt currentBalance = BigInt.parse(balanceData["balance"].toString());
final BigInt currentBalance = (await getBalance(destinationAddress)).currentBalance;
final BigInt txAmount = BigInt.parse(amountRaw);
final BigInt balanceAfterTx = currentBalance + txAmount;
String frontier = infoData["frontier"].toString();
String representative = infoData["representative"].toString();
if (openBlock) {
// we don't have a representative set yet:
representative = DEFAULT_REPRESENTATIVE;
}
// link = send block hash:
final String link = blockHash;
// this "linkAsAccount" is meaningless:
@ -321,8 +296,7 @@ class NanoClient {
Map<String, String> receiveBlock = {
"type": "state",
"account": destinationAddress,
"previous":
openBlock ? "0000000000000000000000000000000000000000000000000000000000000000" : frontier,
"previous": frontier,
"representative": representative,
"balance": balanceAfterTx.toString(),
"link": link,
@ -422,10 +396,6 @@ class NanoClient {
return blocks.keys.length;
}
Future<dynamic> getTransactionDetails(String transactionHash) async {
throw UnimplementedError();
}
void stop() {}
Future<List<NanoTransactionModel>> fetchTransactions(String address) async {

View file

@ -3,7 +3,6 @@ import 'dart:core';
import 'package:cw_core/pathForWallet.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_nano/file.dart';
import 'package:flutter/material.dart';
import 'package:mobx/mobx.dart';
import 'package:cw_core/transaction_history.dart';
import 'package:cw_nano/nano_transaction_info.dart';

View file

@ -3,18 +3,13 @@ import 'dart:typed_data';
import 'package:convert/convert.dart';
import "package:ed25519_hd_key/ed25519_hd_key.dart";
import 'package:flutter/material.dart';
import 'package:libcrypto/libcrypto.dart';
import 'package:nanodart/nanodart.dart';
import 'package:ed25519_hd_key/ed25519_hd_key.dart';
import 'package:nanodart/nanodart.dart';
import 'package:decimal/decimal.dart';
class NanoUtil {
// standard:
static String seedToPrivate(String seed, int index) {
// return NanoHelpers.byteToHex(Ed25519Blake2b.derivePrivkey(NanoHelpers.hexToBytes(seed), index)!)
// .toUpperCase();
return NanoKeys.seedToPrivate(seed, index);
}
@ -31,10 +26,6 @@ class NanoUtil {
return NanoMnemomics.mnemonicListToSeed(mnemonic.split(' '));
}
// static String createPublicKey(String privateKey) {
// return NanoHelpers.byteToHex(Ed25519Blake2b.getPubkey(NanoHelpers.hexToBytes(privateKey))!);
// }
static String privateKeyToPublic(String privateKey) {
// return NanoHelpers.byteToHex(Ed25519Blake2b.getPubkey(NanoHelpers.hexToBytes(privateKey))!);
return NanoKeys.createPublicKey(privateKey);
@ -105,26 +96,9 @@ class NanoUtil {
}
}
// static String hdSeedToPrivate(String seed, int index) {
// // List<int> seedBytes = hex.decode(seed);
// // KeyData data = await ED25519_HD_KEY.derivePath("m/44'/165'/$index'", seedBytes);
// // return hex.encode(data.key);
// Chain chain = Chain.seed(hex.encode(utf8.encode(seed)));
// ExtendedKey key = chain.forPath("m/44'/165'/$index'");
// print(key.privateKeyHex());
// return "";
// }
// static String hdSeedToAddress(String seed, int index) {
// // return NanoAccounts.createAccount(NanoAccountType.NANO, NanoKeys.createPublicKey(seedToPrivate(seed, index)));
// return "";
// }
static bool isValidBip39Seed(String seed) {
// Ensure seed is 128 characters long
if (seed == null || seed.length != 128) {
if (seed.length != 128) {
return false;
}
// Ensure seed only contains hex characters, 0-9;A-F

View file

@ -3,6 +3,7 @@ import 'dart:io';
import 'package:cw_core/cake_hive.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/nano_account_info_response.dart';
import 'package:cw_core/node.dart';
import 'package:cw_core/pathForWallet.dart';
import 'package:cw_core/pending_transaction.dart';
@ -68,7 +69,7 @@ abstract class NanoWalletBase
String? _representativeAddress;
Timer? _receiveTimer;
late NanoClient _client;
late final NanoClient _client;
bool _isTransactionUpdating;
@override
@ -237,7 +238,6 @@ abstract class NanoWalletBase
_isTransactionUpdating = true;
final transactions = await fetchTransactions();
transactionHistory.clear();
transactionHistory.addMany(transactions);
await transactionHistory.save();
_isTransactionUpdating = false;
@ -281,7 +281,7 @@ abstract class NanoWalletBase
@override
Future<void> rescan({required int height}) async {
fetchTransactions();
updateTransactions();
_updateBalance();
return;
}
@ -376,14 +376,11 @@ abstract class NanoWalletBase
Future<void> _updateRep() async {
try {
final accountInfo = await _client.getAccountInfo(_publicAddress!);
if (accountInfo["error"] != null) {
// account not found:
_representativeAddress = NanoClient.DEFAULT_REPRESENTATIVE;
} else {
_representativeAddress = accountInfo["representative"] as String;
}
AccountInfoResponse accountInfo = (await _client.getAccountInfo(_publicAddress!))!;
_representativeAddress = accountInfo.representative;
} catch (e) {
// account not found:
_representativeAddress = NanoClient.DEFAULT_REPRESENTATIVE;
throw Exception("Failed to get representative address $e");
}
}

View file

@ -3,10 +3,7 @@ import 'package:cw_core/wallet_addresses.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/nano_account.dart';
import 'package:cw_nano/nano_account_list.dart';
// import 'package:cw_core/account.dart';
// import 'package:cw_core/subaddress.dart';
import 'package:mobx/mobx.dart';
import 'package:hive/hive.dart';
part 'nano_wallet_addresses.g.dart';
@ -41,5 +38,13 @@ abstract class NanoWalletAddressesBase extends WalletAddresses with Store {
}
@override
Future<void> updateAddressesInBox() async {}
Future<void> updateAddressesInBox() async {
try {
addressesMap.clear();
addressesMap[address] = '';
await saveAddressesInBox();
} catch (e) {
print(e.toString());
}
}
}

View file

@ -1,12 +1,10 @@
import 'dart:io';
import 'package:cw_core/node.dart';
import 'package:cw_core/pathForWallet.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_service.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:cw_nano/nano_client.dart';
import 'package:cw_nano/nano_mnemonic.dart' as nm;
import 'package:cw_nano/nano_util.dart';
import 'package:cw_nano/nano_wallet.dart';
@ -34,10 +32,6 @@ class NanoWalletService extends WalletService<NanoNewWalletCredentials,
String seedKey = NanoSeeds.generateSeed();
String mnemonic = NanoUtil.seedToMnemonic(seedKey);
// bip39:
// derivationType derivationType = DerivationType.bip39;
// String mnemonic = bip39.generateMnemonic();
credentials.walletInfo!.derivationType = derivationType;
final wallet = NanoWallet(
@ -69,8 +63,6 @@ class NanoWalletService extends WalletService<NanoNewWalletCredentials,
final currentWalletInfo = walletInfoSource.values
.firstWhere((info) => info.id == WalletBase.idFor(currentName, getType()));
currentWalletInfo.derivationType = DerivationType.nano; // doesn't matter for the rename action
String randomWords =
(List<String>.from(nm.NanoMnemomics.WORDLIST)..shuffle()).take(24).join(' ');
final currentWallet =
@ -85,111 +77,6 @@ class NanoWalletService extends WalletService<NanoNewWalletCredentials,
await walletInfoSource.put(currentWalletInfo.key, newWalletInfo);
}
static Future<dynamic> getInfoFromSeedOrMnemonic(
DerivationType derivationType, {
String? seedKey,
String? mnemonic,
required Node node,
}) async {
NanoClient nanoClient = NanoClient();
nanoClient.connect(node);
late String publicAddress;
if (seedKey != null) {
if (derivationType == DerivationType.bip39) {
publicAddress = await NanoUtil.hdSeedToAddress(seedKey, 0);
} else if (derivationType == DerivationType.nano) {
publicAddress = await NanoUtil.seedToAddress(seedKey, 0);
}
}
if (derivationType == DerivationType.bip39) {
if (mnemonic != null) {
seedKey = await NanoUtil.hdMnemonicListToSeed(mnemonic.split(' '));
publicAddress = await NanoUtil.hdSeedToAddress(seedKey, 0);
}
}
if (derivationType == DerivationType.nano) {
if (mnemonic != null) {
seedKey = await NanoUtil.mnemonicToSeed(mnemonic);
publicAddress = await NanoUtil.seedToAddress(seedKey, 0);
}
}
var accountInfo = await nanoClient.getAccountInfo(publicAddress);
accountInfo["address"] = publicAddress;
return accountInfo;
}
static Future<List<DerivationType>> compareDerivationMethods(
{String? mnemonic, String? seedKey, required Node node}) async {
if (mnemonic?.split(' ').length == 12) {
return [DerivationType.bip39];
}
if (seedKey?.length == 128) {
return [DerivationType.bip39];
} else if (seedKey?.length == 64) {
return [DerivationType.nano];
}
late String publicAddressStandard;
late String publicAddressBip39;
try {
NanoClient nanoClient = NanoClient();
nanoClient.connect(node);
if (mnemonic != null) {
seedKey = await NanoUtil.hdMnemonicListToSeed(mnemonic.split(' '));
publicAddressBip39 = await NanoUtil.hdSeedToAddress(seedKey, 0);
seedKey = await NanoUtil.mnemonicToSeed(mnemonic);
publicAddressStandard = await NanoUtil.seedToAddress(seedKey, 0);
} else if (seedKey != null) {
try {
publicAddressBip39 = await NanoUtil.hdSeedToAddress(seedKey, 0);
} catch (e) {
return [DerivationType.nano];
}
try {
publicAddressStandard = await NanoUtil.seedToAddress(seedKey, 0);
} catch (e) {
return [DerivationType.bip39];
}
}
// check if account has a history:
var bip39Info;
var standardInfo;
try {
bip39Info = await nanoClient.getAccountInfo(publicAddressBip39);
} catch (e) {
bip39Info = null;
}
try {
standardInfo = await nanoClient.getAccountInfo(publicAddressStandard);
} catch (e) {
standardInfo = null;
}
// one of these is *probably* null:
if ((bip39Info == null || bip39Info["error"] != null) &&
(standardInfo != null && standardInfo["error"] == null)) {
return [DerivationType.nano];
} else if ((standardInfo == null || standardInfo["error"] != null) &&
(bip39Info != null && bip39Info["error"] == null)) {
return [DerivationType.bip39];
}
// we don't know for sure:
return [DerivationType.nano, DerivationType.bip39];
} catch (e) {
return [DerivationType.unknown];
}
}
@override
Future<NanoWallet> restoreFromKeys(NanoRestoreWalletFromKeysCredentials credentials) async {
if (credentials.seedKey.contains(' ')) {
@ -203,9 +90,20 @@ class NanoWalletService extends WalletService<NanoNewWalletCredentials,
DerivationType derivationType = credentials.derivationType ?? DerivationType.nano;
credentials.walletInfo!.derivationType = derivationType;
String? mnemonic;
// we can't derive the mnemonic from the key in all cases, only if it's a "nano" seed
if (credentials.seedKey.length == 64) {
try {
mnemonic = NanoUtil.seedToMnemonic(credentials.seedKey);
} catch (e) {
throw Exception("Wasn't a valid nano style seed!");
}
}
final wallet = await NanoWallet(
password: credentials.password!,
mnemonic: credentials.seedKey, // we can't derive the mnemonic from the key in all cases
mnemonic: mnemonic ?? credentials.seedKey,
walletInfo: credentials.walletInfo!,
);
await wallet.init();

View file

@ -37,10 +37,10 @@ packages:
dependency: transitive
description:
name: async
sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0
sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
url: "https://pub.dev"
source: hosted
version: "2.10.0"
version: "2.11.0"
bip32:
dependency: "direct main"
description:
@ -141,10 +141,10 @@ packages:
dependency: transitive
description:
name: characters
sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
version: "1.3.0"
checked_yaml:
dependency: transitive
description:
@ -173,10 +173,10 @@ packages:
dependency: transitive
description:
name: collection
sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0
sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c"
url: "https://pub.dev"
source: hosted
version: "1.17.0"
version: "1.17.1"
convert:
dependency: transitive
description:
@ -342,10 +342,10 @@ packages:
dependency: "direct main"
description:
name: http
sha256: "5895291c13fa8a3bd82e76d5627f69e0d85ca6a30dcac95c4ea19a5d555879c2"
sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525"
url: "https://pub.dev"
source: hosted
version: "0.13.6"
version: "1.1.0"
http_multi_server:
dependency: transitive
description:
@ -366,10 +366,10 @@ packages:
dependency: transitive
description:
name: intl
sha256: "910f85bce16fb5c6f614e117efa303e85a1731bb0081edf3604a2ae6e9a3cc91"
sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d"
url: "https://pub.dev"
source: hosted
version: "0.17.0"
version: "0.18.1"
io:
dependency: transitive
description:
@ -382,10 +382,10 @@ packages:
dependency: transitive
description:
name: js
sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7"
sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
url: "https://pub.dev"
source: hosted
version: "0.6.5"
version: "0.6.7"
json_annotation:
dependency: transitive
description:
@ -394,14 +394,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.8.1"
json_rpc_2:
dependency: transitive
description:
name: json_rpc_2
sha256: "5e469bffa23899edacb7b22787780068d650b106a21c76db3c49218ab7ca447e"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
libcrypto:
dependency: "direct main"
description:
@ -422,10 +414,10 @@ packages:
dependency: transitive
description:
name: matcher
sha256: "16db949ceee371e9b99d22f88fa3a73c4e59fd0afed0bd25fc336eb76c198b72"
sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb"
url: "https://pub.dev"
source: hosted
version: "0.12.13"
version: "0.12.15"
material_color_utilities:
dependency: transitive
description:
@ -438,10 +430,10 @@ packages:
dependency: transitive
description:
name: meta
sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42"
sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3"
url: "https://pub.dev"
source: hosted
version: "1.8.0"
version: "1.9.1"
mime:
dependency: transitive
description:
@ -486,10 +478,10 @@ packages:
dependency: transitive
description:
name: path
sha256: db9d4f58c908a4ba5953fcee2ae317c94889433e5024c27ce74a37f94267945b
sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917"
url: "https://pub.dev"
source: hosted
version: "1.8.2"
version: "1.8.3"
path_provider:
dependency: transitive
description:
@ -691,10 +683,10 @@ packages:
dependency: transitive
description:
name: test_api
sha256: ad540f65f92caa91bf21dfc8ffb8c589d6e4dc0c2267818b4cc2792857706206
sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb
url: "https://pub.dev"
source: hosted
version: "0.4.16"
version: "0.5.1"
timing:
dependency: transitive
description:
@ -711,14 +703,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.3.2"
uuid:
dependency: transitive
description:
name: uuid
sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313"
url: "https://pub.dev"
source: hosted
version: "3.0.7"
vector_math:
dependency: transitive
description:
@ -735,14 +719,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.2"
web3dart:
dependency: "direct main"
description:
name: web3dart
sha256: "48b89a5fac0029770a18d1a8bd05ce8431722bacf76184e4301dae05781565e5"
url: "https://pub.dev"
source: hosted
version: "2.3.5"
web_socket_channel:
dependency: transitive
description:
@ -776,5 +752,5 @@ packages:
source: hosted
version: "3.1.2"
sdks:
dart: ">=2.19.0 <3.0.0"
dart: ">=3.0.0 <4.0.0"
flutter: ">=3.3.0"

View file

@ -271,4 +271,4 @@ class AddressValidator extends TextValidator {
return null;
}
}
}
}

View file

@ -0,0 +1,5 @@
abstract class ChainService {
String getNamespace();
String getChainId();
List<String> getEvents();
}

View file

@ -0,0 +1,60 @@
class WCEthereumTransactionModel {
final String from;
final String to;
final String value;
final String? nonce;
final String? gasPrice;
final String? maxFeePerGas;
final String? maxPriorityFeePerGas;
final String? gas;
final String? gasLimit;
final String? data;
WCEthereumTransactionModel({
required this.from,
required this.to,
required this.value,
this.nonce,
this.gasPrice,
this.maxFeePerGas,
this.maxPriorityFeePerGas,
this.gas,
this.gasLimit,
this.data,
});
factory WCEthereumTransactionModel.fromJson(Map<String, dynamic> json) {
return WCEthereumTransactionModel(
from: json['from'] as String,
to: json['to'] as String,
value: json['value'] as String,
nonce: json['nonce'] as String?,
gasPrice: json['gasPrice'] as String?,
maxFeePerGas: json['maxFeePerGas'] as String?,
maxPriorityFeePerGas: json['maxPriorityFeePerGas'] as String?,
gas: json['gas'] as String?,
gasLimit: json['gasLimit'] as String?,
data: json['data'] as String?,
);
}
Map<String, dynamic> toJson() {
return {
'from': from,
'to': to,
'value': value,
'nonce': nonce,
'gasPrice': gasPrice,
'maxFeePerGas': maxFeePerGas,
'maxPriorityFeePerGas': maxPriorityFeePerGas,
'gas': gas,
'gasLimit': gasLimit,
'data': data,
};
}
@override
String toString() {
return 'EthereumTransactionModel(from: $from, to: $to, nonce: $nonce, gasPrice: $gasPrice, maxFeePerGas: $maxFeePerGas, maxPriorityFeePerGas: $maxPriorityFeePerGas, gas: $gas, gasLimit: $gasLimit, value: $value, data: $data)';
}
}

View file

@ -0,0 +1,35 @@
import 'package:cake_wallet/core/wallet_connect/evm_chain_service.dart';
enum EVMChainId {
ethereum,
polygon,
goerli,
mumbai,
arbitrum,
}
extension EVMChainIdX on EVMChainId {
String chain() {
String name = '';
switch (this) {
case EVMChainId.ethereum:
name = '1';
break;
case EVMChainId.polygon:
name = '137';
break;
case EVMChainId.goerli:
name = '5';
break;
case EVMChainId.arbitrum:
name = '42161';
break;
case EVMChainId.mumbai:
name = '80001';
break;
}
return '${EvmChainServiceImpl.namespace}:$name';
}
}

View file

@ -0,0 +1,294 @@
import 'dart:convert';
import 'dart:developer';
import 'dart:typed_data';
import 'package:cake_wallet/core/wallet_connect/eth_transaction_model.dart';
import 'package:cake_wallet/core/wallet_connect/evm_chain_id.dart';
import 'package:cake_wallet/core/wallet_connect/wc_bottom_sheet_service.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/wallet_connect/widgets/error_display_widget.dart';
import 'package:cake_wallet/store/app_store.dart';
import 'package:cake_wallet/core/wallet_connect/models/chain_key_model.dart';
import 'package:cake_wallet/core/wallet_connect/models/connection_model.dart';
import 'package:cake_wallet/src/screens/wallet_connect/widgets/connection_widget.dart';
import 'package:cake_wallet/src/screens/wallet_connect/widgets/modals/web3_request_modal.dart';
import 'package:cake_wallet/src/screens/wallet_connect/utils/string_parsing.dart';
import 'package:convert/convert.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:eth_sig_util/eth_sig_util.dart';
import 'package:eth_sig_util/util/utils.dart';
import 'package:http/http.dart' as http;
import 'package:walletconnect_flutter_v2/walletconnect_flutter_v2.dart';
import 'package:web3dart/web3dart.dart';
import 'chain_service.dart';
import 'wallet_connect_key_service.dart';
class EvmChainServiceImpl implements ChainService {
final AppStore appStore;
final BottomSheetService bottomSheetService;
final Web3Wallet wallet;
final WalletConnectKeyService wcKeyService;
static const namespace = 'eip155';
static const pSign = 'personal_sign';
static const eSign = 'eth_sign';
static const eSignTransaction = 'eth_signTransaction';
static const eSignTypedData = 'eth_signTypedData_v4';
static const eSendTransaction = 'eth_sendTransaction';
final EVMChainId reference;
final Web3Client ethClient;
EvmChainServiceImpl({
required this.reference,
required this.appStore,
required this.wcKeyService,
required this.bottomSheetService,
required this.wallet,
Web3Client? ethClient,
}) : ethClient = ethClient ??
Web3Client(
appStore.settingsStore.getCurrentNode(WalletType.ethereum).uri.toString(),
http.Client(),
) {
for (final String event in getEvents()) {
wallet.registerEventEmitter(chainId: getChainId(), event: event);
}
wallet.registerRequestHandler(
chainId: getChainId(),
method: pSign,
handler: personalSign,
);
wallet.registerRequestHandler(
chainId: getChainId(),
method: eSign,
handler: ethSign,
);
wallet.registerRequestHandler(
chainId: getChainId(),
method: eSignTransaction,
handler: ethSignTransaction,
);
wallet.registerRequestHandler(
chainId: getChainId(),
method: eSendTransaction,
handler: ethSignTransaction,
);
wallet.registerRequestHandler(
chainId: getChainId(),
method: eSignTypedData,
handler: ethSignTypedData,
);
}
@override
String getNamespace() {
return namespace;
}
@override
String getChainId() {
return reference.chain();
}
@override
List<String> getEvents() {
return ['chainChanged', 'accountsChanged'];
}
Future<String?> requestAuthorization(String? text) async {
// Show the bottom sheet
final bool? isApproved = await bottomSheetService.queueBottomSheet(
widget: Web3RequestModal(
child: ConnectionWidget(
title: S.current.signTransaction,
info: [
ConnectionModel(
text: text,
),
],
),
),
) as bool?;
if (isApproved != null && isApproved == false) {
return 'User rejected signature';
}
return null;
}
Future<String> personalSign(String topic, dynamic parameters) async {
log('received personal sign request: $parameters');
final String message;
if (parameters[0] == null) {
message = '';
} else {
message = parameters[0].toString().utf8Message;
}
final String? authError = await requestAuthorization(message);
if (authError != null) {
return authError;
}
try {
// Load the private key
final List<ChainKeyModel> keys = wcKeyService.getKeysForChain(getChainId());
final Credentials credentials = EthPrivateKey.fromHex(keys[0].privateKey);
final String signature = hex.encode(
credentials.signPersonalMessageToUint8List(Uint8List.fromList(utf8.encode(message))),
);
return '0x$signature';
} catch (e) {
log(e.toString());
bottomSheetService.queueBottomSheet(
isModalDismissible: true,
widget: BottomSheetMessageDisplayWidget(
message: '${S.current.errorGettingCredentials} ${e.toString()}',
),
);
return 'Failed: Error while getting credentials';
}
}
Future<String> ethSign(String topic, dynamic parameters) async {
log('received eth sign request: $parameters');
final String message;
if (parameters[1] == null) {
message = '';
} else {
message = parameters[1].toString().utf8Message;
}
final String? authError = await requestAuthorization(message);
if (authError != null) {
return authError;
}
try {
// Load the private key
final List<ChainKeyModel> keys = wcKeyService.getKeysForChain(getChainId());
final EthPrivateKey credentials = EthPrivateKey.fromHex(keys[0].privateKey);
final String signature = hex.encode(
credentials.signPersonalMessageToUint8List(
Uint8List.fromList(utf8.encode(message)),
),
);
log(signature);
return '0x$signature';
} catch (e) {
log('error: ${e.toString()}');
bottomSheetService.queueBottomSheet(
isModalDismissible: true,
widget: BottomSheetMessageDisplayWidget(message: '${S.current.error}: ${e.toString()}'),
);
return 'Failed';
}
}
Future<String> ethSignTransaction(String topic, dynamic parameters) async {
log('received eth sign transaction request: $parameters');
final paramsData = parameters[0] as Map<String, dynamic>;
final message = _convertToReadable(paramsData);
final String? authError = await requestAuthorization(message);
if (authError != null) {
return authError;
}
// Load the private key
final List<ChainKeyModel> keys = wcKeyService.getKeysForChain(getChainId());
final Credentials credentials = EthPrivateKey.fromHex(keys[0].privateKey);
WCEthereumTransactionModel ethTransaction =
WCEthereumTransactionModel.fromJson(parameters[0] as Map<String, dynamic>);
final transaction = Transaction(
from: EthereumAddress.fromHex(ethTransaction.from),
to: EthereumAddress.fromHex(ethTransaction.to),
maxGas: ethTransaction.gasLimit != null ? int.tryParse(ethTransaction.gasLimit ?? "") : null,
gasPrice: ethTransaction.gasPrice != null
? EtherAmount.inWei(BigInt.parse(ethTransaction.gasPrice ?? ""))
: null,
value: EtherAmount.inWei(BigInt.parse(ethTransaction.value)),
data: hexToBytes(ethTransaction.data ?? ""),
nonce: ethTransaction.nonce != null ? int.tryParse(ethTransaction.nonce ?? "") : null,
);
try {
final result = await ethClient.sendTransaction(credentials, transaction);
log('Result: $result');
bottomSheetService.queueBottomSheet(
isModalDismissible: true,
widget: BottomSheetMessageDisplayWidget(
message: S.current.awaitDAppProcessing,
isError: false,
),
);
return result;
} catch (e) {
log('An error has occured while signing transaction: ${e.toString()}');
bottomSheetService.queueBottomSheet(
isModalDismissible: true,
widget: BottomSheetMessageDisplayWidget(
message: '${S.current.errorSigningTransaction}: ${e.toString()}',
),
);
return 'Failed';
}
}
Future<String> ethSignTypedData(String topic, dynamic parameters) async {
log('received eth sign typed data request: $parameters');
final String? data = parameters[1] as String?;
final String? authError = await requestAuthorization(data);
if (authError != null) {
return authError;
}
final List<ChainKeyModel> keys = wcKeyService.getKeysForChain(getChainId());
return EthSigUtil.signTypedData(
privateKey: keys[0].privateKey,
jsonData: data ?? '',
version: TypedDataVersion.V4,
);
}
String _convertToReadable(Map<String, dynamic> data) {
String gas = int.parse((data['gas'] as String).substring(2), radix: 16).toString();
String value = data['value'] != null
? (int.parse((data['value'] as String).substring(2), radix: 16) / 1e18).toString() + ' ETH'
: '0 ETH';
String from = data['from'] as String;
String to = data['to'] as String;
return '''
Gas: $gas\n
Value: $value\n
From: $from\n
To: $to
''';
}
}

View file

@ -0,0 +1,16 @@
import 'package:walletconnect_flutter_v2/walletconnect_flutter_v2.dart';
class AuthRequestModel {
final String iss;
final AuthRequest request;
AuthRequestModel({
required this.iss,
required this.request,
});
@override
String toString() {
return 'AuthRequestModel(iss: $iss, request: $request)';
}
}

View file

@ -0,0 +1,20 @@
import 'dart:async';
import 'package:flutter/cupertino.dart';
class BottomSheetQueueItemModel {
final Widget widget;
final bool isModalDismissible;
final Completer<dynamic> completer;
BottomSheetQueueItemModel({
required this.widget,
required this.completer,
this.isModalDismissible = false,
});
@override
String toString() {
return 'BottomSheetQueueItemModel(widget: $widget, completer: $completer)';
}
}

View file

@ -0,0 +1,16 @@
class ChainKeyModel {
final List<String> chains;
final String privateKey;
final String publicKey;
ChainKeyModel({
required this.chains,
required this.privateKey,
required this.publicKey,
});
@override
String toString() {
return 'ChainKeyModel(chains: $chains, privateKey: $privateKey, publicKey: $publicKey)';
}
}

View file

@ -0,0 +1,18 @@
class ConnectionModel {
final String? title;
final String? text;
final List<String>? elements;
final Map<String, void Function()>? elementActions;
ConnectionModel({
this.title,
this.text,
this.elements,
this.elementActions,
});
@override
String toString() {
return 'WalletConnectRequestModel(title: $title, text: $text, elements: $elements, elementActions: $elementActions)';
}
}

View file

@ -0,0 +1,14 @@
import 'package:walletconnect_flutter_v2/walletconnect_flutter_v2.dart';
class SessionRequestModel {
final ProposalData request;
SessionRequestModel({
required this.request,
});
@override
String toString() {
return 'SessionRequestModel(request: $request)';
}
}

View file

@ -0,0 +1,72 @@
import 'package:cake_wallet/ethereum/ethereum.dart';
import 'package:cake_wallet/core/wallet_connect/models/chain_key_model.dart';
import 'package:cw_core/balance.dart';
import 'package:cw_core/transaction_history.dart';
import 'package:cw_core/transaction_info.dart';
import 'package:cw_core/wallet_base.dart';
abstract class WalletConnectKeyService {
/// Returns a list of all the keys.
List<ChainKeyModel> getKeys();
/// Returns a list of all the chain ids.
List<String> getChains();
/// Returns a list of all the keys for a given chain id.
/// If the chain is not found, returns an empty list.
/// - [chain]: The chain to get the keys for.
List<ChainKeyModel> getKeysForChain(String chain);
/// Returns a list of all the accounts in namespace:chainId:address format.
List<String> getAllAccounts();
}
class KeyServiceImpl implements WalletConnectKeyService {
KeyServiceImpl(this.wallet)
: _keys = [
ChainKeyModel(
chains: [
'eip155:1',
'eip155:5',
'eip155:137',
'eip155:42161',
'eip155:80001',
],
privateKey: ethereum!.getPrivateKey(wallet),
publicKey: ethereum!.getPublicKey(wallet),
),
];
late final WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo> wallet;
late final List<ChainKeyModel> _keys;
@override
List<String> getChains() {
final List<String> chainIds = [];
for (final ChainKeyModel key in _keys) {
chainIds.addAll(key.chains);
}
return chainIds;
}
@override
List<ChainKeyModel> getKeys() => _keys;
@override
List<ChainKeyModel> getKeysForChain(String chain) {
return _keys.where((e) => e.chains.contains(chain)).toList();
}
@override
List<String> getAllAccounts() {
final List<String> accounts = [];
for (final ChainKeyModel key in _keys) {
for (final String chain in key.chains) {
accounts.add('$chain:${key.publicKey}');
}
}
return accounts;
}
}

View file

@ -0,0 +1,43 @@
import 'dart:async';
import 'package:cake_wallet/core/wallet_connect/models/bottom_sheet_queue_item_model.dart';
import 'package:flutter/material.dart';
abstract class BottomSheetService {
abstract final ValueNotifier<BottomSheetQueueItemModel?> currentSheet;
Future<dynamic> queueBottomSheet({
required Widget widget,
bool isModalDismissible = false,
});
void resetCurrentSheet();
}
class BottomSheetServiceImpl implements BottomSheetService {
@override
final ValueNotifier<BottomSheetQueueItemModel?> currentSheet = ValueNotifier(null);
@override
Future<dynamic> queueBottomSheet({
required Widget widget,
bool isModalDismissible = false,
}) async {
// Create the bottom sheet queue item
final completer = Completer<dynamic>();
final queueItem = BottomSheetQueueItemModel(
widget: widget,
completer: completer,
isModalDismissible: isModalDismissible,
);
currentSheet.value = queueItem;
return await completer.future;
}
@override
void resetCurrentSheet() {
currentSheet.value = null;
}
}

View file

@ -0,0 +1,277 @@
import 'dart:async';
import 'dart:developer';
import 'dart:typed_data';
import 'package:cake_wallet/core/wallet_connect/evm_chain_id.dart';
import 'package:cake_wallet/core/wallet_connect/evm_chain_service.dart';
import 'package:cake_wallet/core/wallet_connect/wallet_connect_key_service.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';
import 'package:cake_wallet/core/wallet_connect/models/session_request_model.dart';
import 'package:cake_wallet/src/screens/wallet_connect/widgets/connection_request_widget.dart';
import 'package:cake_wallet/src/screens/wallet_connect/widgets/error_display_widget.dart';
import 'package:cake_wallet/src/screens/wallet_connect/widgets/modals/web3_request_modal.dart';
import 'package:cake_wallet/store/app_store.dart';
import 'package:eth_sig_util/eth_sig_util.dart';
import 'package:flutter/material.dart';
import 'package:mobx/mobx.dart';
import 'package:walletconnect_flutter_v2/walletconnect_flutter_v2.dart';
import 'wc_bottom_sheet_service.dart';
import 'package:cake_wallet/.secrets.g.dart' as secrets;
part 'web3wallet_service.g.dart';
class Web3WalletService = Web3WalletServiceBase with _$Web3WalletService;
abstract class Web3WalletServiceBase with Store {
final AppStore appStore;
final BottomSheetService _bottomSheetHandler;
final WalletConnectKeyService walletKeyService;
late Web3Wallet _web3Wallet;
@observable
bool isInitialized;
/// The list of requests from the dapp
/// Potential types include, but aren't limited to:
/// [SessionProposalEvent], [AuthRequest]
@observable
ObservableList<PairingInfo> pairings;
@observable
ObservableList<SessionData> sessions;
@observable
ObservableList<StoredCacao> auth;
Web3WalletServiceBase(this._bottomSheetHandler, this.walletKeyService, this.appStore)
: pairings = ObservableList<PairingInfo>(),
sessions = ObservableList<SessionData>(),
auth = ObservableList<StoredCacao>(),
isInitialized = false;
@action
void create() {
// Create the web3wallet client
_web3Wallet = Web3Wallet(
core: Core(projectId: secrets.walletConnectProjectId),
metadata: const PairingMetadata(
name: 'Cake Wallet',
description: 'Cake Wallet',
url: 'https://cakewallet.com',
icons: ['https://cakewallet.com/assets/image/cake_logo.png'],
),
);
// Setup our accounts
List<ChainKeyModel> chainKeys = walletKeyService.getKeys();
for (final chainKey in chainKeys) {
for (final chainId in chainKey.chains) {
_web3Wallet.registerAccount(
chainId: chainId,
accountAddress: chainKey.publicKey,
);
}
}
// Setup our listeners
log('Created instance of web3wallet');
_web3Wallet.core.pairing.onPairingInvalid.subscribe(_onPairingInvalid);
_web3Wallet.core.pairing.onPairingCreate.subscribe(_onPairingCreate);
_web3Wallet.core.pairing.onPairingDelete.subscribe(_onPairingDelete);
_web3Wallet.core.pairing.onPairingExpire.subscribe(_onPairingDelete);
_web3Wallet.pairings.onSync.subscribe(_onPairingsSync);
_web3Wallet.onSessionProposal.subscribe(_onSessionProposal);
_web3Wallet.onSessionProposalError.subscribe(_onSessionProposalError);
_web3Wallet.onSessionConnect.subscribe(_onSessionConnect);
_web3Wallet.onAuthRequest.subscribe(_onAuthRequest);
}
@action
Future<void> init() async {
// Await the initialization of the web3wallet
log('Intializing web3wallet');
if (!isInitialized) {
try {
await _web3Wallet.init();
log('Initialized');
isInitialized = true;
} catch (e) {
log('Experimentallllll: $e');
isInitialized = false;
}
}
_refreshPairings();
final newSessions = _web3Wallet.sessions.getAll();
sessions.addAll(newSessions);
final newAuthRequests = _web3Wallet.completeRequests.getAll();
auth.addAll(newAuthRequests);
for (final cId in EVMChainId.values) {
EvmChainServiceImpl(
reference: cId,
appStore: appStore,
wcKeyService: walletKeyService,
bottomSheetService: _bottomSheetHandler,
wallet: _web3Wallet,
);
}
}
@action
FutureOr<void> onDispose() {
log('web3wallet dispose');
_web3Wallet.core.pairing.onPairingInvalid.unsubscribe(_onPairingInvalid);
_web3Wallet.pairings.onSync.unsubscribe(_onPairingsSync);
_web3Wallet.onSessionProposal.unsubscribe(_onSessionProposal);
_web3Wallet.onSessionProposalError.unsubscribe(_onSessionProposalError);
_web3Wallet.onSessionConnect.unsubscribe(_onSessionConnect);
_web3Wallet.onAuthRequest.unsubscribe(_onAuthRequest);
_web3Wallet.core.pairing.onPairingDelete.unsubscribe(_onPairingDelete);
_web3Wallet.core.pairing.onPairingExpire.unsubscribe(_onPairingDelete);
}
Web3Wallet getWeb3Wallet() {
return _web3Wallet;
}
void _onPairingsSync(StoreSyncEvent? args) {
if (args != null) {
_refreshPairings();
}
}
void _onPairingDelete(PairingEvent? event) {
_refreshPairings();
}
@action
void _refreshPairings() {
pairings.clear();
final allPairings = _web3Wallet.pairings.getAll();
pairings.addAll(allPairings);
}
Future<void> _onSessionProposalError(SessionProposalErrorEvent? args) async {
log(args.toString());
}
void _onSessionProposal(SessionProposalEvent? args) async {
if (args != null) {
final Widget modalWidget = Web3RequestModal(
child: ConnectionRequestWidget(
wallet: _web3Wallet,
sessionProposal: SessionRequestModel(request: args.params),
),
);
// show the bottom sheet
final bool? isApproved = await _bottomSheetHandler.queueBottomSheet(
widget: modalWidget,
) as bool?;
if (isApproved != null && isApproved) {
_web3Wallet.approveSession(
id: args.id,
namespaces: args.params.generatedNamespaces!,
);
} else {
_web3Wallet.rejectSession(
id: args.id,
reason: Errors.getSdkError(
Errors.USER_REJECTED,
),
);
}
}
}
@action
void _onPairingInvalid(PairingInvalidEvent? args) {
log('Pairing Invalid Event: $args');
_bottomSheetHandler.queueBottomSheet(
isModalDismissible: true,
widget: BottomSheetMessageDisplayWidget(message: '${S.current.pairingInvalidEvent}: $args'),
);
}
void _onPairingCreate(PairingEvent? args) {
log('Pairing Create Event: $args');
}
@action
void _onSessionConnect(SessionConnect? args) {
if (args != null) {
sessions.add(args.session);
}
}
@action
Future<void> _onAuthRequest(AuthRequest? args) async {
if (args != null) {
List<ChainKeyModel> chainKeys = walletKeyService.getKeysForChain('eip155:1');
// Create the message to be signed
final String iss = 'did:pkh:eip155:1:${chainKeys.first.publicKey}';
final Widget modalWidget = Web3RequestModal(
child: ConnectionRequestWidget(
wallet: _web3Wallet,
authRequest: AuthRequestModel(iss: iss, request: args),
),
);
final bool? isAuthenticated = await _bottomSheetHandler.queueBottomSheet(
widget: modalWidget,
) as bool?;
if (isAuthenticated != null && isAuthenticated) {
final String message = _web3Wallet.formatAuthMessage(
iss: iss,
cacaoPayload: CacaoRequestPayload.fromPayloadParams(
args.payloadParams,
),
);
final String sig = EthSigUtil.signPersonalMessage(
message: Uint8List.fromList(message.codeUnits),
privateKey: chainKeys.first.privateKey,
);
await _web3Wallet.respondAuthRequest(
id: args.id,
iss: iss,
signature: CacaoSignature(
t: CacaoSignature.EIP191,
s: sig,
),
);
} else {
await _web3Wallet.respondAuthRequest(
id: args.id,
iss: iss,
error: Errors.getSdkError(
Errors.USER_REJECTED_AUTH,
),
);
}
}
}
@action
Future<void> disconnectSession(String topic) async {
final session = sessions.firstWhere((element) => element.pairingTopic == topic);
await _web3Wallet.core.pairing.disconnect(topic: topic);
await _web3Wallet.disconnectSession(
topic: session.topic, reason: Errors.getSdkError(Errors.USER_DISCONNECTED));
}
@action
List<SessionData> getSessionsForPairingInfo(PairingInfo pairing) {
return sessions.where((element) => element.pairingTopic == pairing.topic).toList();
}
}

View file

@ -3,10 +3,12 @@ import 'package:cake_wallet/anonpay/anonpay_info_base.dart';
import 'package:cake_wallet/anonpay/anonpay_invoice_info.dart';
import 'package:cake_wallet/buy/onramper/onramper_buy_provider.dart';
import 'package:cake_wallet/buy/payfura/payfura_buy_provider.dart';
import 'package:cake_wallet/core/wallet_connect/wallet_connect_key_service.dart';
import 'package:cake_wallet/core/wallet_connect/wc_bottom_sheet_service.dart';
import 'package:cake_wallet/buy/robinhood/robinhood_buy_provider.dart';
import 'package:cake_wallet/core/wallet_connect/web3wallet_service.dart';
import 'package:cake_wallet/core/yat_service.dart';
import 'package:cake_wallet/entities/background_tasks.dart';
import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart';
import 'package:cake_wallet/entities/exchange_api_mode.dart';
import 'package:cake_wallet/entities/parse_address_from_domain.dart';
import 'package:cake_wallet/entities/receive_page_option.dart';
@ -80,7 +82,6 @@ import 'package:cake_wallet/view_model/ionia/ionia_gift_cards_list_view_model.da
import 'package:cake_wallet/view_model/ionia/ionia_purchase_merch_view_model.dart';
import 'package:cake_wallet/view_model/nano_account_list/nano_account_edit_or_create_view_model.dart';
import 'package:cake_wallet/view_model/nano_account_list/nano_account_list_view_model.dart';
import 'package:cake_wallet/view_model/node_list/pow_node_create_or_edit_view_model.dart';
import 'package:cake_wallet/view_model/node_list/pow_node_list_view_model.dart';
import 'package:cake_wallet/view_model/set_up_2fa_viewmodel.dart';
import 'package:cake_wallet/view_model/restore/restore_from_qr_vm.dart';
@ -418,6 +419,10 @@ Future<void> setup({
}
if (appStore.wallet != null) {
authStore.allowed();
if (appStore.wallet!.type == WalletType.ethereum) {
getIt.get<Web3WalletService>().init();
}
return;
}
@ -438,6 +443,10 @@ Future<void> setup({
} else {
if (appStore.wallet != null) {
authStore.allowed();
if (appStore.wallet!.type == WalletType.ethereum) {
getIt.get<Web3WalletService>().init();
}
return;
}
@ -457,11 +466,28 @@ Future<void> setup({
}, closable: false);
}, instanceName: 'login');
getIt.registerSingleton<BottomSheetService>(BottomSheetServiceImpl());
final appStore = getIt.get<AppStore>();
getIt.registerLazySingleton<WalletConnectKeyService>(() => KeyServiceImpl(appStore.wallet!));
getIt.registerLazySingleton<Web3WalletService>(() {
final Web3WalletService web3WalletService = Web3WalletService(
getIt.get<BottomSheetService>(),
getIt.get<WalletConnectKeyService>(),
appStore,
);
web3WalletService.create();
return web3WalletService;
});
getIt.registerFactory(() => BalancePage(
dashboardViewModel: getIt.get<DashboardViewModel>(),
settingsStore: getIt.get<SettingsStore>()));
getIt.registerFactory<DashboardPage>(() => DashboardPage(
bottomSheetService: getIt.get<BottomSheetService>(),
balancePage: getIt.get<BalancePage>(),
dashboardViewModel: getIt.get<DashboardViewModel>(),
addressListViewModel: getIt.get<WalletAddressListViewModel>(),
@ -478,6 +504,7 @@ Future<void> setup({
});
getIt.registerFactoryParam<DesktopDashboardPage, GlobalKey<NavigatorState>, void>(
(desktopKey, _) => DesktopDashboardPage(
bottomSheetService: getIt.get<BottomSheetService>(),
balancePage: getIt.get<BalancePage>(),
dashboardViewModel: getIt.get<DashboardViewModel>(),
addressListViewModel: getIt.get<WalletAddressListViewModel>(),
@ -603,37 +630,22 @@ Future<void> setup({
editingWallet: editingWallet);
});
// getIt.registerFactory(() {
// final wallet = getIt.get<AppStore>().wallet!;
// if (wallet.type == WalletType.monero || wallet.type == WalletType.haven) {
// return MoneroAccountListViewModel(wallet);
// }
// if (wallet.type == WalletType.nano || wallet.type == WalletType.banano) {
// return NanoAccountListViewModel(wallet);
// }
// throw Exception(
// 'Unexpected wallet type: ${wallet.type} for generate Nano/Monero AccountListViewModel');
// });
getIt.registerFactory(() {
getIt.registerFactory<NanoAccountListViewModel>(() {
final wallet = getIt.get<AppStore>().wallet!;
if (wallet.type == WalletType.nano || wallet.type == WalletType.banano) {
return NanoAccountListViewModel(wallet);
}
throw Exception(
'Unexpected wallet type: ${wallet.type} for generate Nano/Monero AccountListViewModel');
'Unexpected wallet type: ${wallet.type} for generate Nano/Banano AccountListViewModel');
});
getIt.registerFactory(() {
getIt.registerFactory<MoneroAccountListViewModel>(() {
final wallet = getIt.get<AppStore>().wallet!;
if (wallet.type == WalletType.monero || wallet.type == WalletType.haven) {
return MoneroAccountListViewModel(wallet);
}
throw Exception(
'Unexpected wallet type: ${wallet.type} for generate Nano/Monero AccountListViewModel');
'Unexpected wallet type: ${wallet.type} for generate Monero AccountListViewModel');
});
getIt.registerFactory(
@ -729,7 +741,13 @@ Future<void> setup({
return PowNodeListViewModel(_powNodeSource, appStore);
});
getIt.registerFactory(() => ConnectionSyncPage(getIt.get<DashboardViewModel>()));
getIt.registerFactory(() {
final wallet = getIt.get<AppStore>().wallet;
return ConnectionSyncPage(
getIt.get<DashboardViewModel>(),
wallet?.type == WalletType.ethereum ? getIt.get<Web3WalletService>() : null,
);
});
getIt.registerFactory(
() => SecurityBackupPage(getIt.get<SecuritySettingsViewModel>(), getIt.get<AuthService>()));
@ -740,11 +758,13 @@ Future<void> setup({
getIt.registerFactory(() => OtherSettingsPage(getIt.get<OtherSettingsViewModel>()));
getIt.registerFactory(() => NanoChangeRepPage());
getIt.registerFactory(() => NanoChangeRepPage(getIt.get<AppStore>().wallet!));
getIt.registerFactoryParam<NodeCreateOrEditViewModel, WalletType?, void>((WalletType? type, _) =>
NodeCreateOrEditViewModel(
_nodeSource, type ?? getIt.get<AppStore>().wallet!.type, getIt.get<SettingsStore>()));
getIt.registerFactoryParam<NodeCreateOrEditViewModel, WalletType?, bool?>(
(WalletType? type, bool? isPow) => NodeCreateOrEditViewModel(
(isPow ?? false) ? _powNodeSource : _nodeSource,
type ?? getIt.get<AppStore>().wallet!.type,
getIt.get<SettingsStore>()));
getIt.registerFactoryParam<PowNodeCreateOrEditViewModel, WalletType?, void>(
(WalletType? type, _) => PowNodeCreateOrEditViewModel(
@ -752,7 +772,13 @@ Future<void> setup({
getIt.registerFactoryParam<NodeCreateOrEditPage, Node?, bool?>(
(Node? editingNode, bool? isSelected) => NodeCreateOrEditPage(
nodeCreateOrEditViewModel: getIt.get<NodeCreateOrEditViewModel>(),
nodeCreateOrEditViewModel: getIt.get<NodeCreateOrEditViewModel>(param2: false),
editingNode: editingNode,
isSelected: isSelected));
getIt.registerFactoryParam<PowNodeCreateOrEditPage, Node?, bool?>(
(Node? editingNode, bool? isSelected) => PowNodeCreateOrEditPage(
nodeCreateOrEditViewModel: getIt.get<NodeCreateOrEditViewModel>(param2: true),
editingNode: editingNode,
isSelected: isSelected));
@ -844,10 +870,9 @@ Future<void> setup({
(type, _) => WalletRestorePage(getIt.get<WalletRestoreViewModel>(param1: type)));
getIt.registerFactoryParam<WalletRestoreChooseDerivationViewModel, List<DerivationInfo>, void>(
(derivations, _) =>
WalletRestoreChooseDerivationViewModel(derivationInfos: derivations));
(derivations, _) => WalletRestoreChooseDerivationViewModel(derivationInfos: derivations));
getIt.registerFactoryParam<WalletRestoreChooseDerivationPage, dynamic, void>(
getIt.registerFactoryParam<WalletRestoreChooseDerivationPage, List<DerivationInfo>, void>(
(credentials, _) =>
WalletRestoreChooseDerivationPage(getIt.get<WalletRestoreChooseDerivationViewModel>(
param1: credentials,
@ -915,7 +940,7 @@ Future<void> setup({
wallet: wallet!);
});
getIt.registerFactoryParam<BuyWebViewPage, List, void>((List args, _) {
getIt.registerFactoryParam<BuyWebViewPage, List<dynamic>, void>((List<dynamic> args, _) {
final url = args.first as String;
final buyViewModel = args[1] as BuyViewModel;
@ -936,16 +961,15 @@ Future<void> setup({
getIt.registerFactory(() => SupportPage(getIt.get<SupportViewModel>()));
getIt.registerFactory(() =>
SupportChatPage(
getIt.get<SupportViewModel>(), secureStorage: getIt.get<FlutterSecureStorage>()));
getIt.registerFactory(() => SupportChatPage(getIt.get<SupportViewModel>(),
secureStorage: getIt.get<FlutterSecureStorage>()));
getIt.registerFactory(() => SupportOtherLinksPage(getIt.get<SupportViewModel>()));
getIt.registerFactory(() {
final wallet = getIt.get<AppStore>().wallet;
return UnspentCoinsListViewModel(wallet: wallet!, unspentCoinsInfo: _unspentCoinsInfoSource!);
return UnspentCoinsListViewModel(wallet: wallet!, unspentCoinsInfo: _unspentCoinsInfoSource);
});
getIt.registerFactory(() =>
@ -956,7 +980,7 @@ Future<void> setup({
(item, model) =>
UnspentCoinsDetailsViewModel(unspentCoinsItem: item, unspentCoinsListViewModel: model));
getIt.registerFactoryParam<UnspentCoinsDetailsPage, List, void>((List args, _) {
getIt.registerFactoryParam<UnspentCoinsDetailsPage, List<dynamic>, void>((List<dynamic> args, _) {
final item = args.first as UnspentCoinsItem;
final unspentCoinsListViewModel = args[1] as UnspentCoinsListViewModel;
@ -967,8 +991,8 @@ Future<void> setup({
getIt.registerFactory(() => YatService());
getIt.registerFactory(() => AddressResolver(
yatService: getIt.get<YatService>(), walletType: getIt.get<AppStore>().wallet!.type));
getIt.registerFactory(() =>
AddressResolver(yatService: getIt.get<YatService>(), wallet: getIt.get<AppStore>().wallet!));
getIt.registerFactoryParam<FullscreenQRPage, QrViewData, void>(
(QrViewData viewData, _) => FullscreenQRPage(qrViewData: viewData));
@ -1009,7 +1033,7 @@ Future<void> setup({
getIt.registerFactory(() => IoniaLoginPage(getIt.get<IoniaAuthViewModel>()));
getIt.registerFactoryParam<IoniaVerifyIoniaOtp, List, void>((List args, _) {
getIt.registerFactoryParam<IoniaVerifyIoniaOtp, List<dynamic>, void>((List<dynamic> args, _) {
final email = args.first as String;
final isSignIn = args[1] as bool;
@ -1018,13 +1042,14 @@ Future<void> setup({
getIt.registerFactory(() => IoniaWelcomePage());
getIt.registerFactoryParam<IoniaBuyGiftCardPage, List, void>((List args, _) {
getIt.registerFactoryParam<IoniaBuyGiftCardPage, List<dynamic>, void>((List<dynamic> args, _) {
final merchant = args.first as IoniaMerchant;
return IoniaBuyGiftCardPage(getIt.get<IoniaBuyCardViewModel>(param1: merchant));
});
getIt.registerFactoryParam<IoniaBuyGiftCardDetailPage, List, void>((List args, _) {
getIt.registerFactoryParam<IoniaBuyGiftCardDetailPage, List<dynamic>, void>(
(List<dynamic> args, _) {
final amount = args.first as double;
final merchant = args.last as IoniaMerchant;
return IoniaBuyGiftCardDetailPage(
@ -1037,7 +1062,7 @@ Future<void> setup({
ioniaService: getIt.get<IoniaService>(), giftCard: giftCard);
});
getIt.registerFactoryParam<IoniaCustomTipViewModel, List, void>((List args, _) {
getIt.registerFactoryParam<IoniaCustomTipViewModel, List<dynamic>, void>((List<dynamic> args, _) {
final amount = args[0] as double;
final merchant = args[1] as IoniaMerchant;
final tip = args[2] as IoniaTip;
@ -1050,7 +1075,7 @@ Future<void> setup({
return IoniaGiftCardDetailPage(getIt.get<IoniaGiftCardDetailsViewModel>(param1: giftCard));
});
getIt.registerFactoryParam<IoniaMoreOptionsPage, List, void>((List args, _) {
getIt.registerFactoryParam<IoniaMoreOptionsPage, List<dynamic>, void>((List<dynamic> args, _) {
final giftCard = args.first as IoniaGiftCard;
return IoniaMoreOptionsPage(giftCard);
@ -1060,13 +1085,13 @@ Future<void> setup({
(IoniaGiftCard giftCard, _) =>
IoniaCustomRedeemViewModel(giftCard: giftCard, ioniaService: getIt.get<IoniaService>()));
getIt.registerFactoryParam<IoniaCustomRedeemPage, List, void>((List args, _) {
getIt.registerFactoryParam<IoniaCustomRedeemPage, List<dynamic>, void>((List<dynamic> args, _) {
final giftCard = args.first as IoniaGiftCard;
return IoniaCustomRedeemPage(getIt.get<IoniaCustomRedeemViewModel>(param1: giftCard));
});
getIt.registerFactoryParam<IoniaCustomTipPage, List, void>((List args, _) {
getIt.registerFactoryParam<IoniaCustomTipPage, List<dynamic>, void>((List<dynamic> args, _) {
return IoniaCustomTipPage(getIt.get<IoniaCustomTipViewModel>(param1: args));
});
@ -1133,9 +1158,12 @@ Future<void> setup({
),
);
getIt.registerFactory<ManageNodesPage>(() => ManageNodesPage(getIt.get<NodeListViewModel>()));
getIt.registerFactory<ManagePowNodesPage>(
() => ManagePowNodesPage(getIt.get<PowNodeListViewModel>()));
getIt.registerFactoryParam<ManageNodesPage, bool, void>((bool isPow, _) {
if (isPow) {
return ManageNodesPage(isPow, powNodeListViewModel: getIt.get<PowNodeListViewModel>());
}
return ManageNodesPage(isPow, nodeListViewModel: getIt.get<NodeListViewModel>());
});
_isSetupFinished = true;
}

View file

@ -26,8 +26,8 @@ const cakeWalletBitcoinElectrumUri = 'electrum.cakewallet.com:50002';
const cakeWalletLitecoinElectrumUri = 'ltc-electrum.cakewallet.com:50002';
const havenDefaultNodeUri = 'nodes.havenprotocol.org:443';
const ethereumDefaultNodeUri = 'ethereum.publicnode.com';
const nanoDefaultNodeUri = 'rpc.nano.to:443';
const nanoDefaultPowNodeUri = 'rpc.nano.to:443';
const nanoDefaultNodeUri = 'rpc.nano.to';
const nanoDefaultPowNodeUri = 'rpc.nano.to';
Future<void> defaultSettingsMigration(
{required int version,
@ -52,6 +52,8 @@ Future<void> defaultSettingsMigration(
await sharedPreferences.setBool(PreferencesKey.isNewInstall, isNewInstall);
await sharedPreferences.setBool(PreferencesKey.isNewInstall, isNewInstall);
final currentVersion =
sharedPreferences.getInt(PreferencesKey.currentDefaultSettingsMigrationVersion) ?? 0;
@ -161,10 +163,10 @@ Future<void> defaultSettingsMigration(
break;
case 22:
await addNanoNodeList(nodes: nodes);
await addNanoPowNodeList(nodes: powNodes);
await changeNanoCurrentNodeToDefault(sharedPreferences: sharedPreferences, nodes: nodes);
await changeNanoCurrentPowNodeToDefault(
sharedPreferences: sharedPreferences, nodes: powNodes);
await resetPowToDefault(powNodes);
break;
default:
@ -516,7 +518,7 @@ Future<void> checkCurrentNodes(
final currentNanoNodeServer =
nodeSource.values.firstWhereOrNull((node) => node.key == currentNanoNodeId);
final currentNanoPowNodeServer =
nodeSource.values.firstWhereOrNull((node) => node.key == currentNanoPowNodeId);
powNodeSource.values.firstWhereOrNull((node) => node.key == currentNanoPowNodeId);
if (currentMoneroNode == null) {
final newCakeWalletNode = Node(uri: newCakeWalletMoneroUri, type: WalletType.monero);
@ -551,7 +553,7 @@ Future<void> checkCurrentNodes(
}
if (currentNanoNodeServer == null) {
final node = Node(uri: nanoDefaultNodeUri, type: WalletType.nano);
final node = Node(uri: nanoDefaultNodeUri, useSSL: true, type: WalletType.nano);
await nodeSource.add(node);
await sharedPreferences.setInt(PreferencesKey.currentNanoNodeIdKey, node.key as int);
}
@ -560,7 +562,7 @@ Future<void> checkCurrentNodes(
Node? node = powNodeSource.values
.firstWhereOrNull((node) => node.uri.toString() == nanoDefaultPowNodeUri);
if (node == null) {
node = Node(uri: nanoDefaultPowNodeUri, type: WalletType.nano);
node = Node(uri: nanoDefaultPowNodeUri, useSSL: true, type: WalletType.nano);
await powNodeSource.add(node);
}
await sharedPreferences.setInt(PreferencesKey.currentNanoPowNodeIdKey, node.key as int);
@ -636,6 +638,15 @@ Future<void> addNanoNodeList({required Box<Node> nodes}) async {
}
}
Future<void> addNanoPowNodeList({required Box<Node> nodes}) async {
final nodeList = await loadDefaultNanoPowNodes();
for (var node in nodeList) {
if (nodes.values.firstWhereOrNull((element) => element.uriRaw == node.uriRaw) == null) {
await nodes.add(node);
}
}
}
Future<void> changeNanoCurrentNodeToDefault(
{required SharedPreferences sharedPreferences, required Box<Node> nodes}) async {
final node = getNanoDefaultNode(nodes: nodes);

View file

@ -0,0 +1,46 @@
import 'package:cake_wallet/ethereum/ethereum.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:ens_dart/ens_dart.dart';
import 'package:http/http.dart';
import 'package:web3dart/web3dart.dart';
class EnsRecord {
static Future<String> fetchEnsAddress(String name, {WalletBase? wallet}) async {
Web3Client? _client;
if (wallet != null && wallet.type == WalletType.ethereum) {
_client = ethereum!.getWeb3Client(wallet);
}
if (_client == null) {
_client = Web3Client("https://ethereum.publicnode.com", Client());
}
try {
final ens = Ens(client: _client);
if (wallet != null) {
switch (wallet.type) {
case WalletType.monero:
return await ens.withName(name).getCoinAddress(CoinType.XMR);
case WalletType.bitcoin:
return await ens.withName(name).getCoinAddress(CoinType.BTC);
case WalletType.litecoin:
return await ens.withName(name).getCoinAddress(CoinType.LTC);
case WalletType.haven:
return await ens.withName(name).getCoinAddress(CoinType.XHV);
case WalletType.ethereum:
default:
return (await ens.withName(name).getAddress()).hex;
}
}
final addr = await ens.withName(name).getAddress();
return addr.hex;
} catch (e) {
print(e);
return "";
}
}
}

View file

@ -1,19 +1,23 @@
import 'package:cake_wallet/core/address_validator.dart';
import 'package:cake_wallet/core/yat_service.dart';
import 'package:cake_wallet/entities/ens_record.dart';
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/mastodon/mastodon_api.dart';
import 'package:cake_wallet/twitter/twitter_api.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/entities/fio_address_provider.dart';
class AddressResolver {
AddressResolver({required this.yatService, required this.walletType});
AddressResolver({required this.yatService, required this.wallet}) : walletType = wallet.type;
final YatService yatService;
final WalletType walletType;
final WalletBase wallet;
static const unstoppableDomains = [
'crypto',
@ -63,13 +67,47 @@ class AddressResolver {
});
final userTweetsText = subString.toString();
final addressFromPinnedTweet =
extractAddressByType(raw: userTweetsText, type: CryptoCurrency.fromString(ticker));
extractAddressByType(raw: userTweetsText, type: CryptoCurrency.fromString(ticker));
if (addressFromPinnedTweet != null) {
return ParsedAddress.fetchTwitterAddress(address: addressFromPinnedTweet, name: text);
}
}
}
if (text.startsWith('@') && text.contains('@', 1) && text.contains('.', 1)) {
final subText = text.substring(1);
final hostNameIndex = subText.indexOf('@');
final hostName = subText.substring(hostNameIndex + 1);
final userName = subText.substring(0, hostNameIndex);
final mastodonUser =
await MastodonAPI.lookupUserByUserName(userName: userName, apiHost: hostName);
if (mastodonUser != null) {
String? addressFromBio =
extractAddressByType(raw: mastodonUser.note, type: CryptoCurrency.fromString(ticker));
if (addressFromBio != null) {
return ParsedAddress.fetchMastodonAddress(address: addressFromBio, name: text);
} else {
final pinnedPosts =
await MastodonAPI.getPinnedPosts(userId: mastodonUser.id, apiHost: hostName);
if (pinnedPosts.isNotEmpty) {
final userPinnedPostsText = pinnedPosts.map((item) => item.content).join('\n');
String? addressFromPinnedPost = extractAddressByType(
raw: userPinnedPostsText, type: CryptoCurrency.fromString(ticker));
if (addressFromPinnedPost != null) {
return ParsedAddress.fetchMastodonAddress(
address: addressFromPinnedPost, name: text);
}
}
}
}
}
if (!text.startsWith('@') && text.contains('@') && !text.contains('.')) {
final bool isFioRegistered = await FioAddressProvider.checkAvail(text);
if (isFioRegistered) {
@ -96,6 +134,13 @@ class AddressResolver {
return ParsedAddress.fetchUnstoppableDomainAddress(address: address, name: text);
}
if (text.endsWith(".eth")) {
final address = await EnsRecord.fetchEnsAddress(text, wallet: wallet);
if (address.isNotEmpty && address != "0x0000000000000000000000000000000000000000") {
return ParsedAddress.fetchEnsAddress(name: text, address: address);
}
}
if (formattedName.contains(".")) {
final txtRecord = await OpenaliasRecord.lookupOpenAliasRecord(formattedName);
if (txtRecord != null) {

View file

@ -1,7 +1,8 @@
import 'package:cake_wallet/entities/openalias_record.dart';
import 'package:cake_wallet/entities/yat_record.dart';
enum ParseFrom { unstoppableDomains, openAlias, yatRecord, fio, notParsed, twitter, contact }
enum ParseFrom { unstoppableDomains, openAlias, yatRecord, fio, notParsed, twitter, ens, contact, mastodon }
class ParsedAddress {
ParsedAddress({
@ -69,6 +70,14 @@ class ParsedAddress {
);
}
factory ParsedAddress.fetchMastodonAddress({required String address, required String name}){
return ParsedAddress(
addresses: [address],
name: name,
parseFrom: ParseFrom.mastodon
);
}
factory ParsedAddress.fetchContactAddress({required String address, required String name}){
return ParsedAddress(
addresses: [address],
@ -77,8 +86,17 @@ class ParsedAddress {
);
}
factory ParsedAddress.fetchEnsAddress({required String address, required String name}) {
return ParsedAddress(
addresses: [address],
name: name,
parseFrom: ParseFrom.ens,
);
}
final List<String> addresses;
final String name;
final String description;
final ParseFrom parseFrom;
}

View file

@ -19,8 +19,7 @@ class PreferencesKey {
static const disableSellKey = 'disable_sell';
static const defaultBuyProvider = 'default_buy_provider';
static const currentFiatApiModeKey = 'current_fiat_api_mode';
static const allowBiometricalAuthenticationKey =
'allow_biometrical_authentication';
static const allowBiometricalAuthenticationKey = 'allow_biometrical_authentication';
static const useTOTP2FA = 'use_totp_2fa';
static const failedTotpTokenTrials = 'failed_token_trials';
static const disableExchangeKey = 'disable_exchange';
@ -58,8 +57,7 @@ class PreferencesKey {
static const clearnetDonationLink = 'clearnet_donation_link';
static const onionDonationLink = 'onion_donation_link';
static const lastSeenAppVersion = 'last_seen_app_version';
static const shouldShowMarketPlaceInDashboard =
'should_show_marketplace_in_dashboard';
static const shouldShowMarketPlaceInDashboard = 'should_show_marketplace_in_dashboard';
static const isNewInstall = 'is_new_install';
static const shouldRequireTOTP2FAForAccessingWallet =
'should_require_totp_2fa_for_accessing_wallets';

View file

@ -33,6 +33,20 @@ class CWEthereum extends Ethereum {
@override
String getAddress(WalletBase wallet) => (wallet as EthereumWallet).walletAddresses.address;
@override
String getPrivateKey(WalletBase wallet) {
final privateKeyHolder = (wallet as EthereumWallet).ethPrivateKey;
String stringKey = bytesToHex(privateKeyHolder.privateKey);
return stringKey;
}
@override
String getPublicKey(WalletBase wallet) {
final privateKeyInUnitInt = (wallet as EthereumWallet).ethPrivateKey;
final publicKey = privateKeyInUnitInt.address.hex;
return publicKey;
}
@override
TransactionPriority getDefaultTransactionPriority() => EthereumTransactionPriority.medium;
@ -131,4 +145,9 @@ class CWEthereum extends Ethereum {
void updateEtherscanUsageState(WalletBase wallet, bool isEnabled) {
(wallet as EthereumWallet).updateEtherscanUsageState(isEnabled);
}
}
@override
Web3Client? getWeb3Client(WalletBase wallet) {
return (wallet as EthereumWallet).getWeb3Client();
}
}

View file

@ -39,7 +39,6 @@ import 'package:cake_wallet/src/screens/root/root.dart';
import 'package:uni_links/uni_links.dart';
import 'package:cw_core/unspent_coins_info.dart';
import 'package:cake_wallet/monero/monero.dart';
import 'package:cake_wallet/wallet_type_utils.dart';
import 'package:cw_core/cake_hive.dart';
final navigatorKey = GlobalKey<NavigatorState>();
@ -317,26 +316,26 @@ class _Home extends StatefulWidget {
}
class _HomeState extends State<_Home> {
@override
@override
void didChangeDependencies() {
if(!ResponsiveLayoutUtil.instance.isMobile){
_setOrientation(context);
if (!ResponsiveLayoutUtil.instance.isMobile) {
_setOrientation(context);
}
super.didChangeDependencies();
}
void _setOrientation(BuildContext context){
void _setOrientation(BuildContext context) {
final orientation = MediaQuery.of(context).orientation;
final width = MediaQuery.of(context).size.width;
final height = MediaQuery.of(context).size.height;
if (orientation == Orientation.portrait && width < height) {
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]);
SystemChrome.setPreferredOrientations(
[DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]);
} else if (orientation == Orientation.landscape && width > height) {
SystemChrome.setPreferredOrientations([DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight]);
SystemChrome.setPreferredOrientations(
[DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight]);
}
}
}
@override
Widget build(BuildContext context) {

View file

@ -0,0 +1,63 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:cake_wallet/mastodon/mastodon_user.dart';
class MastodonAPI {
static const httpsScheme = 'https';
static const userPath = '/api/v1/accounts/lookup';
static const statusesPath = '/api/v1/accounts/:id/statuses';
static Future<MastodonUser?> lookupUserByUserName(
{required String userName, required String apiHost}) async {
try {
final queryParams = {'acct': userName};
final uri = Uri(
scheme: httpsScheme,
host: apiHost,
path: userPath,
queryParameters: queryParams,
);
final response = await http.get(uri);
if (response.statusCode != 200) return null;
final Map<String, dynamic> responseJSON = json.decode(response.body) as Map<String, dynamic>;
return MastodonUser.fromJson(responseJSON);
} catch (e) {
print('Error in lookupUserByUserName: $e');
return null;
}
}
static Future<List<PinnedPost>> getPinnedPosts({
required String userId,
required String apiHost,
}) async {
try {
final queryParams = {'pinned': 'true'};
final uri = Uri(
scheme: httpsScheme,
host: apiHost,
path: statusesPath.replaceAll(':id', userId),
queryParameters: queryParams,
);
final response = await http.get(uri);
if (response.statusCode != 200) {
throw Exception('Unexpected HTTP status: ${response.statusCode}');
}
final List<dynamic> responseJSON = json.decode(response.body) as List<dynamic>;
return responseJSON.map((json) => PinnedPost.fromJson(json as Map<String, dynamic>)).toList();
} catch (e) {
print('Error in getPinnedPosts: $e');
throw e;
}
}
}

View file

@ -0,0 +1,36 @@
class MastodonUser {
String id;
String username;
String acct;
String note;
MastodonUser({
required this.id,
required this.username,
required this.acct,
required this.note,
});
factory MastodonUser.fromJson(Map<String, dynamic> json) {
return MastodonUser(
id: json['id'] as String,
username: json['username'] as String,
acct: json['acct'] as String,
note: json['note'] as String,
);
}
}
class PinnedPost {
final String id;
final String content;
PinnedPost({required this.id, required this.content});
factory PinnedPost.fromJson(Map<String, dynamic> json) {
return PinnedPost(
id: json['id'] as String,
content: json['content'] as String,
);
}
}

View file

@ -79,10 +79,6 @@ class CWNano extends Nano {
return NanoWalletService(walletInfoSource);
}
String getTransactionAddress(Object wallet, int accountIndex, int addressIndex) {
throw UnimplementedError();
}
@override
Map<String, String> getKeys(Object wallet) {
final nanoWallet = wallet as NanoWallet;
@ -109,7 +105,6 @@ class CWNano extends Nano {
required String mnemonic,
DerivationType? derivationType,
}) {
if (derivationType == null) {
// figure out the derivation type as best we can, otherwise set it to "unknown"
if (mnemonic.split(" ").length == 12) {
@ -134,7 +129,6 @@ class CWNano extends Nano {
required String seedKey,
DerivationType? derivationType,
}) {
if (derivationType == null) {
// figure out the derivation type as best we can, otherwise set it to "unknown"
if (seedKey.length == 64) {
@ -152,14 +146,6 @@ class CWNano extends Nano {
);
}
@override
TransactionHistoryBase getTransactionHistory(Object wallet) {
throw UnimplementedError();
}
@override
void onStartup() {}
@override
Object createNanoTransactionCredentials(List<Output> outputs) {
return NanoTransactionCredentials(
@ -179,10 +165,335 @@ class CWNano extends Nano {
}
@override
Future<void> setLabelAccount(Object wallet,
{required int accountIndex, required String label}) async {
final nanoWallet = wallet as NanoWallet;
await nanoWallet.walletAddresses.accountList
.setLabelAccount(accountIndex: accountIndex, label: label);
Future<void> changeRep(Object wallet, String address) async {
return (wallet as NanoWallet).changeRep(address);
}
@override
Future<void> updateTransactions(Object wallet) async {
return (wallet as NanoWallet).updateTransactions();
}
@override
BigInt getTransactionAmountRaw(TransactionInfo transactionInfo) {
return (transactionInfo as NanoTransactionInfo).amountRaw;
}
}
class CWNanoUtil extends NanoUtil {
// standard:
@override
String seedToPrivate(String seed, int index) {
return ND.NanoKeys.seedToPrivate(seed, index);
}
@override
String seedToAddress(String seed, int index) {
return ND.NanoAccounts.createAccount(
ND.NanoAccountType.NANO, privateKeyToPublic(seedToPrivate(seed, index)));
}
@override
String seedToMnemonic(String seed) {
return NanoMnemomics.seedToMnemonic(seed).join(" ");
}
@override
Future<String> mnemonicToSeed(String mnemonic) async {
return NanoMnemomics.mnemonicListToSeed(mnemonic.split(' '));
}
@override
String privateKeyToPublic(String privateKey) {
// return NanoHelpers.byteToHex(Ed25519Blake2b.getPubkey(NanoHelpers.hexToBytes(privateKey))!);
return ND.NanoKeys.createPublicKey(privateKey);
}
@override
String addressToPublicKey(String publicAddress) {
return ND.NanoAccounts.extractPublicKey(publicAddress);
}
// universal:
@override
String privateKeyToAddress(String privateKey) {
return ND.NanoAccounts.createAccount(ND.NanoAccountType.NANO, privateKeyToPublic(privateKey));
}
@override
String publicKeyToAddress(String publicKey) {
return ND.NanoAccounts.createAccount(ND.NanoAccountType.NANO, publicKey);
}
// standard + hd:
@override
bool isValidSeed(String seed) {
// Ensure seed is 64 or 128 characters long
if (seed.length != 64 && seed.length != 128) {
return false;
}
// Ensure seed only contains hex characters, 0-9;A-F
return ND.NanoHelpers.isHexString(seed);
}
// hd:
@override
Future<String> hdMnemonicListToSeed(List<String> words) async {
// if (words.length != 24) {
// throw Exception('Expected a 24-word list, got a ${words.length} list');
// }
final Uint8List salt = Uint8List.fromList(utf8.encode('mnemonic'));
final Pbkdf2 hasher = Pbkdf2(iterations: 2048);
final String seed = await hasher.sha512(words.join(' '), salt);
return seed;
}
@override
Future<String> hdSeedToPrivate(String seed, int index) async {
List<int> seedBytes = hex.decode(seed);
KeyData data = await ED25519_HD_KEY.derivePath("m/44'/165'/$index'", seedBytes);
return hex.encode(data.key);
}
@override
Future<String> hdSeedToAddress(String seed, int index) async {
return ND.NanoAccounts.createAccount(
ND.NanoAccountType.NANO, privateKeyToPublic(await hdSeedToPrivate(seed, index)));
}
@override
Future<String> uniSeedToAddress(String seed, int index, String type) {
if (type == "standard") {
return Future<String>.value(seedToAddress(seed, index));
} else if (type == "hd") {
return hdSeedToAddress(seed, index);
} else {
throw Exception('Unknown seed type');
}
}
@override
Future<String> uniSeedToPrivate(String seed, int index, String type) {
if (type == "standard") {
return Future<String>.value(seedToPrivate(seed, index));
} else if (type == "hd") {
return hdSeedToPrivate(seed, index);
} else {
throw Exception('Unknown seed type');
}
}
@override
bool isValidBip39Seed(String seed) {
// Ensure seed is 128 characters long
if (seed.length != 128) {
return false;
}
// Ensure seed only contains hex characters, 0-9;A-F
return ND.NanoHelpers.isHexString(seed);
}
// number util:
static const int maxDecimalDigits = 6; // Max digits after decimal
BigInt rawPerNano = BigInt.parse("1000000000000000000000000000000");
BigInt rawPerNyano = BigInt.parse("1000000000000000000000000");
BigInt rawPerBanano = BigInt.parse("100000000000000000000000000000");
BigInt rawPerXMR = BigInt.parse("1000000000000");
BigInt convertXMRtoNano = BigInt.parse("1000000000000000000");
// static BigInt convertXMRtoNano = BigInt.parse("1000000000000000000000000000");
/// Convert raw to ban and return as BigDecimal
///
/// @param raw 100000000000000000000000000000
/// @return Decimal value 1.000000000000000000000000000000
///
@override
Decimal getRawAsDecimal(String? raw, BigInt? rawPerCur) {
rawPerCur ??= rawPerNano;
final Decimal amount = Decimal.parse(raw.toString());
final Decimal result = (amount / Decimal.parse(rawPerCur.toString())).toDecimal();
return result;
}
@override
String truncateDecimal(Decimal input, {int digits = maxDecimalDigits}) {
Decimal bigger = input.shift(digits);
bigger = bigger.floor(); // chop off the decimal: 1.059 -> 1.05
bigger = bigger.shift(-digits);
return bigger.toString();
}
/// Return raw as a NANO amount.
///
/// @param raw 100000000000000000000000000000
/// @returns 1
///
@override
String getRawAsUsableString(String? raw, BigInt rawPerCur) {
final String res =
truncateDecimal(getRawAsDecimal(raw, rawPerCur), digits: maxDecimalDigits + 9);
if (raw == null || raw == "0" || raw == "00000000000000000000000000000000") {
return "0";
}
if (!res.contains(".")) {
return res;
}
final String numAmount = res.split(".")[0];
String decAmount = res.split(".")[1];
// truncate:
if (decAmount.length > maxDecimalDigits) {
decAmount = decAmount.substring(0, maxDecimalDigits);
// remove trailing zeros:
decAmount = decAmount.replaceAllMapped(RegExp(r'0+$'), (Match match) => '');
if (decAmount.isEmpty) {
return numAmount;
}
}
return "$numAmount.$decAmount";
}
@override
String getRawAccuracy(String? raw, BigInt rawPerCur) {
final String rawString = getRawAsUsableString(raw, rawPerCur);
final String rawDecimalString = getRawAsDecimal(raw, rawPerCur).toString();
if (raw == null || raw.isEmpty || raw == "0") {
return "";
}
if (rawString != rawDecimalString) {
return "~";
}
return "";
}
/// Return readable string amount as raw string
/// @param amount 1.01
/// @returns 101000000000000000000000000000
///
@override
String getAmountAsRaw(String amount, BigInt rawPerCur) {
final Decimal asDecimal = Decimal.parse(amount);
final Decimal rawDecimal = Decimal.parse(rawPerCur.toString());
return (asDecimal * rawDecimal).toString();
}
@override
Future<AccountInfoResponse?> getInfoFromSeedOrMnemonic(
DerivationType derivationType, {
String? seedKey,
String? mnemonic,
required Node node,
}) async {
NanoClient nanoClient = NanoClient();
nanoClient.connect(node);
late String publicAddress;
if (seedKey != null) {
if (derivationType == DerivationType.bip39) {
publicAddress = await hdSeedToAddress(seedKey, 0);
} else if (derivationType == DerivationType.nano) {
publicAddress = await seedToAddress(seedKey, 0);
}
}
if (derivationType == DerivationType.bip39) {
if (mnemonic != null) {
seedKey = await hdMnemonicListToSeed(mnemonic.split(' '));
publicAddress = await hdSeedToAddress(seedKey, 0);
}
}
if (derivationType == DerivationType.nano) {
if (mnemonic != null) {
seedKey = await mnemonicToSeed(mnemonic);
publicAddress = await seedToAddress(seedKey, 0);
}
}
AccountInfoResponse? accountInfo = await nanoClient.getAccountInfo(publicAddress);
if (accountInfo == null) {
accountInfo = AccountInfoResponse(frontier: "", balance: "0", representative: "", confirmationHeight: 0);
}
accountInfo.address = publicAddress;
return accountInfo;
}
@override
Future<List<DerivationType>> compareDerivationMethods({
String? mnemonic,
String? privateKey,
required Node node,
}) async {
String? seedKey = privateKey;
if (mnemonic?.split(' ').length == 12) {
return [DerivationType.bip39];
}
if (seedKey?.length == 128) {
return [DerivationType.bip39];
} else if (seedKey?.length == 64) {
return [DerivationType.nano];
}
late String publicAddressStandard;
late String publicAddressBip39;
try {
NanoClient nanoClient = NanoClient();
nanoClient.connect(node);
if (mnemonic != null) {
seedKey = await hdMnemonicListToSeed(mnemonic.split(' '));
publicAddressBip39 = await hdSeedToAddress(seedKey, 0);
seedKey = await mnemonicToSeed(mnemonic);
publicAddressStandard = await seedToAddress(seedKey, 0);
} else if (seedKey != null) {
try {
publicAddressBip39 = await hdSeedToAddress(seedKey, 0);
} catch (e) {
return [DerivationType.nano];
}
try {
publicAddressStandard = await seedToAddress(seedKey, 0);
} catch (e) {
return [DerivationType.bip39];
}
}
// check if account has a history:
AccountInfoResponse? bip39Info;
AccountInfoResponse? standardInfo;
try {
bip39Info = await nanoClient.getAccountInfo(publicAddressBip39);
} catch (e) {
bip39Info = null;
}
try {
standardInfo = await nanoClient.getAccountInfo(publicAddressStandard);
} catch (e) {
standardInfo = null;
}
// one of these is *probably* null:
if (bip39Info == null && standardInfo != null) {
return [DerivationType.nano];
} else if (standardInfo == null && bip39Info != null) {
return [DerivationType.bip39];
}
// we don't know for sure:
return [DerivationType.nano, DerivationType.bip39];
} catch (e) {
return [DerivationType.unknown];
}
}
}

View file

@ -201,8 +201,8 @@ Route<dynamic> createRoute(RouteSettings settings) {
case Routes.restoreWalletChooseDerivation:
return MaterialPageRoute<void>(
builder: (_) =>
getIt.get<WalletRestoreChooseDerivationPage>(param1: settings.arguments as List<DerivationInfo>));
builder: (_) => getIt.get<WalletRestoreChooseDerivationPage>(
param1: settings.arguments as List<DerivationInfo>));
case Routes.sweepingWalletPage:
return CupertinoPageRoute<void>(builder: (_) => getIt.get<SweepingWalletPage>());
@ -248,7 +248,7 @@ Route<dynamic> createRoute(RouteSettings settings) {
return CupertinoPageRoute<void>(builder: (_) => DisclaimerPage(isReadOnly: true));
case Routes.changeRep:
return CupertinoPageRoute<void>(builder: (_) => NanoChangeRepPage());
return CupertinoPageRoute<void>(builder: (_) => getIt.get<NanoChangeRepPage>());
case Routes.seedLanguage:
final args = settings.arguments as List<dynamic>;
@ -521,7 +521,7 @@ Route<dynamic> createRoute(RouteSettings settings) {
return CupertinoPageRoute<void>(
builder: (_) => AdvancedPrivacySettingsPage(
getIt.get<AdvancedPrivacySettingsViewModel>(param1: type),
getIt.get<NodeCreateOrEditViewModel>(param1: type),
getIt.get<NodeCreateOrEditViewModel>(param1: type, param2: false),
));
case Routes.anonPayInvoicePage:
@ -585,7 +585,10 @@ Route<dynamic> createRoute(RouteSettings settings) {
);
case Routes.manageNodes:
return MaterialPageRoute<void>(builder: (_) => getIt.get<ManageNodesPage>());
return MaterialPageRoute<void>(builder: (_) => getIt.get<ManageNodesPage>(param1: false));
case Routes.managePowNodes:
return MaterialPageRoute<void>(builder: (_) => getIt.get<ManageNodesPage>(param1: true));
case Routes.managePowNodes:
return MaterialPageRoute<void>(builder: (_) => getIt.get<ManagePowNodesPage>());

View file

@ -1,9 +1,12 @@
import 'dart:async';
import 'package:cake_wallet/core/wallet_connect/wc_bottom_sheet_service.dart';
import 'package:cake_wallet/core/wallet_connect/web3wallet_service.dart';
import 'package:cake_wallet/entities/preferences_key.dart';
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/entities/main_actions.dart';
import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/desktop_sidebar_wrapper.dart';
import 'package:cake_wallet/src/screens/dashboard/widgets/market_place_page.dart';
import 'package:cake_wallet/src/screens/wallet_connect/widgets/modals/bottom_sheet_listener.dart';
import 'package:cake_wallet/src/widgets/gradient_background.dart';
import 'package:cake_wallet/themes/extensions/sync_indicator_theme.dart';
import 'package:cake_wallet/utils/device_info.dart';
@ -35,12 +38,14 @@ import 'package:cake_wallet/themes/extensions/balance_page_theme.dart';
class DashboardPage extends StatelessWidget {
DashboardPage({
required this.bottomSheetService,
required this.balancePage,
required this.dashboardViewModel,
required this.addressListViewModel,
});
final BalancePage balancePage;
final BottomSheetService bottomSheetService;
final DashboardViewModel dashboardViewModel;
final WalletAddressListViewModel addressListViewModel;
@ -55,12 +60,14 @@ class DashboardPage extends StatelessWidget {
} else {
return _DashboardPageView(
balancePage: balancePage,
bottomSheetService: bottomSheetService,
dashboardViewModel: dashboardViewModel,
addressListViewModel: addressListViewModel,
);
}
} else if (ResponsiveLayoutUtil.instance.shouldRenderMobileUI()) {
return _DashboardPageView(
bottomSheetService: bottomSheetService,
balancePage: balancePage,
dashboardViewModel: dashboardViewModel,
addressListViewModel: addressListViewModel,
@ -76,6 +83,7 @@ class DashboardPage extends StatelessWidget {
class _DashboardPageView extends BasePage {
_DashboardPageView({
required this.bottomSheetService,
required this.balancePage,
required this.dashboardViewModel,
required this.addressListViewModel,
@ -126,6 +134,7 @@ class _DashboardPageView extends BasePage {
}
final DashboardViewModel dashboardViewModel;
final BottomSheetService bottomSheetService;
final WalletAddressListViewModel addressListViewModel;
int get initialPage => dashboardViewModel.shouldShowMarketPlaceInDashboard ? 1 : 0;
@ -158,102 +167,106 @@ class _DashboardPageView extends BasePage {
return SafeArea(
minimum: EdgeInsets.only(bottom: 24),
child: Column(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Expanded(
child: Observer(
builder: (context) {
return PageView.builder(
controller: controller,
itemCount: pages.length,
itemBuilder: (context, index) => pages[index],
);
},
),
),
Padding(
padding: EdgeInsets.only(bottom: 24, top: 10),
child: Observer(
builder: (context) {
return ExcludeSemantics(
child: SmoothPageIndicator(
child: BottomSheetListener(
bottomSheetService: bottomSheetService,
child: Column(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Expanded(
child: Observer(
builder: (context) {
return PageView.builder(
controller: controller,
count: pages.length,
effect: ColorTransitionEffect(
spacing: 6.0,
radius: 6.0,
dotWidth: 6.0,
dotHeight: 6.0,
dotColor: Theme.of(context).indicatorColor,
activeDotColor: Theme.of(context)
.extension<DashboardPageTheme>()!
.indicatorDotTheme
.activeIndicatorColor,
),
),
);
},
itemCount: pages.length,
itemBuilder: (context, index) => pages[index],
);
},
),
),
),
Observer(
builder: (_) {
return ClipRect(
child: Container(
margin: const EdgeInsets.only(left: 16, right: 16),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(50.0),
border: Border.all(
color: Theme.of(context).extension<BalancePageTheme>()!.cardBorderColor,
width: 1,
Padding(
padding: EdgeInsets.only(bottom: 24, top: 10),
child: Observer(
builder: (context) {
return ExcludeSemantics(
child: SmoothPageIndicator(
controller: controller,
count: pages.length,
effect: ColorTransitionEffect(
spacing: 6.0,
radius: 6.0,
dotWidth: 6.0,
dotHeight: 6.0,
dotColor: Theme.of(context).indicatorColor,
activeDotColor: Theme.of(context)
.extension<DashboardPageTheme>()!
.indicatorDotTheme
.activeIndicatorColor,
),
color:
Theme.of(context).extension<SyncIndicatorTheme>()!.syncedBackgroundColor,
),
);
},
),
),
Observer(
builder: (_) {
return ClipRect(
child: Container(
margin: const EdgeInsets.only(left: 16, right: 16),
child: Container(
padding: EdgeInsets.only(left: 32, right: 32),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: MainActions.all
.where((element) => element.canShow?.call(dashboardViewModel) ?? true)
.map(
(action) => Semantics(
button: true,
enabled: (action.isEnabled?.call(dashboardViewModel) ?? true),
child: ActionButton(
image: Image.asset(
action.image,
height: 24,
width: 24,
color: action.isEnabled?.call(dashboardViewModel) ?? true
? Theme.of(context)
.extension<DashboardPageTheme>()!
.mainActionsIconColor
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(50.0),
border: Border.all(
color: Theme.of(context).extension<BalancePageTheme>()!.cardBorderColor,
width: 1,
),
color: Theme.of(context)
.extension<SyncIndicatorTheme>()!
.syncedBackgroundColor,
),
child: Container(
padding: EdgeInsets.only(left: 32, right: 32),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: MainActions.all
.where((element) => element.canShow?.call(dashboardViewModel) ?? true)
.map(
(action) => Semantics(
button: true,
enabled: (action.isEnabled?.call(dashboardViewModel) ?? true),
child: ActionButton(
image: Image.asset(
action.image,
height: 24,
width: 24,
color: action.isEnabled?.call(dashboardViewModel) ?? true
? Theme.of(context)
.extension<DashboardPageTheme>()!
.mainActionsIconColor
: Theme.of(context)
.extension<BalancePageTheme>()!
.labelTextColor,
),
title: action.name(context),
onClick: () async =>
await action.onTap(context, dashboardViewModel),
textColor: action.isEnabled?.call(dashboardViewModel) ?? true
? null
: Theme.of(context)
.extension<BalancePageTheme>()!
.labelTextColor,
),
title: action.name(context),
onClick: () async =>
await action.onTap(context, dashboardViewModel),
textColor: action.isEnabled?.call(dashboardViewModel) ?? true
? null
: Theme.of(context)
.extension<BalancePageTheme>()!
.labelTextColor,
),
),
)
.toList(),
)
.toList(),
),
),
),
),
),
);
},
),
],
);
},
),
],
),
),
);
}

View file

@ -1,8 +1,10 @@
import 'dart:async';
import 'package:cake_wallet/core/wallet_connect/wc_bottom_sheet_service.dart';
import 'package:cake_wallet/entities/preferences_key.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/release_notes/release_notes_screen.dart';
import 'package:cake_wallet/src/screens/wallet_connect/widgets/modals/bottom_sheet_listener.dart';
import 'package:cake_wallet/src/screens/yat_emoji_id.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
@ -19,12 +21,14 @@ import 'package:shared_preferences/shared_preferences.dart';
class DesktopDashboardPage extends StatelessWidget {
DesktopDashboardPage({
required this.balancePage,
required this.bottomSheetService,
required this.dashboardViewModel,
required this.addressListViewModel,
required this.desktopKey,
});
final BalancePage balancePage;
final BottomSheetService bottomSheetService;
final DashboardViewModel dashboardViewModel;
final WalletAddressListViewModel addressListViewModel;
final GlobalKey<NavigatorState> desktopKey;
@ -36,31 +40,34 @@ class DesktopDashboardPage extends StatelessWidget {
Widget build(BuildContext context) {
_setEffects(context);
return Container(
color: Theme.of(context).colorScheme.background,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 400,
child: balancePage,
),
Flexible(
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: 500),
child: Navigator(
key: desktopKey,
initialRoute: Routes.desktop_actions,
onGenerateRoute: (settings) => Router.createRoute(settings),
onGenerateInitialRoutes: (NavigatorState navigator, String initialRouteName) {
return [
navigator.widget.onGenerateRoute!(RouteSettings(name: initialRouteName))!
];
},
return BottomSheetListener(
bottomSheetService: bottomSheetService,
child: Container(
color: Theme.of(context).colorScheme.background,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 400,
child: balancePage,
),
Flexible(
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: 500),
child: Navigator(
key: desktopKey,
initialRoute: Routes.desktop_actions,
onGenerateRoute: (settings) => Router.createRoute(settings),
onGenerateInitialRoutes: (NavigatorState navigator, String initialRouteName) {
return [
navigator.widget.onGenerateRoute!(RouteSettings(name: initialRouteName))!
];
},
),
),
),
),
],
],
),
),
);
}

View file

@ -1,10 +1,11 @@
import 'package:cake_wallet/core/address_validator.dart';
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/nano/nano.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
import 'package:cake_wallet/src/widgets/base_text_form_field.dart';
import 'package:cake_wallet/store/app_store.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_nano/nano_wallet.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
@ -14,20 +15,14 @@ import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
class NanoChangeRepPage extends BasePage {
NanoChangeRepPage()
: _formKey = GlobalKey<FormState>(),
NanoChangeRepPage(WalletBase wallet)
: _wallet = wallet,
_addressController = TextEditingController() {
var wallet = getIt.get<AppStore>().wallet!;
if (wallet is NanoWallet /*|| wallet is BananoWallet*/) {
_addressController.text = wallet.representative;
}
_addressController.text = (wallet as NanoWallet).representative;
}
final GlobalKey<FormState> _formKey;
final TextEditingController _addressController;
// final CryptoCurrency type;
final WalletBase _wallet;
@override
String get title => S.current.change_rep;
@ -80,14 +75,18 @@ class NanoChangeRepPage extends BasePage {
if (confirmed) {
try {
final wallet = getIt.get<AppStore>().wallet!;
if (wallet is NanoWallet) {
await wallet.changeRep(_addressController.text);
}
// TODO: show message saying success:
await nano!.changeRep(_wallet, _addressController.text);
Navigator.of(context).pop();
} catch (e) {
await showPopUp<void>(
context: context,
builder: (BuildContext context) {
return AlertWithOneAction(
alertTitle: S.of(context).error,
alertContent: e.toString(),
buttonText: S.of(context).ok,
buttonAction: () => Navigator.pop(context));
});
throw e;
}
}

View file

@ -1,3 +1,4 @@
import 'package:cake_wallet/themes/extensions/account_list_theme.dart';
import 'package:flutter/material.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:cake_wallet/generated/i18n.dart';
@ -21,11 +22,11 @@ class AccountTile extends StatelessWidget {
@override
Widget build(BuildContext context) {
final color = isCurrent
? Theme.of(context).textTheme.titleSmall!.decorationColor!
: Theme.of(context).textTheme.displayLarge!.decorationColor!;
? Theme.of(context).extension<AccountListTheme>()!.currentAccountBackgroundColor
: Theme.of(context).extension<AccountListTheme>()!.tilesBackgroundColor;
final textColor = isCurrent
? Theme.of(context).textTheme.titleSmall!.color!
: Theme.of(context).textTheme.displayLarge!.color!;
? Theme.of(context).extension<AccountListTheme>()!.currentAccountTextColor
: Theme.of(context).extension<AccountListTheme>()!.tilesTextColor;
final Widget cell = GestureDetector(
onTap: onTap,

View file

@ -1,11 +1,10 @@
import 'package:cake_wallet/core/execution_state.dart';
import 'package:cake_wallet/palette.dart';
import 'package:cake_wallet/src/screens/nodes/widgets/node_form.dart';
import 'package:cake_wallet/src/screens/nodes/widgets/pow_node_form.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/node_list/pow_node_create_or_edit_view_model.dart';
import 'package:cake_wallet/view_model/node_list/node_create_or_edit_view_model.dart';
import 'package:cw_core/node.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
@ -14,7 +13,6 @@ import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
import 'package:cake_wallet/view_model/node_list/node_create_or_edit_view_model.dart';
class PowNodeCreateOrEditPage extends BasePage {
PowNodeCreateOrEditPage({required this.nodeCreateOrEditViewModel,this.editingNode, this.isSelected})
@ -81,7 +79,7 @@ class PowNodeCreateOrEditPage extends BasePage {
),
);
final PowNodeCreateOrEditViewModel nodeCreateOrEditViewModel;
final NodeCreateOrEditViewModel nodeCreateOrEditViewModel;
final Node? editingNode;
final bool? isSelected;
@ -124,7 +122,7 @@ class PowNodeCreateOrEditPage extends BasePage {
padding: EdgeInsets.only(left: 24, right: 24),
child: ScrollableWithBottomSection(
contentPadding: EdgeInsets.only(bottom: 24.0),
content: PowNodeForm(
content: NodeForm(
formKey: _formKey,
nodeViewModel: nodeCreateOrEditViewModel,
editingNode: editingNode,

View file

@ -11,10 +11,12 @@ class NodeListRow extends StandardListRow {
{required String title,
required this.node,
required void Function(BuildContext context) onTap,
required bool isSelected})
required bool isSelected,
required this.isPow})
: super(title: title, onTap: onTap, isSelected: isSelected);
final Node node;
final bool isPow;
@override
Widget buildLeading(BuildContext context) {
@ -33,7 +35,7 @@ class NodeListRow extends StandardListRow {
@override
Widget buildTrailing(BuildContext context) {
return GestureDetector(
onTap: () => Navigator.of(context).pushNamed(Routes.newNode,
onTap: () => Navigator.of(context).pushNamed(isPow ? Routes.newPowNode : Routes.newNode,
arguments: {'editingNode': node, 'isSelected': isSelected}),
child: Container(
padding: EdgeInsets.all(10),

View file

@ -66,6 +66,7 @@ class WalletRestoreFromKeysFromState extends State<WalletRestoreFromKeysFrom> {
addressController.dispose();
viewKeyController.dispose();
privateKeyController.dispose();
spendKeyController.dispose();
super.dispose();
}

View file

@ -5,11 +5,9 @@ import 'package:cake_wallet/store/app_store.dart';
import 'package:cake_wallet/themes/extensions/keyboard_theme.dart';
import 'package:cake_wallet/src/widgets/keyboard_done_button.dart';
import 'package:cake_wallet/utils/responsive_layout_util.dart';
import 'package:cw_bitcoin/bitcoin_wallet_service.dart';
import 'package:cw_core/nano_account_info_response.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:cw_nano/nano_util.dart';
import 'package:cw_nano/nano_wallet_service.dart';
import 'package:flutter/material.dart';
import 'package:keyboard_actions/keyboard_actions.dart';
import 'package:mobx/mobx.dart';
@ -242,7 +240,7 @@ class WalletRestorePage extends BasePage {
}
// bip39:
const validSeedLengths = [12, 15, 18, 21, 24];
const validSeedLengths = [12, 18, 24];
if (walletRestoreViewModel.type == WalletType.bitcoin &&
!(validSeedLengths.contains(seedWords.length))) {
return false;
@ -306,6 +304,7 @@ class WalletRestorePage extends BasePage {
var walletType = credentials["walletType"] as WalletType;
var appStore = getIt.get<AppStore>();
var node = appStore.settingsStore.getCurrentNode(walletType);
switch (walletType) {
case WalletType.bitcoin:
String? mnemonic = credentials['seed'] as String?;
@ -314,38 +313,33 @@ class WalletRestorePage extends BasePage {
case WalletType.nano:
String? mnemonic = credentials['seed'] as String?;
String? seedKey = credentials['private_key'] as String?;
dynamic bip39Info = await NanoWalletService.getInfoFromSeedOrMnemonic(DerivationType.bip39,
mnemonic: mnemonic, seedKey: seedKey, node: node);
dynamic standardInfo = await NanoWalletService.getInfoFromSeedOrMnemonic(
AccountInfoResponse? bip39Info = await nanoUtil!.getInfoFromSeedOrMnemonic(
DerivationType.bip39,
mnemonic: mnemonic,
seedKey: seedKey,
node: node);
AccountInfoResponse? standardInfo = await nanoUtil!.getInfoFromSeedOrMnemonic(
DerivationType.nano,
mnemonic: mnemonic,
seedKey: seedKey,
node: node,
);
if (standardInfo["balance"] != null) {
if (standardInfo?.balance != null) {
list.add(DerivationInfo(
derivationType: DerivationType.nano,
balance: NanoUtil.getRawAsUsableString(
standardInfo["balance"] as String, NanoUtil.rawPerNano),
address: standardInfo["address"] as String,
height: int.tryParse(
standardInfo["confirmation_height"] as String,
) ??
0,
balance: nanoUtil!.getRawAsUsableString(standardInfo!.balance, nanoUtil!.rawPerNano),
address: standardInfo.address!,
height: standardInfo.confirmationHeight,
));
}
if (bip39Info["balance"] != null) {
if (bip39Info?.balance != null) {
list.add(DerivationInfo(
derivationType: DerivationType.bip39,
balance:
NanoUtil.getRawAsUsableString(bip39Info["balance"] as String, NanoUtil.rawPerNano),
address: bip39Info["address"] as String,
height: int.tryParse(
bip39Info["confirmation_height"] as String? ?? "",
) ??
0,
balance: nanoUtil!.getRawAsUsableString(bip39Info!.balance, nanoUtil!.rawPerNano),
address: bip39Info.address!,
height: bip39Info.confirmationHeight,
));
}
break;
@ -399,6 +393,7 @@ class WalletRestorePage extends BasePage {
}
}
DerivationInfo? derivationInfo;
if (derivationsWithHistory > 1) {
@ -413,8 +408,6 @@ class WalletRestorePage extends BasePage {
derivationPath: "m/0'/1",
height: 0,
);
this.derivationType = derivationTypes[0];
this.derivationPath = "m/0'/1";
}
if (derivationInfo == null) {

View file

@ -1,6 +1,7 @@
import 'dart:async';
import 'package:cake_wallet/core/auth_service.dart';
import 'package:cake_wallet/core/totp_request_details.dart';
import 'package:cake_wallet/core/wallet_connect/wc_bottom_sheet_service.dart';
import 'package:cake_wallet/utils/device_info.dart';
import 'package:cake_wallet/utils/payment_request.dart';
import 'package:flutter/material.dart';
@ -97,8 +98,7 @@ class RootState extends State<Root> with WidgetsBindingObserver {
return;
}
if (!_isInactive &&
widget.authenticationStore.state == AuthenticationState.allowed) {
if (!_isInactive && widget.authenticationStore.state == AuthenticationState.allowed) {
setState(() => _setInactive(true));
}
@ -125,16 +125,15 @@ class RootState extends State<Root> with WidgetsBindingObserver {
return;
} else {
final useTotp = widget.appStore.settingsStore.useTOTP2FA;
final shouldUseTotp2FAToAccessWallets = widget.appStore
.settingsStore.shouldRequireTOTP2FAForAccessingWallet;
final shouldUseTotp2FAToAccessWallets =
widget.appStore.settingsStore.shouldRequireTOTP2FAForAccessingWallet;
if (useTotp && shouldUseTotp2FAToAccessWallets) {
_reset();
auth.close(
route: Routes.totpAuthCodePage,
arguments: TotpAuthArgumentsModel(
onTotpAuthenticationFinished:
(bool isAuthenticatedSuccessfully,
TotpAuthCodePageState totpAuth) {
(bool isAuthenticatedSuccessfully, TotpAuthCodePageState totpAuth) {
if (!isAuthenticatedSuccessfully) {
return;
}
@ -169,7 +168,10 @@ class RootState extends State<Root> with WidgetsBindingObserver {
launchUri = null;
}
return WillPopScope(onWillPop: () async => false, child: widget.child);
return WillPopScope(
onWillPop: () async => false,
child: widget.child,
);
}
void _reset() {

View file

@ -18,6 +18,11 @@ Future<String> extractAddressFromParsed(
content = S.of(context).address_from_domain(parsedAddress.name);
address = parsedAddress.addresses.first;
break;
case ParseFrom.ens:
title = S.of(context).address_detected;
content = S.of(context).extracted_address_content('${parsedAddress.name} (ENS)');
address = parsedAddress.addresses.first;
break;
case ParseFrom.openAlias:
title = S.of(context).address_detected;
content = S.of(context).extracted_address_content('${parsedAddress.name} (OpenAlias)');
@ -33,6 +38,11 @@ Future<String> extractAddressFromParsed(
content = S.of(context).extracted_address_content('${parsedAddress.name} (Twitter)');
address = parsedAddress.addresses.first;
break;
case ParseFrom.mastodon:
title = S.of(context).address_detected;
content = S.of(context).extracted_address_content('${parsedAddress.name} (Mastodon)');
address = parsedAddress.addresses.first;
break;
case ParseFrom.yatRecord:
if (parsedAddress.name.isEmpty) {
title = S.of(context).yat_error;

View file

@ -1,12 +1,15 @@
import 'package:cake_wallet/core/wallet_connect/web3wallet_service.dart';
import 'package:cake_wallet/src/screens/settings/widgets/settings_cell_with_arrow.dart';
import 'package:cake_wallet/src/screens/settings/widgets/settings_picker_cell.dart';
import 'package:cake_wallet/src/screens/settings/widgets/settings_switcher_cell.dart';
import 'package:cake_wallet/src/screens/settings/widgets/wallet_connect_button.dart';
import 'package:cake_wallet/src/screens/wallet_connect/wc_connections_listing_view.dart';
import 'package:cake_wallet/utils/device_info.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
import 'package:cake_wallet/view_model/settings/sync_mode.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
@ -15,11 +18,12 @@ import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
class ConnectionSyncPage extends BasePage {
ConnectionSyncPage(this.dashboardViewModel);
ConnectionSyncPage(this.dashboardViewModel, this.web3walletService);
@override
String get title => S.current.connection_sync;
final Web3WalletService? web3walletService;
final DashboardViewModel dashboardViewModel;
@override
@ -81,6 +85,20 @@ class ConnectionSyncPage extends BasePage {
);
},
),
if (dashboardViewModel.wallet.type == WalletType.ethereum) ...[
WalletConnectTile(
onTap: () async {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) {
return WalletConnectConnectionsView(web3walletService: web3walletService!);
},
),
);
},
),
const StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)),
]
],
),
);

View file

@ -6,13 +6,17 @@ import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
import 'package:cake_wallet/src/widgets/standard_list.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/node_list/node_list_view_model.dart';
import 'package:cake_wallet/view_model/node_list/pow_node_list_view_model.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
class ManageNodesPage extends BasePage {
ManageNodesPage(this.nodeListViewModel);
ManageNodesPage(this.isPow, {this.nodeListViewModel, this.powNodeListViewModel})
: assert((isPow && powNodeListViewModel != null) || (!isPow && nodeListViewModel != null));
final NodeListViewModel nodeListViewModel;
final NodeListViewModel? nodeListViewModel;
final PowNodeListViewModel? powNodeListViewModel;
final bool isPow;
@override
String get title => S.current.manage_nodes;
@ -34,7 +38,8 @@ class ManageNodesPage extends BasePage {
SizedBox(height: 20),
Observer(
builder: (BuildContext context) {
int itemsCount = nodeListViewModel.nodes.length;
int itemsCount =
nodeListViewModel?.nodes.length ?? powNodeListViewModel!.nodes.length;
return Flexible(
child: SectionStandardList(
sectionCount: 1,
@ -43,12 +48,19 @@ class ManageNodesPage extends BasePage {
itemBuilder: (_, index) {
return Observer(
builder: (context) {
final node = nodeListViewModel.nodes[index];
final isSelected = node.keyIndex == nodeListViewModel.currentNode.keyIndex;
final node =
nodeListViewModel?.nodes[index] ?? powNodeListViewModel!.nodes[index];
late bool isSelected;
if (isPow) {
isSelected = node.keyIndex == powNodeListViewModel!.currentNode.keyIndex;
} else {
isSelected = node.keyIndex == nodeListViewModel!.currentNode.keyIndex;
}
final nodeListRow = NodeListRow(
title: node.uriRaw,
node: node,
isSelected: isSelected,
isPow: false,
onTap: (_) async {
if (isSelected) {
return;
@ -59,12 +71,17 @@ class ManageNodesPage extends BasePage {
builder: (BuildContext context) {
return AlertWithTwoActions(
alertTitle: S.of(context).change_current_node_title,
alertContent: nodeListViewModel.getAlertContent(node.uriRaw),
alertContent: nodeListViewModel?.getAlertContent(node.uriRaw) ??
powNodeListViewModel!.getAlertContent(node.uriRaw),
leftButtonText: S.of(context).cancel,
rightButtonText: S.of(context).change,
actionLeftButton: () => Navigator.of(context).pop(),
actionRightButton: () async {
await nodeListViewModel.setAsCurrent(node);
if (isPow) {
await powNodeListViewModel!.setAsCurrent(node);
} else {
await nodeListViewModel!.setAsCurrent(node);
}
Navigator.of(context).pop();
},
);

View file

@ -0,0 +1,46 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
import 'package:cake_wallet/themes/extensions/transaction_trade_theme.dart';
import 'package:flutter/material.dart';
class WalletConnectTile extends StatelessWidget {
const WalletConnectTile({required this.onTap});
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Padding(
padding: EdgeInsets.all(24),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Image.asset(
'assets/images/walletconnect_logo.png',
height: 24,
width: 24,
),
SizedBox(width: 16),
Expanded(
child: Text(
S.current.walletConnect,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.normal,
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
),
),
),
Image.asset(
'assets/images/select_arrow.png',
color: Theme.of(context).extension<TransactionTradeTheme>()!.detailsTitlesColor,
)
],
),
),
);
}
}

View file

@ -0,0 +1,71 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/wallet_connect/widgets/connection_widget.dart';
import 'package:walletconnect_flutter_v2/walletconnect_flutter_v2.dart';
import '../../../../core/wallet_connect/models/connection_model.dart';
class ConnectionWidgetBuilder {
static List<ConnectionWidget> buildFromRequiredNamespaces(
Map<String, RequiredNamespace> requiredNamespaces,
) {
final List<ConnectionWidget> views = [];
for (final key in requiredNamespaces.keys) {
RequiredNamespace ns = requiredNamespaces[key]!;
final List<ConnectionModel> models = [];
// If the chains property is present, add the chain data to the models
if (ns.chains != null) {
models.add(ConnectionModel(title: S.current.chains, elements: ns.chains!));
}
models.add(ConnectionModel(title: S.current.methods, elements: ns.methods));
models.add(ConnectionModel(title: S.current.events, elements: ns.events));
views.add(ConnectionWidget(title: key, info: models));
}
return views;
}
static List<ConnectionWidget> buildFromNamespaces(
String topic,
Map<String, Namespace> namespaces,
Web3Wallet web3wallet,
) {
final List<ConnectionWidget> views = [];
for (final key in namespaces.keys) {
final Namespace ns = namespaces[key]!;
final List<ConnectionModel> models = [];
// If the chains property is present, add the chain data to the models
models.add(
ConnectionModel(
title: S.current.chains,
elements: ns.accounts,
),
);
models.add(ConnectionModel(
title: S.current.methods,
elements: ns.methods,
));
Map<String, void Function()> actions = {};
for (final String event in ns.events) {
actions[event] = () async {
final String chainId = NamespaceUtils.isValidChainId(key)
? key
: NamespaceUtils.getChainFromAccount(ns.accounts.first);
await web3wallet.emitSessionEvent(
topic: topic,
chainId: chainId,
event: SessionEventParams(name: event, data: '${S.current.event}: $event'),
);
};
}
models.add(
ConnectionModel(title: S.current.events, elements: ns.events, elementActions: actions),
);
views.add(ConnectionWidget(title: key, info: models));
}
return views;
}
}

View file

@ -0,0 +1,16 @@
import 'dart:convert';
import 'package:convert/convert.dart';
extension StringParsing on String {
String get utf8Message {
if (startsWith('0x')) {
final List<int> decoded = hex.decode(
substring(2),
);
return utf8.decode(decoded);
}
return this;
}
}

View file

@ -0,0 +1,142 @@
import 'dart:developer';
import 'package:cake_wallet/core/wallet_connect/web3wallet_service.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:walletconnect_flutter_v2/walletconnect_flutter_v2.dart';
import 'package:cake_wallet/entities/qr_scanner.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'widgets/pairing_item_widget.dart';
import 'wc_pairing_detail_page.dart';
class WalletConnectConnectionsView extends StatelessWidget {
final Web3WalletService web3walletService;
WalletConnectConnectionsView({required this.web3walletService, Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return WCPairingsWidget(web3walletService: web3walletService);
}
}
class WCPairingsWidget extends BasePage {
WCPairingsWidget({required this.web3walletService, Key? key})
: web3wallet = web3walletService.getWeb3Wallet();
final Web3Wallet web3wallet;
final Web3WalletService web3walletService;
@override
String get title => S.current.walletConnect;
Future<void> _onScanQrCode(BuildContext context, Web3Wallet web3Wallet) async {
final String? uri = await presentQRScanner();
if (uri == null) return _invalidUriToast(context, S.current.nullURIError);
try {
log('_onFoundUri: $uri');
final Uri uriData = Uri.parse(uri);
await web3Wallet.pair(uri: uriData);
} on WalletConnectError catch (e) {
await _invalidUriToast(context, e.message);
} catch (e) {
await _invalidUriToast(context, e.toString());
}
}
Future<void> _invalidUriToast(BuildContext context, String message) async {
await showPopUp<void>(
context: context,
builder: (BuildContext context) {
return AlertWithOneAction(
alertTitle: S.of(context).error,
alertContent: message,
buttonText: S.of(context).ok,
buttonAction: Navigator.of(context).pop,
alertBarrierDismissible: false,
);
},
);
}
@override
Widget body(BuildContext context) {
return Observer(
builder: (context) {
return Column(
children: [
Padding(
padding: EdgeInsets.symmetric(horizontal: 24),
child: Column(
children: [
SizedBox(height: 24),
Text(
S.current.connectWalletPrompt,
style: TextStyle(
fontSize: 16.0,
fontWeight: FontWeight.normal,
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
),
),
SizedBox(height: 16),
PrimaryButton(
text: S.current.newConnection,
color: Theme.of(context).primaryColor,
textColor: Colors.white,
onPressed: () => _onScanQrCode(context, web3wallet),
),
],
),
),
SizedBox(height: 48),
Expanded(
child: Visibility(
visible: web3walletService.pairings.isEmpty,
child: Center(
child: Text(
S.current.activeConnectionsPrompt,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16.0,
fontWeight: FontWeight.normal,
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
),
),
),
replacement: ListView.builder(
itemCount: web3walletService.pairings.length,
itemBuilder: (BuildContext context, int index) {
final pairing = web3walletService.pairings[index];
return PairingItemWidget(
key: ValueKey(pairing.topic),
pairing: pairing,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => WalletConnectPairingDetailsPage(
pairing: pairing,
web3walletService: web3walletService,
),
),
);
},
);
},
),
),
),
SizedBox(height: 48),
],
);
},
);
}
}

View file

@ -0,0 +1,186 @@
import 'dart:developer';
import 'package:cake_wallet/core/wallet_connect/web3wallet_service.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:flutter/material.dart';
import 'package:walletconnect_flutter_v2/walletconnect_flutter_v2.dart';
import 'utils/namespace_model_builder.dart';
class WalletConnectPairingDetailsPage extends StatefulWidget {
final PairingInfo pairing;
final Web3WalletService web3walletService;
const WalletConnectPairingDetailsPage({
required this.pairing,
required this.web3walletService,
super.key,
});
@override
WalletConnectPairingDetailsPageState createState() => WalletConnectPairingDetailsPageState();
}
class WalletConnectPairingDetailsPageState extends State<WalletConnectPairingDetailsPage> {
List<Widget> sessionWidgets = [];
late String expiryDate;
@override
void initState() {
super.initState();
initDateTime();
initSessions();
}
void initDateTime() {
DateTime dateTime = DateTime.fromMillisecondsSinceEpoch(widget.pairing.expiry * 1000);
int year = dateTime.year;
int month = dateTime.month;
int day = dateTime.day;
expiryDate = '$year-${month.toString().padLeft(2, '0')}-${day.toString().padLeft(2, '0')}';
}
void initSessions() {
List<SessionData> sessions = widget.web3walletService.getSessionsForPairingInfo(widget.pairing);
for (final SessionData session in sessions) {
List<Widget> namespaceWidget = ConnectionWidgetBuilder.buildFromNamespaces(
session.topic,
session.namespaces,
widget.web3walletService.getWeb3Wallet(),
);
// Loop through and add the namespace widgets, but put 20 pixels between each one
for (int i = 0; i < namespaceWidget.length; i++) {
sessionWidgets.add(namespaceWidget[i]);
if (i != namespaceWidget.length - 1) {
sessionWidgets.add(const SizedBox(height: 20.0));
}
}
}
}
@override
Widget build(BuildContext context) {
return WCCDetailsWidget(
widget.pairing,
expiryDate,
sessionWidgets,
widget.web3walletService,
);
}
}
class WCCDetailsWidget extends BasePage {
WCCDetailsWidget(
this.pairing,
this.expiryDate,
this.sessionWidgets,
this.web3walletService,
);
final PairingInfo pairing;
final String expiryDate;
final List<Widget> sessionWidgets;
final Web3WalletService web3walletService;
@override
Widget body(BuildContext context) {
return Scaffold(
body: SingleChildScrollView(
child: Container(
padding: const EdgeInsets.all(8),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: CircleAvatar(
backgroundImage: (pairing.peerMetadata!.icons.isNotEmpty
? NetworkImage(pairing.peerMetadata!.icons[0])
: const AssetImage('assets/images/default_icon.png'))
as ImageProvider<Object>,
),
),
const SizedBox(height: 20.0),
Text(
pairing.peerMetadata!.name,
style: TextStyle(
fontSize: 16.0,
fontWeight: FontWeight.w500,
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
),
),
const SizedBox(height: 16.0),
Text(
pairing.peerMetadata!.url,
style: TextStyle(
fontSize: 14.0,
fontWeight: FontWeight.normal,
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
),
),
const SizedBox(height: 8.0),
Text(
'${S.current.expiresOn}: $expiryDate',
style: TextStyle(
fontSize: 14.0,
fontWeight: FontWeight.normal,
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
),
),
const SizedBox(height: 20.0),
Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: sessionWidgets,
),
const SizedBox(height: 20.0),
PrimaryButton(
onPressed: () =>
_onDeleteButtonPressed(context, pairing.peerMetadata!.name, web3walletService),
text: S.current.delete,
color: Theme.of(context).primaryColor,
textColor: Colors.white,
),
],
),
),
),
);
}
Future<void> _onDeleteButtonPressed(
BuildContext context, String dAppName, Web3WalletService web3walletService) async {
bool confirmed = false;
await showPopUp<void>(
context: context,
builder: (BuildContext dialogContext) {
return AlertWithTwoActions(
alertTitle: S.of(context).delete,
alertContent: '${S.current.deleteConnectionConfirmationPrompt} $dAppName?',
leftButtonText: S.of(context).cancel,
rightButtonText: S.of(context).delete,
actionLeftButton: () => Navigator.of(dialogContext).pop(),
actionRightButton: () {
confirmed = true;
Navigator.of(dialogContext).pop();
},
);
},
);
if (confirmed) {
try {
await web3walletService.disconnectSession(pairing.topic);
Navigator.of(context).pop();
} catch (e) {
log(e.toString());
}
}
}
}

View file

@ -0,0 +1,102 @@
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
import 'package:flutter/material.dart';
import '../../../../core/wallet_connect/models/connection_model.dart';
class ConnectionItemWidget extends StatelessWidget {
const ConnectionItemWidget({required this.model, Key? key}) : super(key: key);
final ConnectionModel model;
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(8),
),
padding: const EdgeInsets.all(8),
margin: const EdgeInsetsDirectional.only(top: 8),
child: Visibility(
visible: model.elements != null,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
model.title ?? '',
style: TextStyle(
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 8),
if (model.elements != null)
Wrap(
spacing: 4,
runSpacing: 4,
direction: Axis.horizontal,
children: model.elements!
.map((e) => _ModelElementWidget(model: model, modelElement: e))
.toList(),
),
],
),
replacement: _NoModelElementWidget(model: model),
),
);
}
}
class _NoModelElementWidget extends StatelessWidget {
const _NoModelElementWidget({required this.model});
final ConnectionModel model;
@override
Widget build(BuildContext context) {
return Text(
model.text!,
style: TextStyle(
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
fontSize: 14,
fontWeight: FontWeight.w600,
),
);
}
}
class _ModelElementWidget extends StatelessWidget {
const _ModelElementWidget({
required this.model,
required this.modelElement,
});
final ConnectionModel model;
final String modelElement;
@override
Widget build(BuildContext context) {
return InkWell(
onTap: model.elementActions != null ? model.elementActions![modelElement] : null,
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.background,
borderRadius: BorderRadius.circular(6),
),
padding: const EdgeInsets.all(8),
child: Text(
modelElement,
style: TextStyle(
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
fontSize: 14,
fontWeight: FontWeight.w600,
),
maxLines: 10,
overflow: TextOverflow.ellipsis,
),
),
);
}
}

View file

@ -0,0 +1,166 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first
import 'package:cake_wallet/generated/i18n.dart';
import 'package:flutter/material.dart';
import 'package:walletconnect_flutter_v2/walletconnect_flutter_v2.dart';
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
import '../../../../core/wallet_connect/models/auth_request_model.dart';
import '../../../../core/wallet_connect/models/connection_model.dart';
import '../../../../core/wallet_connect/models/session_request_model.dart';
import '../utils/namespace_model_builder.dart';
import 'connection_widget.dart';
class ConnectionRequestWidget extends StatefulWidget {
const ConnectionRequestWidget({
required this.wallet,
this.authRequest,
this.sessionProposal,
Key? key,
}) : super(key: key);
final Web3Wallet wallet;
final AuthRequestModel? authRequest;
final SessionRequestModel? sessionProposal;
@override
State<ConnectionRequestWidget> createState() => _ConnectionRequestWidgetState();
}
class _ConnectionRequestWidgetState extends State<ConnectionRequestWidget> {
ConnectionMetadata? metadata;
@override
void initState() {
super.initState();
// Get the connection metadata
metadata = widget.authRequest?.request.requester ?? widget.sessionProposal?.request.proposer;
}
@override
Widget build(BuildContext context) {
if (metadata == null) {
return Text(
S.current.error,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.normal,
color: Theme.of(context).extension<CakeTextTheme>()!.secondaryTextColor,
),
);
}
return _ConnectionMetadataDisplayWidget(
metadata: metadata,
authRequest: widget.authRequest,
sessionProposal: widget.sessionProposal,
wallet: widget.wallet,
);
}
}
class _ConnectionMetadataDisplayWidget extends StatelessWidget {
const _ConnectionMetadataDisplayWidget({
required this.metadata,
required this.wallet,
this.authRequest,
required this.sessionProposal,
});
final ConnectionMetadata? metadata;
final Web3Wallet wallet;
final AuthRequestModel? authRequest;
final SessionRequestModel? sessionProposal;
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: Color.fromARGB(255, 18, 18, 19),
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Text(
metadata!.metadata.name,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.normal,
color: Theme.of(context).extension<CakeTextTheme>()!.secondaryTextColor,
),
textAlign: TextAlign.center,
),
Text(
S.current.wouoldLikeToConnect,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.normal,
color: Theme.of(context).extension<CakeTextTheme>()!.secondaryTextColor,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
metadata!.metadata.url,
style: TextStyle(
fontSize: 16.0,
fontWeight: FontWeight.normal,
color: Theme.of(context).extension<CakeTextTheme>()!.secondaryTextColor,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Visibility(
visible: authRequest != null,
child: _AuthRequestWidget(wallet: wallet, authRequest: authRequest),
//If authRequest is null, sessionProposal is not null.
replacement: _SessionProposalWidget(sessionProposal: sessionProposal!),
),
],
),
);
}
}
class _AuthRequestWidget extends StatelessWidget {
const _AuthRequestWidget({required this.wallet, this.authRequest});
final Web3Wallet wallet;
final AuthRequestModel? authRequest;
@override
Widget build(BuildContext context) {
final model = ConnectionModel(
text: wallet.formatAuthMessage(
iss: 'did:pkh:eip155:1:${authRequest!.iss}',
cacaoPayload: CacaoRequestPayload.fromPayloadParams(
authRequest!.request.payloadParams,
),
),
);
return ConnectionWidget(
title: S.current.message,
info: [model],
);
}
}
class _SessionProposalWidget extends StatelessWidget {
const _SessionProposalWidget({required this.sessionProposal});
final SessionRequestModel sessionProposal;
@override
Widget build(BuildContext context) {
// Create the connection models using the required and optional namespaces provided by the proposal data
// The key is the title and the list of values is the data
final List<ConnectionWidget> views = ConnectionWidgetBuilder.buildFromRequiredNamespaces(
sessionProposal.request.requiredNamespaces,
);
return Column(children: views);
}
}

View file

@ -0,0 +1,45 @@
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
import 'package:flutter/material.dart';
import '../../../../core/wallet_connect/models/connection_model.dart';
import 'connection_item_widget.dart';
class ConnectionWidget extends StatelessWidget {
const ConnectionWidget({required this.title, required this.info, super.key});
final String title;
final List<ConnectionModel> info;
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: Theme.of(context).primaryColorLight,
borderRadius: BorderRadius.circular(8),
),
padding: const EdgeInsets.all(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.background,
borderRadius: BorderRadius.circular(8),
),
padding: EdgeInsets.symmetric(vertical: 8, horizontal: 8),
child: Text(
title,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
),
),
),
const SizedBox(height: 8),
...info.map((e) => ConnectionItemWidget(model: e)),
],
),
);
}
}

View file

@ -0,0 +1,36 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:flutter/material.dart';
class BottomSheetMessageDisplayWidget extends StatelessWidget {
final String message;
final bool isError;
const BottomSheetMessageDisplayWidget({super.key, required this.message, this.isError = true});
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
isError ? S.current.error : S.current.successful,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.normal,
color: Colors.white,
),
),
SizedBox(height: 8),
Text(
message,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.normal,
color: Colors.white,
),
),
],
);
}
}

View file

@ -0,0 +1,62 @@
import 'package:cake_wallet/core/wallet_connect/wc_bottom_sheet_service.dart';
import 'package:flutter/material.dart';
import '../../../../../core/wallet_connect/models/bottom_sheet_queue_item_model.dart';
class BottomSheetListener extends StatefulWidget {
final BottomSheetService bottomSheetService;
final Widget child;
const BottomSheetListener({
required this.child,
required this.bottomSheetService,
super.key,
});
@override
BottomSheetListenerState createState() => BottomSheetListenerState();
}
class BottomSheetListenerState extends State<BottomSheetListener> {
@override
void initState() {
super.initState();
widget.bottomSheetService.currentSheet.addListener(_showBottomSheet);
}
@override
void dispose() {
widget.bottomSheetService.currentSheet.removeListener(_showBottomSheet);
super.dispose();
}
Future<void> _showBottomSheet() async {
if (widget.bottomSheetService.currentSheet.value != null) {
BottomSheetQueueItemModel item = widget.bottomSheetService.currentSheet.value!;
final value = await showModalBottomSheet(
context: context,
isDismissible: item.isModalDismissible,
backgroundColor: Color.fromARGB(0, 0, 0, 0),
isScrollControlled: true,
constraints: BoxConstraints(maxHeight: MediaQuery.of(context).size.height * 0.9),
builder: (context) {
return Container(
decoration: const BoxDecoration(
color: Color.fromARGB(255, 18, 18, 19),
borderRadius: BorderRadius.all(Radius.circular(16)),
),
padding: const EdgeInsets.all(16),
margin: const EdgeInsets.all(16),
child: item.widget,
);
},
);
item.completer.complete(value);
widget.bottomSheetService.resetCurrentSheet();
}
}
@override
Widget build(BuildContext context) => widget.child;
}

View file

@ -0,0 +1,48 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
import 'package:flutter/material.dart';
class Web3RequestModal extends StatelessWidget {
const Web3RequestModal({required this.child, this.onAccept, this.onReject, super.key});
final Widget child;
final VoidCallback? onAccept;
final VoidCallback? onReject;
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
child,
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Expanded(
child: PrimaryButton(
onPressed: onReject ?? () => Navigator.of(context).pop(false),
text: S.current.reject,
color: Theme.of(context).colorScheme.error,
textColor: Theme.of(context).colorScheme.onError,
),
),
const SizedBox(width: 16),
Expanded(
child: PrimaryButton(
onPressed: onAccept ?? () => Navigator.of(context).pop(true),
text: S.current.approve,
color: Theme.of(context).primaryColor,
textColor: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
),
),
],
),
],
),
);
}
}

View file

@ -0,0 +1,82 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
import 'package:cake_wallet/themes/extensions/receive_page_theme.dart';
import 'package:flutter/material.dart';
import 'package:walletconnect_flutter_v2/apis/core/pairing/utils/pairing_models.dart';
class PairingItemWidget extends StatelessWidget {
const PairingItemWidget({required this.pairing, required this.onTap, super.key});
final PairingInfo pairing;
final void Function() onTap;
@override
Widget build(BuildContext context) {
PairingMetadata? metadata = pairing.peerMetadata;
if (metadata == null) {
return SizedBox.shrink();
}
DateTime dateTime = DateTime.fromMillisecondsSinceEpoch(pairing.expiry * 1000);
int year = dateTime.year;
int month = dateTime.month;
int day = dateTime.day;
String expiryDate =
'$year-${month.toString().padLeft(2, '0')}-${day.toString().padLeft(2, '0')}';
return ListTile(
leading: CircleAvatar(
backgroundImage: (metadata.icons.isNotEmpty
? NetworkImage(metadata.icons[0])
: const AssetImage(
'assets/images/default_icon.png',
)) as ImageProvider<Object>,
),
title: Text(
metadata.name,
style: TextStyle(
fontSize: 16.0,
fontWeight: FontWeight.w700,
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
metadata.url,
style: TextStyle(
fontSize: 14.0,
fontWeight: FontWeight.w700,
color: Theme.of(context).extension<CakeTextTheme>()!.secondaryTextColor,
),
),
Text(
'${S.current.expiresOn}: $expiryDate',
style: TextStyle(
fontSize: 14.0,
fontWeight: FontWeight.w700,
color: Theme.of(context).extension<CakeTextTheme>()!.secondaryTextColor,
),
),
],
),
trailing: Container(
height: 40,
width: 44,
padding: EdgeInsets.all(10),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Theme.of(context).extension<ReceivePageTheme>()!.iconsBackgroundColor,
),
child: Icon(
Icons.edit,
size: 14,
color: Theme.of(context).extension<ReceivePageTheme>()!.iconsColor,
),
),
onTap: onTap,
);
}
}

View file

@ -1,4 +1,4 @@
import 'package:cw_nano/nano_util.dart';
import 'package:cake_wallet/nano/nano.dart';
class PaymentRequest {
PaymentRequest(this.address, this.amount, this.note, this.scheme);
@ -16,10 +16,12 @@ class PaymentRequest {
scheme = uri.scheme;
}
if (address.contains("nano")) {
amount = NanoUtil.getRawAsUsableString(amount, NanoUtil.rawPerNano);
} else if (address.contains("ban")) {
amount = NanoUtil.getRawAsUsableString(amount, NanoUtil.rawPerBanano);
if (nano != null) {
if (address.contains("nano")) {
amount = nanoUtil!.getRawAsUsableString(amount, nanoUtil!.rawPerNano);
} else if (address.contains("ban")) {
amount = nanoUtil!.getRawAsUsableString(amount, nanoUtil!.rawPerBanano);
}
}
return PaymentRequest(address, amount, note, scheme);

View file

@ -284,7 +284,6 @@ abstract class DashboardViewModelBase with Store {
Map<String, List<FilterItem>> filterItems;
BuyProviderType get defaultBuyProvider => settingsStore.defaultBuyProvider;
bool get isBuyEnabled => settingsStore.isBitcoinBuyEnabled;

View file

@ -93,11 +93,10 @@ class TransactionListItem extends ActionListItem with Keyable {
price: price);
break;
case WalletType.nano:
final nanoTransaction = transaction as NanoTransactionInfo;
amount = calculateFiatAmountRaw(
cryptoAmount:
NanoUtil.getRawAsDecimal(nanoTransaction.amountRaw.toString(), NanoUtil.rawPerNano)
.toDouble(),
cryptoAmount: nanoUtil!
.getRawAsDecimal(nano!.getTransactionAmountRaw(transaction).toString(), nanoUtil!.rawPerNano)
.toDouble(),
price: price);
break;
case WalletType.ethereum:

View file

@ -86,13 +86,12 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
super(appStore: appStore) {
_useTorOnly = _settingsStore.exchangeStatus == ExchangeApiMode.torOnly;
_setProviders();
const excludeDepositCurrencies = [CryptoCurrency.btt, CryptoCurrency.nano];
const excludeDepositCurrencies = [CryptoCurrency.btt];
const excludeReceiveCurrencies = [
CryptoCurrency.xlm,
CryptoCurrency.xrp,
CryptoCurrency.bnb,
CryptoCurrency.btt,
CryptoCurrency.nano
CryptoCurrency.btt
];
_initialPairBasedOnWallet();

View file

@ -2,12 +2,9 @@ import 'package:cake_wallet/nano/nano.dart';
import 'package:cw_core/nano_account.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_type.dart';
// import 'package:cw_nano/nano_account_list.dart';
import 'package:flutter/foundation.dart';
import 'package:mobx/mobx.dart';
import 'package:cake_wallet/core/execution_state.dart';
import 'package:cake_wallet/view_model/monero_account_list/account_list_item.dart';
part 'nano_account_edit_or_create_view_model.g.dart';
@ -16,9 +13,7 @@ class NanoAccountEditOrCreateViewModel = NanoAccountEditOrCreateViewModelBase
abstract class NanoAccountEditOrCreateViewModelBase with Store {
NanoAccountEditOrCreateViewModelBase(this._nanoAccountList,
/*this._bananoAccountList,*/
{required WalletBase wallet,
NanoAccount? accountListItem})
{required WalletBase wallet, NanoAccount? accountListItem})
: state = InitialExecutionState(),
isEdit = accountListItem != null,
label = accountListItem?.label ?? '',
@ -34,7 +29,6 @@ abstract class NanoAccountEditOrCreateViewModelBase with Store {
String label;
final NanoAccountList _nanoAccountList;
// final BananoAccountList? _bananoAccountList;
final NanoAccount? _accountListItem;
final WalletBase _wallet;
@ -42,10 +36,6 @@ abstract class NanoAccountEditOrCreateViewModelBase with Store {
if (_wallet.type == WalletType.nano) {
await saveNano();
}
// if (_wallet.type == WalletType.banano) {
// await saveBanano();
// }
}
Future<void> saveNano() async {
@ -53,7 +43,8 @@ abstract class NanoAccountEditOrCreateViewModelBase with Store {
state = IsExecutingState();
if (_accountListItem != null) {
await _nanoAccountList.setLabelAccount(_wallet, accountIndex: _accountListItem!.id, label: label);
await _nanoAccountList.setLabelAccount(_wallet,
accountIndex: _accountListItem!.id, label: label);
} else {
await _nanoAccountList.addAccount(_wallet, label: label);
}
@ -64,26 +55,4 @@ abstract class NanoAccountEditOrCreateViewModelBase with Store {
state = FailureState(e.toString());
}
}
// Future<void> saveBanano() async {
// if (!(_wallet.type == WalletType.banano)) {
// return;
// }
// try {
// state = IsExecutingState();
// if (_accountListItem != null) {
// await _bananoAccountList!
// .setLabelAccount(_wallet, accountIndex: _accountListItem!.id, label: label);
// } else {
// await _bananoAccountList!.addAccount(_wallet, label: label);
// }
// await _wallet.save();
// state = ExecutedSuccessfullyState();
// } catch (e) {
// state = FailureState(e.toString());
// }
// }
}

View file

@ -36,17 +36,6 @@ abstract class NanoAccountListViewModelBase with Store {
.toList();
}
// if (_wallet.type == WalletType.banano) {
// return banano
// !.getAccountList(_wallet)
// .accounts.map((acc) => AccountListItem(
// label: acc.label,
// id: acc.id,
// balance: acc.balance,
// isSelected: acc.id == banano!.getCurrentAccount(_wallet).id))
// .toList();
// }
throw Exception('Unexpected wallet type: ${_wallet.type}');
}
@ -61,13 +50,5 @@ abstract class NanoAccountListViewModelBase with Store {
item.balance,
);
}
// if (_wallet.type == WalletType.banano) {
// banano!.setCurrentAccount(
// _wallet,
// item.id,
// item.label,
// );
// }
}
}

View file

@ -1,4 +1,3 @@
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/entities/priority_for_wallet_type.dart';
import 'package:cake_wallet/entities/transaction_description.dart';
import 'package:cake_wallet/ethereum/ethereum.dart';
@ -323,8 +322,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
await pendingTransaction!.commit();
if (walletType == WalletType.nano) {
var wallet = getIt.get<AppStore>().wallet as NanoWallet?;
wallet?.updateTransactions();
nano!.updateTransactions(wallet);
}
if (pendingTransaction!.id.isNotEmpty) {
@ -432,7 +430,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
String translateErrorMessage(String error, WalletType walletType, CryptoCurrency currency,) {
if (walletType == WalletType.ethereum || walletType == WalletType.haven) {
if (error.contains('gas required exceeds allowance (0)') || error.contains('insufficient funds for gas')) {
if (error.contains('gas required exceeds allowance') || error.contains('insufficient funds for')) {
return S.current.do_not_have_enough_gas_asset(currency.toString());
}
}

View file

@ -25,7 +25,7 @@ abstract class OtherSettingsViewModelBase with Store {
final priority = _settingsStore.priority[_wallet.type];
final priorities = priorityForWalletType(_wallet.type);
if (!priorities.contains(priority)) {
if (!priorities.contains(priority) && priorities.isNotEmpty) {
_settingsStore.priority[_wallet.type] = priorities.first;
}
}

View file

@ -239,7 +239,7 @@ abstract class TransactionDetailsViewModelBase with Store {
StandartListItem(title: S.current.transaction_details_transaction_id, value: tx.id),
StandartListItem(
title: S.current.transaction_details_date, value: dateFormat.format(tx.date)),
// StandartListItem(title: S.current.confirmed, value: (tx.confirmations > 0).toString()),
StandartListItem(title: S.current.confirmations, value: (tx.confirmations > 0).toString()),
StandartListItem(title: S.current.transaction_details_height, value: '${tx.height}'),
StandartListItem(title: S.current.transaction_details_amount, value: tx.amountFormatted()),
];

View file

@ -294,9 +294,9 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
_baseItems = [];
if (wallet.type == WalletType.monero ||
wallet.type == WalletType.haven ||
wallet.type == WalletType.haven /*||
wallet.type == WalletType.nano ||
wallet.type == WalletType.banano) {
wallet.type == WalletType.banano*/) {
_baseItems.add(WalletAccountListHeader());
}

View file

@ -1,11 +1,5 @@
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/store/app_store.dart';
import 'package:cw_bitcoin/bitcoin_wallet_service.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_nano/nano_util.dart';
import 'package:cw_nano/nano_wallet_service.dart';
import 'package:mobx/mobx.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:cake_wallet/view_model/restore/restore_mode.dart';
part 'wallet_restore_choose_derivation_view_model.g.dart';

View file

@ -2,10 +2,6 @@ import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/nano/nano.dart';
import 'package:cake_wallet/ethereum/ethereum.dart';
import 'package:cw_bitcoin/bitcoin_wallet_service.dart';
import 'package:cw_core/node.dart';
import 'package:cw_nano/nano_wallet.dart';
import 'package:cw_nano/nano_wallet_service.dart';
import 'package:hive/hive.dart';
import 'package:mobx/mobx.dart';
import 'package:cake_wallet/store/app_store.dart';
@ -165,19 +161,16 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store {
final seedKey = options['private_key'] as String?;
final mnemonic = options['seed'] as String?;
WalletType walletType = options['walletType'] as WalletType;
AppStore appStore = getIt.get<AppStore>();
Node node = appStore.settingsStore.getCurrentNode(walletType);
var appStore = getIt.get<AppStore>();
var node = appStore.settingsStore.getCurrentNode(walletType);
switch (type) {
case WalletType.bitcoin:
return bitcoin!.compareDerivationMethods(mnemonic: mnemonic!, node: node);
// case WalletType.litecoin:
// return bitcoin!.createBitcoinRestoreWalletFromSeedCredentials(
// name: name, mnemonic: seed, password: password);
case WalletType.nano:
return await NanoWalletService.compareDerivationMethods(
return nanoUtil!.compareDerivationMethods(
mnemonic: mnemonic,
seedKey: seedKey,
privateKey: seedKey,
node: node,
);
default:

View file

@ -82,6 +82,12 @@ dependencies:
shared_preferences_android: 2.0.17
url_launcher_android: 6.0.24
sensitive_clipboard: ^1.0.0
walletconnect_flutter_v2: ^2.1.4
eth_sig_util: ^0.0.9
ens_dart:
git:
url: https://github.com/cake-tech/ens_dart.git
ref: main
bitcoin_flutter:
git:
url: https://github.com/cake-tech/bitcoin_flutter.git

View file

@ -695,8 +695,27 @@
"default_buy_provider": "مزود شراء الافتراضي",
"ask_each_time": "اسأل في كل مرة",
"buy_provider_unavailable": "مزود حاليا غير متوفر.",
"signTransaction": " ﺔﻠﻣﺎﻌﻤﻟﺍ ﻊﻴﻗﻮﺗ",
"errorGettingCredentials": "ﺩﺎﻤﺘﻋﻻﺍ ﺕﺎﻧﺎﻴﺑ ﻰﻠﻋ ﻝﻮﺼﺤﻟﺍ ءﺎﻨﺛﺃ ﺄﻄﺧ ﺙﺪﺣ :ﻞﺸﻓ",
"errorSigningTransaction": "ﺔﻠﻣﺎﻌﻤﻟﺍ ﻊﻴﻗﻮﺗ ءﺎﻨﺛﺃ ﺄﻄﺧ ﺙﺪﺣ",
"pairingInvalidEvent": "ﺢﻟﺎﺻ ﺮﻴﻏ ﺙﺪﺣ ﻥﺍﺮﻗﺇ",
"chains": "ﻞﺳﻼﺴﻟﺍ",
"methods": " ﻕﺮﻃُ",
"events": "ﺙﺍﺪﺣﻷﺍ",
"reject": "ﺾﻓﺮﻳ",
"approve": "ﺪﻤﺘﻌﻳ",
"expiresOn": "ﻲﻓ ﻪﺘﻴﺣﻼﺻ ﻲﻬﺘﻨﺗ",
"walletConnect": "WalletConnect",
"nullURIError": "ﻍﺭﺎﻓ (URI) ﻢﻈﺘﻨﻤﻟﺍ ﺩﺭﺍﻮﻤﻟﺍ ﻑﺮﻌﻣ",
"connectWalletPrompt": "ﺕﻼﻣﺎﻌﻤﻟﺍ ءﺍﺮﺟﻹ WalletConnect ﻊﻣ ﻚﺘﻈﻔﺤﻣ ﻞﻴﺻﻮﺘﺑ ﻢﻗ",
"newConnection": "ﺪﻳﺪﺟ ﻝﺎﺼﺗﺍ",
"activeConnectionsPrompt": "ﺎﻨﻫ ﺔﻄﺸﻨﻟﺍ ﺕﻻﺎﺼﺗﻻﺍ ﺮﻬﻈﺘﺳ",
"deleteConnectionConfirmationPrompt": "ـﺑ ﻝﺎﺼﺗﻻﺍ ﻑﺬﺣ ﺪﻳﺮﺗ ﻚﻧﺃ ﺪﻛﺄﺘﻣ ﺖﻧﺃ ﻞﻫ",
"event": "ﺙﺪﺣ",
"successful": "ﺢﺟﺎﻧ",
"wouoldLikeToConnect": "ﻝﺎﺼﺗﻻﺍ ﻲﻓ ﺐﻏﺮﺗ",
"message": "ﺔﻟﺎﺳﺭ",
"do_not_have_enough_gas_asset": "ليس لديك ما يكفي من ${currency} لإجراء معاملة وفقًا لشروط شبكة blockchain الحالية. أنت بحاجة إلى المزيد من ${currency} لدفع رسوم شبكة blockchain، حتى لو كنت ترسل أصلًا مختلفًا.",
"totp_auth_url": " TOTP ﺔﻗﺩﺎﺼﻤﻟ URL ﻥﺍﻮﻨﻋ"
"totp_auth_url": "TOTP ﺔﻗﺩﺎﺼﻤﻟ URL ﻥﺍﻮﻨﻋ",
"awaitDAppProcessing": ".ﺔﺠﻟﺎﻌﻤﻟﺍ ﻦﻣ dApp ﻲﻬﺘﻨﻳ ﻰﺘﺣ ﺭﺎﻈﺘﻧﻻﺍ ﻰﺟﺮﻳ"
}

View file

@ -691,6 +691,27 @@
"default_buy_provider": "Доставчик по подразбиране купува",
"ask_each_time": "Питайте всеки път",
"buy_provider_unavailable": "Понастоящем доставчик не е наличен.",
"signTransaction": "Подпишете транзакция",
"errorGettingCredentials": "Неуспешно: Грешка при получаване на идентификационни данни",
"errorSigningTransaction": "Възникна грешка при подписване на транзакция",
"pairingInvalidEvent": "Невалидно събитие при сдвояване",
"chains": "Вериги",
"methods": "Методи",
"events": "събития",
"reject": "Отхвърляне",
"approve": "Одобряване",
"expiresOn": "Изтича на",
"walletConnect": "WalletConnect",
"nullURIError": "URI е нула",
"connectWalletPrompt": "Свържете портфейла си с WalletConnect, за да извършвате транзакции",
"newConnection": "Нова връзка",
"activeConnectionsPrompt": "Тук ще се появят активни връзки",
"deleteConnectionConfirmationPrompt": "Сигурни ли сте, че искате да изтриете връзката към",
"event": "Събитие",
"successful": "Успешен",
"wouoldLikeToConnect": "иска да се свърже",
"message": "Съобщение",
"do_not_have_enough_gas_asset": "Нямате достатъчно ${currency}, за да извършите транзакция с текущите условия на блокчейн мрежата. Имате нужда от повече ${currency}, за да платите таксите за блокчейн мрежа, дори ако изпращате различен актив.",
"totp_auth_url": "TOTP AUTH URL"
"totp_auth_url": "TOTP AUTH URL",
"awaitDAppProcessing": "Моля, изчакайте dApp да завърши обработката."
}

View file

@ -691,6 +691,27 @@
"default_buy_provider": "Výchozí poskytovatel nákupu",
"ask_each_time": "Zeptejte se pokaždé",
"buy_provider_unavailable": "Poskytovatel aktuálně nedostupný.",
"signTransaction": "Podepsat transakci",
"errorGettingCredentials": "Selhalo: Chyba při získávání přihlašovacích údajů",
"errorSigningTransaction": "Při podepisování transakce došlo k chybě",
"pairingInvalidEvent": "Neplatná událost párování",
"chains": "Řetězy",
"methods": "Metody",
"events": "Události",
"reject": "Odmítnout",
"approve": "Schvalovat",
"expiresOn": "Vyprší dne",
"walletConnect": "WalletConnect",
"nullURIError": "URI je nulové",
"connectWalletPrompt": "Propojte svou peněženku s WalletConnect a provádějte transakce",
"newConnection": "Nové připojení",
"activeConnectionsPrompt": "Zde se zobrazí aktivní připojení",
"deleteConnectionConfirmationPrompt": "Jste si jisti, že chcete smazat připojení k?",
"event": "událost",
"successful": "Úspěšný",
"wouoldLikeToConnect": "by se chtělo připojit",
"message": "Zpráva",
"do_not_have_enough_gas_asset": "Nemáte dostatek ${currency} k provedení transakce s aktuálními podmínkami blockchainové sítě. K placení poplatků za blockchainovou síť potřebujete více ${currency}, i když posíláte jiné aktivum.",
"totp_auth_url": "URL AUTH TOTP"
"totp_auth_url": "URL AUTH TOTP",
"awaitDAppProcessing": "Počkejte, až dApp dokončí zpracování."
}

View file

@ -699,6 +699,27 @@
"default_buy_provider": "Standard-Kaufanbieter",
"ask_each_time": "Jedes Mal fragen",
"buy_provider_unavailable": "Anbieter derzeit nicht verfügbar.",
"signTransaction": "Transaktion unterzeichnen",
"errorGettingCredentials": "Fehlgeschlagen: Fehler beim Abrufen der Anmeldeinformationen",
"errorSigningTransaction": "Beim Signieren der Transaktion ist ein Fehler aufgetreten",
"pairingInvalidEvent": "Paarung ungültiges Ereignis",
"chains": "Ketten",
"methods": "Methoden",
"events": "Veranstaltungen",
"reject": "Ablehnen",
"approve": "Genehmigen",
"expiresOn": "Läuft aus am",
"walletConnect": "WalletConnect",
"nullURIError": "URI ist null",
"connectWalletPrompt": "Verbinden Sie Ihr Wallet mit WalletConnect, um Transaktionen durchzuführen",
"newConnection": "Neue Verbindung",
"activeConnectionsPrompt": "Hier werden aktive Verbindungen angezeigt",
"deleteConnectionConfirmationPrompt": "Sind Sie sicher, dass Sie die Verbindung zu löschen möchten?",
"event": "Ereignis",
"successful": "Erfolgreich",
"wouoldLikeToConnect": "möchte mich gerne vernetzen",
"message": "Nachricht",
"do_not_have_enough_gas_asset": "Sie verfügen nicht über genügend ${currency}, um eine Transaktion unter den aktuellen Bedingungen des Blockchain-Netzwerks durchzuführen. Sie benötigen mehr ${currency}, um die Gebühren für das Blockchain-Netzwerk zu bezahlen, auch wenn Sie einen anderen Vermögenswert senden.",
"totp_auth_url": "TOTP-Auth-URL"
"totp_auth_url": "TOTP-Auth-URL",
"awaitDAppProcessing": "Bitte warten Sie, bis die dApp die Verarbeitung abgeschlossen hat."
}

View file

@ -700,6 +700,27 @@
"ask_each_time": "Ask each time",
"robinhood_option_description": "Buy and transfer instantly using your debit card, bank account, or Robinhood balance. USA only.",
"buy_provider_unavailable": "Provider currently unavailable.",
"signTransaction": "Sign Transaction",
"errorGettingCredentials": "Failed: Error while getting credentials",
"errorSigningTransaction": "An error has occured while signing transaction",
"pairingInvalidEvent": "Pairing Invalid Event",
"chains": "Chains",
"methods": "Methods",
"events": "Events",
"reject": "Reject",
"approve": "Approve",
"expiresOn": "Expires on",
"walletConnect": "WalletConnect",
"nullURIError": "URI is null",
"connectWalletPrompt": "Connect your wallet with WalletConnect to make transactions",
"newConnection": "New Connection",
"activeConnectionsPrompt": "Active connections will appear here",
"deleteConnectionConfirmationPrompt": "Are you sure that you want to delete the connection to",
"event": "Event",
"successful": "Successful",
"wouoldLikeToConnect": "would like to connect",
"message": "Message",
"do_not_have_enough_gas_asset": "You do not have enough ${currency} to make a transaction with the current blockchain network conditions. You need more ${currency} to pay blockchain network fees, even if you are sending a different asset.",
"totp_auth_url": "TOTP AUTH URL"
"totp_auth_url": "TOTP AUTH URL",
"awaitDAppProcessing": "Kindly wait for the dApp to finish processing."
}

View file

@ -699,6 +699,27 @@
"default_buy_provider": "Proveedor de compra predeterminado",
"ask_each_time": "Pregunta cada vez",
"buy_provider_unavailable": "Proveedor actualmente no disponible.",
"signTransaction": "Firmar transacción",
"errorGettingCredentials": "Error: error al obtener las credenciales",
"errorSigningTransaction": "Se ha producido un error al firmar la transacción.",
"pairingInvalidEvent": "Evento de emparejamiento no válido",
"chains": "Cadenas",
"methods": "Métodos",
"events": "Eventos",
"reject": "Rechazar",
"approve": "Aprobar",
"expiresOn": "Expira el",
"walletConnect": "MonederoConectar",
"nullURIError": "URI es nula",
"connectWalletPrompt": "Conecte su billetera con WalletConnect para realizar transacciones",
"newConnection": "Nueva conexión",
"activeConnectionsPrompt": "Las conexiones activas aparecerán aquí",
"deleteConnectionConfirmationPrompt": "¿Está seguro de que desea eliminar la conexión a",
"event": "Evento",
"successful": "Exitoso",
"wouoldLikeToConnect": "quisiera conectar",
"message": "Mensaje",
"do_not_have_enough_gas_asset": "No tienes suficiente ${currency} para realizar una transacción con las condiciones actuales de la red blockchain. Necesita más ${currency} para pagar las tarifas de la red blockchain, incluso si envía un activo diferente.",
"totp_auth_url": "URL de autenticación TOTP"
"totp_auth_url": "URL de autenticación TOTP",
"awaitDAppProcessing": "Espere a que la dApp termine de procesarse."
}

View file

@ -699,6 +699,27 @@
"default_buy_provider": "Fournisseur d'achat par défaut",
"ask_each_time": "Demandez à chaque fois",
"buy_provider_unavailable": "Fournisseur actuellement indisponible.",
"signTransaction": "Signer une transaction",
"errorGettingCredentials": "Échec : erreur lors de l'obtention des informations d'identification",
"errorSigningTransaction": "Une erreur s'est produite lors de la signature de la transaction",
"pairingInvalidEvent": "Événement de couplage non valide",
"chains": "Chaînes",
"methods": "Méthodes",
"events": "Événements",
"reject": "Rejeter",
"approve": "Approuver",
"expiresOn": "Expire le",
"walletConnect": "PortefeuilleConnect",
"nullURIError": "L'URI est nul",
"connectWalletPrompt": "Connectez votre portefeuille avec WalletConnect pour effectuer des transactions",
"newConnection": "Nouvelle connexion",
"activeConnectionsPrompt": "Les connexions actives apparaîtront ici",
"deleteConnectionConfirmationPrompt": "Êtes-vous sûr de vouloir supprimer la connexion à",
"event": "Événement",
"successful": "Réussi",
"wouoldLikeToConnect": "je voudrais me connecter",
"message": "Message",
"do_not_have_enough_gas_asset": "Vous n'avez pas assez de ${currency} pour effectuer une transaction avec les conditions actuelles du réseau blockchain. Vous avez besoin de plus de ${currency} pour payer les frais du réseau blockchain, même si vous envoyez un actif différent.",
"totp_auth_url": "URL D'AUTORISATION TOTP"
"totp_auth_url": "URL D'AUTORISATION TOTP",
"awaitDAppProcessing": "Veuillez attendre que le dApp termine le traitement."
}

View file

@ -677,6 +677,27 @@
"default_buy_provider": "Tsohuwar Siyarwa",
"ask_each_time": "Tambaya kowane lokaci",
"buy_provider_unavailable": "Mai ba da kyauta a halin yanzu babu.",
"signTransaction": "Sa hannu Ma'amala",
"errorGettingCredentials": "Ba a yi nasara ba: Kuskure yayin samun takaddun shaida",
"errorSigningTransaction": "An sami kuskure yayin sanya hannu kan ciniki",
"pairingInvalidEvent": "Haɗa Lamarin mara inganci",
"chains": "Sarkoki",
"methods": "Hanyoyin",
"events": "Abubuwan da suka faru",
"reject": "Ƙi",
"approve": "Amincewa",
"expiresOn": "Yana ƙarewa",
"walletConnect": "WalletConnect",
"nullURIError": "URI banza ne",
"connectWalletPrompt": "Haɗa walat ɗin ku tare da WalletConnect don yin ma'amala",
"newConnection": "Sabuwar Haɗi",
"activeConnectionsPrompt": "Haɗin kai mai aiki zai bayyana a nan",
"deleteConnectionConfirmationPrompt": "Shin kun tabbata cewa kuna son share haɗin zuwa",
"event": "Lamarin",
"successful": "Nasara",
"wouoldLikeToConnect": "ina son haɗi",
"message": "Sako",
"do_not_have_enough_gas_asset": "Ba ku da isassun ${currency} don yin ma'amala tare da yanayin cibiyar sadarwar blockchain na yanzu. Kuna buƙatar ƙarin ${currency} don biyan kuɗaɗen cibiyar sadarwar blockchain, koda kuwa kuna aika wata kadara daban.",
"totp_auth_url": "TOTP AUTH URL"
"totp_auth_url": "TOTP AUTH URL",
"awaitDAppProcessing": "Da fatan za a jira dApp ya gama aiki."
}

View file

@ -699,6 +699,27 @@
"default_buy_provider": "डिफ़ॉल्ट खरीद प्रदाता",
"ask_each_time": "हर बार पूछें",
"buy_provider_unavailable": "वर्तमान में प्रदाता अनुपलब्ध है।",
"signTransaction": "लेन-देन पर हस्ताक्षर करें",
"errorGettingCredentials": "विफल: क्रेडेंशियल प्राप्त करते समय त्रुटि",
"errorSigningTransaction": "लेन-देन पर हस्ताक्षर करते समय एक त्रुटि उत्पन्न हुई है",
"pairingInvalidEvent": "अमान्य ईवेंट युग्मित करना",
"chains": "चेन",
"methods": "तरीकों",
"events": "आयोजन",
"reject": "अस्वीकार करना",
"approve": "मंज़ूरी देना",
"expiresOn": "पर समय सीमा समाप्त",
"walletConnect": "वॉलेटकनेक्ट",
"nullURIError": "यूआरआई शून्य है",
"connectWalletPrompt": "लेन-देन करने के लिए अपने वॉलेट को वॉलेटकनेक्ट से कनेक्ट करें",
"newConnection": "नया कनेक्शन",
"activeConnectionsPrompt": "सक्रिय कनेक्शन यहां दिखाई देंगे",
"deleteConnectionConfirmationPrompt": "क्या आप वाकई कनेक्शन हटाना चाहते हैं?",
"event": "आयोजन",
"successful": "सफल",
"wouoldLikeToConnect": "जुड़ना चाहेंगे",
"message": "संदेश",
"do_not_have_enough_gas_asset": "वर्तमान ब्लॉकचेन नेटवर्क स्थितियों में लेनदेन करने के लिए आपके पास पर्याप्त ${currency} नहीं है। ब्लॉकचेन नेटवर्क शुल्क का भुगतान करने के लिए आपको अधिक ${currency} की आवश्यकता है, भले ही आप एक अलग संपत्ति भेज रहे हों।",
"totp_auth_url": "TOTP प्रामाणिक यूआरएल"
"totp_auth_url": "TOTP प्रामाणिक यूआरएल",
"awaitDAppProcessing": "कृपया डीएपी की प्रोसेसिंग पूरी होने तक प्रतीक्षा करें।"
}

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