diff --git a/.github/workflows/cache_dependencies.yml b/.github/workflows/cache_dependencies.yml index c0042bf5c..ce098b7f1 100644 --- a/.github/workflows/cache_dependencies.yml +++ b/.github/workflows/cache_dependencies.yml @@ -62,10 +62,22 @@ jobs: /opt/android/cake_wallet/cw_haven/android/.cxx /opt/android/cake_wallet/scripts/monero_c/release key: ${{ hashFiles('**/prepare_moneroc.sh' ,'**/build_monero_all.sh' ,'**/cache_dependencies.yml') }} - - if: ${{ steps.cache-externals.outputs.cache-hit != 'true' }} name: Generate Externals run: | cd /opt/android/cake_wallet/scripts/android/ source ./app_env.sh cakewallet ./build_monero_all.sh + + - name: Cache Keystore + id: cache-keystore + uses: actions/cache@v3 + with: + path: /opt/android/cake_wallet/android/app/key.jks + key: $STORE_PASS + + - if: ${{ steps.cache-keystore.outputs.cache-hit != 'true' }} + name: Generate KeyStore + run: | + cd /opt/android/cake_wallet/android/app + keytool -genkey -v -keystore key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias testKey -noprompt -dname "CN=CakeWallet, OU=CakeWallet, O=CakeWallet, L=Florida, S=America, C=USA" -storepass $STORE_PASS -keypass $KEY_PASS diff --git a/.github/workflows/pr_test_build_android.yml b/.github/workflows/pr_test_build_android.yml index c9021fac0..774404e3b 100644 --- a/.github/workflows/pr_test_build_android.yml +++ b/.github/workflows/pr_test_build_android.yml @@ -115,6 +115,14 @@ jobs: cd /opt/android/cake_wallet/scripts/android/ ./build_mwebd.sh --dont-install +# - name: Cache Keystore +# id: cache-keystore +# uses: actions/cache@v3 +# with: +# path: /opt/android/cake_wallet/android/app/key.jks +# key: $STORE_PASS +# +# - if: ${{ steps.cache-keystore.outputs.cache-hit != 'true' }} - name: Generate KeyStore run: | cd /opt/android/cake_wallet/android/app @@ -192,6 +200,8 @@ jobs: echo "const nanoNowNodesApiKey = '${{ secrets.NANO_NOW_NODES_API_KEY }}';" >> cw_nano/lib/.secrets.g.dart echo "const tronGridApiKey = '${{ secrets.TRON_GRID_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart echo "const tronNowNodesApiKey = '${{ secrets.TRON_NOW_NODES_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart + echo "const meldTestApiKey = '${{ secrets.MELD_TEST_API_KEY }}';" >> lib/.secrets.g.dart + echo "const meldTestPublicKey = '${{ secrets.MELD_TEST_PUBLIC_KEY}}';" >> lib/.secrets.g.dar echo "const letsExchangeBearerToken = '${{ secrets.LETS_EXCHANGE_TOKEN }}';" >> lib/.secrets.g.dart echo "const letsExchangeAffiliateId = '${{ secrets.LETS_EXCHANGE_AFFILIATE_ID }}';" >> lib/.secrets.g.dart echo "const stealthExBearerToken = '${{ secrets.STEALTH_EX_BEARER_TOKEN }}';" >> lib/.secrets.g.dart @@ -201,6 +211,36 @@ jobs: run: | echo -e "id=com.cakewallet.test_${{ env.PR_NUMBER }}\nname=${{ env.BRANCH_NAME }}" > /opt/android/cake_wallet/android/app.properties + # Step 3: Download previous build number + - name: Download previous build number + id: download-build-number + run: | + # Download the artifact if it exists + if [[ ! -f build_number.txt ]]; then + echo "1" > build_number.txt + fi + + # Step 4: Read and Increment Build Number + - name: Increment Build Number + id: increment-build-number + run: | + # Read current build number from file + BUILD_NUMBER=$(cat build_number.txt) + BUILD_NUMBER=$((BUILD_NUMBER + 1)) + echo "New build number: $BUILD_NUMBER" + + # Save the incremented build number + echo "$BUILD_NUMBER" > build_number.txt + + # Export the build number to use in later steps + echo "BUILD_NUMBER=$BUILD_NUMBER" >> $GITHUB_ENV + + # Step 5: Update pubspec.yaml with new build number + - name: Update build number + run: | + cd /opt/android/cake_wallet + sed -i "s/^version: .*/version: 1.0.$BUILD_NUMBER/" pubspec.yaml + - name: Build run: | cd /opt/android/cake_wallet @@ -231,6 +271,13 @@ jobs: with: path: /opt/android/cake_wallet/build/app/outputs/flutter-apk/test-apk/ + # Re-upload updated build number for the next run + - name: Upload updated build number + uses: actions/upload-artifact@v3 + with: + name: build_number + path: build_number.txt + - name: Send Test APK continue-on-error: true uses: adrey/slack-file-upload-action@1.0.5 @@ -241,3 +288,4 @@ jobs: title: "${{ env.BRANCH_NAME }}.apk" filename: ${{ env.BRANCH_NAME }}.apk initial_comment: ${{ github.event.head_commit.message }} + diff --git a/.github/workflows/pr_test_build_linux.yml b/.github/workflows/pr_test_build_linux.yml index 5ea0cb377..f3ce1045d 100644 --- a/.github/workflows/pr_test_build_linux.yml +++ b/.github/workflows/pr_test_build_linux.yml @@ -175,6 +175,8 @@ jobs: echo "const nanoNowNodesApiKey = '${{ secrets.NANO_NOW_NODES_API_KEY }}';" >> cw_nano/lib/.secrets.g.dart echo "const tronGridApiKey = '${{ secrets.TRON_GRID_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart echo "const tronNowNodesApiKey = '${{ secrets.TRON_NOW_NODES_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart + echo "const meldTestApiKey = '${{ secrets.MELD_TEST_API_KEY }}';" >> lib/.secrets.g.dart + echo "const meldTestPublicKey = '${{ secrets.MELD_TEST_PUBLIC_KEY}}';" >> lib/.secrets.g.dar echo "const letsExchangeBearerToken = '${{ secrets.LETS_EXCHANGE_TOKEN }}';" >> lib/.secrets.g.dart echo "const letsExchangeAffiliateId = '${{ secrets.LETS_EXCHANGE_AFFILIATE_ID }}';" >> lib/.secrets.g.dart echo "const stealthExBearerToken = '${{ secrets.STEALTH_EX_BEARER_TOKEN }}';" >> lib/.secrets.g.dart diff --git a/android/app/build.gradle b/android/app/build.gradle index 2f5427531..5005a8bab 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -92,3 +92,8 @@ dependencies { androidTestImplementation 'androidx.test:runner:1.3.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' } +configurations { + implementation.exclude module:'proto-google-common-protos' + implementation.exclude module:'protolite-well-known-types' + implementation.exclude module:'protobuf-javalite' +} \ No newline at end of file diff --git a/android/gradle.properties b/android/gradle.properties index 38c8d4544..66dd09454 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,4 +1,4 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx3072M android.enableR8=true android.useAndroidX=true android.enableJetifier=true diff --git a/assets/images/apple_pay_logo.png b/assets/images/apple_pay_logo.png new file mode 100644 index 000000000..346007e3b Binary files /dev/null and b/assets/images/apple_pay_logo.png differ diff --git a/assets/images/apple_pay_round_dark.svg b/assets/images/apple_pay_round_dark.svg new file mode 100644 index 000000000..82443bfb4 --- /dev/null +++ b/assets/images/apple_pay_round_dark.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/assets/images/apple_pay_round_light.svg b/assets/images/apple_pay_round_light.svg new file mode 100644 index 000000000..2beb1248f --- /dev/null +++ b/assets/images/apple_pay_round_light.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/assets/images/bank.png b/assets/images/bank.png new file mode 100644 index 000000000..9dc68147a Binary files /dev/null and b/assets/images/bank.png differ diff --git a/assets/images/bank_dark.svg b/assets/images/bank_dark.svg new file mode 100644 index 000000000..670120796 --- /dev/null +++ b/assets/images/bank_dark.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/assets/images/bank_light.svg b/assets/images/bank_light.svg new file mode 100644 index 000000000..804716289 --- /dev/null +++ b/assets/images/bank_light.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/assets/images/buy_sell.png b/assets/images/buy_sell.png new file mode 100644 index 000000000..0fbffe56f Binary files /dev/null and b/assets/images/buy_sell.png differ diff --git a/assets/images/card.svg b/assets/images/card.svg new file mode 100644 index 000000000..95530cdc9 --- /dev/null +++ b/assets/images/card.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/images/card_dark.svg b/assets/images/card_dark.svg new file mode 100644 index 000000000..2e5bcf986 --- /dev/null +++ b/assets/images/card_dark.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/assets/images/dollar_coin.svg b/assets/images/dollar_coin.svg new file mode 100644 index 000000000..22218f332 --- /dev/null +++ b/assets/images/dollar_coin.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/images/google_pay_icon.png b/assets/images/google_pay_icon.png new file mode 100644 index 000000000..a3ca38311 Binary files /dev/null and b/assets/images/google_pay_icon.png differ diff --git a/assets/images/meld_logo.svg b/assets/images/meld_logo.svg new file mode 100644 index 000000000..1d9211d64 --- /dev/null +++ b/assets/images/meld_logo.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/revolut.png b/assets/images/revolut.png new file mode 100644 index 000000000..bbe342592 Binary files /dev/null and b/assets/images/revolut.png differ diff --git a/assets/images/skrill.svg b/assets/images/skrill.svg new file mode 100644 index 000000000..b264b57eb --- /dev/null +++ b/assets/images/skrill.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/images/usd_round_dark.svg b/assets/images/usd_round_dark.svg new file mode 100644 index 000000000..f329dd617 --- /dev/null +++ b/assets/images/usd_round_dark.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/assets/images/usd_round_light.svg b/assets/images/usd_round_light.svg new file mode 100644 index 000000000..f5965c597 --- /dev/null +++ b/assets/images/usd_round_light.svg @@ -0,0 +1,2 @@ + + diff --git a/assets/images/wallet_new.png b/assets/images/wallet_new.png new file mode 100644 index 000000000..47c43bfca Binary files /dev/null and b/assets/images/wallet_new.png differ diff --git a/assets/node_list.yml b/assets/node_list.yml index d04a9a2e8..6191129b3 100644 --- a/assets/node_list.yml +++ b/assets/node_list.yml @@ -1,6 +1,7 @@ - uri: xmr-node.cakewallet.com:18081 is_default: true + trusted: true - uri: cakexmrl7bonq7ovjka5kuwuyd3f7qnkz6z6s6dmsy3uckwra7bvggyd.onion:18081 is_default: false diff --git a/assets/text/Monerocom_Release_Notes.txt b/assets/text/Monerocom_Release_Notes.txt index 613ea4281..46f21e172 100644 --- a/assets/text/Monerocom_Release_Notes.txt +++ b/assets/text/Monerocom_Release_Notes.txt @@ -1,3 +1,3 @@ -Monero enhancements -Introducing StealthEx and LetxExchange +Add airgapped Monero wallet support (best used with our new offline app Cupcake) +New Buy & Sell flow Bug fixes \ No newline at end of file diff --git a/assets/text/Release_Notes.txt b/assets/text/Release_Notes.txt index 61aafb6e4..6764826a7 100644 --- a/assets/text/Release_Notes.txt +++ b/assets/text/Release_Notes.txt @@ -1,7 +1,5 @@ -Added Litecoin MWEB -Added wallet groups -Silent Payment enhancements for speed & reliability -Monero enhancements -Introducing StealthEx and LetxExchange -Additional ERC20 tokens scam detection +Add Litecoin Ledger support +Add airgapped Monero wallet support (best used with our new offline app Cupcake) +MWEB fixes and enhancements +New Buy & Sell flow Bug fixes \ No newline at end of file diff --git a/build-guide-win.md b/build-guide-win.md new file mode 100644 index 000000000..6ace961af --- /dev/null +++ b/build-guide-win.md @@ -0,0 +1,38 @@ +# Building CakeWallet for Windows + +## Requirements and Setup + +The following are the system requirements to build CakeWallet for your Windows PC. + +``` +Windows 10 or later (64-bit), x86-64 based +Flutter 3 or above +``` + +## Building CakeWallet on Windows + +These steps will help you configure and execute a build of CakeWallet from its source code. + +### 1. Installing Package Dependencies + +For build CakeWallet windows application from sources you will be needed to have: +> [Install Flutter]Follow installation guide (https://docs.flutter.dev/get-started/install/windows) and install do not miss to dev tools (install https://docs.flutter.dev/get-started/install/windows/desktop#development-tools) which are required for windows desktop development (need to install Git for Windows and Visual Studio 2022). Then install `Desktop development with C++` packages via GUI Visual Studio 2022, or Visual Studio Build Tools 2022 including: `C++ Build Tools core features`, `C++ 2022 Redistributable Update`, `C++ core desktop features`, `MVC v143 - VS 2022 C++ x64/x86 build tools`, `C++ CMake tools for Windwos`, `Testing tools core features - Build Tools`, `C++ AddressSanitizer`. +> [Install WSL] for building monero dependencies need to install Windows WSL (https://learn.microsoft.com/en-us/windows/wsl/install) and required packages for WSL (Ubuntu): +`$ sudo apt update ` +`$ sudo apt build-essential cmake gcc-mingw-w64 g++-mingw-w64 autoconf libtool pkg-config` + +### 2. Pull CakeWallet source code + +You can downlaod CakeWallet source code from our [GitHub repository](github.com/cake-tech/cake_wallet) via git by following next command: +`$ git clone https://github.com/cake-tech/cake_wallet.git --branch MrCyjaneK-cyjan-monerodart` +OR you can download it as [Zip archive](https://github.com/cake-tech/cake_wallet/archive/refs/heads/MrCyjaneK-cyjan-monerodart.zip) + +### 3. Build Monero, Monero_c and their dependencies + +For use monero in the application need to build Monero wrapper - Monero_C which will be used by monero.dart package. For that need to run shell (bash - typically same named utility should be available after WSL is enabled in your system) with previously installed WSL, then change current directory to the application project directory with your used shell and then change current directory to `scripts/windows`: `$ cd scripts/windows`. Run build script: `$ ./build_all.sh`. + +### 4. Configure and build CakeWallet application + +To configure the application open directory where you have downloaded or unarchived CakeWallet sources and run `cakewallet.bat`. +Or if you used WSL and have active shell session you can run `$ ./cakewallet.sh` script in `scripts/windows` which will run `cakewallet.bat` in WSL. +After execution of `cakewallet.bat` you should to get `Cake Wallet.zip` in project root directory which will contains `CakeWallet.exe` file and another needed files for run the application. Now you can extract files from `Cake Wallet.zip` archive and run the application. diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 6245aa787..e58fad00f 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -574,6 +574,8 @@ abstract class ElectrumWalletBase Future connectToNode({required Node node}) async { this.node = node; + if (syncStatus is ConnectingSyncStatus) return; + try { syncStatus = ConnectingSyncStatus(); @@ -2216,13 +2218,14 @@ abstract class ElectrumWalletBase if (syncStatus is NotConnectedSyncStatus || syncStatus is LostConnectionSyncStatus || syncStatus is ConnectingSyncStatus) { - syncStatus = AttemptingSyncStatus(); - startSync(); + syncStatus = ConnectedSyncStatus(); } break; case ConnectionStatus.disconnected: - if (syncStatus is! NotConnectedSyncStatus) { + if (syncStatus is! NotConnectedSyncStatus && + syncStatus is! ConnectingSyncStatus && + syncStatus is! SyncronizingSyncStatus) { syncStatus = NotConnectedSyncStatus(); } break; diff --git a/cw_bitcoin/lib/pending_bitcoin_transaction.dart b/cw_bitcoin/lib/pending_bitcoin_transaction.dart index 140965655..411c7de16 100644 --- a/cw_bitcoin/lib/pending_bitcoin_transaction.dart +++ b/cw_bitcoin/lib/pending_bitcoin_transaction.dart @@ -153,4 +153,9 @@ class PendingBitcoinTransaction with PendingTransaction { inputAddresses: _tx.inputs.map((input) => input.txId).toList(), outputAddresses: outputAddresses, fee: fee); + + @override + Future commitUR() { + throw UnimplementedError(); + } } diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock index 0e4921a3a..c8a83f90c 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -87,8 +87,8 @@ packages: dependency: "direct overridden" description: path: "." - ref: cake-update-v8 - resolved-ref: fc045a11db3d85d806ca67f75e8b916c706745a2 + ref: cake-update-v9 + resolved-ref: "86969a14e337383e14965f5fb45a72a63e5009bc" url: "https://github.com/cake-tech/bitcoin_base" source: git version: "4.7.0" @@ -386,10 +386,10 @@ packages: dependency: transitive description: name: flutter_web_bluetooth - sha256: "52ce64f65d7321c4bf6abfe9dac02fb888731339a5e0ad6de59fb916c20c9f02" + sha256: fcd03e2e5f82edcedcbc940f1b6a0635a50757374183254f447640886c53208e url: "https://pub.dev" source: hosted - version: "0.2.3" + version: "0.2.4" flutter_web_plugins: dependency: transitive description: flutter @@ -560,7 +560,7 @@ packages: description: path: "packages/ledger-bitcoin" ref: HEAD - resolved-ref: dbb5c4956949dc734af3fc8febdbabed89da72aa + resolved-ref: "07cd61ef76a2a017b6d5ef233396740163265457" url: "https://github.com/cake-tech/ledger-flutter-plus-plugins" source: git version: "0.0.3" @@ -577,7 +577,7 @@ packages: description: path: "packages/ledger-litecoin" ref: HEAD - resolved-ref: dbb5c4956949dc734af3fc8febdbabed89da72aa + resolved-ref: "07cd61ef76a2a017b6d5ef233396740163265457" url: "https://github.com/cake-tech/ledger-flutter-plus-plugins" source: git version: "0.0.2" diff --git a/cw_bitcoin_cash/lib/src/pending_bitcoin_cash_transaction.dart b/cw_bitcoin_cash/lib/src/pending_bitcoin_cash_transaction.dart index e1fa9d6e0..05483ce54 100644 --- a/cw_bitcoin_cash/lib/src/pending_bitcoin_cash_transaction.dart +++ b/cw_bitcoin_cash/lib/src/pending_bitcoin_cash_transaction.dart @@ -85,4 +85,8 @@ class PendingBitcoinCashTransaction with PendingTransaction { fee: fee, isReplaced: false, ); + @override + Future commitUR() { + throw UnimplementedError(); + } } diff --git a/cw_core/lib/currency_for_wallet_type.dart b/cw_core/lib/currency_for_wallet_type.dart index 630078949..af6037a3b 100644 --- a/cw_core/lib/currency_for_wallet_type.dart +++ b/cw_core/lib/currency_for_wallet_type.dart @@ -35,3 +35,34 @@ CryptoCurrency currencyForWalletType(WalletType type, {bool? isTestnet}) { 'Unexpected wallet type: ${type.toString()} for CryptoCurrency currencyForWalletType'); } } + +WalletType? walletTypeForCurrency(CryptoCurrency currency) { + switch (currency) { + case CryptoCurrency.btc: + return WalletType.bitcoin; + case CryptoCurrency.xmr: + return WalletType.monero; + case CryptoCurrency.ltc: + return WalletType.litecoin; + case CryptoCurrency.xhv: + return WalletType.haven; + case CryptoCurrency.eth: + return WalletType.ethereum; + case CryptoCurrency.bch: + return WalletType.bitcoinCash; + case CryptoCurrency.nano: + return WalletType.nano; + case CryptoCurrency.banano: + return WalletType.banano; + case CryptoCurrency.maticpoly: + return WalletType.polygon; + case CryptoCurrency.sol: + return WalletType.solana; + case CryptoCurrency.trx: + return WalletType.tron; + case CryptoCurrency.wow: + return WalletType.wownero; + default: + return null; + } +} diff --git a/cw_core/lib/hardware/device_connection_type.dart b/cw_core/lib/hardware/device_connection_type.dart index 9a3069552..466d58e2a 100644 --- a/cw_core/lib/hardware/device_connection_type.dart +++ b/cw_core/lib/hardware/device_connection_type.dart @@ -7,6 +7,7 @@ enum DeviceConnectionType { static List supportedConnectionTypes(WalletType walletType, [bool isIOS = false]) { switch (walletType) { + // case WalletType.monero: case WalletType.bitcoin: case WalletType.litecoin: case WalletType.ethereum: diff --git a/cw_core/lib/monero_wallet_keys.dart b/cw_core/lib/monero_wallet_keys.dart index 1435002a8..fe3784986 100644 --- a/cw_core/lib/monero_wallet_keys.dart +++ b/cw_core/lib/monero_wallet_keys.dart @@ -1,10 +1,12 @@ class MoneroWalletKeys { const MoneroWalletKeys( - {required this.privateSpendKey, + {required this.primaryAddress, + required this.privateSpendKey, required this.privateViewKey, required this.publicSpendKey, required this.publicViewKey}); + final String primaryAddress; final String publicViewKey; final String privateViewKey; final String publicSpendKey; diff --git a/cw_core/lib/pending_transaction.dart b/cw_core/lib/pending_transaction.dart index 0a6103a5f..78eba68a3 100644 --- a/cw_core/lib/pending_transaction.dart +++ b/cw_core/lib/pending_transaction.dart @@ -14,5 +14,8 @@ mixin PendingTransaction { int? get outputCount => null; PendingChange? change; + bool shouldCommitUR() => false; + Future commit(); + Future commitUR(); } diff --git a/cw_core/lib/wallet_service.dart b/cw_core/lib/wallet_service.dart index d90ae30bc..85126853e 100644 --- a/cw_core/lib/wallet_service.dart +++ b/cw_core/lib/wallet_service.dart @@ -61,4 +61,8 @@ abstract class WalletService false; } diff --git a/cw_evm/lib/pending_evm_chain_transaction.dart b/cw_evm/lib/pending_evm_chain_transaction.dart index 0b367da68..6861b41f8 100644 --- a/cw_evm/lib/pending_evm_chain_transaction.dart +++ b/cw_evm/lib/pending_evm_chain_transaction.dart @@ -47,4 +47,9 @@ class PendingEVMChainTransaction with PendingTransaction { return '0x${Hex.HEX.encode(txid)}'; } + + @override + Future commitUR() { + throw UnimplementedError(); + } } diff --git a/cw_haven/lib/api/exceptions/wallet_restore_from_keys_exception.dart b/cw_haven/lib/api/exceptions/wallet_restore_from_keys_exception.dart index c6b6c6ef7..3ff5f2438 100644 --- a/cw_haven/lib/api/exceptions/wallet_restore_from_keys_exception.dart +++ b/cw_haven/lib/api/exceptions/wallet_restore_from_keys_exception.dart @@ -2,4 +2,9 @@ class WalletRestoreFromKeysException implements Exception { WalletRestoreFromKeysException({required this.message}); final String message; + + @override + String toString() { + return message; + } } \ No newline at end of file diff --git a/cw_haven/lib/haven_wallet.dart b/cw_haven/lib/haven_wallet.dart index 06a838100..e2e598bbe 100644 --- a/cw_haven/lib/haven_wallet.dart +++ b/cw_haven/lib/haven_wallet.dart @@ -73,6 +73,7 @@ abstract class HavenWalletBase @override MoneroWalletKeys get keys => MoneroWalletKeys( + primaryAddress: haven_wallet.getAddress(accountIndex: 0, addressIndex: 0), privateSpendKey: haven_wallet.getSecretSpendKey(), privateViewKey: haven_wallet.getSecretViewKey(), publicSpendKey: haven_wallet.getPublicSpendKey(), diff --git a/cw_haven/lib/pending_haven_transaction.dart b/cw_haven/lib/pending_haven_transaction.dart index 1d95de08c..dbf075044 100644 --- a/cw_haven/lib/pending_haven_transaction.dart +++ b/cw_haven/lib/pending_haven_transaction.dart @@ -48,4 +48,9 @@ class PendingHavenTransaction with PendingTransaction { rethrow; } } + + @override + Future commitUR() { + throw UnimplementedError(); + } } diff --git a/cw_monero/lib/api/account_list.dart b/cw_monero/lib/api/account_list.dart index 7cb95a507..e3bb25c97 100644 --- a/cw_monero/lib/api/account_list.dart +++ b/cw_monero/lib/api/account_list.dart @@ -2,6 +2,7 @@ import 'package:cw_monero/api/wallet.dart'; import 'package:monero/monero.dart' as monero; monero.wallet? wptr = null; +bool get isViewOnly => int.tryParse(monero.Wallet_secretSpendKey(wptr!)) == 0; int _wlptrForW = 0; monero.WalletListener? _wlptr = null; diff --git a/cw_monero/lib/api/transaction_history.dart b/cw_monero/lib/api/transaction_history.dart index a308b682e..7748a16af 100644 --- a/cw_monero/lib/api/transaction_history.dart +++ b/cw_monero/lib/api/transaction_history.dart @@ -13,7 +13,13 @@ import 'package:mutex/mutex.dart'; String getTxKey(String txId) { - return monero.Wallet_getTxKey(wptr!, txid: txId); + final txKey = monero.Wallet_getTxKey(wptr!, txid: txId); + final status = monero.Wallet_status(wptr!); + if (status != 0) { + final error = monero.Wallet_errorString(wptr!); + return txId+"_"+error; + } + return txKey; } final txHistoryMutex = Mutex(); monero.TransactionHistory? txhistory; @@ -178,12 +184,13 @@ PendingTransactionDescription createTransactionMultDestSync( ); } -void commitTransactionFromPointerAddress({required int address}) => - commitTransaction(transactionPointer: monero.PendingTransaction.fromAddress(address)); +String? commitTransactionFromPointerAddress({required int address, required bool useUR}) => + commitTransaction(transactionPointer: monero.PendingTransaction.fromAddress(address), useUR: useUR); -void commitTransaction({required monero.PendingTransaction transactionPointer}) { - - final txCommit = monero.PendingTransaction_commit(transactionPointer, filename: '', overwrite: false); +String? commitTransaction({required monero.PendingTransaction transactionPointer, required bool useUR}) { + final txCommit = useUR + ? monero.PendingTransaction_commitUR(transactionPointer, 120) + : monero.PendingTransaction_commit(transactionPointer, filename: '', overwrite: false); final String? error = (() { final status = monero.PendingTransaction_status(transactionPointer.cast()); @@ -196,6 +203,11 @@ void commitTransaction({required monero.PendingTransaction transactionPointer}) if (error != null) { throw CreationTransactionException(message: error); } + if (useUR) { + return txCommit as String?; + } else { + return null; + } } Future _createTransactionSync(Map args) async { diff --git a/cw_monero/lib/api/wallet.dart b/cw_monero/lib/api/wallet.dart index 8e03cff3e..928fd7ef1 100644 --- a/cw_monero/lib/api/wallet.dart +++ b/cw_monero/lib/api/wallet.dart @@ -119,7 +119,7 @@ Future setupNodeSync( daemonUsername: login ?? '', daemonPassword: password ?? ''); }); - // monero.Wallet_init3(wptr!, argv0: '', defaultLogBaseName: 'moneroc', console: true); + // monero.Wallet_init3(wptr!, argv0: '', defaultLogBaseName: 'moneroc', console: true, logPath: ''); final status = monero.Wallet_status(wptr!); @@ -330,4 +330,4 @@ String signMessage(String message, {String address = ""}) { bool verifyMessage(String message, String address, String signature) { return monero.Wallet_verifySignedMessage(wptr!, message: message, address: address, signature: signature); -} \ No newline at end of file +} diff --git a/cw_monero/lib/api/wallet_manager.dart b/cw_monero/lib/api/wallet_manager.dart index dca7586fb..7f9dbd8fa 100644 --- a/cw_monero/lib/api/wallet_manager.dart +++ b/cw_monero/lib/api/wallet_manager.dart @@ -7,19 +7,18 @@ import 'package:cw_monero/api/exceptions/wallet_creation_exception.dart'; import 'package:cw_monero/api/exceptions/wallet_opening_exception.dart'; import 'package:cw_monero/api/exceptions/wallet_restore_from_keys_exception.dart'; import 'package:cw_monero/api/exceptions/wallet_restore_from_seed_exception.dart'; -import 'package:cw_monero/api/wallet.dart'; import 'package:cw_monero/api/transaction_history.dart'; +import 'package:cw_monero/api/wallet.dart'; +import 'package:cw_monero/ledger.dart'; import 'package:monero/monero.dart' as monero; class MoneroCException implements Exception { final String message; MoneroCException(this.message); - + @override - String toString() { - return message; - } + String toString() => message; } void checkIfMoneroCIsFine() { @@ -43,7 +42,6 @@ void checkIfMoneroCIsFine() { throw MoneroCException("monero_c and monero.dart wrapper export list mismatch.\nLogic errors can occur.\nRefusing to run in release mode.\ncpp: '$cppCsExp'\ndart: '$dartCsExp'"); } } - monero.WalletManager? _wmPtr; final monero.WalletManager wmPtr = Pointer.fromAddress((() { try { @@ -60,6 +58,13 @@ final monero.WalletManager wmPtr = Pointer.fromAddress((() { return _wmPtr!.address; })()); +void createWalletPointer() { + final newWptr = monero.WalletManager_createWallet(wmPtr, + path: "", password: "", language: "", networkType: 0); + + wptr = newWptr; +} + void createWalletSync( {required String path, required String password, @@ -124,24 +129,24 @@ void restoreWalletFromKeysSync( int restoreHeight = 0}) { txhistory = null; var newWptr = (spendKey != "") - ? monero.WalletManager_createDeterministicWalletFromSpendKey( - wmPtr, - path: path, - password: password, - language: language, - spendKeyString: spendKey, - newWallet: true, // TODO(mrcyjanek): safe to remove - restoreHeight: restoreHeight) - : monero.WalletManager_createWalletFromKeys( - wmPtr, - path: path, - password: password, - restoreHeight: restoreHeight, - addressString: address, - viewKeyString: viewKey, - spendKeyString: spendKey, - nettype: 0, - ); + ? monero.WalletManager_createDeterministicWalletFromSpendKey(wmPtr, + path: path, + password: password, + language: language, + spendKeyString: spendKey, + newWallet: true, + // TODO(mrcyjanek): safe to remove + restoreHeight: restoreHeight) + : monero.WalletManager_createWalletFromKeys( + wmPtr, + path: path, + password: password, + restoreHeight: restoreHeight, + addressString: address, + viewKeyString: viewKey, + spendKeyString: spendKey, + nettype: 0, + ); final status = monero.Wallet_status(newWptr); if (status != 0) { @@ -156,7 +161,7 @@ void restoreWalletFromKeysSync( if (viewKey != viewKeyRestored && viewKey != "") { monero.WalletManager_closeWallet(wmPtr, newWptr, false); File(path).deleteSync(); - File(path+".keys").deleteSync(); + File(path + ".keys").deleteSync(); newWptr = monero.WalletManager_createWalletFromKeys( wmPtr, path: path, @@ -199,7 +204,7 @@ void restoreWalletFromSpendKeySync( // viewKeyString: '', // nettype: 0, // ); - + txhistory = null; final newWptr = monero.WalletManager_createDeterministicWalletFromSpendKey( wmPtr, @@ -230,41 +235,39 @@ void restoreWalletFromSpendKeySync( String _lastOpenedWallet = ""; -// void restoreMoneroWalletFromDevice( -// {required String path, -// required String password, -// required String deviceName, -// int nettype = 0, -// int restoreHeight = 0}) { -// -// final pathPointer = path.toNativeUtf8(); -// final passwordPointer = password.toNativeUtf8(); -// final deviceNamePointer = deviceName.toNativeUtf8(); -// final errorMessagePointer = ''.toNativeUtf8(); -// -// final isWalletRestored = restoreWalletFromDeviceNative( -// pathPointer, -// passwordPointer, -// deviceNamePointer, -// nettype, -// restoreHeight, -// errorMessagePointer) != 0; -// -// calloc.free(pathPointer); -// calloc.free(passwordPointer); -// -// storeSync(); -// -// if (!isWalletRestored) { -// throw WalletRestoreFromKeysException( -// message: convertUTF8ToString(pointer: errorMessagePointer)); -// } -// } +Future restoreWalletFromHardwareWallet( + {required String path, + required String password, + required String deviceName, + int nettype = 0, + int restoreHeight = 0}) async { + txhistory = null; + + final newWptrAddr = await Isolate.run(() { + return monero.WalletManager_createWalletFromDevice(wmPtr, + path: path, + password: password, + restoreHeight: restoreHeight, + deviceName: deviceName) + .address; + }); + final newWptr = Pointer.fromAddress(newWptrAddr); + + final status = monero.Wallet_status(newWptr); + + if (status != 0) { + final error = monero.Wallet_errorString(newWptr); + throw WalletRestoreFromSeedException(message: error); + } + wptr = newWptr; + + openedWalletsByPath[path] = wptr!; +} Map openedWalletsByPath = {}; -void loadWallet( - {required String path, required String password, int nettype = 0}) { +Future loadWallet( + {required String path, required String password, int nettype = 0}) async { if (openedWalletsByPath[path] != null) { txhistory = null; wptr = openedWalletsByPath[path]!; @@ -278,8 +281,29 @@ void loadWallet( }); } txhistory = null; - final newWptr = monero.WalletManager_openWallet(wmPtr, - path: path, password: password); + + /// Get the device type + /// 0: Software Wallet + /// 1: Ledger + /// 2: Trezor + final deviceType = monero.WalletManager_queryWalletDevice(wmPtr, + keysFileName: "$path.keys", password: password, kdfRounds: 1); + + if (deviceType == 1) { + final dummyWPtr = wptr ?? + monero.WalletManager_openWallet(wmPtr, path: '', password: ''); + enableLedgerExchange(dummyWPtr, gLedger!); + } + + final addr = wmPtr.address; + final newWptrAddr = await Isolate.run(() { + return monero.WalletManager_openWallet(Pointer.fromAddress(addr), + path: path, password: password) + .address; + }); + + final newWptr = Pointer.fromAddress(newWptrAddr); + _lastOpenedWallet = path; final status = monero.Wallet_status(newWptr); if (status != 0) { @@ -287,6 +311,7 @@ void loadWallet( print(err); throw WalletOpeningException(message: err); } + wptr = newWptr; openedWalletsByPath[path] = wptr!; } @@ -351,7 +376,7 @@ Future _openWallet(Map args) async => loadWallet( bool _isWalletExist(String path) => isWalletExistSync(path: path); -void openWallet( +Future openWallet( {required String path, required String password, int nettype = 0}) async => @@ -425,3 +450,5 @@ Future restoreFromSpendKey( }); bool isWalletExist({required String path}) => _isWalletExist(path); + +bool isViewOnlyBySpendKey() => int.tryParse(monero.Wallet_secretSpendKey(wptr!)) == 0; \ No newline at end of file diff --git a/cw_monero/lib/ledger.dart b/cw_monero/lib/ledger.dart new file mode 100644 index 000000000..c947d0944 --- /dev/null +++ b/cw_monero/lib/ledger.dart @@ -0,0 +1,84 @@ +import 'dart:async'; +import 'dart:ffi'; +import 'dart:typed_data'; + +import 'package:ffi/ffi.dart'; +import 'package:ledger_flutter_plus/ledger_flutter_plus.dart'; +import 'package:ledger_flutter_plus/ledger_flutter_plus_dart.dart'; +import 'package:monero/monero.dart' as monero; +// import 'package:polyseed/polyseed.dart'; + +LedgerConnection? gLedger; + +Timer? _ledgerExchangeTimer; +Timer? _ledgerKeepAlive; + +void enableLedgerExchange(monero.wallet ptr, LedgerConnection connection) { + _ledgerExchangeTimer?.cancel(); + _ledgerExchangeTimer = Timer.periodic(Duration(milliseconds: 1), (_) async { + final ledgerRequestLength = monero.Wallet_getSendToDeviceLength(ptr); + final ledgerRequest = monero.Wallet_getSendToDevice(ptr) + .cast() + .asTypedList(ledgerRequestLength); + if (ledgerRequestLength > 0) { + _ledgerKeepAlive?.cancel(); + + final Pointer emptyPointer = malloc(0); + monero.Wallet_setDeviceSendData( + ptr, emptyPointer.cast(), 0); + malloc.free(emptyPointer); + + // print("> ${ledgerRequest.toHexString()}"); + final response = await exchange(connection, ledgerRequest); + // print("< ${response.toHexString()}"); + + final Pointer result = malloc(response.length); + for (var i = 0; i < response.length; i++) { + result.asTypedList(response.length)[i] = response[i]; + } + + monero.Wallet_setDeviceReceivedData( + ptr, result.cast(), response.length); + malloc.free(result); + keepAlive(connection); + } + }); +} + +void keepAlive(LedgerConnection connection) { + if (connection.connectionType == ConnectionType.ble) { + _ledgerKeepAlive = Timer.periodic(Duration(seconds: 10), (_) async { + try { + UniversalBle.setNotifiable( + connection.device.id, + connection.device.deviceInfo.serviceId, + connection.device.deviceInfo.notifyCharacteristicKey, + BleInputProperty.notification, + ); + } catch (_) {} + }); + } +} + +void disableLedgerExchange() { + _ledgerExchangeTimer?.cancel(); + _ledgerKeepAlive?.cancel(); + gLedger?.disconnect(); + gLedger = null; +} + +Future exchange(LedgerConnection connection, Uint8List data) async => + connection.sendOperation(ExchangeOperation(data)); + +class ExchangeOperation extends LedgerRawOperation { + final Uint8List inputData; + + ExchangeOperation(this.inputData); + + @override + Future read(ByteDataReader reader) async => + reader.read(reader.remainingLength); + + @override + Future> write(ByteDataWriter writer) async => [inputData]; +} diff --git a/cw_monero/lib/monero_wallet.dart b/cw_monero/lib/monero_wallet.dart index 0ae2202ba..5f53b30ba 100644 --- a/cw_monero/lib/monero_wallet.dart +++ b/cw_monero/lib/monero_wallet.dart @@ -19,6 +19,7 @@ import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_info.dart'; +import 'package:cw_monero/api/account_list.dart'; import 'package:cw_monero/api/coins_info.dart'; import 'package:cw_monero/api/monero_output.dart'; import 'package:cw_monero/api/structs/pending_transaction.dart'; @@ -27,6 +28,7 @@ import 'package:cw_monero/api/wallet.dart' as monero_wallet; import 'package:cw_monero/api/wallet_manager.dart'; import 'package:cw_monero/exceptions/monero_transaction_creation_exception.dart'; import 'package:cw_monero/exceptions/monero_transaction_no_inputs_exception.dart'; +import 'package:cw_monero/ledger.dart'; import 'package:cw_monero/monero_transaction_creation_credentials.dart'; import 'package:cw_monero/monero_transaction_history.dart'; import 'package:cw_monero/monero_transaction_info.dart'; @@ -35,6 +37,7 @@ import 'package:cw_monero/monero_wallet_addresses.dart'; import 'package:cw_monero/pending_monero_transaction.dart'; import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; +import 'package:ledger_flutter_plus/ledger_flutter_plus.dart'; import 'package:mobx/mobx.dart'; import 'package:monero/monero.dart' as monero; @@ -121,6 +124,7 @@ abstract class MoneroWalletBase extends WalletBase MoneroWalletKeys( + primaryAddress: monero_wallet.getAddress(accountIndex: 0, addressIndex: 0), privateSpendKey: monero_wallet.getSecretSpendKey(), privateViewKey: monero_wallet.getSecretViewKey(), publicSpendKey: monero_wallet.getPublicSpendKey(), @@ -230,6 +234,36 @@ abstract class MoneroWalletBase extends WalletBase submitTransactionUR(String ur) async { + final retStatus = monero.Wallet_submitTransactionUR(wptr!, ur); + final status = monero.Wallet_status(wptr!); + if (status != 0) { + final err = monero.Wallet_errorString(wptr!); + throw MoneroTransactionCreationException("unable to broadcast signed transaction: $err"); + } + return retStatus; + } + + bool importKeyImagesUR(String ur) { + final retStatus = monero.Wallet_importKeyImagesUR(wptr!, ur); + final status = monero.Wallet_status(wptr!); + if (status != 0) { + final err = monero.Wallet_errorString(wptr!); + throw Exception("unable to import key images: $err"); + } + return retStatus; + } + + String exportOutputsUR(bool all) { + final str = monero.Wallet_exportOutputsUR(wptr!, all: all); + final status = monero.Wallet_status(wptr!); + if (status != 0) { + final err = monero.Wallet_errorString(wptr!); + throw MoneroTransactionCreationException("unable to export UR: $err"); + } + return str; + } + @override Future createTransaction(Object credentials) async { final _credentials = credentials as MoneroTransactionCreationCredentials; @@ -796,4 +830,9 @@ abstract class MoneroWalletBase extends WalletBase { + MoneroRestoreWalletFromHardwareCredentials> { MoneroWalletService(this.walletInfoSource, this.unspentCoinsInfoSource); final Box walletInfoSource; @@ -81,7 +92,7 @@ class MoneroWalletService extends WalletService< final lang = PolyseedLang.getByEnglishName(credentials.language); final heightOverride = - getMoneroHeigthByDate(date: DateTime.now().subtract(Duration(days: 2))); + getMoneroHeigthByDate(date: DateTime.now().subtract(Duration(days: 2))); return _restoreFromPolyseed( path, credentials.password!, polyseed, credentials.walletInfo!, lang, @@ -91,9 +102,9 @@ class MoneroWalletService extends WalletService< await monero_wallet_manager.createWallet( path: path, password: credentials.password!, language: credentials.language); final wallet = MoneroWallet( - walletInfo: credentials.walletInfo!, - unspentCoinsInfo: unspentCoinsInfoSource, - password: credentials.password!); + walletInfo: credentials.walletInfo!, + unspentCoinsInfo: unspentCoinsInfoSource, + password: credentials.password!); await wallet.init(); return wallet; @@ -128,13 +139,18 @@ class MoneroWalletService extends WalletService< await monero_wallet_manager .openWalletAsync({'path': path, 'password': password}); final walletInfo = walletInfoSource.values.firstWhere( - (info) => info.id == WalletBase.idFor(name, getType())); + (info) => info.id == WalletBase.idFor(name, getType())); final wallet = MoneroWallet( - walletInfo: walletInfo, - unspentCoinsInfo: unspentCoinsInfoSource, - password: password); + walletInfo: walletInfo, + unspentCoinsInfo: unspentCoinsInfoSource, + password: password); final isValid = wallet.walletAddresses.validate(); + if (wallet.isHardwareWallet) { + wallet.setLedgerConnection(gLedger!); + gLedger = null; + } + if (!isValid) { await restoreOrResetWalletFiles(name); wallet.close(shouldCleanup: false); @@ -185,10 +201,9 @@ class MoneroWalletService extends WalletService< } @override - Future rename( - String currentName, String password, String newName) async { + Future rename(String currentName, String password, String newName) async { final currentWalletInfo = walletInfoSource.values.firstWhere( - (info) => info.id == WalletBase.idFor(currentName, getType())); + (info) => info.id == WalletBase.idFor(currentName, getType())); final currentWallet = MoneroWallet( walletInfo: currentWalletInfo, unspentCoinsInfo: unspentCoinsInfoSource, @@ -218,9 +233,9 @@ class MoneroWalletService extends WalletService< viewKey: credentials.viewKey, spendKey: credentials.spendKey); final wallet = MoneroWallet( - walletInfo: credentials.walletInfo!, - unspentCoinsInfo: unspentCoinsInfoSource, - password: credentials.password!); + walletInfo: credentials.walletInfo!, + unspentCoinsInfo: unspentCoinsInfoSource, + password: credentials.password!); await wallet.init(); return wallet; @@ -232,9 +247,34 @@ class MoneroWalletService extends WalletService< } @override - Future restoreFromHardwareWallet(MoneroNewWalletCredentials credentials) { - throw UnimplementedError( - "Restoring a Monero wallet from a hardware wallet is not yet supported!"); + Future restoreFromHardwareWallet( + MoneroRestoreWalletFromHardwareCredentials credentials) async { + try { + final path = await pathForWallet(name: credentials.name, type: getType()); + final password = credentials.password; + final height = credentials.height; + + if (wptr == null ) monero_wallet_manager.createWalletPointer(); + + enableLedgerExchange(wptr!, credentials.ledgerConnection); + await monero_wallet_manager.restoreWalletFromHardwareWallet( + path: path, + password: password!, + restoreHeight: height!, + deviceName: 'Ledger'); + + final wallet = MoneroWallet( + walletInfo: credentials.walletInfo!, + unspentCoinsInfo: unspentCoinsInfoSource, + password: credentials.password!); + await wallet.init(); + + return wallet; + } catch (e) { + // TODO: Implement Exception for wallet list service. + print('MoneroWalletsManager Error: $e'); + rethrow; + } } @override @@ -253,9 +293,9 @@ class MoneroWalletService extends WalletService< seed: credentials.mnemonic, restoreHeight: credentials.height!); final wallet = MoneroWallet( - walletInfo: credentials.walletInfo!, - unspentCoinsInfo: unspentCoinsInfoSource, - password: credentials.password!); + walletInfo: credentials.walletInfo!, + unspentCoinsInfo: unspentCoinsInfoSource, + password: credentials.password!); await wallet.init(); return wallet; @@ -283,8 +323,8 @@ class MoneroWalletService extends WalletService< } } - Future _restoreFromPolyseed( - String path, String password, Polyseed polyseed, WalletInfo walletInfo, PolyseedLang lang, + Future _restoreFromPolyseed(String path, String password, Polyseed polyseed, + WalletInfo walletInfo, PolyseedLang lang, {PolyseedCoin coin = PolyseedCoin.POLYSEED_MONERO, int? overrideHeight}) async { final height = overrideHeight ?? getMoneroHeigthByDate(date: DateTime.fromMillisecondsSinceEpoch(polyseed.birthday * 1000)); @@ -329,7 +369,9 @@ class MoneroWalletService extends WalletService< dir.listSync().forEach((f) { final file = File(f.path); - final name = f.path.split('/').last; + final name = f.path + .split('/') + .last; final newPath = newWalletDirPath + '/$name'; final newFile = File(newPath); @@ -366,4 +408,11 @@ class MoneroWalletService extends WalletService< return ''; } } + + @override + bool requireHardwareWalletConnection(String name) { + final walletInfo = walletInfoSource.values + .firstWhere((info) => info.id == WalletBase.idFor(name, getType())); + return walletInfo.isHardwareWallet; + } } diff --git a/cw_monero/lib/pending_monero_transaction.dart b/cw_monero/lib/pending_monero_transaction.dart index 2a75fc26b..eb714eeb3 100644 --- a/cw_monero/lib/pending_monero_transaction.dart +++ b/cw_monero/lib/pending_monero_transaction.dart @@ -1,3 +1,4 @@ +import 'package:cw_monero/api/account_list.dart'; import 'package:cw_monero/api/structs/pending_transaction.dart'; import 'package:cw_monero/api/transaction_history.dart' as monero_transaction_history; @@ -35,11 +36,32 @@ class PendingMoneroTransaction with PendingTransaction { String get feeFormatted => AmountConverter.amountIntToString( CryptoCurrency.xmr, pendingTransactionDescription.fee); + bool shouldCommitUR() => isViewOnly; + @override Future commit() async { try { monero_transaction_history.commitTransactionFromPointerAddress( - address: pendingTransactionDescription.pointerAddress); + address: pendingTransactionDescription.pointerAddress, + useUR: false); + } catch (e) { + final message = e.toString(); + + if (message.contains('Reason: double spend')) { + throw DoubleSpendException(); + } + + rethrow; + } + } + + @override + Future commitUR() async { + try { + final ret = monero_transaction_history.commitTransactionFromPointerAddress( + address: pendingTransactionDescription.pointerAddress, + useUR: true); + return ret; } catch (e) { final message = e.toString(); diff --git a/cw_monero/pubspec.lock b/cw_monero/pubspec.lock index ee1d48df1..31d44ac63 100644 --- a/cw_monero/pubspec.lock +++ b/cw_monero/pubspec.lock @@ -29,10 +29,10 @@ packages: dependency: transitive description: name: asn1lib - sha256: "58082b3f0dca697204dbab0ef9ff208bfaea7767ea771076af9a343488428dda" + sha256: "6b151826fcc95ff246cd219a0bf4c753ea14f4081ad71c61939becf3aba27f70" url: "https://pub.dev" source: hosted - version: "1.5.3" + version: "1.5.5" async: dependency: transitive description: @@ -41,6 +41,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.11.0" + bluez: + dependency: transitive + description: + name: bluez + sha256: "203a1924e818a9dd74af2b2c7a8f375ab8e5edf0e486bba8f90a0d8a17ed9fce" + url: "https://pub.dev" + source: hosted + version: "0.8.2" boolean_selector: dependency: transitive description: @@ -209,6 +217,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.4" + dbus: + dependency: transitive + description: + name: dbus + sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac" + url: "https://pub.dev" + source: hosted + version: "0.7.10" encrypt: dependency: "direct main" description: @@ -229,10 +245,10 @@ packages: dependency: "direct main" description: name: ffi - sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" + sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" file: dependency: transitive description: @@ -267,6 +283,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_web_bluetooth: + dependency: transitive + description: + name: flutter_web_bluetooth + sha256: "52ce64f65d7321c4bf6abfe9dac02fb888731339a5e0ad6de59fb916c20c9f02" + url: "https://pub.dev" + source: hosted + version: "0.2.3" frontend_server_client: dependency: transitive description: @@ -295,18 +319,18 @@ packages: dependency: transitive description: name: hashlib - sha256: d41795742c10947930630118c6836608deeb9047cd05aee32d2baeb697afd66a + sha256: f572f2abce09fc7aee53f15927052b9732ea1053e540af8cae211111ee0b99b1 url: "https://pub.dev" source: hosted - version: "1.19.2" + version: "1.21.0" hashlib_codecs: dependency: transitive description: name: hashlib_codecs - sha256: "2b570061f5a4b378425be28a576c1e11783450355ad4345a19f606ff3d96db0f" + sha256: "8cea9ccafcfeaa7324d2ae52c61c69f7ff71f4237507a018caab31b9e416e3b1" url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "2.6.0" hive: dependency: transitive description: @@ -327,10 +351,10 @@ packages: dependency: "direct main" description: name: http - sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938" + sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" http_multi_server: dependency: transitive description: @@ -403,6 +427,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.1" + ledger_flutter_plus: + dependency: "direct main" + description: + name: ledger_flutter_plus + sha256: c7b04008553193dbca7e17b430768eecc372a72b0ff3625b5e7fc5e5c8d3231b + url: "https://pub.dev" + source: hosted + version: "1.4.1" + ledger_usb_plus: + dependency: transitive + description: + name: ledger_usb_plus + sha256: "21cc5d976cf7edb3518bd2a0c4164139cbb0817d2e4f2054707fc4edfdf9ce87" + url: "https://pub.dev" + source: hosted + version: "1.0.4" logging: dependency: transitive description: @@ -439,10 +479,10 @@ packages: dependency: transitive description: name: mime - sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" + sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a" url: "https://pub.dev" source: hosted - version: "1.0.5" + version: "1.0.6" mobx: dependency: "direct main" description: @@ -463,8 +503,8 @@ packages: dependency: "direct main" description: path: "impls/monero.dart" - ref: "6eb571ea498ed7b854934785f00fabfd0dadf75b" - resolved-ref: "6eb571ea498ed7b854934785f00fabfd0dadf75b" + ref: d72c15f4339791a7bbdf17e9d827b7b56ca144e4 + resolved-ref: d72c15f4339791a7bbdf17e9d827b7b56ca144e4 url: "https://github.com/mrcyjanek/monero_c" source: git version: "0.0.0" @@ -504,10 +544,10 @@ packages: dependency: "direct main" description: name: path_provider - sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161 + sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378 url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.4" path_provider_android: dependency: transitive description: @@ -544,10 +584,18 @@ packages: dependency: transitive description: name: path_provider_windows - sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.3.0" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 + url: "https://pub.dev" + source: hosted + version: "6.0.2" platform: dependency: transitive description: @@ -612,6 +660,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + url: "https://pub.dev" + source: hosted + version: "0.28.0" shelf: dependency: transitive description: @@ -737,6 +793,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.2" + universal_ble: + dependency: transitive + description: + name: universal_ble + sha256: "0dfbd6b64bff3ad61ed7a895c232530d9614e9b01ab261a74433a43267edb7f3" + url: "https://pub.dev" + source: hosted + version: "0.12.0" + universal_platform: + dependency: transitive + description: + name: universal_platform + sha256: "64e16458a0ea9b99260ceb5467a214c1f298d647c659af1bff6d3bf82536b1ec" + url: "https://pub.dev" + source: hosted + version: "1.1.0" unorm_dart: dependency: transitive description: @@ -785,22 +857,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.5" - win32: - dependency: transitive - description: - name: win32 - sha256: "0eaf06e3446824099858367950a813472af675116bf63f008a4c2a75ae13e9cb" - url: "https://pub.dev" - source: hosted - version: "5.5.0" xdg_directories: dependency: transitive description: name: xdg_directories - sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 + url: "https://pub.dev" + source: hosted + version: "6.5.0" yaml: dependency: transitive description: @@ -811,4 +883,4 @@ packages: version: "3.1.2" sdks: dart: ">=3.3.0 <4.0.0" - flutter: ">=3.16.6" + flutter: ">=3.19.0" diff --git a/cw_monero/pubspec.yaml b/cw_monero/pubspec.yaml index cb1f5519f..71cbfa243 100644 --- a/cw_monero/pubspec.yaml +++ b/cw_monero/pubspec.yaml @@ -25,9 +25,11 @@ dependencies: monero: git: url: https://github.com/mrcyjanek/monero_c - ref: 6eb571ea498ed7b854934785f00fabfd0dadf75b # monero_c hash + ref: d72c15f4339791a7bbdf17e9d827b7b56ca144e4 +# ref: 6eb571ea498ed7b854934785f00fabfd0dadf75b # monero_c hash path: impls/monero.dart mutex: ^3.1.0 + ledger_flutter_plus: ^1.4.1 dev_dependencies: flutter_test: diff --git a/cw_nano/lib/pending_nano_transaction.dart b/cw_nano/lib/pending_nano_transaction.dart index a027100fd..51a4ef6c1 100644 --- a/cw_nano/lib/pending_nano_transaction.dart +++ b/cw_nano/lib/pending_nano_transaction.dart @@ -37,4 +37,9 @@ class PendingNanoTransaction with PendingTransaction { await nanoClient.processBlock(block, "send"); } } + + @override + Future commitUR() { + throw UnimplementedError(); + } } diff --git a/cw_shared_external/pubspec.lock b/cw_shared_external/pubspec.lock new file mode 100644 index 000000000..203ecd5c3 --- /dev/null +++ b/cw_shared_external/pubspec.lock @@ -0,0 +1,189 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" + source: hosted + version: "2.11.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + characters: + dependency: transitive + description: + name: characters + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.dev" + source: hosted + version: "1.3.0" + clock: + dependency: transitive + description: + name: clock + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" + source: hosted + version: "1.1.1" + collection: + dependency: transitive + description: + name: collection + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + url: "https://pub.dev" + source: hosted + version: "1.18.0" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" + source: hosted + version: "1.3.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" + url: "https://pub.dev" + source: hosted + version: "10.0.4" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" + url: "https://pub.dev" + source: hosted + version: "3.0.3" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + matcher: + dependency: transitive + description: + name: matcher + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + url: "https://pub.dev" + source: hosted + version: "0.12.16+1" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + url: "https://pub.dev" + source: hosted + version: "0.8.0" + meta: + dependency: transitive + description: + name: meta + sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" + url: "https://pub.dev" + source: hosted + version: "1.12.0" + path: + dependency: transitive + description: + name: path + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + url: "https://pub.dev" + source: hosted + version: "1.9.0" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_span: + dependency: transitive + description: + name: source_span + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.dev" + source: hosted + version: "1.10.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + url: "https://pub.dev" + source: hosted + version: "1.11.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + url: "https://pub.dev" + source: hosted + version: "2.1.2" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + test_api: + dependency: transitive + description: + name: test_api + sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" + url: "https://pub.dev" + source: hosted + version: "0.7.0" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" + url: "https://pub.dev" + source: hosted + version: "14.2.1" +sdks: + dart: ">=3.3.0 <4.0.0" + flutter: ">=3.18.0-18.0.pre.54" diff --git a/cw_solana/lib/pending_solana_transaction.dart b/cw_solana/lib/pending_solana_transaction.dart index 38347ed13..e01446000 100644 --- a/cw_solana/lib/pending_solana_transaction.dart +++ b/cw_solana/lib/pending_solana_transaction.dart @@ -40,4 +40,9 @@ class PendingSolanaTransaction with PendingTransaction { @override String get id => ''; + + @override + Future commitUR() { + throw UnimplementedError(); + } } diff --git a/cw_tron/lib/pending_tron_transaction.dart b/cw_tron/lib/pending_tron_transaction.dart index b6d064b31..2420f083b 100644 --- a/cw_tron/lib/pending_tron_transaction.dart +++ b/cw_tron/lib/pending_tron_transaction.dart @@ -30,4 +30,9 @@ class PendingTronTransaction with PendingTransaction { @override String get id => ''; + + @override + Future commitUR() { + throw UnimplementedError(); + } } diff --git a/cw_wownero/lib/api/exceptions/connection_to_node_exception.dart b/cw_wownero/lib/api/exceptions/connection_to_node_exception.dart new file mode 100644 index 000000000..483b0a174 --- /dev/null +++ b/cw_wownero/lib/api/exceptions/connection_to_node_exception.dart @@ -0,0 +1,5 @@ +class ConnectionToNodeException implements Exception { + ConnectionToNodeException({required this.message}); + + final String message; +} \ No newline at end of file diff --git a/cw_wownero/lib/api/exceptions/wallet_restore_from_keys_exception.dart b/cw_wownero/lib/api/exceptions/wallet_restore_from_keys_exception.dart index ad576faa2..6c461ee4c 100644 --- a/cw_wownero/lib/api/exceptions/wallet_restore_from_keys_exception.dart +++ b/cw_wownero/lib/api/exceptions/wallet_restore_from_keys_exception.dart @@ -3,5 +3,6 @@ class WalletRestoreFromKeysException implements Exception { final String message; + @override String toString() => message; } \ No newline at end of file diff --git a/cw_wownero/lib/api/structs/account_row.dart b/cw_wownero/lib/api/structs/account_row.dart new file mode 100644 index 000000000..aa492ee0f --- /dev/null +++ b/cw_wownero/lib/api/structs/account_row.dart @@ -0,0 +1,12 @@ +import 'dart:ffi'; +import 'package:ffi/ffi.dart'; + +class AccountRow extends Struct { + @Int64() + external int id; + + external Pointer label; + + String getLabel() => label.toDartString(); + int getId() => id; +} diff --git a/cw_wownero/lib/api/structs/coins_info_row.dart b/cw_wownero/lib/api/structs/coins_info_row.dart new file mode 100644 index 000000000..ff6f6ce73 --- /dev/null +++ b/cw_wownero/lib/api/structs/coins_info_row.dart @@ -0,0 +1,73 @@ +import 'dart:ffi'; +import 'package:ffi/ffi.dart'; + +class CoinsInfoRow extends Struct { + @Int64() + external int blockHeight; + + external Pointer hash; + + @Uint64() + external int internalOutputIndex; + + @Uint64() + external int globalOutputIndex; + + @Int8() + external int spent; + + @Int8() + external int frozen; + + @Uint64() + external int spentHeight; + + @Uint64() + external int amount; + + @Int8() + external int rct; + + @Int8() + external int keyImageKnown; + + @Uint64() + external int pkIndex; + + @Uint32() + external int subaddrIndex; + + @Uint32() + external int subaddrAccount; + + external Pointer address; + + external Pointer addressLabel; + + external Pointer keyImage; + + @Uint64() + external int unlockTime; + + @Int8() + external int unlocked; + + external Pointer pubKey; + + @Int8() + external int coinbase; + + external Pointer description; + + String getHash() => hash.toDartString(); + + String getAddress() => address.toDartString(); + + String getAddressLabel() => addressLabel.toDartString(); + + String getKeyImage() => keyImage.toDartString(); + + String getPubKey() => pubKey.toDartString(); + + String getDescription() => description.toDartString(); +} diff --git a/cw_wownero/lib/api/structs/subaddress_row.dart b/cw_wownero/lib/api/structs/subaddress_row.dart new file mode 100644 index 000000000..d593a793d --- /dev/null +++ b/cw_wownero/lib/api/structs/subaddress_row.dart @@ -0,0 +1,15 @@ +import 'dart:ffi'; +import 'package:ffi/ffi.dart'; + +class SubaddressRow extends Struct { + @Int64() + external int id; + + external Pointer address; + + external Pointer label; + + String getLabel() => label.toDartString(); + String getAddress() => address.toDartString(); + int getId() => id; +} \ No newline at end of file diff --git a/cw_wownero/lib/api/structs/transaction_info_row.dart b/cw_wownero/lib/api/structs/transaction_info_row.dart new file mode 100644 index 000000000..bdcc64d3f --- /dev/null +++ b/cw_wownero/lib/api/structs/transaction_info_row.dart @@ -0,0 +1,41 @@ +import 'dart:ffi'; +import 'package:ffi/ffi.dart'; + +class TransactionInfoRow extends Struct { + @Uint64() + external int amount; + + @Uint64() + external int fee; + + @Uint64() + external int blockHeight; + + @Uint64() + external int confirmations; + + @Uint32() + external int subaddrAccount; + + @Int8() + external int direction; + + @Int8() + external int isPending; + + @Uint32() + external int subaddrIndex; + + external Pointer hash; + + external Pointer paymentId; + + @Int64() + external int datetime; + + int getDatetime() => datetime; + int getAmount() => amount >= 0 ? amount : amount * -1; + bool getIsPending() => isPending != 0; + String getHash() => hash.toDartString(); + String getPaymentId() => paymentId.toDartString(); +} diff --git a/cw_wownero/lib/api/structs/ut8_box.dart b/cw_wownero/lib/api/structs/ut8_box.dart new file mode 100644 index 000000000..53e678c88 --- /dev/null +++ b/cw_wownero/lib/api/structs/ut8_box.dart @@ -0,0 +1,8 @@ +import 'dart:ffi'; +import 'package:ffi/ffi.dart'; + +class Utf8Box extends Struct { + external Pointer value; + + String getValue() => value.toDartString(); +} diff --git a/cw_wownero/lib/cw_wownero.dart b/cw_wownero/lib/cw_wownero.dart new file mode 100644 index 000000000..33a55e305 --- /dev/null +++ b/cw_wownero/lib/cw_wownero.dart @@ -0,0 +1,8 @@ + +import 'cw_wownero_platform_interface.dart'; + +class CwWownero { + Future getPlatformVersion() { + return CwWowneroPlatform.instance.getPlatformVersion(); + } +} diff --git a/cw_wownero/lib/cw_wownero_method_channel.dart b/cw_wownero/lib/cw_wownero_method_channel.dart new file mode 100644 index 000000000..d797f5f81 --- /dev/null +++ b/cw_wownero/lib/cw_wownero_method_channel.dart @@ -0,0 +1,17 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; + +import 'cw_wownero_platform_interface.dart'; + +/// An implementation of [CwWowneroPlatform] that uses method channels. +class MethodChannelCwWownero extends CwWowneroPlatform { + /// The method channel used to interact with the native platform. + @visibleForTesting + final methodChannel = const MethodChannel('cw_wownero'); + + @override + Future getPlatformVersion() async { + final version = await methodChannel.invokeMethod('getPlatformVersion'); + return version; + } +} diff --git a/cw_wownero/lib/cw_wownero_platform_interface.dart b/cw_wownero/lib/cw_wownero_platform_interface.dart new file mode 100644 index 000000000..78b21592c --- /dev/null +++ b/cw_wownero/lib/cw_wownero_platform_interface.dart @@ -0,0 +1,29 @@ +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +import 'cw_wownero_method_channel.dart'; + +abstract class CwWowneroPlatform extends PlatformInterface { + /// Constructs a CwWowneroPlatform. + CwWowneroPlatform() : super(token: _token); + + static final Object _token = Object(); + + static CwWowneroPlatform _instance = MethodChannelCwWownero(); + + /// The default instance of [CwWowneroPlatform] to use. + /// + /// Defaults to [MethodChannelCwWownero]. + static CwWowneroPlatform get instance => _instance; + + /// Platform-specific implementations should set this with their own + /// platform-specific class that extends [CwWowneroPlatform] when + /// they register themselves. + static set instance(CwWowneroPlatform instance) { + PlatformInterface.verifyToken(instance, _token); + _instance = instance; + } + + Future getPlatformVersion() { + throw UnimplementedError('platformVersion() has not been implemented.'); + } +} diff --git a/cw_wownero/lib/mywownero.dart b/cw_wownero/lib/mywownero.dart new file mode 100644 index 000000000..d50e48b64 --- /dev/null +++ b/cw_wownero/lib/mywownero.dart @@ -0,0 +1,1689 @@ +const prefixLength = 3; + +String swapEndianBytes(String original) { + if (original.length != 8) { + return ''; + } + + return original[6] + + original[7] + + original[4] + + original[5] + + original[2] + + original[3] + + original[0] + + original[1]; +} + +List tructWords(List wordSet) { + final start = 0; + final end = prefixLength; + + return wordSet.map((word) => word.substring(start, end)).toList(); +} + +String mnemonicDecode(String seed) { + final n = englistWordSet.length; + var out = ''; + var wlist = seed.split(' '); + wlist.removeLast(); + + for (var i = 0; i < wlist.length; i += 3) { + final w1 = + tructWords(englistWordSet).indexOf(wlist[i].substring(0, prefixLength)); + final w2 = tructWords(englistWordSet) + .indexOf(wlist[i + 1].substring(0, prefixLength)); + final w3 = tructWords(englistWordSet) + .indexOf(wlist[i + 2].substring(0, prefixLength)); + + if (w1 == -1 || w2 == -1 || w3 == -1) { + print("invalid word in mnemonic"); + return ''; + } + + final x = w1 + n * (((n - w1) + w2) % n) + n * n * (((n - w2) + w3) % n); + + if (x % n != w1) { + print("Something went wrong when decoding your private key, please try again"); + return ''; + } + + final _res = '0000000' + x.toRadixString(16); + final start = _res.length - 8; + final end = _res.length; + final res = _res.substring(start, end); + + out += swapEndianBytes(res); + } + + return out; +} + +final englistWordSet = [ + "abbey", + "abducts", + "ability", + "ablaze", + "abnormal", + "abort", + "abrasive", + "absorb", + "abyss", + "academy", + "aces", + "aching", + "acidic", + "acoustic", + "acquire", + "across", + "actress", + "acumen", + "adapt", + "addicted", + "adept", + "adhesive", + "adjust", + "adopt", + "adrenalin", + "adult", + "adventure", + "aerial", + "afar", + "affair", + "afield", + "afloat", + "afoot", + "afraid", + "after", + "against", + "agenda", + "aggravate", + "agile", + "aglow", + "agnostic", + "agony", + "agreed", + "ahead", + "aided", + "ailments", + "aimless", + "airport", + "aisle", + "ajar", + "akin", + "alarms", + "album", + "alchemy", + "alerts", + "algebra", + "alkaline", + "alley", + "almost", + "aloof", + "alpine", + "already", + "also", + "altitude", + "alumni", + "always", + "amaze", + "ambush", + "amended", + "amidst", + "ammo", + "amnesty", + "among", + "amply", + "amused", + "anchor", + "android", + "anecdote", + "angled", + "ankle", + "annoyed", + "answers", + "antics", + "anvil", + "anxiety", + "anybody", + "apart", + "apex", + "aphid", + "aplomb", + "apology", + "apply", + "apricot", + "aptitude", + "aquarium", + "arbitrary", + "archer", + "ardent", + "arena", + "argue", + "arises", + "army", + "around", + "arrow", + "arsenic", + "artistic", + "ascend", + "ashtray", + "aside", + "asked", + "asleep", + "aspire", + "assorted", + "asylum", + "athlete", + "atlas", + "atom", + "atrium", + "attire", + "auburn", + "auctions", + "audio", + "august", + "aunt", + "austere", + "autumn", + "avatar", + "avidly", + "avoid", + "awakened", + "awesome", + "awful", + "awkward", + "awning", + "awoken", + "axes", + "axis", + "axle", + "aztec", + "azure", + "baby", + "bacon", + "badge", + "baffles", + "bagpipe", + "bailed", + "bakery", + "balding", + "bamboo", + "banjo", + "baptism", + "basin", + "batch", + "bawled", + "bays", + "because", + "beer", + "befit", + "begun", + "behind", + "being", + "below", + "bemused", + "benches", + "berries", + "bested", + "betting", + "bevel", + "beware", + "beyond", + "bias", + "bicycle", + "bids", + "bifocals", + "biggest", + "bikini", + "bimonthly", + "binocular", + "biology", + "biplane", + "birth", + "biscuit", + "bite", + "biweekly", + "blender", + "blip", + "bluntly", + "boat", + "bobsled", + "bodies", + "bogeys", + "boil", + "boldly", + "bomb", + "border", + "boss", + "both", + "bounced", + "bovine", + "bowling", + "boxes", + "boyfriend", + "broken", + "brunt", + "bubble", + "buckets", + "budget", + "buffet", + "bugs", + "building", + "bulb", + "bumper", + "bunch", + "business", + "butter", + "buying", + "buzzer", + "bygones", + "byline", + "bypass", + "cabin", + "cactus", + "cadets", + "cafe", + "cage", + "cajun", + "cake", + "calamity", + "camp", + "candy", + "casket", + "catch", + "cause", + "cavernous", + "cease", + "cedar", + "ceiling", + "cell", + "cement", + "cent", + "certain", + "chlorine", + "chrome", + "cider", + "cigar", + "cinema", + "circle", + "cistern", + "citadel", + "civilian", + "claim", + "click", + "clue", + "coal", + "cobra", + "cocoa", + "code", + "coexist", + "coffee", + "cogs", + "cohesive", + "coils", + "colony", + "comb", + "cool", + "copy", + "corrode", + "costume", + "cottage", + "cousin", + "cowl", + "criminal", + "cube", + "cucumber", + "cuddled", + "cuffs", + "cuisine", + "cunning", + "cupcake", + "custom", + "cycling", + "cylinder", + "cynical", + "dabbing", + "dads", + "daft", + "dagger", + "daily", + "damp", + "dangerous", + "dapper", + "darted", + "dash", + "dating", + "dauntless", + "dawn", + "daytime", + "dazed", + "debut", + "decay", + "dedicated", + "deepest", + "deftly", + "degrees", + "dehydrate", + "deity", + "dejected", + "delayed", + "demonstrate", + "dented", + "deodorant", + "depth", + "desk", + "devoid", + "dewdrop", + "dexterity", + "dialect", + "dice", + "diet", + "different", + "digit", + "dilute", + "dime", + "dinner", + "diode", + "diplomat", + "directed", + "distance", + "ditch", + "divers", + "dizzy", + "doctor", + "dodge", + "does", + "dogs", + "doing", + "dolphin", + "domestic", + "donuts", + "doorway", + "dormant", + "dosage", + "dotted", + "double", + "dove", + "down", + "dozen", + "dreams", + "drinks", + "drowning", + "drunk", + "drying", + "dual", + "dubbed", + "duckling", + "dude", + "duets", + "duke", + "dullness", + "dummy", + "dunes", + "duplex", + "duration", + "dusted", + "duties", + "dwarf", + "dwelt", + "dwindling", + "dying", + "dynamite", + "dyslexic", + "each", + "eagle", + "earth", + "easy", + "eating", + "eavesdrop", + "eccentric", + "echo", + "eclipse", + "economics", + "ecstatic", + "eden", + "edgy", + "edited", + "educated", + "eels", + "efficient", + "eggs", + "egotistic", + "eight", + "either", + "eject", + "elapse", + "elbow", + "eldest", + "eleven", + "elite", + "elope", + "else", + "eluded", + "emails", + "ember", + "emerge", + "emit", + "emotion", + "empty", + "emulate", + "energy", + "enforce", + "enhanced", + "enigma", + "enjoy", + "enlist", + "enmity", + "enough", + "enraged", + "ensign", + "entrance", + "envy", + "epoxy", + "equip", + "erase", + "erected", + "erosion", + "error", + "eskimos", + "espionage", + "essential", + "estate", + "etched", + "eternal", + "ethics", + "etiquette", + "evaluate", + "evenings", + "evicted", + "evolved", + "examine", + "excess", + "exhale", + "exit", + "exotic", + "exquisite", + "extra", + "exult", + "fabrics", + "factual", + "fading", + "fainted", + "faked", + "fall", + "family", + "fancy", + "farming", + "fatal", + "faulty", + "fawns", + "faxed", + "fazed", + "feast", + "february", + "federal", + "feel", + "feline", + "females", + "fences", + "ferry", + "festival", + "fetches", + "fever", + "fewest", + "fiat", + "fibula", + "fictional", + "fidget", + "fierce", + "fifteen", + "fight", + "films", + "firm", + "fishing", + "fitting", + "five", + "fixate", + "fizzle", + "fleet", + "flippant", + "flying", + "foamy", + "focus", + "foes", + "foggy", + "foiled", + "folding", + "fonts", + "foolish", + "fossil", + "fountain", + "fowls", + "foxes", + "foyer", + "framed", + "friendly", + "frown", + "fruit", + "frying", + "fudge", + "fuel", + "fugitive", + "fully", + "fuming", + "fungal", + "furnished", + "fuselage", + "future", + "fuzzy", + "gables", + "gadget", + "gags", + "gained", + "galaxy", + "gambit", + "gang", + "gasp", + "gather", + "gauze", + "gave", + "gawk", + "gaze", + "gearbox", + "gecko", + "geek", + "gels", + "gemstone", + "general", + "geometry", + "germs", + "gesture", + "getting", + "geyser", + "ghetto", + "ghost", + "giant", + "giddy", + "gifts", + "gigantic", + "gills", + "gimmick", + "ginger", + "girth", + "giving", + "glass", + "gleeful", + "glide", + "gnaw", + "gnome", + "goat", + "goblet", + "godfather", + "goes", + "goggles", + "going", + "goldfish", + "gone", + "goodbye", + "gopher", + "gorilla", + "gossip", + "gotten", + "gourmet", + "governing", + "gown", + "greater", + "grunt", + "guarded", + "guest", + "guide", + "gulp", + "gumball", + "guru", + "gusts", + "gutter", + "guys", + "gymnast", + "gypsy", + "gyrate", + "habitat", + "hacksaw", + "haggled", + "hairy", + "hamburger", + "happens", + "hashing", + "hatchet", + "haunted", + "having", + "hawk", + "haystack", + "hazard", + "hectare", + "hedgehog", + "heels", + "hefty", + "height", + "hemlock", + "hence", + "heron", + "hesitate", + "hexagon", + "hickory", + "hiding", + "highway", + "hijack", + "hiker", + "hills", + "himself", + "hinder", + "hippo", + "hire", + "history", + "hitched", + "hive", + "hoax", + "hobby", + "hockey", + "hoisting", + "hold", + "honked", + "hookup", + "hope", + "hornet", + "hospital", + "hotel", + "hounded", + "hover", + "howls", + "hubcaps", + "huddle", + "huge", + "hull", + "humid", + "hunter", + "hurried", + "husband", + "huts", + "hybrid", + "hydrogen", + "hyper", + "iceberg", + "icing", + "icon", + "identity", + "idiom", + "idled", + "idols", + "igloo", + "ignore", + "iguana", + "illness", + "imagine", + "imbalance", + "imitate", + "impel", + "inactive", + "inbound", + "incur", + "industrial", + "inexact", + "inflamed", + "ingested", + "initiate", + "injury", + "inkling", + "inline", + "inmate", + "innocent", + "inorganic", + "input", + "inquest", + "inroads", + "insult", + "intended", + "inundate", + "invoke", + "inwardly", + "ionic", + "irate", + "iris", + "irony", + "irritate", + "island", + "isolated", + "issued", + "italics", + "itches", + "items", + "itinerary", + "itself", + "ivory", + "jabbed", + "jackets", + "jaded", + "jagged", + "jailed", + "jamming", + "january", + "jargon", + "jaunt", + "javelin", + "jaws", + "jazz", + "jeans", + "jeers", + "jellyfish", + "jeopardy", + "jerseys", + "jester", + "jetting", + "jewels", + "jigsaw", + "jingle", + "jittery", + "jive", + "jobs", + "jockey", + "jogger", + "joining", + "joking", + "jolted", + "jostle", + "journal", + "joyous", + "jubilee", + "judge", + "juggled", + "juicy", + "jukebox", + "july", + "jump", + "junk", + "jury", + "justice", + "juvenile", + "kangaroo", + "karate", + "keep", + "kennel", + "kept", + "kernels", + "kettle", + "keyboard", + "kickoff", + "kidneys", + "king", + "kiosk", + "kisses", + "kitchens", + "kiwi", + "knapsack", + "knee", + "knife", + "knowledge", + "knuckle", + "koala", + "laboratory", + "ladder", + "lagoon", + "lair", + "lakes", + "lamb", + "language", + "laptop", + "large", + "last", + "later", + "launching", + "lava", + "lawsuit", + "layout", + "lazy", + "lectures", + "ledge", + "leech", + "left", + "legion", + "leisure", + "lemon", + "lending", + "leopard", + "lesson", + "lettuce", + "lexicon", + "liar", + "library", + "licks", + "lids", + "lied", + "lifestyle", + "light", + "likewise", + "lilac", + "limits", + "linen", + "lion", + "lipstick", + "liquid", + "listen", + "lively", + "loaded", + "lobster", + "locker", + "lodge", + "lofty", + "logic", + "loincloth", + "long", + "looking", + "lopped", + "lordship", + "losing", + "lottery", + "loudly", + "love", + "lower", + "loyal", + "lucky", + "luggage", + "lukewarm", + "lullaby", + "lumber", + "lunar", + "lurk", + "lush", + "luxury", + "lymph", + "lynx", + "lyrics", + "macro", + "madness", + "magically", + "mailed", + "major", + "makeup", + "malady", + "mammal", + "maps", + "masterful", + "match", + "maul", + "maverick", + "maximum", + "mayor", + "maze", + "meant", + "mechanic", + "medicate", + "meeting", + "megabyte", + "melting", + "memoir", + "menu", + "merger", + "mesh", + "metro", + "mews", + "mice", + "midst", + "mighty", + "mime", + "mirror", + "misery", + "mittens", + "mixture", + "moat", + "mobile", + "mocked", + "mohawk", + "moisture", + "molten", + "moment", + "money", + "moon", + "mops", + "morsel", + "mostly", + "motherly", + "mouth", + "movement", + "mowing", + "much", + "muddy", + "muffin", + "mugged", + "mullet", + "mumble", + "mundane", + "muppet", + "mural", + "musical", + "muzzle", + "myriad", + "mystery", + "myth", + "nabbing", + "nagged", + "nail", + "names", + "nanny", + "napkin", + "narrate", + "nasty", + "natural", + "nautical", + "navy", + "nearby", + "necklace", + "needed", + "negative", + "neither", + "neon", + "nephew", + "nerves", + "nestle", + "network", + "neutral", + "never", + "newt", + "nexus", + "nibs", + "niche", + "niece", + "nifty", + "nightly", + "nimbly", + "nineteen", + "nirvana", + "nitrogen", + "nobody", + "nocturnal", + "nodes", + "noises", + "nomad", + "noodles", + "northern", + "nostril", + "noted", + "nouns", + "novelty", + "nowhere", + "nozzle", + "nuance", + "nucleus", + "nudged", + "nugget", + "nuisance", + "null", + "number", + "nuns", + "nurse", + "nutshell", + "nylon", + "oaks", + "oars", + "oasis", + "oatmeal", + "obedient", + "object", + "obliged", + "obnoxious", + "observant", + "obtains", + "obvious", + "occur", + "ocean", + "october", + "odds", + "odometer", + "offend", + "often", + "oilfield", + "ointment", + "okay", + "older", + "olive", + "olympics", + "omega", + "omission", + "omnibus", + "onboard", + "oncoming", + "oneself", + "ongoing", + "onion", + "online", + "onslaught", + "onto", + "onward", + "oozed", + "opacity", + "opened", + "opposite", + "optical", + "opus", + "orange", + "orbit", + "orchid", + "orders", + "organs", + "origin", + "ornament", + "orphans", + "oscar", + "ostrich", + "otherwise", + "otter", + "ouch", + "ought", + "ounce", + "ourselves", + "oust", + "outbreak", + "oval", + "oven", + "owed", + "owls", + "owner", + "oxidant", + "oxygen", + "oyster", + "ozone", + "pact", + "paddles", + "pager", + "pairing", + "palace", + "pamphlet", + "pancakes", + "paper", + "paradise", + "pastry", + "patio", + "pause", + "pavements", + "pawnshop", + "payment", + "peaches", + "pebbles", + "peculiar", + "pedantic", + "peeled", + "pegs", + "pelican", + "pencil", + "people", + "pepper", + "perfect", + "pests", + "petals", + "phase", + "pheasants", + "phone", + "phrases", + "physics", + "piano", + "picked", + "pierce", + "pigment", + "piloted", + "pimple", + "pinched", + "pioneer", + "pipeline", + "pirate", + "pistons", + "pitched", + "pivot", + "pixels", + "pizza", + "playful", + "pledge", + "pliers", + "plotting", + "plus", + "plywood", + "poaching", + "pockets", + "podcast", + "poetry", + "point", + "poker", + "polar", + "ponies", + "pool", + "popular", + "portents", + "possible", + "potato", + "pouch", + "poverty", + "powder", + "pram", + "present", + "pride", + "problems", + "pruned", + "prying", + "psychic", + "public", + "puck", + "puddle", + "puffin", + "pulp", + "pumpkins", + "punch", + "puppy", + "purged", + "push", + "putty", + "puzzled", + "pylons", + "pyramid", + "python", + "queen", + "quick", + "quote", + "rabbits", + "racetrack", + "radar", + "rafts", + "rage", + "railway", + "raking", + "rally", + "ramped", + "randomly", + "rapid", + "rarest", + "rash", + "rated", + "ravine", + "rays", + "razor", + "react", + "rebel", + "recipe", + "reduce", + "reef", + "refer", + "regular", + "reheat", + "reinvest", + "rejoices", + "rekindle", + "relic", + "remedy", + "renting", + "reorder", + "repent", + "request", + "reruns", + "rest", + "return", + "reunion", + "revamp", + "rewind", + "rhino", + "rhythm", + "ribbon", + "richly", + "ridges", + "rift", + "rigid", + "rims", + "ringing", + "riots", + "ripped", + "rising", + "ritual", + "river", + "roared", + "robot", + "rockets", + "rodent", + "rogue", + "roles", + "romance", + "roomy", + "roped", + "roster", + "rotate", + "rounded", + "rover", + "rowboat", + "royal", + "ruby", + "rudely", + "ruffled", + "rugged", + "ruined", + "ruling", + "rumble", + "runway", + "rural", + "rustled", + "ruthless", + "sabotage", + "sack", + "sadness", + "safety", + "saga", + "sailor", + "sake", + "salads", + "sample", + "sanity", + "sapling", + "sarcasm", + "sash", + "satin", + "saucepan", + "saved", + "sawmill", + "saxophone", + "sayings", + "scamper", + "scenic", + "school", + "science", + "scoop", + "scrub", + "scuba", + "seasons", + "second", + "sedan", + "seeded", + "segments", + "seismic", + "selfish", + "semifinal", + "sensible", + "september", + "sequence", + "serving", + "session", + "setup", + "seventh", + "sewage", + "shackles", + "shelter", + "shipped", + "shocking", + "shrugged", + "shuffled", + "shyness", + "siblings", + "sickness", + "sidekick", + "sieve", + "sifting", + "sighting", + "silk", + "simplest", + "sincerely", + "sipped", + "siren", + "situated", + "sixteen", + "sizes", + "skater", + "skew", + "skirting", + "skulls", + "skydive", + "slackens", + "sleepless", + "slid", + "slower", + "slug", + "smash", + "smelting", + "smidgen", + "smog", + "smuggled", + "snake", + "sneeze", + "sniff", + "snout", + "snug", + "soapy", + "sober", + "soccer", + "soda", + "software", + "soggy", + "soil", + "solved", + "somewhere", + "sonic", + "soothe", + "soprano", + "sorry", + "southern", + "sovereign", + "sowed", + "soya", + "space", + "speedy", + "sphere", + "spiders", + "splendid", + "spout", + "sprig", + "spud", + "spying", + "square", + "stacking", + "stellar", + "stick", + "stockpile", + "strained", + "stunning", + "stylishly", + "subtly", + "succeed", + "suddenly", + "suede", + "suffice", + "sugar", + "suitcase", + "sulking", + "summon", + "sunken", + "superior", + "surfer", + "sushi", + "suture", + "swagger", + "swept", + "swiftly", + "sword", + "swung", + "syllabus", + "symptoms", + "syndrome", + "syringe", + "system", + "taboo", + "tacit", + "tadpoles", + "tagged", + "tail", + "taken", + "talent", + "tamper", + "tanks", + "tapestry", + "tarnished", + "tasked", + "tattoo", + "taunts", + "tavern", + "tawny", + "taxi", + "teardrop", + "technical", + "tedious", + "teeming", + "tell", + "template", + "tender", + "tepid", + "tequila", + "terminal", + "testing", + "tether", + "textbook", + "thaw", + "theatrics", + "thirsty", + "thorn", + "threaten", + "thumbs", + "thwart", + "ticket", + "tidy", + "tiers", + "tiger", + "tilt", + "timber", + "tinted", + "tipsy", + "tirade", + "tissue", + "titans", + "toaster", + "tobacco", + "today", + "toenail", + "toffee", + "together", + "toilet", + "token", + "tolerant", + "tomorrow", + "tonic", + "toolbox", + "topic", + "torch", + "tossed", + "total", + "touchy", + "towel", + "toxic", + "toyed", + "trash", + "trendy", + "tribal", + "trolling", + "truth", + "trying", + "tsunami", + "tubes", + "tucks", + "tudor", + "tuesday", + "tufts", + "tugs", + "tuition", + "tulips", + "tumbling", + "tunnel", + "turnip", + "tusks", + "tutor", + "tuxedo", + "twang", + "tweezers", + "twice", + "twofold", + "tycoon", + "typist", + "tyrant", + "ugly", + "ulcers", + "ultimate", + "umbrella", + "umpire", + "unafraid", + "unbending", + "uncle", + "under", + "uneven", + "unfit", + "ungainly", + "unhappy", + "union", + "unjustly", + "unknown", + "unlikely", + "unmask", + "unnoticed", + "unopened", + "unplugs", + "unquoted", + "unrest", + "unsafe", + "until", + "unusual", + "unveil", + "unwind", + "unzip", + "upbeat", + "upcoming", + "update", + "upgrade", + "uphill", + "upkeep", + "upload", + "upon", + "upper", + "upright", + "upstairs", + "uptight", + "upwards", + "urban", + "urchins", + "urgent", + "usage", + "useful", + "usher", + "using", + "usual", + "utensils", + "utility", + "utmost", + "utopia", + "uttered", + "vacation", + "vague", + "vain", + "value", + "vampire", + "vane", + "vapidly", + "vary", + "vastness", + "vats", + "vaults", + "vector", + "veered", + "vegan", + "vehicle", + "vein", + "velvet", + "venomous", + "verification", + "vessel", + "veteran", + "vexed", + "vials", + "vibrate", + "victim", + "video", + "viewpoint", + "vigilant", + "viking", + "village", + "vinegar", + "violin", + "vipers", + "virtual", + "visited", + "vitals", + "vivid", + "vixen", + "vocal", + "vogue", + "voice", + "volcano", + "vortex", + "voted", + "voucher", + "vowels", + "voyage", + "vulture", + "wade", + "waffle", + "wagtail", + "waist", + "waking", + "wallets", + "wanted", + "warped", + "washing", + "water", + "waveform", + "waxing", + "wayside", + "weavers", + "website", + "wedge", + "weekday", + "weird", + "welders", + "went", + "wept", + "were", + "western", + "wetsuit", + "whale", + "when", + "whipped", + "whole", + "wickets", + "width", + "wield", + "wife", + "wiggle", + "wildly", + "winter", + "wipeout", + "wiring", + "wise", + "withdrawn", + "wives", + "wizard", + "wobbly", + "woes", + "woken", + "wolf", + "womanly", + "wonders", + "woozy", + "worry", + "wounded", + "woven", + "wrap", + "wrist", + "wrong", + "yacht", + "yahoo", + "yanks", + "yard", + "yawning", + "yearbook", + "yellow", + "yesterday", + "yeti", + "yields", + "yodel", + "yoga", + "younger", + "yoyo", + "zapped", + "zeal", + "zebra", + "zero", + "zesty", + "zigzags", + "zinger", + "zippers", + "zodiac", + "zombie", + "zones", + "zoom" +]; diff --git a/cw_wownero/lib/pending_wownero_transaction.dart b/cw_wownero/lib/pending_wownero_transaction.dart index 1fc1805eb..967f63756 100644 --- a/cw_wownero/lib/pending_wownero_transaction.dart +++ b/cw_wownero/lib/pending_wownero_transaction.dart @@ -50,4 +50,9 @@ class PendingWowneroTransaction with PendingTransaction { rethrow; } } + + @override + Future commitUR() { + throw UnimplementedError(); + } } diff --git a/cw_wownero/lib/wownero_wallet.dart b/cw_wownero/lib/wownero_wallet.dart index 331957d67..5927d6434 100644 --- a/cw_wownero/lib/wownero_wallet.dart +++ b/cw_wownero/lib/wownero_wallet.dart @@ -120,6 +120,7 @@ abstract class WowneroWalletBase @override MoneroWalletKeys get keys => MoneroWalletKeys( + primaryAddress: wownero_wallet.getAddress(accountIndex: 0, addressIndex: 0), privateSpendKey: wownero_wallet.getSecretSpendKey(), privateViewKey: wownero_wallet.getSecretViewKey(), publicSpendKey: wownero_wallet.getPublicSpendKey(), diff --git a/cw_wownero/pubspec.lock b/cw_wownero/pubspec.lock index c90340800..a861c6bac 100644 --- a/cw_wownero/pubspec.lock +++ b/cw_wownero/pubspec.lock @@ -463,8 +463,8 @@ packages: dependency: "direct main" description: path: "impls/monero.dart" - ref: "6eb571ea498ed7b854934785f00fabfd0dadf75b" - resolved-ref: "6eb571ea498ed7b854934785f00fabfd0dadf75b" + ref: d72c15f4339791a7bbdf17e9d827b7b56ca144e4 + resolved-ref: d72c15f4339791a7bbdf17e9d827b7b56ca144e4 url: "https://github.com/mrcyjanek/monero_c" source: git version: "0.0.0" @@ -552,10 +552,10 @@ packages: dependency: transitive description: name: plugin_platform_interface - sha256: dbf0f707c78beedc9200146ad3cb0ab4d5da13c246336987be6940f026500d3a + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.8" pointycastle: dependency: transitive description: diff --git a/cw_wownero/pubspec.yaml b/cw_wownero/pubspec.yaml index 6943e60c3..28ece82ef 100644 --- a/cw_wownero/pubspec.yaml +++ b/cw_wownero/pubspec.yaml @@ -25,7 +25,8 @@ dependencies: monero: git: url: https://github.com/mrcyjanek/monero_c - ref: 6eb571ea498ed7b854934785f00fabfd0dadf75b # monero_c hash + ref: d72c15f4339791a7bbdf17e9d827b7b56ca144e4 +# ref: 6eb571ea498ed7b854934785f00fabfd0dadf75b # monero_c hash path: impls/monero.dart mutex: ^3.1.0 diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 470bdf4e7..abcac01fb 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,8 +1,4 @@ PODS: - - barcode_scan2 (0.0.1): - - Flutter - - MTBBarcodeScanner - - SwiftProtobuf - connectivity_plus (0.0.1): - Flutter - ReachabilitySwift @@ -76,6 +72,8 @@ PODS: - DKPhotoGallery/Resource (0.0.19): - SDWebImage - SwiftyGif + - fast_scanner (5.1.1): + - Flutter - file_picker (0.0.1): - DKImagePickerController/PhotoGallery - Flutter @@ -100,7 +98,6 @@ PODS: - Flutter - integration_test (0.0.1): - Flutter - - MTBBarcodeScanner (5.0.11) - OrderedSet (5.0.0) - package_info_plus (0.4.5): - Flutter @@ -109,12 +106,7 @@ PODS: - FlutterMacOS - permission_handler_apple (9.1.1): - Flutter - - Protobuf (3.28.2) - ReachabilitySwift (5.2.3) - - reactive_ble_mobile (0.0.1): - - Flutter - - Protobuf (~> 3.5) - - SwiftProtobuf (~> 1.0) - SDWebImage (5.19.7): - SDWebImage/Core (= 5.19.7) - SDWebImage/Core (5.19.7) @@ -127,11 +119,13 @@ PODS: - FlutterMacOS - sp_scanner (0.0.1): - Flutter - - SwiftProtobuf (1.27.1) - SwiftyGif (5.4.5) - Toast (4.1.1) - uni_links (0.0.1): - Flutter + - universal_ble (0.0.1): + - Flutter + - FlutterMacOS - url_launcher_ios (0.0.1): - Flutter - wakelock_plus (0.0.1): @@ -140,7 +134,6 @@ PODS: - Flutter DEPENDENCIES: - - barcode_scan2 (from `.symlinks/plugins/barcode_scan2/ios`) - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) - CryptoSwift - cw_haven (from `.symlinks/plugins/cw_haven/ios`) @@ -149,6 +142,7 @@ DEPENDENCIES: - device_display_brightness (from `.symlinks/plugins/device_display_brightness/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - devicelocale (from `.symlinks/plugins/devicelocale/ios`) + - fast_scanner (from `.symlinks/plugins/fast_scanner/ios`) - file_picker (from `.symlinks/plugins/file_picker/ios`) - Flutter (from `Flutter`) - flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`) @@ -161,12 +155,12 @@ DEPENDENCIES: - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - - reactive_ble_mobile (from `.symlinks/plugins/reactive_ble_mobile/ios`) - sensitive_clipboard (from `.symlinks/plugins/sensitive_clipboard/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - sp_scanner (from `.symlinks/plugins/sp_scanner/ios`) - uni_links (from `.symlinks/plugins/uni_links/ios`) + - universal_ble (from `.symlinks/plugins/universal_ble/darwin`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`) - workmanager (from `.symlinks/plugins/workmanager/ios`) @@ -176,18 +170,13 @@ SPEC REPOS: - CryptoSwift - DKImagePickerController - DKPhotoGallery - - MTBBarcodeScanner - OrderedSet - - Protobuf - ReachabilitySwift - SDWebImage - - SwiftProtobuf - SwiftyGif - Toast EXTERNAL SOURCES: - barcode_scan2: - :path: ".symlinks/plugins/barcode_scan2/ios" connectivity_plus: :path: ".symlinks/plugins/connectivity_plus/ios" cw_haven: @@ -202,6 +191,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/device_info_plus/ios" devicelocale: :path: ".symlinks/plugins/devicelocale/ios" + fast_scanner: + :path: ".symlinks/plugins/fast_scanner/ios" file_picker: :path: ".symlinks/plugins/file_picker/ios" Flutter: @@ -226,8 +217,6 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/path_provider_foundation/darwin" permission_handler_apple: :path: ".symlinks/plugins/permission_handler_apple/ios" - reactive_ble_mobile: - :path: ".symlinks/plugins/reactive_ble_mobile/ios" sensitive_clipboard: :path: ".symlinks/plugins/sensitive_clipboard/ios" share_plus: @@ -238,6 +227,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/sp_scanner/ios" uni_links: :path: ".symlinks/plugins/uni_links/ios" + universal_ble: + :path: ".symlinks/plugins/universal_ble/darwin" url_launcher_ios: :path: ".symlinks/plugins/url_launcher_ios/ios" wakelock_plus: @@ -246,7 +237,6 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/workmanager/ios" SPEC CHECKSUMS: - barcode_scan2: 0af2bb63c81b4565aab6cd78278e4c0fa136dbb0 connectivity_plus: bf0076dd84a130856aa636df1c71ccaff908fa1d CryptoSwift: c63a805d8bb5e5538e88af4e44bb537776af11ea cw_haven: b3e54e1fbe7b8e6fda57a93206bc38f8e89b898a @@ -257,6 +247,7 @@ SPEC CHECKSUMS: devicelocale: b22617f40038496deffba44747101255cee005b0 DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 + fast_scanner: 44c00940355a51258cd6c2085734193cd23d95bc file_picker: 15fd9539e4eb735dc54bae8c0534a7a9511a03de Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 flutter_inappwebview_ios: 97215cf7d4677db55df76782dbd2930c5e1c1ea0 @@ -266,23 +257,20 @@ SPEC CHECKSUMS: fluttertoast: 48c57db1b71b0ce9e6bba9f31c940ff4b001293c in_app_review: 318597b3a06c22bb46dc454d56828c85f444f99d integration_test: 13825b8a9334a850581300559b8839134b124670 - MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6 - Protobuf: 28c89b24435762f60244e691544ed80f50d82701 ReachabilitySwift: 7f151ff156cea1481a8411701195ac6a984f4979 - reactive_ble_mobile: 9ce6723d37ccf701dbffd202d487f23f5de03b4c SDWebImage: 8a6b7b160b4d710e2a22b6900e25301075c34cb3 sensitive_clipboard: d4866e5d176581536c27bb1618642ee83adca986 share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 sp_scanner: eaa617fa827396b967116b7f1f43549ca62e9a12 - SwiftProtobuf: b109bd17979d7993a84da14b1e1fdd8b0ded934a SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 Toast: 1f5ea13423a1e6674c4abdac5be53587ae481c4e uni_links: d97da20c7701486ba192624d99bffaaffcfc298a + universal_ble: cf52a7b3fd2e7c14d6d7262e9fdadb72ab6b88a6 url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe wakelock_plus: 78ec7c5b202cab7761af8e2b2b3d0671be6c4ae1 workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 09c75feee..85d39167a 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -230,6 +230,7 @@ 97C146EC1CF9000F007C117D /* Resources */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, F6F67323547956BC4F7B67F1 /* [CP] Embed Pods Frameworks */, + 777FE2B16F25A3E820834145 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -343,6 +344,23 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin\n"; }; + 777FE2B16F25A3E820834145 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; diff --git a/lib/buy/buy_provider.dart b/lib/buy/buy_provider.dart index 1a37e09b3..8e79e16b8 100644 --- a/lib/buy/buy_provider.dart +++ b/lib/buy/buy_provider.dart @@ -1,6 +1,10 @@ import 'package:cake_wallet/buy/buy_amount.dart'; +import 'package:cake_wallet/buy/buy_quote.dart'; import 'package:cake_wallet/buy/order.dart'; +import 'package:cake_wallet/buy/payment_method.dart'; +import 'package:cake_wallet/entities/fiat_currency.dart'; import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart'; +import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:flutter/material.dart'; @@ -23,14 +27,38 @@ abstract class BuyProvider { String get darkIcon; + bool get isAggregator; + @override String toString() => title; - Future launchProvider(BuildContext context, bool? isBuyAction); + Future? launchProvider( + {required BuildContext context, + required Quote quote, + required double amount, + required bool isBuyAction, + required String cryptoCurrencyAddress, + String? countryCode}) => + null; Future requestUrl(String amount, String sourceCurrency) => throw UnimplementedError(); Future findOrderById(String id) => throw UnimplementedError(); - Future calculateAmount(String amount, String sourceCurrency) => throw UnimplementedError(); + Future calculateAmount(String amount, String sourceCurrency) => + throw UnimplementedError(); + + Future> getAvailablePaymentTypes( + String fiatCurrency, String cryptoCurrency, bool isBuyAction) async => + []; + + Future?> fetchQuote( + {required CryptoCurrency cryptoCurrency, + required FiatCurrency fiatCurrency, + required double amount, + required bool isBuyAction, + required String walletAddress, + PaymentType? paymentType, + String? countryCode}) async => + null; } diff --git a/lib/buy/buy_quote.dart b/lib/buy/buy_quote.dart new file mode 100644 index 000000000..72ab7bd7d --- /dev/null +++ b/lib/buy/buy_quote.dart @@ -0,0 +1,302 @@ +import 'package:cake_wallet/buy/buy_provider.dart'; +import 'package:cake_wallet/buy/payment_method.dart'; +import 'package:cake_wallet/core/selectable_option.dart'; +import 'package:cake_wallet/entities/fiat_currency.dart'; +import 'package:cake_wallet/entities/provider_types.dart'; +import 'package:cake_wallet/exchange/limits.dart'; +import 'package:cw_core/crypto_currency.dart'; + +enum ProviderRecommendation { bestRate, lowKyc, successRate } + +extension RecommendationTitle on ProviderRecommendation { + String get title { + switch (this) { + case ProviderRecommendation.bestRate: + return 'BEST RATE'; + case ProviderRecommendation.lowKyc: + return 'LOW KYC'; + case ProviderRecommendation.successRate: + return 'SUCCESS RATE'; + } + } +} + +ProviderRecommendation? getRecommendationFromString(String title) { + switch (title) { + case 'BEST RATE': + return ProviderRecommendation.bestRate; + case 'LowKyc': + return ProviderRecommendation.lowKyc; + case 'SuccessRate': + return ProviderRecommendation.successRate; + default: + return null; + } +} + +class Quote extends SelectableOption { + Quote({ + required this.rate, + required this.feeAmount, + required this.networkFee, + required this.transactionFee, + required this.payout, + required this.provider, + required this.paymentType, + required this.recommendations, + this.isBuyAction = true, + this.quoteId, + this.rampId, + this.rampName, + this.rampIconPath, + this.limits, + }) : super(title: provider.isAggregator ? rampName ?? '' : provider.title); + + final double rate; + final double feeAmount; + final double networkFee; + final double transactionFee; + final double payout; + final PaymentType paymentType; + final BuyProvider provider; + final String? quoteId; + final List recommendations; + String? rampId; + String? rampName; + String? rampIconPath; + bool _isSelected = false; + bool _isBestRate = false; + bool isBuyAction; + Limits? limits; + + late FiatCurrency _fiatCurrency; + late CryptoCurrency _cryptoCurrency; + + + bool get isSelected => _isSelected; + bool get isBestRate => _isBestRate; + FiatCurrency get fiatCurrency => _fiatCurrency; + CryptoCurrency get cryptoCurrency => _cryptoCurrency; + + @override + bool get isOptionSelected => this._isSelected; + + @override + String get lightIconPath => + provider.isAggregator ? rampIconPath ?? provider.lightIcon : provider.lightIcon; + + @override + String get darkIconPath => + provider.isAggregator ? rampIconPath ?? provider.darkIcon : provider.darkIcon; + + @override + List get badges => recommendations.map((e) => e.title).toList(); + + @override + String get topLeftSubTitle => + this.rate > 0 ? '1 $cryptoName = ${rate.toStringAsFixed(2)} $fiatName' : ''; + + @override + String get bottomLeftSubTitle { + if (limits != null) { + final min = limits!.min; + final max = limits!.max; + return 'min: ${min} ${fiatCurrency.toString()} | max: ${max == double.infinity ? '' : '${max} ${fiatCurrency.toString()}'}'; + } + return ''; + } + + String get fiatName => isBuyAction ? fiatCurrency.toString() : cryptoCurrency.toString(); + + String get cryptoName => isBuyAction ? cryptoCurrency.toString() : fiatCurrency.toString(); + + @override + String? get topRightSubTitle => ''; + + @override + String get topRightSubTitleLightIconPath => provider.isAggregator ? provider.lightIcon : ''; + + @override + String get topRightSubTitleDarkIconPath => provider.isAggregator ? provider.darkIcon : ''; + + String get quoteTitle => '${provider.title} - ${paymentType.name}'; + + String get formatedFee => '$feeAmount ${isBuyAction ? fiatCurrency : cryptoCurrency}'; + + set setIsSelected(bool isSelected) => _isSelected = isSelected; + set setIsBestRate(bool isBestRate) => _isBestRate = isBestRate; + set setFiatCurrency(FiatCurrency fiatCurrency) => _fiatCurrency = fiatCurrency; + set setCryptoCurrency(CryptoCurrency cryptoCurrency) => _cryptoCurrency = cryptoCurrency; + set setLimits(Limits limits) => this.limits = limits; + + factory Quote.fromOnramperJson(Map json, bool isBuyAction, + Map metaData, PaymentType paymentType) { + final rate = _toDouble(json['rate']) ?? 0.0; + final networkFee = _toDouble(json['networkFee']) ?? 0.0; + final transactionFee = _toDouble(json['transactionFee']) ?? 0.0; + final feeAmount = double.parse((networkFee + transactionFee).toStringAsFixed(2)); + + final rampId = json['ramp'] as String? ?? ''; + final rampData = metaData[rampId] ?? {}; + final rampName = rampData['displayName'] as String? ?? ''; + final rampIconPath = rampData['svg'] as String? ?? ''; + + final recommendations = json['recommendations'] != null + ? List.from(json['recommendations'] as List) + : []; + + final enumRecommendations = recommendations + .map((e) => getRecommendationFromString(e)) + .whereType() + .toList(); + + final availablePaymentMethods = json['availablePaymentMethods'] as List? ?? []; + double minLimit = 0.0; + double maxLimit = double.infinity; + + for (var paymentMethod in availablePaymentMethods) { + if (paymentMethod is Map) { + final details = paymentMethod['details'] as Map?; + + if (details != null) { + final limits = details['limits'] as Map?; + + if (limits != null && limits.isNotEmpty) { + final firstLimitEntry = limits.values.first as Map?; + if (firstLimitEntry != null) { + minLimit = _toDouble(firstLimitEntry['min'])?.roundToDouble() ?? 0.0; + maxLimit = _toDouble(firstLimitEntry['max'])?.roundToDouble() ?? double.infinity; + break; + } + } + } + } + } + + return Quote( + rate: rate, + feeAmount: feeAmount, + networkFee: networkFee, + transactionFee: transactionFee, + payout: json['payout'] as double? ?? 0.0, + rampId: rampId, + rampName: rampName, + rampIconPath: rampIconPath, + paymentType: paymentType, + quoteId: json['quoteId'] as String? ?? '', + recommendations: enumRecommendations, + provider: ProvidersHelper.getProviderByType(ProviderType.onramper)!, + isBuyAction: isBuyAction, + limits: Limits(min: minLimit, max: maxLimit), + ); + } + + factory Quote.fromMoonPayJson( + Map json, bool isBuyAction, PaymentType paymentType) { + final rate = isBuyAction + ? json['quoteCurrencyPrice'] as double? ?? 0.0 + : json['baseCurrencyPrice'] as double? ?? 0.0; + final fee = _toDouble(json['feeAmount']) ?? 0.0; + final networkFee = _toDouble(json['networkFeeAmount']) ?? 0.0; + final transactionFee = _toDouble(json['extraFeeAmount']) ?? 0.0; + final feeAmount = double.parse((fee + networkFee + transactionFee).toStringAsFixed(2)); + + final baseCurrency = json['baseCurrency'] as Map?; + + double minLimit = 0.0; + double maxLimit = double.infinity; + + if (baseCurrency != null) { + minLimit = _toDouble(baseCurrency['minAmount']) ?? minLimit; + maxLimit = _toDouble(baseCurrency['maxAmount']) ?? maxLimit; + } + + return Quote( + rate: rate, + feeAmount: feeAmount, + networkFee: networkFee, + transactionFee: transactionFee, + payout: _toDouble(json['quoteCurrencyAmount']) ?? 0.0, + paymentType: paymentType, + recommendations: [], + quoteId: json['signature'] as String? ?? '', + provider: ProvidersHelper.getProviderByType(ProviderType.moonpay)!, + isBuyAction: isBuyAction, + limits: Limits(min: minLimit, max: maxLimit), + ); + } + + factory Quote.fromDFXJson( + Map json, + bool isBuyAction, + PaymentType paymentType, + ) { + final rate = _toDouble(json['exchangeRate']) ?? 0.0; + final fees = json['fees'] as Map; + + final minVolume = _toDouble(json['minVolume']) ?? 0.0; + final maxVolume = _toDouble(json['maxVolume']) ?? double.infinity; + + return Quote( + rate: isBuyAction ? rate : 1 / rate, + feeAmount: _toDouble(json['feeAmount']) ?? 0.0, + networkFee: _toDouble(fees['network']) ?? 0.0, + transactionFee: _toDouble(fees['rate']) ?? 0.0, + payout: _toDouble(json['payout']) ?? 0.0, + paymentType: paymentType, + recommendations: [ProviderRecommendation.lowKyc], + provider: ProvidersHelper.getProviderByType(ProviderType.dfx)!, + isBuyAction: isBuyAction, + limits: Limits(min: minVolume, max: maxVolume), + ); + } + + factory Quote.fromRobinhoodJson( + Map json, bool isBuyAction, PaymentType paymentType) { + final networkFee = json['networkFee'] as Map; + final processingFee = json['processingFee'] as Map; + final networkFeeAmount = _toDouble(networkFee['fiatAmount']) ?? 0.0; + final transactionFeeAmount = _toDouble(processingFee['fiatAmount']) ?? 0.0; + final feeAmount = double.parse((networkFeeAmount + transactionFeeAmount).toStringAsFixed(2)); + + return Quote( + rate: _toDouble(json['price']) ?? 0.0, + feeAmount: feeAmount, + networkFee: _toDouble(networkFee['fiatAmount']) ?? 0.0, + transactionFee: _toDouble(processingFee['fiatAmount']) ?? 0.0, + payout: _toDouble(json['cryptoAmount']) ?? 0.0, + paymentType: paymentType, + recommendations: [], + provider: ProvidersHelper.getProviderByType(ProviderType.robinhood)!, + isBuyAction: isBuyAction, + limits: Limits(min: 0.0, max: double.infinity), + ); + } + + factory Quote.fromMeldJson(Map json, bool isBuyAction, PaymentType paymentType) { + final quotes = json['quotes'][0] as Map; + return Quote( + rate: quotes['exchangeRate'] as double? ?? 0.0, + feeAmount: quotes['totalFee'] as double? ?? 0.0, + networkFee: quotes['networkFee'] as double? ?? 0.0, + transactionFee: quotes['transactionFee'] as double? ?? 0.0, + payout: quotes['payout'] as double? ?? 0.0, + paymentType: paymentType, + recommendations: [], + provider: ProvidersHelper.getProviderByType(ProviderType.meld)!, + isBuyAction: isBuyAction, + limits: Limits(min: 0.0, max: double.infinity), + ); + } + + static double? _toDouble(dynamic value) { + if (value is int) { + return value.toDouble(); + } else if (value is double) { + return value; + } else if (value is String) { + return double.tryParse(value); + } + return null; + } +} diff --git a/lib/buy/dfx/dfx_buy_provider.dart b/lib/buy/dfx/dfx_buy_provider.dart index b3ed72498..c1ed762b1 100644 --- a/lib/buy/dfx/dfx_buy_provider.dart +++ b/lib/buy/dfx/dfx_buy_provider.dart @@ -1,13 +1,17 @@ import 'dart:convert'; +import 'dart:developer'; import 'package:cake_wallet/buy/buy_provider.dart'; +import 'package:cake_wallet/buy/buy_quote.dart'; +import 'package:cake_wallet/buy/payment_method.dart'; +import 'package:cake_wallet/entities/fiat_currency.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/connect_device/connect_device_page.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; -import 'package:cake_wallet/utils/device_info.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart'; +import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; @@ -15,10 +19,12 @@ import 'package:http/http.dart' as http; import 'package:url_launcher/url_launcher.dart'; class DFXBuyProvider extends BuyProvider { - DFXBuyProvider({required WalletBase wallet, bool isTestEnvironment = false, LedgerViewModel? ledgerVM}) + DFXBuyProvider( + {required WalletBase wallet, bool isTestEnvironment = false, LedgerViewModel? ledgerVM}) : super(wallet: wallet, isTestEnvironment: isTestEnvironment, ledgerVM: ledgerVM); static const _baseUrl = 'api.dfx.swiss'; + // static const _signMessagePath = '/v1/auth/signMessage'; static const _authPath = '/v1/auth'; static const walletName = 'CakeWallet'; @@ -35,24 +41,8 @@ class DFXBuyProvider extends BuyProvider { @override String get darkIcon => 'assets/images/dfx_dark.png'; - String get assetOut { - switch (wallet.type) { - case WalletType.bitcoin: - return 'BTC'; - case WalletType.bitcoinCash: - return 'BCH'; - case WalletType.litecoin: - return 'LTC'; - case WalletType.monero: - return 'XMR'; - case WalletType.ethereum: - return 'ETH'; - case WalletType.polygon: - return 'MATIC'; - default: - throw Exception("WalletType is not available for DFX ${wallet.type}"); - } - } + @override + bool get isAggregator => false; String get blockchain { switch (wallet.type) { @@ -60,21 +50,13 @@ class DFXBuyProvider extends BuyProvider { case WalletType.bitcoinCash: case WalletType.litecoin: return 'Bitcoin'; - case WalletType.monero: - return 'Monero'; - case WalletType.ethereum: - return 'Ethereum'; - case WalletType.polygon: - return 'Polygon'; default: - throw Exception("WalletType is not available for DFX ${wallet.type}"); + return walletTypeToString(wallet.type); } } - String get walletAddress => - wallet.walletAddresses.primaryAddress ?? wallet.walletAddresses.address; - Future getSignMessage() async => + Future getSignMessage(String walletAddress) async => "By_signing_this_message,_you_confirm_that_you_are_the_sole_owner_of_the_provided_Blockchain_address._Your_ID:_$walletAddress"; // // Lets keep this just in case, but we can avoid this API Call @@ -92,8 +74,9 @@ class DFXBuyProvider extends BuyProvider { // } // } - Future auth() async { - final signMessage = await getSignature(await getSignMessage()); + Future auth(String walletAddress) async { + final signMessage = await getSignature( + await getSignMessage(walletAddress), walletAddress); final requestBody = jsonEncode({ 'wallet': walletName, @@ -120,7 +103,7 @@ class DFXBuyProvider extends BuyProvider { } } - Future getSignature(String message) async { + Future getSignature(String message, String walletAddress) async { switch (wallet.type) { case WalletType.ethereum: case WalletType.polygon: @@ -135,8 +118,178 @@ class DFXBuyProvider extends BuyProvider { } } + Future> fetchFiatCredentials(String fiatCurrency) async { + final url = Uri.https(_baseUrl, '/v1/fiat'); + + try { + final response = await http.get(url, headers: {'accept': 'application/json'}); + + if (response.statusCode == 200) { + final data = jsonDecode(response.body) as List; + for (final item in data) { + if (item['name'] == fiatCurrency) return item as Map; + } + log('DFX does not support fiat: $fiatCurrency'); + return {}; + } else { + log('DFX Failed to fetch fiat currencies: ${response.statusCode}'); + return {}; + } + } catch (e) { + print('DFX Error fetching fiat currencies: $e'); + return {}; + } + } + + Future> fetchAssetCredential(String assetsName) async { + final url = Uri.https(_baseUrl, '/v1/asset', {'blockchains': blockchain}); + + try { + final response = await http.get(url, headers: {'accept': 'application/json'}); + + if (response.statusCode == 200) { + final responseData = jsonDecode(response.body); + + if (responseData is List && responseData.isNotEmpty) { + return responseData.first as Map; + } else if (responseData is Map) { + return responseData; + } else { + log('DFX: Does not support this asset name : ${blockchain}'); + } + } else { + log('DFX: Failed to fetch assets: ${response.statusCode}'); + } + } catch (e) { + log('DFX: Error fetching assets: $e'); + } + return {}; + } + + Future> getAvailablePaymentTypes( + String fiatCurrency, String cryptoCurrency, bool isBuyAction) async { + final List paymentMethods = []; + + if (isBuyAction) { + final fiatBuyCredentials = await fetchFiatCredentials(fiatCurrency); + if (fiatBuyCredentials.isNotEmpty) { + fiatBuyCredentials.forEach((key, value) { + if (key == 'limits') { + final limits = value as Map; + limits.forEach((paymentMethodKey, paymentMethodValue) { + final min = _toDouble(paymentMethodValue['minVolume']); + final max = _toDouble(paymentMethodValue['maxVolume']); + if (min != null && max != null && min > 0 && max > 0) { + final paymentMethod = PaymentMethod.fromDFX( + paymentMethodKey, _getPaymentTypeByString(paymentMethodKey)); + paymentMethods.add(paymentMethod); + } + }); + } + }); + } + } else { + final assetCredentials = await fetchAssetCredential(cryptoCurrency); + if (assetCredentials.isNotEmpty) { + if (assetCredentials['sellable'] == true) { + final availablePaymentTypes = [ + PaymentType.bankTransfer, + PaymentType.creditCard, + PaymentType.sepa + ]; + availablePaymentTypes.forEach((element) { + final paymentMethod = PaymentMethod.fromDFX(normalizePaymentMethod(element)!, element); + paymentMethods.add(paymentMethod); + }); + } + } + } + + return paymentMethods; + } + @override - Future launchProvider(BuildContext context, bool? isBuyAction) async { + Future?> fetchQuote( + {required CryptoCurrency cryptoCurrency, + required FiatCurrency fiatCurrency, + required double amount, + required bool isBuyAction, + required String walletAddress, + PaymentType? paymentType, + String? countryCode}) async { + /// if buying with any currency other than eur or chf then DFX is not supported + + if (isBuyAction && (fiatCurrency != FiatCurrency.eur && fiatCurrency != FiatCurrency.chf)) { + return null; + } + + String? paymentMethod; + if (paymentType != null && paymentType != PaymentType.all) { + paymentMethod = normalizePaymentMethod(paymentType); + if (paymentMethod == null) paymentMethod = paymentType.name; + } else { + paymentMethod = 'Bank'; + } + + final action = isBuyAction ? 'buy' : 'sell'; + + if (isBuyAction && cryptoCurrency != wallet.currency) return null; + + final fiatCredentials = await fetchFiatCredentials(fiatCurrency.name.toString()); + if (fiatCredentials['id'] == null) return null; + + final assetCredentials = await fetchAssetCredential(cryptoCurrency.title.toString()); + if (assetCredentials['id'] == null) return null; + + log('DFX: Fetching $action quote: ${isBuyAction ? cryptoCurrency : fiatCurrency} -> ${isBuyAction ? fiatCurrency : cryptoCurrency}, amount: $amount, paymentMethod: $paymentMethod'); + + final url = Uri.https(_baseUrl, '/v1/$action/quote'); + final headers = {'accept': 'application/json', 'Content-Type': 'application/json'}; + final body = jsonEncode({ + 'currency': {'id': fiatCredentials['id'] as int}, + 'asset': {'id': assetCredentials['id']}, + 'amount': amount, + 'targetAmount': 0, + 'paymentMethod': paymentMethod, + 'discountCode': '' + }); + + try { + final response = await http.put(url, headers: headers, body: body); + final responseData = jsonDecode(response.body); + + if (response.statusCode == 200) { + if (responseData is Map) { + final paymentType = _getPaymentTypeByString(responseData['paymentMethod'] as String?); + final quote = Quote.fromDFXJson(responseData, isBuyAction, paymentType); + quote.setFiatCurrency = fiatCurrency; + quote.setCryptoCurrency = cryptoCurrency; + return [quote]; + } else { + print('DFX: Unexpected data type: ${responseData.runtimeType}'); + return null; + } + } else { + if (responseData is Map && responseData.containsKey('message')) { + print('DFX Error: ${responseData['message']}'); + } else { + print('DFX Failed to fetch buy quote: ${response.statusCode}'); + } + return null; + } + } catch (e) { + print('DFX Error fetching buy quote: $e'); + return null; + } + } + + Future? launchProvider( + {required BuildContext context, + required Quote quote, + required double amount, + required bool isBuyAction, + required String cryptoCurrencyAddress, + String? countryCode}) async { if (wallet.isHardwareWallet) { if (!ledgerVM!.isConnected) { await Navigator.of(context).pushNamed(Routes.connectDevices, @@ -152,26 +305,21 @@ class DFXBuyProvider extends BuyProvider { } try { - final assetOut = this.assetOut; - final blockchain = this.blockchain; - final actionType = isBuyAction == true ? '/buy' : '/sell'; + final actionType = isBuyAction ? '/buy' : '/sell'; - final accessToken = await auth(); + final accessToken = await auth(cryptoCurrencyAddress); final uri = Uri.https('services.dfx.swiss', actionType, { 'session': accessToken, 'lang': 'en', - 'asset-out': assetOut, + 'asset-out': isBuyAction ? quote.cryptoCurrency.toString() : quote.fiatCurrency.toString(), 'blockchain': blockchain, - 'asset-in': 'EUR', + 'asset-in': isBuyAction ? quote.fiatCurrency.toString() : quote.cryptoCurrency.toString(), + 'amount': amount.toString() //TODO: Amount does not work }); if (await canLaunchUrl(uri)) { - if (DeviceInfo.instance.isMobile) { - Navigator.of(context).pushNamed(Routes.webViewPage, arguments: [title, uri]); - } else { - await launchUrl(uri, mode: LaunchMode.externalApplication); - } + await launchUrl(uri, mode: LaunchMode.externalApplication); } else { throw Exception('Could not launch URL'); } @@ -187,4 +335,39 @@ class DFXBuyProvider extends BuyProvider { }); } } + + String? normalizePaymentMethod(PaymentType paymentMethod) { + switch (paymentMethod) { + case PaymentType.bankTransfer: + return 'Bank'; + case PaymentType.creditCard: + return 'Card'; + case PaymentType.sepa: + return 'Instant'; + default: + return null; + } + } + + PaymentType _getPaymentTypeByString(String? paymentMethod) { + switch (paymentMethod) { + case 'Bank': + return PaymentType.bankTransfer; + case 'Card': + return PaymentType.creditCard; + case 'Instant': + return PaymentType.sepa; + default: + return PaymentType.all; + } + } + + double? _toDouble(dynamic value) { + if (value is int) { + return value.toDouble(); + } else if (value is double) { + return value; + } + return null; + } } diff --git a/lib/buy/meld/meld_buy_provider.dart b/lib/buy/meld/meld_buy_provider.dart new file mode 100644 index 000000000..1ac3931d1 --- /dev/null +++ b/lib/buy/meld/meld_buy_provider.dart @@ -0,0 +1,262 @@ +import 'dart:convert'; + +import 'package:cake_wallet/.secrets.g.dart' as secrets; +import 'package:cake_wallet/buy/buy_provider.dart'; +import 'package:cake_wallet/buy/buy_quote.dart'; +import 'package:cake_wallet/buy/payment_method.dart'; +import 'package:cake_wallet/entities/fiat_currency.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:flutter/material.dart'; +import 'dart:developer'; +import 'package:http/http.dart' as http; +import 'package:url_launcher/url_launcher.dart'; + +class MeldBuyProvider extends BuyProvider { + MeldBuyProvider({required WalletBase wallet, bool isTestEnvironment = false}) + : super(wallet: wallet, isTestEnvironment: isTestEnvironment, ledgerVM: null); + + static const _isProduction = false; + + static const _baseUrl = _isProduction ? 'api.meld.io' : 'api-sb.meld.io'; + static const _providersProperties = '/service-providers/properties'; + static const _paymentMethodsPath = '/payment-methods'; + static const _quotePath = '/payments/crypto/quote'; + + static const String sandboxUrl = 'sb.fluidmoney.xyz'; + static const String productionUrl = 'fluidmoney.xyz'; + + static const String _baseWidgetUrl = _isProduction ? productionUrl : sandboxUrl; + + static String get _testApiKey => secrets.meldTestApiKey; + + static String get _testPublicKey => '' ; //secrets.meldTestPublicKey; + + @override + String get title => 'Meld'; + + @override + String get providerDescription => 'Meld Buy Provider'; + + @override + String get lightIcon => 'assets/images/meld_logo.svg'; + + @override + String get darkIcon => 'assets/images/meld_logo.svg'; + + @override + bool get isAggregator => true; + + @override + Future> getAvailablePaymentTypes( + String fiatCurrency, String cryptoCurrency, bool isBuyAction) async { + final params = {'fiatCurrencies': fiatCurrency, 'statuses': 'LIVE,RECENTLY_ADDED,BUILDING'}; + + final path = '$_providersProperties$_paymentMethodsPath'; + final url = Uri.https(_baseUrl, path, params); + + try { + final response = await http.get( + url, + headers: { + 'Authorization': _isProduction ? '' : _testApiKey, + 'Meld-Version': '2023-12-19', + 'accept': 'application/json', + 'content-type': 'application/json', + }, + ); + + if (response.statusCode == 200) { + final data = jsonDecode(response.body) as List; + final paymentMethods = + data.map((e) => PaymentMethod.fromMeldJson(e as Map)).toList(); + return paymentMethods; + } else { + print('Meld: Failed to fetch payment types'); + return List.empty(); + } + } catch (e) { + print('Meld: Failed to fetch payment types: $e'); + return List.empty(); + } + } + + @override + Future?> fetchQuote( + {required CryptoCurrency cryptoCurrency, + required FiatCurrency fiatCurrency, + required double amount, + required bool isBuyAction, + required String walletAddress, + PaymentType? paymentType, + String? countryCode}) async { + String? paymentMethod; + if (paymentType != null && paymentType != PaymentType.all) { + paymentMethod = normalizePaymentMethod(paymentType); + if (paymentMethod == null) paymentMethod = paymentType.name; + } + + log('Meld: Fetching buy quote: ${isBuyAction ? cryptoCurrency : fiatCurrency} -> ${isBuyAction ? fiatCurrency : cryptoCurrency}, amount: $amount'); + + final url = Uri.https(_baseUrl, _quotePath); + final headers = { + 'Authorization': _testApiKey, + 'Meld-Version': '2023-12-19', + 'accept': 'application/json', + 'content-type': 'application/json', + }; + final body = jsonEncode({ + 'countryCode': countryCode, + 'destinationCurrencyCode': isBuyAction ? fiatCurrency.name : cryptoCurrency.title, + 'sourceAmount': amount, + 'sourceCurrencyCode': isBuyAction ? cryptoCurrency.title : fiatCurrency.name, + if (paymentMethod != null) 'paymentMethod': paymentMethod, + }); + + try { + final response = await http.post(url, headers: headers, body: body); + + if (response.statusCode == 200) { + final data = jsonDecode(response.body) as Map; + final paymentType = _getPaymentTypeByString(data['paymentMethodType'] as String?); + final quote = Quote.fromMeldJson(data, isBuyAction, paymentType); + + quote.setFiatCurrency = fiatCurrency; + quote.setCryptoCurrency = cryptoCurrency; + + return [quote]; + } else { + return null; + } + } catch (e) { + print('Error fetching buy quote: $e'); + return null; + } + } + + Future? launchProvider( + {required BuildContext context, + required Quote quote, + required double amount, + required bool isBuyAction, + required String cryptoCurrencyAddress, + String? countryCode}) async { + final actionType = isBuyAction ? 'BUY' : 'SELL'; + + final params = { + 'publicKey': _isProduction ? '' : _testPublicKey, + 'countryCode': countryCode, + //'paymentMethodType': normalizePaymentMethod(paymentMethod.paymentMethodType), + 'sourceAmount': amount.toString(), + 'sourceCurrencyCode': quote.fiatCurrency, + 'destinationCurrencyCode': quote.cryptoCurrency, + 'walletAddress': cryptoCurrencyAddress, + 'transactionType': actionType + }; + + final uri = Uri.https(_baseWidgetUrl, '', params); + + try { + if (await canLaunchUrl(uri)) { + await launchUrl(uri, mode: LaunchMode.externalApplication); + } else { + throw Exception('Could not launch URL'); + } + } catch (e) { + await showPopUp( + context: context, + builder: (BuildContext context) { + return AlertWithOneAction( + alertTitle: "Meld", + alertContent: S.of(context).buy_provider_unavailable + ': $e', + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(context).pop()); + }); + } + } + + String? normalizePaymentMethod(PaymentType paymentType) { + switch (paymentType) { + case PaymentType.creditCard: + return 'CREDIT_DEBIT_CARD'; + case PaymentType.applePay: + return 'APPLE_PAY'; + case PaymentType.googlePay: + return 'GOOGLE_PAY'; + case PaymentType.neteller: + return 'NETELLER'; + case PaymentType.skrill: + return 'SKRILL'; + case PaymentType.sepa: + return 'SEPA'; + case PaymentType.sepaInstant: + return 'SEPA_INSTANT'; + case PaymentType.ach: + return 'ACH'; + case PaymentType.achInstant: + return 'INSTANT_ACH'; + case PaymentType.Khipu: + return 'KHIPU'; + case PaymentType.ovo: + return 'OVO'; + case PaymentType.zaloPay: + return 'ZALOPAY'; + case PaymentType.zaloBankTransfer: + return 'ZA_BANK_TRANSFER'; + case PaymentType.gcash: + return 'GCASH'; + case PaymentType.imps: + return 'IMPS'; + case PaymentType.dana: + return 'DANA'; + case PaymentType.ideal: + return 'IDEAL'; + default: + return null; + } + } + + PaymentType _getPaymentTypeByString(String? paymentMethod) { + switch (paymentMethod?.toUpperCase()) { + case 'CREDIT_DEBIT_CARD': + return PaymentType.creditCard; + case 'APPLE_PAY': + return PaymentType.applePay; + case 'GOOGLE_PAY': + return PaymentType.googlePay; + case 'NETELLER': + return PaymentType.neteller; + case 'SKRILL': + return PaymentType.skrill; + case 'SEPA': + return PaymentType.sepa; + case 'SEPA_INSTANT': + return PaymentType.sepaInstant; + case 'ACH': + return PaymentType.ach; + case 'INSTANT_ACH': + return PaymentType.achInstant; + case 'KHIPU': + return PaymentType.Khipu; + case 'OVO': + return PaymentType.ovo; + case 'ZALOPAY': + return PaymentType.zaloPay; + case 'ZA_BANK_TRANSFER': + return PaymentType.zaloBankTransfer; + case 'GCASH': + return PaymentType.gcash; + case 'IMPS': + return PaymentType.imps; + case 'DANA': + return PaymentType.dana; + case 'IDEAL': + return PaymentType.ideal; + default: + return PaymentType.all; + } + } +} diff --git a/lib/buy/moonpay/moonpay_provider.dart b/lib/buy/moonpay/moonpay_provider.dart index 67ee75d7c..b93c0f02d 100644 --- a/lib/buy/moonpay/moonpay_provider.dart +++ b/lib/buy/moonpay/moonpay_provider.dart @@ -1,19 +1,20 @@ import 'dart:convert'; +import 'dart:developer'; import 'package:cake_wallet/.secrets.g.dart' as secrets; -import 'package:cake_wallet/buy/buy_amount.dart'; import 'package:cake_wallet/buy/buy_exception.dart'; import 'package:cake_wallet/buy/buy_provider.dart'; import 'package:cake_wallet/buy/buy_provider_description.dart'; +import 'package:cake_wallet/buy/buy_quote.dart'; import 'package:cake_wallet/buy/order.dart'; +import 'package:cake_wallet/buy/payment_method.dart'; +import 'package:cake_wallet/entities/fiat_currency.dart'; import 'package:cake_wallet/exchange/trade_state.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/palette.dart'; -import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/themes/theme_base.dart'; -import 'package:cake_wallet/utils/device_info.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_type.dart'; @@ -39,6 +40,15 @@ class MoonPayProvider extends BuyProvider { static const _baseBuyProductUrl = 'buy.moonpay.com'; static const _cIdBaseUrl = 'exchange-helper.cakewallet.com'; static const _apiUrl = 'https://api.moonpay.com'; + static const _baseUrl = 'api.moonpay.com'; + static const _currenciesPath = '/v3/currencies'; + static const _buyQuote = '/buy_quote'; + static const _sellQuote = '/sell_quote'; + + static const _transactionsSuffix = '/v1/transactions'; + + final String baseBuyUrl; + final String baseSellUrl; @override String get providerDescription => @@ -53,6 +63,17 @@ class MoonPayProvider extends BuyProvider { @override String get darkIcon => 'assets/images/moonpay_dark.png'; + @override + bool get isAggregator => false; + + static String get _apiKey => secrets.moonPayApiKey; + + String get currencyCode => walletTypeToCryptoCurrency(wallet.type).title.toLowerCase(); + + String get trackUrl => baseBuyUrl + '/transaction_receipt?transactionId='; + + static String get _exchangeHelperApiKey => secrets.exchangeHelperApiKey; + static String themeToMoonPayTheme(ThemeBase theme) { switch (theme.type) { case ThemeType.bright: @@ -63,28 +84,12 @@ class MoonPayProvider extends BuyProvider { } } - static String get _apiKey => secrets.moonPayApiKey; - - final String baseBuyUrl; - final String baseSellUrl; - - String get currencyCode => walletTypeToCryptoCurrency(wallet.type).title.toLowerCase(); - - String get trackUrl => baseBuyUrl + '/transaction_receipt?transactionId='; - - static String get _exchangeHelperApiKey => secrets.exchangeHelperApiKey; - Future getMoonpaySignature(String query) async { final uri = Uri.https(_cIdBaseUrl, "/api/moonpay"); - final response = await post( - uri, - headers: { - 'Content-Type': 'application/json', - 'x-api-key': _exchangeHelperApiKey, - }, - body: json.encode({'query': query}), - ); + final response = await post(uri, + headers: {'Content-Type': 'application/json', 'x-api-key': _exchangeHelperApiKey}, + body: json.encode({'query': query})); if (response.statusCode == 200) { return (jsonDecode(response.body) as Map)['signature'] as String; @@ -94,85 +99,195 @@ class MoonPayProvider extends BuyProvider { } } - Future requestSellMoonPayUrl({ - required CryptoCurrency currency, - required String refundWalletAddress, - required SettingsStore settingsStore, - }) async { - final params = { - 'theme': themeToMoonPayTheme(settingsStore.currentTheme), - 'language': settingsStore.languageCode, - 'colorCode': settingsStore.currentTheme.type == ThemeType.dark - ? '#${Palette.blueCraiola.value.toRadixString(16).substring(2, 8)}' - : '#${Palette.moderateSlateBlue.value.toRadixString(16).substring(2, 8)}', - 'defaultCurrencyCode': _normalizeCurrency(currency), - 'refundWalletAddress': refundWalletAddress, - }; + Future> fetchFiatCredentials( + String fiatCurrency, String cryptocurrency, String? paymentMethod) async { + final params = {'baseCurrencyCode': fiatCurrency.toLowerCase(), 'apiKey': _apiKey}; - if (_apiKey.isNotEmpty) { - params['apiKey'] = _apiKey; + if (paymentMethod != null) params['paymentMethod'] = paymentMethod; + + final path = '$_currenciesPath/${cryptocurrency.toLowerCase()}/limits'; + final url = Uri.https(_baseUrl, path, params); + + try { + final response = await get(url, headers: {'accept': 'application/json'}); + if (response.statusCode == 200) { + return jsonDecode(response.body) as Map; + } else { + print('MoonPay does not support fiat: $fiatCurrency'); + return {}; + } + } catch (e) { + print('MoonPay Error fetching fiat currencies: $e'); + return {}; } - - final originalUri = Uri.https( - baseSellUrl, - '', - params, - ); - - if (isTestEnvironment) { - return originalUri; - } - - final signature = await getMoonpaySignature('?${originalUri.query}'); - - final query = Map.from(originalUri.queryParameters); - query['signature'] = signature; - final signedUri = originalUri.replace(queryParameters: query); - return signedUri; } - // BUY: - static const _currenciesSuffix = '/v3/currencies'; - static const _quoteSuffix = '/buy_quote'; - static const _transactionsSuffix = '/v1/transactions'; - static const _ipAddressSuffix = '/v4/ip_address'; + Future> getAvailablePaymentTypes( + String fiatCurrency, String cryptoCurrency, bool isBuyAction) async { + final List paymentMethods = []; + + if (isBuyAction) { + final fiatBuyCredentials = await fetchFiatCredentials(fiatCurrency, cryptoCurrency, null); + if (fiatBuyCredentials.isNotEmpty) { + final paymentMethod = fiatBuyCredentials['paymentMethod'] as String?; + paymentMethods.add(PaymentMethod.fromMoonPayJson( + fiatBuyCredentials, _getPaymentTypeByString(paymentMethod))); + return paymentMethods; + } + } + + return paymentMethods; + } + + @override + Future?> fetchQuote( + {required CryptoCurrency cryptoCurrency, + required FiatCurrency fiatCurrency, + required double amount, + required bool isBuyAction, + required String walletAddress, + PaymentType? paymentType, + String? countryCode}) async { + String? paymentMethod; + + if (paymentType != null && paymentType != PaymentType.all) { + paymentMethod = normalizePaymentMethod(paymentType); + if (paymentMethod == null) paymentMethod = paymentType.name; + } else { + paymentMethod = 'credit_debit_card'; + } + + final action = isBuyAction ? 'buy' : 'sell'; + + final formattedCryptoCurrency = _normalizeCurrency(cryptoCurrency); + final baseCurrencyCode = + isBuyAction ? fiatCurrency.name.toLowerCase() : cryptoCurrency.title.toLowerCase(); - Future requestBuyMoonPayUrl({ - required CryptoCurrency currency, - required SettingsStore settingsStore, - required String walletAddress, - String? amount, - }) async { final params = { - 'theme': themeToMoonPayTheme(settingsStore.currentTheme), - 'language': settingsStore.languageCode, - 'colorCode': settingsStore.currentTheme.type == ThemeType.dark + 'baseCurrencyCode': baseCurrencyCode, + 'baseCurrencyAmount': amount.toString(), + 'amount': amount.toString(), + 'paymentMethod': paymentMethod, + 'areFeesIncluded': 'false', + 'apiKey': _apiKey + }; + + log('MoonPay: Fetching $action quote: ${isBuyAction ? formattedCryptoCurrency : fiatCurrency.name.toLowerCase()} -> ${isBuyAction ? baseCurrencyCode : formattedCryptoCurrency}, amount: $amount, paymentMethod: $paymentMethod'); + + final quotePath = isBuyAction ? _buyQuote : _sellQuote; + + final path = '$_currenciesPath/$formattedCryptoCurrency$quotePath'; + final url = Uri.https(_baseUrl, path, params); + try { + final response = await get(url); + + if (response.statusCode == 200) { + final data = jsonDecode(response.body) as Map; + + // Check if the response is for the correct fiat currency + if (isBuyAction) { + final fiatCurrencyCode = data['baseCurrencyCode'] as String?; + if (fiatCurrencyCode == null || fiatCurrencyCode != fiatCurrency.name.toLowerCase()) + return null; + } else { + final quoteCurrency = data['quoteCurrency'] as Map?; + if (quoteCurrency == null || quoteCurrency['code'] != fiatCurrency.name.toLowerCase()) + return null; + } + + final paymentMethods = data['paymentMethod'] as String?; + final quote = + Quote.fromMoonPayJson(data, isBuyAction, _getPaymentTypeByString(paymentMethods)); + + quote.setFiatCurrency = fiatCurrency; + quote.setCryptoCurrency = cryptoCurrency; + + return [quote]; + } else { + print('Moon Pay: Error fetching buy quote: '); + return null; + } + } catch (e) { + print('Moon Pay: Error fetching buy quote: $e'); + return null; + } + } + + @override + Future? launchProvider( + {required BuildContext context, + required Quote quote, + required double amount, + required bool isBuyAction, + required String cryptoCurrencyAddress, + String? countryCode}) async { + + final Map params = { + 'theme': themeToMoonPayTheme(_settingsStore.currentTheme), + 'language': _settingsStore.languageCode, + 'colorCode': _settingsStore.currentTheme.type == ThemeType.dark ? '#${Palette.blueCraiola.value.toRadixString(16).substring(2, 8)}' : '#${Palette.moderateSlateBlue.value.toRadixString(16).substring(2, 8)}', - 'baseCurrencyCode': settingsStore.fiatCurrency.title, - 'baseCurrencyAmount': amount ?? '0', - 'currencyCode': _normalizeCurrency(currency), - 'walletAddress': walletAddress, + 'baseCurrencyCode': isBuyAction ? quote.fiatCurrency.name : quote.cryptoCurrency.name, + 'baseCurrencyAmount': amount.toString(), + 'walletAddress': cryptoCurrencyAddress, 'lockAmount': 'false', 'showAllCurrencies': 'false', 'showWalletAddressForm': 'false', - 'enabledPaymentMethods': - 'credit_debit_card,apple_pay,google_pay,samsung_pay,sepa_bank_transfer,gbp_bank_transfer,gbp_open_banking_payment', + if (isBuyAction) + 'enabledPaymentMethods': normalizePaymentMethod(quote.paymentType) ?? + 'credit_debit_card,apple_pay,google_pay,samsung_pay,sepa_bank_transfer,gbp_bank_transfer,gbp_open_banking_payment', + if (!isBuyAction) 'refundWalletAddress': cryptoCurrencyAddress }; - if (_apiKey.isNotEmpty) { - params['apiKey'] = _apiKey; - } + if (isBuyAction) params['currencyCode'] = quote.cryptoCurrency.name; + if (!isBuyAction) params['quoteCurrencyCode'] = quote.cryptoCurrency.name; - final originalUri = Uri.https( - baseBuyUrl, - '', - params, - ); + try { + { + final uri = await requestMoonPayUrl( + walletAddress: cryptoCurrencyAddress, + settingsStore: _settingsStore, + isBuyAction: isBuyAction, + amount: amount.toString(), + params: params); - if (isTestEnvironment) { - return originalUri; + if (await canLaunchUrl(uri)) { + await launchUrl(uri, mode: LaunchMode.externalApplication); + } else { + throw Exception('Could not launch URL'); + } + } + } catch (e) { + if (context.mounted) { + await showDialog( + context: context, + builder: (BuildContext context) { + return AlertWithOneAction( + alertTitle: 'MoonPay', + alertContent: 'The MoonPay service is currently unavailable: $e', + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(context).pop(), + ); + }, + ); + } } + } + + Future requestMoonPayUrl({ + required String walletAddress, + required SettingsStore settingsStore, + required bool isBuyAction, + required Map params, + String? amount, + }) async { + if (_apiKey.isNotEmpty) params['apiKey'] = _apiKey; + + final baseUrl = isBuyAction ? baseBuyUrl : baseSellUrl; + final originalUri = Uri.https(baseUrl, '', params); + + if (isTestEnvironment) return originalUri; final signature = await getMoonpaySignature('?${originalUri.query}'); final query = Map.from(originalUri.queryParameters); @@ -181,33 +296,6 @@ class MoonPayProvider extends BuyProvider { return signedUri; } - Future calculateAmount(String amount, String sourceCurrency) async { - final url = _apiUrl + - _currenciesSuffix + - '/$currencyCode' + - _quoteSuffix + - '/?apiKey=' + - _apiKey + - '&baseCurrencyAmount=' + - amount + - '&baseCurrencyCode=' + - sourceCurrency.toLowerCase(); - final uri = Uri.parse(url); - final response = await get(uri); - - if (response.statusCode != 200) { - throw BuyException(title: providerDescription, content: 'Quote is not found!'); - } - - final responseJSON = json.decode(response.body) as Map; - final sourceAmount = responseJSON['totalAmount'] as double; - final destAmount = responseJSON['quoteCurrencyAmount'] as double; - final minSourceAmount = responseJSON['baseCurrency']['minAmount'] as int; - - return BuyAmount( - sourceAmount: sourceAmount, destAmount: destAmount, minAmount: minSourceAmount); - } - Future findOrderById(String id) async { final url = _apiUrl + _transactionsSuffix + '/$id' + '?apiKey=' + _apiKey; final uri = Uri.parse(url); @@ -235,74 +323,83 @@ class MoonPayProvider extends BuyProvider { walletId: wallet.id); } - static Future onEnabled() async { - final url = _apiUrl + _ipAddressSuffix + '?apiKey=' + _apiKey; - var isBuyEnable = false; - final uri = Uri.parse(url); - final response = await get(uri); - - try { - final responseJSON = json.decode(response.body) as Map; - isBuyEnable = responseJSON['isBuyAllowed'] as bool; - } catch (e) { - isBuyEnable = false; - print(e.toString()); - } - - return isBuyEnable; - } - - @override - Future launchProvider(BuildContext context, bool? isBuyAction) async { - try { - late final Uri uri; - if (isBuyAction ?? true) { - uri = await requestBuyMoonPayUrl( - currency: wallet.currency, - walletAddress: wallet.walletAddresses.address, - settingsStore: _settingsStore, - ); - } else { - uri = await requestSellMoonPayUrl( - currency: wallet.currency, - refundWalletAddress: wallet.walletAddresses.address, - settingsStore: _settingsStore, - ); - } - - if (await canLaunchUrl(uri)) { - if (DeviceInfo.instance.isMobile) { - Navigator.of(context).pushNamed(Routes.webViewPage, arguments: ['MoonPay', uri]); - } else { - await launchUrl(uri, mode: LaunchMode.externalApplication); - } - } else { - throw Exception('Could not launch URL'); - } - } catch (e) { - if (context.mounted) { - await showDialog( - context: context, - builder: (BuildContext context) { - return AlertWithOneAction( - alertTitle: 'MoonPay', - alertContent: 'The MoonPay service is currently unavailable: $e', - buttonText: S.of(context).ok, - buttonAction: () => Navigator.of(context).pop(), - ); - }, - ); - } - } - } - String _normalizeCurrency(CryptoCurrency currency) { - if (currency == CryptoCurrency.maticpoly) { - return "POL_POLYGON"; - } else if (currency == CryptoCurrency.matic) { - return "POL"; + if (currency.tag == 'POLY') { + return '${currency.title.toLowerCase()}_polygon'; + } + + if (currency.tag == 'TRX') { + return '${currency.title.toLowerCase()}_trx'; } return currency.toString().toLowerCase(); } + + String? normalizePaymentMethod(PaymentType paymentMethod) { + switch (paymentMethod) { + case PaymentType.creditCard: + return 'credit_debit_card'; + case PaymentType.debitCard: + return 'credit_debit_card'; + case PaymentType.ach: + return 'ach_bank_transfer'; + case PaymentType.applePay: + return 'apple_pay'; + case PaymentType.googlePay: + return 'google_pay'; + case PaymentType.sepa: + return 'sepa_bank_transfer'; + case PaymentType.paypal: + return 'paypal'; + case PaymentType.sepaOpenBankingPayment: + return 'sepa_open_banking_payment'; + case PaymentType.gbpOpenBankingPayment: + return 'gbp_open_banking_payment'; + case PaymentType.lowCostAch: + return 'low_cost_ach'; + case PaymentType.mobileWallet: + return 'mobile_wallet'; + case PaymentType.pixInstantPayment: + return 'pix_instant_payment'; + case PaymentType.yellowCardBankTransfer: + return 'yellow_card_bank_transfer'; + case PaymentType.fiatBalance: + return 'fiat_balance'; + default: + return null; + } + } + + PaymentType _getPaymentTypeByString(String? paymentMethod) { + switch (paymentMethod) { + case 'ach_bank_transfer': + return PaymentType.ach; + case 'apple_pay': + return PaymentType.applePay; + case 'credit_debit_card': + return PaymentType.creditCard; + case 'fiat_balance': + return PaymentType.fiatBalance; + case 'gbp_open_banking_payment': + return PaymentType.gbpOpenBankingPayment; + case 'google_pay': + return PaymentType.googlePay; + case 'low_cost_ach': + return PaymentType.lowCostAch; + case 'mobile_wallet': + return PaymentType.mobileWallet; + case 'paypal': + return PaymentType.paypal; + case 'pix_instant_payment': + return PaymentType.pixInstantPayment; + case 'sepa_bank_transfer': + return PaymentType.sepa; + case 'sepa_open_banking_payment': + return PaymentType.sepaOpenBankingPayment; + case 'yellow_card_bank_transfer': + return PaymentType.yellowCardBankTransfer; + default: + return PaymentType.all; + } + } } diff --git a/lib/buy/onramper/onramper_buy_provider.dart b/lib/buy/onramper/onramper_buy_provider.dart index 1f1c86962..dade41d43 100644 --- a/lib/buy/onramper/onramper_buy_provider.dart +++ b/lib/buy/onramper/onramper_buy_provider.dart @@ -1,13 +1,19 @@ +import 'dart:convert'; +import 'dart:developer'; + import 'package:cake_wallet/.secrets.g.dart' as secrets; import 'package:cake_wallet/buy/buy_provider.dart'; +import 'package:cake_wallet/buy/buy_quote.dart'; +import 'package:cake_wallet/buy/payment_method.dart'; +import 'package:cake_wallet/entities/fiat_currency.dart'; import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; -import 'package:cake_wallet/utils/device_info.dart'; import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/currency.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; import 'package:url_launcher/url_launcher.dart'; class OnRamperBuyProvider extends BuyProvider { @@ -16,9 +22,15 @@ class OnRamperBuyProvider extends BuyProvider { : super(wallet: wallet, isTestEnvironment: isTestEnvironment, ledgerVM: null); static const _baseUrl = 'buy.onramper.com'; + static const _baseApiUrl = 'api.onramper.com'; + static const quotes = '/quotes'; + static const paymentTypes = '/payment-types'; + static const supported = '/supported'; final SettingsStore _settingsStore; + String get _apiKey => secrets.onramperApiKey; + @override String get title => 'Onramper'; @@ -31,74 +43,327 @@ class OnRamperBuyProvider extends BuyProvider { @override String get darkIcon => 'assets/images/onramper_dark.png'; - String get _apiKey => secrets.onramperApiKey; + @override + bool get isAggregator => true; - String get _normalizeCryptoCurrency { - switch (wallet.currency) { - case CryptoCurrency.ltc: - return "LTC_LITECOIN"; - case CryptoCurrency.xmr: - return "XMR_MONERO"; - case CryptoCurrency.bch: - return "BCH_BITCOINCASH"; - case CryptoCurrency.nano: - return "XNO_NANO"; - default: - return wallet.currency.title; + Future> getAvailablePaymentTypes( + String fiatCurrency, String cryptoCurrency, bool isBuyAction) async { + final params = { + 'fiatCurrency': fiatCurrency, + 'type': isBuyAction ? 'buy' : 'sell', + 'isRecurringPayment': 'false' + }; + + final url = Uri.https(_baseApiUrl, '$supported$paymentTypes/$fiatCurrency', params); + + try { + final response = + await http.get(url, headers: {'Authorization': _apiKey, 'accept': 'application/json'}); + + if (response.statusCode == 200) { + final Map data = jsonDecode(response.body) as Map; + final List message = data['message'] as List; + return message + .map((item) => PaymentMethod.fromOnramperJson(item as Map)) + .toList(); + } else { + print('Failed to fetch available payment types'); + return []; + } + } catch (e) { + print('Failed to fetch available payment types: $e'); + return []; } } - String getColorStr(Color color) { - return color.value.toRadixString(16).replaceAll(RegExp(r'^ff'), ""); + Future> getOnrampMetadata() async { + final url = Uri.https(_baseApiUrl, '$supported/onramps/all'); + + try { + final response = + await http.get(url, headers: {'Authorization': _apiKey, 'accept': 'application/json'}); + + if (response.statusCode == 200) { + final Map data = jsonDecode(response.body) as Map; + + final List onramps = data['message'] as List; + + final Map result = { + for (var onramp in onramps) + (onramp['id'] as String): { + 'displayName': onramp['displayName'] as String, + 'svg': onramp['icons']['svg'] as String + } + }; + + return result; + } else { + print('Failed to fetch onramp metadata'); + return {}; + } + } catch (e) { + print('Error occurred: $e'); + return {}; + } } - Uri requestOnramperUrl(BuildContext context, bool? isBuyAction) { - String primaryColor, - secondaryColor, - primaryTextColor, - secondaryTextColor, - containerColor, - cardColor; + @override + Future?> fetchQuote( + {required CryptoCurrency cryptoCurrency, + required FiatCurrency fiatCurrency, + required double amount, + required bool isBuyAction, + required String walletAddress, + PaymentType? paymentType, + String? countryCode}) async { + String? paymentMethod; - primaryColor = getColorStr(Theme.of(context).primaryColor); - secondaryColor = getColorStr(Theme.of(context).colorScheme.background); - primaryTextColor = - getColorStr(Theme.of(context).extension()!.titleColor); - secondaryTextColor = getColorStr( - Theme.of(context).extension()!.secondaryTextColor); - containerColor = getColorStr(Theme.of(context).colorScheme.background); - cardColor = getColorStr(Theme.of(context).cardColor); + if (paymentType != null && paymentType != PaymentType.all) { + paymentMethod = normalizePaymentMethod(paymentType); + if (paymentMethod == null) paymentMethod = paymentType.name; + } + + final actionType = isBuyAction ? 'buy' : 'sell'; + + final normalizedCryptoCurrency = _getNormalizeCryptoCurrency(cryptoCurrency); + + final params = { + 'amount': amount.toString(), + if (paymentMethod != null) 'paymentMethod': paymentMethod, + 'clientName': 'CakeWallet', + 'type': actionType, + 'walletAddress': walletAddress, + 'isRecurringPayment': 'false', + 'input': 'source', + }; + + log('Onramper: Fetching $actionType quote: ${isBuyAction ? normalizedCryptoCurrency : fiatCurrency.name} -> ${isBuyAction ? fiatCurrency.name : normalizedCryptoCurrency}, amount: $amount, paymentMethod: $paymentMethod'); + + final sourceCurrency = isBuyAction ? fiatCurrency.name : normalizedCryptoCurrency; + final destinationCurrency = isBuyAction ? normalizedCryptoCurrency : fiatCurrency.name; + + final url = Uri.https(_baseApiUrl, '$quotes/${sourceCurrency}/${destinationCurrency}', params); + final headers = {'Authorization': _apiKey, 'accept': 'application/json'}; + + try { + final response = await http.get(url, headers: headers); + + if (response.statusCode == 200) { + final data = jsonDecode(response.body) as List; + if (data.isEmpty) return null; + + List validQuotes = []; + + final onrampMetadata = await getOnrampMetadata(); + + for (var item in data) { + + if (item['errors'] != null) continue; + + final paymentMethod = (item as Map)['paymentMethod'] as String; + + final rampId = item['ramp'] as String?; + final rampMetaData = onrampMetadata[rampId] as Map?; + + if (rampMetaData == null) continue; + + final quote = Quote.fromOnramperJson( + item, isBuyAction, onrampMetadata, _getPaymentTypeByString(paymentMethod)); + quote.setFiatCurrency = fiatCurrency; + quote.setCryptoCurrency = cryptoCurrency; + validQuotes.add(quote); + } + + if (validQuotes.isEmpty) return null; + + return validQuotes; + } else { + print('Onramper: Failed to fetch rate'); + return null; + } + } catch (e) { + print('Onramper: Failed to fetch rate $e'); + return null; + } + } + + Future? launchProvider( + {required BuildContext context, + required Quote quote, + required double amount, + required bool isBuyAction, + required String cryptoCurrencyAddress, + String? countryCode}) async { + final actionType = isBuyAction ? 'buy' : 'sell'; + final prefix = actionType == 'sell' ? actionType + '_' : ''; + + final primaryColor = getColorStr(Theme.of(context).primaryColor); + final secondaryColor = getColorStr(Theme.of(context).colorScheme.background); + final primaryTextColor = getColorStr(Theme.of(context).extension()!.titleColor); + final secondaryTextColor = + getColorStr(Theme.of(context).extension()!.secondaryTextColor); + final containerColor = getColorStr(Theme.of(context).colorScheme.background); + var cardColor = getColorStr(Theme.of(context).cardColor); if (_settingsStore.currentTheme.title == S.current.high_contrast_theme) { cardColor = getColorStr(Colors.white); } - final networkName = - wallet.currency.fullName?.toUpperCase().replaceAll(" ", ""); + final defaultCrypto = _getNormalizeCryptoCurrency(quote.cryptoCurrency); - return Uri.https(_baseUrl, '', { + final paymentMethod = normalizePaymentMethod(quote.paymentType); + + final uri = Uri.https(_baseUrl, '', { 'apiKey': _apiKey, - 'defaultCrypto': _normalizeCryptoCurrency, - 'sell_defaultCrypto': _normalizeCryptoCurrency, - 'networkWallets': '${networkName}:${wallet.walletAddresses.address}', + 'mode': actionType, + '${prefix}defaultFiat': quote.fiatCurrency.name, + '${prefix}defaultCrypto': defaultCrypto, + '${prefix}defaultAmount': amount.toString(), + if (paymentMethod != null) '${prefix}defaultPaymentMethod': paymentMethod, + 'onlyOnramps': quote.rampId, + 'networkWallets': '$defaultCrypto:$cryptoCurrencyAddress', + 'walletAddress': cryptoCurrencyAddress, 'supportSwap': "false", 'primaryColor': primaryColor, 'secondaryColor': secondaryColor, + 'containerColor': containerColor, 'primaryTextColor': primaryTextColor, 'secondaryTextColor': secondaryTextColor, - 'containerColor': containerColor, 'cardColor': cardColor, - 'mode': isBuyAction == true ? 'buy' : 'sell', }); - } - Future launchProvider(BuildContext context, bool? isBuyAction) async { - final uri = requestOnramperUrl(context, isBuyAction); - if (DeviceInfo.instance.isMobile) { - Navigator.of(context) - .pushNamed(Routes.webViewPage, arguments: [title, uri]); + if (await canLaunchUrl(uri)) { + await launchUrl(uri, mode: LaunchMode.externalApplication); } else { - await launchUrl(uri); + throw Exception('Could not launch URL'); } } + + List mainCurrency = [ + CryptoCurrency.btc, + CryptoCurrency.eth, + CryptoCurrency.sol, + ]; + + String _tagToNetwork(String tag) { + switch (tag) { + case 'OMNI': + return tag; + case 'POL': + return 'POLYGON'; + default: + return CryptoCurrency.fromString(tag).fullName ?? tag; + } + } + + String _getNormalizeCryptoCurrency(Currency currency) { + if (currency is CryptoCurrency) { + if (!mainCurrency.contains(currency)) { + final network = currency.tag == null ? currency.fullName : _tagToNetwork(currency.tag!); + return '${currency.title}_${network?.replaceAll(' ', '')}'.toUpperCase(); + } + return currency.title.toUpperCase(); + } + return currency.name.toUpperCase(); + } + + String? normalizePaymentMethod(PaymentType paymentType) { + switch (paymentType) { + case PaymentType.bankTransfer: + return 'banktransfer'; + case PaymentType.creditCard: + return 'creditcard'; + case PaymentType.debitCard: + return 'debitcard'; + case PaymentType.applePay: + return 'applepay'; + case PaymentType.googlePay: + return 'googlepay'; + case PaymentType.revolutPay: + return 'revolutpay'; + case PaymentType.neteller: + return 'neteller'; + case PaymentType.skrill: + return 'skrill'; + case PaymentType.sepa: + return 'sepabanktransfer'; + case PaymentType.sepaInstant: + return 'sepainstant'; + case PaymentType.ach: + return 'ach'; + case PaymentType.achInstant: + return 'iach'; + case PaymentType.Khipu: + return 'khipu'; + case PaymentType.palomaBanktTansfer: + return 'palomabanktransfer'; + case PaymentType.ovo: + return 'ovo'; + case PaymentType.zaloPay: + return 'zalopay'; + case PaymentType.zaloBankTransfer: + return 'zalobanktransfer'; + case PaymentType.gcash: + return 'gcash'; + case PaymentType.imps: + return 'imps'; + case PaymentType.dana: + return 'dana'; + case PaymentType.ideal: + return 'ideal'; + default: + return null; + } + } + + PaymentType _getPaymentTypeByString(String paymentMethod) { + switch (paymentMethod.toLowerCase()) { + case 'banktransfer': + return PaymentType.bankTransfer; + case 'creditcard': + return PaymentType.creditCard; + case 'debitcard': + return PaymentType.debitCard; + case 'applepay': + return PaymentType.applePay; + case 'googlepay': + return PaymentType.googlePay; + case 'revolutpay': + return PaymentType.revolutPay; + case 'neteller': + return PaymentType.neteller; + case 'skrill': + return PaymentType.skrill; + case 'sepabanktransfer': + return PaymentType.sepa; + case 'sepainstant': + return PaymentType.sepaInstant; + case 'ach': + return PaymentType.ach; + case 'iach': + return PaymentType.achInstant; + case 'khipu': + return PaymentType.Khipu; + case 'palomabanktransfer': + return PaymentType.palomaBanktTansfer; + case 'ovo': + return PaymentType.ovo; + case 'zalopay': + return PaymentType.zaloPay; + case 'zalobanktransfer': + return PaymentType.zaloBankTransfer; + case 'gcash': + return PaymentType.gcash; + case 'imps': + return PaymentType.imps; + case 'dana': + return PaymentType.dana; + case 'ideal': + return PaymentType.ideal; + default: + return PaymentType.all; + } + } + + String getColorStr(Color color) => color.value.toRadixString(16).replaceAll(RegExp(r'^ff'), ""); } diff --git a/lib/buy/payment_method.dart b/lib/buy/payment_method.dart new file mode 100644 index 000000000..cf85c441b --- /dev/null +++ b/lib/buy/payment_method.dart @@ -0,0 +1,287 @@ +import 'dart:ui'; + +import 'package:cake_wallet/core/selectable_option.dart'; + +enum PaymentType { + all, + bankTransfer, + creditCard, + debitCard, + applePay, + googlePay, + revolutPay, + neteller, + skrill, + sepa, + sepaInstant, + ach, + achInstant, + Khipu, + palomaBanktTansfer, + ovo, + zaloPay, + zaloBankTransfer, + gcash, + imps, + dana, + ideal, + paypal, + sepaOpenBankingPayment, + gbpOpenBankingPayment, + lowCostAch, + mobileWallet, + pixInstantPayment, + yellowCardBankTransfer, + fiatBalance, + bancontact, +} + +extension PaymentTypeTitle on PaymentType { + String? get title { + switch (this) { + case PaymentType.all: + return 'All Payment Methods'; + case PaymentType.bankTransfer: + return 'Bank Transfer'; + case PaymentType.creditCard: + return 'Credit Card'; + case PaymentType.debitCard: + return 'Debit Card'; + case PaymentType.applePay: + return 'Apple Pay'; + case PaymentType.googlePay: + return 'Google Pay'; + case PaymentType.revolutPay: + return 'Revolut Pay'; + case PaymentType.neteller: + return 'Neteller'; + case PaymentType.skrill: + return 'Skrill'; + case PaymentType.sepa: + return 'SEPA'; + case PaymentType.sepaInstant: + return 'SEPA Instant'; + case PaymentType.ach: + return 'ACH'; + case PaymentType.achInstant: + return 'ACH Instant'; + case PaymentType.Khipu: + return 'Khipu'; + case PaymentType.palomaBanktTansfer: + return 'Paloma Bank Transfer'; + case PaymentType.ovo: + return 'OVO'; + case PaymentType.zaloPay: + return 'Zalo Pay'; + case PaymentType.zaloBankTransfer: + return 'Zalo Bank Transfer'; + case PaymentType.gcash: + return 'GCash'; + case PaymentType.imps: + return 'IMPS'; + case PaymentType.dana: + return 'DANA'; + case PaymentType.ideal: + return 'iDEAL'; + case PaymentType.paypal: + return 'PayPal'; + case PaymentType.sepaOpenBankingPayment: + return 'SEPA Open Banking Payment'; + case PaymentType.gbpOpenBankingPayment: + return 'GBP Open Banking Payment'; + case PaymentType.lowCostAch: + return 'Low Cost ACH'; + case PaymentType.mobileWallet: + return 'Mobile Wallet'; + case PaymentType.pixInstantPayment: + return 'PIX Instant Payment'; + case PaymentType.yellowCardBankTransfer: + return 'Yellow Card Bank Transfer'; + case PaymentType.fiatBalance: + return 'Fiat Balance'; + case PaymentType.bancontact: + return 'Bancontact'; + default: + return null; + } + } + + String? get lightIconPath { + switch (this) { + case PaymentType.all: + return 'assets/images/usd_round_light.svg'; + case PaymentType.creditCard: + case PaymentType.debitCard: + case PaymentType.yellowCardBankTransfer: + return 'assets/images/card.svg'; + case PaymentType.bankTransfer: + return 'assets/images/bank_light.svg'; + case PaymentType.skrill: + return 'assets/images/skrill.svg'; + case PaymentType.applePay: + return 'assets/images/apple_pay_round_light.svg'; + default: + return null; + } + } + + String? get darkIconPath { + switch (this) { + case PaymentType.all: + return 'assets/images/usd_round_dark.svg'; + case PaymentType.creditCard: + case PaymentType.debitCard: + case PaymentType.yellowCardBankTransfer: + return 'assets/images/card_dark.svg'; + case PaymentType.bankTransfer: + return 'assets/images/bank_dark.svg'; + case PaymentType.skrill: + return 'assets/images/skrill.svg'; + case PaymentType.applePay: + return 'assets/images/apple_pay_round_dark.svg'; + default: + return null; + } + } + + String? get description { + switch (this) { + default: + return null; + } + } +} + +class PaymentMethod extends SelectableOption { + PaymentMethod({ + required this.paymentMethodType, + required this.customTitle, + required this.customIconPath, + this.customDescription, + }) : super(title: paymentMethodType.title ?? customTitle); + + final PaymentType paymentMethodType; + final String customTitle; + final String customIconPath; + final String? customDescription; + bool isSelected = false; + + @override + String? get description => paymentMethodType.description ?? customDescription; + + @override + String get lightIconPath => paymentMethodType.lightIconPath ?? customIconPath; + + @override + String get darkIconPath => paymentMethodType.darkIconPath ?? customIconPath; + + @override + bool get isOptionSelected => isSelected; + + factory PaymentMethod.all() { + return PaymentMethod( + paymentMethodType: PaymentType.all, + customTitle: 'All Payment Methods', + customIconPath: 'assets/images/dollar_coin.svg'); + } + + factory PaymentMethod.fromOnramperJson(Map json) { + final type = PaymentMethod.getPaymentTypeId(json['paymentTypeId'] as String?); + return PaymentMethod( + paymentMethodType: type, + customTitle: json['name'] as String? ?? 'Unknown', + customIconPath: json['icon'] as String? ?? 'assets/images/card.png', + customDescription: json['description'] as String?); + } + + factory PaymentMethod.fromDFX(String paymentMethod, PaymentType paymentType) { + return PaymentMethod( + paymentMethodType: paymentType, + customTitle: paymentMethod, + customIconPath: 'assets/images/card.png'); + } + + factory PaymentMethod.fromMoonPayJson(Map json, PaymentType paymentType) { + return PaymentMethod( + paymentMethodType: paymentType, + customTitle: json['paymentMethod'] as String, + customIconPath: 'assets/images/card.png'); + } + + factory PaymentMethod.fromMeldJson(Map json) { + final type = PaymentMethod.getPaymentTypeId(json['paymentMethod'] as String?); + final logos = json['logos'] as Map; + return PaymentMethod( + paymentMethodType: type, + customTitle: json['name'] as String? ?? 'Unknown', + customIconPath: logos['dark'] as String? ?? 'assets/images/card.png', + customDescription: json['description'] as String?); + } + + static PaymentType getPaymentTypeId(String? type) { + switch (type?.toLowerCase()) { + case 'banktransfer': + case 'bank': + case 'yellow_card_bank_transfer': + return PaymentType.bankTransfer; + case 'creditcard': + case 'card': + case 'credit_debit_card': + return PaymentType.creditCard; + case 'debitcard': + return PaymentType.debitCard; + case 'applepay': + case 'apple_pay': + return PaymentType.applePay; + case 'googlepay': + case 'google_pay': + return PaymentType.googlePay; + case 'revolutpay': + return PaymentType.revolutPay; + case 'neteller': + return PaymentType.neteller; + case 'skrill': + return PaymentType.skrill; + case 'sepabanktransfer': + case 'sepa': + case 'sepa_bank_transfer': + return PaymentType.sepa; + case 'sepainstant': + case 'sepa_instant': + return PaymentType.sepaInstant; + case 'ach': + case 'ach_bank_transfer': + return PaymentType.ach; + case 'iach': + case 'instant_ach': + return PaymentType.achInstant; + case 'khipu': + return PaymentType.Khipu; + case 'palomabanktransfer': + return PaymentType.palomaBanktTansfer; + case 'ovo': + return PaymentType.ovo; + case 'zalopay': + return PaymentType.zaloPay; + case 'zalobanktransfer': + case 'za_bank_transfer': + return PaymentType.zaloBankTransfer; + case 'gcash': + return PaymentType.gcash; + case 'imps': + return PaymentType.imps; + case 'dana': + return PaymentType.dana; + case 'ideal': + return PaymentType.ideal; + case 'paypal': + return PaymentType.paypal; + case 'sepa_open_banking_payment': + return PaymentType.sepaOpenBankingPayment; + case 'bancontact': + return PaymentType.bancontact; + default: + return PaymentType.all; + } + } +} diff --git a/lib/buy/robinhood/robinhood_buy_provider.dart b/lib/buy/robinhood/robinhood_buy_provider.dart index 2d809772e..e8de5a59c 100644 --- a/lib/buy/robinhood/robinhood_buy_provider.dart +++ b/lib/buy/robinhood/robinhood_buy_provider.dart @@ -1,13 +1,18 @@ import 'dart:convert'; +import 'dart:developer'; import 'package:cake_wallet/.secrets.g.dart' as secrets; import 'package:cake_wallet/buy/buy_provider.dart'; +import 'package:cake_wallet/buy/buy_quote.dart'; +import 'package:cake_wallet/buy/payment_method.dart'; +import 'package:cake_wallet/entities/fiat_currency.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/connect_device/connect_device_page.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart'; +import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; @@ -15,7 +20,8 @@ import 'package:http/http.dart' as http; import 'package:url_launcher/url_launcher.dart'; class RobinhoodBuyProvider extends BuyProvider { - RobinhoodBuyProvider({required WalletBase wallet, bool isTestEnvironment = false, LedgerViewModel? ledgerVM}) + RobinhoodBuyProvider( + {required WalletBase wallet, bool isTestEnvironment = false, LedgerViewModel? ledgerVM}) : super(wallet: wallet, isTestEnvironment: isTestEnvironment, ledgerVM: ledgerVM); static const _baseUrl = 'applink.robinhood.com'; @@ -33,6 +39,9 @@ class RobinhoodBuyProvider extends BuyProvider { @override String get darkIcon => 'assets/images/robinhood_dark.png'; + @override + bool get isAggregator => false; + String get _applicationId => secrets.robinhoodApplicationId; String get _apiSecret => secrets.exchangeHelperApiKey; @@ -86,7 +95,13 @@ class RobinhoodBuyProvider extends BuyProvider { }); } - Future launchProvider(BuildContext context, bool? isBuyAction) async { + Future? launchProvider( + {required BuildContext context, + required Quote quote, + required double amount, + required bool isBuyAction, + required String cryptoCurrencyAddress, + String? countryCode}) async { if (wallet.isHardwareWallet) { if (!ledgerVM!.isConnected) { await Navigator.of(context).pushNamed(Routes.connectDevices, @@ -116,4 +131,87 @@ class RobinhoodBuyProvider extends BuyProvider { }); } } + + @override + Future?> fetchQuote( + {required CryptoCurrency cryptoCurrency, + required FiatCurrency fiatCurrency, + required double amount, + required bool isBuyAction, + required String walletAddress, + PaymentType? paymentType, + String? countryCode}) async { + String? paymentMethod; + + if (paymentType != null && paymentType != PaymentType.all) { + paymentMethod = normalizePaymentMethod(paymentType); + if (paymentMethod == null) paymentMethod = paymentType.name; + } + + final action = isBuyAction ? 'buy' : 'sell'; + log('Robinhood: Fetching $action quote: ${isBuyAction ? cryptoCurrency.title : fiatCurrency.name.toUpperCase()} -> ${isBuyAction ? fiatCurrency.name.toUpperCase() : cryptoCurrency.title}, amount: $amount paymentMethod: $paymentMethod'); + + final queryParams = { + 'applicationId': _applicationId, + 'fiatCode': fiatCurrency.name, + 'assetCode': cryptoCurrency.title, + 'fiatAmount': amount.toString(), + if (paymentMethod != null) 'paymentMethod': paymentMethod, + }; + + final uri = + Uri.https('api.robinhood.com', '/catpay/v1/${cryptoCurrency.title}/quote/', queryParams); + + try { + final response = await http.get(uri, headers: {'accept': 'application/json'}); + final responseData = jsonDecode(response.body) as Map; + + if (response.statusCode == 200) { + final paymentType = _getPaymentTypeByString(responseData['paymentMethod'] as String?); + final quote = Quote.fromRobinhoodJson(responseData, isBuyAction, paymentType); + quote.setFiatCurrency = fiatCurrency; + quote.setCryptoCurrency = cryptoCurrency; + return [quote]; + } else { + if (responseData.containsKey('message')) { + log('Robinhood Error: ${responseData['message']}'); + } else { + print('Robinhood Failed to fetch $action quote: ${response.statusCode}'); + } + return null; + } + } catch (e) { + log('Robinhood: Failed to fetch $action quote: $e'); + return null; + } + + // ● buying_power + // ● crypto_balance + // ● debit_card + // ● bank_transfer + } + + String? normalizePaymentMethod(PaymentType paymentMethod) { + switch (paymentMethod) { + case PaymentType.creditCard: + return 'debit_card'; + case PaymentType.debitCard: + return 'debit_card'; + case PaymentType.bankTransfer: + return 'bank_transfer'; + default: + return null; + } + } + + PaymentType _getPaymentTypeByString(String? paymentMethod) { + switch (paymentMethod) { + case 'debit_card': + return PaymentType.debitCard; + case 'bank_transfer': + return PaymentType.bankTransfer; + default: + return PaymentType.all; + } + } } diff --git a/lib/buy/sell_buy_states.dart b/lib/buy/sell_buy_states.dart new file mode 100644 index 000000000..26ea20205 --- /dev/null +++ b/lib/buy/sell_buy_states.dart @@ -0,0 +1,20 @@ +abstract class PaymentMethodLoadingState {} + +class InitialPaymentMethod extends PaymentMethodLoadingState {} + +class PaymentMethodLoading extends PaymentMethodLoadingState {} + +class PaymentMethodLoaded extends PaymentMethodLoadingState {} + +class PaymentMethodFailed extends PaymentMethodLoadingState {} + + +abstract class BuySellQuotLoadingState {} + +class InitialBuySellQuotState extends BuySellQuotLoadingState {} + +class BuySellQuotLoading extends BuySellQuotLoadingState {} + +class BuySellQuotLoaded extends BuySellQuotLoadingState {} + +class BuySellQuotFailed extends BuySellQuotLoadingState {} \ No newline at end of file diff --git a/lib/buy/wyre/wyre_buy_provider.dart b/lib/buy/wyre/wyre_buy_provider.dart index e09186ad5..78b109ac0 100644 --- a/lib/buy/wyre/wyre_buy_provider.dart +++ b/lib/buy/wyre/wyre_buy_provider.dart @@ -42,6 +42,9 @@ class WyreBuyProvider extends BuyProvider { @override String get darkIcon => 'assets/images/robinhood_dark.png'; + @override + bool get isAggregator => false; + String get trackUrl => isTestEnvironment ? _trackTestUrl : _trackProductUrl; String baseApiUrl; @@ -148,10 +151,4 @@ class WyreBuyProvider extends BuyProvider { receiveAddress: wallet.walletAddresses.address, walletId: wallet.id); } - - @override - Future launchProvider(BuildContext context, bool? isBuyAction) { - // TODO: implement launchProvider - throw UnimplementedError(); - } } diff --git a/lib/core/backup_service.dart b/lib/core/backup_service.dart index d65530eb5..992ed6288 100644 --- a/lib/core/backup_service.dart +++ b/lib/core/backup_service.dart @@ -268,9 +268,7 @@ class BackupService { final currentFiatCurrency = data[PreferencesKey.currentFiatCurrencyKey] as String?; final shouldSaveRecipientAddress = data[PreferencesKey.shouldSaveRecipientAddressKey] as bool?; final isAppSecure = data[PreferencesKey.isAppSecureKey] as bool?; - final disableBuy = data[PreferencesKey.disableBuyKey] as bool?; - final disableSell = data[PreferencesKey.disableSellKey] as bool?; - final defaultBuyProvider = data[PreferencesKey.defaultBuyProvider] as int?; + final disableTradeOption = data[PreferencesKey.disableTradeOption] as bool?; final currentTransactionPriorityKeyLegacy = data[PreferencesKey.currentTransactionPriorityKeyLegacy] as int?; final currentBitcoinElectrumSererId = @@ -323,14 +321,8 @@ class BackupService { if (isAppSecure != null) await _sharedPreferences.setBool(PreferencesKey.isAppSecureKey, isAppSecure); - if (disableBuy != null) - await _sharedPreferences.setBool(PreferencesKey.disableBuyKey, disableBuy); - - if (disableSell != null) - await _sharedPreferences.setBool(PreferencesKey.disableSellKey, disableSell); - - if (defaultBuyProvider != null) - await _sharedPreferences.setInt(PreferencesKey.defaultBuyProvider, defaultBuyProvider); + if (disableTradeOption != null) + await _sharedPreferences.setBool(PreferencesKey.disableTradeOption, disableTradeOption); if (currentTransactionPriorityKeyLegacy != null) await _sharedPreferences.setInt( @@ -516,10 +508,7 @@ class BackupService { _sharedPreferences.getString(PreferencesKey.currentFiatCurrencyKey), PreferencesKey.shouldSaveRecipientAddressKey: _sharedPreferences.getBool(PreferencesKey.shouldSaveRecipientAddressKey), - PreferencesKey.disableBuyKey: _sharedPreferences.getBool(PreferencesKey.disableBuyKey), - PreferencesKey.disableSellKey: _sharedPreferences.getBool(PreferencesKey.disableSellKey), - PreferencesKey.defaultBuyProvider: - _sharedPreferences.getInt(PreferencesKey.defaultBuyProvider), + PreferencesKey.disableTradeOption: _sharedPreferences.getBool(PreferencesKey.disableTradeOption), PreferencesKey.currentPinLength: _sharedPreferences.getInt(PreferencesKey.currentPinLength), PreferencesKey.currentTransactionPriorityKeyLegacy: _sharedPreferences.getInt(PreferencesKey.currentTransactionPriorityKeyLegacy), diff --git a/lib/core/selectable_option.dart b/lib/core/selectable_option.dart new file mode 100644 index 000000000..0d7511c95 --- /dev/null +++ b/lib/core/selectable_option.dart @@ -0,0 +1,47 @@ +abstract class SelectableItem { + SelectableItem({required this.title}); + final String title; +} + +class OptionTitle extends SelectableItem { + OptionTitle({required String title}) : super(title: title); + +} + +abstract class SelectableOption extends SelectableItem { + SelectableOption({required String title}) : super(title: title); + + String get lightIconPath; + + String get darkIconPath; + + String? get description => null; + + String? get topLeftSubTitle => null; + + String? get topLeftSubTitleIconPath => null; + + String? get topRightSubTitle => null; + + String? get topRightSubTitleLightIconPath => null; + + String? get topRightSubTitleDarkIconPath => null; + + String? get bottomLeftSubTitle => null; + + String? get bottomLeftSubTitleIconPath => null; + + String? get bottomRightSubTitle => null; + + String? get bottomRightSubTitleLightIconPath => null; + + String? get bottomRightSubTitleDarkIconPath => null; + + List get badges => []; + + bool get isOptionSelected => false; + + set isOptionSelected(bool isSelected) => false; +} + + diff --git a/lib/core/wallet_loading_service.dart b/lib/core/wallet_loading_service.dart index e58e14652..10e438e0c 100644 --- a/lib/core/wallet_loading_service.dart +++ b/lib/core/wallet_loading_service.dart @@ -14,7 +14,11 @@ import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; class WalletLoadingService { - WalletLoadingService(this.sharedPreferences, this.keyService, this.walletServiceFactory); + WalletLoadingService( + this.sharedPreferences, + this.keyService, + this.walletServiceFactory, + ); final SharedPreferences sharedPreferences; final KeyService keyService; @@ -77,7 +81,8 @@ class WalletLoadingService { await updateMoneroWalletPassword(wallet); } - await sharedPreferences.setString(PreferencesKey.currentWalletName, wallet.name); + await sharedPreferences.setString( + PreferencesKey.currentWalletName, wallet.name); await sharedPreferences.setInt( PreferencesKey.currentWalletType, serializeToInt(wallet.type)); @@ -129,4 +134,9 @@ class WalletLoadingService { return "\n\n$type ($name): ${await walletService.getSeeds(name, password, type)}"; } + + bool requireHardwareWalletConnection(WalletType type, String name) { + final walletService = walletServiceFactory.call(type); + return walletService.requireHardwareWalletConnection(name); + } } diff --git a/lib/di.dart b/lib/di.dart index 540a546fd..facdec912 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -19,6 +19,7 @@ import 'package:cake_wallet/core/backup_service.dart'; import 'package:cake_wallet/core/key_service.dart'; import 'package:cake_wallet/core/new_wallet_type_arguments.dart'; import 'package:cake_wallet/core/secure_storage.dart'; +import 'package:cake_wallet/core/selectable_option.dart'; import 'package:cake_wallet/core/totp_request_details.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'; @@ -31,10 +32,14 @@ import 'package:cake_wallet/entities/biometric_auth.dart'; import 'package:cake_wallet/entities/contact.dart'; import 'package:cake_wallet/entities/contact_record.dart'; import 'package:cake_wallet/entities/exchange_api_mode.dart'; +import 'package:cake_wallet/entities/hardware_wallet/require_hardware_wallet_connection.dart'; import 'package:cake_wallet/entities/parse_address_from_domain.dart'; import 'package:cake_wallet/entities/wallet_edit_page_arguments.dart'; import 'package:cake_wallet/entities/wallet_manager.dart'; +import 'package:cake_wallet/src/screens/buy/buy_sell_options_page.dart'; +import 'package:cake_wallet/src/screens/buy/payment_method_options_page.dart'; import 'package:cake_wallet/src/screens/receive/address_list_page.dart'; +import 'package:cake_wallet/src/screens/wallet_list/wallet_list_page.dart'; import 'package:cake_wallet/src/screens/settings/mweb_logs_page.dart'; import 'package:cake_wallet/src/screens/settings/mweb_node_page.dart'; import 'package:cake_wallet/view_model/link_view_model.dart'; @@ -61,7 +66,6 @@ import 'package:cake_wallet/src/screens/anonpay_details/anonpay_details_page.dar import 'package:cake_wallet/src/screens/auth/auth_page.dart'; import 'package:cake_wallet/src/screens/backup/backup_page.dart'; import 'package:cake_wallet/src/screens/backup/edit_backup_password_page.dart'; -import 'package:cake_wallet/src/screens/buy/buy_options_page.dart'; import 'package:cake_wallet/src/screens/buy/buy_webview_page.dart'; import 'package:cake_wallet/src/screens/buy/webview_page.dart'; import 'package:cake_wallet/src/screens/contact/contact_list_page.dart'; @@ -125,6 +129,7 @@ import 'package:cake_wallet/src/screens/subaddress/address_edit_or_create_page.d import 'package:cake_wallet/src/screens/support/support_page.dart'; import 'package:cake_wallet/src/screens/support_chat/support_chat_page.dart'; import 'package:cake_wallet/src/screens/support_other_links/support_other_links_page.dart'; +import 'package:cake_wallet/src/screens/ur/animated_ur_page.dart'; import 'package:cake_wallet/src/screens/wallet/wallet_edit_page.dart'; import 'package:cake_wallet/src/screens/wallet_connect/wc_connections_listing_view.dart'; import 'package:cake_wallet/src/screens/wallet_unlock/wallet_unlock_arguments.dart'; @@ -134,6 +139,8 @@ import 'package:cake_wallet/utils/device_info.dart'; import 'package:cake_wallet/store/anonpay/anonpay_transactions_store.dart'; import 'package:cake_wallet/utils/payment_request.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; +import 'package:cake_wallet/view_model/buy/buy_sell_view_model.dart'; +import 'package:cake_wallet/view_model/animated_ur_model.dart'; import 'package:cake_wallet/view_model/dashboard/desktop_sidebar_view_model.dart'; import 'package:cake_wallet/view_model/anon_invoice_page_view_model.dart'; import 'package:cake_wallet/view_model/anonpay_details_view_model.dart'; @@ -179,7 +186,6 @@ import 'package:cake_wallet/src/screens/transaction_details/transaction_details_ import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_details_page.dart'; import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_list_page.dart'; import 'package:cake_wallet/src/screens/wallet_keys/wallet_keys_page.dart'; -import 'package:cake_wallet/src/screens/wallet_list/wallet_list_page.dart'; import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/store/authentication_store.dart'; import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; @@ -246,6 +252,8 @@ import 'package:get_it/get_it.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'buy/meld/meld_buy_provider.dart'; +import 'src/screens/buy/buy_sell_page.dart'; import 'cake_pay/cake_pay_payment_credantials.dart'; final getIt = GetIt.instance; @@ -579,7 +587,7 @@ Future setup({ ); } else { // wallet is already loaded: - if (appStore.wallet != null) { + if (appStore.wallet != null || requireHardwareWalletConnection()) { // goes to the dashboard: authStore.allowed(); // trigger any deep links: @@ -773,10 +781,12 @@ Future setup({ ); } - getIt.registerFactory(() => WalletListPage( - walletListViewModel: getIt.get(), - authService: getIt.get(), - )); + getIt.registerFactoryParam( + (Function(BuildContext)? onWalletLoaded, _) => WalletListPage( + walletListViewModel: getIt.get(), + authService: getIt.get(), + onWalletLoaded: onWalletLoaded, + )); getIt.registerFactoryParam( (WalletListViewModel walletListViewModel, _) => WalletEditViewModel( @@ -903,6 +913,11 @@ Future setup({ getIt.registerFactory(() => WalletKeysViewModel(getIt.get())); getIt.registerFactory(() => WalletKeysPage(getIt.get())); + + getIt.registerFactory(() => AnimatedURModel(getIt.get())); + + getIt.registerFactoryParam((String urQr, _) => + AnimatedURPage(getIt.get(), urQr: urQr)); getIt.registerFactoryParam( (ContactRecord? contact, _) => ContactViewModel(_contactSource, contact: contact)); @@ -997,6 +1012,10 @@ Future setup({ wallet: getIt.get().wallet!, )); + getIt.registerFactory(() => MeldBuyProvider( + wallet: getIt.get().wallet!, + )); + getIt.registerFactoryParam((title, uri) => WebViewPage(title, uri)); getIt.registerFactory(() => PayfuraBuyProvider( @@ -1186,8 +1205,25 @@ Future setup({ getIt.registerFactory(() => BuyAmountViewModel()); - getIt.registerFactoryParam( - (isBuyOption, _) => BuySellOptionsPage(getIt.get(), isBuyOption)); + getIt.registerFactory(() => BuySellViewModel(getIt.get())); + + getIt.registerFactory(() => BuySellPage(getIt.get())); + + getIt.registerFactoryParam, void>((List args, _) { + final items = args.first as List; + final pickAnOption = args[1] as void Function(SelectableOption option)?; + final confirmOption = args[2] as void Function(BuildContext contex)?; + return BuyOptionsPage( + items: items, pickAnOption: pickAnOption, confirmOption: confirmOption); + }); + + getIt.registerFactoryParam, void>((List args, _) { + final items = args.first as List; + final pickAnOption = args[1] as void Function(SelectableOption option)?; + + return PaymentMethodOptionsPage( + items: items, pickAnOption: pickAnOption); + }); getIt.registerFactory(() { final wallet = getIt.get().wallet; diff --git a/lib/entities/default_settings_migration.dart b/lib/entities/default_settings_migration.dart index af6f1ce78..f873314c0 100644 --- a/lib/entities/default_settings_migration.dart +++ b/lib/entities/default_settings_migration.dart @@ -252,13 +252,19 @@ Future defaultSettingsMigration( await removeMoneroWorld(sharedPreferences: sharedPreferences, nodes: nodes); break; case 41: - _deselectQuantex(sharedPreferences); + _deselectExchangeProvider(sharedPreferences, "Quantex"); await _addSethNode(nodes, sharedPreferences); await updateTronNodesWithNowNodes(sharedPreferences: sharedPreferences, nodes: nodes); break; case 42: updateBtcElectrumNodeToUseSSL(nodes, sharedPreferences); break; + case 43: + _updateCakeXmrNode(nodes); + _deselectExchangeProvider(sharedPreferences, "THORChain"); + _deselectExchangeProvider(sharedPreferences, "SimpleSwap"); + break; + default: break; } @@ -273,6 +279,15 @@ Future defaultSettingsMigration( await sharedPreferences.setInt(PreferencesKey.currentDefaultSettingsMigrationVersion, version); } +void _updateCakeXmrNode(Box nodes) { + final node = nodes.values.firstWhereOrNull((element) => element.uriRaw == newCakeWalletMoneroUri); + + if (node != null && !node.trusted) { + node.trusted = true; + node.save(); + } +} + void updateBtcElectrumNodeToUseSSL(Box nodes, SharedPreferences sharedPreferences) { final btcElectrumNode = nodes.values.firstWhereOrNull((element) => element.uriRaw == newCakeWalletBitcoinUri); @@ -282,12 +297,12 @@ void updateBtcElectrumNodeToUseSSL(Box nodes, SharedPreferences sharedPref } } -void _deselectQuantex(SharedPreferences sharedPreferences) { +void _deselectExchangeProvider(SharedPreferences sharedPreferences, String providerName) { final Map exchangeProvidersSelection = json.decode(sharedPreferences.getString(PreferencesKey.exchangeProvidersSelection) ?? "{}") as Map; - exchangeProvidersSelection['Quantex'] = false; + exchangeProvidersSelection[providerName] = false; sharedPreferences.setString( PreferencesKey.exchangeProvidersSelection, @@ -843,7 +858,7 @@ Future changeDefaultMoneroNode( } }); - final newCakeWalletNode = Node(uri: newCakeWalletMoneroUri, type: WalletType.monero); + final newCakeWalletNode = Node(uri: newCakeWalletMoneroUri, type: WalletType.monero, trusted: true); await nodeSource.add(newCakeWalletNode); diff --git a/lib/entities/hardware_wallet/require_hardware_wallet_connection.dart b/lib/entities/hardware_wallet/require_hardware_wallet_connection.dart new file mode 100644 index 000000000..a64fa5873 --- /dev/null +++ b/lib/entities/hardware_wallet/require_hardware_wallet_connection.dart @@ -0,0 +1,25 @@ +import 'package:cake_wallet/core/wallet_loading_service.dart'; +import 'package:cake_wallet/di.dart'; +import 'package:cake_wallet/entities/preferences_key.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +bool requireHardwareWalletConnection() { + final name = getIt + .get() + .getString(PreferencesKey.currentWalletName); + final typeRaw = + getIt.get().getInt(PreferencesKey.currentWalletType); + + if (typeRaw == null) { + return false; + } + + if (name == null) { + throw Exception('Incorrect current wallet name: $name'); + } + + final type = deserializeFromInt(typeRaw); + final walletLoadingService = getIt.get(); + return walletLoadingService.requireHardwareWalletConnection(type, name); +} diff --git a/lib/entities/main_actions.dart b/lib/entities/main_actions.dart index c1dd71cc9..94be0d2b7 100644 --- a/lib/entities/main_actions.dart +++ b/lib/entities/main_actions.dart @@ -23,31 +23,18 @@ class MainActions { }); static List all = [ - buyAction, + showWalletsAction, receiveAction, exchangeAction, sendAction, - sellAction, + tradeAction, ]; - static MainActions buyAction = MainActions._( - name: (context) => S.of(context).buy, - image: 'assets/images/buy.png', - isEnabled: (viewModel) => viewModel.isEnabledBuyAction, - canShow: (viewModel) => viewModel.hasBuyAction, + static MainActions showWalletsAction = MainActions._( + name: (context) => S.of(context).wallets, + image: 'assets/images/wallet_new.png', onTap: (BuildContext context, DashboardViewModel viewModel) async { - if (!viewModel.isEnabledBuyAction) { - return; - } - - final defaultBuyProvider = viewModel.defaultBuyProvider; - try { - defaultBuyProvider != null - ? await defaultBuyProvider.launchProvider(context, true) - : await Navigator.of(context).pushNamed(Routes.buySellPage, arguments: true); - } catch (e) { - await _showErrorDialog(context, defaultBuyProvider.toString(), e.toString()); - } + Navigator.pushNamed(context, Routes.walletList); }, ); @@ -79,39 +66,15 @@ class MainActions { }, ); - static MainActions sellAction = MainActions._( - name: (context) => S.of(context).sell, - image: 'assets/images/sell.png', - isEnabled: (viewModel) => viewModel.isEnabledSellAction, - canShow: (viewModel) => viewModel.hasSellAction, - onTap: (BuildContext context, DashboardViewModel viewModel) async { - if (!viewModel.isEnabledSellAction) { - return; - } - final defaultSellProvider = viewModel.defaultSellProvider; - try { - defaultSellProvider != null - ? await defaultSellProvider.launchProvider(context, false) - : await Navigator.of(context).pushNamed(Routes.buySellPage, arguments: false); - } catch (e) { - await _showErrorDialog(context, defaultSellProvider.toString(), e.toString()); - } + static MainActions tradeAction = MainActions._( + name: (context) => '${S.of(context).buy} / ${S.of(context).sell}', + image: 'assets/images/buy_sell.png', + isEnabled: (viewModel) => viewModel.isEnabledTradeAction, + canShow: (viewModel) => viewModel.hasTradeAction, + onTap: (BuildContext context, DashboardViewModel viewModel) async { + if (!viewModel.isEnabledTradeAction) return; + await Navigator.of(context).pushNamed(Routes.buySellPage, arguments: false); }, ); - - static Future _showErrorDialog( - BuildContext context, String title, String errorMessage) async { - await showPopUp( - context: context, - builder: (BuildContext context) { - return AlertWithOneAction( - alertTitle: title, - alertContent: errorMessage, - buttonText: S.of(context).ok, - buttonAction: () => Navigator.of(context).pop(), - ); - }, - ); - } } \ No newline at end of file diff --git a/lib/entities/parse_address_from_domain.dart b/lib/entities/parse_address_from_domain.dart index 481db5620..11ba4724c 100644 --- a/lib/entities/parse_address_from_domain.dart +++ b/lib/entities/parse_address_from_domain.dart @@ -41,7 +41,8 @@ class AddressResolver { 'kresus', 'anime', 'manga', - 'binanceus' + 'binanceus', + 'xmr', ]; static String? extractAddressByType({required String raw, required CryptoCurrency type}) { diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index 0bb526e5d..5ed7a7ed6 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -21,10 +21,8 @@ class PreferencesKey { static const currentBalanceDisplayModeKey = 'current_balance_display_mode'; static const shouldSaveRecipientAddressKey = 'save_recipient_address'; static const isAppSecureKey = 'is_app_secure'; - static const disableBuyKey = 'disable_buy'; - static const disableSellKey = 'disable_sell'; + static const disableTradeOption = 'disable_buy'; static const disableBulletinKey = 'disable_bulletin'; - static const defaultBuyProvider = 'default_buy_provider'; static const walletListOrder = 'wallet_list_order'; static const contactListOrder = 'contact_list_order'; static const walletListAscending = 'wallet_list_ascending'; diff --git a/lib/entities/provider_types.dart b/lib/entities/provider_types.dart index b9dd4ef2a..42ec74c12 100644 --- a/lib/entities/provider_types.dart +++ b/lib/entities/provider_types.dart @@ -1,24 +1,18 @@ import 'package:cake_wallet/buy/buy_provider.dart'; import 'package:cake_wallet/buy/dfx/dfx_buy_provider.dart'; +import 'package:cake_wallet/buy/meld/meld_buy_provider.dart'; import 'package:cake_wallet/buy/moonpay/moonpay_provider.dart'; import 'package:cake_wallet/buy/onramper/onramper_buy_provider.dart'; import 'package:cake_wallet/buy/robinhood/robinhood_buy_provider.dart'; import 'package:cake_wallet/di.dart'; import 'package:cw_core/wallet_type.dart'; +import 'package:http/http.dart'; -enum ProviderType { - askEachTime, - robinhood, - dfx, - onramper, - moonpay, -} +enum ProviderType { robinhood, dfx, onramper, moonpay, meld } extension ProviderTypeName on ProviderType { String get title { switch (this) { - case ProviderType.askEachTime: - return 'Ask each time'; case ProviderType.robinhood: return 'Robinhood Connect'; case ProviderType.dfx: @@ -27,13 +21,13 @@ extension ProviderTypeName on ProviderType { return 'Onramper'; case ProviderType.moonpay: return 'MoonPay'; + case ProviderType.meld: + return 'Meld'; } } String get id { switch (this) { - case ProviderType.askEachTime: - return 'ask_each_time_provider'; case ProviderType.robinhood: return 'robinhood_connect_provider'; case ProviderType.dfx: @@ -42,6 +36,8 @@ extension ProviderTypeName on ProviderType { return 'onramper_provider'; case ProviderType.moonpay: return 'moonpay_provider'; + case ProviderType.meld: + return 'meld_provider'; } } } @@ -52,14 +48,13 @@ class ProvidersHelper { case WalletType.nano: case WalletType.banano: case WalletType.wownero: - return [ProviderType.askEachTime, ProviderType.onramper]; + return [ProviderType.onramper]; case WalletType.monero: - return [ProviderType.askEachTime, ProviderType.onramper, ProviderType.dfx]; + return [ProviderType.onramper, ProviderType.dfx]; case WalletType.bitcoin: case WalletType.polygon: case WalletType.ethereum: return [ - ProviderType.askEachTime, ProviderType.onramper, ProviderType.dfx, ProviderType.robinhood, @@ -68,10 +63,13 @@ class ProvidersHelper { case WalletType.litecoin: case WalletType.bitcoinCash: case WalletType.solana: - return [ProviderType.askEachTime, ProviderType.onramper, ProviderType.robinhood, ProviderType.moonpay]; + return [ + ProviderType.onramper, + ProviderType.robinhood, + ProviderType.moonpay + ]; case WalletType.tron: return [ - ProviderType.askEachTime, ProviderType.onramper, ProviderType.robinhood, ProviderType.moonpay, @@ -88,28 +86,24 @@ class ProvidersHelper { case WalletType.ethereum: case WalletType.polygon: return [ - ProviderType.askEachTime, ProviderType.onramper, ProviderType.moonpay, ProviderType.dfx, ]; case WalletType.litecoin: case WalletType.bitcoinCash: - return [ProviderType.askEachTime, ProviderType.moonpay]; + return [ProviderType.moonpay]; case WalletType.solana: return [ - ProviderType.askEachTime, ProviderType.onramper, - ProviderType.robinhood, ProviderType.moonpay, ]; case WalletType.tron: return [ - ProviderType.askEachTime, - ProviderType.robinhood, ProviderType.moonpay, ]; case WalletType.monero: + return [ProviderType.dfx]; case WalletType.nano: case WalletType.banano: case WalletType.none: @@ -129,7 +123,9 @@ class ProvidersHelper { return getIt.get(); case ProviderType.moonpay: return getIt.get(); - case ProviderType.askEachTime: + case ProviderType.meld: + return getIt.get(); + default: return null; } } diff --git a/lib/entities/qr_scanner.dart b/lib/entities/qr_scanner.dart index c6873a5bc..2e2637566 100644 --- a/lib/entities/qr_scanner.dart +++ b/lib/entities/qr_scanner.dart @@ -1,15 +1,376 @@ -import 'package:barcode_scan2/barcode_scan2.dart'; +import 'dart:math'; + +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/main.dart'; +import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; +import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; +import 'package:fast_scanner/fast_scanner.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; var isQrScannerShown = false; -Future presentQRScanner() async { +Future presentQRScanner(BuildContext context) async { isQrScannerShown = true; try { - final result = await BarcodeScanner.scan(); + final result = await Navigator.of(context).push( + MaterialPageRoute( + builder:(context) { + return BarcodeScannerSimple(); + }, + ), + ); isQrScannerShown = false; - return result.rawContent.trim(); + return result??''; } catch (e) { isQrScannerShown = false; rethrow; } } + +// https://github.com/MrCyjaneK/fast_scanner/blob/master/example/lib/barcode_scanner_simple.dart +class BarcodeScannerSimple extends StatefulWidget { + const BarcodeScannerSimple({super.key}); + + @override + State createState() => _BarcodeScannerSimpleState(); +} + +class _BarcodeScannerSimpleState extends State { + Barcode? _barcode; + bool popped = false; + + List urCodes = []; + late var ur = URQRToURQRData(urCodes); + + void _handleBarcode(BarcodeCapture barcodes) { + try { + _handleBarcodeInternal(barcodes); + } catch (e) { + showPopUp( + context: context, + builder: (context) { + return AlertWithOneAction( + alertTitle: S.of(context).error, + alertContent: S.of(context).error_dialog_content, + buttonText: 'ok', + buttonAction: () { + Navigator.of(context).pop(); + }, + ); + }, + ); + print(e); + } + } + + void _handleBarcodeInternal(BarcodeCapture barcodes) { + for (final barcode in barcodes.barcodes) { + // don't handle unknown QR codes + if (barcode.rawValue?.trim().isEmpty??false == false) continue; + if (barcode.rawValue!.startsWith("ur:")) { + if (urCodes.contains(barcode.rawValue)) continue; + setState(() { + urCodes.add(barcode.rawValue!); + ur = URQRToURQRData(urCodes); + }); + if (ur.progress == 1) { + setState(() { + popped = true; + }); + SchedulerBinding.instance.addPostFrameCallback((_) { + Navigator.of(context).pop(ur.inputs.join("\n")); + }); + }; + } + } + if (urCodes.isNotEmpty) return; + if (mounted) { + setState(() { + _barcode = barcodes.barcodes.firstOrNull; + }); + if (_barcode != null && popped != true) { + setState(() { + popped = true; + }); + SchedulerBinding.instance.addPostFrameCallback((_) { + Navigator.of(context).pop(_barcode?.rawValue ?? ""); + }); + } + } + } + + final MobileScannerController ctrl = MobileScannerController(); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Scan'), + actions: [ + SwitchCameraButton(controller: ctrl), + ToggleFlashlightButton(controller: ctrl), + ], + ), + backgroundColor: Colors.black, + body: Stack( + children: [ + MobileScanner( + onDetect: _handleBarcode, + controller: ctrl, + ), + if (ur.inputs.length != 0) + Center(child: + Text( + "${ur.inputs.length}/${ur.count}", + style: Theme.of(context).textTheme.displayLarge?.copyWith(color: Colors.white) + ), + ), + SizedBox( + child: Center( + child: SizedBox( + width: 250, + height: 250, + child: CustomPaint( + painter: ProgressPainter( + urQrProgress: URQrProgress( + expectedPartCount: ur.count - 1, + processedPartsCount: ur.inputs.length, + receivedPartIndexes: _urParts(), + percentage: ur.progress, + ), + ), + ), + ), + ), + ), + ], + ), + ); + } + + List _urParts() { + List l = []; + for (var inp in ur.inputs) { + try { + l.add(int.parse(inp.split("/")[1].split("-")[0])); + } catch (e) {} + } + return l; + } +} + + +class ToggleFlashlightButton extends StatelessWidget { + const ToggleFlashlightButton({required this.controller, super.key}); + + final MobileScannerController controller; + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: controller, + builder: (context, state, child) { + if (!state.isInitialized || !state.isRunning) { + return const SizedBox.shrink(); + } + + switch (state.torchState) { + case TorchState.auto: + return IconButton( + iconSize: 32.0, + icon: const Icon(Icons.flash_auto), + onPressed: () async { + await controller.toggleTorch(); + }, + ); + case TorchState.off: + return IconButton( + iconSize: 32.0, + icon: const Icon(Icons.flash_off), + onPressed: () async { + await controller.toggleTorch(); + }, + ); + case TorchState.on: + return IconButton( + iconSize: 32.0, + icon: const Icon(Icons.flash_on), + onPressed: () async { + await controller.toggleTorch(); + }, + ); + case TorchState.unavailable: + return const Icon( + Icons.no_flash, + color: Colors.grey, + ); + } + }, + ); + } +} + +class SwitchCameraButton extends StatelessWidget { + const SwitchCameraButton({required this.controller, super.key}); + + final MobileScannerController controller; + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: controller, + builder: (context, state, child) { + if (!state.isInitialized || !state.isRunning) { + return const SizedBox.shrink(); + } + + final int? availableCameras = state.availableCameras; + + if (availableCameras != null && availableCameras < 2) { + return const SizedBox.shrink(); + } + + final Widget icon; + + switch (state.cameraDirection) { + case CameraFacing.front: + icon = const Icon(Icons.camera_front); + case CameraFacing.back: + icon = const Icon(Icons.camera_rear); + } + + return IconButton( + iconSize: 32.0, + icon: icon, + onPressed: () async { + await controller.switchCamera(); + }, + ); + }, + ); + } +} + +class URQRData { + URQRData( + {required this.tag, + required this.str, + required this.progress, + required this.count, + required this.error, + required this.inputs}); + final String tag; + final String str; + final double progress; + final int count; + final String error; + final List inputs; + Map toJson() { + return { + "tag": tag, + "str": str, + "progress": progress, + "count": count, + "error": error, + "inputs": inputs, + }; + } +} + +URQRData URQRToURQRData(List urqr_) { + final urqr = urqr_.toSet().toList(); + urqr.sort((s1, s2) { + final s1s = s1.split("/"); + final s1frameStr = s1s[1].split("-"); + final s1curFrame = int.parse(s1frameStr[0]); + final s2s = s2.split("/"); + final s2frameStr = s2s[1].split("-"); + final s2curFrame = int.parse(s2frameStr[0]); + return s1curFrame - s2curFrame; + }); + + String tag = ''; + int count = 0; + String bw = ''; + for (var elm in urqr) { + final s = elm.substring(elm.indexOf(":") + 1); // strip down ur: prefix + final s2 = s.split("/"); + tag = s2[0]; + final frameStr = s2[1].split("-"); + // final curFrame = int.parse(frameStr[0]); + count = int.parse(frameStr[1]); + final byteWords = s2[2]; + bw += byteWords; + } + String? error; + + return URQRData( + tag: tag, + str: bw, + progress: count == 0 ? 0 : (urqr.length / count), + count: count, + error: error ?? "", + inputs: urqr, + ); +} + +class ProgressPainter extends CustomPainter { + final URQrProgress urQrProgress; + + ProgressPainter({required this.urQrProgress}); + + @override + void paint(Canvas canvas, Size size) { + final c = Offset(size.width / 2.0, size.height / 2.0); + final radius = size.width * 0.9; + final rect = Rect.fromCenter(center: c, width: radius, height: radius); + const fullAngle = 360.0; + var startAngle = 0.0; + for (int i = 0; i < urQrProgress.expectedPartCount.toInt(); i++) { + var sweepAngle = + (1 / urQrProgress.expectedPartCount) * fullAngle * pi / 180.0; + drawSector(canvas, urQrProgress.receivedPartIndexes.contains(i), rect, + startAngle, sweepAngle); + startAngle += sweepAngle; + } + } + + void drawSector(Canvas canvas, bool isActive, Rect rect, double startAngle, + double sweepAngle) { + final paint = Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = 8 + ..strokeCap = StrokeCap.round + ..strokeJoin = StrokeJoin.round + ..color = isActive ? const Color(0xffff6600) : Colors.white70; + canvas.drawArc(rect, startAngle, sweepAngle, false, paint); + } + + @override + bool shouldRepaint(covariant ProgressPainter oldDelegate) { + return urQrProgress != oldDelegate.urQrProgress; + } +} + +class URQrProgress { + int expectedPartCount; + int processedPartsCount; + List receivedPartIndexes; + double percentage; + + URQrProgress({ + required this.expectedPartCount, + required this.processedPartsCount, + required this.receivedPartIndexes, + required this.percentage, + }); + + bool equals(URQrProgress? progress) { + if (progress == null) { + return false; + } + return processedPartsCount == progress.processedPartsCount; + } +} diff --git a/lib/exchange/provider/trocador_exchange_provider.dart b/lib/exchange/provider/trocador_exchange_provider.dart index 652543607..5529d824a 100644 --- a/lib/exchange/provider/trocador_exchange_provider.dart +++ b/lib/exchange/provider/trocador_exchange_provider.dart @@ -89,13 +89,12 @@ class TrocadorExchangeProvider extends ExchangeProvider { required CryptoCurrency to, required bool isFixedRateMode}) async { final params = { - 'api_key': apiKey, 'ticker': _normalizeCurrency(from), 'name': from.name, }; final uri = await _getUri(coinPath, params); - final response = await get(uri); + final response = await get(uri, headers: {'API-Key': apiKey}); if (response.statusCode != 200) throw Exception('Unexpected http status: ${response.statusCode}'); @@ -123,7 +122,6 @@ class TrocadorExchangeProvider extends ExchangeProvider { if (amount == 0) return 0.0; final params = { - 'api_key': apiKey, 'ticker_from': _normalizeCurrency(from), 'ticker_to': _normalizeCurrency(to), 'network_from': _networkFor(from), @@ -136,7 +134,8 @@ class TrocadorExchangeProvider extends ExchangeProvider { }; final uri = await _getUri(newRatePath, params); - final response = await get(uri); + final response = await get(uri, headers: {'API-Key': apiKey}); + final responseJSON = json.decode(response.body) as Map; final fromAmount = double.parse(responseJSON['amount_from'].toString()); final toAmount = double.parse(responseJSON['amount_to'].toString()); @@ -161,7 +160,6 @@ class TrocadorExchangeProvider extends ExchangeProvider { required bool isSendAll, }) async { final params = { - 'api_key': apiKey, 'ticker_from': _normalizeCurrency(request.fromCurrency), 'ticker_to': _normalizeCurrency(request.toCurrency), 'network_from': _networkFor(request.fromCurrency), @@ -202,7 +200,7 @@ class TrocadorExchangeProvider extends ExchangeProvider { params['provider'] = firstAvailableProvider; final uri = await _getUri(createTradePath, params); - final response = await get(uri); + final response = await get(uri, headers: {'API-Key': apiKey}); if (response.statusCode == 400) { final responseJSON = json.decode(response.body) as Map; @@ -248,8 +246,8 @@ class TrocadorExchangeProvider extends ExchangeProvider { @override Future findTradeById({required String id}) async { - final uri = await _getUri(tradePath, {'api_key': apiKey, 'id': id}); - return get(uri).then((response) { + final uri = await _getUri(tradePath, {'id': id}); + return get(uri, headers: {'API-Key': apiKey}).then((response) { if (response.statusCode != 200) throw Exception('Unexpected http status: ${response.statusCode}'); diff --git a/lib/main.dart b/lib/main.dart index 29b216b22..51fab4dd1 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -204,7 +204,7 @@ Future initializeAppConfigs() async { transactionDescriptions: transactionDescriptions, secureStorage: secureStorage, anonpayInvoiceInfo: anonpayInvoiceInfo, - initialMigrationVersion: 42, + initialMigrationVersion: 43, ); } diff --git a/lib/monero/cw_monero.dart b/lib/monero/cw_monero.dart index b1cf49482..89b1579fc 100644 --- a/lib/monero/cw_monero.dart +++ b/lib/monero/cw_monero.dart @@ -225,6 +225,19 @@ class CWMonero extends Monero { language: language, height: height); + @override + WalletCredentials createMoneroRestoreWalletFromHardwareCredentials({ + required String name, + required String password, + required int height, + required ledger.LedgerConnection ledgerConnection, + }) => + MoneroRestoreWalletFromHardwareCredentials( + name: name, + password: password, + height: height, + ledgerConnection: ledgerConnection); + @override WalletCredentials createMoneroRestoreWalletFromSeedCredentials( {required String name, @@ -248,6 +261,7 @@ class CWMonero extends Monero { final moneroWallet = wallet as MoneroWallet; final keys = moneroWallet.keys; return { + 'primaryAddress': keys.primaryAddress, 'privateSpendKey': keys.privateSpendKey, 'privateViewKey': keys.privateViewKey, 'publicSpendKey': keys.publicSpendKey, @@ -357,9 +371,48 @@ class CWMonero extends Monero { Future getCurrentHeight() async { return monero_wallet_api.getCurrentHeight(); } + + @override + bool importKeyImagesUR(Object wallet, String ur) { + final moneroWallet = wallet as MoneroWallet; + return moneroWallet.importKeyImagesUR(ur); + } + + + @override + Future commitTransactionUR(Object wallet, String ur) { + final moneroWallet = wallet as MoneroWallet; + return moneroWallet.submitTransactionUR(ur); + } + + @override + String exportOutputsUR(Object wallet, bool all) { + final moneroWallet = wallet as MoneroWallet; + return moneroWallet.exportOutputsUR(all); + } @override void monerocCheck() { checkIfMoneroCIsFine(); } + + @override + void setLedgerConnection(Object wallet, ledger.LedgerConnection connection) { + final moneroWallet = wallet as MoneroWallet; + moneroWallet.setLedgerConnection(connection); + } + + void resetLedgerConnection() { + disableLedgerExchange(); + } + + @override + void setGlobalLedgerConnection(ledger.LedgerConnection connection) { + gLedger = connection; + keepAlive(connection); + } + + bool isViewOnly() { + return isViewOnlyBySpendKey(); + } } diff --git a/lib/reactions/on_authentication_state_change.dart b/lib/reactions/on_authentication_state_change.dart index 1aa0a12c6..b411c5a15 100644 --- a/lib/reactions/on_authentication_state_change.dart +++ b/lib/reactions/on_authentication_state_change.dart @@ -1,18 +1,28 @@ import 'dart:async'; +import 'package:cake_wallet/core/wallet_connect/wc_bottom_sheet_service.dart'; +import 'package:cake_wallet/di.dart'; +import 'package:cake_wallet/entities/hardware_wallet/require_hardware_wallet_connection.dart'; +import 'package:cake_wallet/entities/load_current_wallet.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/routes.dart'; -import 'package:cake_wallet/utils/exception_handler.dart'; +import 'package:cake_wallet/src/screens/connect_device/connect_device_page.dart'; +import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; +import 'package:cake_wallet/store/authentication_store.dart'; import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cake_wallet/utils/exception_handler.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; +import 'package:cw_core/wallet_type.dart'; import 'package:flutter/widgets.dart'; import 'package:mobx/mobx.dart'; -import 'package:cake_wallet/entities/load_current_wallet.dart'; -import 'package:cake_wallet/store/authentication_store.dart'; import 'package:rxdart/subjects.dart'; ReactionDisposer? _onAuthenticationStateChange; dynamic loginError; -StreamController authenticatedErrorStreamController = BehaviorSubject(); +StreamController authenticatedErrorStreamController = + BehaviorSubject(); void startAuthenticationStateChange( AuthenticationStore authenticationStore, @@ -27,18 +37,49 @@ void startAuthenticationStateChange( _onAuthenticationStateChange ??= autorun((_) async { final state = authenticationStore.state; - if (state == AuthenticationState.installed && !SettingsStoreBase.walletPasswordDirectInput) { + if (state == AuthenticationState.installed && + !SettingsStoreBase.walletPasswordDirectInput) { try { - await loadCurrentWallet(); + if (!requireHardwareWalletConnection()) await loadCurrentWallet(); } catch (error, stack) { loginError = error; - ExceptionHandler.onError(FlutterErrorDetails(exception: error, stack: stack)); + ExceptionHandler.onError( + FlutterErrorDetails(exception: error, stack: stack)); } return; } if (state == AuthenticationState.allowed) { - await navigatorKey.currentState!.pushNamedAndRemoveUntil(Routes.dashboard, (route) => false); + if (requireHardwareWalletConnection()) { + await navigatorKey.currentState!.pushNamedAndRemoveUntil( + Routes.connectDevices, + (route) => false, + arguments: ConnectDevicePageParams( + walletType: WalletType.monero, + onConnectDevice: (context, ledgerVM) async { + monero!.setGlobalLedgerConnection(ledgerVM.connection); + showPopUp( + context: context, + builder: (BuildContext context) => AlertWithOneAction( + alertTitle: S.of(context).proceed_on_device, + alertContent: S.of(context).proceed_on_device_description, + buttonText: S.of(context).cancel, + buttonAction: () => Navigator.of(context).pop()), + ); + await loadCurrentWallet(); + getIt.get().resetCurrentSheet(); + await navigatorKey.currentState! + .pushNamedAndRemoveUntil(Routes.dashboard, (route) => false); + }, + allowChangeWallet: true, + ), + ); + + // await navigatorKey.currentState!.pushNamedAndRemoveUntil(Routes.connectDevices, (route) => false, arguments: ConnectDevicePageParams(walletType: walletType, onConnectDevice: onConnectDevice)); + } else { + await navigatorKey.currentState! + .pushNamedAndRemoveUntil(Routes.dashboard, (route) => false); + } if (!(await authenticatedErrorStreamController.stream.isEmpty)) { ExceptionHandler.showError( (await authenticatedErrorStreamController.stream.first).toString()); diff --git a/lib/reactions/on_current_wallet_change.dart b/lib/reactions/on_current_wallet_change.dart index 6630f1dfc..d7e6f90c0 100644 --- a/lib/reactions/on_current_wallet_change.dart +++ b/lib/reactions/on_current_wallet_change.dart @@ -10,9 +10,6 @@ import 'package:cw_core/transaction_history.dart'; import 'package:cw_core/balance.dart'; import 'package:cw_core/transaction_info.dart'; import 'package:mobx/mobx.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import 'package:cake_wallet/di.dart'; -import 'package:cake_wallet/entities/preferences_key.dart'; import 'package:cake_wallet/reactions/check_connection.dart'; import 'package:cake_wallet/reactions/on_wallet_sync_status_change.dart'; import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; @@ -65,10 +62,6 @@ void startCurrentWalletChangeReaction( startWalletSyncStatusChangeReaction(wallet, fiatConversionStore); startCheckConnectionReaction(wallet, settingsStore); - await getIt.get().setString(PreferencesKey.currentWalletName, wallet.name); - await getIt - .get() - .setInt(PreferencesKey.currentWalletType, serializeToInt(wallet.type)); if (wallet.type == WalletType.monero || wallet.type == WalletType.wownero || diff --git a/lib/router.dart b/lib/router.dart index 6c234ff80..1382da28f 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -17,12 +17,14 @@ import 'package:cake_wallet/src/screens/anonpay_details/anonpay_details_page.dar import 'package:cake_wallet/src/screens/auth/auth_page.dart'; import 'package:cake_wallet/src/screens/backup/backup_page.dart'; import 'package:cake_wallet/src/screens/backup/edit_backup_password_page.dart'; -import 'package:cake_wallet/src/screens/buy/buy_options_page.dart'; +import 'package:cake_wallet/src/screens/buy/buy_sell_options_page.dart'; import 'package:cake_wallet/src/screens/buy/buy_webview_page.dart'; +import 'package:cake_wallet/src/screens/buy/payment_method_options_page.dart'; import 'package:cake_wallet/src/screens/buy/webview_page.dart'; import 'package:cake_wallet/src/screens/cake_pay/auth/cake_pay_account_page.dart'; import 'package:cake_wallet/src/screens/cake_pay/cake_pay.dart'; import 'package:cake_wallet/src/screens/connect_device/connect_device_page.dart'; +import 'package:cake_wallet/src/screens/connect_device/monero_hardware_wallet_options_page.dart'; import 'package:cake_wallet/src/screens/connect_device/select_hardware_wallet_account_page.dart'; import 'package:cake_wallet/src/screens/contact/contact_list_page.dart'; import 'package:cake_wallet/src/screens/contact/contact_page.dart'; @@ -96,6 +98,7 @@ import 'package:cake_wallet/src/screens/transaction_details/rbf_details_page.dar import 'package:cake_wallet/src/screens/transaction_details/transaction_details_page.dart'; import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_details_page.dart'; import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_list_page.dart'; +import 'package:cake_wallet/src/screens/ur/animated_ur_page.dart'; import 'package:cake_wallet/src/screens/wallet/wallet_edit_page.dart'; import 'package:cake_wallet/src/screens/wallet_connect/wc_connections_listing_view.dart'; import 'package:cake_wallet/src/screens/wallet_keys/wallet_keys_page.dart'; @@ -128,7 +131,8 @@ import 'package:cw_core/wallet_type.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; - +import 'package:cake_wallet/src/screens/cake_pay/cake_pay.dart'; +import 'src/screens/buy/buy_sell_page.dart'; import 'src/screens/dashboard/pages/nft_import_page.dart'; late RouteSettings currentRouteSettings; @@ -209,6 +213,9 @@ Route createRoute(RouteSettings settings) { final type = arguments[0] as WalletType; final walletVM = getIt.get(param1: type); + if (type == WalletType.monero) + return CupertinoPageRoute(builder: (_) => MoneroHardwareWalletOptionsPage(walletVM)); + return CupertinoPageRoute(builder: (_) => SelectHardwareWalletAccountPage(walletVM)); case Routes.setupPin: @@ -400,8 +407,11 @@ Route createRoute(RouteSettings settings) { return CupertinoPageRoute(builder: (_) => getIt.get()); case Routes.walletList: + final onWalletLoaded = settings.arguments as Function(BuildContext)?; return MaterialPageRoute( - fullscreenDialog: true, builder: (_) => getIt.get()); + fullscreenDialog: true, + builder: (_) => getIt.get(param1: onWalletLoaded), + ); case Routes.walletEdit: return MaterialPageRoute( @@ -570,7 +580,15 @@ Route createRoute(RouteSettings settings) { case Routes.buySellPage: final args = settings.arguments as bool; - return MaterialPageRoute(builder: (_) => getIt.get(param1: args)); + return MaterialPageRoute(builder: (_) => getIt.get(param1: args)); + + case Routes.buyOptionsPage: + final args = settings.arguments as List; + return MaterialPageRoute(builder: (_) => getIt.get(param1: args)); + + case Routes.paymentMethodOptionsPage: + final args = settings.arguments as List; + return MaterialPageRoute(builder: (_) => getIt.get(param1: args)); case Routes.buyWebView: final args = settings.arguments as List; @@ -732,6 +750,9 @@ Route createRoute(RouteSettings settings) { case Routes.setup2faInfoPage: return MaterialPageRoute(builder: (_) => getIt.get()); + case Routes.urqrAnimatedPage: + return MaterialPageRoute(builder: (_) => getIt.get(param1: settings.arguments)); + case Routes.homeSettings: return CupertinoPageRoute( builder: (_) => getIt.get(param1: settings.arguments), diff --git a/lib/routes.dart b/lib/routes.dart index 5f11b11a3..0b8beb0ea 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -59,6 +59,8 @@ class Routes { static const supportOtherLinks = '/support/other'; static const orderDetails = '/order_details'; static const buySellPage = '/buy_sell_page'; + static const buyOptionsPage = '/buy_sell_options'; + static const paymentMethodOptionsPage = '/payment_method_options'; static const buyWebView = '/buy_web_view'; static const unspentCoinsList = '/unspent_coins_list'; static const unspentCoinsDetails = '/unspent_coins_details'; @@ -108,6 +110,7 @@ class Routes { static const signPage = '/sign_page'; static const connectDevices = '/device/connect'; + static const urqrAnimatedPage = '/urqr/animated_page'; static const walletGroupsDisplayPage = '/wallet_groups_display_page'; static const walletGroupDescription = '/wallet_group_description'; } diff --git a/lib/src/screens/InfoPage.dart b/lib/src/screens/Info_page.dart similarity index 100% rename from lib/src/screens/InfoPage.dart rename to lib/src/screens/Info_page.dart diff --git a/lib/src/screens/buy/buy_options_page.dart b/lib/src/screens/buy/buy_options_page.dart deleted file mode 100644 index 38f3ed968..000000000 --- a/lib/src/screens/buy/buy_options_page.dart +++ /dev/null @@ -1,74 +0,0 @@ -import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/src/screens/base_page.dart'; -import 'package:cake_wallet/src/widgets/option_tile.dart'; -import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; -import 'package:cake_wallet/themes/extensions/option_tile_theme.dart'; -import 'package:cake_wallet/themes/extensions/transaction_trade_theme.dart'; -import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; -import 'package:flutter/material.dart'; - -class BuySellOptionsPage extends BasePage { - BuySellOptionsPage(this.dashboardViewModel, this.isBuyAction); - - final DashboardViewModel dashboardViewModel; - final bool isBuyAction; - - @override - String get title => isBuyAction ? S.current.buy : S.current.sell; - - @override - AppBarStyle get appBarStyle => AppBarStyle.regular; - - @override - Widget body(BuildContext context) { - final isLightMode = Theme.of(context).extension()?.useDarkImage ?? false; - final availableProviders = isBuyAction - ? dashboardViewModel.availableBuyProviders - : dashboardViewModel.availableSellProviders; - - return ScrollableWithBottomSection( - content: Container( - child: Center( - child: ConstrainedBox( - constraints: BoxConstraints(maxWidth: 330), - child: Column( - children: [ - ...availableProviders.map((provider) { - final icon = Image.asset( - isLightMode ? provider.lightIcon : provider.darkIcon, - height: 40, - width: 40, - ); - - return Padding( - padding: EdgeInsets.only(top: 24), - child: OptionTile( - image: icon, - title: provider.toString(), - description: provider.providerDescription, - onPressed: () => provider.launchProvider(context, isBuyAction), - ), - ); - }).toList(), - ], - ), - ), - ), - ), - bottomSection: Padding( - padding: EdgeInsets.fromLTRB(24, 24, 24, 32), - child: Text( - isBuyAction - ? S.of(context).select_buy_provider_notice - : S.of(context).select_sell_provider_notice, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.normal, - color: Theme.of(context).extension()!.detailsTitlesColor, - ), - ), - ), - ); - } -} diff --git a/lib/src/screens/buy/buy_sell_options_page.dart b/lib/src/screens/buy/buy_sell_options_page.dart new file mode 100644 index 000000000..900810f68 --- /dev/null +++ b/lib/src/screens/buy/buy_sell_options_page.dart @@ -0,0 +1,48 @@ +import 'package:cake_wallet/core/selectable_option.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/screens/select_options_page.dart'; +import 'package:flutter/cupertino.dart'; + +class BuyOptionsPage extends SelectOptionsPage { + BuyOptionsPage({required this.items, this.pickAnOption, this.confirmOption}); + + final List items; + final Function(SelectableOption option)? pickAnOption; + final Function(BuildContext context)? confirmOption; + + @override + String get pageTitle => S.current.choose_a_provider; + + @override + EdgeInsets? get contentPadding => null; + + @override + EdgeInsets? get tilePadding => EdgeInsets.only(top: 8); + + @override + EdgeInsets? get innerPadding => EdgeInsets.symmetric(horizontal: 24, vertical: 8); + + @override + double? get imageHeight => 40; + + @override + double? get imageWidth => 40; + + @override + Color? get selectedBackgroundColor => null; + + @override + double? get tileBorderRadius => 30; + + @override + String get bottomSectionText => ''; + + @override + void Function(SelectableOption option)? get onOptionTap => pickAnOption; + + @override + String get primaryButtonText => S.current.confirm; + + @override + void Function(BuildContext context)? get primaryButtonAction => confirmOption; +} diff --git a/lib/src/screens/buy/buy_sell_page.dart b/lib/src/screens/buy/buy_sell_page.dart new file mode 100644 index 000000000..d2f16fe3c --- /dev/null +++ b/lib/src/screens/buy/buy_sell_page.dart @@ -0,0 +1,469 @@ +import 'package:cake_wallet/buy/sell_buy_states.dart'; +import 'package:cake_wallet/core/address_validator.dart'; +import 'package:cake_wallet/di.dart'; +import 'package:cake_wallet/entities/fiat_currency.dart'; +import 'package:cake_wallet/entities/parse_address_from_domain.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/src/screens/exchange/widgets/desktop_exchange_cards_section.dart'; +import 'package:cake_wallet/src/screens/exchange/widgets/exchange_card.dart'; +import 'package:cake_wallet/src/screens/exchange/widgets/mobile_exchange_cards_section.dart'; +import 'package:cake_wallet/src/widgets/keyboard_done_button.dart'; +import 'package:cake_wallet/src/widgets/primary_button.dart'; +import 'package:cake_wallet/src/widgets/provider_optoin_tile.dart'; +import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; +import 'package:cake_wallet/src/widgets/trail_button.dart'; +import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; +import 'package:cake_wallet/themes/extensions/exchange_page_theme.dart'; +import 'package:cake_wallet/themes/extensions/keyboard_theme.dart'; +import 'package:cake_wallet/themes/extensions/send_page_theme.dart'; +import 'package:cake_wallet/themes/theme_base.dart'; +import 'package:cake_wallet/typography.dart'; +import 'package:cake_wallet/src/screens/send/widgets/extract_address_from_parsed.dart'; +import 'package:cake_wallet/utils/responsive_layout_util.dart'; +import 'package:cake_wallet/view_model/buy/buy_sell_view_model.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/currency.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:keyboard_actions/keyboard_actions.dart'; +import 'package:mobx/mobx.dart'; + +class BuySellPage extends BasePage { + BuySellPage(this.buySellViewModel); + + final BuySellViewModel buySellViewModel; + final cryptoCurrencyKey = GlobalKey(); + final fiatCurrencyKey = GlobalKey(); + final _formKey = GlobalKey(); + final _fiatAmountFocus = FocusNode(); + final _cryptoAmountFocus = FocusNode(); + final _cryptoAddressFocus = FocusNode(); + var _isReactionsSet = false; + + final arrowBottomPurple = Image.asset( + 'assets/images/arrow_bottom_purple_icon.png', + color: Colors.white, + height: 8, + ); + final arrowBottomCakeGreen = Image.asset( + 'assets/images/arrow_bottom_cake_green.png', + color: Colors.white, + height: 8, + ); + + late final String? depositWalletName; + late final String? receiveWalletName; + + @override + String get title => S.current.buy + '/' + S.current.sell; + + @override + bool get gradientBackground => true; + + @override + bool get gradientAll => true; + + @override + bool get resizeToAvoidBottomInset => false; + + @override + bool get extendBodyBehindAppBar => true; + + @override + AppBarStyle get appBarStyle => AppBarStyle.transparent; + + @override + Function(BuildContext)? get pushToNextWidget => (context) { + FocusScopeNode currentFocus = FocusScope.of(context); + if (!currentFocus.hasPrimaryFocus) { + currentFocus.focusedChild?.unfocus(); + } + }; + + @override + Widget trailing(BuildContext context) => TrailButton( + caption: S.of(context).clear, + onPressed: () { + _formKey.currentState?.reset(); + buySellViewModel.reset(); + }); + + @override + Widget? leading(BuildContext context) { + final _backButton = Icon( + Icons.arrow_back_ios, + color: titleColor(context), + size: 16, + ); + final _closeButton = + currentTheme.type == ThemeType.dark ? closeButtonImageDarkTheme : closeButtonImage; + + bool isMobileView = responsiveLayoutUtil.shouldRenderMobileUI; + + return MergeSemantics( + child: SizedBox( + height: isMobileView ? 37 : 45, + width: isMobileView ? 37 : 45, + child: ButtonTheme( + minWidth: double.minPositive, + child: Semantics( + label: !isMobileView ? S.of(context).close : S.of(context).seed_alert_back, + child: TextButton( + style: ButtonStyle( + overlayColor: MaterialStateColor.resolveWith((states) => Colors.transparent), + ), + onPressed: () => onClose(context), + child: !isMobileView ? _closeButton : _backButton, + ), + ), + ), + ), + ); + } + + @override + Widget body(BuildContext context) { + WidgetsBinding.instance.addPostFrameCallback((_) => _setReactions(context, buySellViewModel)); + + return KeyboardActions( + disableScroll: true, + config: KeyboardActionsConfig( + keyboardActionsPlatform: KeyboardActionsPlatform.IOS, + keyboardBarColor: Theme.of(context).extension()!.keyboardBarColor, + nextFocus: false, + actions: [ + KeyboardActionsItem( + focusNode: _fiatAmountFocus, toolbarButtons: [(_) => KeyboardDoneButton()]), + KeyboardActionsItem( + focusNode: _cryptoAmountFocus, toolbarButtons: [(_) => KeyboardDoneButton()]) + ]), + child: Container( + color: Theme.of(context).colorScheme.background, + child: Form( + key: _formKey, + child: ScrollableWithBottomSection( + contentPadding: EdgeInsets.only(bottom: 24), + content: Observer( + builder: (_) => Column(children: [ + _exchangeCardsSection(context), + Padding( + padding: EdgeInsets.symmetric(horizontal: 24), + child: Column( + children: [ + SizedBox(height: 12), + _buildPaymentMethodTile(context), + ], + ), + ), + ])), + bottomSectionPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24), + bottomSection: Column(children: [ + Observer( + builder: (_) => LoadingPrimaryButton( + text: S.current.choose_a_provider, + onPressed: () async { + if(!_formKey.currentState!.validate()) return; + buySellViewModel.onTapChoseProvider(context); + }, + color: Theme.of(context).primaryColor, + textColor: Colors.white, + isDisabled: false, + isLoading: !buySellViewModel.isReadyToTrade)), + ]), + )), + )); + } + + Widget _buildPaymentMethodTile(BuildContext context) { + if (buySellViewModel.paymentMethodState is PaymentMethodLoading || + buySellViewModel.paymentMethodState is InitialPaymentMethod) { + return OptionTilePlaceholder( + withBadge: false, + withSubtitle: false, + borderRadius: 30, + padding: EdgeInsets.symmetric(horizontal: 24, vertical: 8), + leadingIcon: Icons.arrow_forward_ios, + isDarkTheme: buySellViewModel.isDarkTheme); + } + if (buySellViewModel.paymentMethodState is PaymentMethodFailed) { + return OptionTilePlaceholder(errorText: 'No payment methods available', borderRadius: 30); + } + if (buySellViewModel.paymentMethodState is PaymentMethodLoaded && + buySellViewModel.selectedPaymentMethod != null) { + return Observer(builder: (_) { + final selectedPaymentMethod = buySellViewModel.selectedPaymentMethod!; + return ProviderOptionTile( + lightImagePath: selectedPaymentMethod.lightIconPath, + darkImagePath: selectedPaymentMethod.darkIconPath, + title: selectedPaymentMethod.title, + onPressed: () => _pickPaymentMethod(context), + leadingIcon: Icons.arrow_forward_ios, + isLightMode: !buySellViewModel.isDarkTheme, + borderRadius: 30, + padding: EdgeInsets.symmetric(horizontal: 24, vertical: 8), + titleTextStyle: + textLargeBold(color: Theme.of(context).extension()!.titleColor), + ); + }); + } + return OptionTilePlaceholder(errorText: 'No payment methods available', borderRadius: 30); + } + + void _pickPaymentMethod(BuildContext context) async { + final currentOption = buySellViewModel.selectedPaymentMethod; + await Navigator.of(context).pushNamed( + Routes.paymentMethodOptionsPage, + arguments: [ + buySellViewModel.paymentMethods, + buySellViewModel.changeOption, + ], + ); + + buySellViewModel.selectedPaymentMethod; + if (currentOption != null && + currentOption.paymentMethodType != + buySellViewModel.selectedPaymentMethod?.paymentMethodType) { + await buySellViewModel.calculateBestRate(); + } + } + + void _setReactions(BuildContext context, BuySellViewModel buySellViewModel) { + if (_isReactionsSet) { + return; + } + + final fiatAmountController = fiatCurrencyKey.currentState!.amountController; + final cryptoAmountController = cryptoCurrencyKey.currentState!.amountController; + final cryptoAddressController = cryptoCurrencyKey.currentState!.addressController; + + _onCurrencyChange(buySellViewModel.cryptoCurrency, buySellViewModel, cryptoCurrencyKey); + _onCurrencyChange(buySellViewModel.fiatCurrency, buySellViewModel, fiatCurrencyKey); + + reaction( + (_) => buySellViewModel.wallet.name, + (String _) => + _onWalletNameChange(buySellViewModel, buySellViewModel.cryptoCurrency, cryptoCurrencyKey)); + + reaction( + (_) => buySellViewModel.cryptoCurrency, + (CryptoCurrency currency) => + _onCurrencyChange(currency, buySellViewModel, cryptoCurrencyKey)); + + reaction( + (_) => buySellViewModel.fiatCurrency, + (FiatCurrency currency) => + _onCurrencyChange(currency, buySellViewModel, fiatCurrencyKey)); + + reaction((_) => buySellViewModel.fiatAmount, (String amount) { + if (fiatCurrencyKey.currentState!.amountController.text != amount) { + fiatCurrencyKey.currentState!.amountController.text = amount; + } + }); + + reaction((_) => buySellViewModel.isCryptoCurrencyAddressEnabled, (bool isEnabled) { + cryptoCurrencyKey.currentState!.isAddressEditable(isEditable: isEnabled); + }); + + reaction((_) => buySellViewModel.cryptoAmount, (String amount) { + if (cryptoCurrencyKey.currentState!.amountController.text != amount) { + cryptoCurrencyKey.currentState!.amountController.text = amount; + } + }); + + reaction((_) => buySellViewModel.cryptoCurrencyAddress, (String address) { + if (cryptoAddressController != address) { + cryptoCurrencyKey.currentState!.addressController.text = address; + } + }); + + fiatAmountController.addListener(() { + if (fiatAmountController.text != buySellViewModel.fiatAmount) { + buySellViewModel.changeFiatAmount(amount: fiatAmountController.text); + } + }); + + cryptoAmountController.addListener(() { + if (cryptoAmountController.text != buySellViewModel.cryptoAmount) { + buySellViewModel.changeCryptoAmount(amount: cryptoAmountController.text); + } + }); + + cryptoAddressController.addListener(() { + buySellViewModel.changeCryptoCurrencyAddress(cryptoAddressController.text); + }); + + _cryptoAddressFocus.addListener(() async { + if (!_cryptoAddressFocus.hasFocus && cryptoAddressController.text.isNotEmpty) { + final domain = cryptoAddressController.text; + buySellViewModel.cryptoCurrencyAddress = + await fetchParsedAddress(context, domain, buySellViewModel.cryptoCurrency); + } + }); + + reaction((_) => buySellViewModel.wallet.walletAddresses.addressForExchange, (String address) { + if (buySellViewModel.cryptoCurrency == CryptoCurrency.xmr) { + cryptoCurrencyKey.currentState!.changeAddress(address: address); + } + }); + + reaction((_) => buySellViewModel.isReadyToTrade, (bool isReady) { + if (isReady) { + if (cryptoAmountController.text.isNotEmpty && + cryptoAmountController.text != S.current.fetching) { + buySellViewModel.changeCryptoAmount(amount: cryptoAmountController.text); + } else if (fiatAmountController.text.isNotEmpty && + fiatAmountController.text != S.current.fetching) { + buySellViewModel.changeFiatAmount(amount: fiatAmountController.text); + } + } + }); + + _isReactionsSet = true; + } + + void _onCurrencyChange(Currency currency, BuySellViewModel buySellViewModel, + GlobalKey key) { + final isCurrentTypeWallet = currency == buySellViewModel.wallet.currency; + + key.currentState!.changeSelectedCurrency(currency); + key.currentState!.changeWalletName(isCurrentTypeWallet ? buySellViewModel.wallet.name : ''); + + key.currentState!.changeAddress( + address: isCurrentTypeWallet ? buySellViewModel.wallet.walletAddresses.addressForExchange : ''); + + key.currentState!.changeAmount(amount: ''); + } + + void _onWalletNameChange(BuySellViewModel buySellViewModel, CryptoCurrency currency, + GlobalKey key) { + final isCurrentTypeWallet = currency == buySellViewModel.wallet.currency; + + if (isCurrentTypeWallet) { + key.currentState!.changeWalletName(buySellViewModel.wallet.name); + key.currentState!.addressController.text = buySellViewModel.wallet.walletAddresses.addressForExchange; + } else if (key.currentState!.addressController.text == + buySellViewModel.wallet.walletAddresses.addressForExchange) { + key.currentState!.changeWalletName(''); + key.currentState!.addressController.text = ''; + } + } + + void disposeBestRateSync() => {}; + + Widget _exchangeCardsSection(BuildContext context) { + final fiatExchangeCard = Observer( + builder: (_) => ExchangeCard( + cardInstanceName: 'fiat_currency_trade_card', + onDispose: disposeBestRateSync, + amountFocusNode: _fiatAmountFocus, + key: fiatCurrencyKey, + title: 'FIAT ${S.of(context).amount}', + initialCurrency: buySellViewModel.fiatCurrency, + initialWalletName: '', + initialAddress: '', + initialIsAmountEditable: true, + isAmountEstimated: false, + currencyRowPadding: EdgeInsets.zero, + addressRowPadding: EdgeInsets.zero, + isMoneroWallet: buySellViewModel.wallet == WalletType.monero, + showAddressField: false, + showLimitsField: false, + currencies: buySellViewModel.fiatCurrencies, + onCurrencySelected: (currency) => + buySellViewModel.changeFiatCurrency(currency: currency), + imageArrow: arrowBottomPurple, + currencyButtonColor: Colors.transparent, + addressButtonsColor: + Theme.of(context).extension()!.textFieldButtonColor, + borderColor: + Theme.of(context).extension()!.textFieldBorderTopPanelColor, + onPushPasteButton: (context) async {}, + onPushAddressBookButton: (context) async {}, + )); + + final cryptoExchangeCard = Observer( + builder: (_) => ExchangeCard( + cardInstanceName: 'crypto_currency_trade_card', + onDispose: disposeBestRateSync, + amountFocusNode: _cryptoAmountFocus, + addressFocusNode: _cryptoAddressFocus, + key: cryptoCurrencyKey, + title: 'Crypto ${S.of(context).amount}', + initialCurrency: buySellViewModel.cryptoCurrency, + initialWalletName: '', + initialAddress: buySellViewModel.cryptoCurrency == buySellViewModel.wallet.currency + ? buySellViewModel.wallet.walletAddresses.addressForExchange + : buySellViewModel.cryptoCurrencyAddress, + initialIsAmountEditable: true, + isAmountEstimated: true, + showLimitsField: false, + currencyRowPadding: EdgeInsets.zero, + addressRowPadding: EdgeInsets.zero, + isMoneroWallet: buySellViewModel.wallet == WalletType.monero, + currencies: buySellViewModel.cryptoCurrencies, + onCurrencySelected: (currency) => + buySellViewModel.changeCryptoCurrency(currency: currency), + imageArrow: arrowBottomCakeGreen, + currencyButtonColor: Colors.transparent, + addressButtonsColor: + Theme.of(context).extension()!.textFieldButtonColor, + borderColor: + Theme.of(context).extension()!.textFieldBorderBottomPanelColor, + addressTextFieldValidator: AddressValidator(type: buySellViewModel.cryptoCurrency), + onPushPasteButton: (context) async {}, + onPushAddressBookButton: (context) async {}, + )); + + if (responsiveLayoutUtil.shouldRenderMobileUI) { + return Observer( + builder: (_) { + if (buySellViewModel.isBuyAction) { + return MobileExchangeCardsSection( + firstExchangeCard: fiatExchangeCard, + secondExchangeCard: cryptoExchangeCard, + onBuyTap: () => null, + onSellTap: () => + buySellViewModel.isBuyAction ? buySellViewModel.changeBuySellAction() : null, + isBuySellOption: true, + ); + } else { + return MobileExchangeCardsSection( + firstExchangeCard: cryptoExchangeCard, + secondExchangeCard: fiatExchangeCard, + onBuyTap: () => + !buySellViewModel.isBuyAction ? buySellViewModel.changeBuySellAction() : null, + onSellTap: () => null, + isBuySellOption: true, + ); + } + }, + ); + } + + return Observer( + builder: (_) { + if (buySellViewModel.isBuyAction) { + return DesktopExchangeCardsSection( + firstExchangeCard: fiatExchangeCard, + secondExchangeCard: cryptoExchangeCard, + ); + } else { + return DesktopExchangeCardsSection( + firstExchangeCard: cryptoExchangeCard, + secondExchangeCard: fiatExchangeCard, + ); + } + }, + ); + } + + Future fetchParsedAddress( + BuildContext context, String domain, CryptoCurrency currency) async { + final parsedAddress = await getIt.get().resolve(context, domain, currency); + final address = await extractAddressFromParsed(context, parsedAddress); + return address; + } +} diff --git a/lib/src/screens/buy/payment_method_options_page.dart b/lib/src/screens/buy/payment_method_options_page.dart new file mode 100644 index 000000000..541f91ab4 --- /dev/null +++ b/lib/src/screens/buy/payment_method_options_page.dart @@ -0,0 +1,47 @@ +import 'package:cake_wallet/core/selectable_option.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/screens/select_options_page.dart'; +import 'package:flutter/cupertino.dart'; + +class PaymentMethodOptionsPage extends SelectOptionsPage { + PaymentMethodOptionsPage({required this.items, this.pickAnOption}); + + final List items; + final Function(SelectableOption option)? pickAnOption; + + @override + String get pageTitle => S.current.choose_a_payment_method; + + @override + EdgeInsets? get contentPadding => null; + + @override + EdgeInsets? get tilePadding => EdgeInsets.only(top: 12); + + @override + EdgeInsets? get innerPadding => EdgeInsets.symmetric(horizontal: 24, vertical: 12); + + @override + double? get imageHeight => null; + + @override + double? get imageWidth => null; + + @override + Color? get selectedBackgroundColor => null; + + @override + double? get tileBorderRadius => 30; + + @override + String get bottomSectionText => ''; + + @override + void Function(SelectableOption option)? get onOptionTap => pickAnOption; + + @override + String get primaryButtonText => S.current.confirm; + + @override + void Function(BuildContext context)? get primaryButtonAction => null; +} diff --git a/lib/src/screens/cake_pay/cards/cake_pay_confirm_purchase_card_page.dart b/lib/src/screens/cake_pay/cards/cake_pay_confirm_purchase_card_page.dart index 309c435a2..9357df2c3 100644 --- a/lib/src/screens/cake_pay/cards/cake_pay_confirm_purchase_card_page.dart +++ b/lib/src/screens/cake_pay/cards/cake_pay_confirm_purchase_card_page.dart @@ -347,8 +347,8 @@ class CakePayBuyCardDetailPage extends BasePage { rightButtonText: S.of(popupContext).send, leftButtonText: S.of(popupContext).cancel, actionRightButton: () async { - Navigator.of(popupContext).pop(); - await cakePayPurchaseViewModel.sendViewModel.commitTransaction(); + Navigator.of(context).pop(); + await cakePayPurchaseViewModel.sendViewModel.commitTransaction(context); }, actionLeftButton: () => Navigator.of(popupContext).pop())); }, diff --git a/lib/src/screens/connect_device/connect_device_page.dart b/lib/src/screens/connect_device/connect_device_page.dart index 9e331e818..c2cc40229 100644 --- a/lib/src/screens/connect_device/connect_device_page.dart +++ b/lib/src/screens/connect_device/connect_device_page.dart @@ -2,9 +2,12 @@ import 'dart:async'; import 'dart:io'; import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/screens/connect_device/widgets/device_tile.dart'; +import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; +import 'package:cake_wallet/themes/extensions/wallet_list_theme.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart'; import 'package:cw_core/wallet_type.dart'; @@ -17,35 +20,46 @@ typedef OnConnectDevice = void Function(BuildContext, LedgerViewModel); class ConnectDevicePageParams { final WalletType walletType; final OnConnectDevice onConnectDevice; + final bool allowChangeWallet; - ConnectDevicePageParams( - {required this.walletType, required this.onConnectDevice}); + ConnectDevicePageParams({ + required this.walletType, + required this.onConnectDevice, + this.allowChangeWallet = false, + }); } class ConnectDevicePage extends BasePage { final WalletType walletType; final OnConnectDevice onConnectDevice; + final bool allowChangeWallet; final LedgerViewModel ledgerVM; ConnectDevicePage(ConnectDevicePageParams params, this.ledgerVM) : walletType = params.walletType, - onConnectDevice = params.onConnectDevice; + onConnectDevice = params.onConnectDevice, + allowChangeWallet = params.allowChangeWallet; @override String get title => S.current.restore_title_from_hardware_wallet; @override - Widget body(BuildContext context) => - ConnectDevicePageBody(walletType, onConnectDevice, ledgerVM); + Widget body(BuildContext context) => ConnectDevicePageBody( + walletType, onConnectDevice, allowChangeWallet, ledgerVM); } class ConnectDevicePageBody extends StatefulWidget { final WalletType walletType; final OnConnectDevice onConnectDevice; + final bool allowChangeWallet; final LedgerViewModel ledgerVM; const ConnectDevicePageBody( - this.walletType, this.onConnectDevice, this.ledgerVM); + this.walletType, + this.onConnectDevice, + this.allowChangeWallet, + this.ledgerVM, + ); @override ConnectDevicePageBodyState createState() => ConnectDevicePageBodyState(); @@ -102,14 +116,16 @@ class ConnectDevicePageBodyState extends State { Future _refreshBleDevices() async { try { - _bleRefresh = widget.ledgerVM - .scanForBleDevices() - .listen((device) => setState(() => bleDevices.add(device))) - ..onError((e) { - throw e.toString(); - }); - _bleRefreshTimer?.cancel(); - _bleRefreshTimer = null; + if (widget.ledgerVM.bleIsEnabled) { + _bleRefresh = widget.ledgerVM + .scanForBleDevices() + .listen((device) => setState(() => bleDevices.add(device))) + ..onError((e) { + throw e.toString(); + }); + _bleRefreshTimer?.cancel(); + _bleRefreshTimer = null; + } } catch (e) { print(e); } @@ -227,9 +243,7 @@ class ConnectDevicePageBodyState extends State { style: TextStyle( fontSize: 14, fontWeight: FontWeight.w400, - color: Theme.of(context) - .extension()! - .titleColor, + color: Theme.of(context).extension()!.titleColor, ), ), ), @@ -247,11 +261,27 @@ class ConnectDevicePageBodyState extends State { ), ) .toList(), - ] + ], + if (widget.allowChangeWallet) ...[ + PrimaryButton( + text: S.of(context).wallets, + color: Theme.of(context).extension()!.createNewWalletButtonBackgroundColor, + textColor: Theme.of(context).extension()!.restoreWalletButtonTextColor, + onPressed: _onChangeWallet, + ) + ], ], ), ), ), ); } + + void _onChangeWallet() { + Navigator.of(context).pushNamed( + Routes.walletList, + arguments: (BuildContext context) => Navigator.of(context) + .pushNamedAndRemoveUntil(Routes.dashboard, (route) => false), + ); + } } diff --git a/lib/src/screens/connect_device/monero_hardware_wallet_options_page.dart b/lib/src/screens/connect_device/monero_hardware_wallet_options_page.dart new file mode 100644 index 000000000..f8ace97bc --- /dev/null +++ b/lib/src/screens/connect_device/monero_hardware_wallet_options_page.dart @@ -0,0 +1,230 @@ +import 'package:cake_wallet/core/wallet_name_validator.dart'; +import 'package:cake_wallet/entities/generate_name.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/src/widgets/blockchain_height_widget.dart'; +import 'package:cake_wallet/src/widgets/primary_button.dart'; +import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; +import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; +import 'package:cake_wallet/themes/extensions/new_wallet_theme.dart'; +import 'package:cake_wallet/themes/extensions/send_page_theme.dart'; +import 'package:cake_wallet/utils/responsive_layout_util.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; +import 'package:cake_wallet/view_model/wallet_hardware_restore_view_model.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:mobx/mobx.dart'; + +class MoneroHardwareWalletOptionsPage extends BasePage { + MoneroHardwareWalletOptionsPage(this._walletHardwareRestoreVM); + + final WalletHardwareRestoreViewModel _walletHardwareRestoreVM; + + @override + String get title => S.current.restore_title_from_hardware_wallet; + + @override + Widget body(BuildContext context) => + _MoneroHardwareWalletOptionsForm(_walletHardwareRestoreVM); +} + +class _MoneroHardwareWalletOptionsForm extends StatefulWidget { + const _MoneroHardwareWalletOptionsForm(this._walletHardwareRestoreVM); + + final WalletHardwareRestoreViewModel _walletHardwareRestoreVM; + + @override + _MoneroHardwareWalletOptionsFormState createState() => + _MoneroHardwareWalletOptionsFormState(_walletHardwareRestoreVM); +} + +class _MoneroHardwareWalletOptionsFormState + extends State<_MoneroHardwareWalletOptionsForm> { + _MoneroHardwareWalletOptionsFormState(this._walletHardwareRestoreVM) + : _formKey = GlobalKey(), + _blockchainHeightKey = GlobalKey(), + _blockHeightFocusNode = FocusNode(), + _controller = TextEditingController(); + + final GlobalKey _formKey; + final GlobalKey _blockchainHeightKey; + final FocusNode _blockHeightFocusNode; + final WalletHardwareRestoreViewModel _walletHardwareRestoreVM; + final TextEditingController _controller; + + @override + void initState() { + super.initState(); + _setEffects(context); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.only(top: 24), + child: ScrollableWithBottomSection( + contentPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24), + content: Center( + child: ConstrainedBox( + constraints: BoxConstraints( + maxWidth: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Padding( + padding: EdgeInsets.only(top: 0), + child: Form( + key: _formKey, + child: Stack( + alignment: Alignment.centerRight, + children: [ + TextFormField( + onChanged: (value) => + _walletHardwareRestoreVM.name = value, + controller: _controller, + style: TextStyle( + fontSize: 20.0, + fontWeight: FontWeight.w600, + color: Theme.of(context) + .extension()! + .titleColor, + ), + decoration: InputDecoration( + hintStyle: TextStyle( + fontSize: 18.0, + fontWeight: FontWeight.w500, + color: Theme.of(context) + .extension()! + .hintTextColor, + ), + hintText: S.of(context).wallet_name, + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context) + .extension()! + .underlineColor, + width: 1.0, + ), + ), + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context) + .extension()! + .underlineColor, + width: 1.0, + ), + ), + suffixIcon: Semantics( + label: S.of(context).generate_name, + child: IconButton( + onPressed: _onGenerateName, + icon: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(6.0), + color: Theme.of(context).hintColor, + ), + width: 34, + height: 34, + child: Image.asset( + 'assets/images/refresh_icon.png', + color: Theme.of(context) + .extension()! + .textFieldButtonIconColor, + ), + ), + ), + ), + ), + validator: WalletNameValidator(), + ), + ], + ), + ), + ), + Padding( + padding: EdgeInsets.only(top: 20), + child: BlockchainHeightWidget( + focusNode: _blockHeightFocusNode, + key: _blockchainHeightKey, + hasDatePicker: true, + walletType: WalletType.monero, + ), + ), + ], + ), + ), + ), + bottomSectionPadding: EdgeInsets.all(24), + bottomSection: Observer( + builder: (context) => LoadingPrimaryButton( + onPressed: _confirmForm, + text: S.of(context).seed_language_next, + color: Colors.green, + textColor: Colors.white, + isDisabled: _walletHardwareRestoreVM.name.isEmpty, + ), + ), + ), + ); + } + + Future _onGenerateName() async { + final rName = await generateName(); + FocusManager.instance.primaryFocus?.unfocus(); + + setState(() { + _controller.text = rName; + _walletHardwareRestoreVM.name = rName; + _controller.selection = TextSelection.fromPosition( + TextPosition(offset: _controller.text.length)); + }); + } + + Future _confirmForm() async { + showPopUp( + context: context, + builder: (BuildContext context) => AlertWithOneAction( + alertTitle: S.of(context).proceed_on_device, + alertContent: S.of(context).proceed_on_device_description, + buttonText: S.of(context).cancel, + buttonAction: () => Navigator.of(context).pop(), + ), + ); + + final options = {'height': _blockchainHeightKey.currentState?.height ?? -1}; + await _walletHardwareRestoreVM.create(options: options); + } + + bool _effectsInstalled = false; + + void _setEffects(BuildContext context) { + if (_effectsInstalled) return; + + reaction((_) => _walletHardwareRestoreVM.error, (String? error) { + if (error != null) { + if (error == S.current.ledger_connection_error) + Navigator.of(context).pop(); + + WidgetsBinding.instance.addPostFrameCallback((_) { + showPopUp( + context: context, + builder: (BuildContext context) => AlertWithOneAction( + alertTitle: S.of(context).error, + alertContent: error, + buttonText: S.of(context).ok, + buttonAction: () { + _walletHardwareRestoreVM.error = null; + Navigator.of(context).pop(); + }, + ), + ); + }); + } + }); + + _effectsInstalled = true; + } +} diff --git a/lib/src/screens/contact/contact_list_page.dart b/lib/src/screens/contact/contact_list_page.dart index 130380b09..166288135 100644 --- a/lib/src/screens/contact/contact_list_page.dart +++ b/lib/src/screens/contact/contact_list_page.dart @@ -1,4 +1,5 @@ import 'package:cake_wallet/core/auth_service.dart'; +import 'package:cake_wallet/entities/contact.dart'; import 'package:cake_wallet/entities/contact_base.dart'; import 'package:cake_wallet/entities/contact_record.dart'; import 'package:cake_wallet/entities/wallet_list_order_types.dart'; @@ -17,7 +18,6 @@ import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; class ContactListPage extends BasePage { @@ -91,16 +91,19 @@ class ContactPageBody extends StatefulWidget { class _ContactPageBodyState extends State with SingleTickerProviderStateMixin { late TabController _tabController; + late ContactListViewModel contactListViewModel; @override void initState() { super.initState(); _tabController = TabController(length: 2, vsync: this); + contactListViewModel = widget.contactListViewModel; } @override void dispose() { _tabController.dispose(); + contactListViewModel.dispose(); super.dispose(); } @@ -160,25 +163,60 @@ class _ContactPageBodyState extends State with SingleTickerProv Widget _buildWalletContacts(BuildContext context) { final walletContacts = widget.contactListViewModel.walletContactsToShow; + final groupedContacts = >{}; + for (var contact in walletContacts) { + final baseName = _extractBaseName(contact.name); + groupedContacts.putIfAbsent(baseName, () => []).add(contact); + } + return ListView.builder( - shrinkWrap: true, - itemCount: walletContacts.length * 2, + itemCount: groupedContacts.length * 2, itemBuilder: (context, index) { if (index.isOdd) { return StandardListSeparator(); } else { - final walletInfo = walletContacts[index ~/ 2]; - return generateRaw(context, walletInfo); + final groupIndex = index ~/ 2; + final groupName = groupedContacts.keys.elementAt(groupIndex); + final groupContacts = groupedContacts[groupName]!; + + if (groupContacts.length == 1) { + final contact = groupContacts[0]; + return generateRaw(context, contact); + } else { + final activeContact = groupContacts.firstWhere( + (contact) => contact.name.contains('Active'), + orElse: () => groupContacts[0], + ); + + return ExpansionTile( + title: Text( + groupName, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + color: Theme.of(context).extension()!.titleColor, + ), + ), + leading: _buildCurrencyIcon(activeContact), + tilePadding: EdgeInsets.zero, + childrenPadding: const EdgeInsets.only(left: 16), + expandedCrossAxisAlignment: CrossAxisAlignment.start, + expandedAlignment: Alignment.topLeft, + children: groupContacts.map((contact) => generateRaw(context, contact)).toList(), + ); + } } }, ); } + String _extractBaseName(String name) { + final bracketIndex = name.indexOf('('); + return (bracketIndex != -1) ? name.substring(0, bracketIndex).trim() : name; + } + Widget generateRaw(BuildContext context, ContactBase contact) { - final image = contact.type.iconPath; - final currencyIcon = image != null - ? Image.asset(image, height: 24, width: 24) - : const SizedBox(height: 24, width: 24); + final currencyIcon = _buildCurrencyIcon(contact); return GestureDetector( onTap: () async { @@ -202,14 +240,16 @@ class _ContactPageBodyState extends State with SingleTickerProv mainAxisAlignment: MainAxisAlignment.start, children: [ currencyIcon, - Padding( - padding: EdgeInsets.only(left: 12), - child: Text( - contact.name, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.normal, - color: Theme.of(context).extension()!.titleColor, + Expanded( + child: Padding( + padding: EdgeInsets.only(left: 12), + child: Text( + contact.name, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + color: Theme.of(context).extension()!.titleColor, + ), ), ), ), @@ -219,6 +259,13 @@ class _ContactPageBodyState extends State with SingleTickerProv ); } + Widget _buildCurrencyIcon(ContactBase contact) { + final image = contact.type.iconPath; + return image != null + ? Image.asset(image, height: 24, width: 24) + : const SizedBox(height: 24, width: 24); + } + Future showNameAndAddressDialog(BuildContext context, String name, String address) async { return await showPopUp( context: context, @@ -263,12 +310,17 @@ class _ContactListBodyState extends State { @override void dispose() { widget.tabController.removeListener(_handleTabChange); + if (widget.contactListViewModel.settingsStore.contactListOrder == FilterListOrderType.Custom) { + widget.contactListViewModel.saveCustomOrder(); + } super.dispose(); } @override Widget build(BuildContext context) { - final contacts = widget.contactListViewModel.contacts; + final contacts = widget.contactListViewModel.isEditable + ? widget.contactListViewModel.contacts + : widget.contactListViewModel.contactsToShow; return Scaffold( body: Container( child: FilteredList( @@ -307,8 +359,9 @@ class _ContactListBodyState extends State { }, ), ), - floatingActionButton: - _isContactsTabActive ? filterButtonWidget(context, widget.contactListViewModel) : null, + floatingActionButton: _isContactsTabActive && widget.contactListViewModel.isEditable + ? filterButtonWidget(context, widget.contactListViewModel) + : null, ); } diff --git a/lib/src/screens/dashboard/desktop_widgets/desktop_dashboard_actions.dart b/lib/src/screens/dashboard/desktop_widgets/desktop_dashboard_actions.dart index d36c06013..7bb5f77f8 100644 --- a/lib/src/screens/dashboard/desktop_widgets/desktop_dashboard_actions.dart +++ b/lib/src/screens/dashboard/desktop_widgets/desktop_dashboard_actions.dart @@ -21,6 +21,14 @@ class DesktopDashboardActions extends StatelessWidget { return Column( children: [ const SizedBox(height: 16), + DesktopActionButton( + title: MainActions.showWalletsAction.name(context), + image: MainActions.showWalletsAction.image, + canShow: MainActions.showWalletsAction.canShow?.call(dashboardViewModel), + isEnabled: MainActions.showWalletsAction.isEnabled?.call(dashboardViewModel), + onTap: () async => + await MainActions.showWalletsAction.onTap(context, dashboardViewModel), + ), DesktopActionButton( title: MainActions.exchangeAction.name(context), image: MainActions.exchangeAction.image, @@ -55,20 +63,11 @@ class DesktopDashboardActions extends StatelessWidget { children: [ Expanded( child: DesktopActionButton( - title: MainActions.buyAction.name(context), - image: MainActions.buyAction.image, - canShow: MainActions.buyAction.canShow?.call(dashboardViewModel), - isEnabled: MainActions.buyAction.isEnabled?.call(dashboardViewModel), - onTap: () async => await MainActions.buyAction.onTap(context, dashboardViewModel), - ), - ), - Expanded( - child: DesktopActionButton( - title: MainActions.sellAction.name(context), - image: MainActions.sellAction.image, - canShow: MainActions.sellAction.canShow?.call(dashboardViewModel), - isEnabled: MainActions.sellAction.isEnabled?.call(dashboardViewModel), - onTap: () async => await MainActions.sellAction.onTap(context, dashboardViewModel), + title: MainActions.tradeAction.name(context), + image: MainActions.tradeAction.image, + canShow: MainActions.tradeAction.canShow?.call(dashboardViewModel), + isEnabled: MainActions.tradeAction.isEnabled?.call(dashboardViewModel), + onTap: () async => await MainActions.tradeAction.onTap(context, dashboardViewModel), ), ), ], diff --git a/lib/src/screens/dashboard/widgets/menu_widget.dart b/lib/src/screens/dashboard/widgets/menu_widget.dart index f0e330f04..b49a08584 100644 --- a/lib/src/screens/dashboard/widgets/menu_widget.dart +++ b/lib/src/screens/dashboard/widgets/menu_widget.dart @@ -101,6 +101,9 @@ class MenuWidgetState extends State { if (!widget.dashboardViewModel.hasSilentPayments) { items.removeWhere((element) => element.name(context) == S.of(context).silent_payments_settings); } + if (!widget.dashboardViewModel.isMoneroViewOnly) { + items.removeWhere((element) => element.name(context) == S.of(context).export_outputs); + } if (!widget.dashboardViewModel.hasMweb) { items.removeWhere((element) => element.name(context) == S.of(context).litecoin_mweb_settings); } @@ -189,7 +192,6 @@ class MenuWidgetState extends State { index--; final item = items[index]; - final isLastTile = index == itemCount - 1; return SettingActionButton( diff --git a/lib/src/screens/exchange/widgets/exchange_card.dart b/lib/src/screens/exchange/widgets/exchange_card.dart index 75a2eadd7..7c4cc948d 100644 --- a/lib/src/screens/exchange/widgets/exchange_card.dart +++ b/lib/src/screens/exchange/widgets/exchange_card.dart @@ -18,7 +18,7 @@ import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart'; import 'package:cake_wallet/themes/extensions/send_page_theme.dart'; -class ExchangeCard extends StatefulWidget { +class ExchangeCard extends StatefulWidget { ExchangeCard({ Key? key, required this.initialCurrency, @@ -40,19 +40,23 @@ class ExchangeCard extends StatefulWidget { this.borderColor = Colors.transparent, this.hasAllAmount = false, this.isAllAmountEnabled = false, + this.showAddressField = true, + this.showLimitsField = true, this.amountFocusNode, this.addressFocusNode, this.allAmount, + this.currencyRowPadding, + this.addressRowPadding, this.onPushPasteButton, this.onPushAddressBookButton, this.onDispose, required this.cardInstanceName, }) : super(key: key); - final List currencies; - final Function(CryptoCurrency) onCurrencySelected; + final List currencies; + final Function(T) onCurrencySelected; final String title; - final CryptoCurrency initialCurrency; + final T initialCurrency; final String initialWalletName; final String initialAddress; final bool initialIsAmountEditable; @@ -70,18 +74,22 @@ class ExchangeCard extends StatefulWidget { final FocusNode? amountFocusNode; final FocusNode? addressFocusNode; final bool hasAllAmount; + final bool showAddressField; + final bool showLimitsField; final bool isAllAmountEnabled; final VoidCallback? allAmount; + final EdgeInsets? currencyRowPadding; + final EdgeInsets? addressRowPadding; final void Function(BuildContext context)? onPushPasteButton; final void Function(BuildContext context)? onPushAddressBookButton; final Function()? onDispose; final String cardInstanceName; @override - ExchangeCardState createState() => ExchangeCardState(); + ExchangeCardState createState() => ExchangeCardState(); } -class ExchangeCardState extends State { +class ExchangeCardState extends State> { ExchangeCardState() : _title = '', _min = '', @@ -89,7 +97,6 @@ class ExchangeCardState extends State { _isAmountEditable = false, _isAddressEditable = false, _walletName = '', - _selectedCurrency = CryptoCurrency.btc, _isAmountEstimated = false, _isMoneroWallet = false, _cardInstanceName = ''; @@ -101,7 +108,7 @@ class ExchangeCardState extends State { String _title; String? _min; String? _max; - CryptoCurrency _selectedCurrency; + late T _selectedCurrency; String _walletName; bool _isAmountEditable; bool _isAddressEditable; @@ -118,7 +125,8 @@ class ExchangeCardState extends State { _selectedCurrency = widget.initialCurrency; _isAmountEstimated = widget.isAmountEstimated; _isMoneroWallet = widget.isMoneroWallet; - addressController.text = widget.initialAddress; + addressController.text = _normalizeAddressFormat(widget.initialAddress); + super.initState(); } @@ -136,7 +144,7 @@ class ExchangeCardState extends State { }); } - void changeSelectedCurrency(CryptoCurrency currency) { + void changeSelectedCurrency(T currency) { setState(() => _selectedCurrency = currency); } @@ -157,7 +165,7 @@ class ExchangeCardState extends State { } void changeAddress({required String address}) { - setState(() => addressController.text = address); + setState(() => addressController.text = _normalizeAddressFormat(address)); } void changeAmount({required String amount}) { @@ -222,7 +230,7 @@ class ExchangeCardState extends State { Divider(height: 1, color: Theme.of(context).extension()!.textFieldHintColor), Padding( padding: EdgeInsets.only(top: 5), - child: Container( + child: widget.showLimitsField ? Container( height: 15, child: Row(mainAxisAlignment: MainAxisAlignment.start, children: [ _min != null @@ -247,7 +255,7 @@ class ExchangeCardState extends State { ), ) : Offstage(), - ])), + ])) : Offstage(), ), !_isAddressEditable && widget.hasRefundAddress ? Padding( @@ -261,10 +269,11 @@ class ExchangeCardState extends State { )) : Offstage(), _isAddressEditable + ? widget.showAddressField ? FocusTraversalOrder( order: NumericFocusOrder(2), child: Padding( - padding: EdgeInsets.only(top: 20), + padding: widget.addressRowPadding ?? EdgeInsets.only(top: 20), child: AddressTextField( addressKey: ValueKey('${_cardInstanceName}_editable_address_textfield_key'), focusNode: widget.addressFocusNode, @@ -280,26 +289,29 @@ class ExchangeCardState extends State { widget.amountFocusNode?.requestFocus(); amountController.text = paymentRequest.amount; }, - placeholder: widget.hasRefundAddress ? S.of(context).refund_address : null, + placeholder: + widget.hasRefundAddress ? S.of(context).refund_address : null, options: [ AddressTextFieldOption.paste, AddressTextFieldOption.qrCode, AddressTextFieldOption.addressBook, ], isBorderExist: false, - textStyle: - TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Colors.white), + textStyle: TextStyle( + fontSize: 16, fontWeight: FontWeight.w600, color: Colors.white), hintStyle: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, - color: Theme.of(context).extension()!.hintTextColor), + color: + Theme.of(context).extension()!.hintTextColor), buttonColor: widget.addressButtonsColor, validator: widget.addressTextFieldValidator, onPushPasteButton: widget.onPushPasteButton, onPushAddressBookButton: widget.onPushAddressBookButton, selectedCurrency: _selectedCurrency), ), - ) + ) + : Offstage() : Padding( padding: EdgeInsets.only(top: 10), child: Builder( @@ -402,7 +414,7 @@ class ExchangeCardState extends State { hintText: S.of(context).search_currency, isMoneroWallet: _isMoneroWallet, isConvertFrom: widget.hasRefundAddress, - onItemSelected: (Currency item) => widget.onCurrencySelected(item as CryptoCurrency), + onItemSelected: (Currency item) => widget.onCurrencySelected(item as T), ), ); } @@ -424,4 +436,10 @@ class ExchangeCardState extends State { actionLeftButton: () => Navigator.of(dialogContext).pop()); }); } + + String _normalizeAddressFormat(String address) { + if (address.startsWith('bitcoincash:')) address = address.substring(12); + return address; + } } + diff --git a/lib/src/screens/exchange/widgets/mobile_exchange_cards_section.dart b/lib/src/screens/exchange/widgets/mobile_exchange_cards_section.dart index 126bca835..d53f16339 100644 --- a/lib/src/screens/exchange/widgets/mobile_exchange_cards_section.dart +++ b/lib/src/screens/exchange/widgets/mobile_exchange_cards_section.dart @@ -1,20 +1,29 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/screens/new_wallet/widgets/select_button.dart'; import 'package:cake_wallet/themes/extensions/exchange_page_theme.dart'; +import 'package:cake_wallet/themes/extensions/send_page_theme.dart'; import 'package:flutter/material.dart'; class MobileExchangeCardsSection extends StatelessWidget { final Widget firstExchangeCard; final Widget secondExchangeCard; + final bool isBuySellOption; + final VoidCallback? onBuyTap; + final VoidCallback? onSellTap; const MobileExchangeCardsSection({ Key? key, required this.firstExchangeCard, required this.secondExchangeCard, + this.isBuySellOption = false, + this.onBuyTap, + this.onSellTap, }) : super(key: key); @override Widget build(BuildContext context) { return Container( - padding: EdgeInsets.only(bottom: 32), + padding: EdgeInsets.only(bottom: isBuySellOption ? 8 : 32), decoration: BoxDecoration( borderRadius: BorderRadius.only( bottomLeft: Radius.circular(24), @@ -45,8 +54,18 @@ class MobileExchangeCardsSection extends StatelessWidget { end: Alignment.bottomRight, ), ), - padding: EdgeInsets.fromLTRB(24, 100, 24, 32), - child: firstExchangeCard, + padding: EdgeInsets.fromLTRB(24, 90, 24, isBuySellOption ? 8 : 32), + child: Column( + children: [ + if (isBuySellOption) Column( + children: [ + const SizedBox(height: 16), + BuySellOptionButtons(onBuyTap: onBuyTap, onSellTap: onSellTap), + ], + ), + firstExchangeCard, + ], + ), ), Padding( padding: EdgeInsets.only(top: 29, left: 24, right: 24), @@ -57,3 +76,69 @@ class MobileExchangeCardsSection extends StatelessWidget { ); } } + +class BuySellOptionButtons extends StatefulWidget { + final VoidCallback? onBuyTap; + final VoidCallback? onSellTap; + + const BuySellOptionButtons({this.onBuyTap, this.onSellTap}); + + @override + _BuySellOptionButtonsState createState() => _BuySellOptionButtonsState(); +} + +class _BuySellOptionButtonsState extends State { + bool isBuySelected = true; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(bottom: 24), + child: Row( + children: [ + Expanded(flex: 2, child: SizedBox()), + Expanded( + flex: 5, + child: SelectButton( + height: 44, + text: S.of(context).buy, + isSelected: isBuySelected, + showTrailingIcon: false, + textColor: Colors.white, + image: Image.asset('assets/images/buy.png', height: 25, width: 25), + padding: EdgeInsets.only(left: 10, right: 30), + color: isBuySelected + ? null + : Theme.of(context).extension()!.textFieldButtonColor, + onTap: () { + setState(() => isBuySelected = true); + if (widget.onBuyTap != null) widget.onBuyTap!(); + }, + ), + ), + Expanded(child: const SizedBox()), + Expanded( + flex: 5, + child: SelectButton( + height: 44, + text: S.of(context).sell, + isSelected: !isBuySelected, + showTrailingIcon: false, + textColor: Colors.white, + image: Image.asset('assets/images/sell.png', height: 25, width: 25), + padding: EdgeInsets.only(left: 10, right: 30), + color: !isBuySelected + ? null + : Theme.of(context).extension()!.textFieldButtonColor, + onTap: () { + setState(() => isBuySelected = false); + if (widget.onSellTap != null) widget.onSellTap!(); + }, + ), + ), + Expanded(flex: 2, child: SizedBox()), + ], + ), + ); + } +} diff --git a/lib/src/screens/exchange_trade/exchange_trade_page.dart b/lib/src/screens/exchange_trade/exchange_trade_page.dart index 0f3cc7bd9..130ffa6b0 100644 --- a/lib/src/screens/exchange_trade/exchange_trade_page.dart +++ b/lib/src/screens/exchange_trade/exchange_trade_page.dart @@ -277,7 +277,7 @@ class ExchangeTradeState extends State { actionRightButton: () async { Navigator.of(popupContext).pop(); await widget.exchangeTradeViewModel.sendViewModel - .commitTransaction(); + .commitTransaction(context); transactionStatePopup(); }, actionLeftButton: () => Navigator.of(popupContext).pop(), diff --git a/lib/src/screens/restore/restore_options_page.dart b/lib/src/screens/restore/restore_options_page.dart index 299846a72..d1d419d60 100644 --- a/lib/src/screens/restore/restore_options_page.dart +++ b/lib/src/screens/restore/restore_options_page.dart @@ -56,8 +56,9 @@ class _RestoreOptionsBodyState extends State<_RestoreOptionsBody> { } if (isMoneroOnly) { - return DeviceConnectionType.supportedConnectionTypes(WalletType.monero, Platform.isIOS) - .isNotEmpty; + // return DeviceConnectionType.supportedConnectionTypes(WalletType.monero, Platform.isIOS) + // .isNotEmpty; + return false; } return true; diff --git a/lib/src/screens/seed/pre_seed_page.dart b/lib/src/screens/seed/pre_seed_page.dart index 23de4564f..475f45fca 100644 --- a/lib/src/screens/seed/pre_seed_page.dart +++ b/lib/src/screens/seed/pre_seed_page.dart @@ -1,6 +1,6 @@ import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/routes.dart'; -import 'package:cake_wallet/src/screens/InfoPage.dart'; +import 'package:cake_wallet/src/screens/Info_page.dart'; import 'package:flutter/cupertino.dart'; class PreSeedPage extends InfoPage { diff --git a/lib/src/screens/select_options_page.dart b/lib/src/screens/select_options_page.dart new file mode 100644 index 000000000..70cb2abc1 --- /dev/null +++ b/lib/src/screens/select_options_page.dart @@ -0,0 +1,199 @@ +import 'package:cake_wallet/core/selectable_option.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/src/widgets/primary_button.dart'; +import 'package:cake_wallet/src/widgets/provider_optoin_tile.dart'; +import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; +import 'package:cake_wallet/themes/extensions/option_tile_theme.dart'; +import 'package:cake_wallet/themes/extensions/transaction_trade_theme.dart'; +import 'package:flutter/material.dart'; + +abstract class SelectOptionsPage extends BasePage { + SelectOptionsPage(); + + String get pageTitle; + + EdgeInsets? get contentPadding; + + EdgeInsets? get tilePadding; + + EdgeInsets? get innerPadding; + + double? get imageHeight; + + double? get imageWidth; + + Color? get selectedBackgroundColor; + + double? get tileBorderRadius; + + String get bottomSectionText; + + bool get primaryButtonEnabled => true; + + String get primaryButtonText => ''; + + List get items; + + void Function(SelectableOption option)? get onOptionTap; + + void Function(BuildContext context)? get primaryButtonAction; + + @override + String get title => pageTitle; + + @override + Widget body(BuildContext context) { + return ScrollableWithBottomSection( + content: BodySelectOptionsPage( + items: items, + onOptionTap: onOptionTap, + tilePadding: tilePadding, + tileBorderRadius: tileBorderRadius, + imageHeight: imageHeight, + imageWidth: imageWidth, + innerPadding: innerPadding), + bottomSection: Padding( + padding: contentPadding ?? EdgeInsets.zero, + child: Column( + children: [ + Text( + bottomSectionText, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + color: Theme.of(context).extension()!.detailsTitlesColor, + ), + ), + if (primaryButtonEnabled) + LoadingPrimaryButton( + text: primaryButtonText, + onPressed: () { + primaryButtonAction != null + ? primaryButtonAction!(context) + : Navigator.pop(context); + }, + color: Theme.of(context).primaryColor, + textColor: Colors.white, + isDisabled: false, + isLoading: false) + ], + ), + ), + ); + } +} + +class BodySelectOptionsPage extends StatefulWidget { + const BodySelectOptionsPage({ + required this.items, + this.onOptionTap, + this.tilePadding, + this.tileBorderRadius, + this.imageHeight, + this.imageWidth, + this.innerPadding, + }); + + final List items; + final void Function(SelectableOption option)? onOptionTap; + final EdgeInsets? tilePadding; + final double? tileBorderRadius; + final double? imageHeight; + final double? imageWidth; + final EdgeInsets? innerPadding; + + @override + _BodySelectOptionsPageState createState() => _BodySelectOptionsPageState(); +} + +class _BodySelectOptionsPageState extends State { + late List _items; + + @override + void initState() { + super.initState(); + _items = widget.items; + } + + void _handleOptionTap(SelectableOption option) { + setState(() { + for (var item in _items) { + if (item is SelectableOption) { + item.isOptionSelected = false; + } + } + option.isOptionSelected = true; + }); + widget.onOptionTap?.call(option); + } + + @override + Widget build(BuildContext context) { + final isLightMode = Theme.of(context).extension()?.useDarkImage ?? false; + + Color titleColor = + isLightMode ? Theme.of(context).appBarTheme.titleTextStyle!.color! : Colors.white; + + return Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 350), + child: Column( + children: _items.map((item) { + if (item is OptionTitle) { + return Padding( + padding: const EdgeInsets.only(top: 18, bottom: 8), + child: Container( + width: double.infinity, + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: titleColor, + width: 1, + ), + ), + ), + child: Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Text( + item.title, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w700, + color: titleColor, + ), + ), + ), + ), + ); + } else if (item is SelectableOption) { + return Padding( + padding: widget.tilePadding ?? const EdgeInsets.only(top: 24), + child: ProviderOptionTile( + title: item.title, + lightImagePath: item.lightIconPath, + darkImagePath: item.darkIconPath, + imageHeight: widget.imageHeight, + imageWidth: widget.imageWidth, + padding: widget.innerPadding, + description: item.description, + topLeftSubTitle: item.topLeftSubTitle, + topRightSubTitle: item.topRightSubTitle, + rightSubTitleLightIconPath: item.topRightSubTitleLightIconPath, + rightSubTitleDarkIconPath: item.topRightSubTitleDarkIconPath, + bottomLeftSubTitle: item.bottomLeftSubTitle, + badges: item.badges, + isSelected: item.isOptionSelected, + borderRadius: widget.tileBorderRadius, + isLightMode: isLightMode, + onPressed: () => _handleOptionTap(item), + ), + ); + } + return const SizedBox.shrink(); + }).toList(), + ), + ), + ); + } +} diff --git a/lib/src/screens/send/send_page.dart b/lib/src/screens/send/send_page.dart index bce82312d..7003ceafb 100644 --- a/lib/src/screens/send/send_page.dart +++ b/lib/src/screens/send/send_page.dart @@ -498,7 +498,7 @@ class SendPage extends BasePage { ValueKey('send_page_confirm_sending_dialog_cancel_button_key'), actionRightButton: () async { Navigator.of(_dialogContext).pop(); - sendViewModel.commitTransaction(); + sendViewModel.commitTransaction(context); await showPopUp( context: context, builder: (BuildContext _dialogContext) { diff --git a/lib/src/screens/settings/desktop_settings/desktop_settings_page.dart b/lib/src/screens/settings/desktop_settings/desktop_settings_page.dart index 79f74065a..6f7afe2ff 100644 --- a/lib/src/screens/settings/desktop_settings/desktop_settings_page.dart +++ b/lib/src/screens/settings/desktop_settings/desktop_settings_page.dart @@ -4,6 +4,7 @@ import 'package:cake_wallet/src/widgets/setting_action_button.dart'; import 'package:cake_wallet/src/widgets/setting_actions.dart'; import 'package:cake_wallet/typography.dart'; import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; +import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; import 'package:cake_wallet/router.dart' as Router; import 'package:cake_wallet/themes/extensions/menu_theme.dart'; @@ -60,8 +61,10 @@ class _DesktopSettingsPageState extends State { return Container(); } - if (!widget.dashboardViewModel.hasMweb && - item.name(context) == S.of(context).litecoin_mweb_settings) { + if ((!widget.dashboardViewModel.isMoneroViewOnly && + item.name(context) == S.of(context).export_outputs) || + (!widget.dashboardViewModel.hasMweb && + item.name(context) == S.of(context).litecoin_mweb_settings)) { return Container(); } diff --git a/lib/src/screens/settings/other_settings_page.dart b/lib/src/screens/settings/other_settings_page.dart index 137f699f5..f6a6288f5 100644 --- a/lib/src/screens/settings/other_settings_page.dart +++ b/lib/src/screens/settings/other_settings_page.dart @@ -57,22 +57,6 @@ class OtherSettingsPage extends BasePage { handler: (BuildContext context) => Navigator.of(context).pushNamed(Routes.changeRep), ), - if(_otherSettingsViewModel.isEnabledBuyAction) - SettingsPickerCell( - title: S.current.default_buy_provider, - items: _otherSettingsViewModel.availableBuyProvidersTypes, - displayItem: _otherSettingsViewModel.getBuyProviderType, - selectedItem: _otherSettingsViewModel.buyProviderType, - onItemSelected: _otherSettingsViewModel.onBuyProviderTypeSelected - ), - if(_otherSettingsViewModel.isEnabledSellAction) - SettingsPickerCell( - title: S.current.default_sell_provider, - items: _otherSettingsViewModel.availableSellProvidersTypes, - displayItem: _otherSettingsViewModel.getSellProviderType, - selectedItem: _otherSettingsViewModel.sellProviderType, - onItemSelected: _otherSettingsViewModel.onSellProviderTypeSelected, - ), SettingsCellWithArrow( title: S.current.settings_terms_and_conditions, handler: (BuildContext context) => diff --git a/lib/src/screens/settings/privacy_page.dart b/lib/src/screens/settings/privacy_page.dart index 53e7686e8..8652c4af6 100644 --- a/lib/src/screens/settings/privacy_page.dart +++ b/lib/src/screens/settings/privacy_page.dart @@ -73,16 +73,10 @@ class PrivacyPage extends BasePage { _privacySettingsViewModel.setIsAppSecure(value); }), SettingsSwitcherCell( - title: S.current.disable_buy, - value: _privacySettingsViewModel.disableBuy, + title: S.current.disable_trade_option, + value: _privacySettingsViewModel.disableTradeOption, onValueChange: (BuildContext _, bool value) { - _privacySettingsViewModel.setDisableBuy(value); - }), - SettingsSwitcherCell( - title: S.current.disable_sell, - value: _privacySettingsViewModel.disableSell, - onValueChange: (BuildContext _, bool value) { - _privacySettingsViewModel.setDisableSell(value); + _privacySettingsViewModel.setDisableTradeOption(value); }), SettingsSwitcherCell( title: S.current.disable_bulletin, diff --git a/lib/src/screens/setup_2fa/setup_2fa_info_page.dart b/lib/src/screens/setup_2fa/setup_2fa_info_page.dart index 8aa0ac3c9..834d01c26 100644 --- a/lib/src/screens/setup_2fa/setup_2fa_info_page.dart +++ b/lib/src/screens/setup_2fa/setup_2fa_info_page.dart @@ -1,6 +1,6 @@ import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/routes.dart'; -import 'package:cake_wallet/src/screens/InfoPage.dart'; +import 'package:cake_wallet/src/screens/Info_page.dart'; import 'package:flutter/cupertino.dart'; class Setup2FAInfoPage extends InfoPage { diff --git a/lib/src/screens/transaction_details/rbf_details_page.dart b/lib/src/screens/transaction_details/rbf_details_page.dart index b117a0b68..2c5edd8b4 100644 --- a/lib/src/screens/transaction_details/rbf_details_page.dart +++ b/lib/src/screens/transaction_details/rbf_details_page.dart @@ -168,7 +168,7 @@ class RBFDetailsPage extends BasePage { leftButtonText: S.of(popupContext).cancel, actionRightButton: () async { Navigator.of(popupContext).pop(); - await transactionDetailsViewModel.sendViewModel.commitTransaction(); + await transactionDetailsViewModel.sendViewModel.commitTransaction(context); try { Navigator.of(popupContext).pop(); } catch (_) {} diff --git a/lib/src/screens/ur/animated_ur_page.dart b/lib/src/screens/ur/animated_ur_page.dart new file mode 100644 index 000000000..dc40e6a12 --- /dev/null +++ b/lib/src/screens/ur/animated_ur_page.dart @@ -0,0 +1,184 @@ +import 'dart:async'; + +import 'package:cake_wallet/di.dart'; +import 'package:cake_wallet/entities/qr_scanner.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/monero/monero.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/src/screens/receive/widgets/qr_image.dart'; +import 'package:cake_wallet/src/screens/send/widgets/confirm_sending_alert.dart'; +import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; +import 'package:cake_wallet/src/widgets/primary_button.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; +import 'package:cake_wallet/view_model/animated_ur_model.dart'; +import 'package:cake_wallet/view_model/dashboard/wallet_balance.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'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +// ur:xmr-txunsigned - unsigned transaction +// should show a scanner afterwards. + +class AnimatedURPage extends BasePage { + final bool isAll; + AnimatedURPage(this.animatedURmodel, {required String urQr, this.isAll = false}) { + if (urQr == "export-outputs") { + this.urQr = monero!.exportOutputsUR(animatedURmodel.wallet, false); + } else if (urQr == "export-outputs-all") { + this.urQr = monero!.exportOutputsUR(animatedURmodel.wallet, true); + } else { + this.urQr = urQr; + } + } + + late String urQr; + + final AnimatedURModel animatedURmodel; + + String get urQrType { + final first = urQr.trim().split("\n")[0]; + return first.split('/')[0]; + } + + @override + Widget body(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.only(top: 64.0), + child: URQR( + frames: urQr.trim().split("\n"), + ), + ), + SizedBox(height: 32), + if (urQrType == "ur:xmr-txunsigned" || urQrType == "ur:xmr-output") + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: SizedBox( + width: double.maxFinite, + child: PrimaryButton( + onPressed: () => _continue(context), + text: "Continue", + color: Theme.of(context).primaryColor, + textColor: Colors.white, + ), + ), + ), + SizedBox(height: 32), + if (urQrType == "ur:xmr-output" && !isAll) Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: SizedBox( + width: double.maxFinite, + child: PrimaryButton( + onPressed: () => _exportAll(context), + text: "Export all", + color: Theme.of(context).colorScheme.secondary, + textColor: Colors.white, + ), + ), + ), + ], + ); + } + + void _exportAll(BuildContext context) { + Navigator.of(context).pushReplacement( + MaterialPageRoute( + builder: (context) { + return AnimatedURPage(animatedURmodel, urQr: "export-outputs-all", isAll: true); + }, + ), + ); + } + + Future _continue(BuildContext context) async { + try { + switch (urQrType) { + case "ur:xmr-txunsigned": // ur:xmr-txsigned + final ur = await presentQRScanner(context); + final result = await monero!.commitTransactionUR(animatedURmodel.wallet, ur); + if (result) { + Navigator.of(context).pop(true); + } + break; + case "ur:xmr-output": // xmr-keyimage + final ur = await presentQRScanner(context); + final result = await monero!.importKeyImagesUR(animatedURmodel.wallet, ur); + if (result) { + Navigator.of(context).pop(true); + } + break; + default: + throw UnimplementedError("unable to handle UR: ${urQrType}"); + } + } catch (e) { + await showPopUp( + context: context, + builder: (BuildContext context) { + return AlertWithOneAction( + alertTitle: S.of(context).error, + alertContent: e.toString(), + buttonText: S.of(context).ok, + buttonAction: () => Navigator.pop(context, true)); + }); + } + } +} + +class URQR extends StatefulWidget { + URQR({super.key, required this.frames}); + + List frames; + + @override + // ignore: library_private_types_in_public_api + _URQRState createState() => _URQRState(); +} + +const urFrameTime = 1000 ~/ 5; + +class _URQRState extends State { + Timer? t; + int frame = 0; + @override + void initState() { + super.initState(); + setState(() { + t = Timer.periodic(const Duration(milliseconds: urFrameTime), (timer) { + _nextFrame(); + }); + }); + } + + void _nextFrame() { + setState(() { + frame++; + }); + } + + @override + void dispose() { + t?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Center( + child: QrImage( + data: widget.frames[frame % widget.frames.length], version: -1, + size: 400, + ), + ), + ], + ); + } +} \ No newline at end of file diff --git a/lib/src/screens/wallet_connect/wc_connections_listing_view.dart b/lib/src/screens/wallet_connect/wc_connections_listing_view.dart index eda2a748f..3a78f6af6 100644 --- a/lib/src/screens/wallet_connect/wc_connections_listing_view.dart +++ b/lib/src/screens/wallet_connect/wc_connections_listing_view.dart @@ -65,7 +65,7 @@ class WCPairingsWidget extends BasePage { bool isCameraPermissionGranted = await PermissionHandler.checkPermission(Permission.camera, context); if (!isCameraPermissionGranted) return; - uri = await presentQRScanner(); + uri = await presentQRScanner(context); } else { uri = await _showEnterWalletConnectURIPopUp(context); } diff --git a/lib/src/screens/wallet_list/wallet_list_page.dart b/lib/src/screens/wallet_list/wallet_list_page.dart index a9a9d9413..46eaa6143 100644 --- a/lib/src/screens/wallet_list/wallet_list_page.dart +++ b/lib/src/screens/wallet_list/wallet_list_page.dart @@ -1,44 +1,56 @@ +import 'package:another_flushbar/flushbar.dart'; +import 'package:cake_wallet/core/auth_service.dart'; import 'package:cake_wallet/core/new_wallet_arguments.dart'; import 'package:cake_wallet/entities/wallet_edit_page_arguments.dart'; import 'package:cake_wallet/entities/wallet_list_order_types.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/monero/monero.dart'; +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/src/screens/auth/auth_page.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/src/screens/connect_device/connect_device_page.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/filter_list_widget.dart'; import 'package:cake_wallet/src/screens/new_wallet/widgets/grouped_wallet_expansion_tile.dart'; import 'package:cake_wallet/src/screens/wallet_list/edit_wallet_button_widget.dart'; import 'package:cake_wallet/src/screens/wallet_list/filtered_list.dart'; import 'package:cake_wallet/src/screens/wallet_unlock/wallet_unlock_arguments.dart'; +import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; +import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; -import 'package:cake_wallet/src/screens/auth/auth_page.dart'; -import 'package:cake_wallet/core/auth_service.dart'; import 'package:cake_wallet/themes/extensions/filter_theme.dart'; import 'package:cake_wallet/themes/extensions/wallet_list_theme.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cake_wallet/utils/show_bar.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/wallet_list/wallet_list_item.dart'; -import 'package:another_flushbar/flushbar.dart'; +import 'package:cake_wallet/view_model/wallet_list/wallet_list_view_model.dart'; +import 'package:cake_wallet/wallet_type_utils.dart'; +import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; -import 'package:cake_wallet/routes.dart'; -import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cw_core/wallet_type.dart'; -import 'package:cake_wallet/view_model/wallet_list/wallet_list_view_model.dart'; -import 'package:cake_wallet/src/widgets/primary_button.dart'; -import 'package:cake_wallet/src/screens/base_page.dart'; -import 'package:cake_wallet/wallet_type_utils.dart'; class WalletListPage extends BasePage { - WalletListPage({required this.walletListViewModel, required this.authService}); + WalletListPage({ + required this.walletListViewModel, + required this.authService, + this.onWalletLoaded, + }); final WalletListViewModel walletListViewModel; final AuthService authService; + final Function(BuildContext)? onWalletLoaded; @override String get title => S.current.wallets; @override - Widget body(BuildContext context) => - WalletListBody(walletListViewModel: walletListViewModel, authService: authService); + Widget body(BuildContext context) => WalletListBody( + walletListViewModel: walletListViewModel, + authService: authService, + onWalletLoaded: + onWalletLoaded ?? (context) => Navigator.of(context).pop(), + ); @override Widget trailing(BuildContext context) { @@ -89,10 +101,15 @@ class WalletListPage extends BasePage { } class WalletListBody extends StatefulWidget { - WalletListBody({required this.walletListViewModel, required this.authService}); + WalletListBody({ + required this.walletListViewModel, + required this.authService, + required this.onWalletLoaded, + }); final WalletListViewModel walletListViewModel; final AuthService authService; + final Function(BuildContext) onWalletLoaded; @override WalletListBodyState createState() => WalletListBodyState(); @@ -118,8 +135,8 @@ class WalletListBodyState extends State { @override Widget build(BuildContext context) { - final newWalletImage = - Image.asset('assets/images/new_wallet.png', height: 12, width: 12, color: Colors.white); + final newWalletImage = Image.asset('assets/images/new_wallet.png', + height: 12, width: 12, color: Colors.white); final restoreWalletImage = Image.asset('assets/images/restore_wallet.png', height: 12, width: 12, @@ -180,8 +197,7 @@ class WalletListBodyState extends State { trailingWidget: EditWalletButtonWidget( width: 74, isGroup: true, - isExpanded: - widget.walletListViewModel.expansionTileStateTrack[index]!, + isExpanded: widget.walletListViewModel.expansionTileStateTrack[index]!, onTap: () { final wallet = widget.walletListViewModel .convertWalletInfoToWalletListItem(group.wallets.first); @@ -198,8 +214,7 @@ class WalletListBodyState extends State { }, ), childWallets: group.wallets.map((walletInfo) { - return widget.walletListViewModel - .convertWalletInfoToWalletListItem(walletInfo); + return widget.walletListViewModel.convertWalletInfoToWalletListItem(walletInfo); }).toList(), isSelected: false, onChildItemTapped: (wallet) => @@ -329,8 +344,7 @@ class WalletListBodyState extends State { arguments: NewWalletArguments( type: widget.walletListViewModel.currentWalletType, ), - conditionToDetermineIfToUse2FA: - widget.walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets, + conditionToDetermineIfToUse2FA: widget.walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets, ); } else { Navigator.of(context).pushNamed( @@ -345,8 +359,7 @@ class WalletListBodyState extends State { widget.authService.authenticateAction( context, route: Routes.newWalletType, - conditionToDetermineIfToUse2FA: - widget.walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets, + conditionToDetermineIfToUse2FA: widget.walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets, ); } else { Navigator.of(context).pushNamed(Routes.newWalletType); @@ -367,8 +380,7 @@ class WalletListBodyState extends State { context, route: Routes.restoreOptions, arguments: false, - conditionToDetermineIfToUse2FA: - widget.walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets, + conditionToDetermineIfToUse2FA: widget.walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets, ); } else { Navigator.of(context).pushNamed(Routes.restoreOptions, arguments: false); @@ -387,39 +399,6 @@ class WalletListBodyState extends State { ); } - Image _imageFor({required WalletType type, bool? isTestnet}) { - switch (type) { - case WalletType.bitcoin: - if (isTestnet == true) { - return tBitcoinIcon; - } - return bitcoinIcon; - case WalletType.monero: - return moneroIcon; - case WalletType.litecoin: - return litecoinIcon; - case WalletType.haven: - return havenIcon; - case WalletType.ethereum: - return ethereumIcon; - case WalletType.bitcoinCash: - return bitcoinCashIcon; - case WalletType.nano: - case WalletType.banano: - return nanoIcon; - case WalletType.polygon: - return polygonIcon; - case WalletType.solana: - return solanaIcon; - case WalletType.tron: - return tronIcon; - case WalletType.wownero: - return wowneroIcon; - case WalletType.none: - return nonWalletTypeIcon; - } - } - Future _loadWallet(WalletListItem wallet) async { if (SettingsStoreBase.walletPasswordDirectInput) { Navigator.of(context).pushNamed(Routes.walletUnlockLoadable, @@ -438,12 +417,36 @@ class WalletListBodyState extends State { await widget.authService.authenticateAction( context, onAuthSuccess: (isAuthenticatedSuccessfully) async { - if (!isAuthenticatedSuccessfully) { - return; - } + if (!isAuthenticatedSuccessfully) return; try { - changeProcessText(S.of(context).wallet_list_loading_wallet(wallet.name)); + if (widget.walletListViewModel + .requireHardwareWalletConnection(wallet)) { + await Navigator.of(context).pushNamed( + Routes.connectDevices, + arguments: ConnectDevicePageParams( + walletType: WalletType.monero, + onConnectDevice: (context, ledgerVM) async { + monero!.setGlobalLedgerConnection(ledgerVM.connection); + Navigator.of(context).pop(); + }, + ), + ); + + showPopUp( + context: context, + builder: (BuildContext context) => AlertWithOneAction( + alertTitle: S.of(context).proceed_on_device, + alertContent: S.of(context).proceed_on_device_description, + buttonText: S.of(context).cancel, + buttonAction: () => Navigator.of(context).pop()), + ); + } + + + + changeProcessText( + S.of(context).wallet_list_loading_wallet(wallet.name)); await widget.walletListViewModel.loadWallet(wallet); await hideProgressText(); // only pop the wallets route in mobile as it will go back to dashboard page @@ -451,13 +454,15 @@ class WalletListBodyState extends State { if (responsiveLayoutUtil.shouldRenderMobileUI) { WidgetsBinding.instance.addPostFrameCallback((_) { if (this.mounted) { - Navigator.of(context).pop(); + widget.onWalletLoaded.call(context); } }); } } catch (e) { if (this.mounted) { - changeProcessText(S.of(context).wallet_list_failed_to_load(wallet.name, e.toString())); + changeProcessText(S + .of(context) + .wallet_list_failed_to_load(wallet.name, e.toString())); } } }, diff --git a/lib/src/widgets/address_text_field.dart b/lib/src/widgets/address_text_field.dart index 0b1ef4796..9b407dedb 100644 --- a/lib/src/widgets/address_text_field.dart +++ b/lib/src/widgets/address_text_field.dart @@ -1,20 +1,21 @@ import 'package:cake_wallet/utils/device_info.dart'; import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; +import 'package:cw_core/currency.dart'; import 'package:flutter/services.dart'; import 'package:flutter/material.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/entities/qr_scanner.dart'; import 'package:cake_wallet/entities/contact_base.dart'; -import 'package:cw_core/crypto_currency.dart'; import 'package:cake_wallet/themes/extensions/send_page_theme.dart'; import 'package:cake_wallet/utils/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart'; enum AddressTextFieldOption { paste, qrCode, addressBook, walletAddresses } -class AddressTextField extends StatelessWidget { + +class AddressTextField extends StatelessWidget{ AddressTextField({ required this.controller, this.isActive = true, @@ -58,7 +59,7 @@ class AddressTextField extends StatelessWidget { final Function(BuildContext context)? onPushAddressBookButton; final Function(BuildContext context)? onPushAddressPickerButton; final Function(ContactBase contact)? onSelectedContact; - final CryptoCurrency? selectedCurrency; + final T? selectedCurrency; final Key? addressKey; @override @@ -231,7 +232,7 @@ class AddressTextField extends StatelessWidget { bool isCameraPermissionGranted = await PermissionHandler.checkPermission(Permission.camera, context); if (!isCameraPermissionGranted) return; - final code = await presentQRScanner(); + final code = await presentQRScanner(context); if (code.isEmpty) { return; } diff --git a/lib/src/widgets/provider_optoin_tile.dart b/lib/src/widgets/provider_optoin_tile.dart new file mode 100644 index 000000000..85396a97d --- /dev/null +++ b/lib/src/widgets/provider_optoin_tile.dart @@ -0,0 +1,527 @@ +import 'package:cake_wallet/themes/extensions/option_tile_theme.dart'; +import 'package:cake_wallet/themes/extensions/receive_page_theme.dart'; +import 'package:cake_wallet/typography.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; + +class ProviderOptionTile extends StatelessWidget { + const ProviderOptionTile({ + required this.onPressed, + required this.lightImagePath, + required this.darkImagePath, + required this.title, + this.topLeftSubTitle, + this.topRightSubTitle, + this.bottomLeftSubTitle, + this.bottomRightSubTitle, + this.leftSubTitleIconPath, + this.rightSubTitleLightIconPath, + this.rightSubTitleDarkIconPath, + this.description, + this.badges, + this.borderRadius, + this.imageHeight, + this.imageWidth, + this.padding, + this.titleTextStyle, + this.firstSubTitleTextStyle, + this.secondSubTitleTextStyle, + this.leadingIcon, + this.selectedBackgroundColor, + this.isSelected = false, + required this.isLightMode, + }); + + final VoidCallback onPressed; + final String lightImagePath; + final String darkImagePath; + final String title; + final String? topLeftSubTitle; + final String? topRightSubTitle; + final String? bottomLeftSubTitle; + final String? bottomRightSubTitle; + final String? leftSubTitleIconPath; + final String? rightSubTitleLightIconPath; + final String? rightSubTitleDarkIconPath; + final String? description; + final List? badges; + final double? borderRadius; + final double? imageHeight; + final double? imageWidth; + final EdgeInsets? padding; + final TextStyle? titleTextStyle; + final TextStyle? firstSubTitleTextStyle; + final TextStyle? secondSubTitleTextStyle; + final IconData? leadingIcon; + final Color? selectedBackgroundColor; + final bool isSelected; + final bool isLightMode; + + @override + Widget build(BuildContext context) { + final backgroundColor = isSelected + ? isLightMode + ? Theme.of(context).extension()!.currentTileBackgroundColor + : Theme.of(context).extension()!.titleColor + : Theme.of(context).cardColor; + + final textColor = isSelected + ? isLightMode + ? Colors.white + : Theme.of(context).cardColor + : Theme.of(context).extension()!.titleColor; + + final badgeColor = isSelected + ? Theme.of(context).cardColor + : Theme.of(context).extension()!.titleColor; + + final badgeTextColor = isSelected + ? Theme.of(context).extension()!.titleColor + : Theme.of(context).cardColor; + + final imagePath = isSelected + ? isLightMode + ? darkImagePath + : lightImagePath + : isLightMode + ? lightImagePath + : darkImagePath; + + final rightSubTitleIconPath = isSelected + ? isLightMode + ? rightSubTitleDarkIconPath + : rightSubTitleLightIconPath + : isLightMode + ? rightSubTitleLightIconPath + : rightSubTitleDarkIconPath; + + return GestureDetector( + onTap: onPressed, + child: Container( + width: double.infinity, + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(borderRadius ?? 12)), + border: isSelected && !isLightMode ? Border.all(color: textColor) : null, + color: backgroundColor, + ), + child: Padding( + padding: padding ?? const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Row( + children: [ + getImage(imagePath, height: imageHeight, width: imageWidth), + SizedBox(width: 8), + Expanded( + child: Container( + child: Row( + children: [ + Expanded( + child: Text(title, + style: titleTextStyle ?? textLargeBold(color: textColor))), + Row( + children: [ + if (leadingIcon != null) + Icon(leadingIcon, size: 16, color: textColor), + ], + ) + ], + ), + ), + ), + ], + ), + if (topLeftSubTitle != null || topRightSubTitle != null) + subTitleWidget( + leftSubTitle: topLeftSubTitle, + subTitleIconPath: leftSubTitleIconPath, + textColor: textColor, + rightSubTitle: topRightSubTitle, + rightSubTitleIconPath: rightSubTitleIconPath), + if (bottomLeftSubTitle != null || bottomRightSubTitle != null) + subTitleWidget( + leftSubTitle: bottomLeftSubTitle, + textColor: textColor, + subTitleFontSize: 12), + if (badges != null && badges!.isNotEmpty) + Padding( + padding: const EdgeInsets.only(top: 12), + child: Row(children: [ + ...badges! + .map((badge) => Badge( + title: badge, textColor: badgeTextColor, backgroundColor: badgeColor)) + .toList() + ]), + ) + ], + ), + ), + ), + ); + } +} + +class subTitleWidget extends StatelessWidget { + const subTitleWidget({ + super.key, + this.leftSubTitle, + this.subTitleIconPath, + required this.textColor, + this.rightSubTitle, + this.rightSubTitleIconPath, + this.subTitleFontSize = 16, + }); + + final String? leftSubTitle; + final String? subTitleIconPath; + final Color textColor; + final String? rightSubTitle; + final String? rightSubTitleIconPath; + final double subTitleFontSize; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + leftSubTitle != null || subTitleIconPath != null + ? Row( + children: [ + if (subTitleIconPath != null && subTitleIconPath!.isNotEmpty) + Padding( + padding: const EdgeInsets.only(right: 6), + child: getImage(subTitleIconPath!), + ), + Text( + leftSubTitle ?? '', + style: TextStyle( + fontSize: subTitleFontSize, + fontWeight: FontWeight.w700, + color: textColor), + ), + ], + ) + : Offstage(), + rightSubTitle != null || rightSubTitleIconPath != null + ? Row( + children: [ + if (rightSubTitleIconPath != null && rightSubTitleIconPath!.isNotEmpty) + Padding( + padding: const EdgeInsets.only(right: 4), + child: getImage(rightSubTitleIconPath!, imageColor: textColor), + ), + Text( + rightSubTitle ?? '', + style: TextStyle( + fontSize: subTitleFontSize, fontWeight: FontWeight.w700, color: textColor), + ), + ], + ) + : Offstage(), + ], + ); + } +} + +class Badge extends StatelessWidget { + Badge({required this.textColor, required this.backgroundColor, required this.title}); + + final String title; + final Color textColor; + final Color backgroundColor; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(right: 8), + child: FittedBox( + fit: BoxFit.fitHeight, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4), + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(24)), color: backgroundColor), + alignment: Alignment.center, + child: Text( + title, + style: TextStyle( + color: textColor, + fontSize: 12, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ); + } +} + +Widget getImage(String imagePath, {double? height, double? width, Color? imageColor}) { + final bool isNetworkImage = imagePath.startsWith('http') || imagePath.startsWith('https'); + final bool isSvg = imagePath.endsWith('.svg'); + final double imageHeight = height ?? 35; + final double imageWidth = width ?? 35; + + if (isNetworkImage) { + return isSvg + ? SvgPicture.network( + imagePath, + height: imageHeight, + width: imageWidth, + colorFilter: imageColor != null ? ColorFilter.mode(imageColor, BlendMode.srcIn) : null, + placeholderBuilder: (BuildContext context) => Container( + height: imageHeight, + width: imageWidth, + child: Center( + child: CircularProgressIndicator(), + ), + ), + ) + : Image.network( + imagePath, + height: imageHeight, + width: imageWidth, + loadingBuilder: (BuildContext context, Widget child, ImageChunkEvent? loadingProgress) { + if (loadingProgress == null) { + return child; + } + return Container( + height: imageHeight, + width: imageWidth, + child: Center( + child: CircularProgressIndicator( + value: loadingProgress.expectedTotalBytes != null + ? loadingProgress.cumulativeBytesLoaded / + loadingProgress.expectedTotalBytes! + : null, + ), + ), + ); + }, + errorBuilder: (BuildContext context, Object exception, StackTrace? stackTrace) { + return Container( + height: imageHeight, + width: imageWidth, + ); + }, + ); + } else { + return isSvg + ? SvgPicture.asset( + imagePath, + height: imageHeight, + width: imageWidth, + colorFilter: imageColor != null ? ColorFilter.mode(imageColor, BlendMode.srcIn) : null, + ) + : Image.asset(imagePath, height: imageHeight, width: imageWidth); + } +} + +class OptionTilePlaceholder extends StatefulWidget { + OptionTilePlaceholder({ + this.borderRadius, + this.imageHeight, + this.imageWidth, + this.padding, + this.leadingIcon, + this.withBadge = true, + this.withSubtitle = true, + this.isDarkTheme = false, + this.errorText, + }); + + final double? borderRadius; + final double? imageHeight; + final double? imageWidth; + final EdgeInsets? padding; + final IconData? leadingIcon; + final bool withBadge; + final bool withSubtitle; + final bool isDarkTheme; + final String? errorText; + + @override + _OptionTilePlaceholderState createState() => _OptionTilePlaceholderState(); +} + +class _OptionTilePlaceholderState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _animation; + + @override + void initState() { + super.initState(); + + _controller = AnimationController( + duration: const Duration(seconds: 2), + vsync: this, + )..repeat(); + + _animation = CurvedAnimation( + parent: _controller, + curve: Curves.linear, + ); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final backgroundColor = Theme.of(context).cardColor; + final titleColor = Theme.of(context).extension()!.titleColor.withOpacity(0.4); + + return widget.errorText != null + ? Container( + width: double.infinity, + padding: widget.padding ?? EdgeInsets.all(16), + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(widget.borderRadius ?? 12)), + color: backgroundColor, + ), + child: Column( + children: [ + Text( + widget.errorText!, + style: TextStyle( + color: titleColor, + fontSize: 16, + ), + ), + if (widget.withSubtitle) SizedBox(height: 8), + Text( + '', + style: TextStyle( + color: titleColor, + fontSize: 16, + ), + ), + ], + ), + ) + : AnimatedBuilder( + animation: _animation, + builder: (context, child) { + return Stack( + children: [ + Container( + width: double.infinity, + padding: widget.padding ?? EdgeInsets.all(16), + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(widget.borderRadius ?? 12)), + color: backgroundColor, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Row( + children: [ + Container( + height: widget.imageHeight ?? 35, + width: widget.imageWidth ?? 35, + decoration: BoxDecoration( + color: titleColor, + shape: BoxShape.circle, + ), + ), + SizedBox(width: 8), + Expanded( + child: Container( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + height: 20, + width: 70, + decoration: BoxDecoration( + color: titleColor, + borderRadius: BorderRadius.all(Radius.circular(8)), + ), + ), + if (widget.leadingIcon != null) + Icon(widget.leadingIcon, size: 16, color: titleColor), + ], + ), + ), + ), + ], + ), + if (widget.withSubtitle) + Padding( + padding: EdgeInsets.symmetric(vertical: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + height: 20, + width: 170, + decoration: BoxDecoration( + color: titleColor, + borderRadius: BorderRadius.all(Radius.circular(8)), + ), + ), + ], + ), + ), + if (widget.withBadge) + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: Row( + children: [ + Container( + height: 30, + width: 70, + decoration: BoxDecoration( + color: titleColor, + borderRadius: BorderRadius.all(Radius.circular(20)), + ), + ), + SizedBox(width: 8), + Container( + height: 30, + width: 70, + decoration: BoxDecoration( + color: titleColor, + borderRadius: BorderRadius.all(Radius.circular(20)), + ), + ), + ], + ), + ), + ], + ), + ), + Positioned.fill( + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(widget.borderRadius ?? 12)), + gradient: LinearGradient( + begin: Alignment(-2, -4), + end: Alignment(2, 4), + stops: [ + _animation.value - 0.2, + _animation.value, + _animation.value + 0.2, + ], + colors: [ + backgroundColor.withOpacity(widget.isDarkTheme ? 0.4 : 0.7), + backgroundColor.withOpacity(widget.isDarkTheme ? 0.7 : 0.4), + backgroundColor.withOpacity(widget.isDarkTheme ? 0.4 : 0.7), + ], + ), + ), + ), + ), + ], + ); + }, + ); + } +} diff --git a/lib/src/widgets/setting_actions.dart b/lib/src/widgets/setting_actions.dart index 048d006cc..da9f26bc6 100644 --- a/lib/src/widgets/setting_actions.dart +++ b/lib/src/widgets/setting_actions.dart @@ -21,6 +21,7 @@ class SettingActions { addressBookSettingAction, silentPaymentsSettingAction, litecoinMwebSettingAction, + exportOutputsAction, securityBackupSettingAction, privacySettingAction, displaySettingAction, @@ -50,6 +51,16 @@ class SettingActions { }, ); + static SettingActions exportOutputsAction = SettingActions._( + key: ValueKey('dashboard_page_menu_widget_export_outputs_settings_button_key'), + name: (context) => S.of(context).export_outputs, + image: 'assets/images/monero_menu.png', + onTap: (BuildContext context) { + Navigator.pop(context); + Navigator.of(context).pushNamed(Routes.urqrAnimatedPage, arguments: 'export-outputs'); + }, + ); + static SettingActions litecoinMwebSettingAction = SettingActions._( key: ValueKey('dashboard_page_menu_widget_litecoin_mweb_settings_button_key'), name: (context) => S.of(context).litecoin_mweb_settings, diff --git a/lib/store/app_store.dart b/lib/store/app_store.dart index cd8881633..24d5dc6a4 100644 --- a/lib/store/app_store.dart +++ b/lib/store/app_store.dart @@ -1,8 +1,10 @@ import 'package:cake_wallet/core/wallet_connect/web3wallet_service.dart'; import 'package:cake_wallet/di.dart'; +import 'package:cake_wallet/entities/preferences_key.dart'; import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cake_wallet/utils/exception_handler.dart'; import 'package:cw_core/transaction_info.dart'; +import 'package:cw_core/wallet_type.dart'; import 'package:mobx/mobx.dart'; import 'package:cw_core/balance.dart'; import 'package:cw_core/wallet_base.dart'; @@ -11,6 +13,7 @@ import 'package:cake_wallet/store/wallet_list_store.dart'; import 'package:cake_wallet/store/authentication_store.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/store/node_list_store.dart'; +import 'package:shared_preferences/shared_preferences.dart'; part 'app_store.g.dart'; @@ -47,5 +50,9 @@ abstract class AppStoreBase with Store { getIt.get().create(); await getIt.get().init(); } + await getIt.get().setString(PreferencesKey.currentWalletName, wallet.name); + await getIt + .get() + .setInt(PreferencesKey.currentWalletType, serializeToInt(wallet.type)); } } diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index 50d51d2ed..1ecaf50cc 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -61,8 +61,7 @@ abstract class SettingsStoreBase with Store { required BitcoinSeedType initialBitcoinSeedType, required NanoSeedType initialNanoSeedType, required bool initialAppSecure, - required bool initialDisableBuy, - required bool initialDisableSell, + required bool initialDisableTrade, required FilterListOrderType initialWalletListOrder, required FilterListOrderType initialContactListOrder, required bool initialDisableBulletin, @@ -150,8 +149,7 @@ abstract class SettingsStoreBase with Store { useTOTP2FA = initialUseTOTP2FA, numberOfFailedTokenTrials = initialFailedTokenTrial, isAppSecure = initialAppSecure, - disableBuy = initialDisableBuy, - disableSell = initialDisableSell, + disableTradeOption = initialDisableTrade, disableBulletin = initialDisableBulletin, walletListOrder = initialWalletListOrder, contactListOrder = initialContactListOrder, @@ -178,9 +176,7 @@ abstract class SettingsStoreBase with Store { initialShouldRequireTOTP2FAForAllSecurityAndBackupSettings, currentSyncMode = initialSyncMode, currentSyncAll = initialSyncAll, - priority = ObservableMap(), - defaultBuyProviders = ObservableMap(), - defaultSellProviders = ObservableMap() { + priority = ObservableMap() { //this.nodes = ObservableMap.of(nodes); if (initialMoneroTransactionPriority != null) { @@ -221,30 +217,6 @@ abstract class SettingsStoreBase with Store { initializeTrocadorProviderStates(); - WalletType.values.forEach((walletType) { - final key = 'buyProvider_${walletType.toString()}'; - final providerId = sharedPreferences.getString(key); - if (providerId != null) { - defaultBuyProviders[walletType] = ProviderType.values.firstWhere( - (provider) => provider.id == providerId, - orElse: () => ProviderType.askEachTime); - } else { - defaultBuyProviders[walletType] = ProviderType.askEachTime; - } - }); - - WalletType.values.forEach((walletType) { - final key = 'sellProvider_${walletType.toString()}'; - final providerId = sharedPreferences.getString(key); - if (providerId != null) { - defaultSellProviders[walletType] = ProviderType.values.firstWhere( - (provider) => provider.id == providerId, - orElse: () => ProviderType.askEachTime); - } else { - defaultSellProviders[walletType] = ProviderType.askEachTime; - } - }); - reaction( (_) => fiatCurrency, (FiatCurrency fiatCurrency) => sharedPreferences.setString( @@ -267,20 +239,6 @@ abstract class SettingsStoreBase with Store { reaction((_) => shouldShowRepWarning, (bool val) => sharedPreferences.setBool(PreferencesKey.shouldShowRepWarning, val)); - defaultBuyProviders.observe((change) { - final String key = 'buyProvider_${change.key.toString()}'; - if (change.newValue != null) { - sharedPreferences.setString(key, change.newValue!.id); - } - }); - - defaultSellProviders.observe((change) { - final String key = 'sellProvider_${change.key.toString()}'; - if (change.newValue != null) { - sharedPreferences.setString(key, change.newValue!.id); - } - }); - priority.observe((change) { final String? key; switch (change.key) { @@ -329,14 +287,9 @@ abstract class SettingsStoreBase with Store { }); } - reaction((_) => disableBuy, - (bool disableBuy) => sharedPreferences.setBool(PreferencesKey.disableBuyKey, disableBuy)); - - reaction( - (_) => disableSell, - (bool disableSell) => - sharedPreferences.setBool(PreferencesKey.disableSellKey, disableSell)); - + reaction((_) => disableTradeOption, + (bool disableTradeOption) => sharedPreferences.setBool(PreferencesKey.disableTradeOption, disableTradeOption)); + reaction( (_) => disableBulletin, (bool disableBulletin) => @@ -680,10 +633,7 @@ abstract class SettingsStoreBase with Store { bool isAppSecure; @observable - bool disableBuy; - - @observable - bool disableSell; + bool disableTradeOption; @observable FilterListOrderType contactListOrder; @@ -769,12 +719,6 @@ abstract class SettingsStoreBase with Store { @observable ObservableMap trocadorProviderStates = ObservableMap(); - @observable - ObservableMap defaultBuyProviders; - - @observable - ObservableMap defaultSellProviders; - @observable SortBalanceBy sortBalanceBy; @@ -956,8 +900,7 @@ abstract class SettingsStoreBase with Store { final shouldSaveRecipientAddress = sharedPreferences.getBool(PreferencesKey.shouldSaveRecipientAddressKey) ?? false; final isAppSecure = sharedPreferences.getBool(PreferencesKey.isAppSecureKey) ?? false; - final disableBuy = sharedPreferences.getBool(PreferencesKey.disableBuyKey) ?? false; - final disableSell = sharedPreferences.getBool(PreferencesKey.disableSellKey) ?? false; + final disableTradeOption = sharedPreferences.getBool(PreferencesKey.disableTradeOption) ?? false; final disableBulletin = sharedPreferences.getBool(PreferencesKey.disableBulletinKey) ?? false; final walletListOrder = FilterListOrderType.values[sharedPreferences.getInt(PreferencesKey.walletListOrder) ?? 0]; @@ -1255,8 +1198,7 @@ abstract class SettingsStoreBase with Store { initialBitcoinSeedType: bitcoinSeedType, initialNanoSeedType: nanoSeedType, initialAppSecure: isAppSecure, - initialDisableBuy: disableBuy, - initialDisableSell: disableSell, + initialDisableTrade: disableTradeOption, initialDisableBulletin: disableBulletin, initialWalletListOrder: walletListOrder, initialWalletListAscending: walletListAscending, @@ -1406,8 +1348,7 @@ abstract class SettingsStoreBase with Store { numberOfFailedTokenTrials = sharedPreferences.getInt(PreferencesKey.failedTotpTokenTrials) ?? numberOfFailedTokenTrials; isAppSecure = sharedPreferences.getBool(PreferencesKey.isAppSecureKey) ?? isAppSecure; - disableBuy = sharedPreferences.getBool(PreferencesKey.disableBuyKey) ?? disableBuy; - disableSell = sharedPreferences.getBool(PreferencesKey.disableSellKey) ?? disableSell; + disableTradeOption = sharedPreferences.getBool(PreferencesKey.disableTradeOption) ?? disableTradeOption; disableBulletin = sharedPreferences.getBool(PreferencesKey.disableBulletinKey) ?? disableBulletin; walletListOrder = diff --git a/lib/typography.dart b/lib/typography.dart index 8ae2cb4e5..816f116b4 100644 --- a/lib/typography.dart +++ b/lib/typography.dart @@ -22,6 +22,8 @@ TextStyle textMediumSemiBold({Color? color}) => _cakeSemiBold(22, color); TextStyle textLarge({Color? color}) => _cakeRegular(18, color); +TextStyle textLargeBold({Color? color}) => _cakeBold(18, color); + TextStyle textLargeSemiBold({Color? color}) => _cakeSemiBold(24, color); TextStyle textXLarge({Color? color}) => _cakeRegular(32, color); diff --git a/lib/view_model/animated_ur_model.dart b/lib/view_model/animated_ur_model.dart new file mode 100644 index 000000000..f613d26be --- /dev/null +++ b/lib/view_model/animated_ur_model.dart @@ -0,0 +1,10 @@ +import 'package:cake_wallet/store/app_store.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:mobx/mobx.dart'; + +class AnimatedURModel with Store { + AnimatedURModel(this.appStore) + : wallet = appStore.wallet!; + final AppStore appStore; + final WalletBase wallet; +} \ No newline at end of file diff --git a/lib/view_model/buy/buy_sell_view_model.dart b/lib/view_model/buy/buy_sell_view_model.dart new file mode 100644 index 000000000..e1c53ee56 --- /dev/null +++ b/lib/view_model/buy/buy_sell_view_model.dart @@ -0,0 +1,446 @@ +import 'dart:async'; + +import 'package:cake_wallet/buy/buy_provider.dart'; +import 'package:cake_wallet/buy/buy_quote.dart'; +import 'package:cake_wallet/buy/onramper/onramper_buy_provider.dart'; +import 'package:cake_wallet/buy/payment_method.dart'; +import 'package:cake_wallet/buy/sell_buy_states.dart'; +import 'package:cake_wallet/core/selectable_option.dart'; +import 'package:cake_wallet/core/wallet_change_listener_view_model.dart'; +import 'package:cake_wallet/entities/fiat_currency.dart'; +import 'package:cake_wallet/entities/provider_types.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/store/app_store.dart'; +import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cake_wallet/themes/theme_base.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/currency_for_wallet_type.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:intl/intl.dart'; +import 'package:mobx/mobx.dart'; + +part 'buy_sell_view_model.g.dart'; + +class BuySellViewModel = BuySellViewModelBase with _$BuySellViewModel; + +abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with Store { + BuySellViewModelBase( + AppStore appStore, + ) : _cryptoNumberFormat = NumberFormat(), + cryptoAmount = '', + fiatAmount = '', + cryptoCurrencyAddress = '', + isCryptoCurrencyAddressEnabled = false, + cryptoCurrencies = [], + fiatCurrencies = [], + paymentMethodState = InitialPaymentMethod(), + buySellQuotState = InitialBuySellQuotState(), + cryptoCurrency = appStore.wallet!.currency, + fiatCurrency = appStore.settingsStore.fiatCurrency, + providerList = [], + sortedRecommendedQuotes = ObservableList(), + sortedQuotes = ObservableList(), + paymentMethods = ObservableList(), + settingsStore = appStore.settingsStore, + super(appStore: appStore) { + const excludeFiatCurrencies = []; + const excludeCryptoCurrencies = []; + + fiatCurrencies = + FiatCurrency.all.where((currency) => !excludeFiatCurrencies.contains(currency)).toList(); + cryptoCurrencies = CryptoCurrency.all + .where((currency) => !excludeCryptoCurrencies.contains(currency)) + .toList(); + _initialize(); + + isCryptoCurrencyAddressEnabled = !(cryptoCurrency == wallet.currency); + } + + final NumberFormat _cryptoNumberFormat; + late Timer bestRateSync; + + List get availableBuyProviders { + final providerTypes = ProvidersHelper.getAvailableBuyProviderTypes( + walletTypeForCurrency(cryptoCurrency) ?? wallet.type); + return providerTypes + .map((type) => ProvidersHelper.getProviderByType(type)) + .where((provider) => provider != null) + .cast() + .toList(); + } + + List get availableSellProviders { + final providerTypes = ProvidersHelper.getAvailableSellProviderTypes( + walletTypeForCurrency(cryptoCurrency) ?? wallet.type); + return providerTypes + .map((type) => ProvidersHelper.getProviderByType(type)) + .where((provider) => provider != null) + .cast() + .toList(); + } + + @override + void onWalletChange(wallet) { + cryptoCurrency = wallet.currency; + } + + bool get isDarkTheme => settingsStore.currentTheme.type == ThemeType.dark; + + double get amount { + final formattedFiatAmount = double.tryParse(fiatAmount) ?? 200.0; + final formattedCryptoAmount = + double.tryParse(cryptoAmount) ?? (cryptoCurrency == CryptoCurrency.btc ? 0.001 : 1); + + return isBuyAction ? formattedFiatAmount : formattedCryptoAmount; + } + + SettingsStore settingsStore; + + Quote? bestRateQuote; + + Quote? selectedQuote; + + @observable + List cryptoCurrencies; + + @observable + List fiatCurrencies; + + @observable + bool isBuyAction = true; + + @observable + List providerList; + + @observable + ObservableList sortedRecommendedQuotes; + + @observable + ObservableList sortedQuotes; + + @observable + ObservableList paymentMethods; + + @observable + FiatCurrency fiatCurrency; + + @observable + CryptoCurrency cryptoCurrency; + + @observable + String cryptoAmount; + + @observable + String fiatAmount; + + @observable + String cryptoCurrencyAddress; + + @observable + bool isCryptoCurrencyAddressEnabled; + + @observable + PaymentMethod? selectedPaymentMethod; + + @observable + PaymentMethodLoadingState paymentMethodState; + + @observable + BuySellQuotLoadingState buySellQuotState; + + @computed + bool get isReadyToTrade { + final hasSelectedQuote = selectedQuote != null; + final hasSelectedPaymentMethod = selectedPaymentMethod != null; + final isPaymentMethodLoaded = paymentMethodState is PaymentMethodLoaded; + final isBuySellQuotLoaded = buySellQuotState is BuySellQuotLoaded; + + return hasSelectedQuote && + hasSelectedPaymentMethod && + isPaymentMethodLoaded && + isBuySellQuotLoaded; + } + + @action + void reset() { + cryptoCurrency = wallet.currency; + fiatCurrency = settingsStore.fiatCurrency; + isCryptoCurrencyAddressEnabled = !(cryptoCurrency == wallet.currency); + _initialize(); + } + + @action + void changeBuySellAction() { + isBuyAction = !isBuyAction; + _initialize(); + } + + @action + void changeFiatCurrency({required FiatCurrency currency}) { + fiatCurrency = currency; + _onPairChange(); + } + + @action + void changeCryptoCurrency({required CryptoCurrency currency}) { + cryptoCurrency = currency; + _onPairChange(); + isCryptoCurrencyAddressEnabled = !(cryptoCurrency == wallet.currency); + } + + @action + void changeCryptoCurrencyAddress(String address) => cryptoCurrencyAddress = address; + + @action + Future changeFiatAmount({required String amount}) async { + fiatAmount = amount; + + if (amount.isEmpty) { + fiatAmount = ''; + cryptoAmount = ''; + return; + } + + final enteredAmount = double.tryParse(amount.replaceAll(',', '.')) ?? 0; + + if (!isReadyToTrade) { + cryptoAmount = S.current.fetching; + return; + } + + if (bestRateQuote != null) { + _cryptoNumberFormat.maximumFractionDigits = cryptoCurrency.decimals; + cryptoAmount = _cryptoNumberFormat + .format(enteredAmount / bestRateQuote!.rate) + .toString() + .replaceAll(RegExp('\\,'), ''); + } else { + await calculateBestRate(); + } + } + + @action + Future changeCryptoAmount({required String amount}) async { + cryptoAmount = amount; + + if (amount.isEmpty) { + fiatAmount = ''; + cryptoAmount = ''; + return; + } + + final enteredAmount = double.tryParse(amount.replaceAll(',', '.')) ?? 0; + + if (!isReadyToTrade) { + fiatAmount = S.current.fetching; + } + + if (bestRateQuote != null) { + fiatAmount = _cryptoNumberFormat + .format(enteredAmount * bestRateQuote!.rate) + .toString() + .replaceAll(RegExp('\\,'), ''); + } else { + await calculateBestRate(); + } + } + + @action + void changeOption(SelectableOption option) { + if (option is Quote) { + sortedRecommendedQuotes.forEach((element) => element.setIsSelected = false); + sortedQuotes.forEach((element) => element.setIsSelected = false); + option.setIsSelected = true; + selectedQuote = option; + } else if (option is PaymentMethod) { + paymentMethods.forEach((element) => element.isSelected = false); + option.isSelected = true; + selectedPaymentMethod = option; + } else { + throw ArgumentError('Unknown option type'); + } + } + + void onTapChoseProvider(BuildContext context) async { + final initialQuotes = List.from(sortedRecommendedQuotes + sortedQuotes); + await calculateBestRate(); + final newQuotes = (sortedRecommendedQuotes + sortedQuotes); + + for (var quote in newQuotes) quote.limits = null; + + final newQuoteProviders = newQuotes + .map((quote) => quote.provider.isAggregator ? quote.rampName : quote.provider.title) + .toSet(); + + final outOfLimitQuotes = initialQuotes.where((initialQuote) { + return !newQuoteProviders.contains( + initialQuote.provider.isAggregator ? initialQuote.rampName : initialQuote.provider.title); + }).map((missingQuote) { + final quote = Quote( + rate: missingQuote.rate, + feeAmount: missingQuote.feeAmount, + networkFee: missingQuote.networkFee, + transactionFee: missingQuote.transactionFee, + payout: missingQuote.payout, + rampId: missingQuote.rampId, + rampName: missingQuote.rampName, + rampIconPath: missingQuote.rampIconPath, + paymentType: missingQuote.paymentType, + quoteId: missingQuote.quoteId, + recommendations: missingQuote.recommendations, + provider: missingQuote.provider, + isBuyAction: missingQuote.isBuyAction, + limits: missingQuote.limits, + ); + quote.setFiatCurrency = missingQuote.fiatCurrency; + quote.setCryptoCurrency = missingQuote.cryptoCurrency; + return quote; + }).toList(); + + final updatedQuoteOptions = List.from([ + OptionTitle(title: 'Recommended'), + ...sortedRecommendedQuotes, + if (sortedQuotes.isNotEmpty) OptionTitle(title: 'All Providers'), + ...sortedQuotes, + if (outOfLimitQuotes.isNotEmpty) OptionTitle(title: 'Out of Limits'), + ...outOfLimitQuotes, + ]); + + await Navigator.of(context).pushNamed( + Routes.buyOptionsPage, + arguments: [ + updatedQuoteOptions, + changeOption, + launchTrade, + ], + ).then((value) => calculateBestRate()); + } + + void _onPairChange() { + _initialize(); + } + + void _setProviders() => + providerList = isBuyAction ? availableBuyProviders : availableSellProviders; + + Future _initialize() async { + _setProviders(); + cryptoAmount = ''; + fiatAmount = ''; + cryptoCurrencyAddress = _getInitialCryptoCurrencyAddress(); + paymentMethodState = InitialPaymentMethod(); + buySellQuotState = InitialBuySellQuotState(); + await _getAvailablePaymentTypes(); + await calculateBestRate(); + } + + String _getInitialCryptoCurrencyAddress() { + return cryptoCurrency == wallet.currency ? wallet.walletAddresses.address : ''; + } + + @action + Future _getAvailablePaymentTypes() async { + paymentMethodState = PaymentMethodLoading(); + selectedPaymentMethod = null; + final result = await Future.wait(providerList.map((element) => element + .getAvailablePaymentTypes(fiatCurrency.title, cryptoCurrency.title, isBuyAction) + .timeout( + Duration(seconds: 10), + onTimeout: () => [], + ))); + + final Map uniquePaymentMethods = {}; + for (var methods in result) { + for (var method in methods) { + uniquePaymentMethods[method.paymentMethodType] = method; + } + } + + paymentMethods = ObservableList.of(uniquePaymentMethods.values); + if (paymentMethods.isNotEmpty) { + paymentMethods.insert(0, PaymentMethod.all()); + selectedPaymentMethod = paymentMethods.first; + selectedPaymentMethod!.isSelected = true; + paymentMethodState = PaymentMethodLoaded(); + } else { + paymentMethodState = PaymentMethodFailed(); + } + } + + @action + Future calculateBestRate() async { + buySellQuotState = BuySellQuotLoading(); + + final result = await Future.wait?>(providerList.map((element) => element + .fetchQuote( + cryptoCurrency: cryptoCurrency, + fiatCurrency: fiatCurrency, + amount: amount, + paymentType: selectedPaymentMethod?.paymentMethodType, + isBuyAction: isBuyAction, + walletAddress: wallet.walletAddresses.address, + ) + .timeout( + Duration(seconds: 10), + onTimeout: () => null, + ))); + + sortedRecommendedQuotes.clear(); + sortedQuotes.clear(); + + final validQuotes = result + .where((element) => element != null && element.isNotEmpty) + .expand((element) => element!) + .toList(); + + if (validQuotes.isEmpty) { + buySellQuotState = BuySellQuotFailed(); + return; + } + + validQuotes.sort((a, b) => a.rate.compareTo(b.rate)); + + final Set addedProviders = {}; + final List uniqueProviderQuotes = validQuotes.where((element) { + if (addedProviders.contains(element.provider.title)) return false; + addedProviders.add(element.provider.title); + return true; + }).toList(); + + sortedRecommendedQuotes.addAll(uniqueProviderQuotes); + + sortedQuotes = ObservableList.of( + validQuotes.where((element) => !uniqueProviderQuotes.contains(element)).toList()); + + if (sortedRecommendedQuotes.isNotEmpty) { + sortedRecommendedQuotes.first + ..setIsBestRate = true + ..recommendations.insert(0, ProviderRecommendation.bestRate); + bestRateQuote = sortedRecommendedQuotes.first; + + sortedRecommendedQuotes.sort((a, b) { + if (a.provider is OnRamperBuyProvider) return -1; + if (b.provider is OnRamperBuyProvider) return 1; + return 0; + }); + + selectedQuote = sortedRecommendedQuotes.first; + sortedRecommendedQuotes.first.setIsSelected = true; + } + + buySellQuotState = BuySellQuotLoaded(); + } + + @action + Future launchTrade(BuildContext context) async { + final provider = selectedQuote!.provider; + await provider.launchProvider( + context: context, + quote: selectedQuote!, + amount: amount, + isBuyAction: isBuyAction, + cryptoCurrencyAddress: cryptoCurrencyAddress, + ); + } +} diff --git a/lib/view_model/contact_list/contact_list_view_model.dart b/lib/view_model/contact_list/contact_list_view_model.dart index 0015463a5..eb3fb837e 100644 --- a/lib/view_model/contact_list/contact_list_view_model.dart +++ b/lib/view_model/contact_list/contact_list_view_model.dart @@ -28,7 +28,8 @@ abstract class ContactListViewModelBase with Store { isAutoGenerateEnabled = settingsStore.autoGenerateSubaddressStatus == AutoGenerateSubaddressStatus.enabled { walletInfoSource.values.forEach((info) { - if ([WalletType.monero, WalletType.wownero, WalletType.haven].contains(info.type) && info.addressInfos != null) { + if ([WalletType.monero, WalletType.wownero, WalletType.haven].contains(info.type) && + info.addressInfos != null) { for (var key in info.addressInfos!.keys) { final value = info.addressInfos![key]; final address = value?.first; @@ -60,15 +61,19 @@ abstract class ContactListViewModelBase with Store { address, name, walletTypeToCryptoCurrency(info.type, - isTestnet: - info.network == null ? false : info.network!.toLowerCase().contains("testnet")), + isTestnet: info.network == null + ? false + : info.network!.toLowerCase().contains("testnet")), )); }); } } else { walletContacts.add(WalletContact( info.address, - _createName(info.name, "", key: [WalletType.monero, WalletType.wownero, WalletType.haven].contains(info.type) ? 0 : null), + _createName(info.name, "", + key: [WalletType.monero, WalletType.wownero, WalletType.haven].contains(info.type) + ? 0 + : null), walletTypeToCryptoCurrency(info.type), )); } @@ -82,8 +87,11 @@ abstract class ContactListViewModelBase with Store { } String _createName(String walletName, String label, {int? key = null}) { - final actualLabel = label.replaceAll(RegExp(r'active', caseSensitive: false), S.current.active).replaceAll(RegExp(r'silent payments', caseSensitive: false), S.current.silent_payments); - return '$walletName${key == null ? "" : " [#${key}]"} ${actualLabel.isNotEmpty ? "($actualLabel)" : ""}'.trim(); + final actualLabel = label + .replaceAll(RegExp(r'active', caseSensitive: false), S.current.active) + .replaceAll(RegExp(r'silent payments', caseSensitive: false), S.current.silent_payments); + return '$walletName${key == null ? "" : " [#${key}]"} ${actualLabel.isNotEmpty ? "($actualLabel)" : ""}' + .trim(); } final bool isAutoGenerateEnabled; @@ -108,18 +116,19 @@ abstract class ContactListViewModelBase with Store { Future delete(ContactRecord contact) async => contact.original.delete(); ObservableList get contactsToShow => - ObservableList.of(contacts.where((element) => _isValidForCurrency(element))); + ObservableList.of(contacts.where((element) => _isValidForCurrency(element, false))); @computed List get walletContactsToShow => - walletContacts.where((element) => _isValidForCurrency(element)).toList(); + walletContacts.where((element) => _isValidForCurrency(element, true)).toList(); - bool _isValidForCurrency(ContactBase element) { - if (element.name.contains('Silent Payments')) return false; - if (element.name.contains('MWEB')) return false; + bool _isValidForCurrency(ContactBase element, bool isWalletContact) { + if (_currency == null) return true; + if (!element.name.contains('Active') && + isWalletContact && + (element.type == CryptoCurrency.btc || element.type == CryptoCurrency.ltc)) return false; - return _currency == null || - element.type == _currency || + return element.type == _currency || (element.type.tag != null && _currency?.tag != null && element.type.tag == _currency?.tag) || @@ -127,10 +136,11 @@ abstract class ContactListViewModelBase with Store { _currency?.tag == element.type.toString(); } - void dispose() async { - _subscription?.cancel(); + void dispose() => _subscription?.cancel(); + + void saveCustomOrder() { final List contactsSourceCopy = contacts.map((e) => e.original).toList(); - await reorderContacts(contactsSourceCopy); + reorderContacts(contactsSourceCopy); } void reorderAccordingToContactList() => diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index 879aea79a..99f1c06e0 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -71,8 +71,7 @@ abstract class DashboardViewModelBase with Store { required this.anonpayTransactionsStore, required this.sharedPreferences, required this.keyService}) - : hasSellAction = false, - hasBuyAction = false, + : hasTradeAction = false, hasExchangeAction = false, isShowFirstYatIntroduction = false, isShowSecondYatIntroduction = false, @@ -398,6 +397,12 @@ abstract class DashboardViewModelBase with Store { wallet.type == WalletType.wownero || wallet.type == WalletType.haven; + @computed + bool get isMoneroViewOnly { + if (wallet.type != WalletType.monero) return false; + return monero!.isViewOnly(); + } + @computed String? get getMoneroError { if (wallet.type != WalletType.monero) return null; @@ -428,7 +433,7 @@ abstract class DashboardViewModelBase with Store { // to not cause work duplication, this will do the job as well, it will be slightly less precise // about what happened - but still enough. // if (keys['privateSpendKey'] == List.generate(64, (index) => "0").join("")) "Private spend key is 0", - if (keys['privateViewKey'] == List.generate(64, (index) => "0").join("")) + if (keys['privateViewKey'] == List.generate(64, (index) => "0").join("") && !wallet.isHardwareWallet) "private view key is 0", // if (keys['publicSpendKey'] == List.generate(64, (index) => "0").join("")) "public spend key is 0", if (keys['publicViewKey'] == List.generate(64, (index) => "0").join("")) @@ -520,37 +525,8 @@ abstract class DashboardViewModelBase with Store { Map> filterItems; - BuyProvider? get defaultBuyProvider => ProvidersHelper.getProviderByType( - settingsStore.defaultBuyProviders[wallet.type] ?? ProviderType.askEachTime); - - BuyProvider? get defaultSellProvider => ProvidersHelper.getProviderByType( - settingsStore.defaultSellProviders[wallet.type] ?? ProviderType.askEachTime); - bool get isBuyEnabled => settingsStore.isBitcoinBuyEnabled; - List get availableBuyProviders { - final providerTypes = ProvidersHelper.getAvailableBuyProviderTypes(wallet.type); - return providerTypes - .map((type) => ProvidersHelper.getProviderByType(type)) - .where((provider) => provider != null) - .cast() - .toList(); - } - - bool get hasBuyProviders => ProvidersHelper.getAvailableBuyProviderTypes(wallet.type).isNotEmpty; - - List get availableSellProviders { - final providerTypes = ProvidersHelper.getAvailableSellProviderTypes(wallet.type); - return providerTypes - .map((type) => ProvidersHelper.getProviderByType(type)) - .where((provider) => provider != null) - .cast() - .toList(); - } - - bool get hasSellProviders => - ProvidersHelper.getAvailableSellProviderTypes(wallet.type).isNotEmpty; - bool get shouldShowYatPopup => settingsStore.shouldShowYatPopup; @action @@ -563,16 +539,10 @@ abstract class DashboardViewModelBase with Store { bool hasExchangeAction; @computed - bool get isEnabledBuyAction => !settingsStore.disableBuy && hasBuyProviders; + bool get isEnabledTradeAction => !settingsStore.disableTradeOption; @observable - bool hasBuyAction; - - @computed - bool get isEnabledSellAction => !settingsStore.disableSell && hasSellProviders; - - @observable - bool hasSellAction; + bool hasTradeAction; @computed bool get isEnabledBulletinAction => !settingsStore.disableBulletin; @@ -775,8 +745,7 @@ abstract class DashboardViewModelBase with Store { void updateActions() { hasExchangeAction = !isHaven; - hasBuyAction = !isHaven; - hasSellAction = !isHaven; + hasTradeAction = !isHaven; } @computed diff --git a/lib/view_model/hardware_wallet/ledger_view_model.dart b/lib/view_model/hardware_wallet/ledger_view_model.dart index 19b190fe3..3cd131efa 100644 --- a/lib/view_model/hardware_wallet/ledger_view_model.dart +++ b/lib/view_model/hardware_wallet/ledger_view_model.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/utils/device_info.dart'; import 'package:cake_wallet/wallet_type_utils.dart'; @@ -97,7 +98,9 @@ abstract class LedgerViewModelBase with Store { print('Ledger Device State Changed: $event'); if (event == sdk.BleConnectionState.disconnected) { _connection = null; - _connectionChangeListener?.cancel(); + if (type == WalletType.monero) { + monero!.resetLedgerConnection(); + } } }); } @@ -114,6 +117,8 @@ abstract class LedgerViewModelBase with Store { void setLedger(WalletBase wallet) { switch (wallet.type) { + case WalletType.monero: + return monero!.setLedgerConnection(wallet, connection); case WalletType.bitcoin: case WalletType.litecoin: return bitcoin!.setLedgerConnection(wallet, connection); diff --git a/lib/view_model/node_list/node_create_or_edit_view_model.dart b/lib/view_model/node_list/node_create_or_edit_view_model.dart index 86ceb654c..8b3c70c5e 100644 --- a/lib/view_model/node_list/node_create_or_edit_view_model.dart +++ b/lib/view_model/node_list/node_create_or_edit_view_model.dart @@ -213,7 +213,7 @@ abstract class NodeCreateOrEditViewModelBase with Store { bool isCameraPermissionGranted = await PermissionHandler.checkPermission(Permission.camera, context); if (!isCameraPermissionGranted) return; - String code = await presentQRScanner(); + String code = await presentQRScanner(context); if (code.isEmpty) { throw Exception('Unexpected scan QR code value: value is empty'); diff --git a/lib/view_model/restore/restore_wallet.dart b/lib/view_model/restore/restore_wallet.dart index 2c2a25005..cc3ad4123 100644 --- a/lib/view_model/restore/restore_wallet.dart +++ b/lib/view_model/restore/restore_wallet.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:cake_wallet/view_model/restore/restore_mode.dart'; import 'package:cw_core/wallet_type.dart'; @@ -32,6 +34,16 @@ class RestoredWallet { final String? privateKey; factory RestoredWallet.fromKey(Map json) { + try { + final codeParsed = jsonDecode(json['raw_qr'].toString()); + if (codeParsed["version"] == 0) { + json['address'] = codeParsed["primaryAddress"]; + json['view_key'] = codeParsed["privateViewKey"]; + json['height'] = codeParsed["restoreHeight"].toString(); + } + } catch (e) { + // fine, we don't care, it is only for monero anyway + } final height = json['height'] as String?; return RestoredWallet( restoreMode: json['mode'] as WalletRestoreMode, @@ -39,7 +51,7 @@ class RestoredWallet { address: json['address'] as String?, spendKey: json['spend_key'] as String?, viewKey: json['view_key'] as String?, - height: height != null ? int.parse(height) : 0, + height: height != null ? int.tryParse(height)??0 : 0, privateKey: json['private_key'] as String?, ); } diff --git a/lib/view_model/restore/wallet_restore_from_qr_code.dart b/lib/view_model/restore/wallet_restore_from_qr_code.dart index 23850befa..c1a19ea57 100644 --- a/lib/view_model/restore/wallet_restore_from_qr_code.dart +++ b/lib/view_model/restore/wallet_restore_from_qr_code.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:cake_wallet/core/seed_validator.dart'; import 'package:cake_wallet/entities/parse_address_from_domain.dart'; import 'package:cake_wallet/entities/qr_scanner.dart'; @@ -48,6 +50,17 @@ class WalletRestoreFromQRCode { final extracted = sortedKeys.firstWhereOrNull((key) => code.toLowerCase().contains(key)); + if (extracted == null) { + // Special case for view-only monero wallet + final codeParsed = json.decode(code); + if (codeParsed["version"] == 0 && + codeParsed["primaryAddress"] != null && + codeParsed["privateViewKey"] != null && + codeParsed["restoreHeight"] != null) { + return WalletType.monero; + } + } + return _walletTypeMap[extracted]; } @@ -75,7 +88,7 @@ class WalletRestoreFromQRCode { } static Future scanQRCodeForRestoring(BuildContext context) async { - String code = await presentQRScanner(); + String code = await presentQRScanner(context); if (code.isEmpty) throw Exception('Unexpected scan QR code value: value is empty'); WalletType? walletType; @@ -109,7 +122,7 @@ class WalletRestoreFromQRCode { queryParameters['address'] = _extractAddressFromUrl(code, walletType!); } - Map credentials = {'type': walletType, ...queryParameters}; + Map credentials = {'type': walletType, ...queryParameters, 'raw_qr': code}; credentials['mode'] = _determineWalletRestoreMode(credentials); @@ -205,6 +218,17 @@ class WalletRestoreFromQRCode { return WalletRestoreMode.keys; } + if (type == WalletType.monero) { + final codeParsed = json.decode(credentials['raw_qr'].toString()); + if (codeParsed["version"] != 0) throw UnimplementedError("Found view-only restore with unsupported version"); + if (codeParsed["primaryAddress"] == null || + codeParsed["privateViewKey"] == null || + codeParsed["restoreHeight"] == null) { + throw UnimplementedError("Missing one or more attributes in the JSON"); + } + return WalletRestoreMode.keys; + } + throw Exception('Unexpected restore mode: restore params are invalid'); } } diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index bef25f62f..960a9c550 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/entities/contact.dart'; import 'package:cake_wallet/entities/priority_for_wallet_type.dart'; import 'package:cake_wallet/entities/transaction_description.dart'; @@ -11,7 +12,9 @@ import 'package:cake_wallet/entities/contact_record.dart'; import 'package:cake_wallet/entities/wallet_contact.dart'; import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/reactions/wallet_connect.dart'; +import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/solana/solana.dart'; +import 'package:cake_wallet/src/screens/ur/animated_ur_page.dart'; import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/tron/tron.dart'; import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart'; @@ -24,6 +27,7 @@ import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/unspent_coin_type.dart'; import 'package:cake_wallet/view_model/send/output.dart'; import 'package:cake_wallet/view_model/send/send_template_view_model.dart'; +import 'package:flutter/material.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/entities/template.dart'; @@ -401,8 +405,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor final updatedOutputs = bitcoin!.updateOutputs(pendingTransaction!, outputs); if (outputs.length == updatedOutputs.length) { - outputs.clear(); - outputs.addAll(updatedOutputs); + outputs.replaceRange(0, outputs.length, updatedOutputs); } } @@ -458,7 +461,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor } @action - Future commitTransaction() async { + Future commitTransaction(BuildContext context) async { if (pendingTransaction == null) { throw Exception("Pending transaction doesn't exist. It should not be happened."); } @@ -477,7 +480,17 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor try { state = TransactionCommitting(); - await pendingTransaction!.commit(); + + if (pendingTransaction!.shouldCommitUR()) { + final urstr = await pendingTransaction!.commitUR(); + final result = await Navigator.of(context).pushNamed(Routes.urqrAnimatedPage, arguments: urstr); + if (result == null) { + state = FailureState("Canceled by user"); + return; + } + } else { + await pendingTransaction!.commit(); + } if (walletType == WalletType.nano) { nano!.updateTransactions(wallet); diff --git a/lib/view_model/settings/other_settings_view_model.dart b/lib/view_model/settings/other_settings_view_model.dart index 9af8c67cf..3036e8ae9 100644 --- a/lib/view_model/settings/other_settings_view_model.dart +++ b/lib/view_model/settings/other_settings_view_model.dart @@ -65,29 +65,6 @@ abstract class OtherSettingsViewModelBase with Store { _wallet.type == WalletType.solana || _wallet.type == WalletType.tron); - @computed - bool get isEnabledBuyAction => - !_settingsStore.disableBuy && _wallet.type != WalletType.haven; - - @computed - bool get isEnabledSellAction => - !_settingsStore.disableSell && _wallet.type != WalletType.haven; - - List get availableBuyProvidersTypes { - return ProvidersHelper.getAvailableBuyProviderTypes(walletType); - } - - List get availableSellProvidersTypes => - ProvidersHelper.getAvailableSellProviderTypes(walletType); - - ProviderType get buyProviderType => - _settingsStore.defaultBuyProviders[walletType] ?? - ProviderType.askEachTime; - - ProviderType get sellProviderType => - _settingsStore.defaultSellProviders[walletType] ?? - ProviderType.askEachTime; - String getDisplayPriority(dynamic priority) { final _priority = priority as TransactionPriority; @@ -115,20 +92,6 @@ abstract class OtherSettingsViewModelBase with Store { return priority.toString(); } - String getBuyProviderType(dynamic buyProviderType) { - final _buyProviderType = buyProviderType as ProviderType; - return _buyProviderType == ProviderType.askEachTime - ? S.current.ask_each_time - : _buyProviderType.title; - } - - String getSellProviderType(dynamic sellProviderType) { - final _sellProviderType = sellProviderType as ProviderType; - return _sellProviderType == ProviderType.askEachTime - ? S.current.ask_each_time - : _sellProviderType.title; - } - void onDisplayPrioritySelected(TransactionPriority priority) => _settingsStore.priority[walletType] = priority; @@ -157,12 +120,4 @@ abstract class OtherSettingsViewModelBase with Store { } return null; } - - @action - ProviderType onBuyProviderTypeSelected(ProviderType buyProviderType) => - _settingsStore.defaultBuyProviders[walletType] = buyProviderType; - - @action - ProviderType onSellProviderTypeSelected(ProviderType sellProviderType) => - _settingsStore.defaultSellProviders[walletType] = sellProviderType; } diff --git a/lib/view_model/settings/privacy_settings_view_model.dart b/lib/view_model/settings/privacy_settings_view_model.dart index c1e0fb1ce..eaa9f9e84 100644 --- a/lib/view_model/settings/privacy_settings_view_model.dart +++ b/lib/view_model/settings/privacy_settings_view_model.dart @@ -59,10 +59,7 @@ abstract class PrivacySettingsViewModelBase with Store { bool get isAppSecure => _settingsStore.isAppSecure; @computed - bool get disableBuy => _settingsStore.disableBuy; - - @computed - bool get disableSell => _settingsStore.disableSell; + bool get disableTradeOption => _settingsStore.disableTradeOption; @computed bool get disableBulletin => _settingsStore.disableBulletin; @@ -119,10 +116,7 @@ abstract class PrivacySettingsViewModelBase with Store { void setIsAppSecure(bool value) => _settingsStore.isAppSecure = value; @action - void setDisableBuy(bool value) => _settingsStore.disableBuy = value; - - @action - void setDisableSell(bool value) => _settingsStore.disableSell = value; + void setDisableTradeOption(bool value) => _settingsStore.disableTradeOption = value; @action void setDisableBulletin(bool value) => _settingsStore.disableBulletin = value; diff --git a/lib/view_model/wallet_hardware_restore_view_model.dart b/lib/view_model/wallet_hardware_restore_view_model.dart index 91e0de685..0971622a5 100644 --- a/lib/view_model/wallet_hardware_restore_view_model.dart +++ b/lib/view_model/wallet_hardware_restore_view_model.dart @@ -1,7 +1,9 @@ import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/core/generate_wallet_password.dart'; import 'package:cake_wallet/core/wallet_creation_service.dart'; import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart'; @@ -56,8 +58,8 @@ abstract class WalletHardwareRestoreViewModelBase extends WalletCreationVM with List accounts; switch (type) { case WalletType.bitcoin: - accounts = await bitcoin! - .getHardwareWalletBitcoinAccounts(ledgerViewModel, index: _nextIndex, limit: limit); + accounts = await bitcoin! + .getHardwareWalletBitcoinAccounts(ledgerViewModel, index: _nextIndex, limit: limit); break; case WalletType.litecoin: accounts = await bitcoin! @@ -104,6 +106,15 @@ abstract class WalletHardwareRestoreViewModelBase extends WalletCreationVM with case WalletType.polygon: credentials = polygon!.createPolygonHardwareWalletCredentials(name: name, hwAccountData: selectedAccount!); break; + case WalletType.monero: + final password = walletPassword ?? generateWalletPassword(); + + credentials = monero!.createMoneroRestoreWalletFromHardwareCredentials( + name: name, + ledgerConnection: ledgerViewModel.connection, + password: password, + height: _options['height'] as int? ?? 0, + ); default: throw Exception('Unexpected type: ${type.toString()}'); } diff --git a/lib/view_model/wallet_keys_view_model.dart b/lib/view_model/wallet_keys_view_model.dart index 6455fc0e3..81eda7cc8 100644 --- a/lib/view_model/wallet_keys_view_model.dart +++ b/lib/view_model/wallet_keys_view_model.dart @@ -72,6 +72,10 @@ abstract class WalletKeysViewModelBase with Store { final keys = monero!.getKeys(_appStore.wallet!); items.addAll([ + if (keys['primaryAddress'] != null) + StandartListItem( + title: S.current.primary_address, + value: keys['primaryAddress']!), if (keys['publicSpendKey'] != null) StandartListItem( key: ValueKey('${_walletName}_wallet_public_spend_key_item_key'), @@ -131,6 +135,10 @@ abstract class WalletKeysViewModelBase with Store { final keys = haven!.getKeys(_appStore.wallet!); items.addAll([ + if (keys['primaryAddress'] != null) + StandartListItem( + title: S.current.primary_address, + value: keys['primaryAddress']!), if (keys['publicSpendKey'] != null) StandartListItem( key: ValueKey('${_walletName}_wallet_public_spend_key_item_key'), @@ -168,6 +176,10 @@ abstract class WalletKeysViewModelBase with Store { final keys = wownero!.getKeys(_appStore.wallet!); items.addAll([ + if (keys['primaryAddress'] != null) + StandartListItem( + title: S.current.primary_address, + value: keys['primaryAddress']!), if (keys['publicSpendKey'] != null) StandartListItem( key: ValueKey('${_walletName}_wallet_public_spend_key_item_key'), diff --git a/lib/view_model/wallet_list/wallet_list_view_model.dart b/lib/view_model/wallet_list/wallet_list_view_model.dart index 725af843f..c903b535f 100644 --- a/lib/view_model/wallet_list/wallet_list_view_model.dart +++ b/lib/view_model/wallet_list/wallet_list_view_model.dart @@ -68,6 +68,10 @@ abstract class WalletListViewModelBase with Store { WalletType get currentWalletType => _appStore.wallet!.type; + bool requireHardwareWalletConnection(WalletListItem walletItem) => + _walletLoadingService.requireHardwareWalletConnection( + walletItem.type, walletItem.name); + @action Future loadWallet(WalletListItem walletItem) async { // bool switchingToSameWalletType = walletItem.type == _appStore.wallet?.type; @@ -87,7 +91,8 @@ abstract class WalletListViewModelBase with Store { singleWalletsList.clear(); wallets.addAll( - _walletInfoSource.values.map((info) => convertWalletInfoToWalletListItem(info)), + _walletInfoSource.values + .map((info) => convertWalletInfoToWalletListItem(info)), ); //========== Split into shared seed groups and single wallets list @@ -95,7 +100,8 @@ abstract class WalletListViewModelBase with Store { for (var group in _walletManager.walletGroups) { if (group.wallets.length == 1) { - singleWalletsList.add(convertWalletInfoToWalletListItem(group.wallets.first)); + singleWalletsList + .add(convertWalletInfoToWalletListItem(group.wallets.first)); } else { multiWalletGroups.add(group); } @@ -148,9 +154,11 @@ abstract class WalletListViewModelBase with Store { List walletInfoSourceCopy = _walletInfoSource.values.toList(); await _walletInfoSource.clear(); if (ascending) { - walletInfoSourceCopy.sort((a, b) => a.type.toString().compareTo(b.type.toString())); + walletInfoSourceCopy + .sort((a, b) => a.type.toString().compareTo(b.type.toString())); } else { - walletInfoSourceCopy.sort((a, b) => b.type.toString().compareTo(a.type.toString())); + walletInfoSourceCopy + .sort((a, b) => b.type.toString().compareTo(a.type.toString())); } await _walletInfoSource.addAll(walletInfoSourceCopy); updateList(); @@ -213,7 +221,8 @@ abstract class WalletListViewModelBase with Store { name: info.name, type: info.type, key: info.key, - isCurrent: info.name == _appStore.wallet?.name && info.type == _appStore.wallet?.type, + isCurrent: info.name == _appStore.wallet?.name && + info.type == _appStore.wallet?.type, isEnabled: availableWalletTypes.contains(info.type), isTestnet: info.network?.toLowerCase().contains('testnet') ?? false, ); diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 9895e6305..42b9fa84c 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -9,6 +9,7 @@ import connectivity_plus import cw_mweb import device_info_plus import devicelocale +import fast_scanner import flutter_inappwebview_macos import flutter_local_authentication import flutter_secure_storage_macos @@ -26,6 +27,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { CwMwebPlugin.register(with: registry.registrar(forPlugin: "CwMwebPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) DevicelocalePlugin.register(with: registry.registrar(forPlugin: "DevicelocalePlugin")) + MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin")) InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin")) FlutterLocalAuthenticationPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalAuthenticationPlugin")) FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) diff --git a/macos/Podfile.lock b/macos/Podfile.lock index d6199d028..001d75696 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -8,6 +8,8 @@ PODS: - FlutterMacOS - devicelocale (0.0.1): - FlutterMacOS + - fast_scanner (5.1.1): + - FlutterMacOS - flutter_inappwebview_macos (0.0.1): - FlutterMacOS - OrderedSet (~> 5.0) @@ -42,6 +44,7 @@ DEPENDENCIES: - cw_mweb (from `Flutter/ephemeral/.symlinks/plugins/cw_mweb/macos`) - device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`) - devicelocale (from `Flutter/ephemeral/.symlinks/plugins/devicelocale/macos`) + - fast_scanner (from `Flutter/ephemeral/.symlinks/plugins/fast_scanner/macos`) - flutter_inappwebview_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_inappwebview_macos/macos`) - flutter_local_authentication (from `Flutter/ephemeral/.symlinks/plugins/flutter_local_authentication/macos`) - flutter_secure_storage_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos`) @@ -69,6 +72,8 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos devicelocale: :path: Flutter/ephemeral/.symlinks/plugins/devicelocale/macos + fast_scanner: + :path: Flutter/ephemeral/.symlinks/plugins/fast_scanner/macos flutter_inappwebview_macos: :path: Flutter/ephemeral/.symlinks/plugins/flutter_inappwebview_macos/macos flutter_local_authentication: @@ -99,6 +104,7 @@ SPEC CHECKSUMS: cw_mweb: 7440b12ead811dda972a9918442ea2a458e8742c device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f devicelocale: 9f0f36ac651cabae2c33f32dcff4f32b61c38225 + fast_scanner: d31bae07e2653403a69dac99fb710c1722b16a97 flutter_inappwebview_macos: 9600c9df9fdb346aaa8933812009f8d94304203d flutter_local_authentication: 85674893931e1c9cfa7c9e4f5973cb8c56b018b0 flutter_secure_storage_macos: d56e2d218c1130b262bef8b4a7d64f88d7f9c9ea diff --git a/pubspec_base.yaml b/pubspec_base.yaml index 2abd8401a..fa70b5837 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -14,8 +14,10 @@ dependencies: # provider: ^6.0.3 rxdart: ^0.28.0 yaml: ^3.1.1 - #barcode_scan: any - barcode_scan2: ^4.2.1 + fast_scanner: + git: + url: https://github.com/MrCyjaneK/fast_scanner + ref: c8311b46cc67dd02250970a54d2a4526168187f3 http: ^1.1.0 path_provider: ^2.0.11 mobx: ^2.1.4 diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index b8c933a2e..9ebab6b6f 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -123,6 +123,8 @@ "change_rep_successful": "تم تغيير ممثل بنجاح", "change_wallet_alert_content": "هل تريد تغيير المحفظة الحالية إلى ${wallet_name}؟", "change_wallet_alert_title": "تغيير المحفظة الحالية", + "choose_a_payment_method": "اختر طريقة الدفع", + "choose_a_provider": "اختر مزودًا", "choose_account": "اختر حساب", "choose_address": "\n\nالرجاء اختيار عنوان:", "choose_card_value": "اختر قيمة بطاقة", @@ -136,7 +138,7 @@ "clearnet_link": "رابط Clearnet", "close": "يغلق", "coin_control": "التحكم في العملة (اختياري)", - "cold_or_recover_wallet": "أضف محفظة باردة أو استعد محفظة ورقية", + "cold_or_recover_wallet": "أضف محفظة للقراءة فقط من Cupcake أو محفظة باردة أو استعاد محفظة ورقية", "color_theme": "سمة اللون", "commit_transaction_amount_fee": "تنفيذ الصفقة\nالمبلغ: ${amount}\nالرسوم: ${fee}", "confirm": "تأكيد", @@ -161,6 +163,7 @@ "contact_list_contacts": "جهات الاتصال", "contact_list_wallets": "محافظ", "contact_name": "اسم جهة الاتصال", + "contact_name_exists": " .ﻒﻠﺘﺨﻣ ﻢﺳﺍ ﺭﺎﻴﺘﺧﺍ ءﺎﺟﺮﻟﺍ .ﻞﻌﻔﻟﺎﺑ ﺓﺩﻮﺟﻮﻣ ﻢﺳﻻﺍ ﺍﺬﻬﺑ ﻝﺎﺼﺗﺍ ﺔﻬﺟ", "contact_support": "اتصل بالدعم", "continue_text": "التالي", "contract_warning": "تم وضع علامة على عنوان العقد هذا على أنه احتيالي محتمل. يرجى المعالجة بحذر.", @@ -216,6 +219,7 @@ "disable_fee_api_warning": "من خلال إيقاف تشغيل هذا ، قد تكون معدلات الرسوم غير دقيقة في بعض الحالات ، لذلك قد ينتهي بك الأمر إلى دفع مبالغ زائدة أو دفع رسوم المعاملات الخاصة بك", "disable_fiat": "تعطيل fiat", "disable_sell": "قم بتعطيل إجراء البيع", + "disable_trade_option": "تعطيل خيار التجارة", "disableBatteryOptimization": "تعطيل تحسين البطارية", "disableBatteryOptimizationDescription": "هل تريد تعطيل تحسين البطارية من أجل جعل الخلفية مزامنة تعمل بحرية وسلاسة؟", "disabled": "معطلة", @@ -296,6 +300,7 @@ "expiry_and_validity": "انتهاء الصلاحية والصلاحية", "export_backup": "تصدير نسخة احتياطية", "export_logs": "سجلات التصدير", + "export_outputs": "مخرجات التصدير", "extra_id": "معرف إضافي:", "extracted_address_content": "سوف ترسل الأموال إلى\n${recipient_name}", "failed_authentication": "${state_error} فشل المصادقة.", @@ -336,7 +341,9 @@ "haven_app": "Haven بواسطة Cake Wallet", "haven_app_wallet_text": "محفظة رائعة ل Haven", "help": "مساعده", + "hidden_addresses": "العناوين المخفية", "hidden_balance": "الميزان الخفي", + "hide": "يخفي", "hide_details": "أخف التفاصيل", "high_contrast_theme": "موضوع عالي التباين", "home_screen_settings": "إعدادات الشاشة الرئيسية", @@ -500,6 +507,7 @@ "pre_seed_title": "مهم", "prepaid_cards": "البطاقات المدفوعة مسبقا", "prevent_screenshots": "منع لقطات الشاشة وتسجيل الشاشة", + "primary_address": "العنوان الأساسي", "privacy": "خصوصية", "privacy_policy": "سياسة الخصوصية", "privacy_settings": "إعدادات الخصوصية", @@ -696,6 +704,7 @@ "share": "يشارك", "share_address": "شارك العنوان", "shared_seed_wallet_groups": "مجموعات محفظة البذور المشتركة", + "show": "يعرض", "show_details": "اظهر التفاصيل", "show_keys": "اظهار السييد / المفاتيح", "show_market_place": "إظهار السوق", @@ -942,6 +951,5 @@ "you_pay": "انت تدفع", "you_will_get": "حول الى", "you_will_send": "تحويل من", - "yy": "YY", - "contact_name_exists": " .ﻒﻠﺘﺨﻣ ﻢﺳﺍ ﺭﺎﻴﺘﺧﺍ ءﺎﺟﺮﻟﺍ .ﻞﻌﻔﻟﺎﺑ ﺓﺩﻮﺟﻮﻣ ﻢﺳﻻﺍ ﺍﺬﻬﺑ ﻝﺎﺼﺗﺍ ﺔﻬﺟ" -} + "yy": "YY" +} \ No newline at end of file diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index 2fd644d01..91256938d 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -123,6 +123,8 @@ "change_rep_successful": "Успешно промени представител", "change_wallet_alert_content": "Искате ли да смените сегашния портфейл на ${wallet_name}?", "change_wallet_alert_title": "Смяна на сегашния портфейл", + "choose_a_payment_method": "Изберете начин на плащане", + "choose_a_provider": "Изберете доставчик", "choose_account": "Избиране на профил", "choose_address": "\n\nМоля, изберете адреса:", "choose_card_value": "Изберете стойност на картата", @@ -136,7 +138,7 @@ "clearnet_link": "Clearnet връзка", "close": "затвори", "coin_control": "Управление на монетите (не е задължително)", - "cold_or_recover_wallet": "Добавете студен портфейл или възстановете хартиен портфейл", + "cold_or_recover_wallet": "Добавете портфейл само за четене от Cupcake или студен портфейл или възстановете хартиен портфейл", "color_theme": "Цвят", "commit_transaction_amount_fee": "Изпълняване на транзакция\nСума: ${amount}\nТакса: ${fee}", "confirm": "Потвърждаване", @@ -161,6 +163,7 @@ "contact_list_contacts": "Контакти", "contact_list_wallets": "Моите портфейли", "contact_name": "Име на контакт", + "contact_name_exists": "Вече съществува контакт с това име. Моля, изберете друго име.", "contact_support": "Свържи се с отдел поддръжка", "continue_text": "Напред", "contract_warning": "Този адрес на договора е маркиран като потенциално измамник. Моля, обработете с повишено внимание.", @@ -216,6 +219,7 @@ "disable_fee_api_warning": "Като изключите това, таксите могат да бъдат неточни в някои случаи, така че може да се препланите или да не плащате таксите за вашите транзакции", "disable_fiat": "Деактивиране на fiat", "disable_sell": "Деактивирайте действието за продажба", + "disable_trade_option": "Деактивирайте опцията за търговия", "disableBatteryOptimization": "Деактивирайте оптимизацията на батерията", "disableBatteryOptimizationDescription": "Искате ли да деактивирате оптимизацията на батерията, за да направите синхронизирането на фона да работи по -свободно и гладко?", "disabled": "Деактивирано", @@ -296,6 +300,7 @@ "expiry_and_validity": "Изтичане и валидност", "export_backup": "Експортиране на резервно копие", "export_logs": "Експортни дневници", + "export_outputs": "Експортни резултати", "extra_id": "Допълнително ID:", "extracted_address_content": "Ще изпратите средства на \n${recipient_name}", "failed_authentication": "Неуспешно удостоверяване. ${state_error}", @@ -336,7 +341,9 @@ "haven_app": "Haven от Cake Wallet", "haven_app_wallet_text": "Невероятен портфейл за Haven", "help": "Помощ", + "hidden_addresses": "Скрити адреси", "hidden_balance": "Скрит баланс", + "hide": "Скрий", "hide_details": "Скриване на подробностите", "high_contrast_theme": "Тема с висок контраст", "home_screen_settings": "Настройки на началния екран", @@ -500,6 +507,7 @@ "pre_seed_title": "ВАЖНО", "prepaid_cards": "Предплатени карти", "prevent_screenshots": "Предотвратете екранни снимки и запис на екрана", + "primary_address": "Първичен адрес", "privacy": "Поверителност", "privacy_policy": "Политика за поверителността", "privacy_settings": "Настройки за поверителност", @@ -696,6 +704,7 @@ "share": "Дял", "share_address": "Сподели адрес", "shared_seed_wallet_groups": "Споделени групи за портфейли за семена", + "show": "Показване", "show_details": "Показване на подробностите", "show_keys": "Покажи seed/keys", "show_market_place": "Покажи пазар", @@ -942,6 +951,5 @@ "you_pay": "Вие плащате", "you_will_get": "Обръщане в", "you_will_send": "Обръщане от", - "yy": "гг", - "contact_name_exists": "Вече съществува контакт с това име. Моля, изберете друго име." -} + "yy": "гг" +} \ No newline at end of file diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index 8f3b5bf28..0fe38166c 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -123,6 +123,8 @@ "change_rep_successful": "Úspěšně změnil zástupce", "change_wallet_alert_content": "Opravdu chcete změnit aktivní peněženku na ${wallet_name}?", "change_wallet_alert_title": "Přepnout peněženku", + "choose_a_payment_method": "Vyberte metodu platby", + "choose_a_provider": "Vyberte poskytovatele", "choose_account": "Zvolte částku", "choose_address": "\n\nProsím vyberte adresu:", "choose_card_value": "Vyberte hodnotu karty", @@ -136,7 +138,7 @@ "clearnet_link": "Odkaz na Clearnet", "close": "zavřít", "coin_control": "Volba mincí (nepovinné)", - "cold_or_recover_wallet": "Přidejte studenou peněženku nebo obnovte papírovou peněženku", + "cold_or_recover_wallet": "Přidejte peněženku pouze pro čtení z Cupcake nebo studené peněženky nebo obnovte papírovou peněženku", "color_theme": "Barevný motiv", "commit_transaction_amount_fee": "Odeslat transakci\nČástka: ${amount}\nPoplatek: ${fee}", "confirm": "Potvrdit", @@ -161,6 +163,7 @@ "contact_list_contacts": "Kontakty", "contact_list_wallets": "Moje peněženky", "contact_name": "Jméno kontaktu", + "contact_name_exists": "Kontakt s tímto jménem již existuje. Vyberte prosím jiný název.", "contact_support": "Kontaktovat podporu", "continue_text": "Pokračovat", "contract_warning": "Tato adresa smlouvy byla označena jako potenciálně podvodná. Zpracovejte prosím opatrně.", @@ -216,6 +219,7 @@ "disable_fee_api_warning": "Tímto vypnutím by sazby poplatků mohly být v některých případech nepřesné, takže byste mohli skončit přepláváním nebo nedoplatkem poplatků za vaše transakce", "disable_fiat": "Zakázat fiat", "disable_sell": "Zakázat akci prodeje", + "disable_trade_option": "Zakázat možnost TRADE", "disableBatteryOptimization": "Zakázat optimalizaci baterie", "disableBatteryOptimizationDescription": "Chcete deaktivovat optimalizaci baterie, aby se synchronizovala pozadí volně a hladce?", "disabled": "Zakázáno", @@ -296,6 +300,7 @@ "expiry_and_validity": "Vypršení a platnost", "export_backup": "Exportovat zálohu", "export_logs": "Vývozní protokoly", + "export_outputs": "Vývozní výstupy", "extra_id": "Extra ID:", "extracted_address_content": "Prostředky budete posílat na\n${recipient_name}", "failed_authentication": "Ověřování selhalo. ${state_error}", @@ -336,7 +341,9 @@ "haven_app": "Haven od Cake Wallet", "haven_app_wallet_text": "Úžasná peněženka pro Haven", "help": "pomoc", + "hidden_addresses": "Skryté adresy", "hidden_balance": "Skrytý zůstatek", + "hide": "Skrýt", "hide_details": "Skrýt detaily", "high_contrast_theme": "Téma s vysokým kontrastem", "home_screen_settings": "Nastavení domovské obrazovky", @@ -500,6 +507,7 @@ "pre_seed_title": "DŮLEŽITÉ", "prepaid_cards": "Předplacené karty", "prevent_screenshots": "Zabránit vytváření snímků obrazovky a nahrávání obrazovky", + "primary_address": "Primární adresa", "privacy": "Soukromí", "privacy_policy": "Zásady ochrany soukromí", "privacy_settings": "Nastavení soukromí", @@ -696,6 +704,7 @@ "share": "Podíl", "share_address": "Sdílet adresu", "shared_seed_wallet_groups": "Skupiny sdílených semen", + "show": "Show", "show_details": "Zobrazit detaily", "show_keys": "Zobrazit seed/klíče", "show_market_place": "Zobrazit trh", @@ -942,6 +951,5 @@ "you_pay": "Zaplatíte", "you_will_get": "Směnit na", "you_will_send": "Směnit z", - "yy": "YY", - "contact_name_exists": "Kontakt s tímto jménem již existuje. Vyberte prosím jiný název." -} + "yy": "YY" +} \ No newline at end of file diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 77a531d1b..212ce05f7 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -123,6 +123,8 @@ "change_rep_successful": "Vertreter erfolgreich gerändert", "change_wallet_alert_content": "Möchten Sie die aktuelle Wallet zu ${wallet_name} ändern?", "change_wallet_alert_title": "Aktuelle Wallet ändern", + "choose_a_payment_method": "Wählen Sie eine Zahlungsmethode", + "choose_a_provider": "Wählen Sie einen Anbieter", "choose_account": "Konto auswählen", "choose_address": "\n\nBitte wählen Sie die Adresse:", "choose_card_value": "Wählen Sie einen Kartenwert", @@ -136,7 +138,7 @@ "clearnet_link": "Clearnet-Link", "close": "Schließen", "coin_control": "Coin Control (optional)", - "cold_or_recover_wallet": "Fügen Sie eine Cold Wallet hinzu oder stellen Sie eine Paper Wallet wieder her", + "cold_or_recover_wallet": "Fügen Sie eine schreibgeschützte Brieftasche von Cupcake oder eine kalte Brieftasche hinzu oder erholen Sie sich eine Brieftasche", "color_theme": "Farbthema", "commit_transaction_amount_fee": "Transaktion absenden\nBetrag: ${amount}\nGebühr: ${fee}", "confirm": "Bestätigen", @@ -161,6 +163,7 @@ "contact_list_contacts": "Kontakte", "contact_list_wallets": "Meine Wallets", "contact_name": "Name des Kontakts", + "contact_name_exists": "Ein Kontakt mit diesem Namen besteht bereits. Bitte wählen Sie einen anderen Namen.", "contact_support": "Support kontaktieren", "continue_text": "Weiter", "contract_warning": "Diese Vertragsadresse wurde als potenziell betrügerisch gekennzeichnet. Bitte verarbeiten Sie mit Vorsicht.", @@ -216,6 +219,7 @@ "disable_fee_api_warning": "Wenn dies ausgeschaltet wird, sind die Gebührenquoten in einigen Fällen möglicherweise ungenau, sodass Sie die Gebühren für Ihre Transaktionen möglicherweise überbezahlt oder unterzahlt", "disable_fiat": "Fiat deaktivieren", "disable_sell": "Verkaufsaktion deaktivieren", + "disable_trade_option": "Handelsoption deaktivieren", "disableBatteryOptimization": "Batterieoptimierung deaktivieren", "disableBatteryOptimizationDescription": "Möchten Sie die Batterieoptimierung deaktivieren, um die Hintergrundsynchronisierung reibungsloser zu gestalten?", "disabled": "Deaktiviert", @@ -296,6 +300,7 @@ "expiry_and_validity": "Ablauf und Gültigkeit", "export_backup": "Sicherung exportieren", "export_logs": "Exportprotokolle", + "export_outputs": "Exportausgaben", "extra_id": "Extra ID:", "extracted_address_content": "Sie senden Geld an\n${recipient_name}", "failed_authentication": "Authentifizierung fehlgeschlagen. ${state_error}", @@ -336,7 +341,9 @@ "haven_app": "Haven von Cake Wallet", "haven_app_wallet_text": "Eine großartige Wallet für Haven", "help": "hilfe", + "hidden_addresses": "Versteckte Adressen", "hidden_balance": "Verstecktes Guthaben", + "hide": "Verstecken", "hide_details": "Details ausblenden", "high_contrast_theme": "Kontrastreiches Thema", "home_screen_settings": "Einstellungen für den Startbildschirm", @@ -501,6 +508,7 @@ "pre_seed_title": "WICHTIG", "prepaid_cards": "Karten mit Guthaben", "prevent_screenshots": "Verhindern Sie Screenshots und Bildschirmaufzeichnungen", + "primary_address": "Primäradresse", "privacy": "Datenschutz", "privacy_policy": "Datenschutzrichtlinie", "privacy_settings": "Datenschutzeinstellungen", @@ -697,6 +705,7 @@ "share": "Teilen", "share_address": "Adresse teilen ", "shared_seed_wallet_groups": "Gemeinsame Walletsseed Gruppen", + "show": "Zeigen", "show_details": "Details anzeigen", "show_keys": "Seed/Schlüssel anzeigen", "show_market_place": "Marktplatz anzeigen", diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 4fe375ff9..15e0c04b3 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -123,6 +123,8 @@ "change_rep_successful": "Successfully changed representative", "change_wallet_alert_content": "Do you want to change current wallet to ${wallet_name}?", "change_wallet_alert_title": "Change current wallet", + "choose_a_payment_method": "Choose a payment method", + "choose_a_provider": "Choose a provider", "choose_account": "Choose account", "choose_address": "\n\nPlease choose the address:", "choose_card_value": "Choose a card value", @@ -136,7 +138,7 @@ "clearnet_link": "Clearnet link", "close": "Close", "coin_control": "Coin control (optional)", - "cold_or_recover_wallet": "Add a cold wallet or recover a paper wallet", + "cold_or_recover_wallet": "Add a read-only wallet from Cupcake or a cold wallet or recover a paper wallet", "color_theme": "Color theme", "commit_transaction_amount_fee": "Commit transaction\nAmount: ${amount}\nFee: ${fee}", "confirm": "Confirm", @@ -161,6 +163,7 @@ "contact_list_contacts": "Contacts", "contact_list_wallets": "My Wallets", "contact_name": "Contact Name", + "contact_name_exists": "A contact with that name already exists. Please choose a different name.", "contact_support": "Contact Support", "continue_text": "Continue", "contract_warning": "This contract address has been flagged as potentially fraudulent. Please process with caution.", @@ -216,6 +219,7 @@ "disable_fee_api_warning": "By turning this off, the fee rates might be inaccurate in some cases, so you might end up overpaying or underpaying the fees for your transactions", "disable_fiat": "Disable fiat", "disable_sell": "Disable sell action", + "disable_trade_option": "Disable trade option", "disableBatteryOptimization": "Disable Battery Optimization", "disableBatteryOptimizationDescription": "Do you want to disable battery optimization in order to make background sync run more freely and smoothly?", "disabled": "Disabled", @@ -296,6 +300,7 @@ "expiry_and_validity": "Expiry and Validity", "export_backup": "Export backup", "export_logs": "Export logs", + "export_outputs": "Export outputs", "extra_id": "Extra ID:", "extracted_address_content": "You will be sending funds to\n${recipient_name}", "failed_authentication": "Failed authentication. ${state_error}", @@ -502,6 +507,7 @@ "pre_seed_title": "IMPORTANT", "prepaid_cards": "Prepaid Cards", "prevent_screenshots": "Prevent screenshots and screen recording", + "primary_address": "Primary Address", "privacy": "Privacy", "privacy_policy": "Privacy Policy", "privacy_settings": "Privacy settings", @@ -945,6 +951,5 @@ "you_pay": "You Pay", "you_will_get": "Convert to", "you_will_send": "Convert from", - "yy": "YY", - "contact_name_exists": "A contact with that name already exists. Please choose a different name." -} + "yy": "YY" +} \ No newline at end of file diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index 31498df2d..83c0a09f0 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -123,6 +123,8 @@ "change_rep_successful": "Representante cambiado con éxito", "change_wallet_alert_content": "¿Quieres cambiar la billetera actual a ${wallet_name}?", "change_wallet_alert_title": "Cambiar billetera actual", + "choose_a_payment_method": "Elija un método de pago", + "choose_a_provider": "Elija un proveedor", "choose_account": "Elegir cuenta", "choose_address": "\n\nPor favor elija la dirección:", "choose_card_value": "Elige un valor de tarjeta", @@ -136,7 +138,7 @@ "clearnet_link": "enlace Clearnet", "close": "Cerca", "coin_control": "Control de monedas (opcional)", - "cold_or_recover_wallet": "Agrega una billetera fría o recupera una billetera de papel", + "cold_or_recover_wallet": "Agregue una billetera de solo lectura de Cupcake o una billetera en frío o recupere una billetera de papel", "color_theme": "Tema de color", "commit_transaction_amount_fee": "Confirmar transacción\nCantidad: ${amount}\nCuota: ${fee}", "confirm": "Confirmar", @@ -161,6 +163,7 @@ "contact_list_contacts": "Contactos", "contact_list_wallets": "Mis billeteras", "contact_name": "Nombre de contacto", + "contact_name_exists": "Ya existe un contacto con ese nombre. Elija un nombre diferente.", "contact_support": "Contactar con Soporte", "continue_text": "Continuar", "contract_warning": "Esta dirección de contrato ha sido marcada como potencialmente fraudulenta. Por favor, procese con precaución.", @@ -216,6 +219,7 @@ "disable_fee_api_warning": "Al apagar esto, las tasas de tarifas pueden ser inexactas en algunos casos, por lo que puede terminar pagando en exceso o pagando menos las tarifas por sus transacciones", "disable_fiat": "Deshabilitar fiat", "disable_sell": "Desactivar acción de venta", + "disable_trade_option": "Deshabilitar la opción de comercio", "disableBatteryOptimization": "Deshabilitar la optimización de la batería", "disableBatteryOptimizationDescription": "¿Desea deshabilitar la optimización de la batería para que la sincronización de fondo se ejecute más libremente y sin problemas?", "disabled": "Desactivado", @@ -296,6 +300,7 @@ "expiry_and_validity": "Vencimiento y validez", "export_backup": "Exportar copia de seguridad", "export_logs": "Registros de exportación", + "export_outputs": "Exportaciones de exportación", "extra_id": "ID adicional:", "extracted_address_content": "Enviará fondos a\n${recipient_name}", "failed_authentication": "Autenticación fallida. ${state_error}", @@ -336,7 +341,9 @@ "haven_app": "Haven by Cake Wallet", "haven_app_wallet_text": "Increíble billetera para Haven", "help": "ayuda", + "hidden_addresses": "Direcciones ocultas", "hidden_balance": "Balance oculto", + "hide": "Esconder", "hide_details": "Ocultar detalles", "high_contrast_theme": "Tema de alto contraste", "home_screen_settings": "Configuración de la pantalla de inicio", @@ -501,6 +508,7 @@ "pre_seed_title": "IMPORTANTE", "prepaid_cards": "Tajetas prepagadas", "prevent_screenshots": "Evitar capturas de pantalla y grabación de pantalla", + "primary_address": "Dirección principal", "privacy": "Privacidad", "privacy_policy": "Política de privacidad", "privacy_settings": "Configuración de privacidad", @@ -697,6 +705,7 @@ "share": "Compartir", "share_address": "Compartir dirección", "shared_seed_wallet_groups": "Grupos de billetera de semillas compartidas", + "show": "Espectáculo", "show_details": "Mostrar detalles", "show_keys": "Mostrar semilla/claves", "show_market_place": "Mostrar mercado", @@ -943,6 +952,5 @@ "you_pay": "Tú pagas", "you_will_get": "Convertir a", "you_will_send": "Convertir de", - "yy": "YY", - "contact_name_exists": "Ya existe un contacto con ese nombre. Elija un nombre diferente." -} + "yy": "YY" +} \ No newline at end of file diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index f96e9e304..549ec5275 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -123,6 +123,8 @@ "change_rep_successful": "Représentant changé avec succès", "change_wallet_alert_content": "Souhaitez-vous changer le portefeuille (wallet) actuel vers ${wallet_name} ?", "change_wallet_alert_title": "Changer le portefeuille (wallet) actuel", + "choose_a_payment_method": "Choisissez un mode de paiement", + "choose_a_provider": "Choisissez un fournisseur", "choose_account": "Choisir le compte", "choose_address": "\n\nMerci de choisir l'adresse :", "choose_card_value": "Choisissez une valeur de carte", @@ -136,7 +138,7 @@ "clearnet_link": "Lien Clearnet", "close": "Fermer", "coin_control": "Contrôle optionnel des pièces (coins)", - "cold_or_recover_wallet": "Ajoutez un portefeuille froid (cold wallet) ou récupérez un portefeuille papier (paper wallet)", + "cold_or_recover_wallet": "Ajoutez un portefeuille en lecture seule de Cupcake ou d'un portefeuille froid ou récupérez un portefeuille en papier", "color_theme": "Thème", "commit_transaction_amount_fee": "Valider la transaction\nMontant : ${amount}\nFrais : ${fee}", "confirm": "Confirmer", @@ -161,6 +163,7 @@ "contact_list_contacts": "Contacts", "contact_list_wallets": "Mes portefeuilles (wallets)", "contact_name": "Nom de Contact", + "contact_name_exists": "Un contact portant ce nom existe déjà. Veuillez choisir un autre nom.", "contact_support": "Contacter l'assistance", "continue_text": "Continuer", "contract_warning": "Cette adresse contractuelle a été signalée comme potentiellement frauduleuse. Veuillez traiter avec prudence.", @@ -216,6 +219,7 @@ "disable_fee_api_warning": "En désactivant cela, les taux de frais peuvent être inexacts dans certains cas, vous pourriez donc finir par payer trop ou sous-paiement les frais pour vos transactions", "disable_fiat": "Désactiver les montants en fiat", "disable_sell": "Désactiver l'action de vente", + "disable_trade_option": "Désactiver l'option de commerce", "disableBatteryOptimization": "Désactiver l'optimisation de la batterie", "disableBatteryOptimizationDescription": "Voulez-vous désactiver l'optimisation de la batterie afin de faire fonctionner la synchronisation d'arrière-plan plus librement et en douceur?", "disabled": "Désactivé", @@ -296,6 +300,7 @@ "expiry_and_validity": "Expiration et validité", "export_backup": "Exporter la sauvegarde", "export_logs": "Journaux d'exportation", + "export_outputs": "Exportation des sorties", "extra_id": "ID supplémentaire :", "extracted_address_content": "Vous allez envoyer des fonds à\n${recipient_name}", "failed_authentication": "Échec d'authentification. ${state_error}", @@ -336,7 +341,9 @@ "haven_app": "Haven par Cake Wallet", "haven_app_wallet_text": "Super portefeuille (wallet) pour Haven", "help": "aide", + "hidden_addresses": "Adresses cachées", "hidden_balance": "Solde Caché", + "hide": "Cacher", "hide_details": "Masquer les détails", "high_contrast_theme": "Thème à contraste élevé", "home_screen_settings": "Paramètres de l'écran d'accueil", @@ -500,6 +507,7 @@ "pre_seed_title": "IMPORTANT", "prepaid_cards": "Cartes prépayées", "prevent_screenshots": "Empêcher les captures d'écran et l'enregistrement d'écran", + "primary_address": "Adresse primaire", "privacy": "Confidentialité", "privacy_policy": "Politique de confidentialité", "privacy_settings": "Paramètres de confidentialité", @@ -696,6 +704,7 @@ "share": "Partager", "share_address": "Partager l'adresse", "shared_seed_wallet_groups": "Groupes de portefeuilles partagés", + "show": "Montrer", "show_details": "Afficher les détails", "show_keys": "Visualiser la phrase secrète (seed) et les clefs", "show_market_place": "Afficher la place de marché", @@ -942,6 +951,5 @@ "you_pay": "Vous payez", "you_will_get": "Convertir vers", "you_will_send": "Convertir depuis", - "yy": "AA", - "contact_name_exists": "Un contact portant ce nom existe déjà. Veuillez choisir un autre nom." -} + "yy": "AA" +} \ No newline at end of file diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index 5fc64e0b3..222a9cf2f 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -123,6 +123,8 @@ "change_rep_successful": "An samu nasarar canzawa wakilin", "change_wallet_alert_content": "Kana so ka canja walat yanzu zuwa ${wallet_name}?", "change_wallet_alert_title": "Canja walat yanzu", + "choose_a_payment_method": "Zabi hanyar biyan kuɗi", + "choose_a_provider": "Zabi mai bada", "choose_account": "Zaɓi asusu", "choose_address": "\n\n Da fatan za a zaɓi adireshin:", "choose_card_value": "Zabi darajar katin", @@ -136,7 +138,7 @@ "clearnet_link": "Lambar makomar kwayoyi", "close": "Rufa", "coin_control": "Sarrafa tsabar kuɗi (na zaɓi)", - "cold_or_recover_wallet": "Samun kashi na baya ko samun kashi na kasa", + "cold_or_recover_wallet": "Aara wani walat mai karanta-kawai Cupcake ko walat ɗin mai sanyi ko murmurewa takarda takarda", "color_theme": "Jigon launi", "commit_transaction_amount_fee": "Aikata ciniki\nAdadi: ${amount}\nKuda: ${fee}", "confirm": "Tabbatar", @@ -161,6 +163,7 @@ "contact_list_contacts": "Lambobin sadarwa", "contact_list_wallets": "Wallets dina", "contact_name": "Sunan Tuntuɓi", + "contact_name_exists": "An riga an sami lamba tare da wannan sunan. Da fatan za a zaɓi suna daban.", "contact_support": "Tuntuɓi Support", "continue_text": "Ci gaba", "contract_warning": "An kafa wannan adireshin kwantaragin kwangilar yayin da yuwuwar zamba. Da fatan za a aiwatar da taka tsantsan.", @@ -216,6 +219,7 @@ "disable_fee_api_warning": "Ta hanyar juya wannan kashe, kudaden da zai iya zama ba daidai ba a wasu halaye, saboda haka zaku iya ƙare da overpaying ko a ƙarƙashin kudaden don ma'amaloli", "disable_fiat": "Dakatar da fiat", "disable_sell": "Kashe karbuwa", + "disable_trade_option": "Musaki zaɓi na kasuwanci", "disableBatteryOptimization": "Kashe ingantawa baturi", "disableBatteryOptimizationDescription": "Shin kana son kashe ingantawa baturi don yin setnc bankwali gudu da yar kyauta da kyau?", "disabled": "tsaya", @@ -296,6 +300,7 @@ "expiry_and_validity": "Karewa da inganci", "export_backup": "Ajiyayyen fitarwa", "export_logs": "Injin fitarwa", + "export_outputs": "Fitarwar fitarwa", "extra_id": "Karin ID:", "extracted_address_content": "Za ku aika da kudade zuwa\n${recipient_name}", "failed_authentication": "Binne wajen shiga. ${state_error}", @@ -336,7 +341,9 @@ "haven_app": "Haven da Cake Wallet", "haven_app_wallet_text": "Aikace-aikacen e-wallet ga Haven", "help": "taimako", + "hidden_addresses": "Adireshin ɓoye", "hidden_balance": "BOYE KUDI", + "hide": "Ɓoye", "hide_details": "Ɓoye cikakkun bayanai", "high_contrast_theme": "Babban Jigon Kwatance", "home_screen_settings": "Saitunan allo na gida", @@ -502,6 +509,7 @@ "pre_seed_title": "MUHIMMANCI", "prepaid_cards": "Katunan shirye-shirye", "prevent_screenshots": "Fada lambobi da jarrabobi na kayan lambobi", + "primary_address": "Adireshin farko", "privacy": "Keɓantawa", "privacy_policy": "takardar kebantawa", "privacy_settings": "Saitunan sirri", @@ -698,6 +706,7 @@ "share": "Raba", "share_address": "Raba adireshin", "shared_seed_wallet_groups": "Raba ƙungiya walat", + "show": "Nuna", "show_details": "Nuna Cikakkun bayanai", "show_keys": "Nuna iri/maɓallai", "show_market_place": "Nuna dan kasuwa", @@ -944,6 +953,5 @@ "you_pay": "Ka Bayar", "you_will_get": "Maida zuwa", "you_will_send": "Maida daga", - "yy": "YY", - "contact_name_exists": "An riga an sami lamba tare da wannan sunan. Da fatan za a zaɓi suna daban." -} + "yy": "YY" +} \ No newline at end of file diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index fb3b78900..1b2f3a1b3 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -123,6 +123,8 @@ "change_rep_successful": "सफलतापूर्वक बदलकर प्रतिनिधि", "change_wallet_alert_content": "क्या आप करंट वॉलेट को बदलना चाहते हैं ${wallet_name}?", "change_wallet_alert_title": "वर्तमान बटुआ बदलें", + "choose_a_payment_method": "एक भुगतान विधि का चयन करें", + "choose_a_provider": "एक प्रदाता चुनें", "choose_account": "खाता चुनें", "choose_address": "\n\nकृपया पता चुनें:", "choose_card_value": "एक कार्ड मूल्य चुनें", @@ -136,7 +138,7 @@ "clearnet_link": "क्लियरनेट लिंक", "close": "बंद करना", "coin_control": "सिक्का नियंत्रण (वैकल्पिक)", - "cold_or_recover_wallet": "कोल्ड वॉलेट जोड़ें या पेपर वॉलेट पुनर्प्राप्त करें", + "cold_or_recover_wallet": "Cupcake या एक कोल्ड वॉलेट से एक रीड-ओनली वॉलेट जोड़ें या एक पेपर वॉलेट को पुनर्प्राप्त करें", "color_theme": "रंग विषय", "commit_transaction_amount_fee": "लेन-देन करें\nरकम: ${amount}\nशुल्क: ${fee}", "confirm": "की पुष्टि करें", @@ -161,6 +163,7 @@ "contact_list_contacts": "संपर्क", "contact_list_wallets": "मेरा बटुआ", "contact_name": "संपर्क नाम", + "contact_name_exists": "उस नाम का एक संपर्क पहले से मौजूद है. कृपया कोई भिन्न नाम चुनें.", "contact_support": "सहायता से संपर्क करें", "continue_text": "जारी रहना", "contract_warning": "इस अनुबंध के पते को संभावित रूप से धोखाधड़ी के रूप में चिह्नित किया गया है। कृपया सावधानी के साथ प्रक्रिया करें।", @@ -216,6 +219,7 @@ "disable_fee_api_warning": "इसे बंद करने से, कुछ मामलों में शुल्क दरें गलत हो सकती हैं, इसलिए आप अपने लेनदेन के लिए फीस को कम कर सकते हैं या कम कर सकते हैं", "disable_fiat": "िएट को अक्षम करें", "disable_sell": "बेचने की कार्रवाई अक्षम करें", + "disable_trade_option": "व्यापार विकल्प अक्षम करें", "disableBatteryOptimization": "बैटरी अनुकूलन अक्षम करें", "disableBatteryOptimizationDescription": "क्या आप बैकग्राउंड सिंक को अधिक स्वतंत्र और सुचारू रूप से चलाने के लिए बैटरी ऑप्टिमाइज़ेशन को अक्षम करना चाहते हैं?", "disabled": "अक्षम", @@ -296,6 +300,7 @@ "expiry_and_validity": "समाप्ति और वैधता", "export_backup": "निर्यात बैकअप", "export_logs": "निर्यात लॉग", + "export_outputs": "निर्यात आउटपुट", "extra_id": "अतिरिक्त आईडी:", "extracted_address_content": "आपको धनराशि भेजी जाएगी\n${recipient_name}", "failed_authentication": "प्रमाणीकरण विफल. ${state_error}", @@ -336,7 +341,9 @@ "haven_app": "Haven by Cake Wallet", "haven_app_wallet_text": "Awesome wallet for Haven", "help": "मदद करना", + "hidden_addresses": "छिपे हुए पते", "hidden_balance": "छिपा हुआ संतुलन", + "hide": "छिपाना", "hide_details": "विवरण छुपाएं", "high_contrast_theme": "उच्च कंट्रास्ट थीम", "home_screen_settings": "होम स्क्रीन सेटिंग्स", @@ -501,6 +508,7 @@ "pre_seed_title": "महत्वपूर्ण", "prepaid_cards": "पूर्वदत्त कार्ड", "prevent_screenshots": "स्क्रीनशॉट और स्क्रीन रिकॉर्डिंग रोकें", + "primary_address": "प्राथमिक पता", "privacy": "गोपनीयता", "privacy_policy": "गोपनीयता नीति", "privacy_settings": "गोपनीयता सेटिंग्स", @@ -698,6 +706,7 @@ "share": "शेयर करना", "share_address": "पता साझा करें", "shared_seed_wallet_groups": "साझा बीज बटुए समूह", + "show": "दिखाओ", "show_details": "विवरण दिखाएं", "show_keys": "बीज / कुंजियाँ दिखाएँ", "show_market_place": "बाज़ार दिखाएँ", @@ -944,6 +953,5 @@ "you_pay": "आप भुगतान करते हैं", "you_will_get": "में बदलें", "you_will_send": "से रूपांतरित करें", - "yy": "वाईवाई", - "contact_name_exists": "उस नाम का एक संपर्क पहले से मौजूद है. कृपया कोई भिन्न नाम चुनें." -} + "yy": "वाईवाई" +} \ No newline at end of file diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index 230aa955b..4e106fdf6 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -123,6 +123,8 @@ "change_rep_successful": "Uspješno promijenjena reprezentativna", "change_wallet_alert_content": "Želite li promijeniti trenutni novčanik u ${wallet_name}?", "change_wallet_alert_title": "Izmijeni trenutni novčanik", + "choose_a_payment_method": "Odaberite način plaćanja", + "choose_a_provider": "Odaberite davatelja usluga", "choose_account": "Odaberi račun", "choose_address": "\n\nOdaberite adresu:", "choose_card_value": "Odaberite vrijednost kartice", @@ -136,7 +138,7 @@ "clearnet_link": "Clearnet veza", "close": "Zatvoriti", "coin_control": "Kontrola novca (nije obavezno)", - "cold_or_recover_wallet": "Dodajte hladni novčanik ili povratite papirnati novčanik", + "cold_or_recover_wallet": "Dodajte novčanik samo za čitanje od Cupcake ili hladnog novčanika ili oporavite papirni novčanik", "color_theme": "Shema boja", "commit_transaction_amount_fee": "Izvrši transakciju \nAmount: ${amount}\nFee: ${fee}", "confirm": "Potvrdi", @@ -161,6 +163,7 @@ "contact_list_contacts": "Kontakti", "contact_list_wallets": "Moji novčanici", "contact_name": "Ime kontakta", + "contact_name_exists": "Kontakt s tim imenom već postoji. Odaberite drugo ime.", "contact_support": "Kontaktirajte podršku", "continue_text": "Nastavak", "contract_warning": "Ova adresa ugovora označena je kao potencijalno lažna. Molimo obradite s oprezom.", @@ -216,6 +219,7 @@ "disable_fee_api_warning": "Isključivanjem ovoga, stope naknade u nekim bi slučajevima mogle biti netočne, tako da biste mogli preplatiti ili predati naknadu za vaše transakcije", "disable_fiat": "Isključi, fiat", "disable_sell": "Onemogući akciju prodaje", + "disable_trade_option": "Onemogući trgovinsku opciju", "disableBatteryOptimization": "Onemogući optimizaciju baterije", "disableBatteryOptimizationDescription": "Želite li onemogućiti optimizaciju baterije kako bi se pozadinska sinkronizacija radila slobodnije i glatko?", "disabled": "Onemogućeno", @@ -296,6 +300,7 @@ "expiry_and_validity": "Istek i valjanost", "export_backup": "Izvezi sigurnosnu kopiju", "export_logs": "Izvozni trupci", + "export_outputs": "Izvoz izlaza", "extra_id": "Dodatni ID:", "extracted_address_content": "Poslat ćete sredstva primatelju\n${recipient_name}", "failed_authentication": "Autentifikacija neuspješna. ${state_error}", @@ -336,7 +341,9 @@ "haven_app": "Haven by Cake Wallet", "haven_app_wallet_text": "Awesome wallet for Haven", "help": "pomozite", + "hidden_addresses": "Skrivene adrese", "hidden_balance": "Skriven iznos", + "hide": "Sakriti", "hide_details": "Sakrij pojedinosti", "high_contrast_theme": "Tema visokog kontrasta", "home_screen_settings": "Postavke početnog zaslona", @@ -500,6 +507,7 @@ "pre_seed_title": "VAŽNO", "prepaid_cards": "Unaprijed plaćene kartice", "prevent_screenshots": "Spriječite snimke zaslona i snimanje zaslona", + "primary_address": "Primarna adresa", "privacy": "Privatnost", "privacy_policy": "Pravila privatnosti", "privacy_settings": "Postavke privatnosti", @@ -696,6 +704,7 @@ "share": "Udio", "share_address": "Podijeli adresu", "shared_seed_wallet_groups": "Zajedničke grupe za sjeme novčanika", + "show": "Pokazati", "show_details": "Prikaži pojedinosti", "show_keys": "Prikaži pristupni izraz/ključ", "show_market_place": "Prikaži tržište", @@ -942,6 +951,5 @@ "you_pay": "Vi plaćate", "you_will_get": "Razmijeni u", "you_will_send": "Razmijeni iz", - "yy": "GG", - "contact_name_exists": "Kontakt s tim imenom već postoji. Odaberite drugo ime." -} + "yy": "GG" +} \ No newline at end of file diff --git a/res/values/strings_hy.arb b/res/values/strings_hy.arb index 63a986103..40142ca5b 100644 --- a/res/values/strings_hy.arb +++ b/res/values/strings_hy.arb @@ -123,6 +123,8 @@ "change_rep_successful": "Ներկայացուցչի փոփոխությունը հաջողությամբ կատարվեց", "change_wallet_alert_content": "Ցանկանում եք փոխել ընթացիկ դրամապանակը ${wallet_name}?", "change_wallet_alert_title": "Փոխել ընթացիկ դրամապանակը", + "choose_a_payment_method": "Ընտրեք վճարման եղանակ", + "choose_a_provider": "Ընտրեք մատակարար", "choose_account": "Ընտրեք հաշիվը", "choose_address": "\n\nԽնդրում ենք ընտրեք հասցեն", "choose_card_value": "Ընտրեք քարտի արժեք", @@ -136,7 +138,7 @@ "clearnet_link": "Բաց ցանցի հղում", "close": "Փակել", "coin_control": "Մետաղադրամի վերահսկում (ըստ ցանկության)", - "cold_or_recover_wallet": "Ավելացնել սառը դրամապանակ կամ վերականգնել թղթային դրամապանակ", + "cold_or_recover_wallet": "Cupcake կամ ցուրտ դրամապանակից ավելացնել միայն ընթերցված դրամապանակ կամ վերականգնել թղթի դրամապանակը", "color_theme": "Գույների տեսք", "commit_transaction_amount_fee": "Հաստատել գործարքը\nՍկզբնական գումար. ${amount}\nՄիջնորդավճար. ${fee}", "confirm": "Հաստատել", @@ -161,6 +163,7 @@ "contact_list_contacts": "Կոնտակտներ", "contact_list_wallets": "Իմ դրամապանակներ", "contact_name": "Կոնտակտի անուն", + "contact_name_exists": "Այդ անվանման հետ կապ կա արդեն: Խնդրում ենք ընտրել այլ անուն:", "contact_support": "Հետադարձ կապ", "continue_text": "Շարունակել", "contract_warning": "Պայմանագրի այս հասցեն դրոշմել է որպես հնարավոր կեղծ: Խնդրում ենք զգուշությամբ մշակել:", @@ -216,6 +219,7 @@ "disable_fee_api_warning": "Դրանից անջատելով, վճարների տեմպերը որոշ դեպքերում կարող են անճիշտ լինել, այնպես որ դուք կարող եք վերջ տալ ձեր գործարքների համար վճարների գերավճարների կամ գերավճարների վրա", "disable_fiat": "Անջատել ֆիատ", "disable_sell": "Անջատել վաճառք գործողությունը", + "disable_trade_option": "Անջատեք առեւտրի տարբերակը", "disableBatteryOptimization": "Անջատել մարտկոցի օպտիմիզացիան", "disableBatteryOptimizationDescription": "Դուք ցանկանում եք անջատել մարտկոցի օպտիմիզացիան ֆոնային համաժամացման ավելի ազատ և հարթ ընթացքի համար?", "disabled": "Անջատված", @@ -296,6 +300,7 @@ "expiry_and_validity": "Վավերականություն և լրացում", "export_backup": "Արտահանել կրկնօրինակը", "export_logs": "Արտահանման տեղեկամատյաններ", + "export_outputs": "Արտահանման արդյունքներ", "extra_id": "Լրացուցիչ ID", "extracted_address_content": "Դուք կուղարկեք գումար ${recipient_name}", "failed_authentication": "Վավերացումը ձախողվեց. ${state_error}", @@ -336,7 +341,9 @@ "haven_app": "Haven ծրագիր", "haven_app_wallet_text": "Հիանալի հաշվեհամար Haven համար", "help": "Օգնություն", + "hidden_addresses": "Թաքնված հասցեներ", "hidden_balance": "Թաքնված մնացորդ", + "hide": "Թաքցնել", "hide_details": "Թաքցնել մանրամասները", "high_contrast_theme": "Բարձր հակադրության տեսք", "home_screen_settings": "Գլխավոր էկրանի կարգավորումներ", @@ -366,14 +373,22 @@ "ledger_error_wrong_app": "Խնդրում ենք համոզվել, որ դուք բացել եք ճիշտ ծրագիրը ձեր Ledger-ում", "ledger_please_enable_bluetooth": "Խնդրում ենք միացնել Bluetooth-ը ձեր Ledger-ը հայտնաբերելու համար", "light_theme": "Լուսավոր", + "litecoin_enable_mweb_sync": "Միացնել MWEB սկան", + "litecoin_mweb": "Մուեբ", + "litecoin_mweb_always_scan": "Սահմանեք Mweb Միշտ սկանավորում", "litecoin_mweb_description": "Mweb- ը նոր արձանագրություն է, որը բերում է ավելի արագ, ավելի էժան եւ ավելի մասնավոր գործարքներ դեպի LITECOIN", "litecoin_mweb_dismiss": "Հեռացնել", + "litecoin_mweb_display_card": "Show ույց տալ Mweb քարտը", "litecoin_mweb_enable": "Միացնել Mweb- ը", "litecoin_mweb_enable_later": "Կարող եք ընտրել Mweb- ը կրկին միացնել ցուցադրման պարամետրերը:", "litecoin_mweb_logs": "Mweb տեղեկամատյաններ", "litecoin_mweb_node": "Mweb հանգույց", "litecoin_mweb_pegin": "Peg in", "litecoin_mweb_pegout": "Հափշտակել", + "litecoin_mweb_scanning": "Mweb սկանավորում", + "litecoin_mweb_settings": "Mweb- ի պարամետրերը", + "litecoin_mweb_warning": "Mweb- ի օգտագործումը սկզբում ներբեռնվի 600 ՄԲ տվյալներ եւ կարող է տեւել 30 րոպե, կախված ցանցի արագությունից: Այս նախնական տվյալները միայն մեկ անգամ ներբեռնելու են եւ հասանելի կլինեն բոլոր Litecoin դրամապանակների համար", + "litecoin_what_is_mweb": "Ինչ է Mweb- ը:", "live_fee_rates": "Ապակի վարձավճարներ API- ի միջոցով", "load_more": "Բեռնել ավելին", "loading_your_wallet": "Ձեր հաշվեհամարը բեռնում է", @@ -492,6 +507,7 @@ "pre_seed_title": "ԿԱՐԵՎՈՐ", "prepaid_cards": "Նախավճարային քարտեր", "prevent_screenshots": "Կանխել էկրանի պատկերները և տեսագրությունը", + "primary_address": "Առաջնային հասցե", "privacy": "Գաղտնիություն", "privacy_policy": "Գաղտնիության քաղաքականություն", "privacy_settings": "Գաղտնիության կարգավորումներ", @@ -688,6 +704,7 @@ "share": "Կիսվել", "share_address": "Կիսվել հասցեով", "shared_seed_wallet_groups": "Համօգտագործված սերմերի դրամապանակների խմբեր", + "show": "Ցուցահանդես", "show_details": "Ցուցադրել մանրամասներ", "show_keys": "Ցուցադրել բանալիներ", "show_market_place": "Ցուցադրել շուկան", diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index 3fa3a958f..04336b76e 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -123,6 +123,8 @@ "change_rep_successful": "Berhasil mengubah perwakilan", "change_wallet_alert_content": "Apakah Anda ingin mengganti dompet saat ini ke ${wallet_name}?", "change_wallet_alert_title": "Ganti dompet saat ini", + "choose_a_payment_method": "Pilih metode pembayaran", + "choose_a_provider": "Pilih penyedia", "choose_account": "Pilih akun", "choose_address": "\n\nSilakan pilih alamat:", "choose_card_value": "Pilih nilai kartu", @@ -136,7 +138,7 @@ "clearnet_link": "Tautan clearnet", "close": "Menutup", "coin_control": "Kontrol koin (opsional)", - "cold_or_recover_wallet": "Tambahkan dompet dingin atau pulihkan dompet kertas", + "cold_or_recover_wallet": "Tambahkan dompet hanya baca dari Cupcake atau dompet dingin atau memulihkan dompet kertas", "color_theme": "Tema warna", "commit_transaction_amount_fee": "Lakukan transaksi\nJumlah: ${amount}\nBiaya: ${fee}", "confirm": "Konfirmasi", @@ -161,6 +163,7 @@ "contact_list_contacts": "Kontak", "contact_list_wallets": "Dompet Saya", "contact_name": "Nama Kontak", + "contact_name_exists": "Kontak dengan nama tersebut sudah ada. Silakan pilih nama lain.", "contact_support": "Hubungi Dukungan", "continue_text": "Lanjutkan", "contract_warning": "Alamat kontrak ini telah ditandai sebagai berpotensi curang. Silakan memproses dengan hati -hati.", @@ -216,6 +219,7 @@ "disable_fee_api_warning": "Dengan mematikan ini, tarif biaya mungkin tidak akurat dalam beberapa kasus, jadi Anda mungkin akan membayar lebih atau membayar biaya untuk transaksi Anda", "disable_fiat": "Nonaktifkan fiat", "disable_sell": "Nonaktifkan aksi jual", + "disable_trade_option": "Nonaktifkan opsi perdagangan", "disableBatteryOptimization": "Nonaktifkan optimasi baterai", "disableBatteryOptimizationDescription": "Apakah Anda ingin menonaktifkan optimasi baterai untuk membuat sinkronisasi latar belakang berjalan lebih bebas dan lancar?", "disabled": "Dinonaktifkan", @@ -296,6 +300,7 @@ "expiry_and_validity": "Kedaluwarsa dan validitas", "export_backup": "Ekspor cadangan", "export_logs": "Log ekspor", + "export_outputs": "Ekspor output", "extra_id": "ID tambahan:", "extracted_address_content": "Anda akan mengirim dana ke\n${recipient_name}", "failed_authentication": "Otentikasi gagal. ${state_error}", @@ -336,7 +341,9 @@ "haven_app": "Haven Oleh Cake Wallet", "haven_app_wallet_text": "Dompet luar biasa untuk Haven", "help": "bantuan", + "hidden_addresses": "Alamat tersembunyi", "hidden_balance": "Saldo Tersembunyi", + "hide": "Bersembunyi", "hide_details": "Sembunyikan Rincian", "high_contrast_theme": "Tema Kontras Tinggi", "home_screen_settings": "Pengaturan layar awal", @@ -502,6 +509,7 @@ "pre_seed_title": "PENTING", "prepaid_cards": "Kartu prabayar", "prevent_screenshots": "Cegah tangkapan layar dan perekaman layar", + "primary_address": "Alamat utama", "privacy": "Privasi", "privacy_policy": "Kebijakan Privasi", "privacy_settings": "Pengaturan privasi", @@ -699,6 +707,7 @@ "share": "Membagikan", "share_address": "Bagikan alamat", "shared_seed_wallet_groups": "Kelompok dompet benih bersama", + "show": "Menunjukkan", "show_details": "Tampilkan Rincian", "show_keys": "Tampilkan seed/kunci", "show_market_place": "Tampilkan Pasar", @@ -945,6 +954,5 @@ "you_pay": "Anda Membayar", "you_will_get": "Konversi ke", "you_will_send": "Konversi dari", - "yy": "YY", - "contact_name_exists": "Kontak dengan nama tersebut sudah ada. Silakan pilih nama lain." -} + "yy": "YY" +} \ No newline at end of file diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index aab18d434..ad15ab3a9 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -123,6 +123,8 @@ "change_rep_successful": "Rappresentante modificato con successo", "change_wallet_alert_content": "Sei sicuro di voler cambiare il portafoglio attuale con ${wallet_name}?", "change_wallet_alert_title": "Cambia portafoglio attuale", + "choose_a_payment_method": "Scegli un metodo di pagamento", + "choose_a_provider": "Scegli un fornitore", "choose_account": "Scegli account", "choose_address": "\n\nSi prega di scegliere l'indirizzo:", "choose_card_value": "Scegli un valore della carta", @@ -136,7 +138,7 @@ "clearnet_link": "Collegamento Clearnet", "close": "Chiudere", "coin_control": "Controllo monete (opzionale)", - "cold_or_recover_wallet": "Aggiungi un cold wallet o recupera un paper wallet", + "cold_or_recover_wallet": "Aggiungi un portafoglio di sola lettura da Cupcake o un portafoglio freddo o recupera un portafoglio di carta", "color_theme": "Colore tema", "commit_transaction_amount_fee": "Invia transazione\nAmmontare: ${amount}\nCommissione: ${fee}", "confirm": "Conferma", @@ -162,6 +164,7 @@ "contact_list_contacts": "Contatti", "contact_list_wallets": "I miei portafogli", "contact_name": "Nome Contatto", + "contact_name_exists": "Esiste già un contatto con quel nome. Scegli un nome diverso.", "contact_support": "Contatta l'assistenza", "continue_text": "Continua", "contract_warning": "Questo indirizzo del contratto è stato contrassegnato come potenzialmente fraudolento. Si prega di elaborare con cautela.", @@ -217,6 +220,7 @@ "disable_fee_api_warning": "Disattivando questo, i tassi delle commissioni potrebbero essere inaccurati in alcuni casi, quindi potresti finire in eccesso o sostenere le commissioni per le transazioni", "disable_fiat": "Disabilita fiat", "disable_sell": "Disabilita l'azione di vendita", + "disable_trade_option": "Disabilita l'opzione commerciale", "disableBatteryOptimization": "Disabilita l'ottimizzazione della batteria", "disableBatteryOptimizationDescription": "Vuoi disabilitare l'ottimizzazione della batteria per far funzionare la sincronizzazione in background più libera e senza intoppi?", "disabled": "Disabilitato", @@ -297,6 +301,7 @@ "expiry_and_validity": "Scadenza e validità", "export_backup": "Esporta backup", "export_logs": "Registri di esportazione", + "export_outputs": "Output di esportazione", "extra_id": "Extra ID:", "extracted_address_content": "Invierai i tuoi fondi a\n${recipient_name}", "failed_authentication": "Autenticazione fallita. ${state_error}", @@ -337,7 +342,9 @@ "haven_app": "Haven by Cake Wallet", "haven_app_wallet_text": "Portafoglio fantastico per Haven", "help": "aiuto", + "hidden_addresses": "Indirizzi nascosti", "hidden_balance": "Saldo Nascosto", + "hide": "Nascondere", "hide_details": "Nascondi dettagli", "high_contrast_theme": "Tema ad alto contrasto", "home_screen_settings": "Impostazioni della schermata iniziale", @@ -502,6 +509,7 @@ "pre_seed_title": "IMPORTANTE", "prepaid_cards": "Carte prepagata", "prevent_screenshots": "Impedisci screenshot e registrazione dello schermo", + "primary_address": "Indirizzo primario", "privacy": "Privacy", "privacy_policy": "Informativa sulla privacy", "privacy_settings": "Impostazioni privacy", @@ -698,6 +706,7 @@ "share": "Condividere", "share_address": "Condividi indirizzo", "shared_seed_wallet_groups": "Gruppi di portafoglio di semi condivisi", + "show": "Spettacolo", "show_details": "Mostra dettagli", "show_keys": "Mostra seme/chiavi", "show_market_place": "Mostra mercato", @@ -945,6 +954,5 @@ "you_pay": "Tu paghi", "you_will_get": "Converti a", "you_will_send": "Conveti da", - "yy": "YY", - "contact_name_exists": "Esiste già un contatto con quel nome. Scegli un nome diverso." -} + "yy": "YY" +} \ No newline at end of file diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index f1fd0acd3..520a73ade 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -123,6 +123,8 @@ "change_rep_successful": "代表者の変更に成功しました", "change_wallet_alert_content": "現在のウォレットをに変更しますか ${wallet_name}?", "change_wallet_alert_title": "現在のウォレットを変更する", + "choose_a_payment_method": "支払い方法を選択します", + "choose_a_provider": "プロバイダーを選択します", "choose_account": "アカウントを選択", "choose_address": "\n\n住所を選択してください:", "choose_card_value": "カード値を選択します", @@ -136,7 +138,7 @@ "clearnet_link": "クリアネット リンク", "close": "近い", "coin_control": "コインコントロール(オプション)", - "cold_or_recover_wallet": "コールド ウォレットを追加するか、ペーパー ウォレットを復元する", + "cold_or_recover_wallet": "Cupcakeまたはコールドウォレットから読み取り専用ウォレットを追加するか、紙の財布を回収する", "color_theme": "カラーテーマ", "commit_transaction_amount_fee": "トランザクションをコミット\n量: ${amount}\n費用: ${fee}", "confirm": "確認する", @@ -161,6 +163,7 @@ "contact_list_contacts": "連絡先", "contact_list_wallets": "マイウォレット", "contact_name": "連絡先", + "contact_name_exists": "その名前の連絡先はすでに存在します。別の名前を選択してください。", "contact_support": "サポートに連絡する", "continue_text": "持続する", "contract_warning": "この契約住所は、潜在的に不正としてフラグが立てられています。注意して処理してください。", @@ -216,6 +219,7 @@ "disable_fee_api_warning": "これをオフにすることで、料金金利は場合によっては不正確になる可能性があるため、取引の費用が過払いまたは不足している可能性があります", "disable_fiat": "フィアットを無効にする", "disable_sell": "販売アクションを無効にする", + "disable_trade_option": "取引オプションを無効にします", "disableBatteryOptimization": "バッテリーの最適化を無効にします", "disableBatteryOptimizationDescription": "バックグラウンドシンクをより自由かつスムーズに実行するために、バッテリーの最適化を無効にしたいですか?", "disabled": "無効", @@ -296,6 +300,7 @@ "expiry_and_validity": "有効期限と有効性", "export_backup": "バックアップのエクスポート", "export_logs": "ログをエクスポートします", + "export_outputs": "エクスポート出力", "extra_id": "追加ID:", "extracted_address_content": "に送金します\n${recipient_name}", "failed_authentication": "認証失敗. ${state_error}", @@ -337,7 +342,9 @@ "haven_app": "Haven by Cake Wallet", "haven_app_wallet_text": "Awesome wallet for Haven", "help": "ヘルプ", + "hidden_addresses": "隠されたアドレス", "hidden_balance": "隠れたバランス", + "hide": "隠れる", "hide_details": "詳細を非表示", "high_contrast_theme": "ハイコントラストテーマ", "home_screen_settings": "ホーム画面の設定", @@ -501,6 +508,7 @@ "pre_seed_title": "重要", "prepaid_cards": "プリペイドカード", "prevent_screenshots": "スクリーンショットと画面録画を防止する", + "primary_address": "主なアドレス", "privacy": "プライバシー", "privacy_policy": "プライバシーポリシー", "privacy_settings": "プライバシー設定", @@ -697,6 +705,7 @@ "share": "共有", "share_address": "住所を共有する", "shared_seed_wallet_groups": "共有シードウォレットグループ", + "show": "見せる", "show_details": "詳細を表示", "show_keys": "シード/キーを表示する", "show_market_place": "マーケットプレイスを表示", @@ -943,6 +952,5 @@ "you_pay": "あなたが支払う", "you_will_get": "に変換", "you_will_send": "から変換", - "yy": "YY", - "contact_name_exists": "その名前の連絡先はすでに存在します。別の名前を選択してください。" -} + "yy": "YY" +} \ No newline at end of file diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index 7e3467cf2..1d9748866 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -123,6 +123,8 @@ "change_rep_successful": "대리인이 성공적으로 변경되었습니다", "change_wallet_alert_content": "현재 지갑을 다음으로 변경 하시겠습니까 ${wallet_name}?", "change_wallet_alert_title": "현재 지갑 변경", + "choose_a_payment_method": "결제 방법을 선택하십시오", + "choose_a_provider": "제공자를 선택하십시오", "choose_account": "계정을 선택하십시오", "choose_address": "\n\n주소를 선택하십시오:", "choose_card_value": "카드 값을 선택하십시오", @@ -136,7 +138,7 @@ "clearnet_link": "클리어넷 링크", "close": "닫다", "coin_control": "코인 제어 (옵션)", - "cold_or_recover_wallet": "콜드 지갑 추가 또는 종이 지갑 복구", + "cold_or_recover_wallet": "Cupcake 또는 차가운 지갑에서 읽기 전용 지갑을 추가하거나 종이 지갑을 복구하십시오.", "color_theme": "색상 테마", "commit_transaction_amount_fee": "커밋 거래\n양: ${amount}\n보수: ${fee}", "confirm": "확인", @@ -161,6 +163,7 @@ "contact_list_contacts": "콘택트 렌즈", "contact_list_wallets": "내 지갑", "contact_name": "담당자 이름", + "contact_name_exists": "해당 이름을 가진 연락처가 이미 존재합니다. 다른 이름을 선택하세요.", "contact_support": "지원팀에 문의", "continue_text": "잇다", "contract_warning": "이 계약 주소는 잠재적으로 사기성으로 표시되었습니다. 주의해서 처리하십시오.", @@ -216,6 +219,7 @@ "disable_fee_api_warning": "이것을 끄면 경우에 따라 수수료가 부정확 할 수 있으므로 거래 수수료를 초과 지불하거나 지불 할 수 있습니다.", "disable_fiat": "법정화폐 비활성화", "disable_sell": "판매 조치 비활성화", + "disable_trade_option": "거래 옵션 비활성화", "disableBatteryOptimization": "배터리 최적화를 비활성화합니다", "disableBatteryOptimizationDescription": "백그라운드 동기화를보다 자유롭고 매끄럽게 실행하기 위해 배터리 최적화를 비활성화하고 싶습니까?", "disabled": "장애가 있는", @@ -296,6 +300,7 @@ "expiry_and_validity": "만료와 타당성", "export_backup": "백업 내보내기", "export_logs": "내보내기 로그", + "export_outputs": "내보내기 출력", "extra_id": "추가 ID:", "extracted_address_content": "당신은에 자금을 보낼 것입니다\n${recipient_name}", "failed_authentication": "인증 실패. ${state_error}", @@ -336,7 +341,9 @@ "haven_app": "Haven by Cake Wallet", "haven_app_wallet_text": "Awesome wallet for Haven", "help": "돕다", + "hidden_addresses": "숨겨진 주소", "hidden_balance": "숨겨진 균형", + "hide": "숨다", "hide_details": "세부 정보 숨기기", "high_contrast_theme": "고대비 테마", "home_screen_settings": "홈 화면 설정", @@ -501,6 +508,7 @@ "pre_seed_title": "중대한", "prepaid_cards": "선불 카드", "prevent_screenshots": "스크린샷 및 화면 녹화 방지", + "primary_address": "기본 주소", "privacy": "프라이버시", "privacy_policy": "개인 정보 보호 정책", "privacy_settings": "개인정보 설정", @@ -697,6 +705,7 @@ "share": "공유하다", "share_address": "주소 공유", "shared_seed_wallet_groups": "공유 종자 지갑 그룹", + "show": "보여주다", "show_details": "세부정보 표시", "show_keys": "시드 / 키 표시", "show_market_place": "마켓플레이스 표시", @@ -944,6 +953,5 @@ "you_will_get": "로 변환하다", "you_will_send": "다음에서 변환", "YY": "YY", - "yy": "YY", - "contact_name_exists": "해당 이름을 가진 연락처가 이미 존재합니다. 다른 이름을 선택하세요." -} + "yy": "YY" +} \ No newline at end of file diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index 10c13cfef..e6be67060 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -123,6 +123,8 @@ "change_rep_successful": "အောင်မြင်စွာကိုယ်စားလှယ်ပြောင်းလဲသွားတယ်", "change_wallet_alert_content": "လက်ရှိပိုက်ဆံအိတ်ကို ${wallet_name} သို့ ပြောင်းလိုပါသလား။", "change_wallet_alert_title": "လက်ရှိပိုက်ဆံအိတ်ကို ပြောင်းပါ။", + "choose_a_payment_method": "ငွေပေးချေမှုနည်းလမ်းကိုရွေးချယ်ပါ", + "choose_a_provider": "ပံ့ပိုးပေးရွေးချယ်ပါ", "choose_account": "အကောင့်ကို ရွေးပါ။", "choose_address": "\n\nလိပ်စာကို ရွေးပါ-", "choose_card_value": "ကဒ်တန်ဖိုးတစ်ခုရွေးပါ", @@ -136,7 +138,7 @@ "clearnet_link": "Clearnet လင့်ခ်", "close": "အနီးကပ်", "coin_control": "အကြွေစေ့ထိန်းချုပ်မှု (ချန်လှပ်ထားနိုင်သည်)", - "cold_or_recover_wallet": "အေးသောပိုက်ဆံအိတ်ထည့်ပါ သို့မဟုတ် စက္ကူပိုက်ဆံအိတ်ကို ပြန်ယူပါ။", + "cold_or_recover_wallet": "Cupcake သို့မဟုတ်အအေးပိုက်ဆံအိတ်မှဖတ်ရန်သာပိုက်ဆံအိတ်တစ်ခုထည့်ပါသို့မဟုတ်စက္ကူပိုက်ဆံအိတ်ကိုပြန်လည်ရယူပါ", "color_theme": "အရောင်အပြင်အဆင်", "commit_transaction_amount_fee": "ငွေလွှဲခြင်း\nပမာဏ- ${amount}\nအခကြေးငွေ- ${fee}", "confirm": "အတည်ပြုပါ။", @@ -161,6 +163,7 @@ "contact_list_contacts": "အဆက်အသွယ်များ", "contact_list_wallets": "ကျွန်ုပ်၏ ပိုက်ဆံအိတ်များ", "contact_name": "ဆက်သွယ်ရန်အမည်", + "contact_name_exists": "ထိုအမည်နှင့် အဆက်အသွယ်တစ်ခု ရှိနှင့်ပြီးဖြစ်သည်။ အခြားအမည်တစ်ခုကို ရွေးပါ။", "contact_support": "ပံ့ပိုးကူညီမှုထံ ဆက်သွယ်ပါ။", "continue_text": "ဆက်လက်", "contract_warning": "ဒီစာချုပ်လိပ်စာအလားအလာအလားအလာအလားအလာအလံများကိုအလံလွှင့်တင်ခဲ့သည်။ ကျေးဇူးပြုပြီးသတိဖြင့်လုပ်ငန်းစဉ်။", @@ -216,6 +219,7 @@ "disable_fee_api_warning": "ဤအရာကိုဖွင့်ခြင်းအားဖြင့်အချို့သောကိစ္စရပ်များတွင်အခကြေးငွေနှုန်းထားများသည်တိကျမှုရှိနိုင်သည်,", "disable_fiat": "Fiat ကိုပိတ်ပါ။", "disable_sell": "ရောင်းချခြင်းလုပ်ဆောင်ချက်ကို ပိတ်ပါ။", + "disable_trade_option": "ကုန်သွယ်ရေး option ကိုပိတ်ပါ", "disableBatteryOptimization": "ဘက်ထရီ optimization ကိုပိတ်ပါ", "disableBatteryOptimizationDescription": "နောက်ခံထပ်တူပြုခြင်းနှင့်ချောချောမွေ့မွေ့ပြုလုပ်နိုင်ရန်ဘက်ထရီ optimization ကိုသင်ပိတ်ထားလိုပါသလား။", "disabled": "မသန်စွမ်း", @@ -296,6 +300,7 @@ "expiry_and_validity": "သက်တမ်းကုန်ဆုံးခြင်းနှင့်တရားဝင်မှု", "export_backup": "အရန်ကူးထုတ်ရန်", "export_logs": "ပို့ကုန်မှတ်တမ်းများ", + "export_outputs": "ပို့ကုန်ထုတ်ကုန်များ", "extra_id": "အပို ID-", "extracted_address_content": "သင်သည် \n${recipient_name} သို့ ရန်ပုံငွေများ ပေးပို့ပါမည်", "failed_authentication": "အထောက်အထားစိစစ်ခြင်း မအောင်မြင်ပါ။. ${state_error}", @@ -336,7 +341,9 @@ "haven_app": "ဟေးဗင် ကိတ် ဝေါလက်", "haven_app_wallet_text": "ဟေဗင်အတွက် အံ့ဩစရာကောင်းတဲ့ ပိုက်ဆံအုံး", "help": "ကူညီပါ", + "hidden_addresses": "လျှို့ဝှက်လိပ်စာများ", "hidden_balance": "Hidden Balance", + "hide": "သားရေ", "hide_details": "အသေးစိတ်ကို ဝှက်ပါ။", "high_contrast_theme": "အလင်းအမှောင် မြင့်မားသော အပြင်အဆင်", "home_screen_settings": "ပင်မစခရင် ဆက်တင်များ", @@ -500,6 +507,7 @@ "pre_seed_title": "အရေးကြီးသည်။", "prepaid_cards": "ကြိုတင်ငွေဖြည့်ကဒ်များ", "prevent_screenshots": "ဖန်သားပြင်ဓာတ်ပုံများနှင့် မျက်နှာပြင်ရိုက်ကူးခြင်းကို တားဆီးပါ။", + "primary_address": "အဓိကလိပ်စာ", "privacy": "ကိုယ်ရေးကိုယ်တာ", "privacy_policy": "ကိုယ်ရေးအချက်အလက်မူဝါဒ", "privacy_settings": "Privacy settings တွေကို", @@ -696,6 +704,7 @@ "share": "မျှဝေပါ။", "share_address": "လိပ်စာမျှဝေပါ။", "shared_seed_wallet_groups": "shared မျိုးစေ့ပိုက်ဆံအိတ်အုပ်စုများ", + "show": "ပြသ", "show_details": "အသေးစိတ်ပြ", "show_keys": "မျိုးစေ့ /သော့များကို ပြပါ။", "show_market_place": "စျေးကွက်ကိုပြသပါ။", @@ -942,6 +951,5 @@ "you_pay": "သင်ပေးချေပါ။", "you_will_get": "သို့ပြောင်းပါ။", "you_will_send": "မှပြောင်းပါ။", - "yy": "YY", - "contact_name_exists": "ထိုအမည်နှင့် အဆက်အသွယ်တစ်ခု ရှိနှင့်ပြီးဖြစ်သည်။ အခြားအမည်တစ်ခုကို ရွေးပါ။" -} + "yy": "YY" +} \ No newline at end of file diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 9552f2439..82c3899d4 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -123,6 +123,8 @@ "change_rep_successful": "Met succes veranderde vertegenwoordiger", "change_wallet_alert_content": "Wilt u de huidige portemonnee wijzigen in ${wallet_name}?", "change_wallet_alert_title": "Wijzig huidige portemonnee", + "choose_a_payment_method": "Kies een betaalmethode", + "choose_a_provider": "Kies een provider", "choose_account": "Kies account", "choose_address": "\n\nKies het adres:", "choose_card_value": "Kies een kaartwaarde", @@ -136,7 +138,7 @@ "clearnet_link": "Clearnet-link", "close": "Dichtbij", "coin_control": "Muntcontrole (optioneel)", - "cold_or_recover_wallet": "Voeg een cold wallet toe of herstel een paper wallet", + "cold_or_recover_wallet": "Voeg een alleen-lezen portemonnee toe van Cupcake of een koude portemonnee of herstel een papieren portemonnee", "color_theme": "Kleur thema", "commit_transaction_amount_fee": "Verricht transactie\nBedrag: ${amount}\nhonorarium: ${fee}", "confirm": "Bevestigen", @@ -161,6 +163,7 @@ "contact_list_contacts": "Contacten", "contact_list_wallets": "Mijn portefeuilles", "contact_name": "Contactnaam", + "contact_name_exists": "Er bestaat al een contact met die naam. Kies een andere naam.", "contact_support": "Contact opnemen met ondersteuning", "continue_text": "Doorgaan met", "contract_warning": "Dit contractadres is gemarkeerd als mogelijk frauduleus. Verwerk met voorzichtigheid.", @@ -216,6 +219,7 @@ "disable_fee_api_warning": "Door dit uit te schakelen, kunnen de tarieven in sommige gevallen onnauwkeurig zijn, dus u kunt de vergoedingen voor uw transacties te veel betalen of te weinig betalen", "disable_fiat": "Schakel Fiat uit", "disable_sell": "Verkoopactie uitschakelen", + "disable_trade_option": "Schakel handelsoptie uit", "disableBatteryOptimization": "Schakel de batterijoptimalisatie uit", "disableBatteryOptimizationDescription": "Wilt u de optimalisatie van de batterij uitschakelen om achtergrondsynchronisatie te laten werken, vrijer en soepeler?", "disabled": "Gehandicapt", @@ -296,6 +300,7 @@ "expiry_and_validity": "Vervallen en geldigheid", "export_backup": "Back-up exporteren", "export_logs": "Exporteer logboeken", + "export_outputs": "Exportuitgangen exporteren", "extra_id": "Extra ID:", "extracted_address_content": "U stuurt geld naar\n${recipient_name}", "failed_authentication": "Mislukte authenticatie. ${state_error}", @@ -336,7 +341,9 @@ "haven_app": "Haven by Cake Wallet", "haven_app_wallet_text": "Awesome wallet for Haven", "help": "helpen", + "hidden_addresses": "Verborgen adressen", "hidden_balance": "Verborgen balans", + "hide": "Verbergen", "hide_details": "Details verbergen", "high_contrast_theme": "Thema met hoog contrast", "home_screen_settings": "Instellingen voor het startscherm", @@ -500,6 +507,7 @@ "pre_seed_title": "BELANGRIJK", "prepaid_cards": "Prepaid-kaarten", "prevent_screenshots": "Voorkom screenshots en schermopname", + "primary_address": "Primair adres", "privacy": "Privacy", "privacy_policy": "Privacybeleid", "privacy_settings": "Privacy-instellingen", @@ -696,6 +704,7 @@ "share": "Deel", "share_address": "Deel adres", "shared_seed_wallet_groups": "Gedeelde zaadportelgroepen", + "show": "Show", "show_details": "Toon details", "show_keys": "Toon zaad/sleutels", "show_market_place": "Toon Marktplaats", @@ -943,6 +952,5 @@ "you_pay": "U betaalt", "you_will_get": "Converteren naar", "you_will_send": "Converteren van", - "yy": "JJ", - "contact_name_exists": "Er bestaat al een contact met die naam. Kies een andere naam." -} + "yy": "JJ" +} \ No newline at end of file diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index f3c3e4810..ed54624bf 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -123,6 +123,8 @@ "change_rep_successful": "Pomyślnie zmienił przedstawiciela", "change_wallet_alert_content": "Czy chcesz zmienić obecny portfel na ${wallet_name}?", "change_wallet_alert_title": "Zmień obecny portfel", + "choose_a_payment_method": "Wybierz metodę płatności", + "choose_a_provider": "Wybierz dostawcę", "choose_account": "Wybierz konto", "choose_address": "\n\nWybierz adres:", "choose_card_value": "Wybierz wartość karty", @@ -136,7 +138,7 @@ "clearnet_link": "łącze Clearnet", "close": "Zamknąć", "coin_control": "Kontrola monet (opcjonalnie)", - "cold_or_recover_wallet": "Dodaj zimny portfel lub odzyskaj portfel papierowy", + "cold_or_recover_wallet": "Dodaj portfel tylko do odczytu od Cupcake lub zimnego portfela lub odzyskaj papierowy portfel", "color_theme": "Motyw kolorystyczny", "commit_transaction_amount_fee": "Zatwierdź transakcję\nIlość: ${amount}\nOpłata: ${fee}", "confirm": "Potwierdzać", @@ -161,6 +163,7 @@ "contact_list_contacts": "Łączność", "contact_list_wallets": "Moje portfele", "contact_name": "Nazwa Kontaktu", + "contact_name_exists": "Kontakt o tej nazwie już istnieje. Proszę wybrać inną nazwę.", "contact_support": "Skontaktuj się z pomocą techniczną", "continue_text": "Dalej", "contract_warning": "Ten adres umowy został oznaczony jako potencjalnie nieuczciwy. Prosimy o ostrożność.", @@ -216,6 +219,7 @@ "disable_fee_api_warning": "Wyłączając to, stawki opłaty mogą być w niektórych przypadkach niedokładne, więc możesz skończyć się przepłaceniem lub wynagrodzeniem opłat za transakcje", "disable_fiat": "Wyłącz waluty FIAT", "disable_sell": "Wyłącz akcję sprzedaży", + "disable_trade_option": "Wyłącz opcję handlu", "disableBatteryOptimization": "Wyłącz optymalizację baterii", "disableBatteryOptimizationDescription": "Czy chcesz wyłączyć optymalizację baterii, aby synchronizacja tła działała swobodniej i płynnie?", "disabled": "Wyłączone", @@ -296,6 +300,7 @@ "expiry_and_validity": "Wygaśnięcie i ważność", "export_backup": "Eksportuj kopię zapasową", "export_logs": "Dzienniki eksportu", + "export_outputs": "Wyjścia eksportowe", "extra_id": "Dodatkowy ID:", "extracted_address_content": "Wysyłasz środki na\n${recipient_name}", "failed_authentication": "Nieudane uwierzytelnienie. ${state_error}", @@ -336,7 +341,9 @@ "haven_app": "Haven by Cake Wallet", "haven_app_wallet_text": "Awesome wallet for Haven", "help": "pomoc", + "hidden_addresses": "Ukryte adresy", "hidden_balance": "Ukryte saldo", + "hide": "Ukrywać", "hide_details": "Ukryj szczegóły", "high_contrast_theme": "Motyw o wysokim kontraście", "home_screen_settings": "Ustawienia ekranu głównego", @@ -500,6 +507,7 @@ "pre_seed_title": "WAŻNY", "prepaid_cards": "Karty przedpłacone", "prevent_screenshots": "Zapobiegaj zrzutom ekranu i nagrywaniu ekranu", + "primary_address": "Adres podstawowy", "privacy": "Prywatność", "privacy_policy": "Polityka prywatności", "privacy_settings": "Ustawienia prywatności", @@ -696,6 +704,7 @@ "share": "Udział", "share_address": "Udostępnij adres", "shared_seed_wallet_groups": "Wspólne grupy portfeli nasion", + "show": "Pokazywać", "show_details": "Pokaż szczegóły", "show_keys": "Pokaż seed/klucze", "show_market_place": "Pokaż rynek", @@ -942,6 +951,5 @@ "you_pay": "Płacisz", "you_will_get": "Konwertuj na", "you_will_send": "Konwertuj z", - "yy": "RR", - "contact_name_exists": "Kontakt o tej nazwie już istnieje. Proszę wybrać inną nazwę." -} + "yy": "RR" +} \ No newline at end of file diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index 410e8cb1c..7b43b5b12 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -123,6 +123,8 @@ "change_rep_successful": "Mudou com sucesso o representante", "change_wallet_alert_content": "Quer mudar a carteira atual para ${wallet_name}?", "change_wallet_alert_title": "Alterar carteira atual", + "choose_a_payment_method": "Escolha um método de pagamento", + "choose_a_provider": "Escolha um provedor", "choose_account": "Escolha uma conta", "choose_address": "\n\nEscolha o endereço:", "choose_card_value": "Escolha um valor de cartão", @@ -136,7 +138,7 @@ "clearnet_link": "link clear net", "close": "Fechar", "coin_control": "Controle de moedas (opcional)", - "cold_or_recover_wallet": "Adicione uma cold wallet ou recupere uma paper wallet", + "cold_or_recover_wallet": "Adicione uma carteira somente leitura de Cupcake ou uma carteira fria ou recupere uma carteira de papel", "color_theme": "Tema de cor", "commit_transaction_amount_fee": "Confirmar transação\nQuantia: ${amount}\nTaxa: ${fee}", "confirm": "Confirmar", @@ -161,6 +163,7 @@ "contact_list_contacts": "Contatos", "contact_list_wallets": "minhas carteiras", "contact_name": "Nome do contato", + "contact_name_exists": "Um contato com esse nome já existe. Escolha um nome diferente.", "contact_support": "Contatar Suporte", "continue_text": "Continuar", "contract_warning": "Este endereço do contrato foi sinalizado como potencialmente fraudulento. Por favor, processe com cautela.", @@ -216,6 +219,7 @@ "disable_fee_api_warning": "Ao desativar isso, as taxas de taxas podem ser imprecisas em alguns casos, para que você possa acabar pagando demais ou pagando as taxas por suas transações", "disable_fiat": "Desativar fiat", "disable_sell": "Desativar ação de venda", + "disable_trade_option": "Desativar a opção comercial", "disableBatteryOptimization": "Desative a otimização da bateria", "disableBatteryOptimizationDescription": "Deseja desativar a otimização da bateria para fazer a sincronização de fundo funcionar de forma mais livre e suave?", "disabled": "Desabilitado", @@ -296,6 +300,7 @@ "expiry_and_validity": "Expiração e validade", "export_backup": "Backup de exportação", "export_logs": "Exportar logs", + "export_outputs": "Saídas de exportação", "extra_id": "ID extra:", "extracted_address_content": "Você enviará fundos para\n${recipient_name}", "failed_authentication": "Falha na autenticação. ${state_error}", @@ -336,7 +341,9 @@ "haven_app": "Haven by Cake Wallet", "haven_app_wallet_text": "Awesome wallet for Haven", "help": "ajuda", + "hidden_addresses": "Endereços ocultos", "hidden_balance": "Saldo escondido", + "hide": "Esconder", "hide_details": "Ocultar detalhes", "high_contrast_theme": "Tema de alto contraste", "home_screen_settings": "Configurações da tela inicial", @@ -502,6 +509,7 @@ "pre_seed_title": "IMPORTANTE", "prepaid_cards": "Cartões pré-pagos", "prevent_screenshots": "Evite capturas de tela e gravação de tela", + "primary_address": "Endereço primário", "privacy": "Privacidade", "privacy_policy": "Política de privacidade", "privacy_settings": "Configurações de privacidade", @@ -698,6 +706,7 @@ "share": "Compartilhar", "share_address": "Compartilhar endereço", "shared_seed_wallet_groups": "Grupos de carteira de sementes compartilhados", + "show": "Mostrar", "show_details": "Mostrar detalhes", "show_keys": "Mostrar semente/chaves", "show_market_place": "Mostrar mercado", @@ -946,4 +955,4 @@ "you_will_get": "Converter para", "you_will_send": "Converter de", "yy": "aa" -} +} \ No newline at end of file diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index a8ee5a309..2795e1ffc 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -123,6 +123,8 @@ "change_rep_successful": "Успешно изменил представитель", "change_wallet_alert_content": "Вы хотите изменить текущий кошелек на ${wallet_name}?", "change_wallet_alert_title": "Изменить текущий кошелек", + "choose_a_payment_method": "Выберите способ оплаты", + "choose_a_provider": "Выберите поставщика", "choose_account": "Выберите аккаунт", "choose_address": "\n\nПожалуйста, выберите адрес:", "choose_card_value": "Выберите значение карты", @@ -136,7 +138,7 @@ "clearnet_link": "Клирнет ссылка", "close": "Закрывать", "coin_control": "Контроль монет (необязательно)", - "cold_or_recover_wallet": "Добавьте холодный кошелек или восстановите бумажный кошелек", + "cold_or_recover_wallet": "Добавить кошелек только для чтения из Cupcake или холодный кошелек или восстановить бумажный кошелек", "color_theme": "Цветовая тема", "commit_transaction_amount_fee": "Подтвердить транзакцию \nСумма: ${amount}\nКомиссия: ${fee}", "confirm": "Подтвердить", @@ -161,6 +163,7 @@ "contact_list_contacts": "Контакты", "contact_list_wallets": "Мои кошельки", "contact_name": "Имя контакта", + "contact_name_exists": "Контакт с таким именем уже существует. Пожалуйста, выберите другое имя.", "contact_support": "Связаться со службой поддержки", "continue_text": "Продолжить", "contract_warning": "Этот адрес контракта был отмечен как потенциально мошеннический. Пожалуйста, обработайтесь с осторожностью.", @@ -216,6 +219,7 @@ "disable_fee_api_warning": "Выключив это, в некоторых случаях ставки платы могут быть неточными, так что вы можете в конечном итоге переплачивать или недоплачивать сборы за ваши транзакции", "disable_fiat": "Отключить фиат", "disable_sell": "Отключить действие продажи", + "disable_trade_option": "Отключить возможность торговли", "disableBatteryOptimization": "Отключить оптимизацию батареи", "disableBatteryOptimizationDescription": "Вы хотите отключить оптимизацию батареи, чтобы сделать фона синхронизации более свободно и плавно?", "disabled": "Отключено", @@ -296,6 +300,7 @@ "expiry_and_validity": "Истечение и достоверность", "export_backup": "Экспорт резервной копии", "export_logs": "Экспортные журналы", + "export_outputs": "Экспортные выходы", "extra_id": "Дополнительный ID:", "extracted_address_content": "Вы будете отправлять средства\n${recipient_name}", "failed_authentication": "Ошибка аутентификации. ${state_error}", @@ -336,7 +341,9 @@ "haven_app": "Haven by Cake Wallet", "haven_app_wallet_text": "Awesome wallet for Haven", "help": "помощь", + "hidden_addresses": "Скрытые адреса", "hidden_balance": "Скрытый баланс", + "hide": "Скрывать", "hide_details": "Скрыть детали", "high_contrast_theme": "Высококонтрастная тема", "home_screen_settings": "Настройки главного экрана", @@ -501,6 +508,7 @@ "pre_seed_title": "ВАЖНО", "prepaid_cards": "Предоплаченные карты", "prevent_screenshots": "Предотвратить скриншоты и запись экрана", + "primary_address": "Первичный адрес", "privacy": "Конфиденциальность", "privacy_policy": "Политика конфиденциальности", "privacy_settings": "Настройки конфиденциальности", @@ -697,6 +705,7 @@ "share": "Делиться", "share_address": "Поделиться адресом", "shared_seed_wallet_groups": "Общие группы кошелька семян", + "show": "Показывать", "show_details": "Показать детали", "show_keys": "Показать мнемоническую фразу/ключи", "show_market_place": "Показать торговую площадку", @@ -943,6 +952,5 @@ "you_pay": "Вы платите", "you_will_get": "Конвертировать в", "you_will_send": "Конвертировать из", - "yy": "ГГ", - "contact_name_exists": "Контакт с таким именем уже существует. Пожалуйста, выберите другое имя." -} + "yy": "ГГ" +} \ No newline at end of file diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index 5102e150c..596861646 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -123,6 +123,8 @@ "change_rep_successful": "เปลี่ยนตัวแทนสำเร็จ", "change_wallet_alert_content": "คุณต้องการเปลี่ยนกระเป๋าปัจจุบันเป็น ${wallet_name} หรือไม่?", "change_wallet_alert_title": "เปลี่ยนกระเป๋าปัจจุบัน", + "choose_a_payment_method": "เลือกวิธีการชำระเงิน", + "choose_a_provider": "เลือกผู้ให้บริการ", "choose_account": "เลือกบัญชี", "choose_address": "\n\nโปรดเลือกที่อยู่:", "choose_card_value": "เลือกค่าบัตร", @@ -136,7 +138,7 @@ "clearnet_link": "ลิงค์เคลียร์เน็ต", "close": "ปิด", "coin_control": "การควบคุมเหรียญ (ตัวเลือก)", - "cold_or_recover_wallet": "เพิ่มกระเป๋าเงินเย็นหรือกู้คืนกระเป๋าเงินกระดาษ", + "cold_or_recover_wallet": "เพิ่มกระเป๋าเงินแบบอ่านอย่างเดียวจาก Cupcake หรือกระเป๋าเงินเย็นหรือกู้คืนกระเป๋ากระดาษ", "color_theme": "ธีมสี", "commit_transaction_amount_fee": "ยืนยันธุรกรรม\nจำนวน: ${amount}\nค่าธรรมเนียม: ${fee}", "confirm": "ยืนยัน", @@ -161,6 +163,7 @@ "contact_list_contacts": "ติดต่อ", "contact_list_wallets": "กระเป๋าเงินของฉัน", "contact_name": "ชื่อผู้ติดต่อ", + "contact_name_exists": "มีผู้ติดต่อชื่อนั้นอยู่แล้ว โปรดเลือกชื่ออื่น", "contact_support": "ติดต่อฝ่ายสนับสนุน", "continue_text": "ดำเนินการต่อ", "contract_warning": "ที่อยู่สัญญานี้ได้รับการตั้งค่าสถานะว่าเป็นการฉ้อโกง กรุณาดำเนินการด้วยความระมัดระวัง", @@ -216,6 +219,7 @@ "disable_fee_api_warning": "โดยการปิดสิ่งนี้อัตราค่าธรรมเนียมอาจไม่ถูกต้องในบางกรณีดังนั้นคุณอาจจบลงด้วยการจ่ายเงินมากเกินไปหรือจ่ายค่าธรรมเนียมสำหรับการทำธุรกรรมของคุณมากเกินไป", "disable_fiat": "ปิดใช้งานสกุลเงินตรา", "disable_sell": "ปิดการใช้งานการขาย", + "disable_trade_option": "ปิดใช้งานตัวเลือกการค้า", "disableBatteryOptimization": "ปิดใช้งานการเพิ่มประสิทธิภาพแบตเตอรี่", "disableBatteryOptimizationDescription": "คุณต้องการปิดใช้งานการเพิ่มประสิทธิภาพแบตเตอรี่เพื่อให้การซิงค์พื้นหลังทำงานได้อย่างอิสระและราบรื่นมากขึ้นหรือไม่?", "disabled": "ปิดใช้งาน", @@ -296,6 +300,7 @@ "expiry_and_validity": "หมดอายุและถูกต้อง", "export_backup": "ส่งออกข้อมูลสำรอง", "export_logs": "บันทึกการส่งออก", + "export_outputs": "เอาต์พุตส่งออก", "extra_id": "ไอดีเพิ่มเติม:", "extracted_address_content": "คุณกำลังจะส่งเงินไปยัง\n${recipient_name}", "failed_authentication": "การยืนยันสิทธิ์ล้มเหลว ${state_error}", @@ -336,7 +341,9 @@ "haven_app": "Haven ของ Cake Wallet", "haven_app_wallet_text": "กระเป๋าสตางค์ที่สวยงามสำหรับ Haven", "help": "ช่วยเหลือ", + "hidden_addresses": "ที่อยู่ที่ซ่อนอยู่", "hidden_balance": "ยอดคงเหลือซ่อนอยู่", + "hide": "ซ่อน", "hide_details": "ซ่อนรายละเอียด", "high_contrast_theme": "ธีมความคมชัดสูง", "home_screen_settings": "การตั้งค่าหน้าจอหลัก", @@ -500,6 +507,7 @@ "pre_seed_title": "สำคัญ", "prepaid_cards": "บัตรเติมเงิน", "prevent_screenshots": "ป้องกันภาพหน้าจอและการบันทึกหน้าจอ", + "primary_address": "ที่อยู่ปฐมภูมิ", "privacy": "ความเป็นส่วนตัว", "privacy_policy": "นโยบายความเป็นส่วนตัว", "privacy_settings": "การตั้งค่าความเป็นส่วนตัว", @@ -696,6 +704,7 @@ "share": "แบ่งปัน", "share_address": "แชร์ที่อยู่", "shared_seed_wallet_groups": "กลุ่มกระเป๋าเงินที่ใช้ร่วมกัน", + "show": "แสดง", "show_details": "แสดงรายละเอียด", "show_keys": "แสดงซีด/คีย์", "show_market_place": "แสดงตลาดกลาง", @@ -942,6 +951,5 @@ "you_pay": "คุณจ่าย", "you_will_get": "แปลงเป็น", "you_will_send": "แปลงจาก", - "yy": "ปี", - "contact_name_exists": "มีผู้ติดต่อชื่อนั้นอยู่แล้ว โปรดเลือกชื่ออื่น" -} + "yy": "ปี" +} \ No newline at end of file diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index eb78c8d3c..f4678e00d 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -123,6 +123,8 @@ "change_rep_successful": "Matagumpay na nagbago ng representative", "change_wallet_alert_content": "Gusto mo bang palitan ang kasalukuyang wallet sa ${wallet_name}?", "change_wallet_alert_title": "Baguhin ang kasalukuyang wallet", + "choose_a_payment_method": "Pumili ng isang paraan ng pagbabayad", + "choose_a_provider": "Pumili ng isang provider", "choose_account": "Pumili ng account", "choose_address": "Mangyaring piliin ang address:", "choose_card_value": "Pumili ng isang halaga ng card", @@ -136,7 +138,7 @@ "clearnet_link": "Link ng Clearnet", "close": "Isara", "coin_control": "Coin control (opsyonal)", - "cold_or_recover_wallet": "Magdagdag ng isang cold wallet o mabawi ang isang paper wallet", + "cold_or_recover_wallet": "Magdagdag ng isang basahin lamang na pitaka mula sa Cupcake o isang malamig na pitaka o mabawi ang isang wallet ng papel", "color_theme": "Color theme", "commit_transaction_amount_fee": "Gumawa ng transaksyon\nHalaga: ${amount}\nFee: ${fee}", "confirm": "Kumpirmahin", @@ -161,6 +163,7 @@ "contact_list_contacts": "Mga Contact", "contact_list_wallets": "Mga Wallet Ko", "contact_name": "Pangalan ng Contact", + "contact_name_exists": "Ang isang pakikipag -ugnay sa pangalang iyon ay mayroon na. Mangyaring pumili ng ibang pangalan.", "contact_support": "Makipag-ugnay sa Suporta", "continue_text": "Magpatuloy", "contract_warning": "Ang address ng kontrata na ito ay na -flag bilang potensyal na mapanlinlang. Mangyaring iproseso nang may pag -iingat.", @@ -216,6 +219,7 @@ "disable_fee_api_warning": "Sa pamamagitan ng pag -off nito, ang mga rate ng bayad ay maaaring hindi tumpak sa ilang mga kaso, kaya maaari mong tapusin ang labis na bayad o pagsuporta sa mga bayarin para sa iyong mga transaksyon", "disable_fiat": "Huwag paganahin ang fiat", "disable_sell": "Huwag paganahin ang pagkilos ng pagbebenta", + "disable_trade_option": "Huwag paganahin ang pagpipilian sa kalakalan", "disableBatteryOptimization": "Huwag Paganahin ang Pag-optimize ng Baterya", "disableBatteryOptimizationDescription": "Nais mo bang huwag paganahin ang pag-optimize ng baterya upang gawing mas malaya at maayos ang background sync?", "disabled": "Hindi pinagana", @@ -296,6 +300,7 @@ "expiry_and_validity": "Pag-expire at Bisa", "export_backup": "I-export ang backup", "export_logs": "Mga log ng pag -export", + "export_outputs": "Mga output ng pag -export", "extra_id": "Dagdag na ID:", "extracted_address_content": "Magpapadala ka ng pondo sa\n${recipient_name}", "failed_authentication": "Nabigo ang pagpapatunay. ${state_error}", @@ -336,7 +341,9 @@ "haven_app": "Haven by Cake Wallet", "haven_app_wallet_text": "Kahanga-hangang wallet para sa Haven", "help": "Tulong", + "hidden_addresses": "Nakatagong mga address", "hidden_balance": "Nakatagong Balanse", + "hide": "Itago", "hide_details": "Itago ang mga detalye", "high_contrast_theme": "High Contrast Theme", "home_screen_settings": "Mga setting ng home screen", @@ -500,6 +507,7 @@ "pre_seed_title": "Mahalaga", "prepaid_cards": "Mga Prepaid Card", "prevent_screenshots": "Maiwasan ang mga screenshot at pag -record ng screen", + "primary_address": "Pangunahing address", "privacy": "Privacy", "privacy_policy": "Patakaran sa Pagkapribado", "privacy_settings": "Settings para sa pagsasa-pribado", @@ -696,6 +704,7 @@ "share": "Ibahagi", "share_address": "Ibahagi ang address", "shared_seed_wallet_groups": "Ibinahaging mga pangkat ng pitaka ng binhi", + "show": "Ipakita", "show_details": "Ipakita ang mga detalye", "show_keys": "Ipakita ang mga seed/key", "show_market_place": "Ipakita ang Marketplace", @@ -943,4 +952,4 @@ "you_will_get": "I-convert sa", "you_will_send": "I-convert mula sa", "yy": "YY" -} +} \ No newline at end of file diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index 0b4700397..d8b3ae3cf 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -123,6 +123,8 @@ "change_rep_successful": "Temsilciyi başarıyla değiştirdi", "change_wallet_alert_content": "Şimdiki cüzdanı ${wallet_name} cüzdanı ile değiştirmek istediğinden emin misin?", "change_wallet_alert_title": "Şimdiki cüzdanı değiştir", + "choose_a_payment_method": "Bir Ödeme Yöntemi Seçin", + "choose_a_provider": "Bir Sağlayıcı Seçin", "choose_account": "Hesabı seç", "choose_address": "\n\nLütfen adresi seçin:", "choose_card_value": "Bir kart değeri seçin", @@ -136,7 +138,7 @@ "clearnet_link": "Net bağlantı", "close": "Kapalı", "coin_control": "Koin kontrolü (isteğe bağlı)", - "cold_or_recover_wallet": "Soğuk bir cüzdan ekleyin veya bir kağıt cüzdanı kurtarın", + "cold_or_recover_wallet": "Cupcake veya soğuk bir cüzdandan salt okunur bir cüzdan ekleyin veya bir kağıt cüzdanı kurtar", "color_theme": "Renk teması", "commit_transaction_amount_fee": "Transferi gerçekleştir\nMiktar: ${amount}\nKomisyon: ${fee}", "confirm": "Onayla", @@ -161,6 +163,7 @@ "contact_list_contacts": "Rehberim", "contact_list_wallets": "Cüzdanlarım", "contact_name": "Kişi ismi", + "contact_name_exists": "Bu isimde bir kişi zaten mevcut. Lütfen farklı bir ad seçin.", "contact_support": "Destek ile İletişime Geç", "continue_text": "Devam et", "contract_warning": "Bu sözleşme adresi potansiyel olarak hileli olarak işaretlenmiştir. Lütfen dikkatle işleyin.", @@ -216,6 +219,7 @@ "disable_fee_api_warning": "Bunu kapatarak, ücret oranları bazı durumlarda yanlış olabilir, bu nedenle işlemleriniz için ücretleri fazla ödeyebilir veya az ödeyebilirsiniz.", "disable_fiat": "İtibari paraları devre dışı bırak", "disable_sell": "Satış işlemini devre dışı bırak", + "disable_trade_option": "Ticaret seçeneğini devre dışı bırakın", "disableBatteryOptimization": "Pil optimizasyonunu devre dışı bırakın", "disableBatteryOptimizationDescription": "Arka plan senkronizasyonunu daha özgür ve sorunsuz bir şekilde çalıştırmak için pil optimizasyonunu devre dışı bırakmak istiyor musunuz?", "disabled": "Devre dışı", @@ -296,6 +300,7 @@ "expiry_and_validity": "Sona erme ve geçerlilik", "export_backup": "Yedeği dışa aktar", "export_logs": "Dışa aktarma günlükleri", + "export_outputs": "İhracat çıktıları", "extra_id": "Ekstra ID:", "extracted_address_content": "Parayı buraya gönderceksin:\n${recipient_name}", "failed_authentication": "Doğrulama başarısız oldu. ${state_error}", @@ -336,7 +341,9 @@ "haven_app": "Cake Wallet tarafından Haven", "haven_app_wallet_text": "Haven için harika cüzdan ", "help": "yardım", + "hidden_addresses": "Gizli adresler", "hidden_balance": "Gizli Bakiye", + "hide": "Saklamak", "hide_details": "Detayları Gizle", "high_contrast_theme": "Yüksek Kontrastlı Tema", "home_screen_settings": "Ana ekran ayarları", @@ -500,6 +507,7 @@ "pre_seed_title": "UYARI", "prepaid_cards": "Ön ödemeli kartlar", "prevent_screenshots": "Ekran görüntülerini ve ekran kaydını önleyin", + "primary_address": "Birincil adres", "privacy": "Gizlilik", "privacy_policy": "Gizlilik Politikası", "privacy_settings": "Gizlilik ayarları", @@ -696,6 +704,7 @@ "share": "Paylaşmak", "share_address": "Adresi paylaş", "shared_seed_wallet_groups": "Paylaşılan tohum cüzdan grupları", + "show": "Göstermek", "show_details": "Detayları Göster", "show_keys": "Tohumları/anahtarları göster", "show_market_place": "Pazar Yerini Göster", @@ -942,6 +951,5 @@ "you_pay": "Şu kadar ödeyeceksin: ", "you_will_get": "Biçimine dönüştür:", "you_will_send": "Biçiminden dönüştür:", - "yy": "YY", - "contact_name_exists": "Bu isimde bir kişi zaten mevcut. Lütfen farklı bir ad seçin." -} + "yy": "YY" +} \ No newline at end of file diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index c9afde7be..f27300c18 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -123,6 +123,8 @@ "change_rep_successful": "Успішно змінив представник", "change_wallet_alert_content": "Ви хочете змінити поточний гаманець на ${wallet_name}?", "change_wallet_alert_title": "Змінити поточний гаманець", + "choose_a_payment_method": "Виберіть метод оплати", + "choose_a_provider": "Виберіть постачальника", "choose_account": "Оберіть акаунт", "choose_address": "\n\nБудь ласка, оберіть адресу:", "choose_card_value": "Виберіть значення картки", @@ -136,7 +138,7 @@ "clearnet_link": "Посилання Clearnet", "close": "Закрити", "coin_control": "Контроль монет (необов’язково)", - "cold_or_recover_wallet": "Додайте холодний гаманець або відновіть паперовий гаманець", + "cold_or_recover_wallet": "Додайте гаманець лише для читання від Cupcake або холодного гаманця або відновіть паперовий гаманець", "color_theme": "Кольорова тема", "commit_transaction_amount_fee": "Підтвердити транзакцію \nСума: ${amount}\nКомісія: ${fee}", "confirm": "Підтвердити", @@ -161,6 +163,7 @@ "contact_list_contacts": "Контакти", "contact_list_wallets": "Мої гаманці", "contact_name": "Ім'я контакту", + "contact_name_exists": "Контакт із такою назвою вже існує. Виберіть інше ім'я.", "contact_support": "Звернутися до служби підтримки", "continue_text": "Продовжити", "contract_warning": "Ця адреса контракту була позначена як потенційно шахрайська. Будь ласка, обробляйте обережно.", @@ -184,7 +187,7 @@ "creating_new_wallet_error": "Помилка: ${description}", "creation_date": "Дата створення", "custom": "на замовлення", - "custom_drag": "На замовлення (утримуйте та перетягується)", + "custom_drag": "На замовлення (утримуйте та перетягуйте)", "custom_redeem_amount": "Власна сума викупу", "custom_value": "Спеціальне значення", "dark_theme": "Темна", @@ -205,17 +208,18 @@ "description": "опис", "destination_tag": "Тег призначення:", "dfx_option_description": "Купуйте криптовалюту з EUR & CHF. Для роздрібних та корпоративних клієнтів у Європі", - "didnt_get_code": "Не отримуєте код?", + "didnt_get_code": "Не отримали код?", "digit_pin": "-значний PIN", "digital_and_physical_card": " цифрова та фізична передплачена дебетова картка", "disable": "Вимкнути", "disable_bulletin": "Вимкнути статус послуги", "disable_buy": "Вимкнути дію покупки", "disable_cake_2fa": "Вимкнути Cake 2FA", - "disable_exchange": "Вимкнути exchange", + "disable_exchange": "Вимкнути можливість обміну", "disable_fee_api_warning": "Вимкнувши це, ставки плати в деяких випадках можуть бути неточними, тому ви можете переплатити або недооплатити плату за свої транзакції", "disable_fiat": "Вимкнути фиат", "disable_sell": "Вимкнути дію продажу", + "disable_trade_option": "Вимкнути можливість торгівлі", "disableBatteryOptimization": "Вимкнути оптимізацію акумулятора", "disableBatteryOptimizationDescription": "Ви хочете відключити оптимізацію акумулятора, щоб зробити фонову синхронізацію більш вільно та плавно?", "disabled": "Вимкнено", @@ -225,14 +229,14 @@ "do_not_have_enough_gas_asset": "У вас недостатньо ${currency}, щоб здійснити трансакцію з поточними умовами мережі блокчейн. Вам потрібно більше ${currency}, щоб сплатити комісію мережі блокчейн, навіть якщо ви надсилаєте інший актив.", "do_not_send": "Не надсилайте", "do_not_share_warning_text": "Не діліться цим нікому, включно зі службою підтримки.\n\nВаші кошти можуть і будуть вкрадені!", - "do_not_show_me": "Не показуй мені це знову", + "do_not_show_me": "Не показувати це знову", "domain_looks_up": "Пошук доменів", "donation_link_details": "Деталі посилання для пожертв", "e_sign_consent": "Згода електронного підпису", "edit": "Редагувати", "edit_backup_password": "Змінити пароль резервної копії", "edit_node": "Редагувати вузол", - "edit_token": "Редагувати маркер", + "edit_token": "Редагувати токен", "electrum_address_disclaimer": "Ми створюємо нові адреси щоразу, коли ви використовуєте їх, але попередні адреси продовжують працювати", "email_address": "Адреса електронної пошти", "enable": "Ввімкнути", @@ -296,6 +300,7 @@ "expiry_and_validity": "Закінчення та обгрунтованість", "export_backup": "Експортувати резервну копію", "export_logs": "Експортні журнали", + "export_outputs": "Експортні результати", "extra_id": "Додатковий ID:", "extracted_address_content": "Ви будете відправляти кошти\n${recipient_name}", "failed_authentication": "Помилка аутентифікації. ${state_error}", @@ -336,7 +341,9 @@ "haven_app": "Haven by Cake Wallet", "haven_app_wallet_text": "Awesome wallet for Haven", "help": "допомога", + "hidden_addresses": "Приховані адреси", "hidden_balance": "Прихований баланс", + "hide": "Сховати", "hide_details": "Приховати деталі", "high_contrast_theme": "Тема високої контрастності", "home_screen_settings": "Налаштування головного екрана", @@ -500,6 +507,7 @@ "pre_seed_title": "ВАЖЛИВО", "prepaid_cards": "Передплачені картки", "prevent_screenshots": "Запобігати знімкам екрана та запису екрана", + "primary_address": "Первинна адреса", "privacy": "Конфіденційність", "privacy_policy": "Політика конфіденційності", "privacy_settings": "Налаштування конфіденційності", @@ -697,6 +705,7 @@ "share": "Поділіться", "share_address": "Поділитися адресою", "shared_seed_wallet_groups": "Спільні групи насіннєвих гаманців", + "show": "Показувати", "show_details": "Показати деталі", "show_keys": "Показати мнемонічну фразу/ключі", "show_market_place": "Відображати маркетплейс", @@ -943,6 +952,5 @@ "you_pay": "Ви платите", "you_will_get": "Конвертувати в", "you_will_send": "Конвертувати з", - "yy": "YY", - "contact_name_exists": "Контакт із такою назвою вже існує. Виберіть інше ім'я." -} + "yy": "YY" +} \ No newline at end of file diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index 50c6f1889..00f336d72 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -123,6 +123,8 @@ "change_rep_successful": "نمائندہ کو کامیابی کے ساتھ تبدیل کیا", "change_wallet_alert_content": "کیا آپ موجودہ والیٹ کو ${wallet_name} میں تبدیل کرنا چاہتے ہیں؟", "change_wallet_alert_title": "موجودہ پرس تبدیل کریں۔", + "choose_a_payment_method": "ادائیگی کا طریقہ منتخب کریں", + "choose_a_provider": "فراہم کنندہ کا انتخاب کریں", "choose_account": "اکاؤنٹ کا انتخاب کریں۔", "choose_address": "\\n\\nبراہ کرم پتہ منتخب کریں:", "choose_card_value": "کارڈ کی قیمت کا انتخاب کریں", @@ -136,7 +138,7 @@ "clearnet_link": "کلیرنیٹ لنک", "close": "بند کریں", "coin_control": "سکے کنٹرول (اختیاری)", - "cold_or_recover_wallet": "ٹھنڈا پرس ڈالیں یا کاغذ کا پرس بازیافت کریں", + "cold_or_recover_wallet": "Cupcake یا سرد بٹوے سے صرف ایک پڑھنے والا پرس شامل کریں یا کاغذ کا پرس بازیافت کریں", "color_theme": "رنگین تھیم", "commit_transaction_amount_fee": "لین دین کا ارتکاب کریں\\nرقم: ${amount}\\nفیس: ${fee}", "confirm": "تصدیق کریں۔", @@ -161,6 +163,7 @@ "contact_list_contacts": "رابطے", "contact_list_wallets": "میرے بٹوے", "contact_name": "رابطے کا نام", + "contact_name_exists": " ۔ﮟﯾﺮﮐ ﺐﺨﺘﻨﻣ ﻡﺎﻧ ﻒﻠﺘﺨﻣ ﮏﯾﺍ ﻡﺮﮐ ﮦﺍﺮﺑ ۔ﮯﮨ ﺩﻮﺟﻮﻣ ﮯﺳ ﮯﻠﮩﭘ ﮧﻄﺑﺍﺭ ﮏﯾﺍ ﮫﺗﺎﺳ ﮯﮐ ﻡﺎﻧ ﺱﺍ", "contact_support": "سپورٹ سے رابطہ کریں۔", "continue_text": "جاری رہے", "contract_warning": "اس معاہدے کے پتے کو ممکنہ طور پر جعلی قرار دیا گیا ہے۔ براہ کرم احتیاط کے ساتھ کارروائی کریں۔", @@ -216,6 +219,7 @@ "disable_fee_api_warning": "اس کو بند کرنے سے ، کچھ معاملات میں فیس کی شرح غلط ہوسکتی ہے ، لہذا آپ اپنے لین دین کے لئے فیسوں کو زیادہ ادائیگی یا ادائیگی ختم کرسکتے ہیں۔", "disable_fiat": "فیاٹ کو غیر فعال کریں۔", "disable_sell": "فروخت کی کارروائی کو غیر فعال کریں۔", + "disable_trade_option": "تجارت کے آپشن کو غیر فعال کریں", "disableBatteryOptimization": "بیٹری کی اصلاح کو غیر فعال کریں", "disableBatteryOptimizationDescription": "کیا آپ پس منظر کی مطابقت پذیری کو زیادہ آزادانہ اور آسانی سے چلانے کے لئے بیٹری کی اصلاح کو غیر فعال کرنا چاہتے ہیں؟", "disabled": "معذور", @@ -296,6 +300,7 @@ "expiry_and_validity": "میعاد ختم اور صداقت", "export_backup": "بیک اپ برآمد کریں۔", "export_logs": "نوشتہ جات برآمد کریں", + "export_outputs": "برآمد کے نتائج", "extra_id": "اضافی ID:", "extracted_address_content": "آپ فنڈز بھیج رہے ہوں گے\n${recipient_name}", "failed_authentication": "ناکام تصدیق۔ ${state_error}", @@ -336,7 +341,9 @@ "haven_app": "Haven از Cake والیٹ", "haven_app_wallet_text": "Havek کے لیے زبردست پرس", "help": "مدد", + "hidden_addresses": "پوشیدہ پتے", "hidden_balance": "پوشیدہ بیلنس", + "hide": "چھپائیں", "hide_details": "تفصیلات چھپائیں۔", "high_contrast_theme": "ہائی کنٹراسٹ تھیم", "home_screen_settings": "ہوم اسکرین کی ترتیبات", @@ -502,6 +509,7 @@ "pre_seed_title": "اہم", "prepaid_cards": "پری پیڈ کارڈز", "prevent_screenshots": "اسکرین شاٹس اور اسکرین ریکارڈنگ کو روکیں۔", + "primary_address": "بنیادی پتہ", "privacy": "رازداری", "privacy_policy": "رازداری کی پالیسی", "privacy_settings": "رازداری کی ترتیبات", @@ -698,6 +706,7 @@ "share": "بانٹیں", "share_address": "پتہ شیئر کریں۔", "shared_seed_wallet_groups": "مشترکہ بیج پرس گروپ", + "show": "دکھائیں", "show_details": "تفصیلات دکھائیں", "show_keys": "بیج / چابیاں دکھائیں۔", "show_market_place": "بازار دکھائیں۔", @@ -944,6 +953,5 @@ "you_pay": "تم ادا کرو", "you_will_get": "میں تبدیل کریں۔", "you_will_send": "سے تبدیل کریں۔", - "yy": "YY", - "contact_name_exists": " ۔ﮟﯾﺮﮐ ﺐﺨﺘﻨﻣ ﻡﺎﻧ ﻒﻠﺘﺨﻣ ﮏﯾﺍ ﻡﺮﮐ ﮦﺍﺮﺑ ۔ﮯﮨ ﺩﻮﺟﻮﻣ ﮯﺳ ﮯﻠﮩﭘ ﮧﻄﺑﺍﺭ ﮏﯾﺍ ﮫﺗﺎﺳ ﮯﮐ ﻡﺎﻧ ﺱﺍ" -} + "yy": "YY" +} \ No newline at end of file diff --git a/res/values/strings_vi.arb b/res/values/strings_vi.arb index e3ffbef3e..1291b505e 100644 --- a/res/values/strings_vi.arb +++ b/res/values/strings_vi.arb @@ -136,7 +136,7 @@ "clearnet_link": "Liên kết Clearnet", "close": "Đóng", "coin_control": "Kiểm soát đồng xu (tùy chọn)", - "cold_or_recover_wallet": "Thêm ví lạnh hoặc khôi phục ví giấy", + "cold_or_recover_wallet": "Thêm ví chỉ đọc từ Cupcake hoặc ví lạnh hoặc thu hồi ví giấy", "color_theme": "Chủ đề màu sắc", "commit_transaction_amount_fee": "Cam kết giao dịch\nSố tiền: ${amount}\nPhí: ${fee}", "confirm": "Xác nhận", @@ -162,6 +162,7 @@ "contact_list_contacts": "Danh bạ", "contact_list_wallets": "Ví của tôi", "contact_name": "Tên liên hệ", + "contact_name_exists": "Một liên hệ với cái tên đó đã tồn tại. Vui lòng chọn một tên khác.", "contact_support": "Liên hệ Hỗ trợ", "continue_text": "Tiếp tục", "contract_warning": "Địa chỉ hợp đồng này đã được gắn cờ là có khả năng lừa đảo. Vui lòng xử lý một cách thận trọng.", @@ -217,6 +218,7 @@ "disable_fee_api_warning": "Khi tắt chức năng này, tỉ lệ phí có thể không chính xác trong một số trường hợp, dẫn đến bạn trả quá hoặc không đủ phí cho giao dịch của mình.", "disable_fiat": "Vô hiệu hóa tiền tệ fiat", "disable_sell": "Vô hiệu hóa chức năng bán", + "disable_trade_option": "Tắt tùy chọn thương mại", "disableBatteryOptimization": "Vô hiệu hóa Tối ưu hóa Pin", "disableBatteryOptimizationDescription": "Bạn có muốn vô hiệu hóa tối ưu hóa pin để đồng bộ hóa nền hoạt động mượt mà hơn không?", "disabled": "Đã vô hiệu hóa", @@ -297,6 +299,7 @@ "expiry_and_validity": "Hạn và hiệu lực", "export_backup": "Xuất sao lưu", "export_logs": "Nhật ký xuất khẩu", + "export_outputs": "Đầu ra xuất khẩu", "extra_id": "ID bổ sung:", "extracted_address_content": "Bạn sẽ gửi tiền cho\n${recipient_name}", "failed_authentication": "Xác thực không thành công. ${state_error}", @@ -337,7 +340,9 @@ "haven_app": "Haven bởi Cake Wallet", "haven_app_wallet_text": "Ví tuyệt vời cho Haven", "help": "Trợ giúp", + "hidden_addresses": "Địa chỉ ẩn", "hidden_balance": "Số dư ẩn", + "hide": "Trốn", "hide_details": "Ẩn chi tiết", "high_contrast_theme": "Chủ đề độ tương phản cao", "home_screen_settings": "Cài đặt màn hình chính", @@ -367,14 +372,22 @@ "ledger_error_wrong_app": "Vui lòng đảm bảo bạn đã mở đúng ứng dụng trên Ledger của mình", "ledger_please_enable_bluetooth": "Vui lòng bật Bluetooth để phát hiện Ledger của bạn", "light_theme": "Chủ đề sáng", + "litecoin_enable_mweb_sync": "Bật quét MWEB", + "litecoin_mweb": "Mweb", + "litecoin_mweb_always_scan": "Đặt MWEB luôn quét", "litecoin_mweb_description": "MWEB là một giao thức mới mang lại các giao dịch nhanh hơn, rẻ hơn và riêng tư hơn cho Litecoin", "litecoin_mweb_dismiss": "Miễn nhiệm", + "litecoin_mweb_display_card": "Hiển thị thẻ MWEB", "litecoin_mweb_enable": "Bật MWEB", "litecoin_mweb_enable_later": "Bạn có thể chọn bật lại MWEB trong cài đặt hiển thị.", "litecoin_mweb_logs": "Nhật ký MWEB", "litecoin_mweb_node": "Nút MWEB", "litecoin_mweb_pegin": "Chốt vào", "litecoin_mweb_pegout": "Chốt ra", + "litecoin_mweb_scanning": "Quét MWEB", + "litecoin_mweb_settings": "Cài đặt MWEB", + "litecoin_mweb_warning": "Sử dụng MWEB ban đầu sẽ tải xuống ~ 600MB dữ liệu và có thể mất tới 30 phút tùy thuộc vào tốc độ mạng. Dữ liệu ban đầu này sẽ chỉ tải xuống một lần và có sẵn cho tất cả các ví Litecoin", + "litecoin_what_is_mweb": "MWEB là gì?", "live_fee_rates": "Tỷ lệ phí hiện tại qua API", "load_more": "Tải thêm", "loading_your_wallet": "Đang tải ví của bạn", @@ -493,6 +506,7 @@ "pre_seed_title": "QUAN TRỌNG", "prepaid_cards": "Thẻ trả trước", "prevent_screenshots": "Ngăn chặn ảnh chụp màn hình và ghi hình màn hình", + "primary_address": "Địa chỉ chính", "privacy": "Quyền riêng tư", "privacy_policy": "Chính sách quyền riêng tư", "privacy_settings": "Cài đặt quyền riêng tư", @@ -689,6 +703,7 @@ "share": "Chia sẻ", "share_address": "Chia sẻ địa chỉ", "shared_seed_wallet_groups": "Nhóm ví hạt được chia sẻ", + "show": "Trình diễn", "show_details": "Hiển thị chi tiết", "show_keys": "Hiển thị hạt giống/khóa", "show_market_place": "Hiển thị Thị trường", diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index 19dc9f110..c9275b018 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -123,6 +123,8 @@ "change_rep_successful": "Ni ifijišẹ yipada aṣoju", "change_wallet_alert_content": "Ṣe ẹ fẹ́ pààrọ̀ àpamọ́wọ́ yìí sí ${wallet_name}?", "change_wallet_alert_title": "Ẹ pààrọ̀ àpamọ́wọ́ yìí", + "choose_a_payment_method": "Yan ọna isanwo kan", + "choose_a_provider": "Yan olupese", "choose_account": "Yan àkáǹtì", "choose_address": "\n\nẸ jọ̀wọ́ yan àdírẹ́sì:", "choose_card_value": "Yan iye kaadi", @@ -136,7 +138,7 @@ "clearnet_link": "Kọja ilọ oke", "close": "sunmo", "coin_control": "Ìdarí owó ẹyọ (ìyàn nìyí)", - "cold_or_recover_wallet": "Fi owo aisan tabi yiyewo owo iwe iwe", + "cold_or_recover_wallet": "Ṣafikun apamọwọ kika-nikan lati Cupcake tabi apamọwọ tutu tabi gba owo apamọwọ iwe kan", "color_theme": "Àwọn ààtò àwọ̀", "commit_transaction_amount_fee": "Jẹ́rìí sí àránṣẹ́\nOwó: ${amount}\nIye àfikún: ${fee}", "confirm": "Jẹ́rìísí", @@ -161,6 +163,7 @@ "contact_list_contacts": "Àwọn olùbásọ̀rọ̀", "contact_list_wallets": "Àwọn àpamọ́wọ́ mi", "contact_name": "Orúkọ olùbásọ̀rọ̀", + "contact_name_exists": "Olubasọrọ pẹlu orukọ yẹn ti wa tẹlẹ. Jọwọ yan orukọ ti o yatọ.", "contact_support": "Bá ìranlọ́wọ́ sọ̀rọ̀", "continue_text": "Tẹ̀síwájú", "contract_warning": "Adirẹsi adehun adehun yii ti samisi bi arekereke. Jọwọ ṣe ilana pẹlu iṣọra.", @@ -216,6 +219,7 @@ "disable_fee_api_warning": "Nipa yiyi eyi kuro, awọn oṣuwọn owo naa le jẹ aiṣe deede ni awọn ọrọ kan, nitorinaa o le pari apọju tabi awọn idiyele ti o ni agbara fun awọn iṣowo rẹ", "disable_fiat": "Pa owó tí ìjọba pàṣẹ wa lò", "disable_sell": "Ko iṣọrọ iṣọrọ", + "disable_trade_option": "Mu aṣayan iṣowo ṣiṣẹ", "disableBatteryOptimization": "Mu Ifasi batiri", "disableBatteryOptimizationDescription": "Ṣe o fẹ lati mu iṣapelo batiri si lati le ṣiṣe ayẹwo ẹhin ati laisiyonu?", "disabled": "Wọ́n tí a ti pa", @@ -297,6 +301,7 @@ "expiry_and_validity": "Ipari ati idaniloju", "export_backup": "Sún ẹ̀dà nípamọ́ síta", "export_logs": "Wọle si okeere", + "export_outputs": "Agbohunlu okeere", "extra_id": "Àmì ìdánimọ̀ tó fikún:", "extracted_address_content": "Ẹ máa máa fi owó ránṣẹ́ sí\n${recipient_name}", "failed_authentication": "Ìfẹ̀rílàdí pipòfo. ${state_error}", @@ -337,7 +342,9 @@ "haven_app": "Haven latí ọwọ́ Cake Wallet", "haven_app_wallet_text": "Àpamọ́wọ́ Haven wà pa", "help": "ìranlọ́wọ́", + "hidden_addresses": "Awọn adirẹsi ti o farapamọ", "hidden_balance": "Ìyókù owó dídé", + "hide": "Pamọ", "hide_details": "Dé ìsọfúnni kékeré", "high_contrast_theme": "Akori Iyatọ giga", "home_screen_settings": "Awọn eto iboju ile", @@ -501,6 +508,7 @@ "pre_seed_title": "Ó TI ṢE PÀTÀKÌ", "prepaid_cards": "Awọn kaadi ti a ti sanwo", "prevent_screenshots": "Pese asapọ ti awọn ẹrọ eto aṣa", + "primary_address": "Adirẹsi akọkọ", "privacy": "Ìdáwà", "privacy_policy": "Òfin Aládàáni", "privacy_settings": "Ààtò àdáni", @@ -697,6 +705,7 @@ "share": "Pinpin", "share_address": "Pín àdírẹ́sì", "shared_seed_wallet_groups": "Awọn ẹgbẹ ti a pin irugbin", + "show": "Fihan", "show_details": "Fi ìsọfúnni kékeré hàn", "show_keys": "Wo hóró / àwọn kọ́kọ́rọ́", "show_market_place": "Wa Sopọ Pataki", @@ -943,6 +952,5 @@ "you_pay": "Ẹ sàn", "you_will_get": "Ṣe pàṣípààrọ̀ sí", "you_will_send": "Ṣe pàṣípààrọ̀ láti", - "yy": "Ọd", - "contact_name_exists": "Olubasọrọ pẹlu orukọ yẹn ti wa tẹlẹ. Jọwọ yan orukọ ti o yatọ." -} + "yy": "Ọd" +} \ No newline at end of file diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index 009a31d86..e508d3c2c 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -123,6 +123,8 @@ "change_rep_successful": "成功改变了代表", "change_wallet_alert_content": "您是否想将当前钱包改为 ${wallet_name}?", "change_wallet_alert_title": "更换当前钱包", + "choose_a_payment_method": "选择付款方式", + "choose_a_provider": "选择一个提供商", "choose_account": "选择账户", "choose_address": "\n\n請選擇地址:", "choose_card_value": "选择卡值", @@ -136,7 +138,7 @@ "clearnet_link": "明网链接", "close": "关闭", "coin_control": "硬幣控制(可選)", - "cold_or_recover_wallet": "添加冷钱包或恢复纸钱包", + "cold_or_recover_wallet": "从Cupcake或冷钱包中添加只读的钱包或恢复纸钱包", "color_theme": "主题", "commit_transaction_amount_fee": "提交交易\n金额: ${amount}\n手续费: ${fee}", "confirm": "确认", @@ -161,6 +163,7 @@ "contact_list_contacts": "联系人", "contact_list_wallets": "我的钱包", "contact_name": "联系人姓名", + "contact_name_exists": "已存在具有该名称的联系人。请选择不同的名称。", "contact_support": "联系支持", "continue_text": "继续", "contract_warning": "该合同地址已被标记为潜在的欺诈性。请谨慎处理。", @@ -216,6 +219,7 @@ "disable_fee_api_warning": "通过将其关闭,在某些情况下,收费率可能不准确,因此您最终可能会超额付款或支付交易费用", "disable_fiat": "禁用法令", "disable_sell": "禁用卖出操作", + "disable_trade_option": "禁用贸易选项", "disableBatteryOptimization": "禁用电池优化", "disableBatteryOptimizationDescription": "您是否要禁用电池优化以使背景同步更加自由,平稳地运行?", "disabled": "禁用", @@ -296,6 +300,7 @@ "expiry_and_validity": "到期和有效性", "export_backup": "导出备份", "export_logs": "导出日志", + "export_outputs": "导出输出", "extra_id": "额外ID:", "extracted_address_content": "您将汇款至\n${recipient_name}", "failed_authentication": "身份验证失败. ${state_error}", @@ -336,7 +341,9 @@ "haven_app": "Haven by Cake Wallet", "haven_app_wallet_text": "Awesome wallet for Haven", "help": "帮助", + "hidden_addresses": "隐藏的地址", "hidden_balance": "隐藏余额", + "hide": "隐藏", "hide_details": "隐藏细节", "high_contrast_theme": "高对比度主题", "home_screen_settings": "主屏幕设置", @@ -500,6 +507,7 @@ "pre_seed_title": "重要", "prepaid_cards": "预付费卡", "prevent_screenshots": "防止截屏和录屏", + "primary_address": "主要地址", "privacy": "隐私", "privacy_policy": "隐私政策", "privacy_settings": "隐私设置", @@ -696,6 +704,7 @@ "share": "分享", "share_address": "分享地址", "shared_seed_wallet_groups": "共享种子钱包组", + "show": "展示", "show_details": "显示详细信息", "show_keys": "显示种子/密钥", "show_market_place": "显示市场", @@ -942,6 +951,5 @@ "you_pay": "你付钱", "you_will_get": "转换到", "you_will_send": "转换自", - "yy": "YY", - "contact_name_exists": "已存在具有该名称的联系人。请选择不同的名称。" -} + "yy": "YY" +} \ No newline at end of file diff --git a/scripts/android/app_env.sh b/scripts/android/app_env.sh index 8beffbcc2..7eee6d6ae 100644 --- a/scripts/android/app_env.sh +++ b/scripts/android/app_env.sh @@ -15,15 +15,15 @@ TYPES=($MONERO_COM $CAKEWALLET $HAVEN) APP_ANDROID_TYPE=$1 MONERO_COM_NAME="Monero.com" -MONERO_COM_VERSION="1.17.0" -MONERO_COM_BUILD_NUMBER=103 +MONERO_COM_VERSION="1.18.0" +MONERO_COM_BUILD_NUMBER=105 MONERO_COM_BUNDLE_ID="com.monero.app" MONERO_COM_PACKAGE="com.monero.app" MONERO_COM_SCHEME="monero.com" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="4.20.1" -CAKEWALLET_BUILD_NUMBER=233 +CAKEWALLET_VERSION="4.21.0" +CAKEWALLET_BUILD_NUMBER=236 CAKEWALLET_BUNDLE_ID="com.cakewallet.cake_wallet" CAKEWALLET_PACKAGE="com.cakewallet.cake_wallet" CAKEWALLET_SCHEME="cakewallet" diff --git a/scripts/ios/app_env.sh b/scripts/ios/app_env.sh index bc3b39747..fe18758c8 100644 --- a/scripts/ios/app_env.sh +++ b/scripts/ios/app_env.sh @@ -13,13 +13,13 @@ TYPES=($MONERO_COM $CAKEWALLET $HAVEN) APP_IOS_TYPE=$1 MONERO_COM_NAME="Monero.com" -MONERO_COM_VERSION="1.17.0" -MONERO_COM_BUILD_NUMBER=101 +MONERO_COM_VERSION="1.18.0" +MONERO_COM_BUILD_NUMBER=103 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="4.20.1" -CAKEWALLET_BUILD_NUMBER=277 +CAKEWALLET_VERSION="4.21.0" +CAKEWALLET_BUILD_NUMBER=281 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" HAVEN_NAME="Haven" diff --git a/scripts/linux/app_env.sh b/scripts/linux/app_env.sh index 2dabf083b..1cdb43ced 100755 --- a/scripts/linux/app_env.sh +++ b/scripts/linux/app_env.sh @@ -14,8 +14,8 @@ if [ -n "$1" ]; then fi CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="1.10.1" -CAKEWALLET_BUILD_NUMBER=37 +CAKEWALLET_VERSION="1.11.0" +CAKEWALLET_BUILD_NUMBER=38 if ! [[ " ${TYPES[*]} " =~ " ${APP_LINUX_TYPE} " ]]; then echo "Wrong app type." diff --git a/scripts/macos/app_env.sh b/scripts/macos/app_env.sh index 026ea034b..8f3b68ab5 100755 --- a/scripts/macos/app_env.sh +++ b/scripts/macos/app_env.sh @@ -16,13 +16,13 @@ if [ -n "$1" ]; then fi MONERO_COM_NAME="Monero.com" -MONERO_COM_VERSION="1.7.0" -MONERO_COM_BUILD_NUMBER=34 +MONERO_COM_VERSION="1.8.0" +MONERO_COM_BUILD_NUMBER=36 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="1.13.1" -CAKEWALLET_BUILD_NUMBER=93 +CAKEWALLET_VERSION="1.14.0" +CAKEWALLET_BUILD_NUMBER=95 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" if ! [[ " ${TYPES[*]} " =~ " ${APP_MACOS_TYPE} " ]]; then diff --git a/scripts/macos/gen_common.sh b/scripts/macos/gen_common.sh index 87fd54b43..d75ac919e 100755 --- a/scripts/macos/gen_common.sh +++ b/scripts/macos/gen_common.sh @@ -9,7 +9,7 @@ gen_podspec() { DEFAULT_FILE_PATH="${CW_PLUGIN_DIR}/${DEFAULT_FILENAME}" rm -f $DEFAULT_FILE_PATH cp $BASE_FILE_PATH $DEFAULT_FILE_PATH - sed -i '' "s/#___VALID_ARCHS___#/${ARCH}/g" $DEFAULT_FILE_PATH + gsed -i "s/#___VALID_ARCHS___#/${ARCH}/g" $DEFAULT_FILE_PATH } gen_project() { @@ -17,7 +17,7 @@ gen_project() { CW_DIR="`pwd`/../../macos/Runner.xcodeproj" DEFAULT_FILENAME="project.pbxproj" DEFAULT_FILE_PATH="${CW_DIR}/${DEFAULT_FILENAME}" - sed -i '' "s/ARCHS =.*/ARCHS = \"${ARCH}\";/g" $DEFAULT_FILE_PATH + gsed -i "s/ARCHS =.*/ARCHS = \"${ARCH}\";/g" $DEFAULT_FILE_PATH } gen() { diff --git a/scripts/prepare_moneroc.sh b/scripts/prepare_moneroc.sh index 24f4d201c..3596bd18b 100755 --- a/scripts/prepare_moneroc.sh +++ b/scripts/prepare_moneroc.sh @@ -6,9 +6,9 @@ cd "$(dirname "$0")" if [[ ! -d "monero_c" ]]; then - git clone https://github.com/mrcyjanek/monero_c --branch rewrite-wip + git clone https://github.com/mrcyjanek/monero_c --branch master cd monero_c - git checkout 6eb571ea498ed7b854934785f00fabfd0dadf75b + git checkout d72c15f4339791a7bbdf17e9d827b7b56ca144e4 git reset --hard git submodule update --init --force --recursive ./apply_patches.sh monero diff --git a/scripts/windows/build_exe_installer.iss b/scripts/windows/build_exe_installer.iss index f65900cca..25f94cb1f 100644 --- a/scripts/windows/build_exe_installer.iss +++ b/scripts/windows/build_exe_installer.iss @@ -1,5 +1,5 @@ #define MyAppName "Cake Wallet" -#define MyAppVersion "0.1.1" +#define MyAppVersion "0.2.0" #define MyAppPublisher "Cake Labs LLC" #define MyAppURL "https://cakewallet.com/" #define MyAppExeName "CakeWallet.exe" diff --git a/tool/configure.dart b/tool/configure.dart index 97541c2fa..f0f79cfb1 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -270,19 +270,22 @@ import 'package:cw_core/output_info.dart'; import 'package:cake_wallet/view_model/send/output.dart'; import 'package:cw_core/wallet_service.dart'; import 'package:hive/hive.dart'; +import 'package:ledger_flutter_plus/ledger_flutter_plus.dart' as ledger; import 'package:polyseed/polyseed.dart';"""; const moneroCWHeaders = """ +import 'package:cw_core/account.dart' as monero_account; import 'package:cw_core/get_height_by_date.dart'; import 'package:cw_core/monero_amount_format.dart'; import 'package:cw_core/monero_transaction_priority.dart'; -import 'package:cw_monero/monero_unspent.dart'; -import 'package:cw_monero/monero_wallet_service.dart'; import 'package:cw_monero/api/wallet_manager.dart'; +import 'package:cw_monero/api/wallet.dart' as monero_wallet_api; +import 'package:cw_monero/ledger.dart'; +import 'package:cw_monero/monero_unspent.dart'; +import 'package:cw_monero/api/account_list.dart'; +import 'package:cw_monero/monero_wallet_service.dart'; import 'package:cw_monero/monero_wallet.dart'; import 'package:cw_monero/monero_transaction_info.dart'; import 'package:cw_monero/monero_transaction_creation_credentials.dart'; -import 'package:cw_core/account.dart' as monero_account; -import 'package:cw_monero/api/wallet.dart' as monero_wallet_api; import 'package:cw_monero/mnemonics/english.dart'; import 'package:cw_monero/mnemonics/chinese_simplified.dart'; import 'package:cw_monero/mnemonics/dutch.dart'; @@ -379,6 +382,12 @@ abstract class Monero { Future getCurrentHeight(); + Future commitTransactionUR(Object wallet, String ur); + + String exportOutputsUR(Object wallet, bool all); + + bool importKeyImagesUR(Object wallet, String ur); + WalletCredentials createMoneroRestoreWalletFromKeysCredentials({ required String name, required String spendKey, @@ -388,6 +397,7 @@ abstract class Monero { required String language, required int height}); WalletCredentials createMoneroRestoreWalletFromSeedCredentials({required String name, required String password, required int height, required String mnemonic}); + WalletCredentials createMoneroRestoreWalletFromHardwareCredentials({required String name, required String password, required int height, required ledger.LedgerConnection ledgerConnection}); WalletCredentials createMoneroNewWalletCredentials({required String name, required String language, required bool isPolyseed, String? password}); Map getKeys(Object wallet); int? getRestoreHeight(Object wallet); @@ -398,11 +408,15 @@ abstract class Monero { int formatterMoneroParseAmount({required String amount}); Account getCurrentAccount(Object wallet); void monerocCheck(); + bool isViewOnly(); void setCurrentAccount(Object wallet, int id, String label, String? balance); void onStartup(); int getTransactionInfoAccountId(TransactionInfo tx); WalletService createMoneroWalletService(Box walletInfoSource, Box unspentCoinSource); Map pendingTransactionInfo(Object transaction); + void setLedgerConnection(Object wallet, ledger.LedgerConnection connection); + void resetLedgerConnection(); + void setGlobalLedgerConnection(ledger.LedgerConnection connection); } abstract class MoneroSubaddressList { diff --git a/tool/utils/secret_key.dart b/tool/utils/secret_key.dart index 88cbbfce3..affe4017c 100644 --- a/tool/utils/secret_key.dart +++ b/tool/utils/secret_key.dart @@ -44,6 +44,8 @@ class SecretKey { SecretKey('cakePayApiKey', () => ''), SecretKey('CSRFToken', () => ''), SecretKey('authorization', () => ''), + SecretKey('meldTestApiKey', () => ''), + SecretKey('meldTestPublicKey', () => ''), SecretKey('moneroTestWalletSeeds', () => ''), SecretKey('moneroLegacyTestWalletSeeds ', () => ''), SecretKey('bitcoinTestWalletSeeds', () => ''),