From 32643823e58fd634a7180aa7ea59ffe7b01f02b7 Mon Sep 17 00:00:00 2001 From: Adegoke David <64401859+Blazebrain@users.noreply.github.com> Date: Tue, 3 Oct 2023 15:56:10 +0100 Subject: [PATCH] Cw 451 wallet connect for ethereum (#1049) * Update Flutter Update packages * Feat: Wallet connect for ethereum * Fix localization issues Fix UI issues Update old packages Update workflow Update how to build guide * feat: Wallet connect * feat: Add wallet connect for ethereum * chore: Add eth dependencies in configure file * Minor: `WalletConnect` settings name, not `Wallet connect` * fix: Merge conflicts * fix: Issues with test cases on various dApps, introduce Arbitrum rinkerby as suported chain * ui: Design fixes for WalletConnect flow * chore: Update repo and comment out send apk to channel in workflow * fix: Core implementation * feat: WalletConnect WIP * feat: WalletConnect WIP * feat: WalletConnect WIP * chore: Unused parameters WIP [skip ci] * fix: Code review fixes * Feat: WalletConnect feat WIP * feat: WalletConnect * feat: WalletConnect * feat: WalletConnect * Feat: WalletConnect * Feat: WalletConnect * feat: Remove queue support for the bottomsheet * feat: WalletConnect feature, bug fixes, folder restructuring, localization * Feat: Add positive feedback prompt on successful transaction * fix: Delete session bug * fix: dependencies registration WIP * feat: Registering dependencies for walletconnect * chore: Move key data to secrets * chore: ensure appropriate null checks * chore: localization * chore: Remove unused code * localization * chore: Remove unused code * chore: Remove unused code * chore: Add walletconnect project id key entry * fix: Revert bash command for linnux support * fix: Issues with translation in some languages and making unneeded external variable private * fix: Add bottomsheet listener to desktop dashboard page * Generalize ethereum not enough gas error check --------- Co-authored-by: OmarHatem Co-authored-by: Justin Ehrenhofer --- .github/workflows/pr_test_build.yml | 34 +- assets/images/walletconnect_logo.png | Bin 0 -> 74348 bytes cw_ethereum/lib/ethereum_wallet.dart | 2 + lib/core/wallet_connect/chain_service.dart | 5 + .../wallet_connect/eth_transaction_model.dart | 60 ++++ lib/core/wallet_connect/evm_chain_id.dart | 35 +++ .../wallet_connect/evm_chain_service.dart | 294 ++++++++++++++++++ .../models/auth_request_model.dart | 16 + .../models/bottom_sheet_queue_item_model.dart | 20 ++ .../models/chain_key_model.dart | 16 + .../models/connection_model.dart | 18 ++ .../models/session_request_model.dart | 14 + .../wallet_connect_key_service.dart | 72 +++++ .../wc_bottom_sheet_service.dart | 43 +++ .../wallet_connect/web3wallet_service.dart | 277 +++++++++++++++++ lib/di.dart | 39 ++- lib/entities/preferences_key.dart | 6 +- lib/ethereum/cw_ethereum.dart | 14 + lib/main.dart | 21 +- lib/src/screens/dashboard/dashboard_page.dart | 179 ++++++----- .../dashboard/desktop_dashboard_page.dart | 53 ++-- lib/src/screens/root/root.dart | 16 +- .../settings/connection_sync_page.dart | 22 +- .../widgets/wallet_connect_button.dart | 46 +++ .../utils/namespace_model_builder.dart | 71 +++++ .../wallet_connect/utils/string_parsing.dart | 16 + .../wc_connections_listing_view.dart | 142 +++++++++ .../wc_pairing_detail_page.dart | 186 +++++++++++ .../widgets/connection_item_widget.dart | 102 ++++++ .../widgets/connection_request_widget.dart | 166 ++++++++++ .../widgets/connection_widget.dart | 45 +++ .../widgets/error_display_widget.dart | 36 +++ .../widgets/modals/bottom_sheet_listener.dart | 62 ++++ .../widgets/modals/web3_request_modal.dart | 48 +++ .../widgets/pairing_item_widget.dart | 82 +++++ lib/store/settings_store.dart | 3 +- .../dashboard/dashboard_view_model.dart | 252 ++++++++------- lib/view_model/send/send_view_model.dart | 2 +- pubspec_base.yaml | 2 + res/values/strings_ar.arb | 25 +- res/values/strings_bg.arb | 23 +- res/values/strings_cs.arb | 23 +- res/values/strings_de.arb | 23 +- res/values/strings_en.arb | 23 +- res/values/strings_es.arb | 23 +- res/values/strings_fr.arb | 23 +- res/values/strings_ha.arb | 23 +- res/values/strings_hi.arb | 23 +- res/values/strings_hr.arb | 25 +- res/values/strings_id.arb | 25 +- res/values/strings_it.arb | 23 +- res/values/strings_ja.arb | 23 +- res/values/strings_ko.arb | 23 +- res/values/strings_my.arb | 23 +- res/values/strings_nl.arb | 23 +- res/values/strings_pl.arb | 23 +- res/values/strings_pt.arb | 23 +- res/values/strings_ru.arb | 23 +- res/values/strings_th.arb | 23 +- res/values/strings_tl.arb | 31 +- res/values/strings_tr.arb | 23 +- res/values/strings_uk.arb | 23 +- res/values/strings_ur.arb | 23 +- res/values/strings_yo.arb | 23 +- res/values/strings_zh.arb | 23 +- tool/configure.dart | 3 + tool/utils/secret_key.dart | 1 + 67 files changed, 2819 insertions(+), 314 deletions(-) create mode 100644 assets/images/walletconnect_logo.png create mode 100644 lib/core/wallet_connect/chain_service.dart create mode 100644 lib/core/wallet_connect/eth_transaction_model.dart create mode 100644 lib/core/wallet_connect/evm_chain_id.dart create mode 100644 lib/core/wallet_connect/evm_chain_service.dart create mode 100644 lib/core/wallet_connect/models/auth_request_model.dart create mode 100644 lib/core/wallet_connect/models/bottom_sheet_queue_item_model.dart create mode 100644 lib/core/wallet_connect/models/chain_key_model.dart create mode 100644 lib/core/wallet_connect/models/connection_model.dart create mode 100644 lib/core/wallet_connect/models/session_request_model.dart create mode 100644 lib/core/wallet_connect/wallet_connect_key_service.dart create mode 100644 lib/core/wallet_connect/wc_bottom_sheet_service.dart create mode 100644 lib/core/wallet_connect/web3wallet_service.dart create mode 100644 lib/src/screens/settings/widgets/wallet_connect_button.dart create mode 100644 lib/src/screens/wallet_connect/utils/namespace_model_builder.dart create mode 100644 lib/src/screens/wallet_connect/utils/string_parsing.dart create mode 100644 lib/src/screens/wallet_connect/wc_connections_listing_view.dart create mode 100644 lib/src/screens/wallet_connect/wc_pairing_detail_page.dart create mode 100644 lib/src/screens/wallet_connect/widgets/connection_item_widget.dart create mode 100644 lib/src/screens/wallet_connect/widgets/connection_request_widget.dart create mode 100644 lib/src/screens/wallet_connect/widgets/connection_widget.dart create mode 100644 lib/src/screens/wallet_connect/widgets/error_display_widget.dart create mode 100644 lib/src/screens/wallet_connect/widgets/modals/bottom_sheet_listener.dart create mode 100644 lib/src/screens/wallet_connect/widgets/modals/web3_request_modal.dart create mode 100644 lib/src/screens/wallet_connect/widgets/pairing_item_widget.dart diff --git a/.github/workflows/pr_test_build.yml b/.github/workflows/pr_test_build.yml index d01d4222e..0f2dfc25b 100644 --- a/.github/workflows/pr_test_build.yml +++ b/.github/workflows/pr_test_build.yml @@ -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 @@ -131,6 +130,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 @@ -140,18 +140,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: | @@ -171,6 +171,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 }} diff --git a/assets/images/walletconnect_logo.png b/assets/images/walletconnect_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..9024b972c482b8410134e259589dce15d934cd18 GIT binary patch literal 74348 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 LtDnm{r-UW|z _ethPrivateKey; + late EthereumClient _client; int? _gasPrice; diff --git a/lib/core/wallet_connect/chain_service.dart b/lib/core/wallet_connect/chain_service.dart new file mode 100644 index 000000000..1e3ce3efd --- /dev/null +++ b/lib/core/wallet_connect/chain_service.dart @@ -0,0 +1,5 @@ +abstract class ChainService { + String getNamespace(); + String getChainId(); + List getEvents(); +} diff --git a/lib/core/wallet_connect/eth_transaction_model.dart b/lib/core/wallet_connect/eth_transaction_model.dart new file mode 100644 index 000000000..deb33586f --- /dev/null +++ b/lib/core/wallet_connect/eth_transaction_model.dart @@ -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 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 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)'; + } +} diff --git a/lib/core/wallet_connect/evm_chain_id.dart b/lib/core/wallet_connect/evm_chain_id.dart new file mode 100644 index 000000000..b71fb562e --- /dev/null +++ b/lib/core/wallet_connect/evm_chain_id.dart @@ -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'; + } +} diff --git a/lib/core/wallet_connect/evm_chain_service.dart b/lib/core/wallet_connect/evm_chain_service.dart new file mode 100644 index 000000000..bcc6622fa --- /dev/null +++ b/lib/core/wallet_connect/evm_chain_service.dart @@ -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 getEvents() { + return ['chainChanged', 'accountsChanged']; + } + + Future 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 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 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 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 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 ethSignTransaction(String topic, dynamic parameters) async { + log('received eth sign transaction request: $parameters'); + + final paramsData = parameters[0] as Map; + + final message = _convertToReadable(paramsData); + + final String? authError = await requestAuthorization(message); + + if (authError != null) { + return authError; + } + + // Load the private key + final List keys = wcKeyService.getKeysForChain(getChainId()); + + final Credentials credentials = EthPrivateKey.fromHex(keys[0].privateKey); + + WCEthereumTransactionModel ethTransaction = + WCEthereumTransactionModel.fromJson(parameters[0] as Map); + + 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 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 keys = wcKeyService.getKeysForChain(getChainId()); + + return EthSigUtil.signTypedData( + privateKey: keys[0].privateKey, + jsonData: data ?? '', + version: TypedDataVersion.V4, + ); + } + + String _convertToReadable(Map 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 + '''; + } +} diff --git a/lib/core/wallet_connect/models/auth_request_model.dart b/lib/core/wallet_connect/models/auth_request_model.dart new file mode 100644 index 000000000..f7fd984c8 --- /dev/null +++ b/lib/core/wallet_connect/models/auth_request_model.dart @@ -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)'; + } +} diff --git a/lib/core/wallet_connect/models/bottom_sheet_queue_item_model.dart b/lib/core/wallet_connect/models/bottom_sheet_queue_item_model.dart new file mode 100644 index 000000000..49eecac0f --- /dev/null +++ b/lib/core/wallet_connect/models/bottom_sheet_queue_item_model.dart @@ -0,0 +1,20 @@ +import 'dart:async'; + +import 'package:flutter/cupertino.dart'; + +class BottomSheetQueueItemModel { + final Widget widget; + final bool isModalDismissible; + final Completer completer; + + BottomSheetQueueItemModel({ + required this.widget, + required this.completer, + this.isModalDismissible = false, + }); + + @override + String toString() { + return 'BottomSheetQueueItemModel(widget: $widget, completer: $completer)'; + } +} diff --git a/lib/core/wallet_connect/models/chain_key_model.dart b/lib/core/wallet_connect/models/chain_key_model.dart new file mode 100644 index 000000000..5cd2764da --- /dev/null +++ b/lib/core/wallet_connect/models/chain_key_model.dart @@ -0,0 +1,16 @@ +class ChainKeyModel { + final List 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)'; + } +} diff --git a/lib/core/wallet_connect/models/connection_model.dart b/lib/core/wallet_connect/models/connection_model.dart new file mode 100644 index 000000000..63cc8260f --- /dev/null +++ b/lib/core/wallet_connect/models/connection_model.dart @@ -0,0 +1,18 @@ +class ConnectionModel { + final String? title; + final String? text; + final List? elements; + final Map? elementActions; + + ConnectionModel({ + this.title, + this.text, + this.elements, + this.elementActions, + }); + + @override + String toString() { + return 'WalletConnectRequestModel(title: $title, text: $text, elements: $elements, elementActions: $elementActions)'; + } +} diff --git a/lib/core/wallet_connect/models/session_request_model.dart b/lib/core/wallet_connect/models/session_request_model.dart new file mode 100644 index 000000000..0c7a5d876 --- /dev/null +++ b/lib/core/wallet_connect/models/session_request_model.dart @@ -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)'; + } +} diff --git a/lib/core/wallet_connect/wallet_connect_key_service.dart b/lib/core/wallet_connect/wallet_connect_key_service.dart new file mode 100644 index 000000000..2e61ebb99 --- /dev/null +++ b/lib/core/wallet_connect/wallet_connect_key_service.dart @@ -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 getKeys(); + + /// Returns a list of all the chain ids. + List 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 getKeysForChain(String chain); + + /// Returns a list of all the accounts in namespace:chainId:address format. + List 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, TransactionInfo> wallet; + + late final List _keys; + + @override + List getChains() { + final List chainIds = []; + for (final ChainKeyModel key in _keys) { + chainIds.addAll(key.chains); + } + return chainIds; + } + + @override + List getKeys() => _keys; + + @override + List getKeysForChain(String chain) { + return _keys.where((e) => e.chains.contains(chain)).toList(); + } + + @override + List getAllAccounts() { + final List accounts = []; + for (final ChainKeyModel key in _keys) { + for (final String chain in key.chains) { + accounts.add('$chain:${key.publicKey}'); + } + } + return accounts; + } +} diff --git a/lib/core/wallet_connect/wc_bottom_sheet_service.dart b/lib/core/wallet_connect/wc_bottom_sheet_service.dart new file mode 100644 index 000000000..3da8660f0 --- /dev/null +++ b/lib/core/wallet_connect/wc_bottom_sheet_service.dart @@ -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 currentSheet; + + Future queueBottomSheet({ + required Widget widget, + bool isModalDismissible = false, + }); + + void resetCurrentSheet(); +} + +class BottomSheetServiceImpl implements BottomSheetService { + + @override + final ValueNotifier currentSheet = ValueNotifier(null); + + @override + Future queueBottomSheet({ + required Widget widget, + bool isModalDismissible = false, + }) async { + // Create the bottom sheet queue item + final completer = Completer(); + final queueItem = BottomSheetQueueItemModel( + widget: widget, + completer: completer, + isModalDismissible: isModalDismissible, + ); + + currentSheet.value = queueItem; + + return await completer.future; + } + + @override + void resetCurrentSheet() { + currentSheet.value = null; + } +} diff --git a/lib/core/wallet_connect/web3wallet_service.dart b/lib/core/wallet_connect/web3wallet_service.dart new file mode 100644 index 000000000..0a7716b71 --- /dev/null +++ b/lib/core/wallet_connect/web3wallet_service.dart @@ -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 pairings; + + @observable + ObservableList sessions; + + @observable + ObservableList auth; + + Web3WalletServiceBase(this._bottomSheetHandler, this.walletKeyService, this.appStore) + : pairings = ObservableList(), + sessions = ObservableList(), + auth = ObservableList(), + 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 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 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 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 _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 _onAuthRequest(AuthRequest? args) async { + if (args != null) { + List 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 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 getSessionsForPairingInfo(PairingInfo pairing) { + return sessions.where((element) => element.pairingTopic == pairing.topic).toList(); + } +} diff --git a/lib/di.dart b/lib/di.dart index 97dd392c5..a08e84cfb 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -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'; @@ -399,6 +401,10 @@ Future setup({ } if (appStore.wallet != null) { authStore.allowed(); + + if (appStore.wallet!.type == WalletType.ethereum) { + getIt.get().init(); + } return; } @@ -419,6 +425,10 @@ Future setup({ } else { if (appStore.wallet != null) { authStore.allowed(); + + if (appStore.wallet!.type == WalletType.ethereum) { + getIt.get().init(); + } return; } @@ -438,11 +448,28 @@ Future setup({ }, closable: false); }, instanceName: 'login'); + getIt.registerSingleton(BottomSheetServiceImpl()); + + final appStore = getIt.get(); + + getIt.registerLazySingleton(() => KeyServiceImpl(appStore.wallet!)); + + getIt.registerLazySingleton(() { + final Web3WalletService web3WalletService = Web3WalletService( + getIt.get(), + getIt.get(), + appStore, + ); + web3WalletService.create(); + return web3WalletService; + }); + getIt.registerFactory(() => BalancePage( dashboardViewModel: getIt.get(), settingsStore: getIt.get())); getIt.registerFactory(() => DashboardPage( + bottomSheetService: getIt.get(), balancePage: getIt.get(), dashboardViewModel: getIt.get(), addressListViewModel: getIt.get(), @@ -459,6 +486,7 @@ Future setup({ }); getIt.registerFactoryParam, void>( (desktopKey, _) => DesktopDashboardPage( + bottomSheetService: getIt.get(), balancePage: getIt.get(), dashboardViewModel: getIt.get(), addressListViewModel: getIt.get(), @@ -668,7 +696,9 @@ Future setup({ return NodeListViewModel(_nodeSource, appStore); }); - getIt.registerFactory(() => ConnectionSyncPage(getIt.get())); + getIt.registerFactory( + () => ConnectionSyncPage(getIt.get(), getIt.get()), + ); getIt.registerFactory( () => SecurityBackupPage(getIt.get(), getIt.get())); @@ -851,9 +881,8 @@ Future setup({ getIt.registerFactory(() => SupportPage(getIt.get())); - getIt.registerFactory(() => - SupportChatPage( - getIt.get(), secureStorage: getIt.get())); + getIt.registerFactory(() => SupportChatPage(getIt.get(), + secureStorage: getIt.get())); getIt.registerFactory(() => SupportOtherLinksPage(getIt.get())); diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index d32dcbca4..65f97be94 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -15,8 +15,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'; @@ -54,8 +53,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'; diff --git a/lib/ethereum/cw_ethereum.dart b/lib/ethereum/cw_ethereum.dart index f2db7741e..d3c11c361 100644 --- a/lib/ethereum/cw_ethereum.dart +++ b/lib/ethereum/cw_ethereum.dart @@ -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; diff --git a/lib/main.dart b/lib/main.dart index 40d7468f2..bd84d7a05 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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(); @@ -155,7 +154,7 @@ Future initializeAppConfigs() async { secureStorage: secureStorage, anonpayInvoiceInfo: anonpayInvoiceInfo, initialMigrationVersion: 21); - } +} Future initialSetup( {required SharedPreferences sharedPreferences, @@ -308,26 +307,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) { diff --git a/lib/src/screens/dashboard/dashboard_page.dart b/lib/src/screens/dashboard/dashboard_page.dart index 68f5ca9aa..a0cf6cd28 100644 --- a/lib/src/screens/dashboard/dashboard_page.dart +++ b/lib/src/screens/dashboard/dashboard_page.dart @@ -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: [ - 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: [ + 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()! - .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()!.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()! + .indicatorDotTheme + .activeIndicatorColor, ), - color: - Theme.of(context).extension()!.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()! - .mainActionsIconColor + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(50.0), + border: Border.all( + color: Theme.of(context).extension()!.cardBorderColor, + width: 1, + ), + color: Theme.of(context) + .extension()! + .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()! + .mainActionsIconColor + : Theme.of(context) + .extension()! + .labelTextColor, + ), + title: action.name(context), + onClick: () async => + await action.onTap(context, dashboardViewModel), + textColor: action.isEnabled?.call(dashboardViewModel) ?? true + ? null : Theme.of(context) .extension()! .labelTextColor, ), - title: action.name(context), - onClick: () async => - await action.onTap(context, dashboardViewModel), - textColor: action.isEnabled?.call(dashboardViewModel) ?? true - ? null - : Theme.of(context) - .extension()! - .labelTextColor, ), - ), - ) - .toList(), + ) + .toList(), + ), ), ), ), - ), - ); - }, - ), - ], + ); + }, + ), + ], + ), ), ); } diff --git a/lib/src/screens/dashboard/desktop_dashboard_page.dart b/lib/src/screens/dashboard/desktop_dashboard_page.dart index 2332f4db6..41d41dd4f 100644 --- a/lib/src/screens/dashboard/desktop_dashboard_page.dart +++ b/lib/src/screens/dashboard/desktop_dashboard_page.dart @@ -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 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))! + ]; + }, + ), ), ), - ), - ], + ], + ), ), ); } diff --git a/lib/src/screens/root/root.dart b/lib/src/screens/root/root.dart index af775705b..7a903bb69 100644 --- a/lib/src/screens/root/root.dart +++ b/lib/src/screens/root/root.dart @@ -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 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 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 with WidgetsBindingObserver { launchUri = null; } - return WillPopScope(onWillPop: () async => false, child: widget.child); + return WillPopScope( + onWillPop: () async => false, + child: widget.child, + ); } void _reset() { diff --git a/lib/src/screens/settings/connection_sync_page.dart b/lib/src/screens/settings/connection_sync_page.dart index c59e71be4..d5a26a0fe 100644 --- a/lib/src/screens/settings/connection_sync_page.dart +++ b/lib/src/screens/settings/connection_sync_page.dart @@ -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 @@ -66,6 +70,20 @@ class ConnectionSyncPage extends BasePage { handler: (context) => Navigator.of(context).pushNamed(Routes.manageNodes), ), const StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)), + 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)), + ] ], ), ); diff --git a/lib/src/screens/settings/widgets/wallet_connect_button.dart b/lib/src/screens/settings/widgets/wallet_connect_button.dart new file mode 100644 index 000000000..d02462619 --- /dev/null +++ b/lib/src/screens/settings/widgets/wallet_connect_button.dart @@ -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: [ + 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()!.titleColor, + ), + ), + ), + Image.asset( + 'assets/images/select_arrow.png', + color: Theme.of(context).extension()!.detailsTitlesColor, + ) + ], + ), + ), + ); + } +} diff --git a/lib/src/screens/wallet_connect/utils/namespace_model_builder.dart b/lib/src/screens/wallet_connect/utils/namespace_model_builder.dart new file mode 100644 index 000000000..936df93d3 --- /dev/null +++ b/lib/src/screens/wallet_connect/utils/namespace_model_builder.dart @@ -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 buildFromRequiredNamespaces( + Map requiredNamespaces, + ) { + final List views = []; + for (final key in requiredNamespaces.keys) { + RequiredNamespace ns = requiredNamespaces[key]!; + final List 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 buildFromNamespaces( + String topic, + Map namespaces, + Web3Wallet web3wallet, + ) { + final List views = []; + for (final key in namespaces.keys) { + final Namespace ns = namespaces[key]!; + final List 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 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; + } +} diff --git a/lib/src/screens/wallet_connect/utils/string_parsing.dart b/lib/src/screens/wallet_connect/utils/string_parsing.dart new file mode 100644 index 000000000..b9fdca7b2 --- /dev/null +++ b/lib/src/screens/wallet_connect/utils/string_parsing.dart @@ -0,0 +1,16 @@ +import 'dart:convert'; + +import 'package:convert/convert.dart'; + +extension StringParsing on String { + String get utf8Message { + if (startsWith('0x')) { + final List decoded = hex.decode( + substring(2), + ); + return utf8.decode(decoded); + } + + return this; + } +} diff --git a/lib/src/screens/wallet_connect/wc_connections_listing_view.dart b/lib/src/screens/wallet_connect/wc_connections_listing_view.dart new file mode 100644 index 000000000..ead50db07 --- /dev/null +++ b/lib/src/screens/wallet_connect/wc_connections_listing_view.dart @@ -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 _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 _invalidUriToast(BuildContext context, String message) async { + await showPopUp( + 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()!.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()!.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), + ], + ); + }, + ); + } +} diff --git a/lib/src/screens/wallet_connect/wc_pairing_detail_page.dart b/lib/src/screens/wallet_connect/wc_pairing_detail_page.dart new file mode 100644 index 000000000..f99eb9cdb --- /dev/null +++ b/lib/src/screens/wallet_connect/wc_pairing_detail_page.dart @@ -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 { + List 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 sessions = widget.web3walletService.getSessionsForPairingInfo(widget.pairing); + + for (final SessionData session in sessions) { + List 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 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, + ), + ), + const SizedBox(height: 20.0), + Text( + pairing.peerMetadata!.name, + style: TextStyle( + fontSize: 16.0, + fontWeight: FontWeight.w500, + color: Theme.of(context).extension()!.titleColor, + ), + ), + const SizedBox(height: 16.0), + Text( + pairing.peerMetadata!.url, + style: TextStyle( + fontSize: 14.0, + fontWeight: FontWeight.normal, + color: Theme.of(context).extension()!.titleColor, + ), + ), + const SizedBox(height: 8.0), + Text( + '${S.current.expiresOn}: $expiryDate', + style: TextStyle( + fontSize: 14.0, + fontWeight: FontWeight.normal, + color: Theme.of(context).extension()!.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 _onDeleteButtonPressed( + BuildContext context, String dAppName, Web3WalletService web3walletService) async { + bool confirmed = false; + + await showPopUp( + 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()); + } + } + } +} diff --git a/lib/src/screens/wallet_connect/widgets/connection_item_widget.dart b/lib/src/screens/wallet_connect/widgets/connection_item_widget.dart new file mode 100644 index 000000000..77c30417a --- /dev/null +++ b/lib/src/screens/wallet_connect/widgets/connection_item_widget.dart @@ -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()!.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()!.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()!.titleColor, + fontSize: 14, + fontWeight: FontWeight.w600, + ), + maxLines: 10, + overflow: TextOverflow.ellipsis, + ), + ), + ); + } +} diff --git a/lib/src/screens/wallet_connect/widgets/connection_request_widget.dart b/lib/src/screens/wallet_connect/widgets/connection_request_widget.dart new file mode 100644 index 000000000..c73c4bfa8 --- /dev/null +++ b/lib/src/screens/wallet_connect/widgets/connection_request_widget.dart @@ -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 createState() => _ConnectionRequestWidgetState(); +} + +class _ConnectionRequestWidgetState extends State { + 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()!.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()!.secondaryTextColor, + ), + textAlign: TextAlign.center, + ), + Text( + S.current.wouoldLikeToConnect, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + color: Theme.of(context).extension()!.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()!.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 views = ConnectionWidgetBuilder.buildFromRequiredNamespaces( + sessionProposal.request.requiredNamespaces, + ); + + return Column(children: views); + } +} diff --git a/lib/src/screens/wallet_connect/widgets/connection_widget.dart b/lib/src/screens/wallet_connect/widgets/connection_widget.dart new file mode 100644 index 000000000..921d8ea5c --- /dev/null +++ b/lib/src/screens/wallet_connect/widgets/connection_widget.dart @@ -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 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()!.titleColor, + ), + ), + ), + const SizedBox(height: 8), + ...info.map((e) => ConnectionItemWidget(model: e)), + ], + ), + ); + } +} diff --git a/lib/src/screens/wallet_connect/widgets/error_display_widget.dart b/lib/src/screens/wallet_connect/widgets/error_display_widget.dart new file mode 100644 index 000000000..0fbbda5c7 --- /dev/null +++ b/lib/src/screens/wallet_connect/widgets/error_display_widget.dart @@ -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, + ), + ), + ], + ); + } +} diff --git a/lib/src/screens/wallet_connect/widgets/modals/bottom_sheet_listener.dart b/lib/src/screens/wallet_connect/widgets/modals/bottom_sheet_listener.dart new file mode 100644 index 000000000..30b6af7e0 --- /dev/null +++ b/lib/src/screens/wallet_connect/widgets/modals/bottom_sheet_listener.dart @@ -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 { + + @override + void initState() { + super.initState(); + widget.bottomSheetService.currentSheet.addListener(_showBottomSheet); + } + + @override + void dispose() { + widget.bottomSheetService.currentSheet.removeListener(_showBottomSheet); + super.dispose(); + } + + Future _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; +} diff --git a/lib/src/screens/wallet_connect/widgets/modals/web3_request_modal.dart b/lib/src/screens/wallet_connect/widgets/modals/web3_request_modal.dart new file mode 100644 index 000000000..f16dcc0f8 --- /dev/null +++ b/lib/src/screens/wallet_connect/widgets/modals/web3_request_modal.dart @@ -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()!.titleColor, + ), + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/src/screens/wallet_connect/widgets/pairing_item_widget.dart b/lib/src/screens/wallet_connect/widgets/pairing_item_widget.dart new file mode 100644 index 000000000..063de8ec3 --- /dev/null +++ b/lib/src/screens/wallet_connect/widgets/pairing_item_widget.dart @@ -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, + ), + title: Text( + metadata.name, + style: TextStyle( + fontSize: 16.0, + fontWeight: FontWeight.w700, + color: Theme.of(context).extension()!.titleColor, + ), + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + metadata.url, + style: TextStyle( + fontSize: 14.0, + fontWeight: FontWeight.w700, + color: Theme.of(context).extension()!.secondaryTextColor, + ), + ), + Text( + '${S.current.expiresOn}: $expiryDate', + style: TextStyle( + fontSize: 14.0, + fontWeight: FontWeight.w700, + color: Theme.of(context).extension()!.secondaryTextColor, + ), + ), + ], + ), + trailing: Container( + height: 40, + width: 44, + padding: EdgeInsets.all(10), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Theme.of(context).extension()!.iconsBackgroundColor, + ), + child: Icon( + Icons.edit, + size: 14, + color: Theme.of(context).extension()!.iconsColor, + ), + ), + onTap: onTap, + ); + } +} diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index 54607815d..6bd3686b5 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -595,8 +595,7 @@ abstract class SettingsStoreBase with Store { SortBalanceBy.values[sharedPreferences.getInt(PreferencesKey.sortBalanceBy) ?? 0]; final pinNativeTokenAtTop = sharedPreferences.getBool(PreferencesKey.pinNativeTokenAtTop) ?? true; - final useEtherscan = - sharedPreferences.getBool(PreferencesKey.useEtherscan) ?? true; + final useEtherscan = sharedPreferences.getBool(PreferencesKey.useEtherscan) ?? true; // If no value if (pinLength == null || pinLength == 0) { diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index 777db8f8d..c8a3a907c 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -47,70 +47,70 @@ abstract class DashboardViewModelBase with Store { required this.yatStore, required this.ordersStore, required this.anonpayTransactionsStore}) - : isOutdatedElectrumWallet = false, - hasSellAction = false, - hasBuyAction = false, - hasExchangeAction = false, - isShowFirstYatIntroduction = false, - isShowSecondYatIntroduction = false, - isShowThirdYatIntroduction = false, - filterItems = { - S.current.transactions: [ - FilterItem( - value: () => transactionFilterStore.displayAll, - caption: S.current.all_transactions, - onChanged: transactionFilterStore.toggleAll), - FilterItem( - value: () => transactionFilterStore.displayIncoming, - caption: S.current.incoming, - onChanged:transactionFilterStore.toggleIncoming), - FilterItem( - value: () => transactionFilterStore.displayOutgoing, - caption: S.current.outgoing, - onChanged: transactionFilterStore.toggleOutgoing), - // FilterItem( - // value: () => false, - // caption: S.current.transactions_by_date, - // onChanged: null), - ], - S.current.trades: [ - FilterItem( - value: () => tradeFilterStore.displayAllTrades, - caption: S.current.all_trades, - onChanged: () => tradeFilterStore - .toggleDisplayExchange(ExchangeProviderDescription.all)), - FilterItem( - value: () => tradeFilterStore.displayChangeNow, - caption: ExchangeProviderDescription.changeNow.title, - onChanged: () => tradeFilterStore - .toggleDisplayExchange(ExchangeProviderDescription.changeNow)), - FilterItem( - value: () => tradeFilterStore.displaySideShift, - caption: ExchangeProviderDescription.sideShift.title, - onChanged: () => tradeFilterStore - .toggleDisplayExchange(ExchangeProviderDescription.sideShift)), - FilterItem( - value: () => tradeFilterStore.displaySimpleSwap, - caption: ExchangeProviderDescription.simpleSwap.title, - onChanged: () => tradeFilterStore - .toggleDisplayExchange(ExchangeProviderDescription.simpleSwap)), - FilterItem( - value: () => tradeFilterStore.displayTrocador, - caption: ExchangeProviderDescription.trocador.title, - onChanged: () => tradeFilterStore - .toggleDisplayExchange(ExchangeProviderDescription.trocador)), - FilterItem( - value: () => tradeFilterStore.displayExolix, - caption: ExchangeProviderDescription.exolix.title, - onChanged: () => tradeFilterStore - .toggleDisplayExchange(ExchangeProviderDescription.exolix)), - ] - }, - subname = '', - name = appStore.wallet!.name, - type = appStore.wallet!.type, - transactions = ObservableList(), - wallet = appStore.wallet! { + : isOutdatedElectrumWallet = false, + hasSellAction = false, + hasBuyAction = false, + hasExchangeAction = false, + isShowFirstYatIntroduction = false, + isShowSecondYatIntroduction = false, + isShowThirdYatIntroduction = false, + filterItems = { + S.current.transactions: [ + FilterItem( + value: () => transactionFilterStore.displayAll, + caption: S.current.all_transactions, + onChanged: transactionFilterStore.toggleAll), + FilterItem( + value: () => transactionFilterStore.displayIncoming, + caption: S.current.incoming, + onChanged: transactionFilterStore.toggleIncoming), + FilterItem( + value: () => transactionFilterStore.displayOutgoing, + caption: S.current.outgoing, + onChanged: transactionFilterStore.toggleOutgoing), + // FilterItem( + // value: () => false, + // caption: S.current.transactions_by_date, + // onChanged: null), + ], + S.current.trades: [ + FilterItem( + value: () => tradeFilterStore.displayAllTrades, + caption: S.current.all_trades, + onChanged: () => + tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.all)), + FilterItem( + value: () => tradeFilterStore.displayChangeNow, + caption: ExchangeProviderDescription.changeNow.title, + onChanged: () => + tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.changeNow)), + FilterItem( + value: () => tradeFilterStore.displaySideShift, + caption: ExchangeProviderDescription.sideShift.title, + onChanged: () => + tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.sideShift)), + FilterItem( + value: () => tradeFilterStore.displaySimpleSwap, + caption: ExchangeProviderDescription.simpleSwap.title, + onChanged: () => + tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.simpleSwap)), + FilterItem( + value: () => tradeFilterStore.displayTrocador, + caption: ExchangeProviderDescription.trocador.title, + onChanged: () => + tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.trocador)), + FilterItem( + value: () => tradeFilterStore.displayExolix, + caption: ExchangeProviderDescription.exolix.title, + onChanged: () => + tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.exolix)), + ] + }, + subname = '', + name = appStore.wallet!.name, + type = appStore.wallet!.type, + transactions = ObservableList(), + wallet = appStore.wallet! { name = wallet.name; type = wallet.type; isOutdatedElectrumWallet = @@ -125,15 +125,17 @@ abstract class DashboardViewModelBase with Store { if (_wallet.type == WalletType.monero) { subname = monero!.getCurrentAccount(_wallet).label; - _onMoneroAccountChangeReaction = reaction((_) => monero!.getMoneroWalletDetails(wallet) - .account, (Account account) => _onMoneroAccountChange(_wallet)); + _onMoneroAccountChangeReaction = reaction( + (_) => monero!.getMoneroWalletDetails(wallet).account, + (Account account) => _onMoneroAccountChange(_wallet)); - _onMoneroBalanceChangeReaction = reaction((_) => monero!.getMoneroWalletDetails(wallet).balance, + _onMoneroBalanceChangeReaction = reaction( + (_) => monero!.getMoneroWalletDetails(wallet).balance, (MoneroBalance balance) => _onMoneroTransactionsUpdate(_wallet)); - final _accountTransactions = _wallet - .transactionHistory.transactions.values - .where((tx) => monero!.getTransactionInfoAccountId(tx) == monero!.getCurrentAccount(wallet).id) + final _accountTransactions = _wallet.transactionHistory.transactions.values + .where((tx) => + monero!.getTransactionInfoAccountId(tx) == monero!.getCurrentAccount(wallet).id) .toList(); transactions = ObservableList.of(_accountTransactions.map((transaction) => @@ -142,34 +144,33 @@ abstract class DashboardViewModelBase with Store { balanceViewModel: balanceViewModel, settingsStore: appStore.settingsStore))); } else { - transactions = ObservableList.of(wallet - .transactionHistory.transactions.values - .map((transaction) => TransactionListItem( + transactions = ObservableList.of(wallet.transactionHistory.transactions.values.map( + (transaction) => TransactionListItem( transaction: transaction, balanceViewModel: balanceViewModel, settingsStore: appStore.settingsStore))); } reaction((_) => appStore.wallet, _onWalletChange); - + connectMapToListWithTransform( appStore.wallet!.transactionHistory.transactions, transactions, (TransactionInfo? transaction) => TransactionListItem( transaction: transaction!, balanceViewModel: balanceViewModel, - settingsStore: appStore.settingsStore), - filter: (TransactionInfo? transaction) { - if (transaction == null) { - return false; - } + settingsStore: appStore.settingsStore), filter: (TransactionInfo? transaction) { + if (transaction == null) { + return false; + } - final wallet = _wallet; - if (wallet.type == WalletType.monero) { - return monero!.getTransactionInfoAccountId(transaction) == monero!.getCurrentAccount(wallet).id; - } + final wallet = _wallet; + if (wallet.type == WalletType.monero) { + return monero!.getTransactionInfoAccountId(transaction) == + monero!.getCurrentAccount(wallet).id; + } - return true; + return true; }); } @@ -216,24 +217,21 @@ abstract class DashboardViewModelBase with Store { } @computed - BalanceDisplayMode get balanceDisplayMode => - appStore.settingsStore.balanceDisplayMode; - + BalanceDisplayMode get balanceDisplayMode => appStore.settingsStore.balanceDisplayMode; + @computed bool get shouldShowMarketPlaceInDashboard { return appStore.settingsStore.shouldShowMarketPlaceInDashboard; } @computed - List get trades => tradesStore.trades - .where((trade) => trade.trade.walletId == wallet.id) - .toList(); + List get trades => + tradesStore.trades.where((trade) => trade.trade.walletId == wallet.id).toList(); @computed - List get orders => ordersStore.orders - .where((item) => item.order.walletId == wallet.id) - .toList(); - + List get orders => + ordersStore.orders.where((item) => item.order.walletId == wallet.id).toList(); + @computed List get anonpayTransactons => anonpayTransactionsStore.transactions .where((item) => item.transaction.walletId == wallet.id) @@ -250,7 +248,8 @@ abstract class DashboardViewModelBase with Store { List get items { final _items = []; - _items.addAll(transactionFilterStore.filtered(transactions: [...transactions, ...anonpayTransactons])); + _items.addAll( + transactionFilterStore.filtered(transactions: [...transactions, ...anonpayTransactons])); _items.addAll(tradeFilterStore.filtered(trades: trades, wallet: wallet)); _items.addAll(orders); @@ -258,8 +257,7 @@ abstract class DashboardViewModelBase with Store { } @observable - WalletBase, TransactionInfo> - wallet; + WalletBase, TransactionInfo> wallet; bool get hasRescan => wallet.type == WalletType.monero || wallet.type == WalletType.haven; @@ -283,7 +281,6 @@ abstract class DashboardViewModelBase with Store { Map> filterItems; - BuyProviderType get defaultBuyProvider => settingsStore.defaultBuyProvider; bool get isBuyEnabled => settingsStore.isBitcoinBuyEnabled; @@ -291,8 +288,7 @@ abstract class DashboardViewModelBase with Store { bool get shouldShowYatPopup => settingsStore.shouldShowYatPopup; @action - void furtherShowYatPopup(bool shouldShow) => - settingsStore.shouldShowYatPopup = shouldShow; + void furtherShowYatPopup(bool shouldShow) => settingsStore.shouldShowYatPopup = shouldShow; @computed bool get isEnabledExchangeAction => settingsStore.exchangeStatus != ExchangeApiMode.disabled; @@ -301,8 +297,7 @@ abstract class DashboardViewModelBase with Store { bool hasExchangeAction; @computed - bool get isEnabledBuyAction => - !settingsStore.disableBuy && wallet.type != WalletType.haven; + bool get isEnabledBuyAction => !settingsStore.disableBuy && wallet.type != WalletType.haven; @observable bool hasBuyAction; @@ -330,9 +325,7 @@ abstract class DashboardViewModelBase with Store { @action void _onWalletChange( - WalletBase, - TransactionInfo>? - wallet) { + WalletBase, TransactionInfo>? wallet) { if (wallet == null) { return; } @@ -350,10 +343,12 @@ abstract class DashboardViewModelBase with Store { _onMoneroAccountChangeReaction?.reaction.dispose(); _onMoneroBalanceChangeReaction?.reaction.dispose(); - _onMoneroAccountChangeReaction = reaction((_) => monero!.getMoneroWalletDetails(wallet) - .account, (Account account) => _onMoneroAccountChange(wallet)); + _onMoneroAccountChangeReaction = reaction( + (_) => monero!.getMoneroWalletDetails(wallet).account, + (Account account) => _onMoneroAccountChange(wallet)); - _onMoneroBalanceChangeReaction = reaction((_) => monero!.getMoneroWalletDetails(wallet).balance, + _onMoneroBalanceChangeReaction = reaction( + (_) => monero!.getMoneroWalletDetails(wallet).balance, (MoneroBalance balance) => _onMoneroTransactionsUpdate(wallet)); _onMoneroTransactionsUpdate(wallet); @@ -364,8 +359,8 @@ abstract class DashboardViewModelBase with Store { transactions.clear(); - transactions.addAll(wallet.transactionHistory.transactions.values.map( - (transaction) => TransactionListItem( + transactions.addAll(wallet.transactionHistory.transactions.values.map((transaction) => + TransactionListItem( transaction: transaction, balanceViewModel: balanceViewModel, settingsStore: appStore.settingsStore))); @@ -374,21 +369,19 @@ abstract class DashboardViewModelBase with Store { connectMapToListWithTransform( appStore.wallet!.transactionHistory.transactions, transactions, - (TransactionInfo? transaction) - => TransactionListItem( + (TransactionInfo? transaction) => TransactionListItem( transaction: transaction!, balanceViewModel: balanceViewModel, - settingsStore: appStore.settingsStore), - filter: (TransactionInfo? tx) { - if (tx == null) { - return false; - } + settingsStore: appStore.settingsStore), filter: (TransactionInfo? tx) { + if (tx == null) { + return false; + } - if (wallet.type == WalletType.monero) { - return monero!.getTransactionInfoAccountId(tx) == monero!.getCurrentAccount(wallet).id; - } + if (wallet.type == WalletType.monero) { + return monero!.getTransactionInfoAccountId(tx) == monero!.getCurrentAccount(wallet).id; + } - return true; + return true; }); } @@ -402,15 +395,18 @@ abstract class DashboardViewModelBase with Store { void _onMoneroTransactionsUpdate(WalletBase wallet) { transactions.clear(); - final _accountTransactions = monero!.getTransactionHistory(wallet).transactions.values - .where((tx) => monero!.getTransactionInfoAccountId(tx) == monero!.getCurrentAccount(wallet).id) + final _accountTransactions = monero! + .getTransactionHistory(wallet) + .transactions + .values + .where( + (tx) => monero!.getTransactionInfoAccountId(tx) == monero!.getCurrentAccount(wallet).id) .toList(); - transactions.addAll(_accountTransactions.map((transaction) => - TransactionListItem( - transaction: transaction, - balanceViewModel: balanceViewModel, - settingsStore: appStore.settingsStore))); + transactions.addAll(_accountTransactions.map((transaction) => TransactionListItem( + transaction: transaction, + balanceViewModel: balanceViewModel, + settingsStore: appStore.settingsStore))); } void updateActions() { diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index 72ce9fdb2..0c322bfe6 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -417,7 +417,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 gas')) { return S.current.do_not_have_enough_gas_asset(currency.toString()); } } diff --git a/pubspec_base.yaml b/pubspec_base.yaml index da79a5076..f11876423 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -82,6 +82,8 @@ 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 bitcoin_flutter: git: url: https://github.com/cake-tech/bitcoin_flutter.git diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index 3a90aa308..f7bb19c45 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -689,8 +689,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 ﻲﻬﺘﻨﻳ ﻰﺘﺣ ﺭﺎﻈﺘﻧﻻﺍ ﻰﺟﺮﻳ" } - diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index 39ce6196b..644e18d33 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -685,6 +685,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 да завърши обработката." } diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index 0c063f0c9..41d428d7b 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -685,6 +685,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í." } diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 4b2cba6d4..4fcf08363 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -693,6 +693,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." } diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 7e3b1b55d..d1d356f29 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -694,6 +694,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." } diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index f2b0729f3..8dea97b87 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -693,6 +693,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." } diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index a081727fb..bdc92e40f 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -693,6 +693,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." } diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index 68051b5d9..18b65fb28 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -671,6 +671,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." } diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index 9f531bb29..1a2edd9ad 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -693,6 +693,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": "कृपया डीएपी की प्रोसेसिंग पूरी होने तक प्रतीक्षा करें।" } diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index 4b56ef344..d47890556 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -693,6 +693,27 @@ "default_buy_provider": "Zadani davatelj kupnje", "ask_each_time": "Pitajte svaki put", "buy_provider_unavailable": "Davatelj trenutno nije dostupan.", + "signTransaction": "Potpišite transakciju", + "errorGettingCredentials": "Neuspješno: Pogreška prilikom dobivanja vjerodajnica", + "errorSigningTransaction": "Došlo je do pogreške prilikom potpisivanja transakcije", + "pairingInvalidEvent": "Nevažeći događaj uparivanja", + "chains": "Lanci", + "methods": "Metode", + "events": "Događaji", + "reject": "Odbiti", + "approve": "Odobriti", + "expiresOn": "Istječe", + "walletConnect": "WalletConnect", + "nullURIError": "URI je nula", + "connectWalletPrompt": "Povežite svoj novčanik s WalletConnectom za obavljanje transakcija", + "newConnection": "Nova veza", + "activeConnectionsPrompt": "Ovdje će se pojaviti aktivne veze", + "deleteConnectionConfirmationPrompt": "Jeste li sigurni da želite izbrisati vezu s", + "event": "Događaj", + "successful": "Uspješno", + "wouoldLikeToConnect": "želio bi se povezati", + "message": "Poruka", "do_not_have_enough_gas_asset": "Nemate dovoljno ${currency} da izvršite transakciju s trenutačnim uvjetima blockchain mreže. Trebate više ${currency} da platite naknade za blockchain mrežu, čak i ako šaljete drugu imovinu.", - "totp_auth_url": "TOTP AUTH URL" -} \ No newline at end of file + "totp_auth_url": "TOTP AUTH URL", + "awaitDAppProcessing": "Molimo pričekajte da dApp završi obradu." +} diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index 6200559e3..6adbe2a1f 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -681,6 +681,27 @@ "default_buy_provider": "Penyedia beli default", "ask_each_time": "Tanyakan setiap kali", "buy_provider_unavailable": "Penyedia saat ini tidak tersedia.", + "signTransaction": "Tandatangani Transaksi", + "errorGettingCredentials": "Gagal: Terjadi kesalahan saat mendapatkan kredensial", + "errorSigningTransaction": "Terjadi kesalahan saat menandatangani transaksi", + "pairingInvalidEvent": "Menyandingkan Acara Tidak Valid", + "chains": "Rantai", + "methods": "Metode", + "events": "Acara", + "reject": "Menolak", + "approve": "Menyetujui", + "expiresOn": "Kadaluarsa pada", + "walletConnect": "DompetConnect", + "nullURIError": "URI adalah nol", + "connectWalletPrompt": "Hubungkan dompet Anda dengan WalletConnect untuk melakukan transaksi", + "newConnection": "Koneksi Baru", + "activeConnectionsPrompt": "Koneksi aktif akan muncul di sini", + "deleteConnectionConfirmationPrompt": "Apakah Anda yakin ingin menghapus koneksi ke", + "event": "Peristiwa", + "successful": "Berhasil", + "wouoldLikeToConnect": "ingin terhubung", + "message": "Pesan", "do_not_have_enough_gas_asset": "Anda tidak memiliki cukup ${currency} untuk melakukan transaksi dengan kondisi jaringan blockchain saat ini. Anda memerlukan lebih banyak ${currency} untuk membayar biaya jaringan blockchain, meskipun Anda mengirimkan aset yang berbeda.", - "totp_auth_url": "URL Otentikasi TOTP" -} \ No newline at end of file + "totp_auth_url": "URL Otentikasi TOTP", + "awaitDAppProcessing": "Mohon tunggu hingga dApp menyelesaikan pemrosesan." +} diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index e3fd1b94b..db687ffe7 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -693,6 +693,27 @@ "default_buy_provider": "Provider di acquisto predefinito", "ask_each_time": "Chiedi ogni volta", "buy_provider_unavailable": "Provider attualmente non disponibile.", + "signTransaction": "Firma la transazione", + "errorGettingCredentials": "Non riuscito: errore durante il recupero delle credenziali", + "errorSigningTransaction": "Si è verificato un errore durante la firma della transazione", + "pairingInvalidEvent": "Associazione evento non valido", + "chains": "Catene", + "methods": "Metodi", + "events": "Eventi", + "reject": "Rifiutare", + "approve": "Approvare", + "expiresOn": "Scade il", + "walletConnect": "PortafoglioConnetti", + "nullURIError": "L'URI è nullo", + "connectWalletPrompt": "Collega il tuo portafoglio con WalletConnect per effettuare transazioni", + "newConnection": "Nuova connessione", + "activeConnectionsPrompt": "Le connessioni attive verranno visualizzate qui", + "deleteConnectionConfirmationPrompt": "Sei sicuro di voler eliminare la connessione a", + "event": "Evento", + "successful": "Riuscito", + "wouoldLikeToConnect": "vorrei connettermi", + "message": "Messaggio", "do_not_have_enough_gas_asset": "Non hai abbastanza ${currency} per effettuare una transazione con le attuali condizioni della rete blockchain. Hai bisogno di più ${currency} per pagare le commissioni della rete blockchain, anche se stai inviando una risorsa diversa.", - "totp_auth_url": "URL DI AUT. TOTP" + "totp_auth_url": "URL DI AUT. TOTP", + "awaitDAppProcessing": "Attendi gentilmente che la dApp termini l'elaborazione." } diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index 8dbf429c5..32dd157a4 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -693,6 +693,27 @@ "default_buy_provider": "デフォルトの購入プロバイダー", "ask_each_time": "毎回尋ねてください", "buy_provider_unavailable": "現在、プロバイダーは利用できません。", + "signTransaction": "トランザクションに署名する", + "errorGettingCredentials": "失敗: 認証情報の取得中にエラーが発生しました", + "errorSigningTransaction": "トランザクションの署名中にエラーが発生しました", + "pairingInvalidEvent": "ペアリング無効イベント", + "chains": "チェーン", + "methods": "メソッド", + "events": "イベント", + "reject": "拒否する", + "approve": "承認する", + "expiresOn": "有効期限は次のとおりです", + "walletConnect": "ウォレットコネクト", + "nullURIError": "URIがnullです", + "connectWalletPrompt": "ウォレットを WalletConnect に接続して取引を行う", + "newConnection": "新しい接続", + "activeConnectionsPrompt": "アクティブな接続がここに表示されます", + "deleteConnectionConfirmationPrompt": "への接続を削除してもよろしいですか?", + "event": "イベント", + "successful": "成功", + "wouoldLikeToConnect": "接続したいです", + "message": "メッセージ", "do_not_have_enough_gas_asset": "現在のブロックチェーン ネットワークの状況では、トランザクションを行うのに十分な ${currency} がありません。別のアセットを送信する場合でも、ブロックチェーン ネットワーク料金を支払うにはさらに ${currency} が必要です。", - "totp_auth_url": "TOTP認証URL" + "totp_auth_url": "TOTP認証URL", + "awaitDAppProcessing": "dAppの処理が完了するまでお待ちください。" } diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index da9b99c2b..2ac9ed4c6 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -693,6 +693,27 @@ "default_buy_provider": "기본 구매 제공자", "ask_each_time": "매번 물어보십시오", "buy_provider_unavailable": "제공자는 현재 사용할 수 없습니다.", + "signTransaction": "거래 서명", + "errorGettingCredentials": "실패: 자격 증명을 가져오는 중 오류가 발생했습니다.", + "errorSigningTransaction": "거래에 서명하는 동안 오류가 발생했습니다.", + "pairingInvalidEvent": "잘못된 이벤트 페어링", + "chains": "쇠사슬", + "methods": "행동 양식", + "events": "이벤트", + "reject": "거부하다", + "approve": "승인하다", + "expiresOn": "만료 날짜", + "walletConnect": "월렛커넥트", + "nullURIError": "URI가 null입니다.", + "connectWalletPrompt": "거래를 하려면 WalletConnect에 지갑을 연결하세요.", + "newConnection": "새로운 연결", + "activeConnectionsPrompt": "활성 연결이 여기에 표시됩니다", + "deleteConnectionConfirmationPrompt": "다음 연결을 삭제하시겠습니까?", + "event": "이벤트", + "successful": "성공적인", + "wouoldLikeToConnect": "연결하고 싶습니다", + "message": "메시지", "do_not_have_enough_gas_asset": "현재 블록체인 네트워크 조건으로 거래를 하기에는 ${currency}이(가) 충분하지 않습니다. 다른 자산을 보내더라도 블록체인 네트워크 수수료를 지불하려면 ${currency}가 더 필요합니다.", - "totp_auth_url": "TOTP 인증 URL" + "totp_auth_url": "TOTP 인증 URL", + "awaitDAppProcessing": "dApp이 처리를 마칠 때까지 기다려주세요." } diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index 043484feb..80f8d6206 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -691,6 +691,27 @@ "default_buy_provider": "Default Provider ကိုဝယ်ပါ", "ask_each_time": "တစ်ခုချင်းစီကိုအချိန်မေးပါ", "buy_provider_unavailable": "လက်ရှိတွင်လက်ရှိမရနိုင်ပါ။", + "signTransaction": "ငွေလွှဲဝင်ပါ။", + "errorGettingCredentials": "မအောင်မြင်ပါ- အထောက်အထားများ ရယူနေစဉ် အမှားအယွင်း", + "errorSigningTransaction": "ငွေပေးငွေယူ လက်မှတ်ထိုးစဉ် အမှားအယွင်းတစ်ခု ဖြစ်ပေါ်ခဲ့သည်။", + "pairingInvalidEvent": "မမှန်ကန်သောဖြစ်ရပ်ကို တွဲချိတ်ခြင်း။", + "chains": "ဆွဲကြိုး", + "methods": "နည်းလမ်းများ", + "events": "အဲ့ဒါနဲ့", + "reject": "ငြင်းပယ်ပါ။", + "approve": "လက်မခံပါ။", + "expiresOn": "သက်တမ်းကုန်သည်။", + "walletConnect": "Wallet ချိတ်ဆက်မှု", + "nullURIError": "URI သည် null ဖြစ်သည်။", + "connectWalletPrompt": "အရောင်းအဝယ်ပြုလုပ်ရန် သင့်ပိုက်ဆံအိတ်ကို WalletConnect နှင့် ချိတ်ဆက်ပါ။", + "newConnection": "ချိတ်ဆက်မှုအသစ်", + "activeConnectionsPrompt": "လက်ရှိချိတ်ဆက်မှုများ ဤနေရာတွင် ပေါ်လာပါမည်။", + "deleteConnectionConfirmationPrompt": "ချိတ်ဆက်မှုကို ဖျက်လိုသည်မှာ သေချာပါသလား။", + "event": "ပွဲ", + "successful": "အောင်မြင်တယ်။", + "wouoldLikeToConnect": "ချိတ်ဆက်ချင်ပါတယ်။", + "message": "မက်ဆေ့ချ်", "do_not_have_enough_gas_asset": "လက်ရှိ blockchain ကွန်ရက်အခြေအနေများနှင့် အရောင်းအဝယ်ပြုလုပ်ရန် သင့်တွင် ${currency} လုံလောက်မှုမရှိပါ။ သင်သည် မတူညီသော ပိုင်ဆိုင်မှုတစ်ခုကို ပေးပို့နေသော်လည်း blockchain ကွန်ရက်အခကြေးငွေကို ပေးဆောင်ရန် သင်သည် နောက်ထပ် ${currency} လိုအပ်ပါသည်။", - "totp_auth_url": "TOTP AUTH URL" + "totp_auth_url": "TOTP AUTH URL", + "awaitDAppProcessing": "ကျေးဇူးပြု၍ dApp ကို စီမံလုပ်ဆောင်ခြင်း အပြီးသတ်ရန် စောင့်ပါ။" } diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 2d9f3655e..45cab5d09 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -693,6 +693,27 @@ "default_buy_provider": "Standaard Koopprovider", "ask_each_time": "Vraag het elke keer", "buy_provider_unavailable": "Provider momenteel niet beschikbaar.", + "signTransaction": "Transactie ondertekenen", + "errorGettingCredentials": "Mislukt: fout bij het ophalen van inloggegevens", + "errorSigningTransaction": "Er is een fout opgetreden tijdens het ondertekenen van de transactie", + "pairingInvalidEvent": "Koppelen Ongeldige gebeurtenis", + "chains": "Ketens", + "methods": "Methoden", + "events": "Evenementen", + "reject": "Afwijzen", + "approve": "Goedkeuren", + "expiresOn": "Verloopt op", + "walletConnect": "WalletConnect", + "nullURIError": "URI is nul", + "connectWalletPrompt": "Verbind uw portemonnee met WalletConnect om transacties uit te voeren", + "newConnection": "Nieuwe verbinding", + "activeConnectionsPrompt": "Actieve verbindingen worden hier weergegeven", + "deleteConnectionConfirmationPrompt": "Weet u zeker dat u de verbinding met", + "event": "Evenement", + "successful": "Succesvol", + "wouoldLikeToConnect": "wil graag verbinden", + "message": "Bericht", "do_not_have_enough_gas_asset": "U heeft niet genoeg ${currency} om een transactie uit te voeren met de huidige blockchain-netwerkomstandigheden. U heeft meer ${currency} nodig om blockchain-netwerkkosten te betalen, zelfs als u een ander item verzendt.", - "totp_auth_url": "TOTP AUTH-URL" + "totp_auth_url": "TOTP AUTH-URL", + "awaitDAppProcessing": "Wacht tot de dApp klaar is met verwerken." } diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index 6f3a41d0b..0d9c22c79 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -693,6 +693,27 @@ "default_buy_provider": "Domyślny dostawca zakupu", "ask_each_time": "Zapytaj za każdym razem", "buy_provider_unavailable": "Dostawca obecnie niedostępny.", + "signTransaction": "Podpisz transakcję", + "errorGettingCredentials": "Niepowodzenie: Błąd podczas uzyskiwania poświadczeń", + "errorSigningTransaction": "Wystąpił błąd podczas podpisywania transakcji", + "pairingInvalidEvent": "Nieprawidłowe zdarzenie parowania", + "chains": "Więzy", + "methods": "Metody", + "events": "Wydarzenia", + "reject": "Odrzucić", + "approve": "Zatwierdzić", + "expiresOn": "Upływa w dniu", + "walletConnect": "PortfelPołącz", + "nullURIError": "URI ma wartość zerową", + "connectWalletPrompt": "Połącz swój portfel z WalletConnect, aby dokonywać transakcji", + "newConnection": "Nowe połączenie", + "activeConnectionsPrompt": "Tutaj pojawią się aktywne połączenia", + "deleteConnectionConfirmationPrompt": "Czy na pewno chcesz usunąć połączenie z", + "event": "Wydarzenie", + "successful": "Udany", + "wouoldLikeToConnect": "chciałbym się połączyć", + "message": "Wiadomość", "do_not_have_enough_gas_asset": "Nie masz wystarczającej ilości ${currency}, aby dokonać transakcji przy bieżących warunkach sieci blockchain. Potrzebujesz więcej ${currency}, aby uiścić opłaty za sieć blockchain, nawet jeśli wysyłasz inny zasób.", - "totp_auth_url": "Adres URL TOTP AUTH" + "totp_auth_url": "Adres URL TOTP AUTH", + "awaitDAppProcessing": "Poczekaj, aż dApp zakończy przetwarzanie." } diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index ea442420f..302955780 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -692,6 +692,27 @@ "default_buy_provider": "Provedor de compra padrão", "ask_each_time": "Pergunte cada vez", "buy_provider_unavailable": "Provedor atualmente indisponível.", + "signTransaction": "Assinar transação", + "errorGettingCredentials": "Falha: Erro ao obter credenciais", + "errorSigningTransaction": "Ocorreu um erro ao assinar a transação", + "pairingInvalidEvent": "Emparelhamento de evento inválido", + "chains": "Correntes", + "methods": "Métodos", + "events": "Eventos", + "reject": "Rejeitar", + "approve": "Aprovar", + "expiresOn": "Expira em", + "walletConnect": "CarteiraConectada", + "nullURIError": "URI é nulo", + "connectWalletPrompt": "Conecte sua carteira ao WalletConnect para fazer transações", + "newConnection": "Nova conexão", + "activeConnectionsPrompt": "Conexões ativas aparecerão aqui", + "deleteConnectionConfirmationPrompt": "Tem certeza de que deseja excluir a conexão com", + "event": "Evento", + "successful": "Bem-sucedido", + "wouoldLikeToConnect": "gostaria de me conectar", + "message": "Mensagem", "do_not_have_enough_gas_asset": "Você não tem ${currency} suficiente para fazer uma transação com as condições atuais da rede blockchain. Você precisa de mais ${currency} para pagar as taxas da rede blockchain, mesmo se estiver enviando um ativo diferente.", - "totp_auth_url": "URL de autenticação TOTP" + "totp_auth_url": "URL de autenticação TOTP", + "awaitDAppProcessing": "Aguarde até que o dApp termine o processamento." } diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index 5e70bc544..c1af0eb74 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -693,6 +693,27 @@ "default_buy_provider": "По умолчанию поставщик покупки", "ask_each_time": "Спросите каждый раз", "buy_provider_unavailable": "Поставщик в настоящее время недоступен.", + "signTransaction": "Подписать транзакцию", + "errorGettingCredentials": "Не удалось: ошибка при получении учетных данных.", + "errorSigningTransaction": "Произошла ошибка при подписании транзакции", + "pairingInvalidEvent": "Недействительное событие сопряжения", + "chains": "Цепи", + "methods": "Методы", + "events": "События", + "reject": "Отклонять", + "approve": "Утвердить", + "expiresOn": "Годен до", + "walletConnect": "КошелекПодключиться", + "nullURIError": "URI имеет значение null", + "connectWalletPrompt": "Подключите свой кошелек к WalletConnect для совершения транзакций.", + "newConnection": "Новое соединение", + "activeConnectionsPrompt": "Здесь появятся активные подключения", + "deleteConnectionConfirmationPrompt": "Вы уверены, что хотите удалить подключение к", + "event": "Событие", + "successful": "Успешный", + "wouoldLikeToConnect": "хотел бы подключиться", + "message": "Сообщение", "do_not_have_enough_gas_asset": "У вас недостаточно ${currency} для совершения транзакции при текущих условиях сети блокчейн. Вам нужно больше ${currency} для оплаты комиссий за сеть блокчейна, даже если вы отправляете другой актив.", - "totp_auth_url": "URL-адрес TOTP-АВТОРИЗАЦИИ" + "totp_auth_url": "URL-адрес TOTP-АВТОРИЗАЦИИ", + "awaitDAppProcessing": "Пожалуйста, подождите, пока dApp завершит обработку." } diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index 8778802a9..eb4b1ac31 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -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": "URL การตรวจสอบสิทธิ์ TOTP" + "totp_auth_url": "URL การตรวจสอบสิทธิ์ TOTP", + "awaitDAppProcessing": "โปรดรอให้ dApp ประมวลผลเสร็จสิ้น" } diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index 18c52aa83..8072eee61 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -688,6 +688,33 @@ "support_description_other_links": "Sumali sa aming mga komunidad o maabot sa amin ang aming mga kasosyo sa pamamagitan ng iba pang mga pamamaraan", "select_destination": "Mangyaring piliin ang patutunguhan para sa backup file.", "save_to_downloads": "I -save sa mga pag -download", + "select_buy_provider_notice": "Pumili ng provider ng pagbili sa itaas. Maaari mong laktawan ang screen na ito sa pamamagitan ng pagtatakda ng iyong default na provider ng pagbili sa mga setting ng app.", + "onramper_option_description": "Mabilis na bumili ng crypto na may maraming paraan ng pagbabayad. Available sa karamihan ng mga bansa. Iba-iba ang mga spread at bayarin.", + "default_buy_provider": "Default na Provider ng Pagbili", + "ask_each_time": "Magtanong sa bawat oras", + "robinhood_option_description": "Bumili at ilipat kaagad gamit ang iyong debit card, bank account, o balanse ng Robinhood. USA lang.", + "buy_provider_unavailable": "Kasalukuyang hindi available ang provider.", + "signTransaction": "Mag-sign Transaksyon", + "errorGettingCredentials": "Nabigo: Error habang kumukuha ng mga kredensyal", + "errorSigningTransaction": "May naganap na error habang pinipirmahan ang transaksyon", + "pairingInvalidEvent": "Pagpares ng Di-wastong Kaganapan", + "chains": "Mga tanikala", + "methods": "Paraan", + "events": "Mga kaganapan", + "reject": "Tanggihan", + "approve": "Aprubahan", + "expiresOn": "Mag-e-expire sa", + "walletConnect": "WalletConnect", + "nullURIError": "Ang URI ay null", + "connectWalletPrompt": "Ikonekta ang iyong wallet sa WalletConnect upang gumawa ng mga transaksyon", + "newConnection": "Bagong Koneksyon", + "activeConnectionsPrompt": "Lalabas dito ang mga aktibong koneksyon", + "deleteConnectionConfirmationPrompt": "Sigurado ka bang gusto mong tanggalin ang koneksyon sa", + "event": "Kaganapan", + "successful": "Matagumpay", + "wouoldLikeToConnect": "gustong kumonekta", + "message": "Mensahe", "do_not_have_enough_gas_asset": "Wala kang sapat na ${currency} para gumawa ng transaksyon sa kasalukuyang kundisyon ng network ng blockchain. Kailangan mo ng higit pang ${currency} upang magbayad ng mga bayarin sa network ng blockchain, kahit na nagpapadala ka ng ibang asset.", - "totp_auth_url": "TOTP AUTH URL" -} \ No newline at end of file + "totp_auth_url": "TOTP AUTH URL", + "awaitDAppProcessing": "Pakihintay na matapos ang pagproseso ng dApp." +} diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index fa4568e93..d0afc5825 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -691,6 +691,27 @@ "default_buy_provider": "Varsayılan Satın Alma Sağlayıcısı", "ask_each_time": "Her seferinde sor", "buy_provider_unavailable": "Sağlayıcı şu anda kullanılamıyor.", + "signTransaction": "İşlem İmzala", + "errorGettingCredentials": "Başarısız: Kimlik bilgileri alınırken hata oluştu", + "errorSigningTransaction": "İşlem imzalanırken bir hata oluştu", + "pairingInvalidEvent": "Geçersiz Etkinliği Eşleştirme", + "chains": "Zincirler", + "methods": "Yöntemler", + "events": "Olaylar", + "reject": "Reddetmek", + "approve": "Onaylamak", + "expiresOn": "Tarihinde sona eriyor", + "walletConnect": "WalletConnect", + "nullURIError": "URI boş", + "connectWalletPrompt": "İşlem yapmak için cüzdanınızı WalletConnect'e bağlayın", + "newConnection": "Yeni bağlantı", + "activeConnectionsPrompt": "Aktif bağlantılar burada görünecek", + "deleteConnectionConfirmationPrompt": "Bağlantıyı silmek istediğinizden emin misiniz?", + "event": "Etkinlik", + "successful": "Başarılı", + "wouoldLikeToConnect": "bağlanmak istiyorum", + "message": "İleti", "do_not_have_enough_gas_asset": "Mevcut blockchain ağ koşullarıyla işlem yapmak için yeterli ${currency} paranız yok. Farklı bir varlık gönderiyor olsanız bile blockchain ağ ücretlerini ödemek için daha fazla ${currency} miktarına ihtiyacınız var.", - "totp_auth_url": "TOTP YETKİ URL'si" + "totp_auth_url": "TOTP YETKİ URL'si", + "awaitDAppProcessing": "Lütfen dApp'in işlemeyi bitirmesini bekleyin." } diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index 7b739e201..99720ca51 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -693,6 +693,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 завершить обробку." } diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index e53784923..21c247fd1 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -685,6 +685,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 ﻡﺮﮐ ﮦﺍﺮﺑ" } diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index 0532ae2eb..e362df16d 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -687,6 +687,27 @@ "default_buy_provider": "Aiyipada Ra Olupese", "ask_each_time": "Beere lọwọ kọọkan", "buy_provider_unavailable": "Olupese lọwọlọwọ ko si.", + "signTransaction": "Wole Idunadura", + "errorGettingCredentials": "Kuna: Aṣiṣe lakoko gbigba awọn iwe-ẹri", + "errorSigningTransaction": "Aṣiṣe kan ti waye lakoko ti o fowo si iṣowo", + "pairingInvalidEvent": "Pipọpọ Iṣẹlẹ Ti ko tọ", + "chains": "Awọn ẹwọn", + "methods": "Awọn ọna", + "events": "Awọn iṣẹlẹ", + "reject": "Kọ", + "approve": "Fi ọwọ si", + "expiresOn": "Ipari lori", + "walletConnect": "Asopọmọra apamọwọ", + "nullURIError": "URI jẹ asan", + "connectWalletPrompt": "So apamọwọ rẹ pọ pẹlu WalletConnect lati ṣe awọn iṣowo", + "newConnection": "Tuntun Asopọ", + "activeConnectionsPrompt": "Awọn asopọ ti nṣiṣe lọwọ yoo han nibi", + "deleteConnectionConfirmationPrompt": "Ṣe o da ọ loju pe o fẹ paarẹ asopọ si", + "event": "Iṣẹlẹ", + "successful": "Aseyori", + "wouoldLikeToConnect": "yoo fẹ lati sopọ", + "message": "Ifiranṣẹ", "do_not_have_enough_gas_asset": "O ko ni to ${currency} lati ṣe idunadura kan pẹlu awọn ipo nẹtiwọki blockchain lọwọlọwọ. O nilo diẹ sii ${currency} lati san awọn owo nẹtiwọọki blockchain, paapaa ti o ba nfi dukia miiran ranṣẹ.", - "totp_auth_url": "TOTP AUTH URL" + "totp_auth_url": "TOTP AUTH URL", + "awaitDAppProcessing": "Fi inurere duro fun dApp lati pari sisẹ." } diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index 7c947847e..19928f5e2 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -692,6 +692,27 @@ "default_buy_provider": "默认购买提供商", "ask_each_time": "每次问", "buy_provider_unavailable": "提供者目前不可用。", + "signTransaction": "签署交易", + "errorGettingCredentials": "失败:获取凭据时出错", + "errorSigningTransaction": "签署交易时发生错误", + "pairingInvalidEvent": "配对无效事件", + "chains": "链条", + "methods": "方法", + "events": "活动", + "reject": "拒绝", + "approve": "批准", + "expiresOn": "到期", + "walletConnect": "钱包连接", + "nullURIError": "URI 为空", + "connectWalletPrompt": "将您的钱包与 WalletConnect 连接以进行交易", + "newConnection": "新连接", + "activeConnectionsPrompt": "活动连接将出现在这里", + "deleteConnectionConfirmationPrompt": "您确定要删除与", + "event": "事件", + "successful": "成功的", + "wouoldLikeToConnect": "想要连接", + "message": "信息", "do_not_have_enough_gas_asset": "您没有足够的 ${currency} 来在当前的区块链网络条件下进行交易。即使您发送的是不同的资产,您也需要更多的 ${currency} 来支付区块链网络费用。", - "totp_auth_url": "TOTP 授权 URL" + "totp_auth_url": "TOTP 授权 URL", + "awaitDAppProcessing": "请等待 dApp 处理完成。" } diff --git a/tool/configure.dart b/tool/configure.dart index 5172f4244..2f594925e 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -477,6 +477,7 @@ 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_service.dart'; +import 'package:eth_sig_util/util/utils.dart'; import 'package:hive/hive.dart'; """; const ethereumCWHeaders = """ @@ -498,6 +499,8 @@ abstract class Ethereum { WalletCredentials createEthereumRestoreWalletFromSeedCredentials({required String name, required String mnemonic, required String password}); WalletCredentials createEthereumRestoreWalletFromPrivateKey({required String name, required String privateKey, required String password}); String getAddress(WalletBase wallet); + String getPrivateKey(WalletBase wallet); + String getPublicKey(WalletBase wallet); TransactionPriority getDefaultTransactionPriority(); List getTransactionPriorities(); TransactionPriority deserializeEthereumTransactionPriority(int raw); diff --git a/tool/utils/secret_key.dart b/tool/utils/secret_key.dart index 5e959b99b..a8c6a6166 100644 --- a/tool/utils/secret_key.dart +++ b/tool/utils/secret_key.dart @@ -35,6 +35,7 @@ class SecretKey { SecretKey('exolixApiKey', () => ''), SecretKey('robinhoodApplicationId', () => ''), SecretKey('robinhoodCIdApiSecret', () => ''), + SecretKey('walletConnectProjectId', () => ''), ]; static final ethereumSecrets = [