diff --git a/.github/workflows/automated_integration_test.yml b/.github/workflows/automated_integration_test.yml index b299c9340..0869db8ea 100644 --- a/.github/workflows/automated_integration_test.yml +++ b/.github/workflows/automated_integration_test.yml @@ -190,7 +190,7 @@ jobs: echo "const cakePayApiKey = '${{ secrets.CAKE_PAY_API_KEY }}';" >> lib/.secrets.g.dart echo "const authorization = '${{ secrets.CAKE_PAY_AUTHORIZATION }}';" >> lib/.secrets.g.dart echo "const CSRFToken = '${{ secrets.CSRF_TOKEN }}';" >> lib/.secrets.g.dart - echo "const quantexExchangeMarkup = '${{ secrets.QUANTEX_EXCHANGE_MARKUP }}';" >> lib/.secrets.g.dart + echo "const swapTradeExchangeMarkup = '${{ secrets.SWAPTRADE_EXCHANGE_MARKUP }}';" >> lib/.secrets.g.dart echo "const nano2ApiKey = '${{ secrets.NANO2_API_KEY }}';" >> cw_nano/lib/.secrets.g.dart 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 diff --git a/.github/workflows/pr_test_build_android.yml b/.github/workflows/pr_test_build_android.yml index c5f875128..25fb144e3 100644 --- a/.github/workflows/pr_test_build_android.yml +++ b/.github/workflows/pr_test_build_android.yml @@ -1,6 +1,6 @@ name: Cake Wallet Android -on: [push] +on: [pull_request] defaults: run: @@ -34,10 +34,19 @@ jobs: - name: Fix github actions messing up $HOME... run: 'echo HOME=/root | sudo tee -a $GITHUB_ENV' - uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} - name: configure git run: | + git config --global --add safe.directory '*' git config --global user.email "ci@cakewallet.com" git config --global user.name "CakeWallet CI" + - name: Get the full commit message + run: | + FULL_MESSAGE="$(git log -1 --pretty=%B)" + echo "message<> $GITHUB_ENV + echo "$FULL_MESSAGE" >> $GITHUB_ENV + echo "EOF" >> $GITHUB_ENV - name: Add secrets run: | touch lib/.secrets.g.dart @@ -126,7 +135,7 @@ jobs: echo "const cakePayApiKey = '${{ secrets.CAKE_PAY_API_KEY }}';" >> lib/.secrets.g.dart echo "const authorization = '${{ secrets.CAKE_PAY_AUTHORIZATION }}';" >> lib/.secrets.g.dart echo "const CSRFToken = '${{ secrets.CSRF_TOKEN }}';" >> lib/.secrets.g.dart - echo "const quantexExchangeMarkup = '${{ secrets.QUANTEX_EXCHANGE_MARKUP }}';" >> lib/.secrets.g.dart + echo "const swapTradeExchangeMarkup = '${{ secrets.SWAPTRADE_EXCHANGE_MARKUP }}';" >> lib/.secrets.g.dart echo "const nano2ApiKey = '${{ secrets.NANO2_API_KEY }}';" >> cw_nano/lib/.secrets.g.dart 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 @@ -160,6 +169,10 @@ jobs: echo "const nanoTestWalletReceiveAddress = '${{ secrets.NANO_TEST_WALLET_RECEIVE_ADDRESS }}';" >> lib/.secrets.g.dart echo "const wowneroTestWalletReceiveAddress = '${{ secrets.WOWNERO_TEST_WALLET_RECEIVE_ADDRESS }}';" >> lib/.secrets.g.dart echo "const moneroTestWalletBlockHeight = '${{ secrets.MONERO_TEST_WALLET_BLOCK_HEIGHT }}';" >> lib/.secrets.g.dart + # end of test secrets + echo "const chainflipApiKey = '${{ secrets.CHAINFLIP_API_KEY }}';" >> lib/.secrets.g.dart + echo "const chainflipAffiliateFee = '${{ secrets.CHAINFLIP_AFFILIATE_FEE }}';" >> lib/.secrets.g.dart + - name: prepare monero_c and cache run: | export MONEROC_HASH=$(cat scripts/prepare_moneroc.sh | grep 'git checkout' | xargs | awk '{ print $3 }') @@ -259,25 +272,23 @@ jobs: mkdir test-apk cp app-arm64-v8a-release.apk test-apk/${BRANCH_NAME}.apk cp app-x86_64-release.apk test-apk/${BRANCH_NAME}_x86.apk - cd test-apk - cp ${BRANCH_NAME}.apk ${BRANCH_NAME}_slack.apk - name: Find APK file id: find_apk run: | set -x - apk_file=$(ls build/app/outputs/flutter-apk/test-apk/*_slack.apk || exit 1) + apk_file=$(ls build/app/outputs/flutter-apk/test-apk/${BRANCH_NAME}.apk || exit 1) echo "APK_FILE=$apk_file" >> $GITHUB_ENV - name: Upload artifact to slack - if: ${{ !contains(github.event.head_commit.message, 'skip slack') }} + if: ${{ !contains(env.message, 'skip slack') }} continue-on-error: true uses: adrey/slack-file-upload-action@1.0.5 with: token: ${{ secrets.SLACK_APP_TOKEN }} path: ${{ env.APK_FILE }} channel: ${{ secrets.SLACK_APK_CHANNEL }} - initial_comment: ${{ github.event.head_commit.message }} + initial_comment: ${{ env.message }} - name: cleanup run: rm -rf build/app/outputs/flutter-apk/test-apk/ diff --git a/.github/workflows/pr_test_build_linux.yml b/.github/workflows/pr_test_build_linux.yml index 345c95c4f..a9d8085b6 100644 --- a/.github/workflows/pr_test_build_linux.yml +++ b/.github/workflows/pr_test_build_linux.yml @@ -1,6 +1,6 @@ name: Cake Wallet Linux -on: [push] +on: [pull_request] defaults: run: @@ -30,10 +30,19 @@ jobs: - name: Fix github actions messing up $HOME... run: 'echo HOME=/root | sudo tee -a $GITHUB_ENV' - uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} - name: configure git run: | + git config --global --add safe.directory '*' git config --global user.email "ci@cakewallet.com" git config --global user.name "CakeWallet CI" + - name: Get the full commit message + run: | + FULL_MESSAGE="$(git log -1 --pretty=%B)" + echo "message<> $GITHUB_ENV + echo "$FULL_MESSAGE" >> $GITHUB_ENV + echo "EOF" >> $GITHUB_ENV - name: Add secrets run: | touch lib/.secrets.g.dart @@ -122,7 +131,7 @@ jobs: echo "const cakePayApiKey = '${{ secrets.CAKE_PAY_API_KEY }}';" >> lib/.secrets.g.dart echo "const authorization = '${{ secrets.CAKE_PAY_AUTHORIZATION }}';" >> lib/.secrets.g.dart echo "const CSRFToken = '${{ secrets.CSRF_TOKEN }}';" >> lib/.secrets.g.dart - echo "const quantexExchangeMarkup = '${{ secrets.QUANTEX_EXCHANGE_MARKUP }}';" >> lib/.secrets.g.dart + echo "const swapTradeExchangeMarkup = '${{ secrets.SWAPTRADE_EXCHANGE_MARKUP }}';" >> lib/.secrets.g.dart echo "const nano2ApiKey = '${{ secrets.NANO2_API_KEY }}';" >> cw_nano/lib/.secrets.g.dart 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 @@ -155,7 +164,11 @@ jobs: echo "const tronTestWalletReceiveAddress = '${{ secrets.TRON_TEST_WALLET_RECEIVE_ADDRESS }}';" >> lib/.secrets.g.dart echo "const nanoTestWalletReceiveAddress = '${{ secrets.NANO_TEST_WALLET_RECEIVE_ADDRESS }}';" >> lib/.secrets.g.dart echo "const wowneroTestWalletReceiveAddress = '${{ secrets.WOWNERO_TEST_WALLET_RECEIVE_ADDRESS }}';" >> lib/.secrets.g.dart - echo "const moneroTestWalletBlockHeight = '${{ secrets.MONERO_TEST_WALLET_BLOCK_HEIGHT }}';" >> lib/.secrets.g.dart + echo "const moneroTestWalletBlockHeight = '${{ secrets.MONERO_TEST_WALLET_BLOCK_HEIGHT }}';" >> lib/.secrets.g.dart + # end of test secrets + echo "const chainflipApiKey = '${{ secrets.CHAINFLIP_API_KEY }}';" >> lib/.secrets.g.dart + echo "const chainflipAffiliateFee = '${{ secrets.CHAINFLIP_AFFILIATE_FEE }}';" >> lib/.secrets.g.dart + - name: prepare monero_c and cache run: | export MONEROC_HASH=$(cat scripts/prepare_moneroc.sh | grep 'git checkout' | xargs | awk '{ print $3 }') @@ -227,7 +240,7 @@ jobs: name: cakewallet_linux - name: Prepare virtual desktop - if: ${{ contains(github.event.head_commit.message, 'run tests') }} + if: ${{ contains(env.message, 'run tests') }} run: | nohup Xvfb :99 -screen 0 720x1280x16 & echo DISPLAY=:99 | sudo tee -a $GITHUB_ENV @@ -243,28 +256,28 @@ jobs: # isn't much in those wallets anyway, we still wouldn't like to leak it to anyone who is able to access github. - name: Test [confirm_seeds_flow_test] - if: ${{ contains(github.event.head_commit.message, 'run tests') }} + if: ${{ contains(env.message, 'run tests') }} timeout-minutes: 20 run: | xmessage -timeout 30 "confirm_seeds_flow_test" & rm -rf ~/.local/share/com.example.cake_wallet/ ~/Documents/cake_wallet/ ~/cake_wallet exec timeout --signal=SIGKILL 900 flutter drive --driver=test_driver/integration_test.dart --target=integration_test/test_suites/confirm_seeds_flow_test.dart - name: Test [create_wallet_flow_test] - if: ${{ contains(github.event.head_commit.message, 'run tests') }} + if: ${{ contains(env.message, 'run tests') }} timeout-minutes: 20 run: | xmessage -timeout 30 "create_wallet_flow_test" & rm -rf ~/.local/share/com.example.cake_wallet/ ~/Documents/cake_wallet/ ~/cake_wallet exec timeout --signal=SIGKILL 900 flutter drive --driver=test_driver/integration_test.dart --target=integration_test/test_suites/create_wallet_flow_test.dart - name: Test [exchange_flow_test] - if: ${{ contains(github.event.head_commit.message, 'run tests') }} + if: ${{ contains(env.message, 'run tests') }} timeout-minutes: 20 run: | xmessage -timeout 30 "exchange_flow_test" & rm -rf ~/.local/share/com.example.cake_wallet/ ~/Documents/cake_wallet/ ~/cake_wallet exec timeout --signal=SIGKILL 900 flutter drive --driver=test_driver/integration_test.dart --target=integration_test/test_suites/exchange_flow_test.dart - name: Test [restore_wallet_through_seeds_flow_test] - if: ${{ contains(github.event.head_commit.message, 'run tests') }} + if: ${{ contains(env.message, 'run tests') }} timeout-minutes: 20 run: | xmessage -timeout 30 "restore_wallet_through_seeds_flow_test" & diff --git a/.gitignore b/.gitignore index 970241189..c431a7f60 100644 --- a/.gitignore +++ b/.gitignore @@ -126,7 +126,7 @@ cw_shared_external/ios/External/ cw_haven/ios/External/ cw_haven/android/.externalNativeBuild/ cw_haven/android/.cxx/ - +cw_zano/ios/External/ lib/bitcoin/bitcoin.dart lib/monero/monero.dart lib/haven/haven.dart @@ -137,6 +137,7 @@ lib/polygon/polygon.dart lib/solana/solana.dart lib/tron/tron.dart lib/wownero/wownero.dart +lib/zano/zano.dart ios/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_180.png ios/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_120.png @@ -179,3 +180,5 @@ scripts/monero_c # iOS generated framework bin ios/MoneroWallet.framework/MoneroWallet ios/WowneroWallet.framework/WowneroWallet +ios/ZanoWallet.framework/ZanoWallet +*_libwallet2_api_c.dylib diff --git a/LICENSE.md b/LICENSE.md index 4268b9710..09bb6208b 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2018-2023 Cake Labs LLC +Copyright (c) 2018-2025 Cake Labs LLC Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/analysis_options.yaml b/analysis_options.yaml index be68a4f26..bd35233ba 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -22,6 +22,7 @@ analyzer: lib/solana/cw_solana.dart, lib/tron/cw_tron.dart, lib/wownero/cw_wownero.dart, + lib/zano/cw_zano.dart, ] language: strict-casts: true diff --git a/android/app/src/main/AndroidManifestBase.xml b/android/app/src/main/AndroidManifestBase.xml index a5282d6bd..d51409da6 100644 --- a/android/app/src/main/AndroidManifestBase.xml +++ b/android/app/src/main/AndroidManifestBase.xml @@ -89,6 +89,9 @@ + + + diff --git a/android/app/src/main/jniLibs/arm64-v8a/libzano_libwallet2_api_c.so b/android/app/src/main/jniLibs/arm64-v8a/libzano_libwallet2_api_c.so new file mode 120000 index 000000000..49ddd0f47 --- /dev/null +++ b/android/app/src/main/jniLibs/arm64-v8a/libzano_libwallet2_api_c.so @@ -0,0 +1 @@ +../../../../../../scripts/monero_c/release/zano/aarch64-linux-android_libwallet2_api_c.so \ No newline at end of file diff --git a/android/app/src/main/jniLibs/armeabi-v7a/libzano_libwallet2_api_c.so b/android/app/src/main/jniLibs/armeabi-v7a/libzano_libwallet2_api_c.so new file mode 120000 index 000000000..43f9b98b2 --- /dev/null +++ b/android/app/src/main/jniLibs/armeabi-v7a/libzano_libwallet2_api_c.so @@ -0,0 +1 @@ +../../../../../../scripts/monero_c/release/zano/armv7a-linux-androideabi_libwallet2_api_c.so \ No newline at end of file diff --git a/android/app/src/main/jniLibs/x86_64/libzano_libwallet2_api_c.so b/android/app/src/main/jniLibs/x86_64/libzano_libwallet2_api_c.so new file mode 120000 index 000000000..8c37d73c2 --- /dev/null +++ b/android/app/src/main/jniLibs/x86_64/libzano_libwallet2_api_c.so @@ -0,0 +1 @@ +../../../../../../scripts/monero_c/release/zano/x86_64-linux-android_libwallet2_api_c.so \ No newline at end of file diff --git a/assets/images/chainflip.png b/assets/images/chainflip.png new file mode 100644 index 000000000..e588e6361 Binary files /dev/null and b/assets/images/chainflip.png differ diff --git a/assets/images/flip_icon.png b/assets/images/flip_icon.png new file mode 100644 index 000000000..e588e6361 Binary files /dev/null and b/assets/images/flip_icon.png differ diff --git a/assets/images/quantex.png b/assets/images/swap_trade.png similarity index 100% rename from assets/images/quantex.png rename to assets/images/swap_trade.png diff --git a/assets/images/zano_icon.png b/assets/images/zano_icon.png new file mode 100644 index 000000000..fd48fd6a5 Binary files /dev/null and b/assets/images/zano_icon.png differ diff --git a/assets/text/Monerocom_Release_Notes.txt b/assets/text/Monerocom_Release_Notes.txt index 09092a8df..011435baa 100644 --- a/assets/text/Monerocom_Release_Notes.txt +++ b/assets/text/Monerocom_Release_Notes.txt @@ -1,2 +1,3 @@ +Ledger fixes UI enhancements Bug fixes \ No newline at end of file diff --git a/assets/text/Release_Notes.txt b/assets/text/Release_Notes.txt index 8abed72a2..ca69e0b98 100644 --- a/assets/text/Release_Notes.txt +++ b/assets/text/Release_Notes.txt @@ -1,4 +1,5 @@ -Bitcoin and Litecoin enhancements -Solana and Nano fixes/improvements +Zano enhancements +Ethereum enhancements +Ledger fixes UI enhancements Bug fixes \ No newline at end of file diff --git a/assets/zano_node_list.yml b/assets/zano_node_list.yml new file mode 100644 index 000000000..f7b874fcb --- /dev/null +++ b/assets/zano_node_list.yml @@ -0,0 +1,7 @@ +- + uri: 37.27.100.59:10500 + useSSL: false +- + uri: zano.cakewallet.com:11211 + is_default: true + useSSL: false \ No newline at end of file diff --git a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart index 04a3cae36..1a122ef9e 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart @@ -2,6 +2,7 @@ import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/bip/bip/bip32/bip32.dart'; import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; import 'package:cw_bitcoin/utils.dart'; +import 'package:cw_core/unspent_coin_type.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:mobx/mobx.dart'; @@ -26,7 +27,10 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S @override String getAddress( - {required int index, required Bip32Slip10Secp256k1 hd, BitcoinAddressType? addressType}) { + {required int index, + required Bip32Slip10Secp256k1 hd, + BitcoinAddressType? addressType, + UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any}) { if (addressType == P2pkhAddressType.p2pkh) return generateP2PKHAddress(hd: hd, index: index, network: network); diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index f57b0cf27..39fcb24b7 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -865,6 +865,7 @@ abstract class ElectrumWalletBase final changeAddress = await walletAddresses.getChangeAddress( inputs: utxoDetails.availableInputs, outputs: updatedOutputs, + coinTypeToSpendFrom: coinTypeToSpendFrom, ); final address = RegexUtils.addressTypeFromStr(changeAddress.address, network); updatedOutputs.add(BitcoinOutput( diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index 13a32c68c..35c15e578 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -4,6 +4,7 @@ import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/electrum_wallet.dart'; +import 'package:cw_core/unspent_coin_type.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_bitcoin/bitcoin_unspent.dart'; import 'package:cw_core/wallet_addresses.dart'; @@ -47,7 +48,6 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { List? initialMwebAddresses, Bip32Slip10Secp256k1? masterHd, BitcoinAddressType? initialAddressPageType, - }) : _addresses = ObservableList.of((initialAddresses ?? []).toSet()), addressesByReceiveType = ObservableList.of(([]).toSet()), @@ -187,13 +187,13 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { return; } try { - final addressRecord = _addresses.firstWhere( - (addressRecord) => addressRecord.address == addr, - ); + final addressRecord = _addresses.firstWhere( + (addressRecord) => addressRecord.address == addr, + ); - previousAddressRecord = addressRecord; - receiveAddresses.remove(addressRecord); - receiveAddresses.insert(0, addressRecord); + previousAddressRecord = addressRecord; + receiveAddresses.remove(addressRecord); + receiveAddresses.insert(0, addressRecord); } catch (e) { printV("ElectrumWalletAddressBase: set address ($addr): $e"); } @@ -274,7 +274,10 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { } @action - Future getChangeAddress({List? inputs, List? outputs, bool isPegIn = false}) async { + Future getChangeAddress( + {List? inputs, + List? outputs, + UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any}) async { updateChangeAddresses(); if (changeAddresses.isEmpty) { diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index 643f1ca23..aecc0b598 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -9,6 +9,7 @@ import 'package:crypto/crypto.dart'; import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart'; import 'package:cw_core/cake_hive.dart'; import 'package:cw_core/mweb_utxo.dart'; +import 'package:cw_core/unspent_coin_type.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/node.dart'; import 'package:cw_mweb/mwebd.pbgrpc.dart'; @@ -394,7 +395,6 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { // if the confirmations haven't changed, skip updating: if (tx.confirmations == confirmations) continue; - // if an outgoing tx is now confirmed, delete the utxo from the box (delete the unspent coin): if (confirmations >= 2 && tx.direction == TransactionDirection.outgoing && @@ -967,10 +967,19 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { List? inputPrivKeyInfos, List? vinOutpoints, }) async { - final spendsMweb = utxos.any((utxo) => utxo.utxo.scriptType == SegwitAddresType.mweb); - final paysToMweb = outputs + bool spendsMweb = utxos.any((utxo) => utxo.utxo.scriptType == SegwitAddresType.mweb); + bool paysToMweb = outputs .any((output) => output.toOutput.scriptPubKey.getAddressType() == SegwitAddresType.mweb); - if (!spendsMweb && !paysToMweb) { + + bool isRegular = !spendsMweb && !paysToMweb; + bool isMweb = spendsMweb || paysToMweb; + + if (isMweb && !mwebEnabled) { + throw Exception("MWEB is not enabled! can't calculate fee without starting the mweb server!"); + // TODO: likely the change address is mweb and just not updated + } + + if (isRegular) { return await super.calcFee( utxos: utxos, outputs: outputs, @@ -982,10 +991,6 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { ); } - if (!mwebEnabled) { - throw Exception("MWEB is not enabled! can't calculate fee without starting the mweb server!"); - } - if (outputs.length == 1 && outputs[0].toOutput.amount == BigInt.zero) { outputs = [ BitcoinScriptOutput( @@ -1056,7 +1061,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { if (!mwebEnabled) { tx.changeAddressOverride = - (await (walletAddresses as LitecoinWalletAddresses).getChangeAddress(isPegIn: false)) + (await (walletAddresses as LitecoinWalletAddresses).getChangeAddress(coinTypeToSpendFrom: UnspentCoinType.nonMweb)) .address; return tx; } @@ -1104,13 +1109,15 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { } } + // could probably be simplified but left for clarity: bool isPegIn = !hasMwebInput && hasMwebOutput; bool isPegOut = hasMwebInput && hasRegularOutput; bool isRegular = !hasMwebInput && !hasMwebOutput; + bool shouldNotUseMwebChange = isPegIn || isRegular || !hasMwebInput; tx.changeAddressOverride = (await (walletAddresses as LitecoinWalletAddresses) - .getChangeAddress(isPegIn: isPegIn || isRegular)) + .getChangeAddress(coinTypeToSpendFrom: shouldNotUseMwebChange ? UnspentCoinType.nonMweb : UnspentCoinType.any)) .address; - if (!hasMwebInput && !hasMwebOutput) { + if (isRegular) { tx.isMweb = false; return tx; } @@ -1213,7 +1220,9 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { } Future setMwebEnabled(bool enabled) async { - if (mwebEnabled == enabled) { + if (mwebEnabled == enabled && + alwaysScan == enabled && + (walletAddresses as LitecoinWalletAddresses).mwebEnabled == enabled) { return; } diff --git a/cw_bitcoin/lib/litecoin_wallet_addresses.dart b/cw_bitcoin/lib/litecoin_wallet_addresses.dart index afe3c75b8..bbb987766 100644 --- a/cw_bitcoin/lib/litecoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/litecoin_wallet_addresses.dart @@ -9,6 +9,7 @@ import 'package:cw_bitcoin/bitcoin_unspent.dart'; import 'package:cw_bitcoin/electrum_wallet.dart'; import 'package:cw_bitcoin/utils.dart'; import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; +import 'package:cw_core/unspent_coin_type.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_mweb/cw_mweb.dart'; @@ -148,10 +149,12 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with @action @override Future getChangeAddress( - {List? inputs, List? outputs, bool isPegIn = false}) async { + {List? inputs, + List? outputs, + UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any}) async { // use regular change address on peg in, otherwise use mweb for change address: - if (!mwebEnabled || isPegIn) { + if (!mwebEnabled || coinTypeToSpendFrom == UnspentCoinType.nonMweb) { return super.getChangeAddress(); } @@ -178,19 +181,17 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with }); bool isPegIn = !comesFromMweb && outputsToMweb; + bool isNonMweb = !comesFromMweb && !outputsToMweb; - if (isPegIn && mwebEnabled) { - return super.getChangeAddress(); - } - - // use regular change address if it's not an mweb tx: - if (!comesFromMweb && !outputsToMweb) { + // use regular change address if it's not an mweb tx or if it's a peg in: + if (isPegIn || isNonMweb) { return super.getChangeAddress(); } } if (mwebEnabled) { await ensureMwebAddressUpToIndexExists(1); + updateChangeAddresses(); return BitcoinAddressRecord( mwebAddrs[0], index: 0, diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock index c65f056bb..6a8e7d5c4 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -316,6 +316,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.10" + decimal: + dependency: transitive + description: + name: decimal + sha256: "24a261d5d5c87e86c7651c417a5dbdf8bcd7080dd592533910e8d0505a279f21" + url: "https://pub.dev" + source: hosted + version: "2.3.3" encrypt: dependency: transitive description: @@ -797,6 +805,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.2.2" + rational: + dependency: transitive + description: + name: rational + sha256: cb808fb6f1a839e6fc5f7d8cb3b0a10e1db48b3be102de73938c627f0b636336 + url: "https://pub.dev" + source: hosted + version: "2.2.3" rxdart: dependency: "direct main" description: diff --git a/cw_core/lib/amount_converter.dart b/cw_core/lib/amount_converter.dart index 1c5456b07..71d0cef42 100644 --- a/cw_core/lib/amount_converter.dart +++ b/cw_core/lib/amount_converter.dart @@ -1,3 +1,5 @@ +import 'package:decimal/decimal.dart'; +import 'package:decimal/intl.dart'; import 'package:intl/intl.dart'; import 'package:cw_core/crypto_currency.dart'; @@ -43,6 +45,8 @@ class AmountConverter { case CryptoCurrency.xnzd: case CryptoCurrency.xusd: return _moneroAmountToString(amount); + case CryptoCurrency.zano: + return _moneroAmountToStringUsingDecimals(amount); default: return ''; } @@ -59,4 +63,10 @@ class AmountConverter { static String _wowneroAmountToString(int amount) => _wowneroAmountFormat .format(cryptoAmountToDouble(amount: amount, divider: _wowneroAmountDivider)); + + static Decimal cryptoAmountToDecimal({required int amount, required int divider}) => + (Decimal.fromInt(amount) / Decimal.fromInt(divider)).toDecimal(); + + static String _moneroAmountToStringUsingDecimals(int amount) => _moneroAmountFormat.format( + DecimalIntl(cryptoAmountToDecimal(amount: amount, divider: _moneroAmountDivider))); } diff --git a/cw_core/lib/crypto_currency.dart b/cw_core/lib/crypto_currency.dart index 0280bb45a..29dc03f9e 100644 --- a/cw_core/lib/crypto_currency.dart +++ b/cw_core/lib/crypto_currency.dart @@ -106,7 +106,9 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen CryptoCurrency.usdcTrc20, CryptoCurrency.tbtc, CryptoCurrency.wow, + CryptoCurrency.zano, CryptoCurrency.ton, + CryptoCurrency.flip ]; static const havenCurrencies = [ @@ -225,7 +227,8 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen static const tbtc = CryptoCurrency(title: 'tBTC', fullName: 'Testnet Bitcoin', raw: 93, name: 'tbtc', iconPath: 'assets/images/tbtc.png', decimals: 8); static const wow = CryptoCurrency(title: 'WOW', fullName: 'Wownero', raw: 94, name: 'wow', iconPath: 'assets/images/wownero_icon.png', decimals: 11); static const ton = CryptoCurrency(title: 'TON', fullName: 'Toncoin', raw: 95, name: 'ton', iconPath: 'assets/images/ton_icon.png', decimals: 8); - + static const zano = CryptoCurrency(title: 'ZANO', tag: 'ZANO', fullName: 'Zano', raw: 96, name: 'zano', iconPath: 'assets/images/zano_icon.png', decimals: 12); + static const flip = CryptoCurrency(title: 'FLIP', tag: 'ETH', fullName: 'Chainflip', raw: 97, name: 'flip', iconPath: 'assets/images/flip_icon.png', decimals: 18); static final Map _rawCurrencyMap = [...all, ...havenCurrencies].fold>({}, (acc, item) { diff --git a/cw_core/lib/currency_for_wallet_type.dart b/cw_core/lib/currency_for_wallet_type.dart index af6037a3b..bd1c224a3 100644 --- a/cw_core/lib/currency_for_wallet_type.dart +++ b/cw_core/lib/currency_for_wallet_type.dart @@ -30,8 +30,11 @@ CryptoCurrency currencyForWalletType(WalletType type, {bool? isTestnet}) { return CryptoCurrency.trx; case WalletType.wownero: return CryptoCurrency.wow; + case WalletType.zano: + return CryptoCurrency.zano; case WalletType.none: throw Exception( + 'Unexpected wallet type: ${type.toString()} for CryptoCurrency currencyForWalletType'); } } diff --git a/cw_core/lib/exceptions.dart b/cw_core/lib/exceptions.dart index 885f5cb2b..364e332e7 100644 --- a/cw_core/lib/exceptions.dart +++ b/cw_core/lib/exceptions.dart @@ -56,3 +56,13 @@ class CreateAssociatedTokenAccountException implements Exception { class SignSPLTokenTransactionRentException implements Exception {} class NoAssociatedTokenAccountException implements Exception {} + + +/// ============================================================================== +/// ============================================================================== + +class RestoreFromSeedException implements Exception { + final String message; + + RestoreFromSeedException(this.message); +} diff --git a/cw_core/lib/hive_type_ids.dart b/cw_core/lib/hive_type_ids.dart index 0899ac665..ff0f53169 100644 --- a/cw_core/lib/hive_type_ids.dart +++ b/cw_core/lib/hive_type_ids.dart @@ -19,4 +19,5 @@ const DERIVATION_INFO_TYPE_ID = 17; const TRON_TOKEN_TYPE_ID = 18; const HARDWARE_WALLET_TYPE_TYPE_ID = 19; const MWEB_UTXO_TYPE_ID = 20; -const HAVEN_SEED_STORE_TYPE_ID = 21; \ No newline at end of file +const HAVEN_SEED_STORE_TYPE_ID = 21; +const ZANO_ASSET_TYPE_ID = 22; diff --git a/cw_core/lib/monero_wallet_keys.dart b/cw_core/lib/monero_wallet_keys.dart index fe3784986..4ead3d743 100644 --- a/cw_core/lib/monero_wallet_keys.dart +++ b/cw_core/lib/monero_wallet_keys.dart @@ -4,11 +4,13 @@ class MoneroWalletKeys { required this.privateSpendKey, required this.privateViewKey, required this.publicSpendKey, - required this.publicViewKey}); + required this.publicViewKey, + required this.passphrase}); final String primaryAddress; final String publicViewKey; final String privateViewKey; final String publicSpendKey; final String privateSpendKey; + final String passphrase; } \ No newline at end of file diff --git a/cw_core/lib/node.dart b/cw_core/lib/node.dart index 7d0c2411f..c984cd03b 100644 --- a/cw_core/lib/node.dart +++ b/cw_core/lib/node.dart @@ -1,13 +1,16 @@ import 'dart:io'; import 'package:cw_core/keyable.dart'; +import 'package:cw_core/utils/print_verbose.dart'; import 'dart:convert'; import 'package:http/http.dart' as http; import 'package:hive/hive.dart'; import 'package:cw_core/hive_type_ids.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:http/io_client.dart' as ioc; +import 'dart:math' as math; +import 'package:convert/convert.dart'; -// import 'package:tor/tor.dart'; +import 'package:crypto/crypto.dart'; part 'node.g.dart'; @@ -99,8 +102,9 @@ class Node extends HiveObject with Keyable { case WalletType.polygon: case WalletType.solana: case WalletType.tron: + case WalletType.zano: return Uri.parse( - "http${isSSL ? "s" : ""}://$uriRaw${path!.startsWith("/") ? path : "/$path"}"); + "http${isSSL ? "s" : ""}://$uriRaw${path!.startsWith("/") || path!.isEmpty ? path : "/$path"}"); case WalletType.none: throw Exception('Unexpected type ${type.toString()} for Node uri'); } @@ -161,6 +165,8 @@ class Node extends HiveObject with Keyable { case WalletType.solana: case WalletType.tron: return requestElectrumServer(); + case WalletType.zano: + return requestZanoNode(); case WalletType.none: return false; } @@ -169,35 +175,71 @@ class Node extends HiveObject with Keyable { } } - Future requestMoneroNode() async { - if (uri.toString().contains(".onion") || useSocksProxy) { - return await requestNodeWithProxy(); - } + Future requestZanoNode() async { final path = '/json_rpc'; final rpcUri = isSSL ? Uri.https(uri.authority, path) : Uri.http(uri.authority, path); - final realm = 'monero-rpc'; - final body = {'jsonrpc': '2.0', 'id': '0', 'method': 'get_info'}; + final body = {'jsonrpc': '2.0', 'id': '0', 'method': "getinfo"}; try { final authenticatingClient = HttpClient(); - authenticatingClient.badCertificateCallback = ((X509Certificate cert, String host, int port) => true); - authenticatingClient.addCredentials( - rpcUri, - realm, - HttpClientDigestCredentials(login ?? '', password ?? ''), - ); - final http.Client client = ioc.IOClient(authenticatingClient); + final jsonBody = json.encode(body); + final response = await client.post( rpcUri, headers: {'Content-Type': 'application/json'}, - body: json.encode(body), + body: jsonBody, ); - client.close(); + + printV("node check response: ${response.body}"); + + final resBody = json.decode(response.body) as Map; + return resBody['result']['height'] != null; + } catch (e) { + printV("error: $e"); + return false; + } + } + + Future requestMoneroNode({String methodName = 'get_info'}) async { + if (useSocksProxy) { + return await requestNodeWithProxy(); + } + + final path = '/json_rpc'; + final rpcUri = isSSL ? Uri.https(uri.authority, path) : Uri.http(uri.authority, path); + final body = {'jsonrpc': '2.0', 'id': '0', 'method': methodName}; + + try { + final authenticatingClient = HttpClient(); + authenticatingClient.badCertificateCallback = + ((X509Certificate cert, String host, int port) => true); + + final http.Client client = ioc.IOClient(authenticatingClient); + + final jsonBody = json.encode(body); + + final response = await client.post( + rpcUri, + headers: {'Content-Type': 'application/json'}, + body: jsonBody, + ); + // Check if we received a 401 Unauthorized response + if (response.statusCode == 401) { + final daemonRpc = DaemonRpc( + rpcUri.toString(), + username: login ?? '', + password: password ?? '', + ); + final response = await daemonRpc.call('get_info', {}); + return !(response['offline'] as bool); + } + + printV("node check response: ${response.body}"); if ((response.body.contains("400 Bad Request") // Some other generic error || @@ -212,7 +254,7 @@ class Node extends HiveObject with Keyable { final oldUseSSL = useSSL; useSSL = true; try { - final ret = await requestMoneroNode(); + final ret = await requestMoneroNode(methodName: methodName); if (ret == true) { await save(); return ret; @@ -225,7 +267,8 @@ class Node extends HiveObject with Keyable { final resBody = json.decode(response.body) as Map; return !(resBody['result']['offline'] as bool); - } catch (_) { + } catch (e) { + printV("error: $e"); return false; } } @@ -278,10 +321,7 @@ class Node extends HiveObject with Keyable { try { final response = await http.post( uri, - headers: { - "Content-Type": "application/json", - "nano-app": "cake-wallet" - }, + headers: {"Content-Type": "application/json", "nano-app": "cake-wallet"}, body: jsonEncode( { "action": "account_balance", @@ -316,3 +356,149 @@ class Node extends HiveObject with Keyable { } } } + +/// https://github.com/ManyMath/digest_auth/ +/// HTTP Digest authentication. +/// +/// Adapted from https://github.com/dart-lang/http/issues/605#issue-963962341. +/// +/// Created because http_auth was not working for Monero daemon RPC responses. +class DigestAuth { + final String username; + final String password; + String? realm; + String? nonce; + String? uri; + String? qop = "auth"; + int _nonceCount = 0; + + DigestAuth(this.username, this.password); + + /// Initialize Digest parameters from the `WWW-Authenticate` header. + void initFromAuthorizationHeader(String authInfo) { + final Map? values = _splitAuthenticateHeader(authInfo); + if (values != null) { + realm = values['realm']; + // Check if the nonce has changed. + if (nonce != values['nonce']) { + nonce = values['nonce']; + _nonceCount = 0; // Reset nonce count when nonce changes. + } + } + } + + /// Generate the Digest Authorization header. + String getAuthString(String method, String uri) { + this.uri = uri; + _nonceCount++; + String cnonce = _computeCnonce(); + String nc = _formatNonceCount(_nonceCount); + + String ha1 = md5Hash("$username:$realm:$password"); + String ha2 = md5Hash("$method:$uri"); + String response = md5Hash("$ha1:$nonce:$nc:$cnonce:$qop:$ha2"); + + return 'Digest username="$username", realm="$realm", nonce="$nonce", uri="$uri", qop=$qop, nc=$nc, cnonce="$cnonce", response="$response"'; + } + + /// Helper to parse the `WWW-Authenticate` header. + Map? _splitAuthenticateHeader(String? header) { + if (header == null || !header.startsWith('Digest ')) { + return null; + } + String token = header.substring(7); // Remove 'Digest '. + final Map result = {}; + + final components = token.split(',').map((token) => token.trim()); + for (final component in components) { + final kv = component.split('='); + final key = kv[0]; + final value = kv.sublist(1).join('=').replaceAll('"', ''); + result[key] = value; + } + return result; + } + + /// Helper to compute a random cnonce. + String _computeCnonce() { + final math.Random rnd = math.Random(); + final List values = List.generate(16, (i) => rnd.nextInt(256)); + return hex.encode(values); + } + + /// Helper to format the nonce count. + String _formatNonceCount(int count) => count.toRadixString(16).padLeft(8, '0'); + + /// Compute the MD5 hash of a string. + String md5Hash(String input) { + return md5.convert(utf8.encode(input)).toString(); + } +} + +class DaemonRpc { + final String rpcUrl; + final String username; + final String password; + + DaemonRpc(this.rpcUrl, {required this.username, required this.password}); + + /// Perform a JSON-RPC call with Digest Authentication. + Future> call(String method, Map params) async { + final http.Client client = http.Client(); + final DigestAuth digestAuth = DigestAuth(username, password); + + // Initial request to get the `WWW-Authenticate` header. + final initialResponse = await client.post( + Uri.parse(rpcUrl), + headers: { + 'Content-Type': 'application/json', + }, + body: jsonEncode({ + 'jsonrpc': '2.0', + 'id': '0', + 'method': method, + 'params': params, + }), + ); + + if (initialResponse.statusCode != 401 || + !initialResponse.headers.containsKey('www-authenticate')) { + throw Exception('Unexpected response: ${initialResponse.body}'); + } + + // Extract Digest details from `WWW-Authenticate` header. + final String authInfo = initialResponse.headers['www-authenticate']!; + digestAuth.initFromAuthorizationHeader(authInfo); + + // Create Authorization header for the second request. + String uri = Uri.parse(rpcUrl).path; + String authHeader = digestAuth.getAuthString('POST', uri); + + // Make the authenticated request. + final authenticatedResponse = await client.post( + Uri.parse(rpcUrl), + headers: { + 'Content-Type': 'application/json', + 'Authorization': authHeader, + }, + body: jsonEncode({ + 'jsonrpc': '2.0', + 'id': '0', + 'method': method, + 'params': params, + }), + ); + + if (authenticatedResponse.statusCode != 200) { + throw Exception('RPC call failed: ${authenticatedResponse.body}'); + } + + final Map result = + jsonDecode(authenticatedResponse.body) as Map; + if (result['error'] != null) { + throw Exception('RPC Error: ${result['error']}'); + } + + return result['result'] as Map; + } +} diff --git a/cw_core/lib/wallet_type.dart b/cw_core/lib/wallet_type.dart index e3957b4e7..79d2b002d 100644 --- a/cw_core/lib/wallet_type.dart +++ b/cw_core/lib/wallet_type.dart @@ -16,6 +16,7 @@ const walletTypes = [ WalletType.polygon, WalletType.solana, WalletType.tron, + WalletType.zano, ]; @HiveType(typeId: WALLET_TYPE_TYPE_ID) @@ -58,6 +59,10 @@ enum WalletType { @HiveField(12) wownero, + + @HiveField(13) + zano, + } int serializeToInt(WalletType type) { @@ -86,6 +91,8 @@ int serializeToInt(WalletType type) { return 10; case WalletType.wownero: return 11; + case WalletType.zano: + return 12; case WalletType.none: return -1; } @@ -117,8 +124,11 @@ WalletType deserializeFromInt(int raw) { return WalletType.tron; case 11: return WalletType.wownero; + case 12: + return WalletType.zano; default: - throw Exception('Unexpected token: $raw for WalletType deserializeFromInt'); + throw Exception( + 'Unexpected token: $raw for WalletType deserializeFromInt'); } } @@ -148,6 +158,8 @@ String walletTypeToString(WalletType type) { return 'Tron'; case WalletType.wownero: return 'Wownero'; + case WalletType.zano: + return 'Zano'; case WalletType.none: return ''; } @@ -179,6 +191,8 @@ String walletTypeToDisplayName(WalletType type) { return 'Tron (TRX)'; case WalletType.wownero: return 'Wownero (WOW)'; + case WalletType.zano: + return 'Zano (ZANO)'; case WalletType.none: return ''; } @@ -213,6 +227,8 @@ CryptoCurrency walletTypeToCryptoCurrency(WalletType type, {bool isTestnet = fal return CryptoCurrency.trx; case WalletType.wownero: return CryptoCurrency.wow; + case WalletType.zano: + return CryptoCurrency.zano; case WalletType.none: throw Exception( 'Unexpected wallet type: ${type.toString()} for CryptoCurrency walletTypeToCryptoCurrency'); diff --git a/cw_core/lib/zano_asset.dart b/cw_core/lib/zano_asset.dart new file mode 100644 index 000000000..0332f5972 --- /dev/null +++ b/cw_core/lib/zano_asset.dart @@ -0,0 +1,131 @@ +import 'dart:convert'; + +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/hive_type_ids.dart'; +import 'package:hive/hive.dart'; + +part 'zano_asset.g.dart'; + +@HiveType(typeId: ZanoAsset.typeId) +class ZanoAsset extends CryptoCurrency with HiveObjectMixin { + @HiveField(0) + final String fullName; + @HiveField(1) + final String ticker; + @HiveField(2) + final String assetId; + @HiveField(3) + final int decimalPoint; + @HiveField(4, defaultValue: true) + bool _enabled; + @HiveField(5) + final String? iconPath; + + // @HiveField(6) + // final String? tag; + @HiveField(6) + final String owner; + @HiveField(7) + final String metaInfo; + @HiveField(8) + final BigInt currentSupply; + @HiveField(9) + final bool hiddenSupply; + @HiveField(10) + final BigInt totalMaxSupply; + @HiveField(11) + final bool isInGlobalWhitelist; + @HiveField(12, defaultValue: null) + final Map? info; + + bool get enabled => _enabled; + + set enabled(bool value) => _enabled = value; + + ZanoAsset({ + this.fullName = '', + this.ticker = '', + required this.assetId, + this.decimalPoint = 12, + bool enabled = true, + this.iconPath, + this.owner = defaultOwner, + this.metaInfo = '', + required this.currentSupply, + this.hiddenSupply = false, + required this.totalMaxSupply, + this.isInGlobalWhitelist = false, + this.info, + }) : _enabled = enabled, + super( + name: fullName, + title: ticker.toUpperCase(), + fullName: fullName, + tag: 'ZANO', + iconPath: iconPath, + decimals: decimalPoint, + ); + + ZanoAsset.copyWith(ZanoAsset other, {String? assetId, bool enabled = true}) + : this.fullName = other.fullName, + this.ticker = other.ticker, + this.assetId = assetId ?? other.assetId, + this.decimalPoint = other.decimalPoint, + this._enabled = enabled && other.enabled, + this.iconPath = other.iconPath, + this.currentSupply = other.currentSupply, + this.hiddenSupply = other.hiddenSupply, + this.metaInfo = other.metaInfo, + this.owner = other.owner, + this.totalMaxSupply = other.totalMaxSupply, + this.isInGlobalWhitelist = other.isInGlobalWhitelist, + this.info = other.info, + super( + name: other.name, + title: other.ticker.toUpperCase(), + fullName: other.name, + tag: 'ZANO', + iconPath: other.iconPath, + decimals: other.decimalPoint, + enabled: enabled, + ); + + factory ZanoAsset.fromJson(Map json, {bool isInGlobalWhitelist = false}) { + Map? info; + try { + info = jsonDecode((json['meta_info'] as String?) ?? '{}') as Map?; + } catch (_) {} + + return ZanoAsset( + assetId: json['asset_id'] as String? ?? '', + currentSupply: bigIntFromDynamic(json['current_supply']), + decimalPoint: json['decimal_point'] as int? ?? 12, + fullName: json['full_name'] as String? ?? '', + hiddenSupply: json['hidden_supply'] as bool? ?? false, + metaInfo: json['meta_info'] as String? ?? '', + owner: json['owner'] as String? ?? '', + ticker: json['ticker'] as String? ?? '', + iconPath: info?['logo_url'] as String? ?? '', + totalMaxSupply: bigIntFromDynamic(json['total_max_supply']), + isInGlobalWhitelist: isInGlobalWhitelist, + info: info, + ); + } + + static const typeId = ZANO_ASSET_TYPE_ID; + static const zanoAssetsBoxName = 'zanoAssetsBox'; + static const defaultOwner = '0000000000000000000000000000000000000000000000000000000000000000'; +} + +BigInt bigIntFromDynamic(dynamic d) { + if (d is int) { + return BigInt.from(d); + } else if (d is BigInt) { + return d; + } else if (d == null) { + return BigInt.zero; + } else { + throw 'cannot cast value of type ${d.runtimeType} to BigInt'; + //return BigInt.zero; + } +} diff --git a/cw_core/pubspec.lock b/cw_core/pubspec.lock index c12839a19..f023dc153 100644 --- a/cw_core/pubspec.lock +++ b/cw_core/pubspec.lock @@ -207,6 +207,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.7" + decimal: + dependency: "direct main" + description: + name: decimal + sha256: "24a261d5d5c87e86c7651c417a5dbdf8bcd7080dd592533910e8d0505a279f21" + url: "https://pub.dev" + source: hosted + version: "2.3.3" encrypt: dependency: "direct main" description: @@ -577,6 +585,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + rational: + dependency: transitive + description: + name: rational + sha256: cb808fb6f1a839e6fc5f7d8cb3b0a10e1db48b3be102de73938c627f0b636336 + url: "https://pub.dev" + source: hosted + version: "2.2.3" shelf: dependency: transitive description: diff --git a/cw_core/pubspec.yaml b/cw_core/pubspec.yaml index 19eda51e0..d59440800 100644 --- a/cw_core/pubspec.yaml +++ b/cw_core/pubspec.yaml @@ -19,6 +19,7 @@ dependencies: flutter_mobx: ^2.0.6+1 intl: ^0.19.0 encrypt: ^5.0.1 + decimal: ^2.3.3 cake_backup: git: url: https://github.com/cake-tech/cake_backup.git diff --git a/cw_ethereum/lib/default_ethereum_erc20_tokens.dart b/cw_ethereum/lib/default_ethereum_erc20_tokens.dart index c26ee1efc..ee60a3d6c 100644 --- a/cw_ethereum/lib/default_ethereum_erc20_tokens.dart +++ b/cw_ethereum/lib/default_ethereum_erc20_tokens.dart @@ -290,6 +290,13 @@ class DefaultEthereumErc20Tokens { decimal: 6, enabled: false, ), + Erc20Token( + name: "Chainflip", + symbol: "FLIP", + contractAddress: "0x826180541412D574cf1336d22c0C0a287822678A", + decimal: 18, + enabled: false, + ), ]; List get initialErc20Tokens => _defaultTokens.map((token) { diff --git a/cw_evm/lib/evm_chain_client.dart b/cw_evm/lib/evm_chain_client.dart index 7dad39f7a..b505577e9 100644 --- a/cw_evm/lib/evm_chain_client.dart +++ b/cw_evm/lib/evm_chain_client.dart @@ -268,12 +268,18 @@ abstract class EVMChainClient { Future fetchERC20Balances( EthereumAddress userAddress, String contractAddress) async { - final erc20 = ERC20(address: EthereumAddress.fromHex(contractAddress), client: _client!); - final balance = await erc20.balanceOf(userAddress); + try { + final erc20 = ERC20(address: EthereumAddress.fromHex(contractAddress), client: _client!); + final balance = await erc20.balanceOf(userAddress); int exponent = (await erc20.decimals()).toInt(); - return EVMChainERC20Balance(balance, exponent: exponent); + return EVMChainERC20Balance(balance, exponent: exponent); + } on RangeError catch (_) { + throw Exception('Invalid token contract for this network.'); + } catch (e) { + throw Exception('Could not fetch balances: ${e.toString()}'); + } } Future getErc20Token(String contractAddress, String chainName) async { diff --git a/cw_evm/lib/evm_chain_wallet.dart b/cw_evm/lib/evm_chain_wallet.dart index b2c964038..c4fd02172 100644 --- a/cw_evm/lib/evm_chain_wallet.dart +++ b/cw_evm/lib/evm_chain_wallet.dart @@ -634,14 +634,21 @@ abstract class EVMChainWalletBase final newToken = createNewErc20TokenObject(token, iconPath); - await evmChainErc20TokensBox.put(newToken.contractAddress, newToken); - if (newToken.enabled) { - balance[newToken] = await _client.fetchERC20Balances( - _evmChainPrivateKey.address, - newToken.contractAddress, - ); + try { + final erc20Balance = await _client.fetchERC20Balances( + _evmChainPrivateKey.address, + newToken.contractAddress, + ); + + balance[newToken] = erc20Balance; + + await evmChainErc20TokensBox.put(newToken.contractAddress, newToken); + } on Exception catch (_) { + rethrow; + } } else { + await evmChainErc20TokensBox.put(newToken.contractAddress, newToken); balance.remove(newToken); } } diff --git a/cw_evm/lib/evm_erc20_balance.dart b/cw_evm/lib/evm_erc20_balance.dart index 8962f7053..952d50ba8 100644 --- a/cw_evm/lib/evm_erc20_balance.dart +++ b/cw_evm/lib/evm_erc20_balance.dart @@ -1,5 +1,6 @@ import 'dart:convert'; import 'dart:math'; +import 'package:intl/intl.dart'; import 'package:cw_core/balance.dart'; @@ -17,8 +18,10 @@ class EVMChainERC20Balance extends Balance { String get formattedAvailableBalance => _balance(); String _balance() { - final String formattedBalance = (balance / BigInt.from(10).pow(exponent)).toString(); - return formattedBalance.substring(0, min(12, formattedBalance.length)); + NumberFormat formatter = NumberFormat('0.00##########', 'en_US'); + double numBalance = (balance / BigInt.from(10).pow(exponent)).toDouble(); + String formattedBalance = formatter.format(numBalance); + return formattedBalance; } String toJSON() => json.encode({ diff --git a/cw_haven/lib/haven_wallet.dart b/cw_haven/lib/haven_wallet.dart index 6c372d344..515fa624e 100644 --- a/cw_haven/lib/haven_wallet.dart +++ b/cw_haven/lib/haven_wallet.dart @@ -78,7 +78,8 @@ abstract class HavenWalletBase privateSpendKey: haven_wallet.getSecretSpendKey(), privateViewKey: haven_wallet.getSecretViewKey(), publicSpendKey: haven_wallet.getPublicSpendKey(), - publicViewKey: haven_wallet.getPublicViewKey()); + publicViewKey: haven_wallet.getPublicViewKey(), + passphrase: ""); haven_wallet.SyncListener? _listener; ReactionDisposer? _onAccountChangeReaction; diff --git a/cw_haven/pubspec.lock b/cw_haven/pubspec.lock index b6cae9f39..da5a11b89 100644 --- a/cw_haven/pubspec.lock +++ b/cw_haven/pubspec.lock @@ -209,6 +209,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.4" + decimal: + dependency: transitive + description: + name: decimal + sha256: "24a261d5d5c87e86c7651c417a5dbdf8bcd7080dd592533910e8d0505a279f21" + url: "https://pub.dev" + source: hosted + version: "2.3.3" encrypt: dependency: transitive description: @@ -571,6 +579,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + rational: + dependency: transitive + description: + name: rational + sha256: cb808fb6f1a839e6fc5f7d8cb3b0a10e1db48b3be102de73938c627f0b636336 + url: "https://pub.dev" + source: hosted + version: "2.2.3" shelf: dependency: transitive description: diff --git a/cw_monero/lib/api/wallet.dart b/cw_monero/lib/api/wallet.dart index d6063069c..8c5bbfee0 100644 --- a/cw_monero/lib/api/wallet.dart +++ b/cw_monero/lib/api/wallet.dart @@ -8,6 +8,7 @@ import 'package:cw_monero/api/exceptions/setup_wallet_exception.dart'; import 'package:flutter/foundation.dart'; import 'package:monero/monero.dart' as monero; import 'package:mutex/mutex.dart'; +import 'package:polyseed/polyseed.dart'; bool debugMonero = false; @@ -34,20 +35,34 @@ String getFilename() => monero.Wallet_filename(wptr!); String getSeed() { // monero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.seed", value: seed); - final cakepolyseed = monero.Wallet_getCacheAttribute(wptr!, key: "cakewallet.seed"); + final cakepolyseed = + monero.Wallet_getCacheAttribute(wptr!, key: "cakewallet.seed"); + final cakepassphrase = getPassphrase(); + + final weirdPolyseed = monero.Wallet_getPolyseed(wptr!, passphrase: cakepassphrase); + if (weirdPolyseed != "") return weirdPolyseed; if (cakepolyseed != "") { + if (cakepassphrase != "") { + try { + final lang = PolyseedLang.getByPhrase(cakepolyseed); + final coin = PolyseedCoin.POLYSEED_MONERO; + final ps = Polyseed.decode(cakepolyseed, lang, coin); + if (ps.isEncrypted || cakepassphrase == "") return ps.encode(lang, coin); + ps.crypt(cakepassphrase); + return ps.encode(lang, coin); + } catch (e) { + printV(e); + } + } return cakepolyseed; } - final polyseed = monero.Wallet_getPolyseed(wptr!, passphrase: ''); - if (polyseed != "") { - return polyseed; - } - final legacy = getSeedLegacy("English"); + final legacy = getSeedLegacy(null); return legacy; } String getSeedLegacy(String? language) { - var legacy = monero.Wallet_seed(wptr!, seedOffset: ''); + final cakepassphrase = getPassphrase(); + var legacy = monero.Wallet_seed(wptr!, seedOffset: cakepassphrase); switch (language) { case "Chinese (Traditional)": language = "Chinese (simplified)"; @@ -67,7 +82,7 @@ String getSeedLegacy(String? language) { } if (monero.Wallet_status(wptr!) != 0) { monero.Wallet_setSeedLanguage(wptr!, language: language ?? "English"); - legacy = monero.Wallet_seed(wptr!, seedOffset: ''); + legacy = monero.Wallet_seed(wptr!, seedOffset: cakepassphrase); } if (monero.Wallet_status(wptr!) != 0) { final err = monero.Wallet_errorString(wptr!); @@ -79,6 +94,10 @@ String getSeedLegacy(String? language) { return legacy; } +String getPassphrase() { + return monero.Wallet_getCacheAttribute(wptr!, key: "cakewallet.passphrase"); +} + Map>> addressCache = {}; String getAddress({int accountIndex = 0, int addressIndex = 0}) { diff --git a/cw_monero/lib/api/wallet_manager.dart b/cw_monero/lib/api/wallet_manager.dart index 530d58d62..621f79577 100644 --- a/cw_monero/lib/api/wallet_manager.dart +++ b/cw_monero/lib/api/wallet_manager.dart @@ -70,6 +70,7 @@ void createWalletSync( {required String path, required String password, required String language, + required String passphrase, int nettype = 0}) { txhistory = null; final newWptr = monero.WalletManager_createWallet(wmPtr, @@ -80,6 +81,7 @@ void createWalletSync( throw WalletCreationException(message: monero.Wallet_errorString(newWptr)); } wptr = newWptr; + monero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.passphrase", value: passphrase); monero.Wallet_store(wptr!, path: path); openedWalletsByPath[path] = wptr!; _lastOpenedWallet = path; @@ -95,6 +97,7 @@ bool isWalletExistSync({required String path}) { void restoreWalletFromSeedSync( {required String path, required String password, + required String passphrase, required String seed, int nettype = 0, int restoreHeight = 0}) { @@ -105,7 +108,7 @@ void restoreWalletFromSeedSync( password: password, mnemonic: seed, restoreHeight: restoreHeight, - seedOffset: '', + seedOffset: passphrase, networkType: 0, ); @@ -113,11 +116,22 @@ void restoreWalletFromSeedSync( if (status != 0) { final error = monero.Wallet_errorString(newWptr); + if (error.contains('word list failed verification')) { + throw WalletRestoreFromSeedException( + message: "Seed verification failed, please make sure you entered the correct seed with the correct words order", + ); + } throw WalletRestoreFromSeedException(message: error); } wptr = newWptr; + setRefreshFromBlockHeight(height: restoreHeight); + + monero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.passphrase", value: passphrase); + openedWalletsByPath[path] = wptr!; + + monero.Wallet_store(wptr!); _lastOpenedWallet = path; } @@ -189,6 +203,48 @@ void restoreWalletFromKeysSync( _lastOpenedWallet = path; } + +// English only, because normalization. +void restoreWalletFromPolyseedWithOffset( + {required String path, + required String password, + required String seed, + required String seedOffset, + required String language, + int nettype = 0}) { + + txhistory = null; + final newWptr = monero.WalletManager_createWalletFromPolyseed( + wmPtr, + path: path, + password: password, + networkType: nettype, + mnemonic: seed, + seedOffset: seedOffset, + newWallet: true, // safe to remove + restoreHeight: 0, + kdfRounds: 1, + ); + + final status = monero.Wallet_status(newWptr); + + if (status != 0) { + final err = monero.Wallet_errorString(newWptr); + printV("err: $err"); + throw WalletRestoreFromKeysException(message: err); + } + + wptr = newWptr; + + monero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.seed", value: seed); + monero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.passphrase", value: seedOffset); + monero.Wallet_store(wptr!); + storeSync(); + + openedWalletsByPath[path] = wptr!; +} + + void restoreWalletFromSpendKeySync( {required String path, required String password, @@ -341,18 +397,20 @@ void _createWallet(Map args) { final path = args['path'] as String; final password = args['password'] as String; final language = args['language'] as String; + final passphrase = args['passphrase'] as String; - createWalletSync(path: path, password: password, language: language); + createWalletSync(path: path, password: password, language: language, passphrase: passphrase); } void _restoreFromSeed(Map args) { final path = args['path'] as String; final password = args['password'] as String; + final passphrase = args['passphrase'] as String; final seed = args['seed'] as String; final restoreHeight = args['restoreHeight'] as int; - restoreWalletFromSeedSync( - path: path, password: password, seed: seed, restoreHeight: restoreHeight); + return restoreWalletFromSeedSync( + path: path, password: password, passphrase: passphrase, seed: seed, restoreHeight: restoreHeight); } void _restoreFromKeys(Map args) { @@ -409,23 +467,27 @@ Future createWallet( {required String path, required String password, required String language, + required String passphrase, int nettype = 0}) async => _createWallet({ 'path': path, 'password': password, 'language': language, + 'passphrase': passphrase, 'nettype': nettype }); -Future restoreFromSeed( +void restoreFromSeed( {required String path, required String password, + required String passphrase, required String seed, int nettype = 0, - int restoreHeight = 0}) async => + int restoreHeight = 0}) => _restoreFromSeed({ 'path': path, 'password': password, + 'passphrase': passphrase, 'seed': seed, 'nettype': nettype, 'restoreHeight': restoreHeight diff --git a/cw_monero/lib/ledger.dart b/cw_monero/lib/ledger.dart index b95c655a0..219d45bb5 100644 --- a/cw_monero/lib/ledger.dart +++ b/cw_monero/lib/ledger.dart @@ -56,14 +56,12 @@ void enableLedgerExchange(monero.wallet ptr, LedgerConnection 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 (_) {} + UniversalBle.setNotifiable( + connection.device.id, + connection.device.deviceInfo.serviceId, + connection.device.deviceInfo.notifyCharacteristicKey, + BleInputProperty.notification, + ).onError((_, __) async {}); }); } } diff --git a/cw_monero/lib/monero_transaction_info.dart b/cw_monero/lib/monero_transaction_info.dart index 76064ad11..0ac48dcba 100644 --- a/cw_monero/lib/monero_transaction_info.dart +++ b/cw_monero/lib/monero_transaction_info.dart @@ -1,9 +1,7 @@ import 'package:cw_core/transaction_info.dart'; import 'package:cw_core/monero_amount_format.dart'; -import 'package:cw_core/parseBoolFromString.dart'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/format_amount.dart'; -import 'package:cw_monero/api/transaction_history.dart'; class MoneroTransactionInfo extends TransactionInfo { MoneroTransactionInfo(this.txHash, this.height, this.direction, this.date, @@ -11,29 +9,6 @@ class MoneroTransactionInfo extends TransactionInfo { this.confirmations) : id = "${txHash}_${amount}_${accountIndex}_${addressIndex}"; - MoneroTransactionInfo.fromMap(Map map) - : id = "${map['hash']}_${map['amount']}_${map['accountIndex']}_${map['addressIndex']}", - txHash = map['hash'] as String, - height = (map['height'] ?? 0) as int, - direction = map['direction'] != null - ? parseTransactionDirectionFromNumber(map['direction'] as String) - : TransactionDirection.incoming, - date = DateTime.fromMillisecondsSinceEpoch( - (int.tryParse(map['timestamp'] as String? ?? '') ?? 0) * 1000), - isPending = parseBoolFromString(map['isPending'] as String), - amount = map['amount'] as int, - accountIndex = int.parse(map['accountIndex'] as String), - addressIndex = map['addressIndex'] as int, - confirmations = map['confirmations'] as int, - key = getTxKey((map['hash'] ?? '') as String), - fee = map['fee'] as int? ?? 0 { - additionalInfo = { - 'key': key, - 'accountIndex': accountIndex, - 'addressIndex': addressIndex - }; - } - final String id; final String txHash; final int height; diff --git a/cw_monero/lib/monero_wallet.dart b/cw_monero/lib/monero_wallet.dart index e46e18b9f..52ff49b2a 100644 --- a/cw_monero/lib/monero_wallet.dart +++ b/cw_monero/lib/monero_wallet.dart @@ -120,15 +120,20 @@ abstract class MoneroWalletBase @override String get password => _password; + @override + String get passphrase => monero_wallet.getPassphrase(); + @override MoneroWalletKeys get keys => MoneroWalletKeys( primaryAddress: monero_wallet.getAddress(accountIndex: 0, addressIndex: 0), privateSpendKey: monero_wallet.getSecretSpendKey(), privateViewKey: monero_wallet.getSecretViewKey(), publicSpendKey: monero_wallet.getPublicSpendKey(), - publicViewKey: monero_wallet.getPublicViewKey()); + publicViewKey: monero_wallet.getPublicViewKey(), + passphrase: monero_wallet.getPassphrase()); - int? get restoreHeight => transactionHistory.transactions.values.firstOrNull?.height; + int? get restoreHeight => + transactionHistory.transactions.values.firstOrNull?.height ?? monero.Wallet_getRefreshFromBlockHeight(wptr!); monero_wallet.SyncListener? _listener; ReactionDisposer? _onAccountChangeReaction; @@ -172,6 +177,23 @@ abstract class MoneroWalletBase @override Future close({bool shouldCleanup = false}) async { + if (isHardwareWallet) { + disableLedgerExchange(); + final currentWalletDirPath = await pathForWalletDir(name: name, type: type); + if (openedWalletsByPath["$currentWalletDirPath/$name"] != null) { + printV("closing wallet"); + final wmaddr = wmPtr.address; + final waddr = openedWalletsByPath["$currentWalletDirPath/$name"]!.address; + await Isolate.run(() { + monero.WalletManager_closeWallet( + Pointer.fromAddress(wmaddr), Pointer.fromAddress(waddr), true); + }); + openedWalletsByPath.remove("$currentWalletDirPath/$name"); + wptr = null; + printV("wallet closed"); + } + } + _listener?.stop(); _onAccountChangeReaction?.reaction.dispose(); _onTxHistoryChangeReaction?.reaction.dispose(); diff --git a/cw_monero/lib/monero_wallet_service.dart b/cw_monero/lib/monero_wallet_service.dart index 18171a568..fbb1c5331 100644 --- a/cw_monero/lib/monero_wallet_service.dart +++ b/cw_monero/lib/monero_wallet_service.dart @@ -11,6 +11,8 @@ import 'package:cw_core/wallet_credentials.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_service.dart'; import 'package:cw_core/wallet_type.dart'; +import 'package:cw_monero/api/exceptions/wallet_opening_exception.dart'; +import 'package:cw_core/get_height_by_date.dart'; import 'package:cw_monero/api/account_list.dart'; import 'package:cw_monero/api/wallet_manager.dart' as monero_wallet_manager; import 'package:cw_monero/api/wallet_manager.dart'; @@ -24,11 +26,12 @@ import 'package:polyseed/polyseed.dart'; class MoneroNewWalletCredentials extends WalletCredentials { MoneroNewWalletCredentials( - {required String name, required this.language, required this.isPolyseed, String? password}) + {required String name, required this.language, required this.isPolyseed, String? password, this.passphrase}) : super(name: name, password: password); final String language; final bool isPolyseed; + final String? passphrase; } class MoneroRestoreWalletFromHardwareCredentials extends WalletCredentials { @@ -42,10 +45,15 @@ class MoneroRestoreWalletFromHardwareCredentials extends WalletCredentials { class MoneroRestoreWalletFromSeedCredentials extends WalletCredentials { MoneroRestoreWalletFromSeedCredentials( - {required String name, required this.mnemonic, int height = 0, String? password}) + {required String name, + required this.mnemonic, + required this.passphrase, + int height = 0, + String? password}) : super(name: name, password: password, height: height); final String mnemonic; + final String passphrase; } class MoneroWalletLoadingException implements Exception { @@ -85,7 +93,7 @@ class MoneroWalletService extends WalletService< @override WalletType getType() => WalletType.monero; - @override + @override Future create(MoneroNewWalletCredentials credentials, {bool? isTestnet}) async { try { final path = await pathForWallet(name: credentials.name, type: getType()); @@ -94,16 +102,18 @@ class MoneroWalletService extends WalletService< final polyseed = Polyseed.create(); final lang = PolyseedLang.getByEnglishName(credentials.language); + if (credentials.passphrase != null) polyseed.crypt(credentials.passphrase!); + final heightOverride = getMoneroHeigthByDate(date: DateTime.now().subtract(Duration(days: 2))); return _restoreFromPolyseed( path, credentials.password!, polyseed, credentials.walletInfo!, lang, - overrideHeight: heightOverride); + overrideHeight: heightOverride, passphrase: credentials.passphrase); } await monero_wallet_manager.createWallet( - path: path, password: credentials.password!, language: credentials.language); + path: path, password: credentials.password!, language: credentials.language, passphrase: credentials.passphrase??""); final wallet = MoneroWallet( walletInfo: credentials.walletInfo!, unspentCoinsInfo: unspentCoinsInfoSource, @@ -282,16 +292,24 @@ class MoneroWalletService extends WalletService< Future restoreFromSeed( MoneroRestoreWalletFromSeedCredentials credentials, {bool? isTestnet}) async { - // Restore from Polyseed - if (Polyseed.isValidSeed(credentials.mnemonic)) { - return restoreFromPolyseed(credentials); + if (credentials.mnemonic.split(" ").length == 16) { + // Restore from Polyseed + try { + if (Polyseed.isValidSeed(credentials.mnemonic)) { + return restoreFromPolyseed(credentials); + } + } catch (e) { + printV("Polyseed restore failed: $e"); + rethrow; + } } try { final path = await pathForWallet(name: credentials.name, type: getType()); - await monero_wallet_manager.restoreFromSeed( + monero_wallet_manager.restoreFromSeed( path: path, password: credentials.password!, + passphrase: credentials.passphrase, seed: credentials.mnemonic, restoreHeight: credentials.height!); final wallet = MoneroWallet( @@ -318,7 +336,8 @@ class MoneroWalletService extends WalletService< Polyseed.decode(credentials.mnemonic, lang, polyseedCoin); return _restoreFromPolyseed( - path, credentials.password!, polyseed, credentials.walletInfo!, lang); + path, credentials.password!, polyseed, credentials.walletInfo!, lang, + passphrase: credentials.passphrase); } catch (e) { // TODO: Implement Exception for wallet list service. printV('MoneroWalletsManager Error: $e'); @@ -326,9 +345,35 @@ class MoneroWalletService extends WalletService< } } - Future _restoreFromPolyseed(String path, String password, Polyseed polyseed, - WalletInfo walletInfo, PolyseedLang lang, - {PolyseedCoin coin = PolyseedCoin.POLYSEED_MONERO, int? overrideHeight}) async { + Future _restoreFromPolyseed( + String path, String password, Polyseed polyseed, WalletInfo walletInfo, PolyseedLang lang, + {PolyseedCoin coin = PolyseedCoin.POLYSEED_MONERO, + int? overrideHeight, + String? passphrase}) async { + + if (polyseed.isEncrypted == false && + (passphrase??'') != "") { + // Fallback to the different passphrase offset method, when a passphrase + // was provided but the polyseed is not encrypted. + monero_wallet_manager.restoreWalletFromPolyseedWithOffset( + path: path, + password: password, + seed: polyseed.encode(lang, coin), + seedOffset: passphrase??'', + language: "English"); + + final wallet = MoneroWallet( + walletInfo: walletInfo, + unspentCoinsInfo: unspentCoinsInfoSource, + password: password, + ); + await wallet.init(); + + return wallet; + } + + if (polyseed.isEncrypted) polyseed.crypt(passphrase ?? ''); + final height = overrideHeight ?? getMoneroHeigthByDate(date: DateTime.fromMillisecondsSinceEpoch(polyseed.birthday * 1000)); final spendKey = polyseed.generateKey(coin, 32).toHexString(); @@ -345,6 +390,10 @@ class MoneroWalletService extends WalletService< restoreHeight: height, spendKey: spendKey); + + monero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.seed", value: seed); + monero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.passphrase", value: passphrase??''); + final wallet = MoneroWallet( walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfoSource, diff --git a/cw_monero/linux/flutter/ephemeral/.plugin_symlinks/path_provider_linux b/cw_monero/linux/flutter/ephemeral/.plugin_symlinks/path_provider_linux index 17553f81e..1306eaecd 120000 --- a/cw_monero/linux/flutter/ephemeral/.plugin_symlinks/path_provider_linux +++ b/cw_monero/linux/flutter/ephemeral/.plugin_symlinks/path_provider_linux @@ -1 +1 @@ -/Users/omarhatem/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/ \ No newline at end of file +/home/parallels/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/ \ No newline at end of file diff --git a/cw_monero/pubspec.lock b/cw_monero/pubspec.lock index 24be1c0dd..f1510b4bc 100644 --- a/cw_monero/pubspec.lock +++ b/cw_monero/pubspec.lock @@ -225,6 +225,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.10" + decimal: + dependency: transitive + description: + name: decimal + sha256: "24a261d5d5c87e86c7651c417a5dbdf8bcd7080dd592533910e8d0505a279f21" + url: "https://pub.dev" + source: hosted + version: "2.3.3" encrypt: dependency: "direct main" description: @@ -503,8 +511,8 @@ packages: dependency: "direct main" description: path: "impls/monero.dart" - ref: af5277f96073917185864d3596e82b67bee54e78 - resolved-ref: af5277f96073917185864d3596e82b67bee54e78 + ref: "65608c09e9093f1cd42c6afd8e9131016c82574b" + resolved-ref: "65608c09e9093f1cd42c6afd8e9131016c82574b" url: "https://github.com/mrcyjanek/monero_c" source: git version: "0.0.0" @@ -660,6 +668,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + rational: + dependency: transitive + description: + name: rational + sha256: cb808fb6f1a839e6fc5f7d8cb3b0a10e1db48b3be102de73938c627f0b636336 + url: "https://pub.dev" + source: hosted + version: "2.2.3" rxdart: dependency: transitive description: diff --git a/cw_monero/pubspec.yaml b/cw_monero/pubspec.yaml index 61caf93da..6a96e41cc 100644 --- a/cw_monero/pubspec.yaml +++ b/cw_monero/pubspec.yaml @@ -25,8 +25,7 @@ dependencies: monero: git: url: https://github.com/mrcyjanek/monero_c - ref: af5277f96073917185864d3596e82b67bee54e78 -# ref: 6eb571ea498ed7b854934785f00fabfd0dadf75b # monero_c hash + ref: 65608c09e9093f1cd42c6afd8e9131016c82574b path: impls/monero.dart mutex: ^3.1.0 ledger_flutter_plus: ^1.4.1 diff --git a/cw_mweb/ios/.gitignore b/cw_mweb/ios/.gitignore index 0c885071e..7912ef55a 100644 --- a/cw_mweb/ios/.gitignore +++ b/cw_mweb/ios/.gitignore @@ -35,4 +35,6 @@ Icon? /Flutter/Generated.xcconfig /Flutter/ephemeral/ -/Flutter/flutter_export_environment.sh \ No newline at end of file +/Flutter/flutter_export_environment.sh + +Mwebd.xcframework \ No newline at end of file diff --git a/cw_mweb/ios/cw_mweb.podspec b/cw_mweb/ios/cw_mweb.podspec index 4a1903bae..19857c5ee 100644 --- a/cw_mweb/ios/cw_mweb.podspec +++ b/cw_mweb/ios/cw_mweb.podspec @@ -16,11 +16,12 @@ A new Flutter plugin project. s.source_files = 'Classes/**/*' s.dependency 'Flutter' s.platform = :ios, '11.0' + s.libraries = 'resolv' # Flutter.framework does not contain a i386 slice. s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } s.swift_version = '5.0' - s.ios.vendored_frameworks = 'Mwebd.xcframework' + s.vendored_frameworks = 'Mwebd.xcframework' s.preserve_paths = 'Mwebd.xcframework/**/*' end diff --git a/cw_wownero/lib/api/transaction_history.dart b/cw_wownero/lib/api/transaction_history.dart index 3cebdd811..10bfb535e 100644 --- a/cw_wownero/lib/api/transaction_history.dart +++ b/cw_wownero/lib/api/transaction_history.dart @@ -10,7 +10,7 @@ import 'package:cw_wownero/exceptions/wownero_transaction_creation_exception.dar import 'package:ffi/ffi.dart'; import 'package:monero/wownero.dart' as wownero; import 'package:monero/src/generated_bindings_wownero.g.dart' as wownero_gen; - +import 'package:mutex/mutex.dart'; String getTxKey(String txId) { final ret = wownero.Wallet_getTxKey(wptr!, txid: txId); @@ -18,6 +18,7 @@ String getTxKey(String txId) { return ret; } +final txHistoryMutex = Mutex(); wownero.TransactionHistory? txhistory; bool isRefreshingTx = false; @@ -26,22 +27,25 @@ Future refreshTransactions() async { isRefreshingTx = true; txhistory ??= wownero.Wallet_history(wptr!); final ptr = txhistory!.address; + await txHistoryMutex.acquire(); await Isolate.run(() { wownero.TransactionHistory_refresh(Pointer.fromAddress(ptr)); }); + txHistoryMutex.release(); isRefreshingTx = false; } int countOfTransactions() => wownero.TransactionHistory_count(txhistory!); -List getAllTransactions() { +Future> getAllTransactions() async { List dummyTxs = []; + await txHistoryMutex.acquire(); txhistory ??= wownero.Wallet_history(wptr!); - wownero.TransactionHistory_refresh(txhistory!); int size = countOfTransactions(); final list = List.generate(size, (index) => Transaction(txInfo: wownero.TransactionHistory_transaction(txhistory!, index: index))); - + txHistoryMutex.release(); + final accts = wownero.Wallet_numSubaddressAccounts(wptr!); for (var i = 0; i < accts; i++) { final fullBalance = wownero.Wallet_balance(wptr!, accountIndex: i); diff --git a/cw_wownero/lib/api/wallet.dart b/cw_wownero/lib/api/wallet.dart index fb031c489..8375f6f1f 100644 --- a/cw_wownero/lib/api/wallet.dart +++ b/cw_wownero/lib/api/wallet.dart @@ -7,6 +7,7 @@ import 'package:cw_wownero/api/account_list.dart'; import 'package:cw_wownero/api/exceptions/setup_wallet_exception.dart'; import 'package:monero/wownero.dart' as wownero; import 'package:mutex/mutex.dart'; +import 'package:polyseed/polyseed.dart'; int getSyncingHeight() { // final height = wownero.WOWNERO_cw_WalletListener_height(getWlptr()); @@ -32,21 +33,36 @@ bool isNewTransactionExist() { String getFilename() => wownero.Wallet_filename(wptr!); String getSeed() { - // wownero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.seed", value: seed); - final cakepolyseed = wownero.Wallet_getCacheAttribute(wptr!, key: "cakewallet.seed"); + // monero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.seed", value: seed); + final cakepolyseed = + wownero.Wallet_getCacheAttribute(wptr!, key: "cakewallet.seed"); + final cakepassphrase = getPassphrase(); + + final weirdPolyseed = wownero.Wallet_getPolyseed(wptr!, passphrase: cakepassphrase); + if (weirdPolyseed != "") return weirdPolyseed; if (cakepolyseed != "") { + if (cakepassphrase != "") { + try { + final lang = PolyseedLang.getByPhrase(cakepolyseed); + final coin = PolyseedCoin.POLYSEED_WOWNERO; + final ps = Polyseed.decode(cakepolyseed, lang, coin); + final passphrase = getPassphrase(); + if (ps.isEncrypted || passphrase == "") return ps.encode(lang, coin); + ps.crypt(passphrase); + return ps.encode(lang, coin); + } catch (e) { + printV(e); + } + } return cakepolyseed; } - final polyseed = wownero.Wallet_getPolyseed(wptr!, passphrase: ''); - if (polyseed != "") { - return polyseed; - } final legacy = getSeedLegacy(null); return legacy; } String getSeedLegacy(String? language) { - var legacy = wownero.Wallet_seed(wptr!, seedOffset: ''); + final cakepassphrase = getPassphrase(); + var legacy = wownero.Wallet_seed(wptr!, seedOffset: cakepassphrase); switch (language) { case "Chinese (Traditional)": language = "Chinese (simplified)"; @@ -66,7 +82,7 @@ String getSeedLegacy(String? language) { } if (wownero.Wallet_status(wptr!) != 0) { wownero.Wallet_setSeedLanguage(wptr!, language: language ?? "English"); - legacy = wownero.Wallet_seed(wptr!, seedOffset: ''); + legacy = wownero.Wallet_seed(wptr!, seedOffset: cakepassphrase); } if (wownero.Wallet_status(wptr!) != 0) { final err = wownero.Wallet_errorString(wptr!); @@ -80,6 +96,10 @@ String getSeedLegacy(String? language) { Map>> addressCache = {}; +String getPassphrase() { + return wownero.Wallet_getCacheAttribute(wptr!, key: "cakewallet.passphrase"); +} + String getAddress({int accountIndex = 0, int addressIndex = 1}) { while (wownero.Wallet_numSubaddresses(wptr!, accountIndex: accountIndex) - 1 < addressIndex) { printV("adding subaddress"); @@ -357,6 +377,5 @@ String signMessage(String message, {String address = ""}) { } bool verifyMessage(String message, String address, String signature) { - return wownero.Wallet_verifySignedMessage(wptr!, - message: message, address: address, signature: signature); + return wownero.Wallet_verifySignedMessage(wptr!, message: message, address: address, signature: signature); } diff --git a/cw_wownero/lib/api/wallet_manager.dart b/cw_wownero/lib/api/wallet_manager.dart index 6681652db..8d745ef8e 100644 --- a/cw_wownero/lib/api/wallet_manager.dart +++ b/cw_wownero/lib/api/wallet_manager.dart @@ -66,6 +66,7 @@ void createWalletSync( {required String path, required String password, required String language, + required String passphrase, int nettype = 0}) { txhistory = null; final newWptr = wownero.WalletManager_createWallet(wmPtr, @@ -76,6 +77,8 @@ void createWalletSync( throw WalletCreationException(message: wownero.Wallet_errorString(newWptr)); } wptr = newWptr; + wownero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.passphrase", value: passphrase); + wownero.Wallet_store(wptr!, path: path); openedWalletsByPath[path] = wptr!; @@ -90,6 +93,7 @@ bool isWalletExistSync({required String path}) { void restoreWalletFromSeedSync( {required String path, required String password, + required String passphrase, required String seed, int nettype = 0, int restoreHeight = 0}) { @@ -102,10 +106,12 @@ void restoreWalletFromSeedSync( language: seed, // I KNOW - this is supposed to be called seed networkType: 0, ); - + final oldwptr = wptr; + wptr = newWptr; setRefreshFromBlockHeight( height: wownero.WOWNERO_deprecated_14WordSeedHeight(seed: seed), ); + wptr = oldwptr; } else { txhistory = null; newWptr = wownero.WalletManager_recoveryWallet( @@ -114,7 +120,7 @@ void restoreWalletFromSeedSync( password: password, mnemonic: seed, restoreHeight: restoreHeight, - seedOffset: '', + seedOffset: passphrase, networkType: 0, ); } @@ -127,8 +133,13 @@ void restoreWalletFromSeedSync( } wptr = newWptr; - + + wownero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.passphrase", value: passphrase); + wownero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.seed", value: seed); + openedWalletsByPath[path] = wptr!; + + store(); } void restoreWalletFromKeysSync( @@ -196,6 +207,48 @@ void restoreWalletFromKeysSync( openedWalletsByPath[path] = wptr!; } + + +// English only, because normalization. +void restoreWalletFromPolyseedWithOffset( + {required String path, + required String password, + required String seed, + required String seedOffset, + required String language, + int nettype = 0}) { + + txhistory = null; + final newWptr = wownero.WalletManager_createWalletFromPolyseed( + wmPtr, + path: path, + password: password, + networkType: nettype, + mnemonic: seed, + seedOffset: seedOffset, + newWallet: true, // safe to remove + restoreHeight: 0, + kdfRounds: 1, + ); + + final status = wownero.Wallet_status(newWptr); + + if (status != 0) { + final err = wownero.Wallet_errorString(newWptr); + printV("err: $err"); + throw WalletRestoreFromKeysException(message: err); + } + + wptr = newWptr; + + wownero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.seed", value: seed); + wownero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.passphrase", value: seedOffset); + + storeSync(); + + openedWalletsByPath[path] = wptr!; +} + void restoreWalletFromSpendKeySync( {required String path, required String password, @@ -312,18 +365,20 @@ void _createWallet(Map args) { final path = args['path'] as String; final password = args['password'] as String; final language = args['language'] as String; + final passphrase = args['passphrase'] as String; - createWalletSync(path: path, password: password, language: language); + createWalletSync(path: path, password: password, language: language, passphrase: passphrase); } void _restoreFromSeed(Map args) { final path = args['path'] as String; final password = args['password'] as String; + final passphrase = args['passphrase'] as String; final seed = args['seed'] as String; final restoreHeight = args['restoreHeight'] as int; restoreWalletFromSeedSync( - path: path, password: password, seed: seed, restoreHeight: restoreHeight); + path: path, password: password, passphrase: passphrase, seed: seed, restoreHeight: restoreHeight); } void _restoreFromKeys(Map args) { @@ -380,23 +435,27 @@ Future createWallet( {required String path, required String password, required String language, + required String passphrase, int nettype = 0}) async => _createWallet({ 'path': path, 'password': password, 'language': language, + 'passphrase': passphrase, 'nettype': nettype }); Future restoreFromSeed( {required String path, required String password, + required String passphrase, required String seed, int nettype = 0, int restoreHeight = 0}) async => _restoreFromSeed({ 'path': path, 'password': password, + 'passphrase': passphrase, 'seed': seed, 'nettype': nettype, 'restoreHeight': restoreHeight diff --git a/cw_wownero/lib/wownero_wallet.dart b/cw_wownero/lib/wownero_wallet.dart index 963edf8fa..776ac35e3 100644 --- a/cw_wownero/lib/wownero_wallet.dart +++ b/cw_wownero/lib/wownero_wallet.dart @@ -20,6 +20,7 @@ import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wownero_amount_format.dart'; import 'package:cw_core/wownero_balance.dart'; +import 'package:cw_wownero/api/account_list.dart'; import 'package:cw_wownero/api/coins_info.dart'; import 'package:cw_wownero/api/structs/pending_transaction.dart'; import 'package:cw_wownero/api/transaction_history.dart' as transaction_history; @@ -119,6 +120,9 @@ abstract class WowneroWalletBase String get password => _password; + @override + String get passphrase => wownero_wallet.getPassphrase(); + String _password; @override @@ -127,7 +131,12 @@ abstract class WowneroWalletBase privateSpendKey: wownero_wallet.getSecretSpendKey(), privateViewKey: wownero_wallet.getSecretViewKey(), publicSpendKey: wownero_wallet.getPublicSpendKey(), - publicViewKey: wownero_wallet.getPublicViewKey()); + publicViewKey: wownero_wallet.getPublicViewKey(), + passphrase: wownero_wallet.getPassphrase()); + + int? get restoreHeight => + transactionHistory.transactions.values.firstOrNull?.height ?? wownero.Wallet_getRefreshFromBlockHeight(wptr!); + wownero_wallet.SyncListener? _listener; ReactionDisposer? _onAccountChangeReaction; @@ -571,7 +580,7 @@ abstract class WowneroWalletBase @override Future> fetchTransactions() async { transaction_history.refreshTransactions(); - return _getAllTransactionsOfAccount(walletAddresses.account?.id) + return (await _getAllTransactionsOfAccount(walletAddresses.account?.id)) .fold>({}, (Map acc, WowneroTransactionInfo tx) { acc[tx.id] = tx; @@ -600,9 +609,9 @@ abstract class WowneroWalletBase String getSubaddressLabel(int accountIndex, int addressIndex) => wownero_wallet.getSubaddressLabel(accountIndex, addressIndex); - List _getAllTransactionsOfAccount(int? accountIndex) => - transaction_history - .getAllTransactions() + Future> _getAllTransactionsOfAccount(int? accountIndex) async => + (await transaction_history + .getAllTransactions()) .map( (row) => WowneroTransactionInfo( row.hash, diff --git a/cw_wownero/lib/wownero_wallet_service.dart b/cw_wownero/lib/wownero_wallet_service.dart index 1cd462cd9..72eefabd2 100644 --- a/cw_wownero/lib/wownero_wallet_service.dart +++ b/cw_wownero/lib/wownero_wallet_service.dart @@ -10,6 +10,7 @@ import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_service.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:cw_core/get_height_by_date.dart'; +import 'package:cw_wownero/api/account_list.dart'; import 'package:cw_wownero/api/exceptions/wallet_opening_exception.dart'; import 'package:cw_wownero/api/wallet_manager.dart' as wownero_wallet_manager; import 'package:cw_wownero/api/wallet_manager.dart'; @@ -21,19 +22,21 @@ import 'package:monero/wownero.dart' as wownero; class WowneroNewWalletCredentials extends WalletCredentials { WowneroNewWalletCredentials( - {required String name, required this.language, required this.isPolyseed, String? password}) + {required String name, required this.language, required this.isPolyseed, this.passphrase, String? password}) : super(name: name, password: password); final String language; final bool isPolyseed; + final String? passphrase; } class WowneroRestoreWalletFromSeedCredentials extends WalletCredentials { WowneroRestoreWalletFromSeedCredentials( - {required String name, required this.mnemonic, int height = 0, String? password}) + {required String name, required this.mnemonic, required this.passphrase, int height = 0, String? password}) : super(name: name, password: password, height: height); final String mnemonic; + final String passphrase; } class WowneroWalletLoadingException implements Exception { @@ -83,16 +86,18 @@ class WowneroWalletService extends WalletService< final polyseed = Polyseed.create(); final lang = PolyseedLang.getByEnglishName(credentials.language); + if (credentials.passphrase != null) polyseed.crypt(credentials.passphrase!); + final heightOverride = getWowneroHeightByDate(date: DateTime.now().subtract(Duration(days: 2))); return _restoreFromPolyseed( path, credentials.password!, polyseed, credentials.walletInfo!, lang, - overrideHeight: heightOverride); + overrideHeight: heightOverride, passphrase: credentials.passphrase); } await wownero_wallet_manager.createWallet( - path: path, password: credentials.password!, language: credentials.language); + path: path, password: credentials.password!, language: credentials.language, passphrase: credentials.passphrase??''); final wallet = WowneroWallet( walletInfo: credentials.walletInfo!, unspentCoinsInfo: unspentCoinsInfoSource, password: credentials.password!); await wallet.init(); @@ -266,6 +271,7 @@ class WowneroWalletService extends WalletService< await wownero_wallet_manager.restoreFromSeed( path: path, password: credentials.password!, + passphrase: credentials.passphrase, seed: credentials.mnemonic, restoreHeight: credentials.height!); final wallet = WowneroWallet( @@ -289,7 +295,7 @@ class WowneroWalletService extends WalletService< final polyseed = Polyseed.decode(credentials.mnemonic, lang, polyseedCoin); return _restoreFromPolyseed( - path, credentials.password!, polyseed, credentials.walletInfo!, lang); + path, credentials.password!, polyseed, credentials.walletInfo!, lang, passphrase: credentials.passphrase); } catch (e) { // TODO: Implement Exception for wallet list service. printV('WowneroWalletsManager Error: $e'); @@ -299,7 +305,32 @@ class WowneroWalletService extends WalletService< Future _restoreFromPolyseed( String path, String password, Polyseed polyseed, WalletInfo walletInfo, PolyseedLang lang, - {PolyseedCoin coin = PolyseedCoin.POLYSEED_WOWNERO, int? overrideHeight}) async { + {PolyseedCoin coin = PolyseedCoin.POLYSEED_WOWNERO, int? overrideHeight, String? passphrase}) async { + + + if (polyseed.isEncrypted == false && + (passphrase??'') != "") { + // Fallback to the different passphrase offset method, when a passphrase + // was provided but the polyseed is not encrypted. + wownero_wallet_manager.restoreWalletFromPolyseedWithOffset( + path: path, + password: password, + seed: polyseed.encode(lang, coin), + seedOffset: passphrase??'', + language: "English"); + + final wallet = WowneroWallet( + walletInfo: walletInfo, + unspentCoinsInfo: unspentCoinsInfoSource, + password: password, + ); + await wallet.init(); + + return wallet; + } + + if (polyseed.isEncrypted) polyseed.crypt(passphrase ?? ''); + final height = overrideHeight ?? getWowneroHeightByDate(date: DateTime.fromMillisecondsSinceEpoch(polyseed.birthday * 1000)); final spendKey = polyseed.generateKey(coin, 32).toHexString(); @@ -316,6 +347,9 @@ class WowneroWalletService extends WalletService< restoreHeight: height, spendKey: spendKey); + wownero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.seed", value: seed); + wownero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.passphrase", value: passphrase??''); + final wallet = WowneroWallet(walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfoSource, password: password); await wallet.init(); diff --git a/cw_wownero/pubspec.lock b/cw_wownero/pubspec.lock index 1e16fa089..44182ed9f 100644 --- a/cw_wownero/pubspec.lock +++ b/cw_wownero/pubspec.lock @@ -209,6 +209,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.4" + decimal: + dependency: transitive + description: + name: decimal + sha256: "24a261d5d5c87e86c7651c417a5dbdf8bcd7080dd592533910e8d0505a279f21" + url: "https://pub.dev" + source: hosted + version: "2.3.3" encrypt: dependency: "direct main" description: @@ -463,8 +471,8 @@ packages: dependency: "direct main" description: path: "impls/monero.dart" - ref: af5277f96073917185864d3596e82b67bee54e78 - resolved-ref: af5277f96073917185864d3596e82b67bee54e78 + ref: "65608c09e9093f1cd42c6afd8e9131016c82574b" + resolved-ref: "65608c09e9093f1cd42c6afd8e9131016c82574b" url: "https://github.com/mrcyjanek/monero_c" source: git version: "0.0.0" @@ -612,6 +620,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + rational: + dependency: transitive + description: + name: rational + sha256: cb808fb6f1a839e6fc5f7d8cb3b0a10e1db48b3be102de73938c627f0b636336 + url: "https://pub.dev" + source: hosted + version: "2.2.3" shelf: dependency: transitive description: diff --git a/cw_wownero/pubspec.yaml b/cw_wownero/pubspec.yaml index a92f530f6..a57c2f25b 100644 --- a/cw_wownero/pubspec.yaml +++ b/cw_wownero/pubspec.yaml @@ -25,8 +25,7 @@ dependencies: monero: git: url: https://github.com/mrcyjanek/monero_c - ref: af5277f96073917185864d3596e82b67bee54e78 -# ref: 6eb571ea498ed7b854934785f00fabfd0dadf75b # monero_c hash + ref: 65608c09e9093f1cd42c6afd8e9131016c82574b # monero_c hash path: impls/monero.dart mutex: ^3.1.0 diff --git a/cw_zano/.gitignore b/cw_zano/.gitignore new file mode 100644 index 000000000..e9dc58d3d --- /dev/null +++ b/cw_zano/.gitignore @@ -0,0 +1,7 @@ +.DS_Store +.dart_tool/ + +.packages +.pub/ + +build/ diff --git a/cw_zano/.metadata b/cw_zano/.metadata new file mode 100644 index 000000000..cb1a29e7c --- /dev/null +++ b/cw_zano/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 4d7946a68d26794349189cf21b3f68cc6fe61dcb + channel: stable + +project_type: plugin diff --git a/cw_zano/CHANGELOG.md b/cw_zano/CHANGELOG.md new file mode 100644 index 000000000..41cc7d819 --- /dev/null +++ b/cw_zano/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/cw_zano/LICENSE b/cw_zano/LICENSE new file mode 100644 index 000000000..ba75c69f7 --- /dev/null +++ b/cw_zano/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/cw_zano/README.md b/cw_zano/README.md new file mode 100644 index 000000000..4a297e8e3 --- /dev/null +++ b/cw_zano/README.md @@ -0,0 +1,15 @@ +# cw_zano + +A new flutter plugin project. + +## Getting Started + +This project is a starting point for a Flutter +[plug-in package](https://flutter.dev/developing-packages/), +a specialized package that includes platform-specific implementation code for +Android and/or iOS. + +For help getting started with Flutter, view our +[online documentation](https://flutter.dev/docs), which offers tutorials, +samples, guidance on mobile development, and a full API reference. + diff --git a/cw_zano/lib/api/consts.dart b/cw_zano/lib/api/consts.dart new file mode 100644 index 000000000..80002b880 --- /dev/null +++ b/cw_zano/lib/api/consts.dart @@ -0,0 +1,6 @@ +class Consts { + static const errorWrongSeed = 'WRONG_SEED'; + static const errorAlreadyExists = 'ALREADY_EXISTS'; + static const errorWalletWrongId = 'WALLET_WRONG_ID'; + static const errorBusy = 'BUSY'; +} \ No newline at end of file diff --git a/cw_zano/lib/api/model/asset_id_params.dart b/cw_zano/lib/api/model/asset_id_params.dart new file mode 100644 index 000000000..3856f5f4e --- /dev/null +++ b/cw_zano/lib/api/model/asset_id_params.dart @@ -0,0 +1,9 @@ +class AssetIdParams { + final String assetId; + + AssetIdParams({required this.assetId}); + + Map toJson() => { + 'asset_id': assetId, + }; +} \ No newline at end of file diff --git a/cw_zano/lib/api/model/balance.dart b/cw_zano/lib/api/model/balance.dart new file mode 100644 index 000000000..b796338fb --- /dev/null +++ b/cw_zano/lib/api/model/balance.dart @@ -0,0 +1,32 @@ +import 'package:cw_core/zano_asset.dart'; +import 'package:cw_zano/model/zano_asset.dart'; +import 'package:cw_zano/zano_formatter.dart'; + +class Balance { + final ZanoAsset assetInfo; + final BigInt awaitingIn; + final BigInt awaitingOut; + final BigInt total; + final BigInt unlocked; + + Balance( + {required this.assetInfo, + required this.awaitingIn, + required this.awaitingOut, + required this.total, + required this.unlocked}); + + String get assetId => assetInfo.assetId; + + @override + String toString() => '$assetInfo: $total/$unlocked'; + + factory Balance.fromJson(Map json) => Balance( + assetInfo: + ZanoAsset.fromJson(json['asset_info'] as Map? ?? {}), + awaitingIn: ZanoFormatter.bigIntFromDynamic(json['awaiting_in']), + awaitingOut: ZanoFormatter.bigIntFromDynamic(json['awaiting_out']), + total: ZanoFormatter.bigIntFromDynamic(json['total']), + unlocked: ZanoFormatter.bigIntFromDynamic(json['unlocked']), + ); +} diff --git a/cw_zano/lib/api/model/create_wallet_result.dart b/cw_zano/lib/api/model/create_wallet_result.dart new file mode 100644 index 000000000..236911db1 --- /dev/null +++ b/cw_zano/lib/api/model/create_wallet_result.dart @@ -0,0 +1,52 @@ +import 'package:cw_zano/api/model/recent_history.dart'; +import 'package:cw_zano/api/model/wi.dart'; +import 'package:cw_zano/zano_wallet.dart'; + +class CreateWalletResult { + final String name; + final String pass; + final RecentHistory recentHistory; + final bool recovered; + final int walletFileSize; + final int walletId; + final int walletLocalBcSize; + final Wi wi; + final String privateSpendKey; + final String privateViewKey; + final String publicSpendKey; + final String publicViewKey; + + CreateWalletResult( + {required this.name, + required this.pass, + required this.recentHistory, + required this.recovered, + required this.walletFileSize, + required this.walletId, + required this.walletLocalBcSize, + required this.wi, + required this.privateSpendKey, + required this.privateViewKey, + required this.publicSpendKey, + required this.publicViewKey}); + + factory CreateWalletResult.fromJson(Map json) => + CreateWalletResult( + name: json['name'] as String? ?? '', + pass: json['pass'] as String? ?? '', + recentHistory: RecentHistory.fromJson( + json['recent_history'] as Map? ?? {}), + recovered: json['recovered'] as bool? ?? false, + walletFileSize: json['wallet_file_size'] as int? ?? 0, + walletId: json['wallet_id'] as int? ?? 0, + walletLocalBcSize: json['wallet_local_bc_size'] as int? ?? 0, + wi: Wi.fromJson(json['wi'] as Map? ?? {}), + privateSpendKey: json['private_spend_key'] as String? ?? '', + privateViewKey: json['private_view_key'] as String? ?? '', + publicSpendKey: json['public_spend_key'] as String? ?? '', + publicViewKey: json['public_view_key'] as String? ?? '', + ); + Future seed(ZanoWalletBase api) { + return api.getSeed(); + } +} diff --git a/cw_zano/lib/api/model/destination.dart b/cw_zano/lib/api/model/destination.dart new file mode 100644 index 000000000..3db4f6652 --- /dev/null +++ b/cw_zano/lib/api/model/destination.dart @@ -0,0 +1,20 @@ +class Destination { + final BigInt amount; // transfered as string + final String address; + final String assetId; + + Destination( + {required this.amount, required this.address, required this.assetId}); + + factory Destination.fromJson(Map json) => Destination( + amount: BigInt.parse(json['amount'] as String? ?? '0'), + address: json['address'] as String? ?? '', + assetId: json['asset_id'] as String? ?? '', + ); + + Map toJson() => { + 'amount': amount.toString(), + 'address': address, + 'asset_id': assetId, + }; +} diff --git a/cw_zano/lib/api/model/employed_entries.dart b/cw_zano/lib/api/model/employed_entries.dart new file mode 100644 index 000000000..59e5fe34d --- /dev/null +++ b/cw_zano/lib/api/model/employed_entries.dart @@ -0,0 +1,18 @@ +import 'package:cw_zano/api/model/receive.dart'; + +class EmployedEntries { + final List receive; + final List send; + + EmployedEntries({required this.receive, required this.send}); + + factory EmployedEntries.fromJson(Map json) => + EmployedEntries( + receive: json['receive'] == null ? [] : (json['receive'] as List) + .map((e) => Receive.fromJson(e as Map)) + .toList(), + send: json['spent'] == null ? [] : (json['spent'] as List) + .map((e) => Receive.fromJson(e as Map)) + .toList(), + ); +} diff --git a/cw_zano/lib/api/model/get_address_info_result.dart b/cw_zano/lib/api/model/get_address_info_result.dart new file mode 100644 index 000000000..e8399adb1 --- /dev/null +++ b/cw_zano/lib/api/model/get_address_info_result.dart @@ -0,0 +1,16 @@ +class GetAddressInfoResult { + final bool valid; + final bool auditable; + final bool paymentId; + final bool wrap; + + GetAddressInfoResult( + {required this.valid, required this.auditable, required this.paymentId, required this.wrap}); + + factory GetAddressInfoResult.fromJson(Map json) => GetAddressInfoResult( + valid: json['valid'] as bool? ?? false, + auditable: json['auditable'] as bool? ?? false, + paymentId: json['payment_id'] as bool? ?? false, + wrap: json['wrap'] as bool? ?? false, + ); +} diff --git a/cw_zano/lib/api/model/get_recent_txs_and_info_params.dart b/cw_zano/lib/api/model/get_recent_txs_and_info_params.dart new file mode 100644 index 000000000..1ad9fc155 --- /dev/null +++ b/cw_zano/lib/api/model/get_recent_txs_and_info_params.dart @@ -0,0 +1,14 @@ +class GetRecentTxsAndInfoParams { + final int offset; + final int count; + final bool updateProvisionInfo; + + GetRecentTxsAndInfoParams({required this.offset, required this.count, this.updateProvisionInfo = true}); + + Map toJson() => { + 'offset': offset, + 'count': count, + 'update_provision_info': updateProvisionInfo, + 'order': 'FROM_BEGIN_TO_END', + }; +} \ No newline at end of file diff --git a/cw_zano/lib/api/model/get_recent_txs_and_info_result.dart b/cw_zano/lib/api/model/get_recent_txs_and_info_result.dart new file mode 100644 index 000000000..6b725490c --- /dev/null +++ b/cw_zano/lib/api/model/get_recent_txs_and_info_result.dart @@ -0,0 +1,12 @@ +import 'package:cw_zano/api/model/transfer.dart'; + +class GetRecentTxsAndInfoResult { + final List transfers; + final int lastItemIndex; + final int totalTransfers; + + GetRecentTxsAndInfoResult({required this.transfers, required this.lastItemIndex, required this.totalTransfers}); + + GetRecentTxsAndInfoResult.empty(): this.transfers = [], this.lastItemIndex = 0, this.totalTransfers = 0; + +} \ No newline at end of file diff --git a/cw_zano/lib/api/model/get_wallet_info_result.dart b/cw_zano/lib/api/model/get_wallet_info_result.dart new file mode 100644 index 000000000..e14d19375 --- /dev/null +++ b/cw_zano/lib/api/model/get_wallet_info_result.dart @@ -0,0 +1,14 @@ +import 'package:cw_zano/api/model/wi.dart'; +import 'package:cw_zano/api/model/wi_extended.dart'; + +class GetWalletInfoResult { + final Wi wi; + final WiExtended wiExtended; + + GetWalletInfoResult({required this.wi, required this.wiExtended}); + + factory GetWalletInfoResult.fromJson(Map json) => GetWalletInfoResult( + wi: Wi.fromJson(json['wi'] as Map? ?? {}), + wiExtended: WiExtended.fromJson(json['wi_extended'] as Map? ?? {}), + ); +} diff --git a/cw_zano/lib/api/model/get_wallet_status_result.dart b/cw_zano/lib/api/model/get_wallet_status_result.dart new file mode 100644 index 000000000..da11c4c93 --- /dev/null +++ b/cw_zano/lib/api/model/get_wallet_status_result.dart @@ -0,0 +1,26 @@ +class GetWalletStatusResult { + final int currentDaemonHeight; + final int currentWalletHeight; + final bool isDaemonConnected; + final bool isInLongRefresh; + final int progress; + final int walletState; + + GetWalletStatusResult( + {required this.currentDaemonHeight, + required this.currentWalletHeight, + required this.isDaemonConnected, + required this.isInLongRefresh, + required this.progress, + required this.walletState}); + + factory GetWalletStatusResult.fromJson(Map json) => + GetWalletStatusResult( + currentDaemonHeight: json['current_daemon_height'] as int? ?? 0, + currentWalletHeight: json['current_wallet_height'] as int? ?? 0, + isDaemonConnected: json['is_daemon_connected'] as bool? ?? false, + isInLongRefresh: json['is_in_long_refresh'] as bool? ?? false, + progress: json['progress'] as int? ?? 0, + walletState: json['wallet_state'] as int? ?? 0, + ); +} diff --git a/cw_zano/lib/api/model/proxy_to_daemon_params.dart b/cw_zano/lib/api/model/proxy_to_daemon_params.dart new file mode 100644 index 000000000..328187cfa --- /dev/null +++ b/cw_zano/lib/api/model/proxy_to_daemon_params.dart @@ -0,0 +1,13 @@ +import 'dart:convert'; + +class ProxyToDaemonParams { + final String body; + final String uri; + + ProxyToDaemonParams({required this.body, required this.uri}); + + Map toJson() => { + 'base64_body': base64Encode(utf8.encode(body)), + 'uri': uri, + }; +} diff --git a/cw_zano/lib/api/model/proxy_to_daemon_result.dart b/cw_zano/lib/api/model/proxy_to_daemon_result.dart new file mode 100644 index 000000000..bf8da7c8d --- /dev/null +++ b/cw_zano/lib/api/model/proxy_to_daemon_result.dart @@ -0,0 +1,13 @@ +import 'dart:convert'; + +class ProxyToDaemonResult { + final String body; + final int responseCode; + + ProxyToDaemonResult({required this.body, required this.responseCode}); + + factory ProxyToDaemonResult.fromJson(Map json) => ProxyToDaemonResult( + body: utf8.decode(base64Decode(json['base64_body'] as String? ?? '')), + responseCode: json['response_code'] as int? ?? 0, + ); +} diff --git a/cw_zano/lib/api/model/receive.dart b/cw_zano/lib/api/model/receive.dart new file mode 100644 index 000000000..6364bf181 --- /dev/null +++ b/cw_zano/lib/api/model/receive.dart @@ -0,0 +1,15 @@ +import 'package:cw_zano/zano_formatter.dart'; + +class Receive { + final BigInt amount; + final String assetId; + final int index; + + Receive({required this.amount, required this.assetId, required this.index}); + + factory Receive.fromJson(Map json) => Receive( + amount: ZanoFormatter.bigIntFromDynamic(json['amount']), + assetId: json['asset_id'] as String? ?? '', + index: json['index'] as int? ?? 0, + ); +} diff --git a/cw_zano/lib/api/model/recent_history.dart b/cw_zano/lib/api/model/recent_history.dart new file mode 100644 index 000000000..6591f426d --- /dev/null +++ b/cw_zano/lib/api/model/recent_history.dart @@ -0,0 +1,20 @@ +import 'package:cw_zano/api/model/transfer.dart'; + +class RecentHistory { + final List? history; + final int lastItemIndex; + final int totalHistoryItems; + + RecentHistory( + {required this.history, + required this.lastItemIndex, + required this.totalHistoryItems}); + + factory RecentHistory.fromJson(Map json) => RecentHistory( + history: json['history'] == null ? null : (json['history'] as List) + .map((e) => Transfer.fromJson(e as Map)) + .toList(), + lastItemIndex: json['last_item_index'] as int? ?? 0, + totalHistoryItems: json['total_history_items'] as int? ?? 0, + ); +} diff --git a/cw_zano/lib/api/model/store_result.dart b/cw_zano/lib/api/model/store_result.dart new file mode 100644 index 000000000..0ff6625c1 --- /dev/null +++ b/cw_zano/lib/api/model/store_result.dart @@ -0,0 +1,9 @@ +class StoreResult { + final int walletFileSize; + + StoreResult({required this.walletFileSize}); + + factory StoreResult.fromJson(Map json) => StoreResult( + walletFileSize: json['wallet_file_size'] as int? ?? 0, + ); +} \ No newline at end of file diff --git a/cw_zano/lib/api/model/subtransfer.dart b/cw_zano/lib/api/model/subtransfer.dart new file mode 100644 index 000000000..d92f1407a --- /dev/null +++ b/cw_zano/lib/api/model/subtransfer.dart @@ -0,0 +1,16 @@ +import 'package:cw_zano/zano_formatter.dart'; + +class Subtransfer { + final BigInt amount; + final String assetId; + final bool isIncome; + + Subtransfer( + {required this.amount, required this.assetId, required this.isIncome}); + + factory Subtransfer.fromJson(Map json) => Subtransfer( + amount: ZanoFormatter.bigIntFromDynamic(json['amount']), + assetId: json['asset_id'] as String? ?? '', + isIncome: json['is_income'] as bool? ?? false, + ); +} diff --git a/cw_zano/lib/api/model/transfer.dart b/cw_zano/lib/api/model/transfer.dart new file mode 100644 index 000000000..594490a4f --- /dev/null +++ b/cw_zano/lib/api/model/transfer.dart @@ -0,0 +1,133 @@ +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_core/zano_asset.dart'; +import 'package:cw_zano/api/model/employed_entries.dart'; +import 'package:cw_zano/api/model/subtransfer.dart'; +import 'package:collection/collection.dart'; +import 'package:cw_zano/model/zano_transaction_info.dart'; +import 'package:cw_zano/zano_wallet.dart'; + +class Transfer { + final String comment; + final EmployedEntries employedEntries; + final int fee; + final int height; + final bool isMining; + final bool isMixing; + final bool isService; + final String paymentId; + final List remoteAddresses; + final List remoteAliases; + final bool showSender; + final List subtransfers; + final int timestamp; + final int transferInternalIndex; + final int txBlobSize; + final String txHash; + final int txType; + final int unlockTime; + + Transfer({ + required this.comment, + required this.employedEntries, + required this.fee, + required this.height, + required this.isMining, + required this.isMixing, + required this.isService, + required this.paymentId, + required this.remoteAddresses, + required this.remoteAliases, + required this.showSender, + required this.subtransfers, + required this.timestamp, + required this.transferInternalIndex, + required this.txBlobSize, + required this.txHash, + required this.txType, + required this.unlockTime, + }); + + factory Transfer.fromJson(Map json) => + Transfer( + comment: json['comment'] as String? ?? '', + employedEntries: EmployedEntries.fromJson( + json['employed_entries'] as Map? ?? {}), + fee: json['fee'] as int? ?? 0, + height: json['height'] as int? ?? 0, + isMining: json['is_mining'] as bool? ?? false, + isMixing: json['is_mixing'] as bool? ?? false, + isService: json['is_service'] as bool? ?? false, + paymentId: json['payment_id'] as String? ?? '', + remoteAddresses: json['remote_addresses'] == null ? [] : (json['remote_addresses'] as List< + dynamic>).cast(), + remoteAliases: json['remote_aliases'] == null ? [] : (json['remote_aliases'] as List< + dynamic>).cast(), + showSender: json['show_sender'] as bool? ?? false, + subtransfers: (json['subtransfers'] as List? ?? []).map((e) => + Subtransfer.fromJson(e as Map)).toList(), + timestamp: json['timestamp'] as int? ?? 0, + transferInternalIndex: json['transfer_internal_index'] == null + ? 0 + : json['transfer_internal_index'] is double + ? (json['transfer_internal_index'] as double).toInt() + : json['transfer_internal_index'] as int, + txBlobSize: json['tx_blob_size'] as int? ?? 0, + txHash: json['tx_hash'] as String? ?? '', + txType: json['tx_type'] as int? ?? 0, + unlockTime: json['unlock_time'] as int? ?? 0, + ); + + static Map makeMap(List transfers, + Map zanoAssets, int currentDaemonHeight) { + return Map.fromIterable( + transfers, + key: (item) => (item as Transfer).txHash, + value: (transfer) { + transfer as Transfer; + // Simple (only one subtransfer OR two subtransfers and the second is Zano, outgoing and amount equals to fee) or complex? + Subtransfer? single = transfer.subtransfers.singleOrNull; + if (transfer.subtransfers.length == 2) { + final zano = transfer.subtransfers.firstWhereOrNull((element) => + element.assetId == ZanoWalletBase.zanoAssetId); + if (zano != null && !zano.isIncome && zano.amount == BigInt.from(transfer.fee)) { + single = transfer.subtransfers.firstWhere((element) => element.assetId != + ZanoWalletBase.zanoAssetId); + } + } + bool isSimple = single != null; + // TODO: for complex transactions we show zano or any other transaction, will fix it later + if (!isSimple) { + single = + transfer.subtransfers.firstWhereOrNull((element) => + element.assetId == ZanoWalletBase.zanoAssetId) ?? transfer.subtransfers.first; + } + if (single.assetId != ZanoWalletBase.zanoAssetId) { + final asset = zanoAssets[single.assetId]; + if (asset == null) { + printV('unknown asset ${single.assetId}'); + } + final ticker = asset == null ? '***' : asset.ticker; + final decimalPoint = asset == null ? 0 : asset.decimalPoint; + return ZanoTransactionInfo.fromTransfer( + transfer, + confirmations: currentDaemonHeight - transfer.height, + isIncome: single.isIncome, + assetId: single.assetId, + amount: single.amount, + tokenSymbol: isSimple ? ticker : '*${ticker}', + decimalPoint: decimalPoint, + ); + } + final amount = single.isIncome ? single.amount : single.amount - BigInt.from(transfer.fee); + return ZanoTransactionInfo.fromTransfer( + transfer, + confirmations: currentDaemonHeight - transfer.height, + isIncome: single.isIncome, + assetId: single.assetId, + amount: amount, + tokenSymbol: isSimple ? 'ZANO' : '*ZANO', + ); + }, + ); + } +} diff --git a/cw_zano/lib/api/model/transfer_params.dart b/cw_zano/lib/api/model/transfer_params.dart new file mode 100644 index 000000000..586d5ddb8 --- /dev/null +++ b/cw_zano/lib/api/model/transfer_params.dart @@ -0,0 +1,41 @@ +import 'package:cw_zano/api/model/destination.dart'; + +class TransferParams { + final List destinations; + final BigInt fee; + final int mixin; + final String paymentId; + final String comment; + final bool pushPayer; + final bool hideReceiver; + + TransferParams({ + required this.destinations, + required this.fee, + required this.mixin, + required this.paymentId, + required this.comment, + required this.pushPayer, + required this.hideReceiver, + }); + + Map toJson() => { + 'destinations': destinations, + 'fee': fee.toInt(), + 'mixin': mixin, + 'payment_id': paymentId, + 'comment': comment, + 'push_payer': pushPayer, + 'hide_receiver': hideReceiver, + }; + + factory TransferParams.fromJson(Map json) => TransferParams( + destinations: (json['destinations'] as List?)?.map((e) => Destination.fromJson(e as Map)).toList() ?? [], + fee: BigInt.from(json['fee'] as int? ?? 0), + mixin: json['mixin'] as int? ?? 0, + paymentId: json['payment_id'] as String? ?? '', + comment: json['comment'] as String? ?? '', + pushPayer: json['push_payer'] as bool? ?? false, + hideReceiver: json['hide_receiver'] as bool? ?? false, + ); +} diff --git a/cw_zano/lib/api/model/transfer_result.dart b/cw_zano/lib/api/model/transfer_result.dart new file mode 100644 index 000000000..e0259fecc --- /dev/null +++ b/cw_zano/lib/api/model/transfer_result.dart @@ -0,0 +1,13 @@ +class TransferResult { + final String txHash; + final int txSize; + final String txUnsignedHex; + + TransferResult({required this.txHash, required this.txSize, required this.txUnsignedHex}); + + factory TransferResult.fromJson(Map json) => TransferResult( + txHash: json['tx_hash'] as String? ?? '', + txSize: json['tx_size'] as int? ?? 0, + txUnsignedHex: json['tx_unsigned_hex'] as String? ?? '', + ); +} diff --git a/cw_zano/lib/api/model/wi.dart b/cw_zano/lib/api/model/wi.dart new file mode 100644 index 000000000..0375cdf96 --- /dev/null +++ b/cw_zano/lib/api/model/wi.dart @@ -0,0 +1,32 @@ +import 'package:cw_zano/api/model/balance.dart'; + +class Wi { + final String address; + final List balances; + final bool isAuditable; + final bool isWatchOnly; + final int minedTotal; + final String path; + final String viewSecKey; + + Wi( + {required this.address, + required this.balances, + required this.isAuditable, + required this.isWatchOnly, + required this.minedTotal, + required this.path, + required this.viewSecKey}); + + factory Wi.fromJson(Map json) => Wi( + address: json['address'] as String? ?? '', + balances: (json['balances'] as List? ?? []) + .map((e) => Balance.fromJson(e as Map)) + .toList(), + isAuditable: json['is_auditable'] as bool? ?? false, + isWatchOnly: json['is_watch_only'] as bool? ?? false, + minedTotal: json['mined_total'] as int? ?? 0, + path: json['path'] as String? ?? '', + viewSecKey: json['view_sec_key'] as String? ?? '', + ); +} diff --git a/cw_zano/lib/api/model/wi_extended.dart b/cw_zano/lib/api/model/wi_extended.dart new file mode 100644 index 000000000..ef2745992 --- /dev/null +++ b/cw_zano/lib/api/model/wi_extended.dart @@ -0,0 +1,21 @@ +import 'package:cw_zano/zano_wallet.dart'; + +class WiExtended { + final String spendPrivateKey; + final String spendPublicKey; + final String viewPrivateKey; + final String viewPublicKey; + + WiExtended({required this.spendPrivateKey, required this.spendPublicKey, required this.viewPrivateKey, required this.viewPublicKey}); + + factory WiExtended.fromJson(Map json) => WiExtended( + spendPrivateKey: json['spend_private_key'] as String? ?? '', + spendPublicKey: json['spend_public_key'] as String? ?? '', + viewPrivateKey: json['view_private_key'] as String? ?? '', + viewPublicKey: json['view_public_key'] as String? ?? '', + ); + + Future seed(ZanoWalletBase api) { + return api.getSeed(); + } +} \ No newline at end of file diff --git a/cw_zano/lib/mnemonics/english.dart b/cw_zano/lib/mnemonics/english.dart new file mode 100644 index 000000000..9749f974b --- /dev/null +++ b/cw_zano/lib/mnemonics/english.dart @@ -0,0 +1,1630 @@ +class EnglishMnemonics { + static const words = [ + "like", + "just", + "love", + "know", + "never", + "want", + "time", + "out", + "there", + "make", + "look", + "eye", + "down", + "only", + "think", + "heart", + "back", + "then", + "into", + "about", + "more", + "away", + "still", + "them", + "take", + "thing", + "even", + "through", + "long", + "always", + "world", + "too", + "friend", + "tell", + "try", + "hand", + "thought", + "over", + "here", + "other", + "need", + "smile", + "again", + "much", + "cry", + "been", + "night", + "ever", + "little", + "said", + "end", + "some", + "those", + "around", + "mind", + "people", + "girl", + "leave", + "dream", + "left", + "turn", + "myself", + "give", + "nothing", + "really", + "off", + "before", + "something", + "find", + "walk", + "wish", + "good", + "once", + "place", + "ask", + "stop", + "keep", + "watch", + "seem", + "everything", + "wait", + "got", + "yet", + "made", + "remember", + "start", + "alone", + "run", + "hope", + "maybe", + "believe", + "body", + "hate", + "after", + "close", + "talk", + "stand", + "own", + "each", + "hurt", + "help", + "home", + "god", + "soul", + "new", + "many", + "two", + "inside", + "should", + "true", + "first", + "fear", + "mean", + "better", + "play", + "another", + "gone", + "change", + "use", + "wonder", + "someone", + "hair", + "cold", + "open", + "best", + "any", + "behind", + "happen", + "water", + "dark", + "laugh", + "stay", + "forever", + "name", + "work", + "show", + "sky", + "break", + "came", + "deep", + "door", + "put", + "black", + "together", + "upon", + "happy", + "such", + "great", + "white", + "matter", + "fill", + "past", + "please", + "burn", + "cause", + "enough", + "touch", + "moment", + "soon", + "voice", + "scream", + "anything", + "stare", + "sound", + "red", + "everyone", + "hide", + "kiss", + "truth", + "death", + "beautiful", + "mine", + "blood", + "broken", + "very", + "pass", + "next", + "forget", + "tree", + "wrong", + "air", + "mother", + "understand", + "lip", + "hit", + "wall", + "memory", + "sleep", + "free", + "high", + "realize", + "school", + "might", + "skin", + "sweet", + "perfect", + "blue", + "kill", + "breath", + "dance", + "against", + "fly", + "between", + "grow", + "strong", + "under", + "listen", + "bring", + "sometimes", + "speak", + "pull", + "person", + "become", + "family", + "begin", + "ground", + "real", + "small", + "father", + "sure", + "feet", + "rest", + "young", + "finally", + "land", + "across", + "today", + "different", + "guy", + "line", + "fire", + "reason", + "reach", + "second", + "slowly", + "write", + "eat", + "smell", + "mouth", + "step", + "learn", + "three", + "floor", + "promise", + "breathe", + "darkness", + "push", + "earth", + "guess", + "save", + "song", + "above", + "along", + "both", + "color", + "house", + "almost", + "sorry", + "anymore", + "brother", + "okay", + "dear", + "game", + "fade", + "already", + "apart", + "warm", + "beauty", + "heard", + "notice", + "question", + "shine", + "began", + "piece", + "whole", + "shadow", + "secret", + "street", + "within", + "finger", + "point", + "morning", + "whisper", + "child", + "moon", + "green", + "story", + "glass", + "kid", + "silence", + "since", + "soft", + "yourself", + "empty", + "shall", + "angel", + "answer", + "baby", + "bright", + "dad", + "path", + "worry", + "hour", + "drop", + "follow", + "power", + "war", + "half", + "flow", + "heaven", + "act", + "chance", + "fact", + "least", + "tired", + "children", + "near", + "quite", + "afraid", + "rise", + "sea", + "taste", + "window", + "cover", + "nice", + "trust", + "lot", + "sad", + "cool", + "force", + "peace", + "return", + "blind", + "easy", + "ready", + "roll", + "rose", + "drive", + "held", + "music", + "beneath", + "hang", + "mom", + "paint", + "emotion", + "quiet", + "clear", + "cloud", + "few", + "pretty", + "bird", + "outside", + "paper", + "picture", + "front", + "rock", + "simple", + "anyone", + "meant", + "reality", + "road", + "sense", + "waste", + "bit", + "leaf", + "thank", + "happiness", + "meet", + "men", + "smoke", + "truly", + "decide", + "self", + "age", + "book", + "form", + "alive", + "carry", + "escape", + "damn", + "instead", + "able", + "ice", + "minute", + "throw", + "catch", + "leg", + "ring", + "course", + "goodbye", + "lead", + "poem", + "sick", + "corner", + "desire", + "known", + "problem", + "remind", + "shoulder", + "suppose", + "toward", + "wave", + "drink", + "jump", + "woman", + "pretend", + "sister", + "week", + "human", + "joy", + "crack", + "grey", + "pray", + "surprise", + "dry", + "knee", + "less", + "search", + "bleed", + "caught", + "clean", + "embrace", + "future", + "king", + "son", + "sorrow", + "chest", + "hug", + "remain", + "sat", + "worth", + "blow", + "daddy", + "final", + "parent", + "tight", + "also", + "create", + "lonely", + "safe", + "cross", + "dress", + "evil", + "silent", + "bone", + "fate", + "perhaps", + "anger", + "class", + "scar", + "snow", + "tiny", + "tonight", + "continue", + "control", + "dog", + "edge", + "mirror", + "month", + "suddenly", + "comfort", + "given", + "loud", + "quickly", + "gaze", + "plan", + "rush", + "stone", + "town", + "battle", + "ignore", + "spirit", + "stood", + "stupid", + "yours", + "brown", + "build", + "dust", + "hey", + "kept", + "pay", + "phone", + "twist", + "although", + "ball", + "beyond", + "hidden", + "nose", + "taken", + "fail", + "float", + "pure", + "somehow", + "wash", + "wrap", + "angry", + "cheek", + "creature", + "forgotten", + "heat", + "rip", + "single", + "space", + "special", + "weak", + "whatever", + "yell", + "anyway", + "blame", + "job", + "choose", + "country", + "curse", + "drift", + "echo", + "figure", + "grew", + "laughter", + "neck", + "suffer", + "worse", + "yeah", + "disappear", + "foot", + "forward", + "knife", + "mess", + "somewhere", + "stomach", + "storm", + "beg", + "idea", + "lift", + "offer", + "breeze", + "field", + "five", + "often", + "simply", + "stuck", + "win", + "allow", + "confuse", + "enjoy", + "except", + "flower", + "seek", + "strength", + "calm", + "grin", + "gun", + "heavy", + "hill", + "large", + "ocean", + "shoe", + "sigh", + "straight", + "summer", + "tongue", + "accept", + "crazy", + "everyday", + "exist", + "grass", + "mistake", + "sent", + "shut", + "surround", + "table", + "ache", + "brain", + "destroy", + "heal", + "nature", + "shout", + "sign", + "stain", + "choice", + "doubt", + "glance", + "glow", + "mountain", + "queen", + "stranger", + "throat", + "tomorrow", + "city", + "either", + "fish", + "flame", + "rather", + "shape", + "spin", + "spread", + "ash", + "distance", + "finish", + "image", + "imagine", + "important", + "nobody", + "shatter", + "warmth", + "became", + "feed", + "flesh", + "funny", + "lust", + "shirt", + "trouble", + "yellow", + "attention", + "bare", + "bite", + "money", + "protect", + "amaze", + "appear", + "born", + "choke", + "completely", + "daughter", + "fresh", + "friendship", + "gentle", + "probably", + "six", + "deserve", + "expect", + "grab", + "middle", + "nightmare", + "river", + "thousand", + "weight", + "worst", + "wound", + "barely", + "bottle", + "cream", + "regret", + "relationship", + "stick", + "test", + "crush", + "endless", + "fault", + "itself", + "rule", + "spill", + "art", + "circle", + "join", + "kick", + "mask", + "master", + "passion", + "quick", + "raise", + "smooth", + "unless", + "wander", + "actually", + "broke", + "chair", + "deal", + "favorite", + "gift", + "note", + "number", + "sweat", + "box", + "chill", + "clothes", + "lady", + "mark", + "park", + "poor", + "sadness", + "tie", + "animal", + "belong", + "brush", + "consume", + "dawn", + "forest", + "innocent", + "pen", + "pride", + "stream", + "thick", + "clay", + "complete", + "count", + "draw", + "faith", + "press", + "silver", + "struggle", + "surface", + "taught", + "teach", + "wet", + "bless", + "chase", + "climb", + "enter", + "letter", + "melt", + "metal", + "movie", + "stretch", + "swing", + "vision", + "wife", + "beside", + "crash", + "forgot", + "guide", + "haunt", + "joke", + "knock", + "plant", + "pour", + "prove", + "reveal", + "steal", + "stuff", + "trip", + "wood", + "wrist", + "bother", + "bottom", + "crawl", + "crowd", + "fix", + "forgive", + "frown", + "grace", + "loose", + "lucky", + "party", + "release", + "surely", + "survive", + "teacher", + "gently", + "grip", + "speed", + "suicide", + "travel", + "treat", + "vein", + "written", + "cage", + "chain", + "conversation", + "date", + "enemy", + "however", + "interest", + "million", + "page", + "pink", + "proud", + "sway", + "themselves", + "winter", + "church", + "cruel", + "cup", + "demon", + "experience", + "freedom", + "pair", + "pop", + "purpose", + "respect", + "shoot", + "softly", + "state", + "strange", + "bar", + "birth", + "curl", + "dirt", + "excuse", + "lord", + "lovely", + "monster", + "order", + "pack", + "pants", + "pool", + "scene", + "seven", + "shame", + "slide", + "ugly", + "among", + "blade", + "blonde", + "closet", + "creek", + "deny", + "drug", + "eternity", + "gain", + "grade", + "handle", + "key", + "linger", + "pale", + "prepare", + "swallow", + "swim", + "tremble", + "wheel", + "won", + "cast", + "cigarette", + "claim", + "college", + "direction", + "dirty", + "gather", + "ghost", + "hundred", + "loss", + "lung", + "orange", + "present", + "swear", + "swirl", + "twice", + "wild", + "bitter", + "blanket", + "doctor", + "everywhere", + "flash", + "grown", + "knowledge", + "numb", + "pressure", + "radio", + "repeat", + "ruin", + "spend", + "unknown", + "buy", + "clock", + "devil", + "early", + "false", + "fantasy", + "pound", + "precious", + "refuse", + "sheet", + "teeth", + "welcome", + "add", + "ahead", + "block", + "bury", + "caress", + "content", + "depth", + "despite", + "distant", + "marry", + "purple", + "threw", + "whenever", + "bomb", + "dull", + "easily", + "grasp", + "hospital", + "innocence", + "normal", + "receive", + "reply", + "rhyme", + "shade", + "someday", + "sword", + "toe", + "visit", + "asleep", + "bought", + "center", + "consider", + "flat", + "hero", + "history", + "ink", + "insane", + "muscle", + "mystery", + "pocket", + "reflection", + "shove", + "silently", + "smart", + "soldier", + "spot", + "stress", + "train", + "type", + "view", + "whether", + "bus", + "energy", + "explain", + "holy", + "hunger", + "inch", + "magic", + "mix", + "noise", + "nowhere", + "prayer", + "presence", + "shock", + "snap", + "spider", + "study", + "thunder", + "trail", + "admit", + "agree", + "bag", + "bang", + "bound", + "butterfly", + "cute", + "exactly", + "explode", + "familiar", + "fold", + "further", + "pierce", + "reflect", + "scent", + "selfish", + "sharp", + "sink", + "spring", + "stumble", + "universe", + "weep", + "women", + "wonderful", + "action", + "ancient", + "attempt", + "avoid", + "birthday", + "branch", + "chocolate", + "core", + "depress", + "drunk", + "especially", + "focus", + "fruit", + "honest", + "match", + "palm", + "perfectly", + "pillow", + "pity", + "poison", + "roar", + "shift", + "slightly", + "thump", + "truck", + "tune", + "twenty", + "unable", + "wipe", + "wrote", + "coat", + "constant", + "dinner", + "drove", + "egg", + "eternal", + "flight", + "flood", + "frame", + "freak", + "gasp", + "glad", + "hollow", + "motion", + "peer", + "plastic", + "root", + "screen", + "season", + "sting", + "strike", + "team", + "unlike", + "victim", + "volume", + "warn", + "weird", + "attack", + "await", + "awake", + "built", + "charm", + "crave", + "despair", + "fought", + "grant", + "grief", + "horse", + "limit", + "message", + "ripple", + "sanity", + "scatter", + "serve", + "split", + "string", + "trick", + "annoy", + "blur", + "boat", + "brave", + "clearly", + "cling", + "connect", + "fist", + "forth", + "imagination", + "iron", + "jock", + "judge", + "lesson", + "milk", + "misery", + "nail", + "naked", + "ourselves", + "poet", + "possible", + "princess", + "sail", + "size", + "snake", + "society", + "stroke", + "torture", + "toss", + "trace", + "wise", + "bloom", + "bullet", + "cell", + "check", + "cost", + "darling", + "during", + "footstep", + "fragile", + "hallway", + "hardly", + "horizon", + "invisible", + "journey", + "midnight", + "mud", + "nod", + "pause", + "relax", + "shiver", + "sudden", + "value", + "youth", + "abuse", + "admire", + "blink", + "breast", + "bruise", + "constantly", + "couple", + "creep", + "curve", + "difference", + "dumb", + "emptiness", + "gotta", + "honor", + "plain", + "planet", + "recall", + "rub", + "ship", + "slam", + "soar", + "somebody", + "tightly", + "weather", + "adore", + "approach", + "bond", + "bread", + "burst", + "candle", + "coffee", + "cousin", + "crime", + "desert", + "flutter", + "frozen", + "grand", + "heel", + "hello", + "language", + "level", + "movement", + "pleasure", + "powerful", + "random", + "rhythm", + "settle", + "silly", + "slap", + "sort", + "spoken", + "steel", + "threaten", + "tumble", + "upset", + "aside", + "awkward", + "bee", + "blank", + "board", + "button", + "card", + "carefully", + "complain", + "crap", + "deeply", + "discover", + "drag", + "dread", + "effort", + "entire", + "fairy", + "giant", + "gotten", + "greet", + "illusion", + "jeans", + "leap", + "liquid", + "march", + "mend", + "nervous", + "nine", + "replace", + "rope", + "spine", + "stole", + "terror", + "accident", + "apple", + "balance", + "boom", + "childhood", + "collect", + "demand", + "depression", + "eventually", + "faint", + "glare", + "goal", + "group", + "honey", + "kitchen", + "laid", + "limb", + "machine", + "mere", + "mold", + "murder", + "nerve", + "painful", + "poetry", + "prince", + "rabbit", + "shelter", + "shore", + "shower", + "soothe", + "stair", + "steady", + "sunlight", + "tangle", + "tease", + "treasure", + "uncle", + "begun", + "bliss", + "canvas", + "cheer", + "claw", + "clutch", + "commit", + "crimson", + "crystal", + "delight", + "doll", + "existence", + "express", + "fog", + "football", + "gay", + "goose", + "guard", + "hatred", + "illuminate", + "mass", + "math", + "mourn", + "rich", + "rough", + "skip", + "stir", + "student", + "style", + "support", + "thorn", + "tough", + "yard", + "yearn", + "yesterday", + "advice", + "appreciate", + "autumn", + "bank", + "beam", + "bowl", + "capture", + "carve", + "collapse", + "confusion", + "creation", + "dove", + "feather", + "girlfriend", + "glory", + "government", + "harsh", + "hop", + "inner", + "loser", + "moonlight", + "neighbor", + "neither", + "peach", + "pig", + "praise", + "screw", + "shield", + "shimmer", + "sneak", + "stab", + "subject", + "throughout", + "thrown", + "tower", + "twirl", + "wow", + "army", + "arrive", + "bathroom", + "bump", + "cease", + "cookie", + "couch", + "courage", + "dim", + "guilt", + "howl", + "hum", + "husband", + "insult", + "led", + "lunch", + "mock", + "mostly", + "natural", + "nearly", + "needle", + "nerd", + "peaceful", + "perfection", + "pile", + "price", + "remove", + "roam", + "sanctuary", + "serious", + "shiny", + "shook", + "sob", + "stolen", + "tap", + "vain", + "void", + "warrior", + "wrinkle", + "affection", + "apologize", + "blossom", + "bounce", + "bridge", + "cheap", + "crumble", + "decision", + "descend", + "desperately", + "dig", + "dot", + "flip", + "frighten", + "heartbeat", + "huge", + "lazy", + "lick", + "odd", + "opinion", + "process", + "puzzle", + "quietly", + "retreat", + "score", + "sentence", + "separate", + "situation", + "skill", + "soak", + "square", + "stray", + "taint", + "task", + "tide", + "underneath", + "veil", + "whistle", + "anywhere", + "bedroom", + "bid", + "bloody", + "burden", + "careful", + "compare", + "concern", + "curtain", + "decay", + "defeat", + "describe", + "double", + "dreamer", + "driver", + "dwell", + "evening", + "flare", + "flicker", + "grandma", + "guitar", + "harm", + "horrible", + "hungry", + "indeed", + "lace", + "melody", + "monkey", + "nation", + "object", + "obviously", + "rainbow", + "salt", + "scratch", + "shown", + "shy", + "stage", + "stun", + "third", + "tickle", + "useless", + "weakness", + "worship", + "worthless", + "afternoon", + "beard", + "boyfriend", + "bubble", + "busy", + "certain", + "chin", + "concrete", + "desk", + "diamond", + "doom", + "drawn", + "due", + "felicity", + "freeze", + "frost", + "garden", + "glide", + "harmony", + "hopefully", + "hunt", + "jealous", + "lightning", + "mama", + "mercy", + "peel", + "physical", + "position", + "pulse", + "punch", + "quit", + "rant", + "respond", + "salty", + "sane", + "satisfy", + "savior", + "sheep", + "slept", + "social", + "sport", + "tuck", + "utter", + "valley", + "wolf", + "aim", + "alas", + "alter", + "arrow", + "awaken", + "beaten", + "belief", + "brand", + "ceiling", + "cheese", + "clue", + "confidence", + "connection", + "daily", + "disguise", + "eager", + "erase", + "essence", + "everytime", + "expression", + "fan", + "flag", + "flirt", + "foul", + "fur", + "giggle", + "glorious", + "ignorance", + "law", + "lifeless", + "measure", + "mighty", + "muse", + "north", + "opposite", + "paradise", + "patience", + "patient", + "pencil", + "petal", + "plate", + "ponder", + "possibly", + "practice", + "slice", + "spell", + "stock", + "strife", + "strip", + "suffocate", + "suit", + "tender", + "tool", + "trade", + "velvet", + "verse", + "waist", + "witch", + "aunt", + "bench", + "bold", + "cap", + "certainly", + "click", + "companion", + "creator", + "dart", + "delicate", + "determine", + "dish", + "dragon", + "drama", + "drum", + "dude", + "everybody", + "feast", + "forehead", + "former", + "fright", + "fully", + "gas", + "hook", + "hurl", + "invite", + "juice", + "manage", + "moral", + "possess", + "raw", + "rebel", + "royal", + "scale", + "scary", + "several", + "slight", + "stubborn", + "swell", + "talent", + "tea", + "terrible", + "thread", + "torment", + "trickle", + "usually", + "vast", + "violence", + "weave", + "acid", + "agony", + "ashamed", + "awe", + "belly", + "blend", + "blush", + "character", + "cheat", + "common", + "company", + "coward", + "creak", + "danger", + "deadly", + "defense", + "define", + "depend", + "desperate", + "destination", + "dew", + "duck", + "dusty", + "embarrass", + "engine", + "example", + "explore", + "foe", + "freely", + "frustrate", + "generation", + "glove", + "guilty", + "health", + "hurry", + "idiot", + "impossible", + "inhale", + "jaw", + "kingdom", + "mention", + "mist", + "moan", + "mumble", + "mutter", + "observe", + "ode", + "pathetic", + "pattern", + "pie", + "prefer", + "puff", + "rape", + "rare", + "revenge", + "rude", + "scrape", + "spiral", + "squeeze", + "strain", + "sunset", + "suspend", + "sympathy", + "thigh", + "throne", + "total", + "unseen", + "weapon", + "weary" + ]; +} diff --git a/cw_zano/lib/model/pending_zano_transaction.dart b/cw_zano/lib/model/pending_zano_transaction.dart new file mode 100644 index 000000000..719c370a1 --- /dev/null +++ b/cw_zano/lib/model/pending_zano_transaction.dart @@ -0,0 +1,52 @@ +import 'package:cw_core/pending_transaction.dart'; +import 'package:cw_zano/api/model/destination.dart'; +import 'package:cw_zano/api/model/transfer_result.dart'; +import 'package:cw_zano/zano_formatter.dart'; +import 'package:cw_zano/zano_wallet.dart'; + +class PendingZanoTransaction with PendingTransaction { + PendingZanoTransaction({ + required this.zanoWallet, + required this.destinations, + required this.fee, + required this.comment, + required this.assetId, + required this.ticker, + this.decimalPoint = ZanoFormatter.defaultDecimalPoint, + required this.amount, + }); + + final ZanoWalletBase zanoWallet; + final List destinations; + final BigInt fee; + final String comment; + final String assetId; + final String ticker; + final int decimalPoint; + final BigInt amount; + + @override + String get id => transferResult?.txHash ?? ''; + + @override + String get hex => ''; + + @override + String get amountFormatted => ZanoFormatter.bigIntAmountToString(amount, decimalPoint); + + @override + String get feeFormatted => ZanoFormatter.bigIntAmountToString(fee); + + TransferResult? transferResult; + + @override + Future commit() async { + await zanoWallet.transfer(destinations, fee, comment); + zanoWallet.fetchTransactions(); + } + + @override + Future commitUR() { + throw UnimplementedError(); + } +} diff --git a/cw_zano/lib/model/zano_asset.dart b/cw_zano/lib/model/zano_asset.dart new file mode 100644 index 000000000..e69de29bb diff --git a/cw_zano/lib/model/zano_balance.dart b/cw_zano/lib/model/zano_balance.dart new file mode 100644 index 000000000..882c0e11b --- /dev/null +++ b/cw_zano/lib/model/zano_balance.dart @@ -0,0 +1,17 @@ +import 'package:cw_core/balance.dart'; +import 'package:cw_zano/zano_formatter.dart'; + +class ZanoBalance extends Balance { + final BigInt total; + final BigInt unlocked; + final int decimalPoint; + ZanoBalance({required this.total, required this.unlocked, this.decimalPoint = ZanoFormatter.defaultDecimalPoint}) : super(unlocked.isValidInt ? unlocked.toInt() : 0, (total - unlocked).isValidInt ? (total - unlocked).toInt() : 0); + + ZanoBalance.empty({this.decimalPoint = ZanoFormatter.defaultDecimalPoint}): total = BigInt.zero, unlocked = BigInt.zero, super(0, 0); + + @override + String get formattedAdditionalBalance => ZanoFormatter.bigIntAmountToString(total - unlocked, decimalPoint); + + @override + String get formattedAvailableBalance => ZanoFormatter.bigIntAmountToString(unlocked, decimalPoint); +} diff --git a/cw_zano/lib/model/zano_transaction_creation_exception.dart b/cw_zano/lib/model/zano_transaction_creation_exception.dart new file mode 100644 index 000000000..74a5f77c6 --- /dev/null +++ b/cw_zano/lib/model/zano_transaction_creation_exception.dart @@ -0,0 +1,8 @@ +class ZanoTransactionCreationException implements Exception { + ZanoTransactionCreationException(this.message); + + final String message; + + @override + String toString() => message; +} \ No newline at end of file diff --git a/cw_zano/lib/model/zano_transaction_credentials.dart b/cw_zano/lib/model/zano_transaction_credentials.dart new file mode 100644 index 000000000..dbbfe53c6 --- /dev/null +++ b/cw_zano/lib/model/zano_transaction_credentials.dart @@ -0,0 +1,11 @@ +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/monero_transaction_priority.dart'; +import 'package:cw_core/output_info.dart'; + +class ZanoTransactionCredentials { + ZanoTransactionCredentials({required this.outputs, required this.priority, required this.currency}); + + final List outputs; + final MoneroTransactionPriority priority; + final CryptoCurrency currency; +} diff --git a/cw_zano/lib/model/zano_transaction_info.dart b/cw_zano/lib/model/zano_transaction_info.dart new file mode 100644 index 000000000..d643e9207 --- /dev/null +++ b/cw_zano/lib/model/zano_transaction_info.dart @@ -0,0 +1,81 @@ +import 'package:cw_core/format_amount.dart'; +import 'package:cw_core/transaction_direction.dart'; +import 'package:cw_core/transaction_info.dart'; +import 'package:cw_zano/api/model/transfer.dart'; +import 'package:cw_zano/zano_formatter.dart'; + +class ZanoTransactionInfo extends TransactionInfo { + ZanoTransactionInfo({ + required this.id, + required this.height, + required this.direction, + required this.date, + required this.isPending, + required this.zanoAmount, + required this.fee, + required this.confirmations, + required this.tokenSymbol, + required this.decimalPoint, + required String assetId, + }) : amount = zanoAmount.isValidInt ? zanoAmount.toInt() : 0 { + additionalInfo['assetId'] = assetId; + } + + ZanoTransactionInfo.fromTransfer(Transfer transfer, + {required int confirmations, + required bool isIncome, + required String assetId, + required BigInt amount, + this.tokenSymbol = 'ZANO', + this.decimalPoint = ZanoFormatter.defaultDecimalPoint}) + : id = transfer.txHash, + height = transfer.height, + direction = isIncome ? TransactionDirection.incoming : TransactionDirection.outgoing, + date = DateTime.fromMillisecondsSinceEpoch(transfer.timestamp * 1000), + zanoAmount = amount, + amount = amount.isValidInt ? amount.toInt() : 0, + fee = transfer.fee, + confirmations = confirmations, + isPending = confirmations < 10, + recipientAddress = transfer.remoteAddresses.isNotEmpty + ? transfer.remoteAddresses.first + : '' { + additionalInfo = { + 'comment': transfer.comment, + 'assetId': assetId, + }; + } + + String get assetId => additionalInfo["assetId"] as String; + + set assetId(String newId) => additionalInfo["assetId"] = newId; + final String id; + final int height; + final TransactionDirection direction; + final DateTime date; + final bool isPending; + final BigInt zanoAmount; + final int amount; + final int fee; + final int confirmations; + final int decimalPoint; + late String recipientAddress; + final String tokenSymbol; + String? _fiatAmount; + String? key; + + @override + String amountFormatted() => + '${formatAmount(ZanoFormatter.bigIntAmountToString(zanoAmount, decimalPoint))} $tokenSymbol'; + + @override + String fiatAmount() => _fiatAmount ?? ''; + + @override + void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount); + + @override + String feeFormatted() => '${formatAmount(ZanoFormatter.intAmountToString(fee))} $feeCurrency'; + + String get feeCurrency => 'ZANO'; +} diff --git a/cw_zano/lib/model/zano_wallet_keys.dart b/cw_zano/lib/model/zano_wallet_keys.dart new file mode 100644 index 000000000..5a224633b --- /dev/null +++ b/cw_zano/lib/model/zano_wallet_keys.dart @@ -0,0 +1,12 @@ +class ZanoWalletKeys { + const ZanoWalletKeys( + {required this.privateSpendKey, + required this.privateViewKey, + required this.publicSpendKey, + required this.publicViewKey}); + + final String publicViewKey; + final String privateViewKey; + final String publicSpendKey; + final String privateSpendKey; +} \ No newline at end of file diff --git a/cw_zano/lib/zano_formatter.dart b/cw_zano/lib/zano_formatter.dart new file mode 100644 index 000000000..e20e3a5f7 --- /dev/null +++ b/cw_zano/lib/zano_formatter.dart @@ -0,0 +1,78 @@ +import 'dart:math'; + +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_zano/zano_wallet_api.dart'; +import 'package:decimal/decimal.dart'; +import 'package:decimal/intl.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:intl/intl.dart'; + +class ZanoFormatter { + static const defaultDecimalPoint = 12; + + //static final numberFormat = NumberFormat() + // ..maximumFractionDigits = defaultDecimalPoint + // ..minimumFractionDigits = 1; + + static Decimal _bigIntDivision({required BigInt amount, required BigInt divider}) { + return (Decimal.fromBigInt(amount) / Decimal.fromBigInt(divider)).toDecimal(); + } + + static String intAmountToString(int amount, [int decimalPoint = defaultDecimalPoint]) { + final numberFormat = NumberFormat()..maximumFractionDigits = decimalPoint + ..minimumFractionDigits = 1; + return numberFormat.format( + DecimalIntl( + _bigIntDivision( + amount: BigInt.from(amount), + divider: BigInt.from(pow(10, decimalPoint)), + ), + ), + ) + .replaceAll(',', ''); + } + + static String bigIntAmountToString(BigInt amount, [int decimalPoint = defaultDecimalPoint]) { + if (decimalPoint == 0) { + return '0'; + } + final numberFormat = NumberFormat()..maximumFractionDigits = decimalPoint + ..minimumFractionDigits = 1; + return numberFormat.format( + DecimalIntl( + _bigIntDivision( + amount: amount, + divider: BigInt.from(pow(10, decimalPoint)), + ), + ), + ) + .replaceAll(',', ''); + } + + static double intAmountToDouble(int amount, [int decimalPoint = defaultDecimalPoint]) => _bigIntDivision( + amount: BigInt.from(amount), + divider: BigInt.from(pow(10, decimalPoint)), + ).toDouble(); + + static int parseAmount(String amount, [int decimalPoint = defaultDecimalPoint]) { + final resultBigInt = (Decimal.parse(amount) * Decimal.fromBigInt(BigInt.from(10).pow(decimalPoint))).toBigInt(); + if (!resultBigInt.isValidInt) { + Fluttertoast.showToast(msg: 'Cannot transfer $amount. Maximum is ${intAmountToString(resultBigInt.toInt(), decimalPoint)}.'); + } + return resultBigInt.toInt(); + } + + static BigInt bigIntFromDynamic(dynamic d) { + if (d is int) { + return BigInt.from(d); + } else if (d is BigInt) { + return d; + } else if (d == null) { + return BigInt.zero; + } else { + printV(('cannot cast value of type ${d.runtimeType} to BigInt')); + throw 'cannot cast value of type ${d.runtimeType} to BigInt'; + //return BigInt.zero; + } + } +} diff --git a/cw_zano/lib/zano_transaction_history.dart b/cw_zano/lib/zano_transaction_history.dart new file mode 100644 index 000000000..4c6a2d554 --- /dev/null +++ b/cw_zano/lib/zano_transaction_history.dart @@ -0,0 +1,27 @@ +import 'dart:core'; +import 'package:mobx/mobx.dart'; +import 'package:cw_core/transaction_history.dart'; +import 'package:cw_zano/model/zano_transaction_info.dart'; + +part 'zano_transaction_history.g.dart'; + +class ZanoTransactionHistory = ZanoTransactionHistoryBase + with _$ZanoTransactionHistory; + +abstract class ZanoTransactionHistoryBase + extends TransactionHistoryBase with Store { + ZanoTransactionHistoryBase() { + transactions = ObservableMap(); + } + + @override + Future save() async {} + + @override + void addOne(ZanoTransactionInfo transaction) => + transactions[transaction.id] = transaction; + + @override + void addMany(Map transactions) => + this.transactions.addAll(transactions); +} diff --git a/cw_zano/lib/zano_utils.dart b/cw_zano/lib/zano_utils.dart new file mode 100644 index 000000000..f39f20462 --- /dev/null +++ b/cw_zano/lib/zano_utils.dart @@ -0,0 +1,17 @@ +import 'dart:convert'; + +import 'package:monero/zano.dart' as zano; +import 'package:cw_zano/api/model/get_address_info_result.dart'; + +class ZanoUtils { + static bool validateAddress(String address) { + try { + final result = GetAddressInfoResult.fromJson( + jsonDecode(zano.PlainWallet_getAddressInfo(address)) as Map, + ); + return result.valid; + } catch (err) { + return false; + } + } +} diff --git a/cw_zano/lib/zano_wallet.dart b/cw_zano/lib/zano_wallet.dart new file mode 100644 index 000000000..7f68b71e6 --- /dev/null +++ b/cw_zano/lib/zano_wallet.dart @@ -0,0 +1,577 @@ +import 'dart:async'; +import 'dart:core'; +import 'dart:io'; +import 'dart:math'; + +import 'package:cw_core/cake_hive.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/node.dart'; +import 'package:cw_core/pathForWallet.dart'; +import 'package:cw_core/pending_transaction.dart'; +import 'package:cw_core/sync_status.dart'; +import 'package:cw_core/transaction_priority.dart'; +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_credentials.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/zano_asset.dart'; +import 'package:cw_zano/api/model/create_wallet_result.dart'; +import 'package:cw_zano/api/model/destination.dart'; +import 'package:cw_zano/api/model/get_recent_txs_and_info_result.dart'; +import 'package:cw_zano/api/model/get_wallet_status_result.dart'; +import 'package:cw_zano/api/model/transfer.dart'; +import 'package:cw_zano/model/pending_zano_transaction.dart'; +import 'package:cw_zano/model/zano_balance.dart'; +import 'package:cw_zano/model/zano_transaction_creation_exception.dart'; +import 'package:cw_zano/model/zano_transaction_credentials.dart'; +import 'package:cw_zano/model/zano_transaction_info.dart'; +import 'package:cw_zano/model/zano_wallet_keys.dart'; +import 'package:cw_zano/zano_formatter.dart'; +import 'package:cw_zano/zano_transaction_history.dart'; +import 'package:cw_zano/zano_wallet_addresses.dart'; +import 'package:cw_zano/zano_wallet_api.dart'; +import 'package:cw_zano/zano_wallet_exceptions.dart'; +import 'package:cw_zano/zano_wallet_service.dart'; +import 'package:cw_zano/api/model/balance.dart'; + +import 'package:mobx/mobx.dart'; + +part 'zano_wallet.g.dart'; + +class ZanoWallet = ZanoWalletBase with _$ZanoWallet; + +abstract class ZanoWalletBase + extends WalletBase + with Store, ZanoWalletApi { + static const int _autoSaveIntervalSeconds = 30; + static const int _pollIntervalMilliseconds = 5000; + static const int _maxLoadAssetsRetries = 5; + + @override + void setPassword(String password) { + _password = password; + super.setPassword(password); + } + + String _password; + + @override + String get password => _password; + + @override + Future signMessage(String message, {String? address = null}) { + throw UnimplementedError(); + } + + @override + Future verifyMessage(String message, String signature, {String? address = null}) { + throw UnimplementedError(); + } + + @override + ZanoWalletAddresses walletAddresses; + + @override + @observable + SyncStatus syncStatus; + + @override + @observable + ObservableMap balance; + + @override + String seed = ''; + + @override + String? passphrase = ''; + + @override + ZanoWalletKeys keys = ZanoWalletKeys( + privateSpendKey: '', privateViewKey: '', publicSpendKey: '', publicViewKey: ''); + + static const String zanoAssetId = + 'd6329b5b1f7c0805b5c345f4957554002a2f557845f64d7645dae0e051a6498a'; + + Map zanoAssets = {}; + + Timer? _updateSyncInfoTimer; + + int _lastKnownBlockHeight = 0; + int _initialSyncHeight = 0; + int currentDaemonHeight = 0; + bool _isTransactionUpdating; + bool _hasSyncAfterStartup; + Timer? _autoSaveTimer; + + /// number of transactions in each request + static final int _txChunkSize = (pow(2, 32) - 1).toInt(); + + ZanoWalletBase(WalletInfo walletInfo, String password) + : balance = ObservableMap.of({CryptoCurrency.zano: ZanoBalance.empty()}), + _isTransactionUpdating = false, + _hasSyncAfterStartup = false, + walletAddresses = ZanoWalletAddresses(walletInfo), + syncStatus = NotConnectedSyncStatus(), + _password = password, + super(walletInfo) { + transactionHistory = ZanoTransactionHistory(); + if (!CakeHive.isAdapterRegistered(ZanoAsset.typeId)) { + CakeHive.registerAdapter(ZanoAssetAdapter()); + } + } + + @override + int calculateEstimatedFee(TransactionPriority priority, [int? amount = null]) => + getCurrentTxFee(priority); + + @override + Future changePassword(String password) async { + setPassword(password); + } + + static Future create({required WalletCredentials credentials}) async { + final wallet = ZanoWallet(credentials.walletInfo!, credentials.password!); + await wallet.initWallet(); + final path = await pathForWallet(name: credentials.name, type: credentials.walletInfo!.type); + final createWalletResult = await wallet.createWallet(path, credentials.password!); + await wallet.initWallet(); + await wallet.parseCreateWalletResult(createWalletResult); + if (credentials.passphrase != null) { + await wallet.setPassphrase(credentials.passphrase!); + wallet.seed = await createWalletResult.seed(wallet); + wallet.passphrase = await wallet.getPassphrase(); + } + await wallet.init(createWalletResult.wi.address); + return wallet; + } + + static Future restore( + {required ZanoRestoreWalletFromSeedCredentials credentials}) async { + final wallet = ZanoWallet(credentials.walletInfo!, credentials.password!); + await wallet.initWallet(); + final path = await pathForWallet(name: credentials.name, type: credentials.walletInfo!.type); + final createWalletResult = await wallet.restoreWalletFromSeed( + path, credentials.password!, credentials.mnemonic, credentials.passphrase); + await wallet.initWallet(); + await wallet.parseCreateWalletResult(createWalletResult); + if (credentials.passphrase != null) { + await wallet.setPassphrase(credentials.passphrase!); + wallet.seed = await createWalletResult.seed(wallet); + wallet.passphrase = await wallet.getPassphrase(); + } + await wallet.init(createWalletResult.wi.address); + return wallet; + } + + static Future open( + {required String name, required String password, required WalletInfo walletInfo}) async { + final path = await pathForWallet(name: name, type: walletInfo.type); + if (ZanoWalletApi.openWalletCache[path] != null) { + final wallet = ZanoWallet(walletInfo, password); + await wallet.parseCreateWalletResult(ZanoWalletApi.openWalletCache[path]!).then((_) { + unawaited(wallet.init(ZanoWalletApi.openWalletCache[path]!.wi.address)); + }); + return wallet; + } else { + final wallet = ZanoWallet(walletInfo, password); + await wallet.initWallet(); + final createWalletResult = await wallet.loadWallet(path, password); + await wallet.parseCreateWalletResult(createWalletResult).then((_) { + unawaited(wallet.init(createWalletResult.wi.address)); + }); + return wallet; + } + } + + Future parseCreateWalletResult(CreateWalletResult result) async { + hWallet = result.walletId; + seed = await result.seed(this); + keys = ZanoWalletKeys( + privateSpendKey: result.privateSpendKey, + privateViewKey: result.privateViewKey, + publicSpendKey: result.publicSpendKey, + publicViewKey: result.publicViewKey, + ); + passphrase = await getPassphrase(); + + printV('setting hWallet = ${result.walletId}'); + walletAddresses.address = result.wi.address; + await loadAssets(result.wi.balances, maxRetries: _maxLoadAssetsRetries); + for (final item in result.wi.balances) { + if (item.assetInfo.assetId == zanoAssetId) { + balance[CryptoCurrency.zano] = ZanoBalance( + total: item.total, + unlocked: item.unlocked, + ); + } + } + if (result.recentHistory.history != null) { + final transfers = result.recentHistory.history!; + final transactions = Transfer.makeMap(transfers, zanoAssets, currentDaemonHeight); + transactionHistory.addMany(transactions); + await transactionHistory.save(); + } + } + + @override + Future close({bool shouldCleanup = true}) async { + closeWallet(null); + _updateSyncInfoTimer?.cancel(); + _autoSaveTimer?.cancel(); + } + + @override + Future connectToNode({required Node node}) async { + syncStatus = ConnectingSyncStatus(); + await setupNode(node.uriRaw); + syncStatus = ConnectedSyncStatus(); + } + + @override + Future createTransaction(Object credentials) async { + credentials as ZanoTransactionCredentials; + final isZano = credentials.currency == CryptoCurrency.zano; + final outputs = credentials.outputs; + final hasMultiDestination = outputs.length > 1; + final unlockedBalanceZano = balance[CryptoCurrency.zano]?.unlocked ?? BigInt.zero; + final unlockedBalanceCurrency = balance[credentials.currency]?.unlocked ?? BigInt.zero; + final fee = BigInt.from(calculateEstimatedFee(credentials.priority)); + late BigInt totalAmount; + void checkForEnoughBalances() { + if (isZano) { + if (totalAmount + fee > unlockedBalanceZano) { + throw ZanoTransactionCreationException( + "You don't have enough coins (required: ${ZanoFormatter.bigIntAmountToString(totalAmount + fee)} ZANO, unlocked ${ZanoFormatter.bigIntAmountToString(unlockedBalanceZano)} ZANO)."); + } + } else { + if (fee > unlockedBalanceZano) { + throw ZanoTransactionCreationException( + "You don't have enough coins (required: ${ZanoFormatter.bigIntAmountToString(fee)} ZANO, unlocked ${ZanoFormatter.bigIntAmountToString(unlockedBalanceZano)} ZANO)."); + } + if (totalAmount > unlockedBalanceCurrency) { + throw ZanoTransactionCreationException( + "You don't have enough coins (required: ${ZanoFormatter.bigIntAmountToString(totalAmount, credentials.currency.decimals)} ${credentials.currency.title}, unlocked ${ZanoFormatter.bigIntAmountToString(unlockedBalanceCurrency, credentials.currency.decimals)} ${credentials.currency.title})."); + } + } + } + + final assetId = isZano ? zanoAssetId : (credentials.currency as ZanoAsset).assetId; + late List destinations; + if (hasMultiDestination) { + if (outputs.any((output) => output.sendAll || (output.formattedCryptoAmount ?? 0) <= 0)) { + throw ZanoTransactionCreationException("You don't have enough coins."); + } + totalAmount = outputs.fold( + BigInt.zero, (acc, value) => acc + BigInt.from(value.formattedCryptoAmount ?? 0)); + checkForEnoughBalances(); + destinations = outputs + .map((output) => Destination( + amount: BigInt.from(output.formattedCryptoAmount ?? 0), + address: output.isParsedAddress ? output.extractedAddress! : output.address, + assetId: assetId, + )) + .toList(); + } else { + final output = outputs.first; + if (output.sendAll) { + if (isZano) { + totalAmount = unlockedBalanceZano - fee; + } else { + totalAmount = unlockedBalanceCurrency; + } + } else { + totalAmount = BigInt.from(output.formattedCryptoAmount!); + } + checkForEnoughBalances(); + destinations = [ + Destination( + amount: totalAmount, + address: output.isParsedAddress ? output.extractedAddress! : output.address, + assetId: assetId, + ) + ]; + } + return PendingZanoTransaction( + zanoWallet: this, + destinations: destinations, + fee: fee, + comment: outputs.first.note ?? '', + assetId: assetId, + ticker: credentials.currency.title, + decimalPoint: credentials.currency.decimals, + amount: totalAmount, + ); + } + + @override + Future> fetchTransactions() async { + try { + final transfers = []; + late GetRecentTxsAndInfoResult result; + do { + result = await getRecentTxsAndInfo(offset: 0, count: _txChunkSize); + // _lastTxIndex += result.transfers.length; + transfers.addAll(result.transfers); + } while (result.lastItemIndex + 1 < result.totalTransfers); + return Transfer.makeMap(transfers, zanoAssets, currentDaemonHeight); + } catch (e) { + printV((e.toString())); + return {}; + } + } + + Future init(String address) async { + await walletAddresses.init(); + await walletAddresses.updateAddress(address); + await updateTransactions(); + _autoSaveTimer = Timer.periodic(Duration(seconds: _autoSaveIntervalSeconds), (_) async { + await save(); + }); + } + + @override + Future renameWalletFiles(String newWalletName) async { + final currentWalletPath = await pathForWallet(name: name, type: type); + final currentCacheFile = File(currentWalletPath); + final currentKeysFile = File('$currentWalletPath.keys'); + final currentAddressListFile = File('$currentWalletPath.address.txt'); + + final newWalletPath = await pathForWallet(name: newWalletName, type: type); + + // Copies current wallet files into new wallet name's dir and files + if (currentCacheFile.existsSync()) { + await currentCacheFile.copy(newWalletPath); + } + if (currentKeysFile.existsSync()) { + await currentKeysFile.copy('$newWalletPath.keys'); + } + if (currentAddressListFile.existsSync()) { + await currentAddressListFile.copy('$newWalletPath.address.txt'); + } + + // Delete old name's dir and files + await Directory(currentWalletPath).delete(recursive: true); + } + + @override + Future rescan({required int height}) => throw UnimplementedError(); + + @override + Future save() async { + try { + await store(); + await walletAddresses.updateAddressesInBox(); + } catch (e) { + printV(('Error while saving Zano wallet file ${e.toString()}')); + } + } + + Future loadAssets(List balances, {int maxRetries = 1}) async { + List assets = []; + int retryCount = 0; + + while (retryCount < maxRetries) { + try { + assets = await getAssetsWhitelist(); + break; + } on ZanoWalletBusyException { + if (retryCount < maxRetries - 1) { + retryCount++; + await Future.delayed(Duration(seconds: 1)); + } else { + printV(('failed to load assets after $retryCount retries')); + break; + } + } + } + zanoAssets = {}; + for (final asset in assets) { + final newAsset = ZanoAsset.copyWith( + asset, + enabled: balances.any((element) => element.assetId == asset.assetId), + ); + zanoAssets.putIfAbsent(asset.assetId, () => newAsset); + } + } + + @override + Future startSync() async { + try { + syncStatus = AttemptingSyncStatus(); + _lastKnownBlockHeight = 0; + _initialSyncHeight = 0; + _updateSyncInfoTimer ??= + Timer.periodic(Duration(milliseconds: _pollIntervalMilliseconds), (_) => _updateSyncInfo()); + } catch (e) { + syncStatus = FailedSyncStatus(); + printV((e.toString())); + } + } + + @override + Future? updateBalance() => null; + + Future updateTransactions() async { + try { + if (_isTransactionUpdating) { + return; + } + _isTransactionUpdating = true; + final transactions = await fetchTransactions(); + transactionHistory.clear(); + transactionHistory.addMany(transactions); + await transactionHistory.save(); + _isTransactionUpdating = false; + } catch (e) { + printV("e: $e"); + printV((e.toString())); + _isTransactionUpdating = false; + } + } + + Future addZanoAssetById(String assetId) async { + if (zanoAssets.containsKey(assetId)) { + throw ZanoWalletException('zano asset with id $assetId already added'); + } + final assetDescriptor = await addAssetsWhitelist(assetId); + if (assetDescriptor == null) { + throw ZanoWalletException("there's no zano asset with id $assetId"); + } + final asset = ZanoAsset.copyWith( + assetDescriptor, + assetId: assetId, + enabled: true, + ); + zanoAssets[asset.assetId] = asset; + balance[asset] = ZanoBalance.empty(decimalPoint: asset.decimalPoint); + return asset; + } + + Future changeZanoAssetAvailability(ZanoAsset asset) async { + if (asset.enabled) { + final assetDescriptor = await addAssetsWhitelist(asset.assetId); + if (assetDescriptor == null) { + printV(('Error adding zano asset')); + } + } else { + final result = await removeAssetsWhitelist(asset.assetId); + if (result == false) { + printV(('Error removing zano asset')); + } + } + } + + Future deleteZanoAsset(ZanoAsset asset) async { + final _ = await removeAssetsWhitelist(asset.assetId); + } + + Future getZanoAsset(String assetId) async { + // wallet api is not available while the wallet is syncing so only call it if it's synced + if (syncStatus is SyncedSyncStatus) { + return await getAssetInfo(assetId); + } + return null; + } + + Future _askForUpdateTransactionHistory() async => await updateTransactions(); + + void _onNewBlock(int height, int blocksLeft, double ptc) async { + try { + if (blocksLeft < 1000) { + await _askForUpdateTransactionHistory(); + syncStatus = SyncedSyncStatus(); + + if (!_hasSyncAfterStartup) { + _hasSyncAfterStartup = true; + await save(); + } + } else { + syncStatus = SyncingSyncStatus(blocksLeft, ptc); + } + } catch (e) { + printV((e.toString())); + } + } + + void _updateSyncProgress(GetWalletStatusResult walletStatus) { + final syncHeight = walletStatus.currentWalletHeight; + if (_initialSyncHeight <= 0) { + _initialSyncHeight = syncHeight; + } + final bchHeight = walletStatus.currentDaemonHeight; + + if (_lastKnownBlockHeight == syncHeight) { + return; + } + + _lastKnownBlockHeight = syncHeight; + final track = bchHeight - _initialSyncHeight; + final diff = track - (bchHeight - syncHeight); + final ptc = diff <= 0 ? 0.0 : diff / track; + final left = bchHeight - syncHeight; + + if (syncHeight < 0 || left < 0) { + return; + } + + // 1. Actual new height; 2. Blocks left to finish; 3. Progress in percents; + _onNewBlock.call(syncHeight, left, ptc); + } + + void _updateSyncInfo() async { + GetWalletStatusResult walletStatus; + // ignoring get wallet status exception (in case of wrong wallet id) + try { + walletStatus = await getWalletStatus(); + } on ZanoWalletException { + return; + } + currentDaemonHeight = walletStatus.currentDaemonHeight; + _updateSyncProgress(walletStatus); + + // we can call getWalletInfo ONLY if getWalletStatus returns NOT is in long refresh and wallet state is 2 (ready) + if (!walletStatus.isInLongRefresh && walletStatus.walletState == 2) { + final walletInfo = await getWalletInfo(); + seed = await walletInfo.wiExtended.seed(this); + keys = ZanoWalletKeys( + privateSpendKey: walletInfo.wiExtended.spendPrivateKey, + privateViewKey: walletInfo.wiExtended.viewPrivateKey, + publicSpendKey: walletInfo.wiExtended.spendPublicKey, + publicViewKey: walletInfo.wiExtended.viewPublicKey, + ); + loadAssets(walletInfo.wi.balances); + // matching balances and whitelists + // 1. show only balances available in whitelists + // 2. set whitelists available in balances as 'enabled' ('disabled' by default) + for (final b in walletInfo.wi.balances) { + if (b.assetId == zanoAssetId) { + balance[CryptoCurrency.zano] = ZanoBalance(total: b.total, unlocked: b.unlocked); + } else { + final asset = zanoAssets[b.assetId]; + if (asset == null) { + printV('balance for an unknown asset ${b.assetInfo.assetId}'); + continue; + } + if (balance.keys.any( + (element) => element is ZanoAsset && element.assetId == b.assetInfo.assetId)) { + balance[balance.keys.firstWhere((element) => + element is ZanoAsset && element.assetId == b.assetInfo.assetId)] = + ZanoBalance( + total: b.total, unlocked: b.unlocked, decimalPoint: asset.decimalPoint); + } else { + balance[asset] = ZanoBalance( + total: b.total, unlocked: b.unlocked, decimalPoint: asset.decimalPoint); + } + } + } + await updateTransactions(); + // removing balances for assets missing in wallet info balances + balance.removeWhere( + (key, _) => + key != CryptoCurrency.zano && + !walletInfo.wi.balances + .any((element) => element.assetId == (key as ZanoAsset).assetId), + ); + } + } +} diff --git a/cw_zano/lib/zano_wallet_addresses.dart b/cw_zano/lib/zano_wallet_addresses.dart new file mode 100644 index 000000000..39e61be7f --- /dev/null +++ b/cw_zano/lib/zano_wallet_addresses.dart @@ -0,0 +1,41 @@ +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_core/wallet_addresses.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_zano/zano_wallet_api.dart'; +import 'package:mobx/mobx.dart'; + +part 'zano_wallet_addresses.g.dart'; + +class ZanoWalletAddresses = ZanoWalletAddressesBase with _$ZanoWalletAddresses; + +abstract class ZanoWalletAddressesBase extends WalletAddresses with Store { + ZanoWalletAddressesBase(WalletInfo walletInfo) + : address = '', + super(walletInfo); + + @override + @observable + String address; + + @override + Future init() async { + address = walletInfo.address; + await updateAddressesInBox(); + } + + Future updateAddress(String address) async { + this.address = address; + await updateAddressesInBox(); + } + + @override + Future updateAddressesInBox() async { + try { + addressesMap.clear(); + addressesMap[address] = ''; + await saveAddressesInBox(); + } catch (e) { + printV(e.toString()); + } + } +} diff --git a/cw_zano/lib/zano_wallet_api.dart b/cw_zano/lib/zano_wallet_api.dart new file mode 100644 index 000000000..f2c5469c4 --- /dev/null +++ b/cw_zano/lib/zano_wallet_api.dart @@ -0,0 +1,511 @@ +import 'dart:convert' as convert; +import 'dart:ffi'; +import 'dart:io'; +import 'dart:isolate'; + +import 'package:cw_core/transaction_priority.dart'; +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_core/zano_asset.dart'; +import 'package:cw_zano/api/consts.dart'; +import 'package:cw_zano/api/model/asset_id_params.dart'; +import 'package:cw_zano/api/model/create_wallet_result.dart'; +import 'package:cw_zano/api/model/destination.dart'; +import 'package:cw_zano/api/model/get_address_info_result.dart'; +import 'package:cw_zano/api/model/get_recent_txs_and_info_params.dart'; +import 'package:cw_zano/api/model/get_recent_txs_and_info_result.dart'; +import 'package:cw_zano/api/model/get_wallet_info_result.dart'; +import 'package:cw_zano/api/model/get_wallet_status_result.dart'; +import 'package:cw_zano/api/model/proxy_to_daemon_params.dart'; +import 'package:cw_zano/api/model/proxy_to_daemon_result.dart'; +import 'package:cw_zano/api/model/store_result.dart'; +import 'package:cw_zano/api/model/transfer.dart'; +import 'package:cw_zano/api/model/transfer_params.dart'; +import 'package:cw_zano/api/model/transfer_result.dart'; +import 'package:cw_zano/zano_wallet_exceptions.dart'; +import 'package:ffi/ffi.dart'; +import 'package:json_bigint/json_bigint.dart'; +import 'package:monero/zano.dart' as zano; +import 'package:monero/src/generated_bindings_zano.g.dart' as zanoapi; +import 'package:path/path.dart' as p; + +mixin ZanoWalletApi { + static const _maxReopenAttempts = 5; + static const _logInfo = false; + static const int _zanoMixinValue = 10; + + int _hWallet = 0; + + int get hWallet => _hWallet; + + set hWallet(int value) { + _hWallet = value; + } + + int getCurrentTxFee(TransactionPriority priority) => zano.PlainWallet_getCurrentTxFee(priority.raw); + + void setPassword(String password) => zano.PlainWallet_resetWalletPassword(hWallet, password); + + void closeWallet(int? walletToClose, {bool force = false}) async { + printV('close_wallet ${walletToClose ?? hWallet}: $force'); + if (Platform.isWindows || force) { + final result = await _closeWallet(walletToClose ?? hWallet); + printV('close_wallet result $result'); + openWalletCache.removeWhere((_, cwr) => cwr.walletId == (walletToClose ?? hWallet)); + } + } + + static bool isInit = false; + + Future initWallet() async { + if (isInit) return true; + final result = zano.PlainWallet_init("", "", 0); + isInit = true; + return result == "OK"; + } + + Future setupNode(String nodeUrl) async { + await _setupNode(hWallet, nodeUrl); + return true; + } + + Future getWalletDir() async { + final walletInfoResult = await getWalletInfo(); + return Directory(p.dirname(walletInfoResult.wi.path)); + } + + Future _getWalletSecretsFile() async { + final dir = await getWalletDir(); + final file = File(p.join(dir.path, "zano-secrets.json.bin")); + return file; + } + + Future> _getSecrets() async { + final file = await _getWalletSecretsFile(); + if (!file.existsSync()) { + return {}; + } + final data = file.readAsBytesSync(); + final b64 = convert.base64.encode(data); + final respStr = await invokeMethod("decrypt_data", {"buff": "$b64"}); + final resp = convert.json.decode(respStr); + final dataBytes = convert.base64.decode(resp["result"]["res_buff"] as String); + final dataStr = convert.utf8.decode(dataBytes); + final dataObject = convert.json.decode(dataStr); + return dataObject as Map; + } + + Future _setSecrets(Map data) async { + final dataStr = convert.json.encode(data); + final b64 = convert.base64.encode(convert.utf8.encode(dataStr)); + final respStr = await invokeMethod("encrypt_data", {"buff": "$b64"}); + final resp = convert.json.decode(respStr); + final dataBytes = convert.base64.decode(resp["result"]["res_buff"] as String); + final file = await _getWalletSecretsFile(); + file.writeAsBytesSync(dataBytes); + } + + Future _getWalletSecret(String key) async { + final secrets = await _getSecrets(); + return secrets[key] as String?; + } + + Future _setWalletSecret(String key, String value) async { + final secrets = await _getSecrets(); + secrets[key] = value; + await _setSecrets(secrets); + } + + Future getPassphrase() async { + return await _getWalletSecret("passphrase"); + } + + Future setPassphrase(String passphrase) { + return _setWalletSecret("passphrase", passphrase); + } + + Future getSeed() async { + final passphrase = await getPassphrase(); + final respStr = await invokeMethod("get_restore_info", {"seed_password": passphrase??""}); + final resp = convert.json.decode(respStr); + return resp["result"]["seed_phrase"] as String; + } + + Future getWalletInfo() async { + final json = await _getWalletInfo(hWallet); + final result = GetWalletInfoResult.fromJson(jsonDecode(json)); + printV('get_wallet_info got ${result.wi.balances.length} balances: ${result.wi.balances}'); + return result; + } + + Future getWalletStatus() async { + final json = await _getWalletStatus(hWallet); + if (json == Consts.errorWalletWrongId) { + printV('wrong wallet id'); + throw ZanoWalletException('Wrong wallet id'); + } + final status = GetWalletStatusResult.fromJson(jsonDecode(json)); + if (_logInfo) + printV( + 'get_wallet_status connected: ${status.isDaemonConnected} in refresh: ${status.isInLongRefresh} progress: ${status.progress} wallet state: ${status.walletState} sync: ${status.currentWalletHeight}/${status.currentDaemonHeight} ${(status.currentWalletHeight/status.currentDaemonHeight*100).toStringAsFixed(2)}%'); + return status; + } + + Future invokeMethod(String methodName, Object params) async { + final request = jsonEncode({ + "method": methodName, + "params": params, + }); + final invokeResult = await callSyncMethod('invoke', hWallet, request); + try { + jsonDecode(invokeResult); + } catch (e) { + if (invokeResult.contains(Consts.errorWalletWrongId)) throw ZanoWalletException('Wrong wallet id'); + printV('exception in parsing json in invokeMethod: $invokeResult'); + rethrow; + } + return invokeResult; + } + + Future> getAssetsWhitelist() async { + try { + final json = await invokeMethod('assets_whitelist_get', '{}'); + final map = jsonDecode(json) as Map?; + _checkForErrors(map); + List assets(String type, bool isGlobalWhitelist) => + (map?['result']?[type] as List?) + ?.map((e) => ZanoAsset.fromJson(e as Map, isInGlobalWhitelist: isGlobalWhitelist)) + .toList() ?? + []; + final localWhitelist = assets('local_whitelist', false); + final globalWhitelist = assets('global_whitelist', true); + final ownAssets = assets('own_assets', false); + if (_logInfo) + printV('assets_whitelist_get got local whitelist: ${localWhitelist.length} ($localWhitelist); ' + 'global whitelist: ${globalWhitelist.length} ($globalWhitelist); ' + 'own assets: ${ownAssets.length} ($ownAssets)'); + return [...globalWhitelist, ...localWhitelist, ...ownAssets]; + } catch (e) { + printV('assets_whitelist_get $e'); + return []; + // rethrow; + } + } + + Future addAssetsWhitelist(String assetId) async { + try { + final json = await invokeMethod('assets_whitelist_add', AssetIdParams(assetId: assetId)); + final map = jsonDecode(json) as Map?; + _checkForErrors(map); + if (map!['result']!['status']! == 'OK') { + final assetDescriptor = ZanoAsset.fromJson(map['result']!['asset_descriptor']! as Map); + printV('assets_whitelist_add added ${assetDescriptor.fullName} ${assetDescriptor.ticker}'); + return assetDescriptor; + } else { + printV('assets_whitelist_add status ${map['result']!['status']!}'); + return null; + } + } catch (e) { + printV('assets_whitelist_add $e'); + return null; + } + } + + Future removeAssetsWhitelist(String assetId) async { + try { + final json = await invokeMethod('assets_whitelist_remove', AssetIdParams(assetId: assetId)); + final map = jsonDecode(json) as Map?; + _checkForErrors(map); + printV('assets_whitelist_remove status ${map!['result']!['status']!}'); + return (map['result']!['status']! == 'OK'); + } catch (e) { + printV('assets_whitelist_remove $e'); + return false; + } + } + + Future _proxyToDaemon(String uri, String body) async { + final json = await invokeMethod('proxy_to_daemon', ProxyToDaemonParams(body: body, uri: uri)); + final map = jsonDecode(json) as Map?; + _checkForErrors(map); + return ProxyToDaemonResult.fromJson(map!['result'] as Map); + } + + Future getAssetInfo(String assetId) async { + final methodName = 'get_asset_info'; + final params = AssetIdParams(assetId: assetId); + final result = await _proxyToDaemon('/json_rpc', '{"method": "$methodName","params": ${jsonEncode(params)}}'); + if (result == null) { + printV('get_asset_info empty result'); + return null; + } + final map = jsonDecode(result.body) as Map?; + if (map!['error'] != null) { + printV('get_asset_info $assetId error ${map['error']!['code']} ${map['error']!['message']}'); + return null; + } else if (map['result']!['status']! == 'OK') { + final assetDescriptor = ZanoAsset.fromJson(map['result']!['asset_descriptor']! as Map); + printV('get_asset_info $assetId ${assetDescriptor.fullName} ${assetDescriptor.ticker}'); + return assetDescriptor; + } else { + printV('get_asset_info $assetId status ${map['result']!['status']!}'); + return null; + } + } + + Future store() async { + try { + final json = await invokeMethod('store', {}); + final map = jsonDecode(json) as Map?; + _checkForErrors(map); + return StoreResult.fromJson(map!['result'] as Map); + } catch (e) { + printV('store $e'); + return null; + } + } + + Future getRecentTxsAndInfo({required int offset, required int count}) async { + printV('get_recent_txs_and_info $offset $count'); + try { + final json = await invokeMethod('get_recent_txs_and_info', GetRecentTxsAndInfoParams(offset: offset, count: count)); + final map = jsonDecode(json) as Map?; + _checkForErrors(map); + final lastItemIndex = map?['result']?['last_item_index'] as int?; + final totalTransfers = map?['result']?['total_transfers'] as int?; + final transfers = map?['result']?['transfers'] as List?; + if (transfers == null || lastItemIndex == null || totalTransfers == null) { + printV('get_recent_txs_and_info empty transfers'); + return GetRecentTxsAndInfoResult.empty(); + } + printV('get_recent_txs_and_info transfers.length: ${transfers.length}'); + return GetRecentTxsAndInfoResult( + transfers: transfers.map((e) => Transfer.fromJson(e as Map)).toList(), + lastItemIndex: lastItemIndex, + totalTransfers: totalTransfers, + ); + } catch (e) { + printV('get_recent_txs_and_info $e'); + return GetRecentTxsAndInfoResult.empty(); + } + } + + GetAddressInfoResult getAddressInfo(String address) => GetAddressInfoResult.fromJson( + jsonDecode(zano.PlainWallet_getAddressInfo(address)), + ); + + String _shorten(String s) => s.length > 10 ? '${s.substring(0, 4)}...${s.substring(s.length - 4)}' : s; + + Future createWallet(String path, String password) async { + printV('create_wallet path $path password ${_shorten(password)}'); + final json = zano.PlainWallet_generate(path, password); + final map = jsonDecode(json) as Map?; + if (map?['error'] != null) { + final code = map!['error']?['code'] ?? ''; + final message = map['error']?['message'] ?? ''; + throw ZanoWalletException('Error creating wallet file, $message ($code)'); + } + if (map?['result'] == null) { + throw ZanoWalletException('Error creating wallet file, empty response'); + } + final result = CreateWalletResult.fromJson(map!['result'] as Map); + openWalletCache[path] = result; + printV('create_wallet ${result.name}'); + return result; + } + + Future restoreWalletFromSeed(String path, String password, String seed, String? passphrase) async { + printV('restore_wallet path $path'); + final json = zano.PlainWallet_restore(seed, path, password, passphrase??''); + final map = jsonDecode(json) as Map?; + if (map?['error'] != null) { + final code = map!['error']!['code'] ?? ''; + final message = map['error']!['message'] ?? ''; + if (code == Consts.errorWrongSeed) { + throw RestoreFromSeedsException('Error restoring wallet\nPlease check the seed words are correct. Additionally, if you created this wallet with a passphrase please add it under the Advanced Settings page.'); + } else if (code == Consts.errorAlreadyExists) { + throw RestoreFromSeedsException('Error restoring wallet, already exists'); + } + throw RestoreFromSeedsException('Error restoring wallet, $message ($code)'); + } + if (map?['result'] == null) { + throw RestoreFromSeedsException('Error restoring wallet, empty response'); + } + final result = CreateWalletResult.fromJson(map!['result'] as Map); + openWalletCache[path] = result; + printV('restore_wallet ${result.name} ${result.wi.address}'); + return result; + } + + Future loadWallet(String path, String password, [int attempt = 0]) async { + printV('load_wallet1 path $path'); + final String json; + try { + json = zano.PlainWallet_open(path, password); + } catch (e) { + printV('error in loadingWallet $e'); + rethrow; + } + + final map = jsonDecode(json) as Map?; + if (map?['error'] != null) { + final code = map?['error']!['code'] ?? ''; + final message = map?['error']!['message'] ?? ''; + if (code == Consts.errorAlreadyExists && attempt <= _maxReopenAttempts) { + // already connected to this wallet. closing and trying to reopen + printV('already connected. closing and reopen wallet (attempt $attempt)'); + closeWallet(attempt, force: true); + await Future.delayed(const Duration(milliseconds: 500)); + return await loadWallet(path, password, attempt + 1); + } + throw ZanoWalletException('Error loading wallet, $message ($code)'); + } + if (map?['result'] == null) { + throw ZanoWalletException('Error loading wallet, empty response'); + } + final result = CreateWalletResult.fromJson(map!['result'] as Map); + printV('load_wallet3 ${result.name} ${result.wi.address}'); + openWalletCache[path] = result; + return result; + } + + static Map openWalletCache = {}; + + Future transfer(List destinations, BigInt fee, String comment) async { + final params = TransferParams( + destinations: destinations, + fee: fee, + mixin: _zanoMixinValue, + paymentId: '', + comment: comment, + pushPayer: false, + hideReceiver: true, + ); + final json = await invokeMethod('transfer', params); + final map = jsonDecode(json); + final resultMap = map as Map?; + if (resultMap != null) { + final transferResultMap = resultMap['result'] as Map?; + if (transferResultMap != null) { + final transferResult = TransferResult.fromJson(transferResultMap); + printV('transfer success hash ${transferResult.txHash}'); + return transferResult; + } else { + final errorCode = resultMap['error']?['code']; + final code = errorCode is int ? errorCode.toString() : errorCode as String? ?? ''; + final message = resultMap['error']?['message'] as String? ?? ''; + printV('transfer error $code $message'); + throw TransferException('Transfer error, $message ($code)'); + } + } + printV('transfer error empty result'); + throw TransferException('Transfer error, empty result'); + } + + void _checkForErrors(Map? map) { + if (map == null) { + throw ZanoWalletException('Empty response'); + } + final result = map['result']; + if (result == null) { + throw ZanoWalletException('Empty response'); + } + if (result['error'] != null) { + final code = result['error']!['code'] ?? ''; + final message = result['error']!['message'] ?? ''; + if (code == -1 && message == Consts.errorBusy) { + throw ZanoWalletBusyException(); + } + throw ZanoWalletException('Error, $message ($code)'); + } + } + +} + +Future callSyncMethod(String methodName, int hWallet, String params) async { + final params_ = params.toNativeUtf8().address; + final method_name_ = methodName.toNativeUtf8().address; + final invokeResult = await Isolate.run(() async { + final lib = zanoapi.ZanoC(DynamicLibrary.open(zano.libPath)); + final txid = lib.ZANO_PlainWallet_syncCall( + Pointer.fromAddress(method_name_).cast(), + hWallet, + Pointer.fromAddress(params_).cast() + ); + try { + final strPtr = txid.cast(); + final str = strPtr.toDartString(); + lib.ZANO_free(strPtr.cast()); + return str; + } catch (e) { + return ""; + } + }); + calloc.free(Pointer.fromAddress(method_name_)); + calloc.free(Pointer.fromAddress(params_)); + return invokeResult; +} + +Map jsonDecode(String json) { + try { + return decodeJson(json.replaceAll("\\/", "/")) as Map; + } catch (e) { + return convert.jsonDecode(json) as Map; + } +} + +String jsonEncode(Object? object) { + return convert.jsonEncode(object); +} + +Future _getWalletStatus(int hWallet) async { + final jsonPtr = await Isolate.run(() async { + final lib = zanoapi.ZanoC(DynamicLibrary.open(zano.libPath)); + final status = lib.ZANO_PlainWallet_getWalletStatus( + hWallet, + ); + return status.address; + }); + String json = ""; + try { + final strPtr = Pointer.fromAddress(jsonPtr).cast(); + final str = strPtr.toDartString(); + zano.ZANO_free(strPtr.cast()); + json = str; + } catch (e) { + json = ""; + } + return json; +} +Future _getWalletInfo(int hWallet) async { + final jsonPtr = await Isolate.run(() async { + final lib = zanoapi.ZanoC(DynamicLibrary.open(zano.libPath)); + final status = lib.ZANO_PlainWallet_getWalletInfo( + hWallet, + ); + return status.address; + }); + String json = ""; + try { + final strPtr = Pointer.fromAddress(jsonPtr).cast(); + final str = strPtr.toDartString(); + zano.ZANO_free(strPtr.cast()); + json = str; + } catch (e) { + json = ""; + } + return json; +} + +Future _setupNode(int hWallet, String nodeUrl) async { + await callSyncMethod("reset_connection_url", hWallet, nodeUrl); + await callSyncMethod("run_wallet", hWallet, ""); + return "OK"; +} + +Future _closeWallet(int hWallet) async { + final str = await Isolate.run(() async { + return zano.PlainWallet_closeWallet(hWallet); + }); + printV("Closing wallet: $str"); + return str; +} \ No newline at end of file diff --git a/cw_zano/lib/zano_wallet_exceptions.dart b/cw_zano/lib/zano_wallet_exceptions.dart new file mode 100644 index 000000000..a0a7b4939 --- /dev/null +++ b/cw_zano/lib/zano_wallet_exceptions.dart @@ -0,0 +1,21 @@ +import 'package:cw_core/exceptions.dart'; + +class ZanoWalletException implements Exception { + final String message; + + ZanoWalletException(this.message); + @override + String toString() => '${this.runtimeType} (message: $message)'; +} + +class RestoreFromSeedsException extends RestoreFromSeedException { + RestoreFromSeedsException(String message) : super(message); +} + +class TransferException extends ZanoWalletException { + TransferException(String message): super(message); +} + +class ZanoWalletBusyException extends ZanoWalletException { + ZanoWalletBusyException(): super(''); +} \ No newline at end of file diff --git a/cw_zano/lib/zano_wallet_service.dart b/cw_zano/lib/zano_wallet_service.dart new file mode 100644 index 000000000..879402cff --- /dev/null +++ b/cw_zano/lib/zano_wallet_service.dart @@ -0,0 +1,124 @@ +import 'dart:io'; + +import 'package:collection/collection.dart'; +import 'package:cw_core/pathForWallet.dart'; +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_credentials.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/wallet_service.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:cw_zano/zano_wallet.dart'; +import 'package:cw_zano/zano_wallet_api.dart'; +import 'package:hive/hive.dart'; +import 'package:monero/zano.dart' as zano; + +class ZanoNewWalletCredentials extends WalletCredentials { + ZanoNewWalletCredentials({required String name, String? password, required String? passphrase}) : super(name: name, password: password, passphrase: passphrase); +} + +class ZanoRestoreWalletFromSeedCredentials extends WalletCredentials { + ZanoRestoreWalletFromSeedCredentials({required String name, required String password, required String passphrase, required int height, required this.mnemonic}) + : super(name: name, password: password, passphrase: passphrase, height: height); + + final String mnemonic; +} + +class ZanoRestoreWalletFromKeysCredentials extends WalletCredentials { + ZanoRestoreWalletFromKeysCredentials( + {required String name, + required String password, + required this.language, + required this.address, + required this.viewKey, + required this.spendKey, + required int height}) + : super(name: name, password: password, height: height); + + final String language; + final String address; + final String viewKey; + final String spendKey; +} + +class ZanoWalletService extends WalletService { + ZanoWalletService(this.walletInfoSource); + + final Box walletInfoSource; + + static bool walletFilesExist(String path) => !File(path).existsSync() && !File('$path.keys').existsSync(); + + int hWallet = 0; + + @override + WalletType getType() => WalletType.zano; + + @override + Future create(WalletCredentials credentials, {bool? isTestnet}) async { + printV('zanowallet service create isTestnet $isTestnet'); + return await ZanoWalletBase.create(credentials: credentials); + } + + @override + Future isWalletExit(String name) async { + final path = await pathForWallet(name: name, type: getType()); + return zano.PlainWallet_isWalletExist(path); + } + + @override + Future openWallet(String name, String password) async { + final walletInfo = walletInfoSource.values.firstWhereOrNull((info) => info.id == WalletBase.idFor(name, getType()))!; + try { + final wallet = await ZanoWalletBase.open(name: name, password: password, walletInfo: walletInfo); + saveBackup(name); + return wallet; + } catch (e) { + await restoreWalletFilesFromBackup(name); + return await ZanoWalletBase.open(name: name, password: password, walletInfo: walletInfo); + } + } + + @override + Future remove(String wallet) async { + final path = await pathForWalletDir(name: wallet, type: getType()); + final file = Directory(path); + final isExist = file.existsSync(); + + if (isExist) { + await file.delete(recursive: true); + } + + final walletInfo = walletInfoSource.values.firstWhere((info) => info.id == WalletBase.idFor(wallet, getType())); + await walletInfoSource.delete(walletInfo.key); + } + + @override + Future rename(String currentName, String password, String newName) async { + final currentWalletInfo = walletInfoSource.values.firstWhere((info) => info.id == WalletBase.idFor(currentName, getType())); + final currentWallet = ZanoWallet(currentWalletInfo, password); + + await currentWallet.renameWalletFiles(newName); + + final newWalletInfo = currentWalletInfo; + newWalletInfo.id = WalletBase.idFor(newName, getType()); + newWalletInfo.name = newName; + + await walletInfoSource.put(currentWalletInfo.key, newWalletInfo); + } + + @override + Future restoreFromKeys(ZanoRestoreWalletFromKeysCredentials credentials, {bool? isTestnet}) async { + throw UnimplementedError(); + } + + @override + Future restoreFromSeed(ZanoRestoreWalletFromSeedCredentials credentials, {bool? isTestnet}) async { + return ZanoWalletBase.restore(credentials: credentials); + } + + @override + Future restoreFromHardwareWallet(ZanoNewWalletCredentials credentials) { + throw UnimplementedError("Restoring a Zano wallet from a hardware wallet is not yet supported!"); + } +} diff --git a/cw_zano/pubspec.lock b/cw_zano/pubspec.lock new file mode 100644 index 000000000..74a87487e --- /dev/null +++ b/cw_zano/pubspec.lock @@ -0,0 +1,827 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: "4897882604d919befd350648c7f91926a9d5de99e67b455bf0917cc2362f4bb8" + url: "https://pub.dev" + source: hosted + version: "47.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "690e335554a8385bc9d787117d9eb52c0c03ee207a607e593de3c9d71b1cfe80" + url: "https://pub.dev" + source: hosted + version: "4.7.0" + args: + dependency: transitive + description: + name: args + sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6 + url: "https://pub.dev" + source: hosted + version: "2.6.0" + asn1lib: + dependency: transitive + description: + name: asn1lib + sha256: "4bae5ae63e6d6dd17c4aac8086f3dec26c0236f6a0f03416c6c19d830c367cf5" + url: "https://pub.dev" + source: hosted + version: "1.5.8" + 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" + build: + dependency: transitive + description: + name: build + sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + build_config: + dependency: transitive + description: + name: build_config + sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 + url: "https://pub.dev" + source: hosted + version: "1.1.1" + build_daemon: + dependency: transitive + description: + name: build_daemon + sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" + url: "https://pub.dev" + source: hosted + version: "4.0.2" + build_resolvers: + dependency: "direct dev" + description: + name: build_resolvers + sha256: "687cf90a3951affac1bd5f9ecb5e3e90b60487f3d9cdc359bb310f8876bb02a6" + url: "https://pub.dev" + source: hosted + version: "2.0.10" + build_runner: + dependency: "direct dev" + description: + name: build_runner + sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d" + url: "https://pub.dev" + source: hosted + version: "2.4.13" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + sha256: "6d6ee4276b1c5f34f21fdf39425202712d2be82019983d52f351c94aafbc2c41" + url: "https://pub.dev" + source: hosted + version: "7.2.10" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb + url: "https://pub.dev" + source: hosted + version: "8.9.2" + cake_backup: + dependency: transitive + description: + path: "." + ref: main + resolved-ref: "3aba867dcab6737f6707782f5db15d71f303db38" + url: "https://github.com/cake-tech/cake_backup.git" + source: git + version: "1.0.0+1" + characters: + dependency: transitive + description: + name: characters + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.dev" + source: hosted + version: "1.3.0" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff + url: "https://pub.dev" + source: hosted + version: "2.0.3" + clock: + dependency: transitive + description: + name: clock + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" + source: hosted + version: "1.1.1" + code_builder: + dependency: transitive + description: + name: code_builder + sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e" + url: "https://pub.dev" + source: hosted + version: "4.10.1" + collection: + dependency: transitive + description: + name: collection + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + url: "https://pub.dev" + source: hosted + version: "1.18.0" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + crypto: + dependency: transitive + description: + name: crypto + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" + url: "https://pub.dev" + source: hosted + version: "3.0.6" + cryptography: + dependency: transitive + description: + name: cryptography + sha256: d146b76d33d94548cf035233fbc2f4338c1242fa119013bead807d033fc4ae05 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + cupertino_icons: + dependency: transitive + description: + name: cupertino_icons + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 + url: "https://pub.dev" + source: hosted + version: "1.0.8" + cw_core: + dependency: "direct main" + description: + path: "../cw_core" + relative: true + source: path + version: "0.0.1" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: "7a03456c3490394c8e7665890333e91ae8a49be43542b616e414449ac358acd4" + url: "https://pub.dev" + source: hosted + version: "2.2.4" + decimal: + dependency: "direct main" + description: + name: decimal + sha256: "24a261d5d5c87e86c7651c417a5dbdf8bcd7080dd592533910e8d0505a279f21" + url: "https://pub.dev" + source: hosted + version: "2.3.3" + encrypt: + dependency: transitive + description: + name: encrypt + sha256: "62d9aa4670cc2a8798bab89b39fc71b6dfbacf615de6cf5001fb39f7e4a996a2" + url: "https://pub.dev" + source: hosted + version: "5.0.3" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" + source: hosted + version: "1.3.1" + ffi: + dependency: "direct main" + description: + name: ffi + sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" + url: "https://pub.dev" + source: hosted + version: "2.1.3" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_mobx: + dependency: "direct main" + description: + name: flutter_mobx + sha256: "859fbf452fa9c2519d2700b125dd7fb14c508bbdd7fb65e26ca8ff6c92280e2e" + url: "https://pub.dev" + source: hosted + version: "2.2.1+1" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + fluttertoast: + dependency: "direct main" + description: + name: fluttertoast + sha256: "95f349437aeebe524ef7d6c9bde3e6b4772717cf46a0eb6a3ceaddc740b297cc" + url: "https://pub.dev" + source: hosted + version: "8.2.8" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.dev" + source: hosted + version: "4.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + graphs: + dependency: transitive + description: + name: graphs + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + hive: + dependency: transitive + description: + name: hive + sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941" + url: "https://pub.dev" + source: hosted + version: "2.2.3" + hive_generator: + dependency: "direct dev" + description: + name: hive_generator + sha256: "81fd20125cb2ce8fd23623d7744ffbaf653aae93706c9bd3bf7019ea0ace3938" + url: "https://pub.dev" + source: hosted + version: "1.1.3" + http: + dependency: "direct main" + description: + name: http + sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 + url: "https://pub.dev" + source: hosted + version: "1.2.2" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" + source: hosted + version: "4.0.2" + intl: + dependency: "direct main" + description: + name: intl + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + url: "https://pub.dev" + source: hosted + version: "0.19.0" + io: + dependency: transitive + description: + name: io + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b + url: "https://pub.dev" + source: hosted + version: "1.0.5" + js: + dependency: transitive + description: + name: js + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" + source: hosted + version: "0.6.7" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + url: "https://pub.dev" + source: hosted + version: "4.9.0" + json_bigint: + dependency: "direct main" + description: + name: json_bigint + sha256: "9e613e731847ab2154d67160682adf104cbd9863741ec2f7abfcf6e77c70592f" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" + url: "https://pub.dev" + source: hosted + version: "10.0.5" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" + url: "https://pub.dev" + source: hosted + version: "3.0.5" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + 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: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + url: "https://pub.dev" + source: hosted + version: "1.15.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + mobx: + dependency: "direct main" + description: + name: mobx + sha256: "1f01a429529ac55e5e80c0fcad62c60112fb91df3dec11a9113d71cf0c2e2c4c" + url: "https://pub.dev" + source: hosted + version: "2.4.0" + mobx_codegen: + dependency: "direct dev" + description: + name: mobx_codegen + sha256: d4beb9cea4b7b014321235f8fdc7c2193ee0fe1d1198e9da7403f8bc85c4407c + url: "https://pub.dev" + source: hosted + version: "2.3.0" + monero: + dependency: "direct main" + description: + path: "impls/monero.dart" + ref: "65608c09e9093f1cd42c6afd8e9131016c82574b" + resolved-ref: "65608c09e9093f1cd42c6afd8e9131016c82574b" + url: "https://github.com/mrcyjanek/monero_c" + source: git + version: "0.0.0" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + package_config: + dependency: transitive + description: + name: package_config + sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + path: + dependency: transitive + description: + name: path + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + url: "https://pub.dev" + source: hosted + version: "1.9.0" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2" + url: "https://pub.dev" + source: hosted + version: "2.2.15" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + 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: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + pointycastle: + dependency: transitive + description: + name: pointycastle + sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe" + url: "https://pub.dev" + source: hosted + version: "3.9.1" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" + source: hosted + version: "1.5.1" + provider: + dependency: transitive + description: + name: provider + sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c + url: "https://pub.dev" + source: hosted + version: "6.1.2" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "7b3cfbf654f3edd0c6298ecd5be782ce997ddf0e00531b9464b55245185bbbbd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + rational: + dependency: transitive + description: + name: rational + sha256: cb808fb6f1a839e6fc5f7d8cb3b0a10e1db48b3be102de73938c627f0b636336 + url: "https://pub.dev" + source: hosted + version: "2.2.3" + shelf: + dependency: transitive + description: + name: shelf + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + url: "https://pub.dev" + source: hosted + version: "1.4.1" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + socks5_proxy: + dependency: transitive + description: + name: socks5_proxy + sha256: "616818a0ea1064a4823b53c9f7eaf8da64ed82dcd51ed71371c7e54751ed5053" + url: "https://pub.dev" + source: hosted + version: "1.0.6" + source_gen: + dependency: transitive + description: + name: source_gen + sha256: "2d79738b6bbf38a43920e2b8d189e9a3ce6cc201f4b8fc76be5e4fe377b1c38d" + url: "https://pub.dev" + source: hosted + version: "1.2.6" + source_helper: + dependency: transitive + description: + name: source_helper + sha256: "3b67aade1d52416149c633ba1bb36df44d97c6b51830c2198e934e3fca87ca1f" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + 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" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + 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: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" + url: "https://pub.dev" + source: hosted + version: "0.7.2" + timing: + dependency: transitive + description: + name: timing + sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" + url: "https://pub.dev" + source: hosted + version: "1.0.2" + tuple: + dependency: transitive + description: + name: tuple + sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151 + url: "https://pub.dev" + source: hosted + version: "2.0.2" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + unorm_dart: + dependency: transitive + description: + name: unorm_dart + sha256: "23d8bf65605401a6a32cff99435fed66ef3dab3ddcad3454059165df46496a3b" + url: "https://pub.dev" + source: hosted + version: "0.3.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: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc + url: "https://pub.dev" + source: hosted + version: "14.2.4" + watcher: + dependency: "direct overridden" + description: + name: watcher + sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + web: + dependency: transitive + description: + name: web + sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb + url: "https://pub.dev" + source: hosted + version: "1.1.0" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" + url: "https://pub.dev" + source: hosted + version: "0.1.6" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + yaml: + dependency: transitive + description: + name: yaml + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + url: "https://pub.dev" + source: hosted + version: "3.1.2" +sdks: + dart: ">=3.5.0 <4.0.0" + flutter: ">=3.24.0" diff --git a/cw_zano/pubspec.yaml b/cw_zano/pubspec.yaml new file mode 100644 index 000000000..111731f4f --- /dev/null +++ b/cw_zano/pubspec.yaml @@ -0,0 +1,81 @@ +name: cw_zano +description: A new flutter plugin project. +version: 0.0.1 +publish_to: none +author: Cake Wallet +homepage: https://cakewallet.com + +environment: + sdk: ">=2.19.0 <3.0.0" + flutter: ">=1.20.0" + +dependencies: + flutter: + sdk: flutter + ffi: ^2.0.1 + http: ^1.1.0 + path_provider: ^2.0.11 + mobx: ^2.1.4 + flutter_mobx: ^2.0.6+1 + intl: ^0.19.0 + decimal: ^2.3.3 + cw_core: + path: ../cw_core + json_bigint: ^3.0.0 + fluttertoast: ^8.2.8 + monero: + git: + url: https://github.com/mrcyjanek/monero_c + ref: 65608c09e9093f1cd42c6afd8e9131016c82574b # monero_c hash + path: impls/monero.dart +dev_dependencies: + flutter_test: + sdk: flutter + build_runner: ^2.4.7 + mobx_codegen: ^2.1.1 + build_resolvers: ^2.0.9 + hive_generator: ^1.1.3 + +dependency_overrides: + watcher: ^1.1.0 + + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter. +flutter: + # This section identifies this Flutter project as a plugin project. + # The 'pluginClass' and Android 'package' identifiers should not ordinarily + # be modified. They are used by the tooling to maintain consistency when + # adding or updating assets for this project. + # To add assets to your plugin package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/assets-and-images/#from-packages + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware. + + # To add custom fonts to your plugin package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/custom-fonts/#from-packages diff --git a/ios/.gitignore b/ios/.gitignore index 8ded86f14..87057e358 100644 --- a/ios/.gitignore +++ b/ios/.gitignore @@ -31,4 +31,5 @@ Runner/GeneratedPluginRegistrant.* !default.pbxuser !default.perspectivev3 -Mwebd.xcframework \ No newline at end of file +Mwebd.xcframework +*Wallet.xcframework \ No newline at end of file diff --git a/ios/MoneroWallet.framework/Info.plist b/ios/MoneroWallet.framework/Info.plist deleted file mode 100644 index 8858589f7..000000000 Binary files a/ios/MoneroWallet.framework/Info.plist and /dev/null differ diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 85d39167a..864d6c420 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -8,7 +8,6 @@ /* Begin PBXBuildFile section */ 0C44A71A2518EF8000B570ED /* decrypt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C44A7192518EF8000B570ED /* decrypt.swift */; }; - 0C50DFB92BF3CB56002B0EB3 /* MoneroWallet.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 0C50DFB82BF3CB56002B0EB3 /* MoneroWallet.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 0C9D68C9264854B60011B691 /* secRandom.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C9D68C8264854B60011B691 /* secRandom.swift */; }; 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 2193F104374FA2746CE8945B /* ResourceHelper.swift in Resources */ = {isa = PBXBuildFile; fileRef = 78D25C60B94E9D9E48D52E5E /* ResourceHelper.swift */; settings = {ASSET_TAGS = (BreezSDK, ); }; }; @@ -27,7 +26,9 @@ A1B4A70C9CFA13AB71662216 /* LnurlPay.swift in Resources */ = {isa = PBXBuildFile; fileRef = 7D3364C03978A8A74B6D586E /* LnurlPay.swift */; settings = {ASSET_TAGS = (BreezSDK, ); }; }; A3D5E17CC53DF13FA740DEFA /* RedeemSwap.swift in Resources */ = {isa = PBXBuildFile; fileRef = 9D2F2C9F2555316C95EE7EA3 /* RedeemSwap.swift */; settings = {ASSET_TAGS = (BreezSDK, ); }; }; B6C6E59403ACDE44724C12F4 /* ServiceConfig.swift in Resources */ = {isa = PBXBuildFile; fileRef = B3D5E78267F5F18D882FDC3B /* ServiceConfig.swift */; settings = {ASSET_TAGS = (BreezSDK, ); }; }; - CE291CFE2C15DB9A00B9F709 /* WowneroWallet.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = CE291CFD2C15DB9A00B9F709 /* WowneroWallet.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + CE918BF82D533ECE007F186E /* MoneroWallet.xcframework in CopyFiles */ = {isa = PBXBuildFile; fileRef = CE918BF72D533ECE007F186E /* MoneroWallet.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + CE918BFA2D533ED4007F186E /* WowneroWallet.xcframework in CopyFiles */ = {isa = PBXBuildFile; fileRef = CE918BF92D533ED4007F186E /* WowneroWallet.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + CE918BFC2D533ED8007F186E /* ZanoWallet.xcframework in CopyFiles */ = {isa = PBXBuildFile; fileRef = CE918BFB2D533ED8007F186E /* ZanoWallet.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; CEAFE4A02C53926F009FF3AD /* libresolv.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = C58D93382C00FAC6004BCF69 /* libresolv.tbd */; }; CFEFC24F82F78FE747DF1D22 /* LnurlPayInfo.swift in Resources */ = {isa = PBXBuildFile; fileRef = 58C22CBD8C22B9D6023D59F8 /* LnurlPayInfo.swift */; settings = {ASSET_TAGS = (BreezSDK, ); }; }; D0D7A0D4E13F31C4E02E235B /* ReceivePayment.swift in Resources */ = {isa = PBXBuildFile; fileRef = 91C524F800843E0A3F17E004 /* ReceivePayment.swift */; settings = {ASSET_TAGS = (BreezSDK, ); }; }; @@ -42,8 +43,9 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( - CE291CFE2C15DB9A00B9F709 /* WowneroWallet.framework in CopyFiles */, - 0C50DFB92BF3CB56002B0EB3 /* MoneroWallet.framework in CopyFiles */, + CE918BF82D533ECE007F186E /* MoneroWallet.xcframework in CopyFiles */, + CE918BFA2D533ED4007F186E /* WowneroWallet.xcframework in CopyFiles */, + CE918BFC2D533ED8007F186E /* ZanoWallet.xcframework in CopyFiles */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -53,7 +55,6 @@ 014D7E4DBCFD76DDE652A4D9 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 0C400E0F25B21ABB0025E469 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; }; 0C44A7192518EF8000B570ED /* decrypt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = decrypt.swift; sourceTree = ""; }; - 0C50DFB82BF3CB56002B0EB3 /* MoneroWallet.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = MoneroWallet.framework; sourceTree = ""; }; 0C9986A3251A932F00D566FD /* CryptoSwift.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = CryptoSwift.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 0C9D68C8264854B60011B691 /* secRandom.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = secRandom.swift; sourceTree = ""; }; 0CCA7ADAD6FF9185EBBB2BCA /* Constants.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Constants.swift; path = "../.symlinks/plugins/breez_sdk/ios/bindings-swift/Sources/BreezSDK/Constants.swift"; sourceTree = ""; }; @@ -84,8 +85,9 @@ ABD6FCBB0F4244B090459128 /* BreezSDK.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BreezSDK.swift; path = "../.symlinks/plugins/breez_sdk/ios/bindings-swift/Sources/BreezSDK/BreezSDK.swift"; sourceTree = ""; }; B3D5E78267F5F18D882FDC3B /* ServiceConfig.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ServiceConfig.swift; path = "../.symlinks/plugins/breez_sdk/ios/bindings-swift/Sources/BreezSDK/ServiceConfig.swift"; sourceTree = ""; }; C58D93382C00FAC6004BCF69 /* libresolv.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libresolv.tbd; path = usr/lib/libresolv.tbd; sourceTree = SDKROOT; }; - CE291CFD2C15DB9A00B9F709 /* WowneroWallet.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = WowneroWallet.framework; sourceTree = ""; }; - CEAFE49D2C539250009FF3AD /* Mwebd.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = Mwebd.xcframework; sourceTree = ""; }; + CE918BF72D533ECE007F186E /* MoneroWallet.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = MoneroWallet.xcframework; sourceTree = ""; }; + CE918BF92D533ED4007F186E /* WowneroWallet.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = WowneroWallet.xcframework; sourceTree = ""; }; + CE918BFB2D533ED8007F186E /* ZanoWallet.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = ZanoWallet.xcframework; sourceTree = ""; }; D139E30AEB36740C21C00A9E /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; D7CD6B6020744E8FA471915D /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; DCEA540E3586164FB47AD13E /* LnurlPayInvoice.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = LnurlPayInvoice.swift; path = "../.symlinks/plugins/breez_sdk/ios/bindings-swift/Sources/BreezSDK/Task/LnurlPayInvoice.swift"; sourceTree = ""; }; @@ -97,8 +99,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - CEAFE4A02C53926F009FF3AD /* libresolv.tbd in Frameworks */, 8B1F4FCAA5EB9F3A83D32D5F /* Pods_Runner.framework in Frameworks */, + CEAFE4A02C53926F009FF3AD /* libresolv.tbd in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -108,7 +110,9 @@ 06957875428D0F5AAE053765 /* Frameworks */ = { isa = PBXGroup; children = ( - CEAFE49D2C539250009FF3AD /* Mwebd.xcframework */, + CE918BFB2D533ED8007F186E /* ZanoWallet.xcframework */, + CE918BF92D533ED4007F186E /* WowneroWallet.xcframework */, + CE918BF72D533ECE007F186E /* MoneroWallet.xcframework */, C58D93382C00FAC6004BCF69 /* libresolv.tbd */, 0C9986A3251A932F00D566FD /* CryptoSwift.framework */, D7CD6B6020744E8FA471915D /* Pods_Runner.framework */, @@ -159,8 +163,6 @@ 97C146E51CF9000F007C117D = { isa = PBXGroup; children = ( - CE291CFD2C15DB9A00B9F709 /* WowneroWallet.framework */, - 0C50DFB82BF3CB56002B0EB3 /* MoneroWallet.framework */, 0C44A7182518EF4A00B570ED /* CakeWallet */, 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, @@ -495,6 +497,7 @@ FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", + "$(PROJECT_DIR)", ); INFOPLIST_FILE = Runner/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 12.0; @@ -641,6 +644,7 @@ FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", + "$(PROJECT_DIR)", ); INFOPLIST_FILE = Runner/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 12.0; @@ -679,6 +683,7 @@ FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", + "$(PROJECT_DIR)", ); INFOPLIST_FILE = Runner/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 12.0; diff --git a/ios/WowneroWallet.framework/Info.plist b/ios/WowneroWallet.framework/Info.plist deleted file mode 100644 index 61ab961f4..000000000 Binary files a/ios/WowneroWallet.framework/Info.plist and /dev/null differ diff --git a/ios/ZanoWallet.framework/ZanoWallet b/ios/ZanoWallet.framework/ZanoWallet deleted file mode 100644 index 701878274..000000000 Binary files a/ios/ZanoWallet.framework/ZanoWallet and /dev/null differ diff --git a/ios/monero_libwallet2_api_c.dylib b/ios/monero_libwallet2_api_c.dylib deleted file mode 120000 index 4ad2b0213..000000000 --- a/ios/monero_libwallet2_api_c.dylib +++ /dev/null @@ -1 +0,0 @@ -../scripts/monero_c/release/monero/host-apple-ios_libwallet2_api_c.dylib \ No newline at end of file diff --git a/ios/wownero_libwallet2_api_c.dylib b/ios/wownero_libwallet2_api_c.dylib deleted file mode 120000 index 9baa13cc8..000000000 --- a/ios/wownero_libwallet2_api_c.dylib +++ /dev/null @@ -1 +0,0 @@ -../scripts/monero_c/release/wownero/host-apple-ios_libwallet2_api_c.dylib \ No newline at end of file diff --git a/ios/zano_libwallet2_api_c.dylib b/ios/zano_libwallet2_api_c.dylib deleted file mode 120000 index ed324a208..000000000 --- a/ios/zano_libwallet2_api_c.dylib +++ /dev/null @@ -1 +0,0 @@ -../scripts/monero_c/release/zano/host-apple-ios_libwallet2_api_c.dylib \ No newline at end of file diff --git a/lib/buy/moonpay/moonpay_provider.dart b/lib/buy/moonpay/moonpay_provider.dart index 5794e0794..959d38a57 100644 --- a/lib/buy/moonpay/moonpay_provider.dart +++ b/lib/buy/moonpay/moonpay_provider.dart @@ -82,6 +82,8 @@ class MoonPayProvider extends BuyProvider { return 'light'; case ThemeType.dark: return 'dark'; + case ThemeType.oled: + return 'dark'; } } diff --git a/lib/core/address_validator.dart b/lib/core/address_validator.dart index dbb6f9541..65c9faff9 100644 --- a/lib/core/address_validator.dart +++ b/lib/core/address_validator.dart @@ -2,9 +2,9 @@ import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/core/validator.dart'; import 'package:cake_wallet/solana/solana.dart'; +import 'package:cake_wallet/zano/zano.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/erc20_token.dart'; - const BEFORE_REGEX = '(^|\\s)'; const AFTER_REGEX = '(\$|\\s)'; @@ -19,7 +19,9 @@ class AddressValidator extends TextValidator { ? BitcoinNetwork.mainnet : LitecoinNetwork.mainnet, ) - : null, + : type == CryptoCurrency.zano + ? zano?.validateAddress + : null, pattern: getPattern(type), length: getLength(type)); @@ -132,6 +134,8 @@ class AddressValidator extends TextValidator { pattern = 'D([1-9a-km-zA-HJ-NP-Z]){33}'; case CryptoCurrency.btcln: pattern = '(lnbc|LNBC)([0-9]{1,}[a-zA-Z0-9]+)'; + case CryptoCurrency.zano: + pattern = r'([1-9A-HJ-NP-Za-km-z]{90,200})|(@[\w\d.-]+)'; default: return ''; } @@ -271,6 +275,7 @@ class AddressValidator extends TextValidator { return [64]; case CryptoCurrency.btcln: case CryptoCurrency.kaspa: + case CryptoCurrency.zano: default: return null; } @@ -310,6 +315,8 @@ class AddressValidator extends TextValidator { pattern = '[1-9A-HJ-NP-Za-km-z]+'; case CryptoCurrency.trx: pattern = '(T|t)[1-9A-HJ-NP-Za-km-z]{33}'; + case CryptoCurrency.zano: + pattern = '([1-9A-HJ-NP-Za-km-z]{90,200})|(@[\w\d.-]+)'; default: if (type.tag == CryptoCurrency.eth.title) { pattern = '0x[0-9a-zA-Z]{42}'; diff --git a/lib/core/amount_validator.dart b/lib/core/amount_validator.dart index 38983dfb2..f85df483f 100644 --- a/lib/core/amount_validator.dart +++ b/lib/core/amount_validator.dart @@ -76,6 +76,8 @@ class DecimalAmountValidator extends TextValidator { return '^([0-9]+([.\,][0-9]{1,12})?|[.\,][0-9]{1,12})\$'; case CryptoCurrency.btc: return '^([0-9]+([.\,][0-9]{1,8})?|[.\,][0-9]{1,8})\$'; + case CryptoCurrency.zano: + return '^([0-9]+([.\,][0-9]{1,12})?|[.\,][0-9]{1,18})\$'; default: return '^([0-9]+([.\,][0-9]{1,12})?|[.\,][0-9]{1,12})\$'; } diff --git a/lib/core/seed_validator.dart b/lib/core/seed_validator.dart index 2d2a0c6ee..d963fb523 100644 --- a/lib/core/seed_validator.dart +++ b/lib/core/seed_validator.dart @@ -9,6 +9,7 @@ import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/solana/solana.dart'; import 'package:cake_wallet/tron/tron.dart'; import 'package:cake_wallet/wownero/wownero.dart'; +import 'package:cake_wallet/zano/zano.dart'; import 'package:cake_wallet/utils/language_list.dart'; import 'package:cw_core/wallet_type.dart'; @@ -21,7 +22,8 @@ class SeedValidator extends Validator { final String language; final List _words; - static List getWordList({required WalletType type, required String language}) { + static List getWordList( + {required WalletType type, required String language}) { switch (type) { case WalletType.bitcoin: return getBitcoinWordList(language); @@ -46,6 +48,8 @@ class SeedValidator extends Validator { return tron!.getTronWordList(language); case WalletType.wownero: return wownero!.getWowneroWordList(language); + case WalletType.zano: + return zano!.getWordList(language); case WalletType.none: return []; } diff --git a/lib/core/wallet_creation_service.dart b/lib/core/wallet_creation_service.dart index 3ee630b33..3fb4b5b1d 100644 --- a/lib/core/wallet_creation_service.dart +++ b/lib/core/wallet_creation_service.dart @@ -89,6 +89,7 @@ class WalletCreationService { case WalletType.haven: case WalletType.nano: case WalletType.banano: + case WalletType.zano: return false; } } diff --git a/lib/di.dart b/lib/di.dart index 882d88108..47425cf49 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -246,6 +246,7 @@ import 'package:cake_wallet/view_model/wallet_seed_view_model.dart'; import 'package:cake_wallet/view_model/wallet_unlock_loadable_view_model.dart'; import 'package:cake_wallet/view_model/wallet_unlock_verifiable_view_model.dart'; import 'package:cake_wallet/wownero/wownero.dart'; +import 'package:cake_wallet/zano/zano.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; @@ -1088,6 +1089,8 @@ Future setup({ _walletInfoSource, SettingsStoreBase.walletPasswordDirectInput); case WalletType.wownero: return wownero!.createWowneroWalletService(_walletInfoSource, _unspentCoinsInfoSource); + case WalletType.zano: + return zano!.createZanoWalletService(_walletInfoSource); case WalletType.none: throw Exception('Unexpected token: ${param1.toString()} for generating of WalletService'); } @@ -1416,4 +1419,4 @@ Future setup({ getIt.registerFactory(() => SeedVerificationPage(getIt.get())); _isSetupFinished = true; -} +} \ No newline at end of file diff --git a/lib/entities/bip_353_record.dart b/lib/entities/bip_353_record.dart new file mode 100644 index 000000000..80c0099a8 --- /dev/null +++ b/lib/entities/bip_353_record.dart @@ -0,0 +1,151 @@ +import 'package:basic_utils/basic_utils.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/widgets/picker.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:flutter/material.dart'; + +class Bip353Record { + Bip353Record({ + required this.uri, + required this.domain, + }); + + final String uri; + final String domain; + + static const Map keyDisplayMap = { + 'lno': 'BOLT 12 Offer', + 'sp': 'Silent Payment', + 'address': 'On-Chain Address', + }; + + static Future?> fetchUriByCryptoCurrency( + String bip353Name, String asset) async { + try { + // 1. Parse the user and domain from "user@domain" + final parts = bip353Name.split('@'); + + if (parts.length != 2) return null; + + final userPart = parts[0]; + final domainPart = parts[1]; + + // 2. Construct the correct subdomain: "user._bitcoin-payment.domain" + final bip353Domain = '$userPart.user._bitcoin-payment.$domainPart'; + + // 3. Lookup the TXT record with DNSSEC + final txtRecords = await DnsUtils.lookupRecord( + bip353Domain, + RRecordType.TXT, + dnssec: true, + ); + + if (txtRecords == null) return null; + + final assetName = CryptoCurrency.fromString(asset).fullName; + if (assetName == null) throw Exception('Invalid asset name'); + final formattedAssetName = assetName.toLowerCase().replaceAll(' ', '') + ':'; + + for (final record in txtRecords) { + final data = record.data.replaceAll('"', ''); + if (data.startsWith(formattedAssetName)) { + return _parseAssetUri(data, formattedAssetName); + } + } + } catch (e) { + printV('BIP353Record.fetchBitcoinUri error: $e'); + } + return null; + } + + static Map? _parseAssetUri(String fullUri, String prefix) { + final afterPrefix = fullUri.substring(prefix.length); + if (afterPrefix.isEmpty) return null; + + final questionIndex = afterPrefix.indexOf('?'); + if (questionIndex == -1) { + return {'address': afterPrefix}; + } else { + final addressPart = afterPrefix.substring(0, questionIndex); + final queryPart = afterPrefix.substring(questionIndex + 1); + final result = {}; + + if (addressPart.isNotEmpty) result['address'] = addressPart; + final queryMap = Uri.splitQueryString(queryPart); + result.addAll(queryMap); + + return result; + } + } + + static Future pickBip353AddressChoice( + BuildContext context, + String bip353Name, + Map addressMap, + ) async { + if (addressMap.length == 1) { + return addressMap.values.first; + } + + final chosenAddress = await _showAddressChoiceDialog(context, bip353Name, addressMap); + + return chosenAddress; + } + + static Future _showAddressChoiceDialog( + BuildContext context, + String bip353Name, + Map addressMap, + ) async { + final entriesList = addressMap.entries.toList(); + final displayItems = entriesList.map((e) { + final extendedKeyName = keyDisplayMap[e.key] ?? e.key; + final truncatedValue = _truncate(e.value, front: 6, back: 6); + return '$extendedKeyName : $truncatedValue'; + }).toList(); + + String? selectedDisplayItem; + + if (context.mounted) { + await showPopUp( + context: context, + builder: (dialogContext) { + return Picker( + selectedAtIndex: 0, + title: + '$bip353Name \n was successfully resolved to the following addresses, please choose one:', + items: displayItems, + onItemSelected: (String displayItem) { + selectedDisplayItem = displayItem; + }, + ); + }, + ); + } + + if (selectedDisplayItem == null) { + if (displayItems.isEmpty) { + return null; + } + selectedDisplayItem = displayItems[0]; + } + + final index = displayItems.indexOf(selectedDisplayItem!); + if (index < 0) { + return null; + } + + return entriesList[index].value; + } + + static String _truncate(String value, {int front = 6, int back = 6}) { + if (value.length <= front + back) return value; + + final start = value.substring(0, front); + final end = value.substring(value.length - back); + return '$start...$end'; + } + +} diff --git a/lib/entities/default_settings_migration.dart b/lib/entities/default_settings_migration.dart index 049bfa15c..a02b40d44 100644 --- a/lib/entities/default_settings_migration.dart +++ b/lib/entities/default_settings_migration.dart @@ -44,6 +44,7 @@ const solanaDefaultNodeUri = 'solana-mainnet.core.chainstack.com'; const tronDefaultNodeUri = 'api.trongrid.io'; const newCakeWalletBitcoinUri = 'btc-electrum.cakewallet.com:50002'; const wowneroDefaultNodeUri = 'node3.monerodevs.org:34568'; +const zanoDefaultNodeUri = 'zano.cakewallet.com:11211'; const moneroWorldNodeUri = '.moneroworld.com'; Future defaultSettingsMigration( @@ -260,7 +261,11 @@ Future defaultSettingsMigration( await removeMoneroWorld(sharedPreferences: sharedPreferences, nodes: nodes); break; case 41: - _deselectExchangeProvider(sharedPreferences, "Quantex"); + _changeExchangeProviderAvailability( + sharedPreferences, + providerName: "SwapTrade", + enabled: false, + ); await _addSethNode(nodes, sharedPreferences); await updateTronNodesWithNowNodes(sharedPreferences: sharedPreferences, nodes: nodes); break; @@ -269,8 +274,16 @@ Future defaultSettingsMigration( break; case 43: _fixNodesUseSSLFlag(nodes); - _deselectExchangeProvider(sharedPreferences, "THORChain"); - _deselectExchangeProvider(sharedPreferences, "SimpleSwap"); + _changeExchangeProviderAvailability( + sharedPreferences, + providerName: "THORChain", + enabled: false, + ); + _changeExchangeProviderAvailability( + sharedPreferences, + providerName: "SimpleSwap", + enabled: false, + ); break; case 44: _fixNodesUseSSLFlag(nodes); @@ -311,6 +324,7 @@ Future defaultSettingsMigration( type: WalletType.ethereum, useSSL: true, ); + _changeDefaultNode( nodes: nodes, sharedPreferences: sharedPreferences, @@ -373,6 +387,20 @@ Future defaultSettingsMigration( useSSL: true, ); break; + case 47: + await addZanoNodeList(nodes: nodes); + await changeZanoCurrentNodeToDefault(sharedPreferences: sharedPreferences, nodes: nodes); + _changeExchangeProviderAvailability( + sharedPreferences, + providerName: "SimpleSwap", + enabled: true, + ); + _changeExchangeProviderAvailability( + sharedPreferences, + providerName: "SwapTrade", + enabled: false, + ); + break; default: break; } @@ -469,12 +497,13 @@ Future updateWalletTypeNodesWithNewNode({ ); } -void _deselectExchangeProvider(SharedPreferences sharedPreferences, String providerName) { +void _changeExchangeProviderAvailability(SharedPreferences sharedPreferences, + {required String providerName, required bool enabled}) { final Map exchangeProvidersSelection = json.decode(sharedPreferences.getString(PreferencesKey.exchangeProvidersSelection) ?? "{}") as Map; - exchangeProvidersSelection[providerName] = false; + exchangeProvidersSelection[providerName] = enabled; sharedPreferences.setString( PreferencesKey.exchangeProvidersSelection, @@ -703,6 +732,12 @@ Node? getBitcoinCashDefaultElectrumServer({required Box nodes}) { nodes.values.firstWhereOrNull((node) => node.type == WalletType.bitcoinCash); } +Node? getZanoDefaultNode({required Box nodes}) { + return nodes.values.firstWhereOrNull( + (Node node) => node.uriRaw == zanoDefaultNodeUri) + ?? nodes.values.firstWhereOrNull((node) => node.type == WalletType.zano); +} + Node getMoneroDefaultNode({required Box nodes}) { var nodeUri = newCakeWalletMoneroUri; @@ -1199,6 +1234,7 @@ Future checkCurrentNodes( final currentSolanaNodeId = sharedPreferences.getInt(PreferencesKey.currentSolanaNodeIdKey); final currentTronNodeId = sharedPreferences.getInt(PreferencesKey.currentTronNodeIdKey); final currentWowneroNodeId = sharedPreferences.getInt(PreferencesKey.currentWowneroNodeIdKey); + final currentZanoNodeId = sharedPreferences.getInt(PreferencesKey.currentZanoNodeIdKey); final currentMoneroNode = nodeSource.values.firstWhereOrNull((node) => node.key == currentMoneroNodeId); final currentBitcoinElectrumServer = @@ -1223,6 +1259,8 @@ Future checkCurrentNodes( nodeSource.values.firstWhereOrNull((node) => node.key == currentTronNodeId); final currentWowneroNodeServer = nodeSource.values.firstWhereOrNull((node) => node.key == currentWowneroNodeId); + final currentZanoNode = nodeSource.values.firstWhereOrNull((node) => node.key == currentZanoNodeId); + if (currentMoneroNode == null) { final newCakeWalletNode = Node(uri: newCakeWalletMoneroUri, type: WalletType.monero); await nodeSource.add(newCakeWalletNode); @@ -1306,6 +1344,12 @@ Future checkCurrentNodes( await nodeSource.add(node); await sharedPreferences.setInt(PreferencesKey.currentWowneroNodeIdKey, node.key as int); } + + if (currentZanoNode == null) { + final node = Node(uri: zanoDefaultNodeUri, type: WalletType.zano); + await nodeSource.add(node); + await sharedPreferences.setInt(PreferencesKey.currentZanoNodeIdKey, node.key as int); + } } Future resetBitcoinElectrumServer( @@ -1381,6 +1425,15 @@ Future addWowneroNodeList({required Box nodes}) async { } } +Future addZanoNodeList({required Box nodes}) async { + final nodeList = await loadDefaultZanoNodes(); + for (var node in nodeList) { + if (nodes.values.firstWhereOrNull((element) => element.uriRaw == node.uriRaw) == null) { + await nodes.add(node); + } + } +} + Future changeWowneroCurrentNodeToDefault( {required SharedPreferences sharedPreferences, required Box nodes}) async { final node = getWowneroDefaultNode(nodes: nodes); @@ -1389,6 +1442,13 @@ Future changeWowneroCurrentNodeToDefault( await sharedPreferences.setInt(PreferencesKey.currentWowneroNodeIdKey, nodeId); } +Future changeZanoCurrentNodeToDefault( + {required SharedPreferences sharedPreferences, required Box nodes}) async { + final node = getZanoDefaultNode(nodes: nodes); + final nodeId = node?.key as int? ?? 0; + await sharedPreferences.setInt(PreferencesKey.currentZanoNodeIdKey, nodeId); +} + Future addNanoNodeList({required Box nodes}) async { final nodeList = await loadDefaultNanoNodes(); for (var node in nodeList) { diff --git a/lib/entities/ens_record.dart b/lib/entities/ens_record.dart index e07d0731f..512244c1b 100644 --- a/lib/entities/ens_record.dart +++ b/lib/entities/ens_record.dart @@ -20,7 +20,7 @@ class EnsRecord { } if (_client == null) { - _client = Web3Client("https://ethereum.publicnode.com", Client()); + _client = Web3Client("https://ethereum-rpc.publicnode.com", Client()); } try { diff --git a/lib/entities/fiat_currency.dart b/lib/entities/fiat_currency.dart index 1a031ee82..cf1112096 100644 --- a/lib/entities/fiat_currency.dart +++ b/lib/entities/fiat_currency.dart @@ -10,8 +10,9 @@ class FiatCurrency extends EnumerableItem with Serializable impl static List get all => _all.values.toList(); static List get currenciesAvailableToBuyWith => - [aud, bgn, brl, cad, chf, clp, cop, czk, dkk, egp, eur, gbp, gtq, hkd, hrk, huf, idr, ils, inr, isk, jpy, krw, mad, mxn, myr, ngn, nok, nzd, php, pkr, pln, ron, sek, sgd, thb, twd, usd, vnd, zar, tur]; + [amd, aud, bgn, brl, cad, chf, clp, cop, czk, dkk, egp, eur, gbp, gtq, hkd, hrk, huf, idr, ils, inr, isk, jpy, krw, mad, mxn, myr, ngn, nok, nzd, php, pkr, pln, ron, sek, sgd, thb, twd, usd, vnd, zar, tur,]; + static const amd = FiatCurrency(symbol: 'AMD', countryCode: "arm", fullName: "Armenian Dram"); static const ars = FiatCurrency(symbol: 'ARS', countryCode: "arg", fullName: "Argentine Peso"); static const aud = FiatCurrency(symbol: 'AUD', countryCode: "aus", fullName: "Australian Dollar"); static const bdt = FiatCurrency(symbol: 'BDT', countryCode: "bgd", fullName: "Bangladeshi Taka"); @@ -63,6 +64,7 @@ class FiatCurrency extends EnumerableItem with Serializable impl static const tur = FiatCurrency(symbol: 'TRY', countryCode: "tur", fullName: "Turkish Lira"); static final _all = { + FiatCurrency.amd.raw: FiatCurrency.amd, FiatCurrency.ars.raw: FiatCurrency.ars, FiatCurrency.aud.raw: FiatCurrency.aud, FiatCurrency.bdt.raw: FiatCurrency.bdt, diff --git a/lib/entities/main_actions.dart b/lib/entities/main_actions.dart index 29c161c5f..68f599718 100644 --- a/lib/entities/main_actions.dart +++ b/lib/entities/main_actions.dart @@ -23,7 +23,7 @@ class MainActions { static List all = [ showWalletsAction, receiveAction, - exchangeAction, + swapAction, sendAction, tradeAction, ]; @@ -44,13 +44,13 @@ class MainActions { }, ); - static MainActions exchangeAction = MainActions._( - name: (context) => S.of(context).exchange, + static MainActions swapAction = MainActions._( + name: (context) => S.of(context).swap, image: 'assets/images/transfer.png', - isEnabled: (viewModel) => viewModel.isEnabledExchangeAction, - canShow: (viewModel) => viewModel.hasExchangeAction, + isEnabled: (viewModel) => viewModel.isEnabledSwapAction, + canShow: (viewModel) => viewModel.hasSwapAction, onTap: (BuildContext context, DashboardViewModel viewModel) async { - if (viewModel.isEnabledExchangeAction) { + if (viewModel.isEnabledSwapAction) { await Navigator.of(context).pushNamed(Routes.exchange); } }, @@ -66,7 +66,7 @@ class MainActions { static MainActions tradeAction = MainActions._( - name: (context) => '${S.of(context).buy}/${S.of(context).sell}', + name: (context) => S.of(context).exchange, image: 'assets/images/buy_sell.png', isEnabled: (viewModel) => viewModel.isEnabledTradeAction, canShow: (viewModel) => viewModel.hasTradeAction, diff --git a/lib/entities/node_list.dart b/lib/entities/node_list.dart index 85e37a7bc..5147aa614 100644 --- a/lib/entities/node_list.dart +++ b/lib/entities/node_list.dart @@ -200,6 +200,23 @@ Future> loadDefaultWowneroNodes() async { return nodes; } +Future> loadDefaultZanoNodes() async { + final nodesRaw = await rootBundle.loadString('assets/zano_node_list.yml'); + final loadedNodes = loadYaml(nodesRaw) as YamlList; + final nodes = []; + + for (final raw in loadedNodes) { + if (raw is Map) { + final node = Node.fromMap(Map.from(raw)); + + node.type = WalletType.zano; + nodes.add(node); + } + } + + return nodes; +} + Future resetToDefault(Box nodeSource) async { final moneroNodes = await loadDefaultNodes(); final bitcoinElectrumServerList = await loadBitcoinElectrumServerList(); @@ -211,6 +228,7 @@ Future resetToDefault(Box nodeSource) async { final polygonNodes = await loadDefaultPolygonNodes(); final solanaNodes = await loadDefaultSolanaNodes(); final tronNodes = await loadDefaultTronNodes(); + final zanoNodes = await loadDefaultZanoNodes(); final nodes = moneroNodes + bitcoinElectrumServerList + @@ -220,7 +238,7 @@ Future resetToDefault(Box nodeSource) async { bitcoinCashElectrumServerList + nanoNodes + polygonNodes + - solanaNodes + tronNodes; + solanaNodes + tronNodes + zanoNodes; await nodeSource.clear(); await nodeSource.addAll(nodes); diff --git a/lib/entities/parse_address_from_domain.dart b/lib/entities/parse_address_from_domain.dart index 9be125081..f5f96a8cd 100644 --- a/lib/entities/parse_address_from_domain.dart +++ b/lib/entities/parse_address_from_domain.dart @@ -6,6 +6,7 @@ import 'package:cake_wallet/entities/parsed_address.dart'; import 'package:cake_wallet/entities/unstoppable_domain_address.dart'; import 'package:cake_wallet/entities/emoji_string_extension.dart'; import 'package:cake_wallet/entities/wellknown_record.dart'; +import 'package:cake_wallet/entities/zano_alias.dart'; import 'package:cake_wallet/exchange/provider/thorchain_exchange.provider.dart'; import 'package:cake_wallet/mastodon/mastodon_api.dart'; import 'package:cake_wallet/nostr/nostr_api.dart'; @@ -18,6 +19,8 @@ import 'package:cw_core/wallet_type.dart'; import 'package:cake_wallet/entities/fio_address_provider.dart'; import 'package:flutter/cupertino.dart'; +import 'bip_353_record.dart'; + class AddressResolver { AddressResolver({required this.yatService, required this.wallet, required this.settingsStore}) : walletType = wallet.type; @@ -137,13 +140,23 @@ class AddressResolver { try { // twitter handle example: @username if (text.startsWith('@') && !text.substring(1).contains('@')) { + if (currency == CryptoCurrency.zano && settingsStore.lookupsZanoAlias) { + final formattedName = text.substring(1); + final zanoAddress = await ZanoAlias.fetchZanoAliasAddress(formattedName); + if (zanoAddress != null && zanoAddress.isNotEmpty) { + return ParsedAddress.zanoAddress( + address: zanoAddress, + name: text, + ); + } + } if (settingsStore.lookupsTwitter) { final formattedName = text.substring(1); final twitterUser = await TwitterApi.lookupUserByName(userName: formattedName); final addressFromBio = extractAddressByType( raw: twitterUser.description, type: CryptoCurrency.fromString(ticker, walletCurrency: wallet.currency)); - if (addressFromBio != null) { + if (addressFromBio != null && addressFromBio.isNotEmpty) { return ParsedAddress.fetchTwitterAddress( address: addressFromBio, name: text, @@ -181,7 +194,7 @@ class AddressResolver { if (mastodonUser != null) { String? addressFromBio = extractAddressByType(raw: mastodonUser.note, type: currency); - if (addressFromBio != null) { + if (addressFromBio != null && addressFromBio.isNotEmpty) { return ParsedAddress.fetchMastodonAddress( address: addressFromBio, name: text, @@ -196,7 +209,7 @@ class AddressResolver { String? addressFromPinnedPost = extractAddressByType(raw: userPinnedPostsText, type: currency); - if (addressFromPinnedPost != null) { + if (addressFromPinnedPost != null && addressFromPinnedPost.isNotEmpty) { return ParsedAddress.fetchMastodonAddress( address: addressFromPinnedPost, name: text, @@ -237,7 +250,7 @@ class AddressResolver { } final thorChainAddress = await ThorChainExchangeProvider.lookupAddressByName(text); - if (thorChainAddress != null) { + if (thorChainAddress != null && thorChainAddress.isNotEmpty) { String? address = thorChainAddress[ticker] ?? (ticker == 'RUNE' ? thorChainAddress['THOR'] : null); if (address != null) { @@ -262,6 +275,15 @@ class AddressResolver { } } + final bip353AddressMap = await Bip353Record.fetchUriByCryptoCurrency(text, ticker); + + if (bip353AddressMap != null && bip353AddressMap.isNotEmpty) { + final chosenAddress = await Bip353Record.pickBip353AddressChoice(context, text, bip353AddressMap); + if (chosenAddress != null) { + return ParsedAddress.fetchBip353AddressAddress(address: chosenAddress, name: text); + } + } + if (text.endsWith(".eth")) { if (settingsStore.lookupsENS) { final address = await EnsRecord.fetchEnsAddress(text, wallet: wallet); @@ -290,7 +312,7 @@ class AddressResolver { if (nostrUserData != null) { String? addressFromBio = extractAddressByType(raw: nostrUserData.about, type: currency); - if (addressFromBio != null) { + if (addressFromBio != null && addressFromBio.isNotEmpty) { return ParsedAddress.nostrAddress( address: addressFromBio, name: text, diff --git a/lib/entities/parsed_address.dart b/lib/entities/parsed_address.dart index eabc606db..1f2529221 100644 --- a/lib/entities/parsed_address.dart +++ b/lib/entities/parsed_address.dart @@ -13,7 +13,9 @@ enum ParseFrom { mastodon, nostr, thorChain, - wellKnown + wellKnown, + zanoAlias, + bip353 } class ParsedAddress { @@ -54,6 +56,17 @@ class ParsedAddress { ); } + factory ParsedAddress.fetchBip353AddressAddress ({ + required String address, + required String name, + }) { + return ParsedAddress( + addresses: [address], + name: name, + parseFrom: ParseFrom.bip353, + ); + } + factory ParsedAddress.fetchOpenAliasAddress( {required OpenaliasRecord record, required String name}) { if (record.address.isEmpty) { @@ -143,6 +156,14 @@ class ParsedAddress { ); } + factory ParsedAddress.zanoAddress({required String address, required String name}) { + return ParsedAddress( + addresses: [address], + name: name, + parseFrom: ParseFrom.zanoAlias, + ); + } + factory ParsedAddress.fetchWellKnownAddress({required String address, required String name}) { return ParsedAddress( addresses: [address], diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index 04c4d7146..5f4dcaaab 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -5,6 +5,7 @@ class PreferencesKey { static const currentBitcoinElectrumSererIdKey = 'current_node_id_btc'; static const currentLitecoinElectrumSererIdKey = 'current_node_id_ltc'; static const currentHavenNodeIdKey = 'current_node_id_xhv'; + static const currentZanoNodeIdKey = 'current_node_id_zano'; static const currentEthereumNodeIdKey = 'current_node_id_eth'; static const currentPolygonNodeIdKey = 'current_node_id_matic'; static const currentNanoNodeIdKey = 'current_node_id_nano'; @@ -45,6 +46,7 @@ class PreferencesKey { static const ethereumTransactionPriority = 'current_fee_priority_ethereum'; static const polygonTransactionPriority = 'current_fee_priority_polygon'; static const bitcoinCashTransactionPriority = 'current_fee_priority_bitcoin_cash'; + static const zanoTransactionPriority = 'current_fee_priority_zano'; static const wowneroTransactionPriority = 'current_fee_priority_wownero'; static const customBitcoinFeeRate = 'custom_electrum_fee_rate'; static const silentPaymentsCardDisplay = 'silentPaymentsCardDisplay'; @@ -75,6 +77,7 @@ class PreferencesKey { static const defaultNanoRep = 'default_nano_representative'; static const defaultBananoRep = 'default_banano_representative'; static const lookupsTwitter = 'looks_up_twitter'; + static const lookupsZanoAlias = 'looks_up_zano_alias'; static const lookupsMastodon = 'looks_up_mastodon'; static const lookupsYatService = 'looks_up_yat'; static const lookupsUnstoppableDomains = 'looks_up_unstoppable_domain'; diff --git a/lib/entities/priority_for_wallet_type.dart b/lib/entities/priority_for_wallet_type.dart index 534287494..bbd98d17d 100644 --- a/lib/entities/priority_for_wallet_type.dart +++ b/lib/entities/priority_for_wallet_type.dart @@ -5,6 +5,7 @@ import 'package:cake_wallet/haven/haven.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/wownero/wownero.dart'; +import 'package:cake_wallet/zano/zano.dart'; import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/wallet_type.dart'; @@ -32,6 +33,8 @@ List priorityForWalletType(WalletType type) { case WalletType.solana: case WalletType.tron: return []; + case WalletType.zano: + return zano!.getTransactionPriorities(); default: return []; } diff --git a/lib/entities/provider_types.dart b/lib/entities/provider_types.dart index 42ec74c12..c65ac267b 100644 --- a/lib/entities/provider_types.dart +++ b/lib/entities/provider_types.dart @@ -76,6 +76,7 @@ class ProvidersHelper { ]; case WalletType.none: case WalletType.haven: + case WalletType.zano: return []; } } @@ -109,6 +110,7 @@ class ProvidersHelper { case WalletType.none: case WalletType.haven: case WalletType.wownero: + case WalletType.zano: return []; } } diff --git a/lib/entities/qr_scanner.dart b/lib/entities/qr_scanner.dart index 311bc498a..91c63a574 100644 --- a/lib/entities/qr_scanner.dart +++ b/lib/entities/qr_scanner.dart @@ -12,7 +12,7 @@ import 'package:flutter/scheduler.dart'; var isQrScannerShown = false; -Future presentQRScanner(BuildContext context) async { +Future presentQRScanner(BuildContext context) async { isQrScannerShown = true; try { final result = await Navigator.of(context).push( @@ -23,7 +23,7 @@ Future presentQRScanner(BuildContext context) async { ), ); isQrScannerShown = false; - return result??''; + return result; } catch (e) { isQrScannerShown = false; rethrow; @@ -95,9 +95,7 @@ class _BarcodeScannerSimpleState extends State { setState(() { popped = true; }); - SchedulerBinding.instance.addPostFrameCallback((_) { - Navigator.of(context).pop(_barcode?.rawValue ?? ""); - }); + Navigator.of(context).pop(_barcode!.rawValue ?? _barcode!.rawBytes); } } } diff --git a/lib/entities/wellknown_record.dart b/lib/entities/wellknown_record.dart index edc972f76..dbe808281 100644 --- a/lib/entities/wellknown_record.dart +++ b/lib/entities/wellknown_record.dart @@ -79,7 +79,7 @@ class WellKnownRecord { }) async { String name = formattedName; - print("formattedName: $formattedName"); + printV("formattedName: $formattedName"); final address = await checkWellKnownUsername(formattedName, currency); diff --git a/lib/entities/zano_alias.dart b/lib/entities/zano_alias.dart new file mode 100644 index 000000000..1ddf95178 --- /dev/null +++ b/lib/entities/zano_alias.dart @@ -0,0 +1,28 @@ +import 'dart:convert'; + +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:http/http.dart' as http; + +class ZanoAlias { + static Future fetchZanoAliasAddress(String alias) async { + try { + final uri = Uri.parse("http://zano.cakewallet.com:11211/json_rpc"); + final response = await http.post( + uri, + body: json.encode({ + "id": 0, + "jsonrpc": "2.0", + "method": "get_alias_details", + "params": {"alias": alias} + }), + ); + final jsonParsed = json.decode(response.body) as Map; + + return jsonParsed['result']['alias_details']['address'] as String?; + } catch (e) { + printV('Zano Alias error: ${e.toString()}'); + } + + return null; + } +} diff --git a/lib/exchange/exchange_provider_description.dart b/lib/exchange/exchange_provider_description.dart index 9f3723356..8d1c75211 100644 --- a/lib/exchange/exchange_provider_description.dart +++ b/lib/exchange/exchange_provider_description.dart @@ -16,22 +16,25 @@ class ExchangeProviderDescription extends EnumerableItem with Serializable< ExchangeProviderDescription(title: 'MorphToken', raw: 2, image: 'assets/images/morph.png'); static const sideShift = ExchangeProviderDescription(title: 'SideShift', raw: 3, image: 'assets/images/sideshift.png'); - static const simpleSwap = ExchangeProviderDescription( - title: 'SimpleSwap', raw: 4, image: 'assets/images/simpleSwap.png'); + static const simpleSwap = + ExchangeProviderDescription(title: 'SimpleSwap', raw: 4, image: 'assets/images/simpleSwap.png'); static const trocador = ExchangeProviderDescription(title: 'Trocador', raw: 5, image: 'assets/images/trocador.png'); static const exolix = ExchangeProviderDescription(title: 'Exolix', raw: 6, image: 'assets/images/exolix.png'); - static const all = ExchangeProviderDescription(title: 'All trades', raw: 7, image: ''); + static const all = + ExchangeProviderDescription(title: 'All trades', raw: 7, image: ''); static const thorChain = ExchangeProviderDescription(title: 'ThorChain', raw: 8, image: 'assets/images/thorchain.png'); - static const quantex = - ExchangeProviderDescription(title: 'Quantex', raw: 9, image: 'assets/images/quantex.png'); + static const swapTrade = + ExchangeProviderDescription(title: 'SwapTrade', raw: 9, image: 'assets/images/swap_trade.png'); static const letsExchange = - ExchangeProviderDescription(title: 'LetsExchange', raw: 10, image: 'assets/images/letsexchange_icon.svg'); + ExchangeProviderDescription(title: 'LetsExchange', raw: 10, image: 'assets/images/letsexchange_icon.svg'); static const stealthEx = - ExchangeProviderDescription(title: 'StealthEx', raw: 11, image: 'assets/images/stealthex.png'); - + ExchangeProviderDescription(title: 'StealthEx', raw: 11, image: 'assets/images/stealthex.png'); + static const chainflip = + ExchangeProviderDescription(title: 'Chainflip', raw: 12, image: 'assets/images/chainflip.png'); + static ExchangeProviderDescription deserialize({required int raw}) { switch (raw) { case 0: @@ -53,11 +56,13 @@ class ExchangeProviderDescription extends EnumerableItem with Serializable< case 8: return thorChain; case 9: - return quantex; + return swapTrade; case 10: return letsExchange; case 11: return stealthEx; + case 12: + return chainflip; default: throw Exception('Unexpected token: $raw for ExchangeProviderDescription deserialize'); } diff --git a/lib/exchange/provider/chainflip_exchange_provider.dart b/lib/exchange/provider/chainflip_exchange_provider.dart new file mode 100644 index 000000000..a2c27b745 --- /dev/null +++ b/lib/exchange/provider/chainflip_exchange_provider.dart @@ -0,0 +1,315 @@ +import 'dart:convert'; +import 'dart:math'; + +import 'package:cake_wallet/.secrets.g.dart' as secrets; +import 'package:cake_wallet/exchange/exchange_provider_description.dart'; +import 'package:cake_wallet/exchange/limits.dart'; +import 'package:cake_wallet/exchange/provider/exchange_provider.dart'; +import 'package:cake_wallet/exchange/trade.dart'; +import 'package:cake_wallet/exchange/trade_request.dart'; +import 'package:cake_wallet/exchange/trade_state.dart'; +import 'package:cake_wallet/exchange/utils/currency_pairs_utils.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:hive/hive.dart'; +import 'package:http/http.dart' as http; + +class ChainflipExchangeProvider extends ExchangeProvider { + ChainflipExchangeProvider({required this.tradesStore}) + : super(pairList: supportedPairs(_notSupported)); + + static final List _notSupported = [ + ...(CryptoCurrency.all + .where((element) => ![ + CryptoCurrency.btc, + CryptoCurrency.eth, + CryptoCurrency.usdc, + CryptoCurrency.usdterc20, + CryptoCurrency.flip, + CryptoCurrency.sol, + CryptoCurrency.usdcsol, + // TODO: Add CryptoCurrency.etharb + // TODO: Add CryptoCurrency.usdcarb + // TODO: Add CryptoCurrency.dot + ].contains(element)) + .toList()) + ]; + + static const _baseURL = 'chainflip-broker.io'; + static const _assetsPath = '/assets'; + static const _quotePath = '/quote-native'; + static const _swapPath = '/swap'; + static const _txInfoPath = '/status-by-deposit-channel'; + static const _affiliateBps = secrets.chainflipAffiliateFee; + static const _affiliateKey = secrets.chainflipApiKey; + + final Box tradesStore; + + @override + String get title => 'Chainflip'; + + @override + bool get isAvailable => true; + + @override + bool get isEnabled => true; + + @override + bool get supportsFixedRate => false; + + @override + ExchangeProviderDescription get description => + ExchangeProviderDescription.chainflip; + + @override + Future checkIsAvailable() async => true; + + @override + Future fetchLimits( + {required CryptoCurrency from, + required CryptoCurrency to, + required bool isFixedRateMode}) async { + final assetId = _normalizeCurrency(from); + + final assetsResponse = await _getAssets(); + final assets = assetsResponse['assets'] as List; + + final minAmount = assets.firstWhere( + (asset) => asset['id'] == assetId, + orElse: () => null)?['minimalAmountNative'] ?? '0'; + + return Limits(min: _amountFromNative(minAmount.toString(), from)); + } + + @override + Future fetchRate( + {required CryptoCurrency from, + required CryptoCurrency to, + required double amount, + required bool isFixedRateMode, + required bool isReceiveAmount}) async { + // TODO: It seems this rate is getting cached, and re-used for different amounts, can we not do this? + + try { + if (amount == 0) return 0.0; + + final quoteParams = { + 'apiKey': _affiliateKey, + 'sourceAsset': _normalizeCurrency(from), + 'destinationAsset': _normalizeCurrency(to), + 'amount': _amountToNative(amount, from), + 'commissionBps': _affiliateBps + }; + + final quoteResponse = await _getSwapQuote(quoteParams); + + final expectedAmountOut = + quoteResponse['egressAmountNative'] as String? ?? '0'; + + return _amountFromNative(expectedAmountOut, to) / amount; + } catch (e) { + printV(e.toString()); + return 0.0; + } + } + + @override + Future createTrade( + {required TradeRequest request, + required bool isFixedRateMode, + required bool isSendAll}) async { + try { + final maxSlippage = 2; + + final quoteParams = { + 'apiKey': _affiliateKey, + 'sourceAsset': _normalizeCurrency(request.fromCurrency), + 'destinationAsset': _normalizeCurrency(request.toCurrency), + 'amount': _amountToNative(double.parse(request.fromAmount), request.fromCurrency), + 'commissionBps': _affiliateBps + }; + + final quoteResponse = await _getSwapQuote(quoteParams); + final estimatedPrice = quoteResponse['estimatedPrice'] as double; + final minimumPrice = estimatedPrice * (100 - maxSlippage) / 100; + + final swapParams = { + 'apiKey': _affiliateKey, + 'sourceAsset': _normalizeCurrency(request.fromCurrency), + 'destinationAsset': _normalizeCurrency(request.toCurrency), + 'destinationAddress': request.toAddress, + 'commissionBps': _affiliateBps, + 'minimumPrice': minimumPrice.toString(), + 'refundAddress': request.refundAddress, + 'boostFee': '6', + 'retryDurationInBlocks': '150' + }; + + final swapResponse = await _openDepositChannel(swapParams); + + final id = '${swapResponse['issuedBlock']}-${swapResponse['network'].toString().toUpperCase()}-${swapResponse['channelId']}'; + + return Trade( + id: id, + from: request.fromCurrency, + to: request.toCurrency, + provider: description, + inputAddress: swapResponse['address'].toString(), + createdAt: DateTime.now(), + amount: request.fromAmount, + receiveAmount: request.toAmount, + state: TradeState.waiting, + payoutAddress: request.toAddress, + isSendAll: isSendAll); + } catch (e) { + printV(e.toString()); + rethrow; + } + } + + @override + Future findTradeById({required String id}) async { + try { + final channelParts = id.split('-'); + + final statusParams = { + 'apiKey': _affiliateKey, + 'issuedBlock': channelParts[0], + 'network': channelParts[1], + 'channelId': channelParts[2] + }; + + final statusResponse = await _getStatus(statusParams); + + if (statusResponse == null) + throw Exception('Trade not found for id: $id'); + + final status = statusResponse['status']; + final currentState = _determineState(status['state'].toString()); + + final depositAmount = status['deposit']?['amount']?.toString() ?? '0.0'; + final receiveAmount = status['swapEgress']?['amount']?.toString() ?? '0.0'; + final refundAmount = status['refundEgress']?['amount']?.toString() ?? '0.0'; + final isRefund = status['refundEgress'] != null; + final amount = isRefund ? refundAmount : receiveAmount; + + final newTrade = Trade( + id: id, + from: _toCurrency(status['sourceAsset'].toString()), + to: _toCurrency(status['destinationAsset'].toString()), + provider: description, + amount: depositAmount, + receiveAmount: amount, + state: currentState, + payoutAddress: status['destinationAddress'].toString(), + outputTransaction: status['swapEgress']?['transactionReference']?.toString(), + isRefund: isRefund); + + // Find trade and update receiveAmount with the real value received + final storedTrade = _getStoredTrade(id); + + if (storedTrade != null) { + storedTrade.$2.receiveAmount = newTrade.receiveAmount; + storedTrade.$2.outputTransaction = newTrade.outputTransaction; + tradesStore.put(storedTrade.$1, storedTrade.$2); + } + + return newTrade; + } catch (e) { + printV(e.toString()); + rethrow; + } + } + + String _normalizeCurrency(CryptoCurrency currency) { + final network = switch (currency.tag) { + 'ETH' => 'eth', + 'SOL' => 'sol', + _ => currency.title.toLowerCase() + }; + + return '${currency.title.toLowerCase()}.$network'; + } + + CryptoCurrency? _toCurrency(String name) { + final currency = switch (name) { + 'btc.btc' => CryptoCurrency.btc, + 'eth.eth' => CryptoCurrency.eth, + 'usdc.eth' => CryptoCurrency.usdc, + 'usdt.eth' => CryptoCurrency.usdterc20, + 'flip.eth' => CryptoCurrency.flip, + 'sol.sol' => CryptoCurrency.sol, + 'usdc.sol' => CryptoCurrency.usdcsol, + _ => null + }; + + return currency; + } + + (dynamic, Trade)? _getStoredTrade(String id) { + for (var i = tradesStore.length -1; i >= 0; i--) { + Trade? t = tradesStore.getAt(i); + + if (t != null && t.id == id) + return (i, t); + } + + return null; + } + + String _amountToNative(double amount, CryptoCurrency currency) => + (amount * pow(10, currency.decimals)).toInt().toString(); + + double _amountFromNative(String amount, CryptoCurrency currency) => + double.parse(amount) / pow(10, currency.decimals); + + Future> _getAssets() async => + _getRequest(_assetsPath, {}); + + Future> _getSwapQuote(Map params) async => + _getRequest(_quotePath, params); + + Future> _openDepositChannel(Map params) async => + _getRequest(_swapPath, params); + + Future> _getRequest(String path, Map params) async { + final uri = Uri.https(_baseURL, path, params); + + final response = await http.get(uri); + + if ((response.statusCode != 200) || (response.body.contains('error'))) { + throw Exception('Unexpected response: ${response.statusCode} / ${uri.toString()} / ${response.body}'); + } + + return json.decode(response.body) as Map; + } + + Future?> _getStatus(Map params) async { + final uri = Uri.https(_baseURL, _txInfoPath, params); + + final response = await http.get(uri); + + if (response.statusCode == 404) return null; + + if ((response.statusCode != 200) || (response.body.contains('error'))) { + throw Exception('Unexpected response: ${response.statusCode} / ${uri.toString()} / ${response.body}'); + } + + return json.decode(response.body) as Map; + } + + TradeState _determineState(String state) { + final swapState = switch (state) { + 'waiting' => TradeState.waiting, + 'receiving' => TradeState.processing, + 'swapping' => TradeState.processing, + 'sending' => TradeState.processing, + 'sent' => TradeState.processing, + 'completed' => TradeState.success, + 'failed' => TradeState.failed, + _ => TradeState.notFound + }; + + return swapState; + } +} diff --git a/lib/exchange/provider/exolix_exchange_provider.dart b/lib/exchange/provider/exolix_exchange_provider.dart index ee5a46bbc..43f63b8ca 100644 --- a/lib/exchange/provider/exolix_exchange_provider.dart +++ b/lib/exchange/provider/exolix_exchange_provider.dart @@ -203,7 +203,7 @@ class ExolixExchangeProvider extends ExchangeProvider { extraId: extraId, createdAt: DateTime.now(), amount: amount, - receiveAmount:receiveAmount ?? request.toAmount, + receiveAmount: receiveAmount ?? request.toAmount, state: TradeState.created, payoutAddress: payoutAddress, isSendAll: isSendAll, diff --git a/lib/exchange/provider/letsexchange_exchange_provider.dart b/lib/exchange/provider/letsexchange_exchange_provider.dart index 95520d5f0..d07297fcf 100644 --- a/lib/exchange/provider/letsexchange_exchange_provider.dart +++ b/lib/exchange/provider/letsexchange_exchange_provider.dart @@ -11,6 +11,7 @@ import 'package:cake_wallet/exchange/trade_request.dart'; import 'package:cake_wallet/exchange/trade_state.dart'; import 'package:cake_wallet/exchange/utils/currency_pairs_utils.dart'; import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/utils/print_verbose.dart'; import 'package:http/http.dart' as http; class LetsExchangeExchangeProvider extends ExchangeProvider { @@ -101,7 +102,7 @@ class LetsExchangeExchangeProvider extends ExchangeProvider { return isFixedRateMode ? amount / amountToGet : amountToGet / amount; } catch (e) { - log(e.toString()); + printV(e.toString()); return 0.0; } } @@ -170,8 +171,8 @@ class LetsExchangeExchangeProvider extends ExchangeProvider { final expiredAtTimestamp = responseJSON['expired_at'] as int; final extraId = responseJSON['deposit_extra_id'] as String?; - final createdAt = DateTime.parse(createdAtString); - final expiredAt = DateTime.fromMillisecondsSinceEpoch(expiredAtTimestamp * 1000); + final createdAt = DateTime.parse(createdAtString).toLocal(); + final expiredAt = DateTime.fromMillisecondsSinceEpoch(expiredAtTimestamp * 1000).toLocal(); CryptoCurrency fromCurrency; if (request.fromCurrency.tag != null && request.fromCurrency.title == from) { @@ -235,8 +236,8 @@ class LetsExchangeExchangeProvider extends ExchangeProvider { final expiredAtTimestamp = responseJSON['expired_at'] as int; final extraId = responseJSON['deposit_extra_id'] as String?; - final createdAt = DateTime.parse(createdAtString); - final expiredAt = DateTime.fromMillisecondsSinceEpoch(expiredAtTimestamp * 1000); + final createdAt = DateTime.parse(createdAtString).toLocal(); + final expiredAt = DateTime.fromMillisecondsSinceEpoch(expiredAtTimestamp * 1000).toLocal(); return Trade( id: id, diff --git a/lib/exchange/provider/sideshift_exchange_provider.dart b/lib/exchange/provider/sideshift_exchange_provider.dart index 7373d5f2d..12ec59100 100644 --- a/lib/exchange/provider/sideshift_exchange_provider.dart +++ b/lib/exchange/provider/sideshift_exchange_provider.dart @@ -12,6 +12,7 @@ import 'package:cake_wallet/exchange/trade_request.dart'; import 'package:cake_wallet/exchange/trade_state.dart'; import 'package:cake_wallet/exchange/utils/currency_pairs_utils.dart'; import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/utils/print_verbose.dart'; import 'package:http/http.dart'; class SideShiftExchangeProvider extends ExchangeProvider { @@ -152,7 +153,7 @@ class SideShiftExchangeProvider extends ExchangeProvider { return double.parse(responseJSON['rate'] as String); } catch (e) { - log('Error fetching rate in SideShift Provider: ${e.toString()}'); + printV(e.toString()); return 0.00; } } diff --git a/lib/exchange/provider/stealth_ex_exchange_provider.dart b/lib/exchange/provider/stealth_ex_exchange_provider.dart index 53c40ee62..11b8d768a 100644 --- a/lib/exchange/provider/stealth_ex_exchange_provider.dart +++ b/lib/exchange/provider/stealth_ex_exchange_provider.dart @@ -156,9 +156,9 @@ class StealthExExchangeProvider extends ExchangeProvider { final createdAtString = responseJSON['created_at'] as String; final extraId = deposit['extra_id'] as String?; - final createdAt = DateTime.parse(createdAtString); + final createdAt = DateTime.parse(createdAtString).toLocal(); final expiredAt = validUntil != null - ? DateTime.parse(validUntil) + ? DateTime.parse(validUntil).toLocal() : DateTime.now().add(Duration(minutes: 5)); @@ -221,7 +221,7 @@ class StealthExExchangeProvider extends ExchangeProvider { final receiveAmount = toDouble(withdrawal['amount']); final status = responseJSON['status'] as String; final createdAtString = responseJSON['created_at'] as String; - final createdAt = DateTime.parse(createdAtString); + final createdAt = DateTime.parse(createdAtString).toLocal(); final extraId = deposit['extra_id'] as String?; return Trade( diff --git a/lib/exchange/provider/quantex_exchange_provider.dart b/lib/exchange/provider/swaptrade_exchange_provider.dart similarity index 96% rename from lib/exchange/provider/quantex_exchange_provider.dart rename to lib/exchange/provider/swaptrade_exchange_provider.dart index ee3473360..9553c9559 100644 --- a/lib/exchange/provider/quantex_exchange_provider.dart +++ b/lib/exchange/provider/swaptrade_exchange_provider.dart @@ -14,8 +14,8 @@ import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:http/http.dart'; -class QuantexExchangeProvider extends ExchangeProvider { - QuantexExchangeProvider() : super(pairList: supportedPairs(_notSupported)); +class SwapTradeExchangeProvider extends ExchangeProvider { + SwapTradeExchangeProvider() : super(pairList: supportedPairs(_notSupported)); static final List _notSupported = [ ...(CryptoCurrency.all @@ -33,15 +33,15 @@ class QuantexExchangeProvider extends ExchangeProvider { .toList()) ]; - static final markup = secrets.quantexExchangeMarkup; + static final markup = secrets.swapTradeExchangeMarkup; - static const apiAuthority = 'api.myquantex.com'; + static const apiAuthority = 'api.swaptrade.io'; static const getRate = '/api/swap/get-rate'; static const getCoins = '/api/swap/get-coins'; static const createOrder = '/api/swap/create-order'; @override - String get title => 'Quantex'; + String get title => 'SwapTrade'; @override bool get isAvailable => true; @@ -53,7 +53,7 @@ class QuantexExchangeProvider extends ExchangeProvider { bool get supportsFixedRate => false; @override - ExchangeProviderDescription get description => ExchangeProviderDescription.quantex; + ExchangeProviderDescription get description => ExchangeProviderDescription.swapTrade; @override Future checkIsAvailable() async => true; diff --git a/lib/exchange/provider/trocador_exchange_provider.dart b/lib/exchange/provider/trocador_exchange_provider.dart index 151ded371..4d262049f 100644 --- a/lib/exchange/provider/trocador_exchange_provider.dart +++ b/lib/exchange/provider/trocador_exchange_provider.dart @@ -53,12 +53,12 @@ class TrocadorExchangeProvider extends ExchangeProvider { static const apiKey = secrets.trocadorApiKey; static const onionApiAuthority = 'trocadorfyhlu27aefre5u7zri66gudtzdyelymftvr4yjwcxhfaqsid.onion'; - static const clearNetAuthority = 'trocador.app'; + static const clearNetAuthority = 'api.trocador.app'; static const markup = secrets.trocadorExchangeMarkup; - static const newRatePath = '/api/new_rate'; - static const createTradePath = 'api/new_trade'; - static const tradePath = 'api/trade'; - static const coinPath = 'api/coin'; + static const newRatePath = '/new_rate'; + static const createTradePath = '/new_trade'; + static const tradePath = '/trade'; + static const coinPath = '/coin'; String _lastUsedRateId; List _provider; @@ -107,8 +107,9 @@ class TrocadorExchangeProvider extends ExchangeProvider { final coinJson = responseJSON.first as Map; return Limits( - min: coinJson['minimum'] as double, - max: coinJson['maximum'] as double, + min: coinJson['minimum'] as double?, + // TODO: remove hardcoded value and call `api/new_rate` when Trocador adds min and max to it + max: from == CryptoCurrency.zano ? 2600 : coinJson['maximum'] as double?, ); } @@ -138,6 +139,9 @@ class TrocadorExchangeProvider extends ExchangeProvider { final response = await get(uri, headers: {'API-Key': apiKey}); final responseJSON = json.decode(response.body) as Map; + + if (responseJSON['error'] != null) throw Exception(responseJSON['error']); + final fromAmount = double.parse(responseJSON['amount_from'].toString()); final toAmount = double.parse(responseJSON['amount_to'].toString()); final rateId = responseJSON['trade_id'] as String? ?? ''; diff --git a/lib/exchange/trade.dart b/lib/exchange/trade.dart index c5738cfd9..2482bdc83 100644 --- a/lib/exchange/trade.dart +++ b/lib/exchange/trade.dart @@ -168,6 +168,7 @@ class Trade extends HiveObject { } String amountFormatted() => formatAmount(amount); + String receiveAmountFormatted() => formatAmount(receiveAmount ?? ''); } class TradeAdapter extends TypeAdapter { diff --git a/lib/main.dart b/lib/main.dart index d7077a6ed..666cb4617 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -216,7 +216,7 @@ Future initializeAppConfigs({bool loadWallet = true}) async { secureStorage: secureStorage, anonpayInvoiceInfo: anonpayInvoiceInfo, havenSeedStore: havenSeedStore, - initialMigrationVersion: 46, + initialMigrationVersion: 47, ); } @@ -286,7 +286,7 @@ class AppState extends State with SingleTickerProviderStateMixin { final statusBarColor = Colors.transparent; final authenticationStore = getIt.get(); final initialRoute = authenticationStore.state == AuthenticationState.uninitialized - ? Routes.disclaimer + ? Routes.welcome : Routes.login; final currentTheme = settingsStore.currentTheme; final statusBarBrightness = diff --git a/lib/monero/cw_monero.dart b/lib/monero/cw_monero.dart index 037ab8f9c..aa6fafc6b 100644 --- a/lib/monero/cw_monero.dart +++ b/lib/monero/cw_monero.dart @@ -242,19 +242,21 @@ class CWMonero extends Monero { WalletCredentials createMoneroRestoreWalletFromSeedCredentials( {required String name, required String password, + required String passphrase, required int height, required String mnemonic}) => MoneroRestoreWalletFromSeedCredentials( - name: name, password: password, height: height, mnemonic: mnemonic); + name: name, password: password, passphrase: passphrase, height: height, mnemonic: mnemonic); @override WalletCredentials createMoneroNewWalletCredentials({ required String name, required String language, required bool isPolyseed, + required String? passphrase, String? password}) => MoneroNewWalletCredentials( - name: name, password: password, language: language, isPolyseed: isPolyseed); + name: name, password: password, language: language, isPolyseed: isPolyseed, passphrase: passphrase); @override Map getKeys(Object wallet) { @@ -265,7 +267,8 @@ class CWMonero extends Monero { 'privateSpendKey': keys.privateSpendKey, 'privateViewKey': keys.privateViewKey, 'publicSpendKey': keys.publicSpendKey, - 'publicViewKey': keys.publicViewKey + 'publicViewKey': keys.publicViewKey, + 'passphrase': keys.passphrase }; } diff --git a/lib/reactions/bip39_wallet_utils.dart b/lib/reactions/bip39_wallet_utils.dart index a31fec91f..8b99331ce 100644 --- a/lib/reactions/bip39_wallet_utils.dart +++ b/lib/reactions/bip39_wallet_utils.dart @@ -15,6 +15,7 @@ bool isBIP39Wallet(WalletType walletType) { case WalletType.monero: case WalletType.wownero: case WalletType.haven: + case WalletType.zano: case WalletType.none: return false; } diff --git a/lib/reactions/on_authentication_state_change.dart b/lib/reactions/on_authentication_state_change.dart index c05f6fb0d..bce160b4a 100644 --- a/lib/reactions/on_authentication_state_change.dart +++ b/lib/reactions/on_authentication_state_change.dart @@ -67,6 +67,7 @@ void startAuthenticationStateChange( alertTitle: S.of(context).proceed_on_device, alertContent: S.of(context).proceed_on_device_description, buttonText: S.of(context).cancel, + alertBarrierDismissible: false, buttonAction: () => Navigator.of(context).pop()), ); await loadCurrentWallet(); diff --git a/lib/reactions/on_current_node_change.dart b/lib/reactions/on_current_node_change.dart index 730fba674..4b49d05ad 100644 --- a/lib/reactions/on_current_node_change.dart +++ b/lib/reactions/on_current_node_change.dart @@ -1,6 +1,5 @@ import 'package:cw_core/utils/print_verbose.dart'; import 'package:mobx/mobx.dart'; -import 'package:cw_core/node.dart'; import 'package:cake_wallet/store/app_store.dart'; ReactionDisposer? _onCurrentNodeChangeReaction; diff --git a/lib/router.dart b/lib/router.dart index fc6092795..bdbe77cc0 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -50,6 +50,7 @@ import 'package:cake_wallet/src/screens/new_wallet/advanced_privacy_settings_pag import 'package:cake_wallet/src/screens/new_wallet/new_wallet_page.dart'; import 'package:cake_wallet/src/screens/new_wallet/new_wallet_type_page.dart'; import 'package:cake_wallet/src/screens/new_wallet/wallet_group_description_page.dart'; +import 'package:cake_wallet/src/screens/new_wallet/wallet_group_existing_seed_description_page.dart'; import 'package:cake_wallet/src/screens/nodes/node_create_or_edit_page.dart'; import 'package:cake_wallet/src/screens/nodes/pow_node_create_or_edit_page.dart'; import 'package:cake_wallet/src/screens/order_details/order_details_page.dart'; @@ -146,7 +147,8 @@ Route createRoute(RouteSettings settings) { switch (settings.name) { case Routes.welcome: - return MaterialPageRoute(builder: (_) => CreatePinWelcomePage()); + return MaterialPageRoute( + builder: (_) => CreatePinWelcomePage(SettingsStoreBase.walletPasswordDirectInput)); case Routes.welcomeWallet: if (SettingsStoreBase.walletPasswordDirectInput) { @@ -588,6 +590,11 @@ Route createRoute(RouteSettings settings) { return MaterialPageRoute( builder: (_) => getIt.get(param1: settings.arguments as int)); + case Routes.walletGroupExistingSeedDescriptionPage: + return MaterialPageRoute( + builder: (_) => WalletGroupExistingSeedDescriptionPage( + seedPhraseWordsLength: settings.arguments as int)); + case Routes.transactionSuccessPage: return MaterialPageRoute( builder: (_) => getIt.get(param1: settings.arguments as String)); diff --git a/lib/routes.dart b/lib/routes.dart index 588c1af4a..54ea5d468 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -117,5 +117,6 @@ class Routes { static const urqrAnimatedPage = '/urqr/animated_page'; static const walletGroupsDisplayPage = '/wallet_groups_display_page'; static const walletGroupDescription = '/wallet_group_description'; + static const walletGroupExistingSeedDescriptionPage = '/wallet_group_existing_seed_description_page'; static const walletSeedVerificationPage = '/wallet_seed_verification_page'; } diff --git a/lib/src/screens/backup/backup_page.dart b/lib/src/screens/backup/backup_page.dart index b8065cf36..91813ee89 100644 --- a/lib/src/screens/backup/backup_page.dart +++ b/lib/src/screens/backup/backup_page.dart @@ -26,12 +26,6 @@ class BackupPage extends BasePage { @override String get title => S.current.backup; - @override - Widget trailing(BuildContext context) => TrailButton( - caption: S.of(context).change_password, - onPressed: () => Navigator.of(context).pushNamed(Routes.editBackupPassword), - textColor: Palette.blueCraiola); - @override Widget body(BuildContext context) { return Stack( @@ -53,7 +47,9 @@ class BackupPage extends BasePage { builder: (_) => GestureDetector( onTap: () { ClipboardUtil.setSensitiveDataToClipboard( - ClipboardData(text: backupViewModelBase.backupPassword)); + ClipboardData( + text: backupViewModelBase + .backupPassword)); showBar( context, S.of(context).transaction_details_copied( @@ -74,15 +70,25 @@ class BackupPage extends BasePage { )) ]))), Positioned( - child: Observer( - builder: (_) => LoadingPrimaryButton( - isLoading: backupViewModelBase.state is IsExecutingState, - onPressed: () => onExportBackup(context), - text: S.of(context).export_backup, - color: Theme.of(context).primaryColor, + child: Column(children: [ + PrimaryButton( + onPressed: () => + Navigator.of(context).pushNamed(Routes.editBackupPassword), + text: S.of(context).change_password, + color: Theme.of(context).cardColor, textColor: Colors.white, ), - ), + SizedBox(height: 10), + Observer( + builder: (_) => LoadingPrimaryButton( + isLoading: backupViewModelBase.state is IsExecutingState, + onPressed: () => onExportBackup(context), + text: S.of(context).export_backup, + color: Theme.of(context).primaryColor, + textColor: Colors.white, + ), + ), + ]), bottom: 24, left: 24, right: 24, @@ -130,7 +136,8 @@ class BackupPage extends BasePage { rightButtonText: S.of(context).save_to_downloads, leftButtonText: S.of(context).share, actionRightButton: () async { - await backupViewModelBase.saveToDownload(backup.name, backup.content); + await backupViewModelBase.saveToDownload( + backup.name, backup.content); Navigator.of(dialogContext).pop(); }, actionLeftButton: () async { @@ -142,13 +149,15 @@ class BackupPage extends BasePage { Future share(BackupExportFile backup, BuildContext context) async { final path = await backupViewModelBase.saveBackupFileLocally(backup); - await ShareUtil.shareFile(filePath: path, fileName: backup.name, context: context); + await ShareUtil.shareFile( + filePath: path, fileName: backup.name, context: context); await backupViewModelBase.removeBackupFileLocally(backup); } Future _saveFile(BackupExportFile backup) async { - String? outputFile = await FilePicker.platform - .saveFile(dialogTitle: 'Save Your File to desired location', fileName: backup.name); + String? outputFile = await FilePicker.platform.saveFile( + dialogTitle: 'Save Your File to desired location', + fileName: backup.name); try { File returnedFile = File(outputFile!); 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 index f8ace97bc..ab9ea5cef 100644 --- a/lib/src/screens/connect_device/monero_hardware_wallet_options_page.dart +++ b/lib/src/screens/connect_device/monero_hardware_wallet_options_page.dart @@ -190,6 +190,7 @@ class _MoneroHardwareWalletOptionsFormState alertTitle: S.of(context).proceed_on_device, alertContent: S.of(context).proceed_on_device_description, buttonText: S.of(context).cancel, + alertBarrierDismissible: false, buttonAction: () => Navigator.of(context).pop(), ), ); diff --git a/lib/src/screens/connect_device/select_hardware_wallet_account_page.dart b/lib/src/screens/connect_device/select_hardware_wallet_account_page.dart index 31542ab5f..11a126be6 100644 --- a/lib/src/screens/connect_device/select_hardware_wallet_account_page.dart +++ b/lib/src/screens/connect_device/select_hardware_wallet_account_page.dart @@ -147,7 +147,7 @@ class _SelectHardwareWalletAccountFormState extends State Column( children: _walletHardwareRestoreVM.availableAccounts.map((acc) { + final address = acc.address; return Padding( padding: EdgeInsets.only(top: 10), @@ -170,7 +171,7 @@ class _SelectHardwareWalletAccountFormState extends State with SingleTickerProv @override Widget build(BuildContext context) { return Padding( - padding: const EdgeInsets.only(left: 24), + padding: const EdgeInsets.only(), child: Column( children: [ - Align( + Padding( + padding: const EdgeInsets.only(left: 24, right: 24, bottom: 8), + child: Align( alignment: Alignment.centerLeft, child: TabBar( controller: _tabController, @@ -135,7 +137,7 @@ class _ContactPageBodyState extends State with SingleTickerProv indicatorColor: Theme.of(context).appBarTheme.titleTextStyle!.color, indicatorPadding: EdgeInsets.zero, labelPadding: EdgeInsets.only(right: 24), - tabAlignment: TabAlignment.center, + tabAlignment: TabAlignment.start, dividerColor: Colors.transparent, padding: EdgeInsets.zero, tabs: [ @@ -144,6 +146,7 @@ class _ContactPageBodyState extends State with SingleTickerProv ], ), ), + ), Expanded( child: TabBarView( controller: _tabController, @@ -173,7 +176,7 @@ class _ContactPageBodyState extends State with SingleTickerProv itemCount: groupedContacts.length * 2, itemBuilder: (context, index) { if (index.isOdd) { - return StandardListSeparator(); + return StandardListSeparator(height: 0); } else { final groupIndex = index ~/ 2; final groupName = groupedContacts.keys.elementAt(groupIndex); @@ -188,7 +191,9 @@ class _ContactPageBodyState extends State with SingleTickerProv orElse: () => groupContacts[0], ); - return ExpansionTile( + return Padding( + padding: const EdgeInsets.only(left: 16, right: 16, top: 4, bottom: 4), + child: ExpansionTile( title: Text( groupName, style: TextStyle( @@ -198,11 +203,16 @@ class _ContactPageBodyState extends State with SingleTickerProv ), ), leading: _buildCurrencyIcon(activeContact), - tilePadding: EdgeInsets.zero, + tilePadding: const EdgeInsets.only(left: 16, right: 16), childrenPadding: const EdgeInsets.only(left: 16), expandedCrossAxisAlignment: CrossAxisAlignment.start, expandedAlignment: Alignment.topLeft, + backgroundColor: Theme.of(context).cardColor, + collapsedBackgroundColor: Theme.of(context).cardColor, + collapsedShape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), children: groupContacts.map((contact) => generateRaw(context, contact)).toList(), + ), ); } } @@ -234,7 +244,12 @@ class _ContactPageBodyState extends State with SingleTickerProv }, behavior: HitTestBehavior.opaque, child: Container( - padding: const EdgeInsets.only(top: 16, bottom: 16, right: 24), + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(10)), + color: Theme.of(context).cardColor, + ), + margin: const EdgeInsets.only(top: 4, bottom: 4, left: 16, right: 16), + padding: const EdgeInsets.only(top: 16, bottom: 16, right: 16, left: 16), child: Row( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.start, @@ -375,7 +390,12 @@ class _ContactListBodyState extends State { children: [ Container( key: Key('${contact.name}'), - padding: const EdgeInsets.only(top: 16, bottom: 16, right: 24), + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(8)), + color: Theme.of(context).cardColor, + ), + margin: const EdgeInsets.only(top: 4, bottom: 4, left: 16, right: 16), + padding: const EdgeInsets.only(top: 16, bottom: 16, right: 16, left: 16), child: Row( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.start, @@ -396,7 +416,6 @@ class _ContactListBodyState extends State { ], ), ), - StandardListSeparator() ], ); } diff --git a/lib/src/screens/dashboard/dashboard_page.dart b/lib/src/screens/dashboard/dashboard_page.dart index b1934f4a3..f219409da 100644 --- a/lib/src/screens/dashboard/dashboard_page.dart +++ b/lib/src/screens/dashboard/dashboard_page.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'package:cake_wallet/core/wallet_connect/wc_bottom_sheet_service.dart'; import 'package:cake_wallet/entities/preferences_key.dart'; import 'package:cake_wallet/di.dart'; -import 'package:cake_wallet/entities/main_actions.dart'; import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/desktop_sidebar_wrapper.dart'; import 'package:cake_wallet/src/screens/dashboard/pages/cake_features_page.dart'; import 'package:cake_wallet/src/screens/wallet_connect/widgets/modals/bottom_sheet_listener.dart'; @@ -10,7 +9,6 @@ import 'package:cake_wallet/src/widgets/gradient_background.dart'; import 'package:cake_wallet/src/widgets/haven_wallet_removal_popup.dart'; import 'package:cake_wallet/src/widgets/services_updates_widget.dart'; import 'package:cake_wallet/src/widgets/vulnerable_seeds_popup.dart'; -import 'package:cake_wallet/themes/extensions/sync_indicator_theme.dart'; import 'package:cake_wallet/utils/device_info.dart'; import 'package:cake_wallet/utils/version_comparator.dart'; import 'package:cake_wallet/view_model/dashboard/cake_features_view_model.dart'; @@ -23,8 +21,8 @@ import 'package:flutter/material.dart'; import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/menu_widget.dart'; -import 'package:cake_wallet/src/screens/dashboard/widgets/action_button.dart'; import 'package:cake_wallet/src/screens/dashboard/pages/balance/balance_page.dart'; +import 'package:cake_wallet/src/screens/dashboard/pages/navigation_dock.dart'; import 'package:cake_wallet/src/screens/dashboard/pages/transactions_page.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/sync_indicator.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart'; @@ -35,7 +33,6 @@ import 'package:smooth_page_indicator/smooth_page_indicator.dart'; import 'package:cake_wallet/main.dart'; import 'package:cake_wallet/src/screens/release_notes/release_notes_screen.dart'; import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart'; -import 'package:cake_wallet/themes/extensions/balance_page_theme.dart'; class DashboardPage extends StatefulWidget { DashboardPage({ @@ -140,7 +137,8 @@ class _DashboardPageView extends BasePage { bool get resizeToAvoidBottomInset => false; @override - Widget get endDrawer => MenuWidget(dashboardViewModel, ValueKey('dashboard_page_drawer_menu_widget_key')); + Widget get endDrawer => + MenuWidget(dashboardViewModel, ValueKey('dashboard_page_drawer_menu_widget_key')); @override Widget leading(BuildContext context) { @@ -176,10 +174,6 @@ class _DashboardPageView extends BasePage { width: 40, child: TextButton( key: ValueKey('dashboard_page_wallet_menu_button_key'), - // FIX-ME: Style - //highlightColor: Colors.transparent, - //splashColor: Colors.transparent, - //padding: EdgeInsets.all(0), onPressed: () => onOpenEndDrawer(), child: Semantics(label: S.of(context).wallet_menu, child: menuButton), ), @@ -219,14 +213,15 @@ class _DashboardPageView extends BasePage { _setEffects(context); return SafeArea( - minimum: EdgeInsets.only(bottom: 24), + minimum: EdgeInsets.only(bottom: 0), child: BottomSheetListener( bottomSheetService: bottomSheetService, - child: Column( - mainAxisSize: MainAxisSize.max, - children: [ - Expanded( - child: Observer( + child: Container( + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + //new Expanded( + Observer( builder: (context) { return PageView.builder( key: ValueKey('dashboard_page_view_key'), @@ -236,101 +231,44 @@ class _DashboardPageView extends BasePage { ); }, ), - ), - Padding( - padding: EdgeInsets.only(bottom: 24, top: 10), - child: Observer( - builder: (context) { - return Semantics( - button: false, - label: 'Page Indicator', - hint: 'Swipe to change page', - excludeSemantics: true, - child: SmoothPageIndicator( - controller: controller, - count: pages.length, - effect: ColorTransitionEffect( - spacing: 6.0, - radius: 6.0, - dotWidth: 6.0, - dotHeight: 6.0, - dotColor: Theme.of(context) - .extension()! - .indicatorDotTheme - .indicatorColor, - activeDotColor: Theme.of(context) - .extension()! - .indicatorDotTheme - .activeIndicatorColor, - ), - ), - ); - }, - ), - ), - Observer( - builder: (_) { - return ClipRect( - child: Container( - margin: const EdgeInsets.only(left: 16, right: 16), - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(50.0), - border: Border.all( - color: Theme.of(context).extension()!.cardBorderColor, - width: 1, + //), + Positioned( + child: Container( + alignment: Alignment.bottomCenter, + margin: EdgeInsets.only(bottom: 110), + child: Observer( + builder: (context) { + return Semantics( + button: false, + label: 'Page Indicator', + hint: 'Swipe to change page', + excludeSemantics: true, + child: SmoothPageIndicator( + controller: controller, + count: pages.length, + effect: ColorTransitionEffect( + spacing: 6.0, + radius: 6.0, + dotWidth: 6.0, + dotHeight: 6.0, + dotColor: Theme.of(context) + .extension()! + .indicatorDotTheme + .indicatorColor, + activeDotColor: Theme.of(context) + .extension()! + .indicatorDotTheme + .activeIndicatorColor, + ), ), - color: Theme.of(context) - .extension()! - .syncedBackgroundColor, - ), - child: Container( - padding: EdgeInsets.symmetric(horizontal: 10), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: MainActions.all - .where((element) => element.canShow?.call(dashboardViewModel) ?? true) - .map( - (action) => Expanded( - child: Semantics( - button: true, - enabled: (action.isEnabled?.call(dashboardViewModel) ?? true), - child: ActionButton( - key: ValueKey( - 'dashboard_page_${action.name(context)}_action_button_key'), - image: Image.asset( - action.image, - height: 24, - width: 24, - color: action.isEnabled?.call(dashboardViewModel) ?? true - ? Theme.of(context) - .extension()! - .mainActionsIconColor - : Theme.of(context) - .extension()! - .labelTextColor, - ), - title: action.name(context), - onClick: () async => - await action.onTap(context, dashboardViewModel), - textColor: action.isEnabled?.call(dashboardViewModel) ?? true - ? null - : Theme.of(context) - .extension()! - .labelTextColor, - ), - ), - ), - ) - .toList(), - ), - ), - ), + ); + }, ), - ); - }, - ), - ], + ), + ), + NavigationDock(dashboardViewModel: dashboardViewModel) + ], + ), ), ), ); 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 7bb5f77f8..a1e89c0f6 100644 --- a/lib/src/screens/dashboard/desktop_widgets/desktop_dashboard_actions.dart +++ b/lib/src/screens/dashboard/desktop_widgets/desktop_dashboard_actions.dart @@ -30,11 +30,11 @@ class DesktopDashboardActions extends StatelessWidget { await MainActions.showWalletsAction.onTap(context, dashboardViewModel), ), DesktopActionButton( - title: MainActions.exchangeAction.name(context), - image: MainActions.exchangeAction.image, - canShow: MainActions.exchangeAction.canShow?.call(dashboardViewModel), - isEnabled: MainActions.exchangeAction.isEnabled?.call(dashboardViewModel), - onTap: () async => await MainActions.exchangeAction.onTap(context, dashboardViewModel), + title: MainActions.swapAction.name(context), + image: MainActions.swapAction.image, + canShow: MainActions.swapAction.canShow?.call(dashboardViewModel), + isEnabled: MainActions.swapAction.isEnabled?.call(dashboardViewModel), + onTap: () async => await MainActions.swapAction.onTap(context, dashboardViewModel), ), Row( children: [ diff --git a/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart b/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart index e5f38010d..6804467f7 100644 --- a/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart +++ b/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart @@ -46,6 +46,7 @@ class _DesktopWalletSelectionDropDownState extends State Image.asset( @@ -158,6 +159,8 @@ class _DesktopWalletSelectionDropDownState extends State { _contractAddressController.text, ); final actionCall = () async { - await widget.homeSettingsViewModel.addToken( - token: CryptoCurrency( - name: _tokenNameController.text, - title: _tokenSymbolController.text.toUpperCase(), - decimals: int.parse(_tokenDecimalController.text), - iconPath: _tokenIconPathController.text, - ), - contractAddress: _contractAddressController.text, - ); + try { + await widget.homeSettingsViewModel.addToken( + token: CryptoCurrency( + name: _tokenNameController.text, + title: _tokenSymbolController.text.toUpperCase(), + decimals: int.parse(_tokenDecimalController.text), + iconPath: _tokenIconPathController.text, + ), + contractAddress: _contractAddressController.text, + ); + + if (mounted) { + Navigator.pop(context); + } + + } catch (e) { + showPopUp( + context: context, + builder: (dialogContext) { + return AlertWithOneAction( + alertTitle: S.current.warning, + alertContent: e.toString(), + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(dialogContext).pop(), + ); + }, + ); + } }; if (hasPotentialError) { @@ -233,16 +254,27 @@ class _EditTokenPageBodyState extends State { actionRightButton: () async { Navigator.of(dialogContext).pop(); await actionCall(); - if (mounted) { - Navigator.pop(context); - } }, actionLeftButton: () => Navigator.of(dialogContext).pop(), ); }, ); } else { - await actionCall(); + try { + await actionCall(); + } catch (e) { + showPopUp( + context: context, + builder: (dialogContext) { + return AlertWithOneAction( + alertTitle: "Unable to add token", + alertContent: "$e", + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(context).pop(), + ); + }, + ); + } if (mounted) { Navigator.pop(context); } @@ -269,11 +301,12 @@ class _EditTokenPageBodyState extends State { final token = await widget.homeSettingsViewModel.getToken(_contractAddressController.text); if (token != null) { - if (_tokenNameController.text.isEmpty) _tokenNameController.text = token.name; - if (_tokenSymbolController.text.isEmpty) _tokenSymbolController.text = token.title; + final isZano = widget.homeSettingsViewModel.walletType == WalletType.zano; + if (_tokenNameController.text.isEmpty || isZano) _tokenNameController.text = token.name; + if (_tokenSymbolController.text.isEmpty || isZano) _tokenSymbolController.text = token.title; if (_tokenIconPathController.text.isEmpty) _tokenIconPathController.text = token.iconPath ?? ''; - if (_tokenDecimalController.text.isEmpty) + if (_tokenDecimalController.text.isEmpty || isZano) _tokenDecimalController.text = token.decimals.toString(); } } @@ -305,7 +338,7 @@ class _EditTokenPageBodyState extends State { placeholder: S.of(context).token_contract_address, options: [AddressTextFieldOption.paste], buttonColor: Theme.of(context).hintColor, - validator: AddressValidator(type: widget.homeSettingsViewModel.nativeToken), + validator: widget.homeSettingsViewModel.walletType == WalletType.zano ? null : AddressValidator(type: widget.homeSettingsViewModel.nativeToken).call, onPushPasteButton: (_) { _pasteText(); }, diff --git a/lib/src/screens/dashboard/pages/balance/balance_page.dart b/lib/src/screens/dashboard/pages/balance/balance_page.dart index b53d2d56b..e9c2115a9 100644 --- a/lib/src/screens/dashboard/pages/balance/balance_page.dart +++ b/lib/src/screens/dashboard/pages/balance/balance_page.dart @@ -25,6 +25,7 @@ class BalancePage extends StatelessWidget { builder: (context) { final isEVMCompatible = isEVMCompatibleChain(dashboardViewModel.type); return DefaultTabController( + key: ValueKey(isEVMCompatible), length: isEVMCompatible ? 2 : 1, child: Column( children: [ diff --git a/lib/src/screens/dashboard/pages/balance/balance_row_widget.dart b/lib/src/screens/dashboard/pages/balance/balance_row_widget.dart index f86c72b80..875618784 100644 --- a/lib/src/screens/dashboard/pages/balance/balance_row_widget.dart +++ b/lib/src/screens/dashboard/pages/balance/balance_row_widget.dart @@ -16,6 +16,7 @@ import 'package:cw_core/unspent_coin_type.dart'; import 'package:flutter/material.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:url_launcher/url_launcher.dart'; +import 'package:cake_wallet/themes/theme_base.dart'; class BalanceRowWidget extends StatelessWidget { BalanceRowWidget({ @@ -65,6 +66,8 @@ class BalanceRowWidget extends StatelessWidget { @override Widget build(BuildContext context) { + bool brightThemeType = false; + if (dashboardViewModel.settingsStore.currentTheme.type == ThemeType.bright) brightThemeType = true; return Column( children: [ Container( @@ -76,6 +79,15 @@ class BalanceRowWidget extends StatelessWidget { width: 1, ), color: Theme.of(context).extension()!.syncedBackgroundColor, + // boxShadow: [ + // BoxShadow( + // color: Theme.of(context) + // .extension()! + // .cardBorderColor + // .withAlpha(50), + // spreadRadius: dashboardViewModel.getShadowSpread(), + // blurRadius: dashboardViewModel.getShadowBlur()) + // ], ), child: TextButton( onPressed: () => Fluttertoast.showToast( @@ -310,7 +322,7 @@ class BalanceRowWidget extends StatelessWidget { ), ), if (hasSecondAdditionalBalance || hasSecondAvailableBalance) ...[ - SizedBox(height: 10), + SizedBox(height: 16), Container( margin: const EdgeInsets.only(left: 16, right: 16), decoration: BoxDecoration( @@ -320,6 +332,15 @@ class BalanceRowWidget extends StatelessWidget { width: 1, ), color: Theme.of(context).extension()!.syncedBackgroundColor, + // boxShadow: [ + // BoxShadow( + // color: Theme.of(context) + // .extension()! + // .cardBorderColor + // .withAlpha(50), + // spreadRadius: dashboardViewModel.getShadowSpread(), + // blurRadius: dashboardViewModel.getShadowBlur()) + // ], ), child: TextButton( onPressed: () => Fluttertoast.showToast( @@ -643,6 +664,25 @@ class BalanceRowWidget extends StatelessWidget { ); } + // double getShadowSpread(){ + // double spread = 3; + // if (dashboardViewModel.settingsStore.currentTheme.type == ThemeType.bright) spread = 3; + // else if (dashboardViewModel.settingsStore.currentTheme.type == ThemeType.light) spread = 3; + // else if (dashboardViewModel.settingsStore.currentTheme.type == ThemeType.dark) spread = 1; + // else if (dashboardViewModel.settingsStore.currentTheme.type == ThemeType.oled) spread = 3; + // return spread; + // } + // + // + // double getShadowBlur(){ + // double blur = 7; + // if (dashboardViewModel.settingsStore.currentTheme.type == ThemeType.bright) blur = 7; + // else if (dashboardViewModel.settingsStore.currentTheme.type == ThemeType.light) blur = 7; + // else if (dashboardViewModel.settingsStore.currentTheme.type == ThemeType.dark) blur = 3; + // else if (dashboardViewModel.settingsStore.currentTheme.type == ThemeType.oled) blur = 7; + // return blur; + // } + void _showBalanceDescription(BuildContext context, String content) { showPopUp(context: context, builder: (_) => InformationPage(information: content)); } diff --git a/lib/src/screens/dashboard/pages/balance/crypto_balance_widget.dart b/lib/src/screens/dashboard/pages/balance/crypto_balance_widget.dart index 0bdf388d3..ae0605fff 100644 --- a/lib/src/screens/dashboard/pages/balance/crypto_balance_widget.dart +++ b/lib/src/screens/dashboard/pages/balance/crypto_balance_widget.dart @@ -39,7 +39,6 @@ class CryptoBalanceWidget extends StatelessWidget { child: DashBoardRoundedCardWidget( title: "Invalid monero bindings", subTitle: dashboardViewModel.getMoneroError.toString(), - onTap: () {}, ), ); } @@ -54,7 +53,6 @@ class CryptoBalanceWidget extends StatelessWidget { child: DashBoardRoundedCardWidget( title: "Invalid wownero bindings", subTitle: dashboardViewModel.getWowneroError.toString(), - onTap: () {}, )); } return Container(); @@ -153,7 +151,7 @@ class CryptoBalanceWidget extends StatelessWidget { return ListView.separated( physics: NeverScrollableScrollPhysics(), shrinkWrap: true, - separatorBuilder: (_, __) => Container(padding: EdgeInsets.only(bottom: 8)), + separatorBuilder: (_, __) => Container(padding: EdgeInsets.only(bottom: 16)), itemCount: dashboardViewModel.balanceViewModel.formattedBalances.length, itemBuilder: (__, index) { final balance = @@ -173,7 +171,7 @@ class CryptoBalanceWidget extends StatelessWidget { frozenFiatBalance: balance.fiatFrozenBalance, currency: balance.asset, hasAdditionalBalance: - dashboardViewModel.balanceViewModel.hasAdditionalBalance, + dashboardViewModel.balanceViewModel.hasAdditionalBalance(balance.asset), hasSecondAdditionalBalance: dashboardViewModel.balanceViewModel.hasSecondAdditionalBalance, hasSecondAvailableBalance: @@ -206,14 +204,17 @@ class CryptoBalanceWidget extends StatelessWidget { subTitle: "Here are the things that you should note:\n - " + dashboardViewModel.isMoneroWalletBrokenReasons.join("\n - ") + "\n\nPlease restart your wallet and if it doesn't help contact our support.", - onTap: () {}, )) ], if (dashboardViewModel.showSilentPaymentsCard) ...[ - SizedBox(height: 10), + SizedBox(height: 16), Padding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), child: DashBoardRoundedCardWidget( + shadowBlur: dashboardViewModel.getShadowBlur(), + shadowSpread: dashboardViewModel.getShadowSpread(), + marginV: 0, + marginH: 0, customBorder: 30, title: S.of(context).silent_payments, subTitle: S.of(context).enable_silent_payments_scanning, @@ -276,10 +277,12 @@ class CryptoBalanceWidget extends StatelessWidget { ), ], if (dashboardViewModel.showMwebCard) ...[ - SizedBox(height: 10), + SizedBox(height: 16), Padding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), child: DashBoardRoundedCardWidget( + marginV: 0, + marginH: 0, customBorder: 30, title: S.of(context).litecoin_mweb, subTitle: S.of(context).litecoin_mweb_description, @@ -338,7 +341,6 @@ class CryptoBalanceWidget extends StatelessWidget { ), ], ), - onTap: () => {}, icon: Container( decoration: BoxDecoration( color: Colors.white, @@ -356,6 +358,7 @@ class CryptoBalanceWidget extends StatelessWidget { ], ); }), + SizedBox(height: 130), ], ), ); diff --git a/lib/src/screens/dashboard/pages/cake_features_page.dart b/lib/src/screens/dashboard/pages/cake_features_page.dart index bd96fd534..c98182d32 100644 --- a/lib/src/screens/dashboard/pages/cake_features_page.dart +++ b/lib/src/screens/dashboard/pages/cake_features_page.dart @@ -22,15 +22,13 @@ class CakeFeaturesPage extends StatelessWidget { @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 10.0), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 10.0), + return Container( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox(height: 50), - Text( + Padding( + padding: EdgeInsets.only(left: 24, top: 16), + child: Text( 'Cake ${S.of(context).features}', style: TextStyle( fontSize: 24, @@ -38,11 +36,14 @@ class CakeFeaturesPage extends StatelessWidget { color: Theme.of(context).extension()!.pageTitleTextColor, ), ), + ), Expanded( child: ListView( children: [ - SizedBox(height: 20), + SizedBox(height: 2), DashBoardRoundedCardWidget( + shadowBlur: dashboardViewModel.getShadowBlur(), + shadowSpread: dashboardViewModel.getShadowSpread(), onTap: () { if (Platform.isMacOS) { _launchUrl("buy.cakepay.com"); @@ -59,8 +60,9 @@ class CakeFeaturesPage extends StatelessWidget { fit: BoxFit.cover, ), ), - SizedBox(height: 10), DashBoardRoundedCardWidget( + shadowBlur: dashboardViewModel.getShadowBlur(), + shadowSpread: dashboardViewModel.getShadowSpread(), onTap: () => _launchUrl("cake.nano-gpt.com"), title: "NanoGPT", subTitle: S.of(context).nanogpt_subtitle, @@ -71,32 +73,13 @@ class CakeFeaturesPage extends StatelessWidget { fit: BoxFit.cover, ), ), - SizedBox(height: 10), - Observer( - builder: (context) { - if (!dashboardViewModel.hasSignMessages) { - return const SizedBox(); - } - return DashBoardRoundedCardWidget( - onTap: () => Navigator.of(context).pushNamed(Routes.signPage), - title: S.current.sign_verify_message, - subTitle: S.current.sign_verify_message_sub, - icon: Icon( - Icons.speaker_notes_rounded, - color: - Theme.of(context).extension()!.pageTitleTextColor, - size: 75, - ), - ); - }, - ), + SizedBox(height: 125), ], ), ), ], ), - ), - ); + ); } void _launchUrl(String url) { diff --git a/lib/src/screens/dashboard/pages/navigation_dock.dart b/lib/src/screens/dashboard/pages/navigation_dock.dart new file mode 100644 index 000000000..52b39cdf7 --- /dev/null +++ b/lib/src/screens/dashboard/pages/navigation_dock.dart @@ -0,0 +1,147 @@ +import 'dart:ui'; +import 'package:cake_wallet/entities/main_actions.dart'; +import 'package:cake_wallet/themes/extensions/sync_indicator_theme.dart'; +import 'package:flutter/material.dart'; +import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; +import 'package:cake_wallet/src/screens/dashboard/widgets/action_button.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart'; +import 'package:cake_wallet/themes/extensions/balance_page_theme.dart'; +import '../../../../themes/theme_base.dart'; + +class NavigationDock extends StatelessWidget { + const NavigationDock({ + required this.dashboardViewModel, + }); + + final DashboardViewModel dashboardViewModel; + + @override + Widget build(BuildContext context) { + return Positioned( + child: Observer( + builder: (_) { + return Container( + height: 150, + alignment: Alignment.bottomCenter, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: _getColors(context), + ), + ), + child: Container( + margin: const EdgeInsets.only(left: 16, right: 16, bottom: 16), + child: ClipRRect( + borderRadius: BorderRadius.circular(50), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 50, sigmaY: 50), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(50.0), + border: Border.all( + color: Theme.of(context).extension()!.cardBorderColor, + width: 1, + ), + color: + Theme.of(context).extension()!.syncedBackgroundColor, + ), + child: Container( + padding: EdgeInsets.symmetric(horizontal: 10), + child: IntrinsicHeight( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: MainActions.all + .where((element) => element.canShow?.call(dashboardViewModel) ?? true) + .map( + (action) => Expanded( + child: Semantics( + button: true, + enabled: (action.isEnabled?.call(dashboardViewModel) ?? true), + child: ActionButton( + key: ValueKey( + 'dashboard_page_${action.name(context)}_action_button_key'), + image: Image.asset( + action.image, + height: 24, + width: 24, + color: action.isEnabled?.call(dashboardViewModel) ?? true + ? Theme.of(context) + .extension()! + .mainActionsIconColor + : Theme.of(context) + .extension()! + .labelTextColor, + ), + title: action.name(context), + onClick: () async => + await action.onTap(context, dashboardViewModel), + textColor: action.isEnabled?.call(dashboardViewModel) ?? true + ? null + : Theme.of(context) + .extension()! + .labelTextColor, + ), + ), + ), + ) + .toList(), + ), + ), + ), + ), + ), + ), + ), + ); + }, + ), + ); + } + + List _getColors(BuildContext context) { + final isBright = dashboardViewModel.settingsStore.currentTheme.type == ThemeType.bright; + return isBright + ? [ + Theme.of(context) + .extension()! + .thirdGradientBackgroundColor + .withAlpha(10), + Theme.of(context) + .extension()! + .thirdGradientBackgroundColor + .withAlpha(75), + Theme.of(context) + .extension()! + .thirdGradientBackgroundColor + .withAlpha(150), + Theme.of(context).extension()!.thirdGradientBackgroundColor, + Theme.of(context).extension()!.thirdGradientBackgroundColor + ] + : [ + Theme.of(context) + .extension()! + .thirdGradientBackgroundColor + .withAlpha(5), + Theme.of(context) + .extension()! + .thirdGradientBackgroundColor + .withAlpha(50), + Theme.of(context) + .extension()! + .thirdGradientBackgroundColor + .withAlpha(125), + Theme.of(context) + .extension()! + .thirdGradientBackgroundColor + .withAlpha(150), + Theme.of(context) + .extension()! + .thirdGradientBackgroundColor + .withAlpha(200), + Theme.of(context).extension()!.thirdGradientBackgroundColor, + Theme.of(context).extension()!.thirdGradientBackgroundColor + ]; + } +} diff --git a/lib/src/screens/dashboard/pages/transactions_page.dart b/lib/src/screens/dashboard/pages/transactions_page.dart index 9472110ea..654663f92 100644 --- a/lib/src/screens/dashboard/pages/transactions_page.dart +++ b/lib/src/screens/dashboard/pages/transactions_page.dart @@ -40,7 +40,6 @@ class TransactionsPage extends StatelessWidget { color: responsiveLayoutUtil.shouldRenderMobileUI ? null : Theme.of(context).colorScheme.background, - padding: EdgeInsets.only(top: 24, bottom: 24), child: Column( children: [ Observer(builder: (_) { @@ -73,12 +72,15 @@ class TransactionsPage extends StatelessWidget { child: Observer( builder: (_) { final items = dashboardViewModel.items; - + final amount = items.length + 1; return items.isNotEmpty ? ListView.builder( key: ValueKey('transactions_page_list_view_builder_key'), - itemCount: items.length, + itemCount: items.length + 1, itemBuilder: (context, index) { + + if(index == items.length) return SizedBox(height: 150); + final item = items[index]; if (item is DateSectionItem) { @@ -118,7 +120,6 @@ class TransactionsPage extends StatelessWidget { dashboardViewModel.balanceViewModel.isFiatDisabled ? '' : item.formattedFiatAmount, - isPending: transaction.isPending, title: item.formattedTitle + item.formattedStatus + transactionType, tags: tags, @@ -160,11 +161,11 @@ class TransactionsPage extends StatelessWidget { createdAtFormattedDate: trade.createdAt != null ? DateFormat('HH:mm').format(trade.createdAt!) : null, - formattedAmount: item.tradeFormattedAmount, + formattedAmount: item.tradeFormattedAmount, + formattedReceiveAmount: item.tradeFormattedReceiveAmount ), ); } - if (item is OrderListItem) { final order = item.order; @@ -182,7 +183,6 @@ class TransactionsPage extends StatelessWidget { ), ); } - return Container(color: Colors.transparent, height: 1); }) : Center( @@ -197,7 +197,7 @@ class TransactionsPage extends StatelessWidget { ); }, ), - ) + ), ], ), ), diff --git a/lib/src/screens/dashboard/widgets/action_button.dart b/lib/src/screens/dashboard/widgets/action_button.dart index 21f056c0c..786c56658 100644 --- a/lib/src/screens/dashboard/widgets/action_button.dart +++ b/lib/src/screens/dashboard/widgets/action_button.dart @@ -21,8 +21,8 @@ class ActionButton extends StatelessWidget { @override Widget build(BuildContext context) { - return GestureDetector( - onTap: () { + return TextButton( + onPressed: () { if (route?.isNotEmpty ?? false) { Navigator.of(context, rootNavigator: true).pushNamed(route!); } else { @@ -31,11 +31,12 @@ class ActionButton extends StatelessWidget { }, child: Container( color: Colors.transparent, - padding: EdgeInsets.only(top: 14, bottom: 16, left: 10, right: 10), + padding: EdgeInsets.only(top: 5, bottom: 4, left: 0, right: 0), alignment: alignment, child: Column( mainAxisSize: MainAxisSize.max, crossAxisAlignment: CrossAxisAlignment.center, + //mainAxisAlignment: MainAxisAlignment.center, children: [ Container( alignment: Alignment.center, @@ -47,7 +48,7 @@ class ActionButton extends StatelessWidget { Text( title, style: TextStyle( - fontSize: 10, + fontSize: 9, color: textColor ?? Theme.of(context).extension()!.cardTextColor), textAlign: TextAlign.center, diff --git a/lib/src/screens/dashboard/widgets/menu_widget.dart b/lib/src/screens/dashboard/widgets/menu_widget.dart index b49a08584..16e0f88aa 100644 --- a/lib/src/screens/dashboard/widgets/menu_widget.dart +++ b/lib/src/screens/dashboard/widgets/menu_widget.dart @@ -37,7 +37,8 @@ class MenuWidgetState extends State { this.polygonIcon = Image.asset('assets/images/matic_icon.png'), this.solanaIcon = Image.asset('assets/images/sol_icon.png'), this.tronIcon = Image.asset('assets/images/trx_icon.png'), - this.wowneroIcon = Image.asset('assets/images/wownero_icon.png'); + this.wowneroIcon = Image.asset('assets/images/wownero_icon.png'), + this.zanoIcon = Image.asset('assets/images/zano_icon.png'); final largeScreen = 731; @@ -62,6 +63,7 @@ class MenuWidgetState extends State { Image solanaIcon; Image tronIcon; Image wowneroIcon; + Image zanoIcon; @override void initState() { @@ -141,6 +143,7 @@ class MenuWidgetState extends State { return Container( height: headerHeight, decoration: BoxDecoration( + borderRadius: BorderRadius.only(bottomLeft: Radius.circular(24)), gradient: LinearGradient(colors: [ Theme.of(context).extension()!.headerFirstGradientColor, Theme.of(context).extension()!.headerSecondGradientColor, @@ -207,7 +210,7 @@ class MenuWidgetState extends State { ); }, separatorBuilder: (_, index) => Container( - height: 1, + height: 0, color: Theme.of(context).extension()!.dividerColor, ), itemCount: itemCount + 1, @@ -245,6 +248,8 @@ class MenuWidgetState extends State { return tronIcon; case WalletType.wownero: return wowneroIcon; + case WalletType.zano: + return zanoIcon; default: throw Exception('No icon for ${type.toString()}'); } diff --git a/lib/src/screens/dashboard/widgets/trade_row.dart b/lib/src/screens/dashboard/widgets/trade_row.dart index 84a5d2beb..8eea77b19 100644 --- a/lib/src/screens/dashboard/widgets/trade_row.dart +++ b/lib/src/screens/dashboard/widgets/trade_row.dart @@ -13,6 +13,7 @@ class TradeRow extends StatelessWidget { required this.createdAtFormattedDate, this.onTap, this.formattedAmount, + this.formattedReceiveAmount, super.key, }); @@ -22,10 +23,12 @@ class TradeRow extends StatelessWidget { final CryptoCurrency to; final String? createdAtFormattedDate; final String? formattedAmount; + final String? formattedReceiveAmount; @override Widget build(BuildContext context) { final amountCrypto = from.toString(); + final receiveAmountCrypto = to.toString(); return InkWell( onTap: onTap, @@ -61,12 +64,21 @@ class TradeRow extends StatelessWidget { : Container() ]), SizedBox(height: 5), - Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - if (createdAtFormattedDate != null) - Text(createdAtFormattedDate!, - style: TextStyle( - fontSize: 14, - color: Theme.of(context).extension()!.dateSectionRowColor)) + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + createdAtFormattedDate != null + ? Text(createdAtFormattedDate!, + style: TextStyle( + fontSize: 14, + color: Theme.of(context).extension()!.dateSectionRowColor)) + : Container(), + formattedReceiveAmount != null + ? Text(formattedReceiveAmount! + ' ' + receiveAmountCrypto, + style: TextStyle( + fontSize: 14, + color: Theme.of(context).extension()!.dateSectionRowColor)) + : Container(), ]) ], )) diff --git a/lib/src/screens/dashboard/widgets/transaction_raw.dart b/lib/src/screens/dashboard/widgets/transaction_raw.dart index 2d7cbb809..d604863c8 100644 --- a/lib/src/screens/dashboard/widgets/transaction_raw.dart +++ b/lib/src/screens/dashboard/widgets/transaction_raw.dart @@ -10,7 +10,6 @@ class TransactionRow extends StatelessWidget { required this.formattedDate, required this.formattedAmount, required this.formattedFiatAmount, - required this.isPending, required this.tags, required this.title, required this.onTap, @@ -22,7 +21,6 @@ class TransactionRow extends StatelessWidget { final String formattedDate; final String formattedAmount; final String formattedFiatAmount; - final bool isPending; final String title; final List tags; diff --git a/lib/src/screens/disclaimer/disclaimer_page.dart b/lib/src/screens/disclaimer/disclaimer_page.dart index c9d959b40..a7d3b5ff9 100644 --- a/lib/src/screens/disclaimer/disclaimer_page.dart +++ b/lib/src/screens/disclaimer/disclaimer_page.dart @@ -41,7 +41,6 @@ class DisclaimerPageBody extends StatefulWidget { } class DisclaimerBodyState extends State { - static const changenowUrl = 'https://changenow.io/terms-of-use'; bool _checked = false; String _fileText = ''; @@ -63,7 +62,7 @@ class DisclaimerBodyState extends State { @override Widget build(BuildContext context) { return WillPopScope( - onWillPop: () async => false, + onWillPop: () async => widget.isReadOnly, child: Container( color: Theme.of(context).colorScheme.background, child: Column( @@ -125,49 +124,6 @@ class DisclaimerBodyState extends State { SizedBox( height: 16.0, ), - Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Expanded( - child: Text( - 'Other Terms and Conditions', - textAlign: TextAlign.left, - style: TextStyle( - fontSize: 14.0, - fontWeight: FontWeight.bold, - color: Theme.of(context).extension()!.titleColor), - ), - ) - ], - ), - SizedBox( - height: 16.0, - ), - Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Expanded( - child: GestureDetector( - onTap: () async { - final uri = Uri.parse(changenowUrl); - if (await canLaunchUrl(uri)) - await launchUrl(uri, mode: LaunchMode.externalApplication); - }, - child: Text( - changenowUrl, - textAlign: TextAlign.left, - style: TextStyle( - color: Theme.of(context).primaryColor, - fontSize: 14.0, - fontWeight: FontWeight.normal, - decoration: TextDecoration.underline), - ), - )) - ], - ), - SizedBox( - height: 16.0, - ) ], ), ), diff --git a/lib/src/screens/exchange/exchange_page.dart b/lib/src/screens/exchange/exchange_page.dart index 2f8e3eb5c..f11a50bfe 100644 --- a/lib/src/screens/exchange/exchange_page.dart +++ b/lib/src/screens/exchange/exchange_page.dart @@ -1,4 +1,5 @@ import 'package:cake_wallet/exchange/exchange_provider_description.dart'; +import 'package:cake_wallet/exchange/provider/chainflip_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/thorchain_exchange.provider.dart'; import 'package:cake_wallet/themes/extensions/exchange_page_theme.dart'; import 'package:cake_wallet/themes/extensions/keyboard_theme.dart'; @@ -211,26 +212,46 @@ class ExchangePage extends BasePage { : S.of(context).fixed_pair_not_supported : exchangeViewModel.isAvailableInSelected ? S.of(context).amount_is_estimate - : S.of(context).variable_pair_not_supported; - return Center( - child: Text( - description, - textAlign: TextAlign.center, - style: TextStyle( - color: Theme.of(context) - .extension()! - .receiveAmountColor, - fontWeight: FontWeight.w500, - fontSize: 12), - ), + : S.of(context).this_pair_is_not_supported_warning; + return Row( + children: [ + if(description == S.of(context).this_pair_is_not_supported_warning) + Expanded( + child: Container( + alignment: Alignment.centerRight, + child: Icon(Icons.warning_amber_rounded, + color: Theme.of(context).extension()!.receiveAmountColor, + size: 26), + ), + ), + Expanded( + flex: 8, + child: Text( + description, + textAlign: TextAlign.center, + softWrap: true, + overflow: TextOverflow.ellipsis, + maxLines: 3, + style: TextStyle( + color: Theme.of(context) + .extension()! + .receiveAmountColor, + fontWeight: FontWeight.w500, + fontSize: 12, + ), + ), + ), + ], ); }), ), Observer( builder: (_) => LoadingPrimaryButton( key: ValueKey('exchange_page_exchange_button_key'), - text: S.of(context).exchange, - onPressed: () { + text: exchangeViewModel.isAvailableInSelected ? S.of(context).exchange : S.of(context).change_selected_exchanges, + onPressed: exchangeViewModel.isAvailableInSelected ? () { + FocusScope.of(context).unfocus(); + if (_formKey.currentState != null && _formKey.currentState!.validate()) { if ((exchangeViewModel.depositCurrency == CryptoCurrency.xmr) && @@ -257,7 +278,7 @@ class ExchangePage extends BasePage { ); } } - }, + } : () => PresentProviderPicker(exchangeViewModel: exchangeViewModel).presentProviderPicker(context), color: Theme.of(context).primaryColor, textColor: Colors.white, isDisabled: exchangeViewModel.selectedProviders.isEmpty, @@ -442,7 +463,8 @@ class ExchangePage extends BasePage { } if (state is TradeIsCreatedSuccessfully) { exchangeViewModel.reset(); - (exchangeViewModel.tradesStore.trade?.provider == ExchangeProviderDescription.thorChain) + (exchangeViewModel.tradesStore.trade?.provider == ExchangeProviderDescription.thorChain || + exchangeViewModel.tradesStore.trade?.provider == ExchangeProviderDescription.chainflip) ? Navigator.of(context).pushReplacementNamed(Routes.exchangeTrade) : Navigator.of(context).pushReplacementNamed(Routes.exchangeConfirm); } @@ -476,6 +498,14 @@ class ExchangePage extends BasePage { } }); + reaction((_) => exchangeViewModel.bestRate, (double rate) { + if (exchangeViewModel.isFixedRateMode) { + exchangeViewModel.changeReceiveAmount(amount: receiveAmountController.text); + } else { + exchangeViewModel.changeDepositAmount(amount: depositAmountController.text); + } + }); + depositAddressController .addListener(() => exchangeViewModel.depositAddress = depositAddressController.text); @@ -485,12 +515,15 @@ class ExchangePage extends BasePage { exchangeViewModel.isSendAllEnabled = false; final isThorChain = exchangeViewModel.selectedProviders .any((provider) => provider is ThorChainExchangeProvider); + final isChainflip = exchangeViewModel.selectedProviders + .any((provider) => provider is ChainflipExchangeProvider); - _depositAmountDebounce = isThorChain + _depositAmountDebounce = isThorChain || isChainflip ? Debounce(Duration(milliseconds: 1000)) : Debounce(Duration(milliseconds: 500)); _depositAmountDebounce.run(() { + exchangeViewModel.calculateBestRate(); exchangeViewModel.changeDepositAmount(amount: depositAmountController.text); exchangeViewModel.isReceiveAmountEntered = false; }); @@ -503,6 +536,7 @@ class ExchangePage extends BasePage { receiveAmountController.addListener(() { if (receiveAmountController.text != exchangeViewModel.receiveAmount) { _receiveAmountDebounce.run(() { + exchangeViewModel.calculateBestRate(); exchangeViewModel.changeReceiveAmount(amount: receiveAmountController.text); exchangeViewModel.isReceiveAmountEntered = true; }); diff --git a/lib/src/screens/exchange/widgets/exchange_card.dart b/lib/src/screens/exchange/widgets/exchange_card.dart index 7c4cc948d..a73c7fb66 100644 --- a/lib/src/screens/exchange/widgets/exchange_card.dart +++ b/lib/src/screens/exchange/widgets/exchange_card.dart @@ -193,9 +193,11 @@ class ExchangeCardState extends State> { width: double.infinity, color: Colors.transparent, child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + SizedBox(height: 10), Row( mainAxisAlignment: MainAxisAlignment.start, children: [ + SizedBox(height: 40), Text( key: ValueKey('${_cardInstanceName}_title_key'), _title, @@ -207,6 +209,7 @@ class ExchangeCardState extends State> { ], ), CurrencyAmountTextField( + padding: EdgeInsets.zero, currencyPickerButtonKey: ValueKey('${_cardInstanceName}_currency_picker_button_key'), selectedCurrencyTextKey: ValueKey('${_cardInstanceName}_selected_currency_text_key'), selectedCurrencyTagTextKey: @@ -273,7 +276,7 @@ class ExchangeCardState extends State> { ? FocusTraversalOrder( order: NumericFocusOrder(2), child: Padding( - padding: widget.addressRowPadding ?? EdgeInsets.only(top: 20), + padding: widget.addressRowPadding ?? EdgeInsets.only(top: 12), child: AddressTextField( addressKey: ValueKey('${_cardInstanceName}_editable_address_textfield_key'), focusNode: widget.addressFocusNode, @@ -313,7 +316,7 @@ class ExchangeCardState extends State> { ) : Offstage() : Padding( - padding: EdgeInsets.only(top: 10), + padding: EdgeInsets.only(top: 0), child: Builder( builder: (context) => Stack(children: [ FocusTraversalOrder( 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 d53f16339..80f8bd72d 100644 --- a/lib/src/screens/exchange/widgets/mobile_exchange_cards_section.dart +++ b/lib/src/screens/exchange/widgets/mobile_exchange_cards_section.dart @@ -23,7 +23,7 @@ class MobileExchangeCardsSection extends StatelessWidget { @override Widget build(BuildContext context) { return Container( - padding: EdgeInsets.only(bottom: isBuySellOption ? 8 : 32), + padding: EdgeInsets.only(bottom: isBuySellOption ? 16 : 16), decoration: BoxDecoration( borderRadius: BorderRadius.only( bottomLeft: Radius.circular(24), @@ -54,7 +54,7 @@ class MobileExchangeCardsSection extends StatelessWidget { end: Alignment.bottomRight, ), ), - padding: EdgeInsets.fromLTRB(24, 90, 24, isBuySellOption ? 8 : 32), + padding: EdgeInsets.fromLTRB(24, 90, 24, isBuySellOption ? 24 : 16), child: Column( children: [ if (isBuySellOption) Column( @@ -68,7 +68,7 @@ class MobileExchangeCardsSection extends StatelessWidget { ), ), Padding( - padding: EdgeInsets.only(top: 29, left: 24, right: 24), + padding: EdgeInsets.only(top: 20, left: 24, right: 24), child: secondExchangeCard, ) ], diff --git a/lib/src/screens/exchange/widgets/present_provider_picker.dart b/lib/src/screens/exchange/widgets/present_provider_picker.dart index 75429aea9..c79325910 100644 --- a/lib/src/screens/exchange/widgets/present_provider_picker.dart +++ b/lib/src/screens/exchange/widgets/present_provider_picker.dart @@ -20,7 +20,7 @@ class PresentProviderPicker extends StatelessWidget { height: 6); return TextButton( - onPressed: () => _presentProviderPicker(context), + onPressed: () => presentProviderPicker(context), style: ButtonStyle( padding: MaterialStateProperty.all(EdgeInsets.zero), splashFactory: NoSplash.splashFactory, @@ -62,7 +62,7 @@ class PresentProviderPicker extends StatelessWidget { )); } - void _presentProviderPicker(BuildContext context) async { + void presentProviderPicker(BuildContext context) async { await showPopUp( builder: (BuildContext popUpContext) => CheckBoxPicker( items: exchangeViewModel.providerList diff --git a/lib/src/screens/new_wallet/advanced_privacy_settings_page.dart b/lib/src/screens/new_wallet/advanced_privacy_settings_page.dart index 26c96fb74..e5853570e 100644 --- a/lib/src/screens/new_wallet/advanced_privacy_settings_page.dart +++ b/lib/src/screens/new_wallet/advanced_privacy_settings_page.dart @@ -219,7 +219,7 @@ class _AdvancedPrivacySettingsBodyState extends State<_AdvancedPrivacySettingsBo }), child: Icon( Icons.remove_red_eye, - color: obscurePassphrase ? Colors.black54 : Colors.black26, + // color: obscurePassphrase ? Colors.black54 : Colors.black26, ), ), ), @@ -241,7 +241,7 @@ class _AdvancedPrivacySettingsBodyState extends State<_AdvancedPrivacySettingsBo }), child: Icon( Icons.remove_red_eye, - color: obscurePassphrase ? Colors.black54 : Colors.black26, + // color: obscurePassphrase ? Colors.black54 : Colors.black26, ), ), ), diff --git a/lib/src/screens/new_wallet/new_wallet_page.dart b/lib/src/screens/new_wallet/new_wallet_page.dart index e2d5a953c..368b3440d 100644 --- a/lib/src/screens/new_wallet/new_wallet_page.dart +++ b/lib/src/screens/new_wallet/new_wallet_page.dart @@ -101,8 +101,14 @@ class _WalletNameFormState extends State { void initState() { _stateReaction ??= reaction((_) => _walletNewVM.state, (ExecutionState state) async { if (state is ExecutedSuccessfullyState) { - Navigator.of(navigatorKey.currentContext ?? context) - .pushNamed(Routes.preSeedPage, arguments: _walletNewVM.seedPhraseWordsLength); + if (widget.isChildWallet) { + Navigator.of(navigatorKey.currentContext ?? context) + .pushNamed(Routes.walletGroupExistingSeedDescriptionPage, + arguments: _walletNewVM.seedPhraseWordsLength); + } else { + Navigator.of(navigatorKey.currentContext ?? context) + .pushNamed(Routes.preSeedPage, arguments: _walletNewVM.seedPhraseWordsLength); + } } if (state is FailureState) { diff --git a/lib/src/screens/new_wallet/wallet_group_description_page.dart b/lib/src/screens/new_wallet/wallet_group_description_page.dart index e892e0d49..b566d0422 100644 --- a/lib/src/screens/new_wallet/wallet_group_description_page.dart +++ b/lib/src/screens/new_wallet/wallet_group_description_page.dart @@ -7,6 +7,7 @@ import 'package:cake_wallet/themes/theme_base.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/themes/extensions/theme_type_images.dart'; class WalletGroupDescriptionPage extends BasePage { WalletGroupDescriptionPage({required this.selectedWalletType}); @@ -25,10 +26,7 @@ class WalletGroupDescriptionPage extends BasePage { padding: EdgeInsets.all(24), child: Column( children: [ - Image.asset( - _getThemedWalletGroupImage(currentTheme.type), - height: 200, - ), + Image.asset(currentTheme.type.walletGroupImage, height: 200), SizedBox(height: 32), Expanded( child: Text.rich( @@ -91,19 +89,4 @@ class WalletGroupDescriptionPage extends BasePage { ), ); } - - String _getThemedWalletGroupImage(ThemeType theme) { - final lightImage = 'assets/images/wallet_group_light.png'; - final darkImage = 'assets/images/wallet_group_dark.png'; - final brightImage = 'assets/images/wallet_group_bright.png'; - - switch (theme) { - case ThemeType.bright: - return brightImage; - case ThemeType.light: - return lightImage; - default: - return darkImage; - } - } } diff --git a/lib/src/screens/new_wallet/wallet_group_display_page.dart b/lib/src/screens/new_wallet/wallet_group_display_page.dart index a99d2bac7..549449a68 100644 --- a/lib/src/screens/new_wallet/wallet_group_display_page.dart +++ b/lib/src/screens/new_wallet/wallet_group_display_page.dart @@ -4,12 +4,13 @@ import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/screens/new_wallet/widgets/grouped_wallet_expansion_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/theme_type_images.dart'; import 'package:cake_wallet/themes/theme_base.dart'; import 'package:cake_wallet/view_model/wallet_groups_display_view_model.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; -import '../../../themes/extensions/cake_text_theme.dart'; class WalletGroupsDisplayPage extends BasePage { WalletGroupsDisplayPage(this.walletGroupsDisplayViewModel); @@ -165,10 +166,7 @@ class WalletGroupEmptyStateWidget extends StatelessWidget { Widget build(BuildContext context) { return Column( children: [ - Image.asset( - _getThemedWalletGroupImage(currentTheme.type), - scale: 1.8, - ), + Image.asset(currentTheme.type.walletGroupImage, scale: 1.8), SizedBox(height: 32), Text.rich( TextSpan( @@ -194,19 +192,4 @@ class WalletGroupEmptyStateWidget extends StatelessWidget { ], ); } - - String _getThemedWalletGroupImage(ThemeType theme) { - final lightImage = 'assets/images/wallet_group_light.png'; - final darkImage = 'assets/images/wallet_group_dark.png'; - final brightImage = 'assets/images/wallet_group_bright.png'; - - switch (theme) { - case ThemeType.bright: - return brightImage; - case ThemeType.light: - return lightImage; - default: - return darkImage; - } - } } diff --git a/lib/src/screens/new_wallet/wallet_group_existing_seed_description_page.dart b/lib/src/screens/new_wallet/wallet_group_existing_seed_description_page.dart new file mode 100644 index 000000000..4eab8fcec --- /dev/null +++ b/lib/src/screens/new_wallet/wallet_group_existing_seed_description_page.dart @@ -0,0 +1,106 @@ +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/widgets/primary_button.dart'; +import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; +import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart'; +import 'package:cake_wallet/themes/extensions/theme_type_images.dart'; +import 'package:cake_wallet/themes/theme_base.dart'; +import 'package:flutter/material.dart'; + +class WalletGroupExistingSeedDescriptionPage extends BasePage { + WalletGroupExistingSeedDescriptionPage({required this.seedPhraseWordsLength}); + + final int seedPhraseWordsLength; + + @override + String get title => S.current.wallet_group; + + @override + Widget body(BuildContext context) { + final textStyle = TextStyle( + fontSize: 16, + fontWeight: FontWeight.w400, + color: Theme.of(context).extension()!.secondaryTextColor, + ); + + return Container( + alignment: Alignment.center, + padding: EdgeInsets.all(24), + child: Column( + children: [ + Image.asset(currentTheme.type.walletGroupImage, height: 200), + SizedBox(height: 32), + Expanded( + child: RichText( + text: TextSpan( + children: [ + TextSpan( + text: S.current.wallet_group_description_existing_seed + '\n\n', + style: textStyle), + TextSpan( + text: S.current.wallet_group_description_open_wallet + '\n\n', + style: textStyle), + TextSpan( + text: S.current.wallet_group_description_view_seed + '\n', style: textStyle), + TextSpan( + text: S.current.seed_display_path, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w800, + color: Theme.of(context).extension()!.secondaryTextColor, + ), + ), + ], + ), + textAlign: TextAlign.center, + ), + ), + Column( + children: [ + Row( + mainAxisSize: MainAxisSize.max, + children: [ + Flexible( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: PrimaryButton( + key: ValueKey( + 'wallet_group_existing_seed_description_page_verify_seed_button_key'), + onPressed: () => Navigator.pushNamed(context, Routes.preSeedPage, + arguments: seedPhraseWordsLength), + text: S.current.verify_seed, + color: Theme.of(context).cardColor, + textColor: currentTheme.type == ThemeType.dark + ? Theme.of(context).extension()!.textColor + : Theme.of(context).extension()!.buttonTextColor, + ), + ), + ), + Flexible( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Builder( + builder: (context) => PrimaryButton( + key: ValueKey( + 'wallet_group_existing_seed_description_page_open_wallet_button_key'), + onPressed: () { + Navigator.of(context).popUntil((route) => route.isFirst); + }, + text: S.current.open_wallet, + color: Theme.of(context).primaryColor, + textColor: Colors.white, + ), + ), + ), + ) + ], + ), + SizedBox(height: 12), + ], + ) + ], + ), + ); + } +} diff --git a/lib/src/screens/nodes/widgets/node_form.dart b/lib/src/screens/nodes/widgets/node_form.dart index 74daa41cc..22a38f423 100644 --- a/lib/src/screens/nodes/widgets/node_form.dart +++ b/lib/src/screens/nodes/widgets/node_form.dart @@ -58,6 +58,12 @@ class NodeForm extends StatelessWidget { } }); + reaction((_) => nodeViewModel.path, (String path) { + if (path != _pathController.text) { + _pathController.text = path; + } + }); + _addressController.addListener(() => nodeViewModel.address = _addressController.text); _pathController.addListener(() => nodeViewModel.path = _pathController.text); _portController.addListener(() => nodeViewModel.port = _portController.text); diff --git a/lib/src/screens/nodes/widgets/node_list_row.dart b/lib/src/screens/nodes/widgets/node_list_row.dart index 180942f84..935a6f713 100644 --- a/lib/src/screens/nodes/widgets/node_list_row.dart +++ b/lib/src/screens/nodes/widgets/node_list_row.dart @@ -18,6 +18,37 @@ class NodeListRow extends StandardListRow { final Node node; final bool isPow; + @override + Widget build(BuildContext context) { + final leading = buildLeading(context); + final trailing = buildTrailing(context); + return Container( + height: 56, + padding: EdgeInsets.only(left: 12, right: 12, top: 2, bottom: 2), + margin: EdgeInsets.only(top: 2, bottom: 2), + child: TextButton( + onPressed: () => onTap?.call(context), + style: ButtonStyle( + backgroundColor: MaterialStateProperty.all(Theme.of(context).cardColor), + shape: MaterialStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(10) + ), + ), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (leading != null) leading, + buildCenter(context, hasLeftOffset: leading != null), + if (trailing != null) trailing, + ], + ), + ), + ); + } + @override Widget buildLeading(BuildContext context) { return FutureBuilder( @@ -56,6 +87,36 @@ class NodeHeaderListRow extends StandardListRow { NodeHeaderListRow({required String title, required void Function(BuildContext context) onTap}) : super(title: title, onTap: onTap, isSelected: false); + @override + Widget build(BuildContext context) { + final leading = buildLeading(context); + final trailing = buildTrailing(context); + return Container( + height: 56, + padding: EdgeInsets.only(left: 12, right: 12, top: 2, bottom: 2), + child: TextButton( + onPressed: () => onTap?.call(context), + style: ButtonStyle( + backgroundColor: MaterialStateProperty.all(Theme.of(context).cardColor), + shape: MaterialStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(10) + ), + ), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (leading != null) leading, + buildCenter(context, hasLeftOffset: leading != null), + if (trailing != null) trailing, + ], + ), + ), + ); + } + @override Widget buildTrailing(BuildContext context) { return SizedBox( diff --git a/lib/src/screens/receive/widgets/address_list.dart b/lib/src/screens/receive/widgets/address_list.dart index 9f15018d0..45891d03a 100644 --- a/lib/src/screens/receive/widgets/address_list.dart +++ b/lib/src/screens/receive/widgets/address_list.dart @@ -92,6 +92,7 @@ class _AddressListState extends State { walletAddressListViewModel: widget.addressListViewModel, trailingButtonTap: () async { if (widget.addressListViewModel.type == WalletType.monero || + widget.addressListViewModel.type == WalletType.wownero || widget.addressListViewModel.type == WalletType.haven) { await showPopUp( context: context, builder: (_) => getIt.get()); diff --git a/lib/src/screens/restore/wallet_restore_from_seed_form.dart b/lib/src/screens/restore/wallet_restore_from_seed_form.dart index 6a493087b..f22deb3e1 100644 --- a/lib/src/screens/restore/wallet_restore_from_seed_form.dart +++ b/lib/src/screens/restore/wallet_restore_from_seed_form.dart @@ -58,7 +58,7 @@ class WalletRestoreFromSeedFormState extends State { repeatedPasswordTextEditingController = displayWalletPassword ? TextEditingController() : null, - seedTypeController = TextEditingController(); + seedTypeController = TextEditingController(); final GlobalKey seedWidgetStateKey; final GlobalKey blockchainHeightKey; @@ -75,7 +75,8 @@ class WalletRestoreFromSeedFormState extends State { @override void initState() { - _setSeedType(widget.seedSettingsViewModel.moneroSeedType); + // _setSeedType(widget.seedTypeViewModel.moneroSeedType); + _setSeedType(MoneroSeedType.defaultSeedType); _setLanguageLabel(language); if (passwordTextEditingController != null) { @@ -99,7 +100,7 @@ class WalletRestoreFromSeedFormState extends State { } @override - void dispose() { + void dispose() { moneroSeedTypeReaction(); if (passwordListener != null) { @@ -200,9 +201,9 @@ class WalletRestoreFromSeedFormState extends State { items: _getItems(), selectedAtIndex: isPolyseed ? 1 - : seedTypeController.value.text.contains("14") - ? 2 - : 0, + : seedTypeController.value.text.contains("14") && widget.type == WalletType.wownero + ? 2 + : 0, mainAxisAlignment: MainAxisAlignment.start, onItemSelected: _changeSeedType, isSeparated: false, diff --git a/lib/src/screens/restore/wallet_restore_page.dart b/lib/src/screens/restore/wallet_restore_page.dart index 6215e26c3..dcaf2f4c7 100644 --- a/lib/src/screens/restore/wallet_restore_page.dart +++ b/lib/src/screens/restore/wallet_restore_page.dart @@ -20,64 +20,18 @@ import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:keyboard_actions/keyboard_actions.dart'; import 'package:mobx/mobx.dart'; -import 'package:smooth_page_indicator/smooth_page_indicator.dart'; class WalletRestorePage extends BasePage { WalletRestorePage(this.walletRestoreViewModel, this.seedSettingsViewModel) : walletRestoreFromSeedFormKey = GlobalKey(), walletRestoreFromKeysFormKey = GlobalKey(), - _pages = [], - _blockHeightFocusNode = FocusNode(), - _controller = PageController(initialPage: 0) { - walletRestoreViewModel.availableModes.forEach((mode) { - switch (mode) { - case WalletRestoreMode.seed: - _pages.add(WalletRestoreFromSeedForm( - seedSettingsViewModel: seedSettingsViewModel, - displayBlockHeightSelector: - walletRestoreViewModel.hasBlockchainHeightLanguageSelector, - displayLanguageSelector: walletRestoreViewModel.hasSeedLanguageSelector, - type: walletRestoreViewModel.type, - key: walletRestoreFromSeedFormKey, - blockHeightFocusNode: _blockHeightFocusNode, - onHeightOrDateEntered: (value) { - if (_isValidSeed()) { - walletRestoreViewModel.isButtonEnabled = value; - } - }, - onSeedChange: (String seed) { - final isPolyseed = walletRestoreViewModel.isPolyseed(seed); - _validateOnChange(isPolyseed: isPolyseed); - }, - onLanguageChange: (String language) { - final isPolyseed = language.startsWith("POLYSEED_"); - _validateOnChange(isPolyseed: isPolyseed); - }, - displayWalletPassword: walletRestoreViewModel.hasWalletPassword, - onPasswordChange: (String password) => walletRestoreViewModel.walletPassword = password, - onRepeatedPasswordChange: (String repeatedPassword) => walletRestoreViewModel.repeatedWalletPassword = repeatedPassword)); - break; - case WalletRestoreMode.keys: - _pages.add(WalletRestoreFromKeysFrom( - key: walletRestoreFromKeysFormKey, - walletRestoreViewModel: walletRestoreViewModel, - onPrivateKeyChange: (String seed) { - if (walletRestoreViewModel.type == WalletType.nano || - walletRestoreViewModel.type == WalletType.banano) { - walletRestoreViewModel.isButtonEnabled = _isValidSeedKey(); - } - }, - displayPrivateKeyField: walletRestoreViewModel.hasRestoreFromPrivateKey, - displayWalletPassword: walletRestoreViewModel.hasWalletPassword, - onPasswordChange: (String password) => walletRestoreViewModel.walletPassword = password, - onRepeatedPasswordChange: (String repeatedPassword) => walletRestoreViewModel.repeatedWalletPassword = repeatedPassword, - onHeightOrDateEntered: (value) => walletRestoreViewModel.isButtonEnabled = value)); - break; - default: - break; - } - }); - } + _blockHeightFocusNode = FocusNode(); + + final WalletRestoreViewModel walletRestoreViewModel; + final SeedSettingsViewModel seedSettingsViewModel; + final GlobalKey walletRestoreFromSeedFormKey; + final GlobalKey walletRestoreFromKeysFormKey; + final FocusNode _blockHeightFocusNode; bool _formProcessing = false; @@ -94,14 +48,6 @@ class WalletRestorePage extends BasePage { color: titleColor(context)), )); - final WalletRestoreViewModel walletRestoreViewModel; - final SeedSettingsViewModel seedSettingsViewModel; - final PageController _controller; - final List _pages; - final GlobalKey walletRestoreFromSeedFormKey; - final GlobalKey walletRestoreFromKeysFormKey; - final FocusNode _blockHeightFocusNode; - // DerivationType derivationType = DerivationType.unknown; // String? derivationPath = null; DerivationInfo? derivationInfo; @@ -116,40 +62,6 @@ class WalletRestorePage extends BasePage { @override Widget body(BuildContext context) { - reaction((_) => walletRestoreViewModel.state, (ExecutionState state) { - if (state is FailureState) { - WidgetsBinding.instance.addPostFrameCallback((_) { - showPopUp( - context: context, - builder: (_) { - return AlertWithOneAction( - alertTitle: S.current.new_wallet, - alertContent: state.error, - buttonText: S.of(context).ok, - buttonAction: () => Navigator.of(context).pop()); - }); - }); - } - }); - - reaction((_) => walletRestoreViewModel.mode, (WalletRestoreMode mode) { - walletRestoreViewModel.isButtonEnabled = false; - walletRestoreViewModel.walletPassword = null; - walletRestoreViewModel.repeatedWalletPassword = null; - - walletRestoreFromSeedFormKey - .currentState!.blockchainHeightKey.currentState!.restoreHeightController.text = ''; - walletRestoreFromSeedFormKey - .currentState!.blockchainHeightKey.currentState!.dateController.text = ''; - walletRestoreFromSeedFormKey.currentState!.nameTextEditingController.text = ''; - - walletRestoreFromKeysFormKey - .currentState!.blockchainHeightKey.currentState!.restoreHeightController.text = ''; - walletRestoreFromKeysFormKey - .currentState!.blockchainHeightKey.currentState!.dateController.text = ''; - walletRestoreFromKeysFormKey.currentState!.nameTextEditingController.text = ''; - }); - return KeyboardActions( config: KeyboardActionsConfig( keyboardActionsPlatform: KeyboardActionsPlatform.IOS, @@ -170,41 +82,18 @@ class WalletRestorePage extends BasePage { constraints: BoxConstraints(maxWidth: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint), child: Column( - mainAxisAlignment: MainAxisAlignment.center, children: [ Expanded( - child: PageView.builder( - onPageChanged: (page) { - walletRestoreViewModel.mode = - page == 0 ? WalletRestoreMode.seed : WalletRestoreMode.keys; - }, - controller: _controller, - itemCount: _pages.length, - itemBuilder: (_, index) => SingleChildScrollView(child: _pages[index]), + child: _WalletRestorePageBody( + walletRestoreViewModel: walletRestoreViewModel, + seedSettingsViewModel: seedSettingsViewModel, + walletRestoreFromSeedFormKey: walletRestoreFromSeedFormKey, + walletRestoreFromKeysFormKey: walletRestoreFromKeysFormKey, + blockHeightFocusNode: _blockHeightFocusNode, + derivationInfo: derivationInfo, + onDerivationInfoChanged: (info) => derivationInfo = info, ), ), - if (_pages.length > 1) - Padding( - padding: EdgeInsets.only(top: 10), - child: Semantics( - button: false, - label: 'Page Indicator', - hint: 'Swipe to change restore mode', - excludeSemantics: true, - child: SmoothPageIndicator( - controller: _controller, - count: _pages.length, - effect: ColorTransitionEffect( - spacing: 6.0, - radius: 6.0, - dotWidth: 6.0, - dotHeight: 6.0, - dotColor: Theme.of(context).hintColor.withOpacity(0.5), - activeDotColor: Theme.of(context).hintColor, - ), - ), - ), - ), Padding( padding: EdgeInsets.only(top: 20, bottom: 24, left: 24, right: 24), child: Column( @@ -213,9 +102,7 @@ class WalletRestorePage extends BasePage { builder: (context) { return LoadingPrimaryButton( key: ValueKey('wallet_restore_seed_or_key_restore_button_key'), - onPressed: () async { - await _confirmForm(context); - }, + onPressed: () async => await _confirmForm(context), text: S.of(context).restore_recover, color: Theme.of(context) .extension()! @@ -253,59 +140,6 @@ class WalletRestorePage extends BasePage { ); } - void _validateOnChange({bool isPolyseed = false}) { - if (!isPolyseed && walletRestoreViewModel.hasBlockchainHeightLanguageSelector) { - final hasHeight = walletRestoreFromSeedFormKey - .currentState?.blockchainHeightKey.currentState?.restoreHeightController.text.isNotEmpty; - - if (hasHeight == true) { - walletRestoreViewModel.isButtonEnabled = _isValidSeed(); - } - } else { - walletRestoreViewModel.isButtonEnabled = _isValidSeed(); - } - } - - bool _isValidSeed() { - final seedPhrase = - walletRestoreFromSeedFormKey.currentState!.seedWidgetStateKey.currentState!.text; - if (walletRestoreViewModel.isPolyseed(seedPhrase)) return true; - - final seedWords = seedPhrase.split(' '); - - if (seedWords.length == 14 && walletRestoreViewModel.type == WalletType.wownero) return true; - - if ((walletRestoreViewModel.type == WalletType.monero || - walletRestoreViewModel.type == WalletType.wownero || - walletRestoreViewModel.type == WalletType.haven) && - seedWords.length != WalletRestoreViewModelBase.moneroSeedMnemonicLength) { - return false; - } - - // bip39: - final validBip39SeedLengths = [12, 18, 24]; - final nonBip39WalletTypes = [WalletType.monero, WalletType.wownero, WalletType.haven]; - // if it's a bip39 wallet and the length is not valid return false - if (!nonBip39WalletTypes.contains(walletRestoreViewModel.type) && - !(validBip39SeedLengths.contains(seedWords.length))) { - return false; - } - - final words = - walletRestoreFromSeedFormKey.currentState!.seedWidgetStateKey.currentState!.words.toSet(); - return seedWords.toSet().difference(words).toSet().isEmpty; - } - - bool _isValidSeedKey() { - final seedKey = walletRestoreFromKeysFormKey.currentState!.privateKeyController.text; - - if (seedKey.length != 64 && seedKey.length != 128) { - return false; - } - - return true; - } - Map _credentials() { final credentials = {}; @@ -433,3 +267,283 @@ class WalletRestorePage extends BasePage { }); } } + +class _WalletRestorePageBody extends StatefulWidget { + const _WalletRestorePageBody({ + Key? key, + required this.walletRestoreViewModel, + required this.seedSettingsViewModel, + required this.walletRestoreFromSeedFormKey, + required this.walletRestoreFromKeysFormKey, + required this.blockHeightFocusNode, + required this.derivationInfo, + required this.onDerivationInfoChanged, + }) : super(key: key); + + final WalletRestoreViewModel walletRestoreViewModel; + final SeedSettingsViewModel seedSettingsViewModel; + final GlobalKey walletRestoreFromSeedFormKey; + final GlobalKey walletRestoreFromKeysFormKey; + final FocusNode blockHeightFocusNode; + final DerivationInfo? derivationInfo; + final void Function(DerivationInfo?) onDerivationInfoChanged; + + @override + State<_WalletRestorePageBody> createState() => _WalletRestorePageBodyState( + walletRestoreViewModel: walletRestoreViewModel, + seedSettingsViewModel: seedSettingsViewModel, + walletRestoreFromSeedFormKey: walletRestoreFromSeedFormKey, + walletRestoreFromKeysFormKey: walletRestoreFromKeysFormKey, + blockHeightFocusNode: blockHeightFocusNode, + derivationInfo: derivationInfo); +} + +class _WalletRestorePageBodyState extends State<_WalletRestorePageBody> + with SingleTickerProviderStateMixin { + _WalletRestorePageBodyState( + {required this.walletRestoreViewModel, + required this.seedSettingsViewModel, + required this.walletRestoreFromSeedFormKey, + required this.walletRestoreFromKeysFormKey, + required this.blockHeightFocusNode, + required this.derivationInfo}); + + final WalletRestoreViewModel walletRestoreViewModel; + final SeedSettingsViewModel seedSettingsViewModel; + final GlobalKey walletRestoreFromSeedFormKey; + final GlobalKey walletRestoreFromKeysFormKey; + final FocusNode blockHeightFocusNode; + DerivationInfo? derivationInfo; + + late TabController _tabController; + + late bool _hasKeysTab; + + @override + void initState() { + super.initState(); + + _hasKeysTab = widget.walletRestoreViewModel.availableModes.contains(WalletRestoreMode.keys); + final tabCount = _hasKeysTab ? 2 : 1; + _tabController = TabController(length: tabCount, vsync: this); + + _tabController.addListener(() { + if (!_tabController.indexIsChanging) { + widget.walletRestoreViewModel.mode = + _tabController.index == 0 ? WalletRestoreMode.seed : WalletRestoreMode.keys; + } + }); + + reaction( + (_) => widget.walletRestoreViewModel.mode, + (mode) { + final index = mode == WalletRestoreMode.seed ? 0 : 1; + if (_tabController.index != index) { + _tabController.animateTo(index); + } + }, + ); + + reaction((_) => walletRestoreViewModel.state, (ExecutionState state) { + if (state is FailureState) { + WidgetsBinding.instance.addPostFrameCallback((_) { + showPopUp( + context: context, + builder: (_) { + return AlertWithOneAction( + alertTitle: S.current.new_wallet, + alertContent: state.error, + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(context).pop()); + }); + }); + } + }); + + reaction((_) => walletRestoreViewModel.mode, (WalletRestoreMode mode) { + walletRestoreViewModel.isButtonEnabled = false; + walletRestoreViewModel.walletPassword = null; + walletRestoreViewModel.repeatedWalletPassword = null; + + walletRestoreFromSeedFormKey + .currentState!.blockchainHeightKey.currentState!.restoreHeightController.text = ''; + walletRestoreFromSeedFormKey + .currentState!.blockchainHeightKey.currentState!.dateController.text = ''; + walletRestoreFromSeedFormKey.currentState!.nameTextEditingController.text = ''; + + walletRestoreFromKeysFormKey + .currentState!.blockchainHeightKey.currentState!.restoreHeightController.text = ''; + walletRestoreFromKeysFormKey + .currentState!.blockchainHeightKey.currentState!.dateController.text = ''; + walletRestoreFromKeysFormKey.currentState!.nameTextEditingController.text = ''; + }); + } + + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Container( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 22), + child: TabBar( + controller: _tabController, + splashFactory: NoSplash.splashFactory, + indicatorSize: TabBarIndicatorSize.label, + isScrollable: true, + labelStyle: TextStyle( + fontSize: 18, + fontFamily: 'Lato', + fontWeight: FontWeight.w600, + color: Theme.of(context).appBarTheme.titleTextStyle!.color), + unselectedLabelStyle: TextStyle( + fontSize: 18, + fontFamily: 'Lato', + fontWeight: FontWeight.w600, + color: Theme.of(context).appBarTheme.titleTextStyle!.color?.withOpacity(0.5)), + labelColor: Theme.of(context).appBarTheme.titleTextStyle!.color, + indicatorColor: Theme.of(context).appBarTheme.titleTextStyle!.color, + indicatorPadding: EdgeInsets.zero, + labelPadding: const EdgeInsets.only(right: 24), + tabAlignment: TabAlignment.start, + dividerColor: Colors.transparent, + padding: EdgeInsets.zero, + tabs: [ + Tab(text: S.of(context).widgets_seed), + if (_hasKeysTab) Tab(text: S.of(context).keys), + ], + ), + ), + const SizedBox(height: 20), + Expanded( + child: TabBarView( + controller: _tabController, + children: [ + SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: _buildWalletRestoreFromSeedTab(), + ), + if (_hasKeysTab) + SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: _buildWalletRestoreFromKeysTab(), + ), + ], + ), + ), + ], + ), + ); + } + + WalletRestoreFromKeysFrom _buildWalletRestoreFromKeysTab() { + return WalletRestoreFromKeysFrom( + key: widget.walletRestoreFromKeysFormKey, + walletRestoreViewModel: widget.walletRestoreViewModel, + displayPrivateKeyField: widget.walletRestoreViewModel.hasRestoreFromPrivateKey, + displayWalletPassword: widget.walletRestoreViewModel.hasWalletPassword, + onPrivateKeyChange: (String seed) { + // For nano/banano, set button state if valid seed key + if (widget.walletRestoreViewModel.type == WalletType.nano || + widget.walletRestoreViewModel.type == WalletType.banano) { + widget.walletRestoreViewModel.isButtonEnabled = _isValidSeedKey(); + } + }, + onPasswordChange: (String password) => + widget.walletRestoreViewModel.walletPassword = password, + onRepeatedPasswordChange: (String repeatedPassword) => + widget.walletRestoreViewModel.repeatedWalletPassword = repeatedPassword, + onHeightOrDateEntered: (value) => widget.walletRestoreViewModel.isButtonEnabled = value, + ); + } + + WalletRestoreFromSeedForm _buildWalletRestoreFromSeedTab() { + return WalletRestoreFromSeedForm( + key: widget.walletRestoreFromSeedFormKey, + seedSettingsViewModel: widget.seedSettingsViewModel, + displayBlockHeightSelector: widget.walletRestoreViewModel.hasBlockchainHeightLanguageSelector, + displayLanguageSelector: widget.walletRestoreViewModel.hasSeedLanguageSelector, + type: widget.walletRestoreViewModel.type, + blockHeightFocusNode: widget.blockHeightFocusNode, + onHeightOrDateEntered: (value) { + // set button state + if (_isValidSeed()) { + widget.walletRestoreViewModel.isButtonEnabled = value; + } + }, + onSeedChange: (String seed) { + final isPolyseed = widget.walletRestoreViewModel.isPolyseed(seed); + _validateOnChange(isPolyseed: isPolyseed); + }, + onLanguageChange: (String language) { + final isPolyseed = language.startsWith("POLYSEED_"); + _validateOnChange(isPolyseed: isPolyseed); + }, + displayWalletPassword: widget.walletRestoreViewModel.hasWalletPassword, + onPasswordChange: (String password) => + widget.walletRestoreViewModel.walletPassword = password, + onRepeatedPasswordChange: (String repeatedPassword) => + widget.walletRestoreViewModel.repeatedWalletPassword = repeatedPassword, + ); + } + + void _validateOnChange({bool isPolyseed = false}) { + if (!isPolyseed && walletRestoreViewModel.hasBlockchainHeightLanguageSelector) { + final hasHeight = walletRestoreFromSeedFormKey + .currentState?.blockchainHeightKey.currentState?.restoreHeightController.text.isNotEmpty; + + if (hasHeight == true) { + walletRestoreViewModel.isButtonEnabled = _isValidSeed(); + } + } else { + walletRestoreViewModel.isButtonEnabled = _isValidSeed(); + } + } + + bool _isValidSeed() { + final seedPhrase = + walletRestoreFromSeedFormKey.currentState!.seedWidgetStateKey.currentState!.text; + if (walletRestoreViewModel.isPolyseed(seedPhrase)) return true; + + final seedWords = seedPhrase.split(' '); + + if (seedWords.length == 14 && walletRestoreViewModel.type == WalletType.wownero) return true; + if (seedWords.length == 26 && walletRestoreViewModel.type == WalletType.zano) return true; + + if ((walletRestoreViewModel.type == WalletType.monero || + walletRestoreViewModel.type == WalletType.wownero || + walletRestoreViewModel.type == WalletType.haven) && + seedWords.length != WalletRestoreViewModelBase.moneroSeedMnemonicLength) { + return false; + } + + // bip39: + final validBip39SeedLengths = [12, 18, 24]; + final nonBip39WalletTypes = [WalletType.monero, WalletType.wownero, WalletType.haven]; + // if it's a bip39 wallet and the length is not valid return false + if (!nonBip39WalletTypes.contains(walletRestoreViewModel.type) && + !(validBip39SeedLengths.contains(seedWords.length))) { + return false; + } + + final words = + walletRestoreFromSeedFormKey.currentState!.seedWidgetStateKey.currentState!.words.toSet(); + return seedWords.toSet().difference(words).toSet().isEmpty; + } + + bool _isValidSeedKey() { + final seedKey = walletRestoreFromKeysFormKey.currentState!.privateKeyController.text; + + if (seedKey.length != 64 && seedKey.length != 128) { + return false; + } + + return true; + } +} diff --git a/lib/src/screens/seed/wallet_seed_page.dart b/lib/src/screens/seed/wallet_seed_page.dart index 11dffdeac..d40e7671d 100644 --- a/lib/src/screens/seed/wallet_seed_page.dart +++ b/lib/src/screens/seed/wallet_seed_page.dart @@ -1,13 +1,13 @@ import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/src/widgets/seedphrase_grid_widget.dart'; +import 'package:cake_wallet/src/widgets/warning_box_widget.dart'; import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart'; import 'package:cake_wallet/themes/theme_base.dart'; -import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; import 'package:cake_wallet/utils/clipboard_util.dart'; import 'package:cake_wallet/utils/share_util.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:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; @@ -16,14 +16,9 @@ import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/view_model/wallet_seed_view_model.dart'; -import '../../../themes/extensions/send_page_theme.dart'; - class WalletSeedPage extends BasePage { WalletSeedPage(this.walletSeedViewModel, {required this.isNewWalletCreated}); - final imageLight = Image.asset('assets/images/crypto_lock_light.png'); - final imageDark = Image.asset('assets/images/crypto_lock.png'); - @override String get title => S.current.seed_title; @@ -68,7 +63,6 @@ class WalletSeedPage extends BasePage { return WillPopScope( onWillPop: () async => false, child: Container( - padding: EdgeInsets.symmetric(horizontal: 14, vertical: 8), alignment: Alignment.center, child: ConstrainedBox( constraints: @@ -79,46 +73,14 @@ class WalletSeedPage extends BasePage { Observer( builder: (_) { return Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 22, right: 22), child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ - Container( - padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8), - decoration: BoxDecoration( - color: currentTheme.type == ThemeType.dark - ? Color.fromRGBO(132, 110, 64, 1) - : Color.fromRGBO(194, 165, 94, 1), - borderRadius: BorderRadius.all(Radius.circular(12)), - border: Border.all( - color: currentTheme.type == ThemeType.dark - ? Color.fromRGBO(177, 147, 41, 1) - : Color.fromRGBO(125, 122, 15, 1), - width: 2.0, - )), - child: Row( - children: [ - Icon( - Icons.warning_amber_rounded, - size: 64, - color: Colors.white.withOpacity(0.75), - ), - SizedBox(width: 6), - Expanded( - child: Text( - S.current.cake_seeds_save_disclaimer, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w800, - color: currentTheme.type == ThemeType.dark - ? Colors.white.withOpacity(0.75) - : Colors.white.withOpacity(0.85), - ), - ), - ), - ], - ), - ), + WarningBox( + content: S.current.cake_seeds_save_disclaimer, + currentTheme: currentTheme), SizedBox(height: 20), Text( key: ValueKey('wallet_seed_page_wallet_name_text_key'), @@ -131,70 +93,19 @@ class WalletSeedPage extends BasePage { ), SizedBox(height: 20), Expanded( - child: GridView.builder( - itemCount: walletSeedViewModel.seedSplit.length, - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 3, - childAspectRatio: 2.8, - mainAxisSpacing: 8.0, - crossAxisSpacing: 8.0, - ), - itemBuilder: (context, index) { - final item = walletSeedViewModel.seedSplit[index]; - final numberCount = index + 1; - - return Container( - padding: const EdgeInsets.symmetric(horizontal: 8), - alignment: Alignment.center, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - color: Theme.of(context).cardColor, - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - SizedBox( - child: Text( - //maxLines: 1, - numberCount.toString(), - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 12, - height: 1, - fontWeight: FontWeight.w800, - color: Theme.of(context) - .extension()! - .buttonTextColor - .withOpacity(0.5)), - ), - ), - const SizedBox(width: 6), - Expanded( - child: Text( - '${item[0].toLowerCase()}${item.substring(1)}', - style: TextStyle( - fontSize: 14, - height: 0.8, - fontWeight: FontWeight.w700, - color: Theme.of(context) - .extension()! - .buttonTextColor), - ), - ), - ], - ), - ); - }, - ), + child: SeedPhraseGridWidget(list: walletSeedViewModel.seedSplit), ), ], ), + ), ); }, ), Column( children: [ - Row( + Padding( + padding: EdgeInsets.symmetric(horizontal: 14, vertical: 8), + child: Row( mainAxisSize: MainAxisSize.max, children: [ Flexible( @@ -233,6 +144,7 @@ class WalletSeedPage extends BasePage { ) ], ), + ), SizedBox(height: 12), ], ) diff --git a/lib/src/screens/send/send_page.dart b/lib/src/screens/send/send_page.dart index 2d64c55a3..6e9178cda 100644 --- a/lib/src/screens/send/send_page.dart +++ b/lib/src/screens/send/send_page.dart @@ -599,6 +599,7 @@ class SendPage extends BasePage { alertTitle: S.of(context).proceed_on_device, alertContent: S.of(context).proceed_on_device_description, buttonText: S.of(context).cancel, + alertBarrierDismissible: false, buttonAction: () { sendViewModel.state = InitialExecutionState(); Navigator.of(context).pop(); diff --git a/lib/src/screens/send/transaction_success_info_page.dart b/lib/src/screens/send/transaction_success_info_page.dart index 628cc0393..bd4f5cc05 100644 --- a/lib/src/screens/send/transaction_success_info_page.dart +++ b/lib/src/screens/send/transaction_success_info_page.dart @@ -27,6 +27,7 @@ class TransactionSuccessPage extends InfoPage { Key? get buttonKey => ValueKey('transaction_success_info_page_button_key'); @override - void Function(BuildContext) get onPressed => - (BuildContext context) => Navigator.of(context).pop(); + void Function(BuildContext) get onPressed => (BuildContext context) { + if (context.mounted) Navigator.of(context).pop(); + }; } diff --git a/lib/src/screens/send/widgets/extract_address_from_parsed.dart b/lib/src/screens/send/widgets/extract_address_from_parsed.dart index 106be97ee..29c4eb793 100644 --- a/lib/src/screens/send/widgets/extract_address_from_parsed.dart +++ b/lib/src/screens/send/widgets/extract_address_from_parsed.dart @@ -66,6 +66,16 @@ Future extractAddressFromParsed( content = S.of(context).extracted_address_content('${parsedAddress.name} (ThorChain)'); address = parsedAddress.addresses.first; break; + case ParseFrom.zanoAlias: + title = S.of(context).address_detected; + content = S.of(context).extracted_address_content('${parsedAddress.name} (Zano Alias)'); + address = parsedAddress.addresses.first; + break; + case ParseFrom.bip353: + title = S.of(context).address_detected; + content = S.of(context).extracted_address_content('${parsedAddress.name} (BIP-353)'); + address = parsedAddress.addresses.first; + break; case ParseFrom.yatRecord: if (parsedAddress.name.isEmpty) { title = S.of(context).yat_error; diff --git a/lib/src/screens/send/widgets/send_card.dart b/lib/src/screens/send/widgets/send_card.dart index 0713fb8c4..eed0ce233 100644 --- a/lib/src/screens/send/widgets/send_card.dart +++ b/lib/src/screens/send/widgets/send_card.dart @@ -142,7 +142,7 @@ class SendCardState extends State with AutomaticKeepAliveClientMixin { - final int itemCount = SettingActions.desktopSettings.length; + final int itemCount = SettingActions.all.length; int? currentPage; @@ -35,6 +34,7 @@ class _DesktopSettingsPageState extends State { Widget build(BuildContext context) { return Scaffold( body: Container( + color: Theme.of(context).colorScheme.background, height: MediaQuery.of(context).size.height, child: Row( children: [ @@ -54,7 +54,7 @@ class _DesktopSettingsPageState extends State { child: ListView.separated( padding: EdgeInsets.only(top: 0), itemBuilder: (_, index) { - final item = SettingActions.desktopSettings[index]; + final item = SettingActions.all[index]; if (!widget.dashboardViewModel.hasSilentPayments && item.name(context) == S.of(context).silent_payments_settings) { diff --git a/lib/src/screens/settings/display_settings_page.dart b/lib/src/screens/settings/display_settings_page.dart index ba59df3af..ae9100e4a 100644 --- a/lib/src/screens/settings/display_settings_page.dart +++ b/lib/src/screens/settings/display_settings_page.dart @@ -27,12 +27,6 @@ class DisplaySettingsPage extends BasePage { padding: EdgeInsets.only(top: 10), child: Column( children: [ - SettingsSwitcherCell( - title: S.current.settings_display_balance, - value: _displaySettingsViewModel.shouldDisplayBalance, - onValueChange: (_, bool value) { - _displaySettingsViewModel.setShouldDisplayBalance(value); - }), SettingsSwitcherCell( title: S.current.show_market_place, value: _displaySettingsViewModel.shouldShowMarketPlaceInDashboard, @@ -40,6 +34,13 @@ class DisplaySettingsPage extends BasePage { _displaySettingsViewModel.setShouldShowMarketPlaceInDashbaord(value); }, ), + SettingsSwitcherCell( + title: S.of(context).show_address_book_popup, + value: _displaySettingsViewModel.showAddressBookPopup, + onValueChange: (_, bool value) { + _displaySettingsViewModel.setShowAddressBookPopup(value); + }, + ), //if (!isHaven) it does not work correctly if (!_displaySettingsViewModel.disabledFiatApiMode) SettingsPickerCell( diff --git a/lib/src/screens/settings/domain_lookups_page.dart b/lib/src/screens/settings/domain_lookups_page.dart index 0eb559817..ed5916437 100644 --- a/lib/src/screens/settings/domain_lookups_page.dart +++ b/lib/src/screens/settings/domain_lookups_page.dart @@ -49,6 +49,10 @@ class DomainLookupsPage extends BasePage { title: '.well-known', value: _privacySettingsViewModel.looksUpWellKnown, onValueChange: (_, bool value) => _privacySettingsViewModel.setLookupsWellKnown(value)), + SettingsSwitcherCell( + title: 'Zano Aliases', + value: _privacySettingsViewModel.lookupsZanoAlias, + onValueChange: (_, bool value) => _privacySettingsViewModel.setLookupsZanoAlias(value)), //if (!isHaven) it does not work correctly ], diff --git a/lib/src/screens/settings/manage_nodes_page.dart b/lib/src/screens/settings/manage_nodes_page.dart index 3cc251019..a5db773cc 100644 --- a/lib/src/screens/settings/manage_nodes_page.dart +++ b/lib/src/screens/settings/manage_nodes_page.dart @@ -34,7 +34,6 @@ class ManageNodesPage extends BasePage { onTap: (_) async => await Navigator.of(context).pushNamed(Routes.newNode), ), ), - const StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)), SizedBox(height: 20), Observer( builder: (BuildContext context) { diff --git a/lib/src/screens/settings/mweb_logs_page.dart b/lib/src/screens/settings/mweb_logs_page.dart index 3c5470214..ae476052e 100644 --- a/lib/src/screens/settings/mweb_logs_page.dart +++ b/lib/src/screens/settings/mweb_logs_page.dart @@ -77,15 +77,12 @@ class MwebLogsPage extends BasePage { return AlertWithTwoActions( alertTitle: S.of(context).export_backup, alertContent: S.of(context).select_destination, - rightButtonText: S.of(context).save_to_downloads, - leftButtonText: S.of(context).share, - actionRightButton: () async { - const downloadDirPath = "/storage/emulated/0/Download"; - final filePath = downloadDirPath + "/debug.log"; - await mwebSettingsViewModelBase.saveLogsLocally(filePath); + rightButtonText: S.of(context).save, + leftButtonText: S.of(context).cancel, + actionLeftButton: () async { Navigator.of(dialogContext).pop(); }, - actionLeftButton: () async { + actionRightButton: () async { Navigator.of(dialogContext).pop(); try { await share(context); @@ -101,11 +98,8 @@ class MwebLogsPage extends BasePage { } Future share(BuildContext context) async { - final filePath = (await getAppDir()).path + "/debug.log"; - bool success = await mwebSettingsViewModelBase.saveLogsLocally(filePath); - if (!success) return; - await ShareUtil.shareFile(filePath: filePath, fileName: "debug.log", context: context); - await mwebSettingsViewModelBase.removeLogsLocally(filePath); + final inAppPath = "${(await getApplicationSupportDirectory()).path}/logs/debug.log"; + await ShareUtil.shareFile(filePath: inAppPath, fileName: "debug.log", context: context); } Future _saveFile() async { diff --git a/lib/src/screens/settings/other_settings_page.dart b/lib/src/screens/settings/other_settings_page.dart index 24f321798..8e0a25958 100644 --- a/lib/src/screens/settings/other_settings_page.dart +++ b/lib/src/screens/settings/other_settings_page.dart @@ -63,13 +63,6 @@ class OtherSettingsPage extends BasePage { handler: (BuildContext context) => Navigator.of(context).pushNamed(Routes.readDisclaimer), ), - SettingsSwitcherCell( - title: S.of(context).show_address_book_popup, - value: _otherSettingsViewModel.showAddressBookPopup, - onValueChange: (_, bool value) { - _otherSettingsViewModel.setShowAddressBookPopup(value); - }, - ), Spacer(), SettingsVersionCell( title: S.of(context).version(_otherSettingsViewModel.currentVersion)), diff --git a/lib/src/screens/settings/security_backup_page.dart b/lib/src/screens/settings/security_backup_page.dart index bb1c00ff5..172ae78d9 100644 --- a/lib/src/screens/settings/security_backup_page.dart +++ b/lib/src/screens/settings/security_backup_page.dart @@ -35,41 +35,6 @@ class SecurityBackupPage extends BasePage { child: Column( mainAxisSize: MainAxisSize.min, children: [ - if (!_isHardwareWallet) - SettingsCellWithArrow( - key: ValueKey('security_backup_page_show_keys_button_key'), - title: S.current.show_keys, - handler: (_) => _authService.authenticateAction( - context, - route: Routes.showKeys, - conditionToDetermineIfToUse2FA: - _securitySettingsViewModel.shouldRequireTOTP2FAForAllSecurityAndBackupSettings, - ), - ), - if (!SettingsStoreBase.walletPasswordDirectInput) - SettingsCellWithArrow( - key: ValueKey('security_backup_page_create_backup_button_key'), - title: S.current.create_backup, - handler: (_) => _authService.authenticateAction( - context, - route: Routes.backup, - conditionToDetermineIfToUse2FA: - _securitySettingsViewModel.shouldRequireTOTP2FAForAllSecurityAndBackupSettings, - ), - ), - SettingsCellWithArrow( - key: ValueKey('security_backup_page_change_pin_button_key'), - title: S.current.settings_change_pin, - handler: (_) => _authService.authenticateAction( - context, - route: Routes.setupPin, - arguments: (PinCodeState setupPinContext, String _) { - setupPinContext.close(); - }, - conditionToDetermineIfToUse2FA: - _securitySettingsViewModel.shouldRequireTOTP2FAForAllSecurityAndBackupSettings, - ), - ), if (DeviceInfo.instance.isMobile || Platform.isMacOS || Platform.isLinux) Observer(builder: (_) { return SettingsSwitcherCell( @@ -110,6 +75,47 @@ class SecurityBackupPage extends BasePage { }, ); }), + if (!_isHardwareWallet) + SettingsCellWithArrow( + key: ValueKey('security_backup_page_show_keys_button_key'), + title: S.current.show_keys, + handler: (_) => _authService.authenticateAction( + context, + route: Routes.showKeys, + conditionToDetermineIfToUse2FA: + _securitySettingsViewModel.shouldRequireTOTP2FAForAllSecurityAndBackupSettings, + ), + ), + if (!SettingsStoreBase.walletPasswordDirectInput) + SettingsCellWithArrow( + key: ValueKey('security_backup_page_create_backup_button_key'), + title: S.current.create_backup, + handler: (_) => _authService.authenticateAction( + context, + route: Routes.backup, + conditionToDetermineIfToUse2FA: + _securitySettingsViewModel.shouldRequireTOTP2FAForAllSecurityAndBackupSettings, + ), + ), + SettingsCellWithArrow( + key: ValueKey('security_backup_page_change_pin_button_key'), + title: S.current.settings_change_pin, + handler: (_) => _authService.authenticateAction( + context, + route: Routes.setupPin, + arguments: (PinCodeState setupPinContext, String _) { + setupPinContext.close(); + }, + conditionToDetermineIfToUse2FA: + _securitySettingsViewModel.shouldRequireTOTP2FAForAllSecurityAndBackupSettings, + ), + ), + SettingsCellWithArrow( + key: ValueKey('security_backup_page_sign_and_verify'), + title: S.current.sign_verify_title, + handler: (_) => Navigator.of(context).pushNamed(Routes.signPage) + //_securitySettingsViewModel.pinCodeRequiredDuration, + ), Observer( builder: (context) { return SettingsCellWithArrow( diff --git a/lib/src/screens/settings/widgets/settings_choices_cell.dart b/lib/src/screens/settings/widgets/settings_choices_cell.dart index 63ad1ef9a..cbb8c60e9 100644 --- a/lib/src/screens/settings/widgets/settings_choices_cell.dart +++ b/lib/src/screens/settings/widgets/settings_choices_cell.dart @@ -12,7 +12,7 @@ class SettingsChoicesCell extends StatelessWidget { Widget build(BuildContext context) { return Container( color: Theme.of(context).colorScheme.background, - padding: EdgeInsets.all(24), + padding: EdgeInsets.only(left: 24, right: 24, top: 16, bottom: 16), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -30,7 +30,7 @@ class SettingsChoicesCell extends StatelessWidget { ), ], ), - const SizedBox(height: 24), + const SizedBox(height: 12), ], Center( child: Container( diff --git a/lib/src/screens/settings/widgets/settings_switcher_cell.dart b/lib/src/screens/settings/widgets/settings_switcher_cell.dart index 6a3c8b4a0..bc3421ead 100644 --- a/lib/src/screens/settings/widgets/settings_switcher_cell.dart +++ b/lib/src/screens/settings/widgets/settings_switcher_cell.dart @@ -1,6 +1,7 @@ import 'package:flutter/cupertino.dart'; import 'package:cake_wallet/src/widgets/standard_list.dart'; import 'package:cake_wallet/src/widgets/standard_switch.dart'; +import 'package:flutter/material.dart'; class SettingsSwitcherCell extends StandardListRow { SettingsSwitcherCell({ @@ -21,6 +22,37 @@ class SettingsSwitcherCell extends StandardListRow { Widget buildTrailing(BuildContext context) => StandardSwitch(value: value, onTaped: () => onValueChange?.call(context, !value)); + @override + Widget build(BuildContext context) { + final leading = buildLeading(context); + final trailing = buildTrailing(context); + return Container( + height: 56, + padding: EdgeInsets.only(left: 12, right: 12), + child: TextButton( + onPressed: () => onValueChange?.call(context, !value), + style: ButtonStyle( + //backgroundColor: MaterialStateProperty.all(Theme.of(context).cardColor), + shape: MaterialStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(10) + ), + ), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (leading != null) leading, + buildCenter(context, hasLeftOffset: leading != null), + if (trailing != null) trailing, + ], + ), + ), + ); + } + + @override Widget? buildLeading(BuildContext context) => leading; } diff --git a/lib/src/screens/support/support_page.dart b/lib/src/screens/support/support_page.dart index 88154e91d..2dd098813 100644 --- a/lib/src/screens/support/support_page.dart +++ b/lib/src/screens/support/support_page.dart @@ -42,7 +42,7 @@ class SupportPage extends BasePage { child: Column( children: [ Padding( - padding: EdgeInsets.only(top: 24), + padding: EdgeInsets.only(top: 20), child: OptionTile( icon: Icon( Icons.support_agent, @@ -61,7 +61,7 @@ class SupportPage extends BasePage { ), ), Padding( - padding: EdgeInsets.only(top: 24), + padding: EdgeInsets.only(top: 20), child: OptionTile( icon: Icon( Icons.find_in_page, @@ -74,7 +74,7 @@ class SupportPage extends BasePage { ), ), Padding( - padding: EdgeInsets.only(top: 24), + padding: EdgeInsets.only(top: 20), child: OptionTile( icon: Icon( Icons.contact_support, diff --git a/lib/src/screens/ur/animated_ur_page.dart b/lib/src/screens/ur/animated_ur_page.dart index dc40e6a12..cf9aa8509 100644 --- a/lib/src/screens/ur/animated_ur_page.dart +++ b/lib/src/screens/ur/animated_ur_page.dart @@ -100,6 +100,7 @@ class AnimatedURPage extends BasePage { switch (urQrType) { case "ur:xmr-txunsigned": // ur:xmr-txsigned final ur = await presentQRScanner(context); + if (ur == null) return; final result = await monero!.commitTransactionUR(animatedURmodel.wallet, ur); if (result) { Navigator.of(context).pop(true); @@ -107,6 +108,7 @@ class AnimatedURPage extends BasePage { break; case "ur:xmr-output": // xmr-keyimage final ur = await presentQRScanner(context); + if (ur == null) return; final result = await monero!.importKeyImagesUR(animatedURmodel.wallet, ur); if (result) { Navigator.of(context).pop(true); diff --git a/lib/src/screens/wallet_keys/wallet_keys_page.dart b/lib/src/screens/wallet_keys/wallet_keys_page.dart index ac00bb161..ab6762f8d 100644 --- a/lib/src/screens/wallet_keys/wallet_keys_page.dart +++ b/lib/src/screens/wallet_keys/wallet_keys_page.dart @@ -1,10 +1,14 @@ -import 'package:auto_size_text/auto_size_text.dart'; import 'package:cake_wallet/entities/qr_view_data.dart'; -import 'package:cake_wallet/src/screens/base_page.dart'; -import 'package:cake_wallet/src/widgets/list_row.dart'; -import 'package:cake_wallet/src/widgets/section_divider.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/widgets/primary_button.dart'; +import 'package:cake_wallet/src/widgets/seedphrase_grid_widget.dart'; +import 'package:cake_wallet/src/widgets/text_info_box.dart'; +import 'package:cake_wallet/src/widgets/warning_box_widget.dart'; +import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; +import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart'; +import 'package:cake_wallet/themes/theme_base.dart'; import 'package:cake_wallet/utils/brightness_util.dart'; import 'package:cake_wallet/utils/clipboard_util.dart'; import 'package:cake_wallet/utils/show_bar.dart'; @@ -13,7 +17,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:qr_flutter/qr_flutter.dart'; -import 'package:cake_wallet/themes/extensions/picker_theme.dart'; class WalletKeysPage extends BasePage { WalletKeysPage(this.walletKeysViewModel); @@ -24,95 +27,366 @@ class WalletKeysPage extends BasePage { final WalletKeysViewModel walletKeysViewModel; @override - Widget trailing(BuildContext context) => IconButton( - key: ValueKey('wallet_keys_page_fullscreen_qr_button_key'), - onPressed: () async { - final url = await walletKeysViewModel.url; + Widget body(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 0), + child: Column( + children: [ + Padding( + padding: EdgeInsets.only(left: 14, right: 14, bottom: 8), + child: WarningBox( + key: const ValueKey('wallet_keys_page_share_warning_text_key'), + content: S.of(context).do_not_share_warning_text.toUpperCase(), + currentTheme: currentTheme, + ), + ), + Expanded( + child: WalletKeysPageBody( + walletKeysViewModel: walletKeysViewModel, + currentTheme: currentTheme, + ), + ), + ], + ), + ); + } +} - BrightnessUtil.changeBrightnessForFunction(() async { - await Navigator.pushNamed( - context, - Routes.fullscreenQR, - arguments: QrViewData(data: url.toString(), version: QrVersions.auto), - ); - }); - }, - splashColor: Colors.transparent, - highlightColor: Colors.transparent, - hoverColor: Colors.transparent, - icon: Image.asset( - 'assets/images/qr_code_icon.png', - ), - ); +class WalletKeysPageBody extends StatefulWidget { + WalletKeysPageBody({ + required this.walletKeysViewModel, + required this.currentTheme, + }); + + final WalletKeysViewModel walletKeysViewModel; + final ThemeBase currentTheme; @override - Widget body(BuildContext context) { + State createState() => _WalletKeysPageBodyState(); +} + +class _WalletKeysPageBodyState extends State + with SingleTickerProviderStateMixin { + late TabController _tabController; + late bool showKeyTab; + late bool showLegacySeedTab; + late bool isLegacySeedOnly; + + @override + void initState() { + super.initState(); + + showKeyTab = widget.walletKeysViewModel.items.isNotEmpty; + showLegacySeedTab = widget.walletKeysViewModel.legacySeedSplit.isNotEmpty; + isLegacySeedOnly = widget.walletKeysViewModel.isLegacySeedOnly; + + final totalTabs = 1 + (showKeyTab ? 1 : 0) + (showLegacySeedTab ? 1 : 0); + + _tabController = TabController(length: totalTabs, vsync: this); + } + + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 0), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.only(left: 22, right: 22, top: 0), + child: TabBar( + controller: _tabController, + splashFactory: NoSplash.splashFactory, + indicatorSize: TabBarIndicatorSize.label, + isScrollable: true, + labelStyle: TextStyle( + fontSize: 18, + fontFamily: 'Lato', + fontWeight: FontWeight.w600, + color: Theme.of(context).appBarTheme.titleTextStyle!.color, + ), + unselectedLabelStyle: TextStyle( + fontSize: 18, + fontFamily: 'Lato', + fontWeight: FontWeight.w600, + color: Theme.of(context).appBarTheme.titleTextStyle!.color?.withOpacity(0.5), + ), + labelColor: Theme.of(context).appBarTheme.titleTextStyle!.color, + indicatorColor: Theme.of(context).appBarTheme.titleTextStyle!.color, + indicatorPadding: EdgeInsets.zero, + labelPadding: const EdgeInsets.only(right: 24), + tabAlignment: TabAlignment.start, + dividerColor: Colors.transparent, + padding: EdgeInsets.zero, + tabs: [ + Tab(text: S.of(context).widgets_seed), + if (showKeyTab) Tab(text: S.of(context).keys), + if (showLegacySeedTab) Tab(text: S.of(context).legacy), + ], + ), + ), + const SizedBox(height: 20), + Expanded( + child: TabBarView( + controller: _tabController, + children: [ + Padding( + padding: const EdgeInsets.only(left: 22, right: 22), + child: _buildSeedTab(context, false), + ), + if (showKeyTab) + Padding( + padding: const EdgeInsets.only(left: 22, right: 22), + child: _buildKeysTab(context), + ), + if (showLegacySeedTab) + Padding( + padding: const EdgeInsets.only(left: 22, right: 22), + child: _buildSeedTab(context, showLegacySeedTab), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildSeedTab(BuildContext context, bool isLegacySeed) { return Column( children: [ + if (isLegacySeedOnly || isLegacySeed) + ...[ + _buildHeightBox(), + const SizedBox(height: 20), + ], + (_buildPassphraseBox() ?? Container()), + if (widget.walletKeysViewModel.passphrase.isNotEmpty) const SizedBox(height: 20), Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 24.0), - child: Container( - width: double.infinity, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12.0), - color: Theme.of(context).cardColor, - ), - child: Center( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: AutoSizeText( - key: ValueKey('wallet_keys_page_share_warning_text_key'), - S.of(context).do_not_share_warning_text.toUpperCase(), - textAlign: TextAlign.center, - maxLines: 4, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - color: Colors.red, - ), - ), - ), - ), - ), + child: SeedPhraseGridWidget( + list: isLegacySeed + ? widget.walletKeysViewModel.legacySeedSplit + : widget.walletKeysViewModel.seedSplit, ), ), - Expanded( - flex: 7, - child: Container( - padding: EdgeInsets.only(top: 20.0, bottom: 20.0), - child: Observer( - builder: (_) { - return ListView.separated( - key: ValueKey('wallet_keys_page_credentials_list_view_key'), - separatorBuilder: (context, index) => Container( - height: 1, - padding: EdgeInsets.only(left: 24), - color: Theme.of(context).extension()!.dividerColor, - child: const HorizontalSectionDivider(), - ), - itemCount: walletKeysViewModel.items.length, - itemBuilder: (BuildContext context, int index) { - final item = walletKeysViewModel.items[index]; - - return GestureDetector( - key: item.key, - onTap: () { - ClipboardUtil.setSensitiveDataToClipboard(ClipboardData(text: item.value)); - showBar(context, S.of(context).copied_key_to_clipboard(item.title)); - }, - child: ListRow( - title: item.title + ':', - value: item.value, - ), - ); - }, - ); - }, - ), - ), + const SizedBox(height: 10), + _buildBottomActionPanel( + titleForClipboard: S.of(context).wallet_seed.toLowerCase(), + dataToCopy: isLegacySeed + ? widget.walletKeysViewModel.legacySeed + : widget.walletKeysViewModel.seed, + onShowQR: () async => _showQR(context), ), ], ); } + + Widget _buildKeysTab(BuildContext context) { + return Column( + children: [ + Expanded( + child: ListView.separated( + shrinkWrap: true, + itemCount: widget.walletKeysViewModel.items.length, + itemBuilder: (context, index) { + final item = widget.walletKeysViewModel.items[index]; + return TextInfoBox( + key: item.key, + title: item.title, + value: item.value, + onCopy: (context) => _onCopy(item.title, item.value, context), + ); + }, + separatorBuilder: (context, index) => const SizedBox(height: 20), + ), + ), + SizedBox(height: 20), + ], + ); + } + + Widget _buildHeightBox() { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 14), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: Theme.of(context).cardColor, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + S.of(context).block_height, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Theme.of(context).extension()!.buttonTextColor.withOpacity(0.5), + ), + ), + const SizedBox(width: 6), + Expanded( + child: FutureBuilder( + future: widget.walletKeysViewModel.restoreHeight, + builder: (context, snapshot) { + final textToDisplay = snapshot.connectionState == ConnectionState.waiting + ? 'Fetching...' + : (snapshot.data ?? '---'); + + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + textToDisplay, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Theme.of(context).extension()!.buttonTextColor, + ), + ), + if (snapshot.connectionState == ConnectionState.done && snapshot.data != null) + GestureDetector( + onTap: () => _onCopy(S.of(context).block_height, snapshot.data!, context), + child: Icon( + Icons.copy, + size: 16, + color: Theme.of(context).textTheme.bodyLarge?.color?.withOpacity(0.7), + ), + ), + ], + ); + }, + ), + ), + ], + ), + ); + } + + Widget? _buildPassphraseBox() { + if (widget.walletKeysViewModel.passphrase.isEmpty) return null; + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 14), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: Theme.of(context).cardColor, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + S.of(context).passphrase_view_keys, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Theme.of(context).extension()!.buttonTextColor.withOpacity(0.5), + ), + ), + const SizedBox(width: 6), + Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Observer(builder: (BuildContext context) { + return Text( + (widget.walletKeysViewModel.obscurePassphrase) + ? "*****" + : widget.walletKeysViewModel.passphrase, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Theme.of(context).extension()!.buttonTextColor, + ), + ); + }), + Observer(builder: (BuildContext context) { + return GestureDetector( + onTap: () { + widget.walletKeysViewModel.obscurePassphrase = + !widget.walletKeysViewModel.obscurePassphrase; + }, + child: Icon( + widget.walletKeysViewModel.obscurePassphrase + ? Icons.visibility_off + : Icons.visibility, + size: 16, + color: Theme.of(context).textTheme.bodyLarge?.color?.withOpacity(0.7), + )); + }), + ], + ), + ), + ], + ), + ); + } + + Widget _buildBottomActionPanel({ + required String titleForClipboard, + required String dataToCopy, + required VoidCallback onShowQR, + }) { + return Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + Flexible( + child: Container( + padding: const EdgeInsets.only(right: 8.0, top: 8.0), + child: PrimaryButton( + key: const ValueKey('wallet_keys_page_copy_seeds_button_key'), + onPressed: () => _onCopy(titleForClipboard, dataToCopy, context), + text: S.of(context).copy, + color: Theme.of(context).cardColor, + textColor: widget.currentTheme.type == ThemeType.dark + ? Theme.of(context).extension()!.textColor + : Theme.of(context).extension()!.buttonTextColor, + ), + ), + ), + Flexible( + child: Container( + padding: const EdgeInsets.only(left: 8.0, top: 8.0), + child: PrimaryButton( + key: const ValueKey('wallet_keys_page_show_qr_seeds_button_key'), + onPressed: onShowQR, + text: S.current.show + ' QR', + color: Theme.of(context).primaryColor, + textColor: Colors.white, + ), + ), + ), + ], + ), + ), + const SizedBox(height: 12), + ], + ); + } + + Future _onCopy(String title, String text, BuildContext context) async { + await ClipboardUtil.setSensitiveDataToClipboard(ClipboardData(text: text)); + showBar(context, S.of(context).copied_key_to_clipboard(title)); + } + + Future _showQR(BuildContext context) async { + final url = await widget.walletKeysViewModel.getUrl(false); + + BrightnessUtil.changeBrightnessForFunction(() async { + await Navigator.pushNamed( + context, + Routes.fullscreenQR, + arguments: QrViewData(data: url.toString(), version: QrVersions.auto), + ); + }); + } } diff --git a/lib/src/screens/wallet_list/wallet_list_page.dart b/lib/src/screens/wallet_list/wallet_list_page.dart index 9bf924f61..681ca4d8a 100644 --- a/lib/src/screens/wallet_list/wallet_list_page.dart +++ b/lib/src/screens/wallet_list/wallet_list_page.dart @@ -1,7 +1,6 @@ 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/core/secure_storage.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'; @@ -89,7 +88,8 @@ class WalletListPage extends BasePage { width: 36, decoration: BoxDecoration( shape: BoxShape.circle, - color: Theme.of(context).extension()!.buttonColor, + color: + Theme.of(context).extension()!.buttonColor, ), child: filterIcon, ), @@ -131,6 +131,7 @@ class WalletListBodyState extends State { final solanaIcon = Image.asset('assets/images/sol_icon.png', height: 24, width: 24); final tronIcon = Image.asset('assets/images/trx_icon.png', height: 24, width: 24); final wowneroIcon = Image.asset('assets/images/wownero_icon.png', height: 24, width: 24); + final zanoIcon = Image.asset('assets/images/zano_icon.png', height: 24, width: 24); final scrollController = ScrollController(); final double tileHeight = 60; Flushbar? _progressBar; @@ -145,259 +146,336 @@ class WalletListBodyState extends State { color: Theme.of(context).extension()!.buttonTextColor); return Container( + height: double.infinity, padding: EdgeInsets.only(top: 16), - child: Column( - children: [ - Expanded( - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (widget.walletListViewModel.multiWalletGroups.isNotEmpty) ...{ - Padding( - padding: const EdgeInsets.only(left: 24), - child: Text( - S.current.shared_seed_wallet_groups, - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w500, - color: Theme.of(context).extension()!.titleColor, - ), + + child: Stack( + alignment: Alignment.bottomCenter, + fit: StackFit.expand, + children: [ + SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (widget + .walletListViewModel.multiWalletGroups.isNotEmpty) ...{ + Padding( + padding: const EdgeInsets.only(left: 24), + child: Text( + S.current.shared_seed_wallet_groups, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w500, + color: Theme.of(context) + .extension()! + .titleColor, ), ), - SizedBox(height: 16), - Container( - child: Observer( - builder: (_) => FilteredList( - shrinkWrap: true, - list: widget.walletListViewModel.multiWalletGroups, - updateFunction: widget.walletListViewModel.reorderAccordingToWalletList, - itemBuilder: (context, index) { - final group = widget.walletListViewModel.multiWalletGroups[index]; - final groupName = group.groupName ?? - '${S.current.wallet_group} ${index + 1}'; + ), + SizedBox(height: 16), + Container( + child: Observer( + builder: (_) => FilteredList( + shrinkWrap: true, + list: widget.walletListViewModel.multiWalletGroups, + updateFunction: widget + .walletListViewModel.reorderAccordingToWalletList, + itemBuilder: (context, index) { + final group = widget + .walletListViewModel.multiWalletGroups[index]; + final groupName = group.groupName ?? + '${S.current.wallet_group} ${index + 1}'; - widget.walletListViewModel.updateTileState( - index, - widget.walletListViewModel.expansionTileStateTrack[index] ?? false, - ); + widget.walletListViewModel.updateTileState( + index, + widget.walletListViewModel + .expansionTileStateTrack[index] ?? + false, + ); - return GroupedWalletExpansionTile( - onExpansionChanged: (value) { - widget.walletListViewModel.updateTileState(index, value); - setState(() {}); + return GroupedWalletExpansionTile( + onExpansionChanged: (value) { + widget.walletListViewModel + .updateTileState(index, value); + setState(() {}); + }, + shouldShowCurrentWalletPointer: true, + borderRadius: BorderRadius.all(Radius.circular(16)), + margin: EdgeInsets.only( + left: 20, right: 20, bottom: 12), + title: groupName, + tileKey: ValueKey( + 'group_wallets_expansion_tile_widget_$index'), + leadingWidget: Icon( + Icons.account_balance_wallet_outlined, + size: 28, + ), + trailingWidget: EditWalletButtonWidget( + width: 74, + isGroup: true, + isExpanded: widget.walletListViewModel + .expansionTileStateTrack[index]!, + onTap: () { + final wallet = widget.walletListViewModel + .convertWalletInfoToWalletListItem( + group.wallets.first); + Navigator.of(context).pushNamed( + Routes.walletEdit, + arguments: WalletEditPageArguments( + walletListViewModel: + widget.walletListViewModel, + editingWallet: wallet, + isWalletGroup: true, + groupName: groupName, + parentAddress: group.parentAddress, + ), + ); }, - shouldShowCurrentWalletPointer: true, - borderRadius: BorderRadius.all(Radius.circular(16)), - margin: EdgeInsets.only(left: 20, right: 20, bottom: 12), - title: groupName, - tileKey: ValueKey('group_wallets_expansion_tile_widget_$index'), - leadingWidget: Icon( - Icons.account_balance_wallet_outlined, - size: 28, - ), - trailingWidget: EditWalletButtonWidget( - width: 74, - isGroup: true, - isExpanded: widget.walletListViewModel.expansionTileStateTrack[index]!, - onTap: () { - final wallet = widget.walletListViewModel - .convertWalletInfoToWalletListItem(group.wallets.first); - Navigator.of(context).pushNamed( - Routes.walletEdit, - arguments: WalletEditPageArguments( - walletListViewModel: widget.walletListViewModel, - editingWallet: wallet, - isWalletGroup: true, - groupName: groupName, - parentAddress: group.parentAddress, - ), - ); - }, - ), - childWallets: group.wallets.map((walletInfo) { - return widget.walletListViewModel.convertWalletInfoToWalletListItem(walletInfo); - }).toList(), - isSelected: false, - onChildItemTapped: (wallet) => - wallet.isCurrent ? null : _loadWallet(wallet), - childTrailingWidget: (item) { - return item.isCurrent - ? SizedBox.shrink() - : Padding( - padding: const EdgeInsets.only(right: 16), - child: EditWalletButtonWidget( - width: 44, - onTap: () => Navigator.of(context).pushNamed( - Routes.walletEdit, - arguments: WalletEditPageArguments( - walletListViewModel: widget.walletListViewModel, - editingWallet: item, - ), - ), - ), - ); - }, - ); - }, - ), - ), - ), - SizedBox(height: 24), - }, - if (widget.walletListViewModel.singleWalletsList.isNotEmpty) ...{ - Padding( - padding: const EdgeInsets.only(left: 24), - child: Text( - S.current.single_seed_wallets_group, - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w500, - color: Theme.of(context).extension()!.titleColor, - ), - ), - ), - SizedBox(height: 16), - Container( - child: Observer( - builder: (_) => FilteredList( - shrinkWrap: true, - list: widget.walletListViewModel.singleWalletsList, - updateFunction: widget.walletListViewModel.reorderAccordingToWalletList, - itemBuilder: (context, index) { - final wallet = widget.walletListViewModel.singleWalletsList[index]; - final currentColor = wallet.isCurrent - ? Theme.of(context) - .extension()! - .createNewWalletButtonBackgroundColor - : Theme.of(context).colorScheme.background; - - return GroupedWalletExpansionTile( - tileKey: ValueKey('single_wallets_expansion_tile_widget_$index'), - isCurrentlySelectedWallet: wallet.isCurrent, - leadingWidget: SizedBox( - width: wallet.isCurrent ? 56 : 40, - child: Row( - children: [ - wallet.isCurrent - ? Container( - height: 35, - width: 6, - margin: EdgeInsets.only(right: 16), - decoration: BoxDecoration( - borderRadius: BorderRadius.only( - topRight: Radius.circular(16), - bottomRight: Radius.circular(16), - ), - color: currentColor, - ), - ) - : SizedBox(width: 6), - Image.asset( - walletTypeToCryptoCurrency(wallet.type).iconPath!, - width: 32, - height: 32, - ), - ], - ), - ), - title: wallet.name, - isSelected: false, - borderRadius: BorderRadius.all(Radius.circular(16)), - margin: EdgeInsets.only(left: 20, right: 20, bottom: 12), - onTitleTapped: () => wallet.isCurrent ? null : _loadWallet(wallet), - trailingWidget: wallet.isCurrent - ? null - : EditWalletButtonWidget( - width: 44, - onTap: () { - Navigator.of(context).pushNamed( + ), + childWallets: group.wallets.map((walletInfo) { + return widget.walletListViewModel + .convertWalletInfoToWalletListItem( + walletInfo); + }).toList(), + isSelected: false, + onChildItemTapped: (wallet) => + wallet.isCurrent ? null : _loadWallet(wallet), + childTrailingWidget: (item) { + return item.isCurrent + ? SizedBox.shrink() + : Padding( + padding: const EdgeInsets.only(right: 16), + child: EditWalletButtonWidget( + width: 44, + onTap: () => + Navigator.of(context).pushNamed( Routes.walletEdit, arguments: WalletEditPageArguments( - walletListViewModel: widget.walletListViewModel, - editingWallet: wallet, + walletListViewModel: + widget.walletListViewModel, + editingWallet: item, ), - ); - }, - ), - ); - }, - ), + ), + ), + ); + }, + ); + }, ), ), - }, - ], - ), - ), - ), - Padding( - padding: const EdgeInsets.all(24), - child: Column( - children: [ - PrimaryImageButton( - key: ValueKey('wallet_list_page_restore_wallet_button_key'), - onPressed: () { - if (widget.walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets) { - widget.authService.authenticateAction( - context, - route: Routes.restoreOptions, - arguments: false, - conditionToDetermineIfToUse2FA: widget.walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets, - ); - } else { - Navigator.of(context).pushNamed(Routes.restoreOptions, arguments: false); - } - }, - image: restoreWalletImage, - text: S.of(context).wallet_list_restore_wallet, - color: Theme.of(context).cardColor, - textColor: Theme.of(context).extension()!.buttonTextColor, - ), - SizedBox(height: 10.0), - PrimaryImageButton( - key: ValueKey('wallet_list_page_create_new_wallet_button_key'), - onPressed: () { - //TODO(David): Find a way to optimize this - if (isSingleCoin) { - if (widget.walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets) { - widget.authService.authenticateAction( - context, - route: Routes.newWallet, - arguments: NewWalletArguments( - type: widget.walletListViewModel.currentWalletType, - ), - conditionToDetermineIfToUse2FA: widget.walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets, - ); - } else { - Navigator.of(context).pushNamed( - Routes.newWallet, - arguments: NewWalletArguments( - type: widget.walletListViewModel.currentWalletType, - ), - ); - } - } else { - if (widget.walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets) { - widget.authService.authenticateAction( - context, - route: Routes.newWalletType, - conditionToDetermineIfToUse2FA: widget.walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets, - ); - } else { - Navigator.of(context).pushNamed(Routes.newWalletType); - } - } - }, - image: newWalletImage, - text: S.of(context).wallet_list_create_new_wallet, - color: Theme.of(context).primaryColor, - textColor: Colors.white, - ), + ), + SizedBox(height: 24), + }, + if (widget + .walletListViewModel.singleWalletsList.isNotEmpty) ...{ + Padding( + padding: const EdgeInsets.only(left: 24), + child: Text( + S.current.single_seed_wallets_group, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w500, + color: Theme.of(context) + .extension()! + .titleColor, + ), + ), + ), + SizedBox(height: 16), + Container( + child: Observer( + builder: (_) => FilteredList( + shrinkWrap: true, + list: widget.walletListViewModel.singleWalletsList, + updateFunction: widget + .walletListViewModel.reorderAccordingToWalletList, + itemBuilder: (context, index) { + final wallet = widget + .walletListViewModel.singleWalletsList[index]; + final currentColor = wallet.isCurrent + ? Theme.of(context) + .extension()! + .createNewWalletButtonBackgroundColor + : Theme.of(context).colorScheme.background; + + return GroupedWalletExpansionTile( + tileKey: ValueKey( + 'single_wallets_expansion_tile_widget_$index'), + isCurrentlySelectedWallet: wallet.isCurrent, + leadingWidget: SizedBox( + width: wallet.isCurrent ? 56 : 40, + child: Row( + children: [ + wallet.isCurrent + ? Container( + height: 35, + width: 6, + margin: EdgeInsets.only(right: 16), + decoration: BoxDecoration( + borderRadius: BorderRadius.only( + topRight: Radius.circular(16), + bottomRight: Radius.circular(16), + ), + color: currentColor, + ), + ) + : SizedBox(width: 6), + Image.asset( + walletTypeToCryptoCurrency(wallet.type) + .iconPath!, + width: 32, + height: 32, + ), + ], + ), + ), + title: wallet.name, + isSelected: false, + borderRadius: BorderRadius.all(Radius.circular(16)), + margin: EdgeInsets.only( + left: 20, right: 20, bottom: 12), + onTitleTapped: () => + wallet.isCurrent ? null : _loadWallet(wallet), + trailingWidget: wallet.isCurrent + ? null + : EditWalletButtonWidget( + width: 44, + onTap: () { + Navigator.of(context).pushNamed( + Routes.walletEdit, + arguments: WalletEditPageArguments( + walletListViewModel: + widget.walletListViewModel, + editingWallet: wallet, + ), + ); + }, + ), + ); + }, + ), + ), + ), + SizedBox(height: 150), + }, ], ), ), + Positioned( + bottom: 0.0, + child: Container( + //padding: EdgeInsets.only(top: 100), + alignment: Alignment.bottomCenter, + height: 185, + //width: 600, + //padding: EdgeInsets.only(top: 50), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Theme.of(context).colorScheme.background.withAlpha(10), + Theme.of(context).colorScheme.background, + Theme.of(context).colorScheme.background, + Theme.of(context).colorScheme.background + ], + ), + ), + child: Container( + height: 120, + width: MediaQuery.of(context).size.width, + //alignment: Alignment.bottomCenter, + margin: EdgeInsets.only(bottom: 24), + padding: EdgeInsets.only(left: 16, right: 16), + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + PrimaryImageButton( + key: ValueKey( + 'wallet_list_page_restore_wallet_button_key'), + onPressed: () { + if (widget.walletListViewModel + .shouldRequireTOTP2FAForCreatingNewWallets) { + widget.authService.authenticateAction( + context, + route: Routes.restoreOptions, + arguments: false, + conditionToDetermineIfToUse2FA: widget + .walletListViewModel + .shouldRequireTOTP2FAForCreatingNewWallets, + ); + } else { + Navigator.of(context).pushNamed(Routes.restoreOptions, + arguments: false); + } + }, + image: restoreWalletImage, + text: S.of(context).wallet_list_restore_wallet, + color: Theme.of(context).cardColor, + textColor: Theme.of(context) + .extension()! + .buttonTextColor, + ), + SizedBox(height: 10.0), + PrimaryImageButton( + key: ValueKey( + 'wallet_list_page_create_new_wallet_button_key'), + onPressed: () { + //TODO(David): Find a way to optimize this + if (isSingleCoin) { + if (widget.walletListViewModel + .shouldRequireTOTP2FAForCreatingNewWallets) { + widget.authService.authenticateAction( + context, + route: Routes.newWallet, + arguments: NewWalletArguments( + type: widget + .walletListViewModel.currentWalletType, + ), + conditionToDetermineIfToUse2FA: widget + .walletListViewModel + .shouldRequireTOTP2FAForCreatingNewWallets, + ); + } else { + Navigator.of(context).pushNamed( + Routes.newWallet, + arguments: NewWalletArguments( + type: widget + .walletListViewModel.currentWalletType, + ), + ); + } + } else { + if (widget.walletListViewModel + .shouldRequireTOTP2FAForCreatingNewWallets) { + widget.authService.authenticateAction( + context, + route: Routes.newWalletType, + conditionToDetermineIfToUse2FA: widget + .walletListViewModel + .shouldRequireTOTP2FAForCreatingNewWallets, + ); + } else { + Navigator.of(context) + .pushNamed(Routes.newWalletType); + } + } + }, + image: newWalletImage, + text: S.of(context).wallet_list_create_new_wallet, + color: Theme.of(context).primaryColor, + textColor: Colors.white, + ), + ], + ), + ), + ), + ), ], ), + ); } @@ -405,7 +483,8 @@ class WalletListBodyState extends State { if (SettingsStoreBase.walletPasswordDirectInput) { Navigator.of(context).pushNamed(Routes.walletUnlockLoadable, arguments: WalletUnlockArguments( - callback: (bool isAuthenticatedSuccessfully, AuthPageState auth) async { + callback: + (bool isAuthenticatedSuccessfully, AuthPageState auth) async { if (isAuthenticatedSuccessfully) { auth.close(); setState(() {}); @@ -425,23 +504,28 @@ class WalletListBodyState extends State { final requireHardwareWalletConnection = widget.walletListViewModel .requireHardwareWalletConnection(wallet); if (requireHardwareWalletConnection) { + bool didConnect = false; await Navigator.of(context).pushNamed( Routes.connectDevices, arguments: ConnectDevicePageParams( walletType: WalletType.monero, onConnectDevice: (context, ledgerVM) async { monero!.setGlobalLedgerConnection(ledgerVM.connection); + didConnect = true; Navigator.of(context).pop(); }, ), ); + if (!didConnect) return; + 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, + alertBarrierDismissible: false, buttonAction: () => Navigator.of(context).pop()), ); } diff --git a/lib/src/screens/welcome/create_pin_welcome_page.dart b/lib/src/screens/welcome/create_pin_welcome_page.dart index d8ff1578e..ddf3b8d5b 100644 --- a/lib/src/screens/welcome/create_pin_welcome_page.dart +++ b/lib/src/screens/welcome/create_pin_welcome_page.dart @@ -2,6 +2,7 @@ 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/theme_base.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; @@ -12,6 +13,10 @@ import 'package:cake_wallet/themes/extensions/new_wallet_theme.dart'; import 'package:cake_wallet/themes/extensions/wallet_list_theme.dart'; class CreatePinWelcomePage extends BasePage { + CreatePinWelcomePage(this.isWalletPasswordDirectInput); + + final bool isWalletPasswordDirectInput; + static const aspectRatioImage = 1.25; final welcomeImageLight = Image.asset('assets/images/welcome_light.png'); final welcomeImageDark = Image.asset('assets/images/welcome.png'); @@ -64,7 +69,7 @@ class CreatePinWelcomePage extends BasePage { alignment: Alignment.center, child: ConstrainedBox( constraints: - BoxConstraints(maxWidth: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint), + BoxConstraints(maxWidth: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint), child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -118,19 +123,44 @@ class CreatePinWelcomePage extends BasePage { ), ), ), - bottomSection: Padding( - padding: EdgeInsets.only(top: 24), - child: PrimaryImageButton( - key: ValueKey('create_pin_welcome_page_create_a_pin_button_key'), - onPressed: () => Navigator.pushNamed(context, Routes.welcomeWallet), - image: newWalletImage, - text: S.current.set_a_pin, - color: Theme.of(context) - .extension()! - .createNewWalletButtonBackgroundColor, - textColor: - Theme.of(context).extension()!.restoreWalletButtonTextColor, - ), + bottomSection: Column( + children: [ + RichText( + textAlign: TextAlign.center, + text: TextSpan( + style: TextStyle( + fontSize: 14, + color: Theme.of(context).extension()!.hintTextColor, + ), + children: [ + TextSpan(text: 'By continuing, you agree to our '), + TextSpan( + text: 'Terms of Service', + style: TextStyle( + color: Theme.of(context).primaryColor, + decoration: TextDecoration.underline, + ), + recognizer: TapGestureRecognizer() + ..onTap = () => Navigator.pushNamed(context, Routes.readDisclaimer), + ), + ], + ), + ), + Padding( + padding: EdgeInsets.only(top: 24), + child: PrimaryImageButton( + key: ValueKey('create_pin_welcome_page_create_a_pin_button_key'), + onPressed: () => Navigator.pushNamed(context, Routes.welcomeWallet), + image: newWalletImage, + text: isWalletPasswordDirectInput ? S.current.set_up_a_wallet : S.current.set_a_pin, + color: Theme.of(context) + .extension()! + .createNewWalletButtonBackgroundColor, + textColor: + Theme.of(context).extension()!.restoreWalletButtonTextColor, + ), + ), + ], ), ), ); diff --git a/lib/src/widgets/address_text_field.dart b/lib/src/widgets/address_text_field.dart index 9b407dedb..1f30e648b 100644 --- a/lib/src/widgets/address_text_field.dart +++ b/lib/src/widgets/address_text_field.dart @@ -233,6 +233,7 @@ class AddressTextField extends StatelessWidget{ await PermissionHandler.checkPermission(Permission.camera, context); if (!isCameraPermissionGranted) return; final code = await presentQRScanner(context); + if (code == null) return; if (code.isEmpty) { return; } diff --git a/lib/src/widgets/dashboard_card_widget.dart b/lib/src/widgets/dashboard_card_widget.dart index dc223eb02..6fc660592 100644 --- a/lib/src/widgets/dashboard_card_widget.dart +++ b/lib/src/widgets/dashboard_card_widget.dart @@ -6,7 +6,7 @@ import 'package:flutter_svg/flutter_svg.dart'; class DashBoardRoundedCardWidget extends StatelessWidget { DashBoardRoundedCardWidget({ - required this.onTap, + this.onTap, required this.title, required this.subTitle, this.hint, @@ -15,10 +15,14 @@ class DashBoardRoundedCardWidget extends StatelessWidget { this.icon, this.onClose, this.customBorder, + this.shadowSpread, + this.shadowBlur, super.key, + this.marginV, + this.marginH, }); - final VoidCallback onTap; + final VoidCallback? onTap; final VoidCallback? onClose; final String title; final String subTitle; @@ -27,26 +31,43 @@ class DashBoardRoundedCardWidget extends StatelessWidget { final Widget? icon; final Image? image; final double? customBorder; + final double? marginV; + final double? marginH; + final double? shadowSpread; + final double? shadowBlur; @override Widget build(BuildContext context) { - return InkWell( - onTap: onTap, - hoverColor: Colors.transparent, - splashColor: Colors.transparent, - highlightColor: Colors.transparent, - child: Stack( - children: [ - Container( - padding: EdgeInsets.all(20), - width: double.infinity, - decoration: BoxDecoration( - color: Theme.of(context).extension()!.syncedBackgroundColor, - borderRadius: BorderRadius.circular(customBorder ?? 20), - border: Border.all( - color: Theme.of(context).extension()!.cardBorderColor, - ), - ), + return Stack( + children: [ + Container( + margin: EdgeInsets.symmetric(horizontal: marginH ?? 20, vertical: marginV ?? 8), + //padding: EdgeInsets.all(20), + width: double.infinity, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(customBorder ?? 20), + // border: Border.all( + // color: Theme.of(context).extension()!.cardBorderColor, + // width: 1 + // ), + // boxShadow: [ + // BoxShadow( + // color: Theme.of(context).extension()!.cardBorderColor + // .withAlpha(50), + // spreadRadius: shadowSpread ?? 3, + // blurRadius: shadowBlur ?? 7, + // ) + // ], + ), + child: OutlinedButton( + onPressed: onTap, + style: OutlinedButton.styleFrom( + side: BorderSide(width: 1, color: Theme.of(context).extension()!.cardBorderColor), + backgroundColor: + Theme.of(context).extension()!.syncedBackgroundColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(customBorder ?? 20)), + padding: EdgeInsets.all(24)), child: Column( children: [ Row( @@ -59,8 +80,9 @@ class DashBoardRoundedCardWidget extends StatelessWidget { Text( title, style: TextStyle( - color: - Theme.of(context).extension()!.cardTextColor, + color: Theme.of(context) + .extension()! + .cardTextColor, fontSize: 24, fontWeight: FontWeight.w900, ), @@ -80,8 +102,7 @@ class DashBoardRoundedCardWidget extends StatelessWidget { ], ), ), - if (image != null) image! - else if (svgPicture != null) svgPicture!, + if (image != null) image! else if (svgPicture != null) svgPicture!, if (icon != null) icon! ], ), @@ -92,18 +113,18 @@ class DashBoardRoundedCardWidget extends StatelessWidget { ], ), ), - if (onClose != null) - Positioned( - top: 10, - right: 10, - child: IconButton( - icon: Icon(Icons.close), - onPressed: onClose, - color: Theme.of(context).extension()!.cardTextColor, - ), + ), + if (onClose != null) + Positioned( + top: 10, + right: 10, + child: IconButton( + icon: Icon(Icons.close), + onPressed: onClose, + color: Theme.of(context).extension()!.cardTextColor, ), - ], - ), + ), + ], ); } } diff --git a/lib/src/widgets/option_tile.dart b/lib/src/widgets/option_tile.dart index c2d8b9506..c26987658 100644 --- a/lib/src/widgets/option_tile.dart +++ b/lib/src/widgets/option_tile.dart @@ -19,16 +19,17 @@ class OptionTile extends StatelessWidget { @override Widget build(BuildContext context) { - return GestureDetector( - onTap: onPressed, - child: Container( - width: double.infinity, - padding: EdgeInsets.all(24), - alignment: Alignment.center, - decoration: BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(12)), - color: Theme.of(context).cardColor, + return Container( + width: double.infinity, + padding: EdgeInsets.only(left: 6, right: 6), + alignment: Alignment.center, + child: TextButton( + style: TextButton.styleFrom( + backgroundColor: Theme.of(context).cardColor, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + padding: EdgeInsets.all(24) ), + onPressed: onPressed, child: Row( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.center, @@ -48,7 +49,9 @@ class OptionTile extends StatelessWidget { style: TextStyle( fontSize: 20, fontWeight: FontWeight.w500, - color: Theme.of(context).extension()!.titleColor, + color: Theme.of(context) + .extension()! + .titleColor, ), ), Padding( @@ -58,7 +61,9 @@ class OptionTile extends StatelessWidget { style: TextStyle( fontSize: 14, fontWeight: FontWeight.normal, - color: Theme.of(context).extension()!.descriptionColor, + color: Theme.of(context) + .extension()! + .descriptionColor, ), ), ) diff --git a/lib/src/widgets/primary_button.dart b/lib/src/widgets/primary_button.dart index 06bfda157..d5800aa5b 100644 --- a/lib/src/widgets/primary_button.dart +++ b/lib/src/widgets/primary_button.dart @@ -91,7 +91,10 @@ class LoadingPrimaryButton extends StatelessWidget { width: double.infinity, height: 52.0, child: TextButton( - onPressed: (isLoading || isDisabled) ? null : onPressed, + onPressed: (isLoading || isDisabled) ? null : () { + FocusScope.of(context).unfocus(); + onPressed.call(); + }, style: ButtonStyle( backgroundColor: MaterialStateProperty.all(isDisabled ? color.withOpacity(0.5) : color), diff --git a/lib/src/widgets/seedphrase_grid_widget.dart b/lib/src/widgets/seedphrase_grid_widget.dart new file mode 100644 index 000000000..4f537c064 --- /dev/null +++ b/lib/src/widgets/seedphrase_grid_widget.dart @@ -0,0 +1,68 @@ +import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; +import 'package:flutter/material.dart'; + +class SeedPhraseGridWidget extends StatelessWidget { + const SeedPhraseGridWidget({ + super.key, + required this.list, + }); + + final List list; + + @override + Widget build(BuildContext context) { + return GridView.builder( + itemCount: list.length, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + childAspectRatio: 2.8, + mainAxisSpacing: 8.0, + crossAxisSpacing: 8.0, + ), + itemBuilder: (context, index) { + final item = list[index]; + final numberCount = index + 1; + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8), + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: Theme.of(context).cardColor, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + child: Text( + //maxLines: 1, + numberCount.toString(), + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + height: 1, + fontWeight: FontWeight.w800, + color: Theme.of(context) + .extension()! + .buttonTextColor + .withOpacity(0.5)), + ), + ), + const SizedBox(width: 6), + Expanded( + child: Text( + '${item[0].toLowerCase()}${item.substring(1)}', + style: TextStyle( + fontSize: 14, + height: 0.8, + fontWeight: FontWeight.w700, + color: Theme.of(context).extension()!.buttonTextColor), + ), + ), + ], + ), + ); + }, + ); + } +} diff --git a/lib/src/widgets/setting_action_button.dart b/lib/src/widgets/setting_action_button.dart index bebc4b8e1..4fd7260f0 100644 --- a/lib/src/widgets/setting_action_button.dart +++ b/lib/src/widgets/setting_action_button.dart @@ -1,6 +1,10 @@ import 'package:cake_wallet/palette.dart'; import 'package:cake_wallet/themes/extensions/menu_theme.dart'; import 'package:flutter/material.dart'; +import 'package:cake_wallet/themes/extensions/sync_indicator_theme.dart'; +import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart'; +import 'package:cake_wallet/themes/extensions/balance_page_theme.dart'; +import 'package:cake_wallet/themes/extensions/option_tile_theme.dart'; class SettingActionButton extends StatelessWidget { final bool isLastTile; @@ -29,56 +33,64 @@ class SettingActionButton extends StatelessWidget { @override Widget build(BuildContext context) { + final isLightMode = Theme.of(context).extension()?.useDarkImage ?? false; Color? color = isSelected ? Theme.of(context).extension()!.settingTitleColor : selectionActive - ? Palette.darkBlue - : Theme.of(context).extension()!.settingTitleColor; - return InkWell( - onTap: onTap, - hoverColor: Colors.transparent, - child: Container( - height: tileHeight, - padding: isLastTile - ? EdgeInsets.only( - left: 24, - right: 24, - top: fromBottomEdge, - ) - : EdgeInsets.only(left: 24, right: 24), - alignment: isLastTile ? Alignment.topLeft : null, - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Image.asset( - image, - height: 16, - width: 16, - color: Theme.of(context) - .extension()! - .settingActionsIconColor, + ? Palette.darkBlue + : Theme.of(context).extension()!.settingTitleColor; + return Container( + //padding: EdgeInsets.only(top: 5, left: 15, bottom: 5), + margin: EdgeInsets.only(top: 10, left: 20, bottom: 0, right: 20), + child: TextButton( + style: ButtonStyle( + backgroundColor: MaterialStateProperty.all(isLightMode ? Theme.of(context).cardColor : Colors.black12), + shape: MaterialStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), ), - SizedBox(width: 16), - Expanded( - child: Text( - title, - style: TextStyle( - color: color, - fontSize: 16, - fontWeight: FontWeight.bold, + ), + ), + onPressed: onTap, + //hoverColor: Colors.transparent, + child: Container( + width: double.infinity, + padding: EdgeInsets.only(top: 12, left: 20, bottom: 12, right: 15), + //margin: EdgeInsets.only(top: 5, left: 15, bottom: 5), + alignment: isLastTile ? Alignment.topLeft : null, + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Image.asset( + image, + height: 16, + width: 16, + color: Theme.of(context) + .extension()! + .settingActionsIconColor, + ), + SizedBox(width: 16), + Expanded( + child: Text( + title, + style: TextStyle( + color: color, + fontSize: 16, + fontWeight: FontWeight.bold, + ), ), ), - ), - if (isArrowVisible) - Icon( - Icons.arrow_forward_ios, - color: color, - size: 16, - ) - ], + if(isArrowVisible) + Icon( + Icons.arrow_forward_ios, + color: Colors.grey, + size: 16, + ) + ], + ), ), ), ); } -} +} \ No newline at end of file diff --git a/lib/src/widgets/setting_actions.dart b/lib/src/widgets/setting_actions.dart index da9f26bc6..d383129cc 100644 --- a/lib/src/widgets/setting_actions.dart +++ b/lib/src/widgets/setting_actions.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/routes.dart'; import 'package:flutter/material.dart'; @@ -20,20 +22,8 @@ class SettingActions { walletSettingAction, addressBookSettingAction, silentPaymentsSettingAction, - litecoinMwebSettingAction, - exportOutputsAction, - securityBackupSettingAction, - privacySettingAction, - displaySettingAction, - otherSettingAction, - supportSettingAction, - ]; - - static List desktopSettings = [ - connectionSettingAction, - walletSettingAction, - addressBookSettingAction, - silentPaymentsSettingAction, + if (Platform.isIOS || Platform.isAndroid) litecoinMwebSettingAction, + if (Platform.isIOS || Platform.isAndroid) exportOutputsAction, securityBackupSettingAction, privacySettingAction, displaySettingAction, @@ -46,7 +36,6 @@ class SettingActions { name: (context) => S.of(context).silent_payments_settings, image: 'assets/images/bitcoin_menu.png', onTap: (BuildContext context) { - Navigator.pop(context); Navigator.of(context).pushNamed(Routes.silentPaymentsSettings); }, ); @@ -56,7 +45,6 @@ class SettingActions { 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'); }, ); @@ -66,7 +54,6 @@ class SettingActions { name: (context) => S.of(context).litecoin_mweb_settings, image: 'assets/images/litecoin_menu.png', onTap: (BuildContext context) { - Navigator.pop(context); Navigator.of(context).pushNamed(Routes.mwebSettings); }, ); @@ -76,7 +63,6 @@ class SettingActions { name: (context) => S.of(context).connection_sync, image: 'assets/images/nodes_menu.png', onTap: (BuildContext context) { - Navigator.pop(context); Navigator.of(context).pushNamed(Routes.connectionSync); }, ); @@ -96,7 +82,6 @@ class SettingActions { name: (context) => S.of(context).address_book_menu, image: 'assets/images/open_book_menu.png', onTap: (BuildContext context) { - Navigator.pop(context); Navigator.of(context).pushNamed(Routes.addressBook); }, ); @@ -106,7 +91,6 @@ class SettingActions { name: (context) => S.of(context).security_and_backup, image: 'assets/images/key_menu.png', onTap: (BuildContext context) { - Navigator.pop(context); Navigator.of(context).pushNamed(Routes.securityBackupPage); }, ); @@ -116,7 +100,6 @@ class SettingActions { name: (context) => S.of(context).privacy, image: 'assets/images/privacy_menu.png', onTap: (BuildContext context) { - Navigator.pop(context); Navigator.of(context).pushNamed(Routes.privacyPage); }, ); @@ -126,7 +109,6 @@ class SettingActions { name: (context) => S.of(context).display_settings, image: 'assets/images/eye_menu.png', onTap: (BuildContext context) { - Navigator.pop(context); Navigator.of(context).pushNamed(Routes.displaySettingsPage); }, ); @@ -136,7 +118,6 @@ class SettingActions { name: (context) => S.of(context).other_settings, image: 'assets/images/settings_menu.png', onTap: (BuildContext context) { - Navigator.pop(context); Navigator.of(context).pushNamed(Routes.otherSettingsPage); }, ); @@ -146,7 +127,6 @@ class SettingActions { name: (context) => S.of(context).settings_support, image: 'assets/images/question_mark.png', onTap: (BuildContext context) { - Navigator.pop(context); Navigator.of(context).pushNamed(Routes.support); }, ); diff --git a/lib/src/widgets/standard_list.dart b/lib/src/widgets/standard_list.dart index 0780d64cd..c845301cc 100644 --- a/lib/src/widgets/standard_list.dart +++ b/lib/src/widgets/standard_list.dart @@ -21,16 +21,20 @@ class StandardListRow extends StatelessWidget { Widget build(BuildContext context) { final leading = buildLeading(context); final trailing = buildTrailing(context); - - return InkWell( - onTap: () => onTap?.call(context), - child: Container( + return Container( height: 56, - padding: EdgeInsets.only(left: 24, right: 24), - decoration: decoration ?? - BoxDecoration( - color: Theme.of(context).colorScheme.background, + padding: EdgeInsets.only(left: 12, right: 12), + child: TextButton( + onPressed: () => onTap?.call(context), + style: ButtonStyle( + //backgroundColor: MaterialStateProperty.all(Theme.of(context).cardColor), + shape: MaterialStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(10) + ), ), + ), + ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -97,10 +101,10 @@ class StandardListSeparator extends StatelessWidget { return Container( height: height, padding: padding, - color: Theme.of(context).colorScheme.background, + color: Colors.transparent, child: Container( height: height, - color: Theme.of(context).extension()!.textfieldUnderlineColor, + color: Colors.transparent, ), ); } @@ -140,6 +144,7 @@ class SectionStandardList extends StatelessWidget { final int sectionCount; final bool hasTopSeparator; + final int Function(int sectionIndex) itemCounter; final Widget Function(int sectionIndex, int itemIndex) itemBuilder; final Widget Function(int sectionIndex)? sectionTitleBuilder; diff --git a/lib/src/widgets/text_info_box.dart b/lib/src/widgets/text_info_box.dart new file mode 100644 index 000000000..8a4149c55 --- /dev/null +++ b/lib/src/widgets/text_info_box.dart @@ -0,0 +1,51 @@ +import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; +import 'package:flutter/material.dart'; + +class TextInfoBox extends StatelessWidget { + const TextInfoBox({Key? key, required this.title, required this.value, this.onCopy}) + : super(key: key); + + final String title; + final String value; + final void Function(BuildContext context)? onCopy; + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: Theme.of(context).cardColor, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(title, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Theme.of(context) + .extension()! + .buttonTextColor + .withOpacity(0.5))), + GestureDetector( + onTap: onCopy != null ? () => onCopy!(context) : null, + child: Icon(Icons.copy, + size: 13, + color: Theme.of(context).textTheme.bodyLarge?.color?.withOpacity(0.7))), + ], + ), + const SizedBox(height: 4), + Text(value, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Theme.of(context).extension()!.buttonTextColor)), + ], + ), + ); + } +} diff --git a/lib/src/widgets/warning_box_widget.dart b/lib/src/widgets/warning_box_widget.dart new file mode 100644 index 000000000..02d688956 --- /dev/null +++ b/lib/src/widgets/warning_box_widget.dart @@ -0,0 +1,51 @@ +import 'package:cake_wallet/themes/theme_base.dart'; +import 'package:flutter/material.dart'; + +class WarningBox extends StatelessWidget { + const WarningBox({required this.content, required this.currentTheme, Key? key}) + : super(key: key); + + final String content; + final ThemeBase currentTheme; + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: currentTheme.type == ThemeType.dark + ? Color.fromRGBO(132, 110, 64, 1) + : Color.fromRGBO(194, 165, 94, 1), + borderRadius: BorderRadius.all(Radius.circular(12)), + border: Border.all( + color: currentTheme.type == ThemeType.dark + ? Color.fromRGBO(177, 147, 41, 1) + : Color.fromRGBO(125, 122, 15, 1), + width: 2.0, + )), + child: Row( + children: [ + Icon( + Icons.warning_amber_rounded, + size: 64, + color: Colors.white.withOpacity(0.75), + ), + SizedBox(width: 6), + Expanded( + child: Text( + content, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w800, + color: currentTheme.type == ThemeType.dark + ? Colors.white.withOpacity(0.75) + : Colors.white.withOpacity(0.85), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/store/dashboard/trade_filter_store.dart b/lib/store/dashboard/trade_filter_store.dart index c1e462cd6..a2c6e3646 100644 --- a/lib/store/dashboard/trade_filter_store.dart +++ b/lib/store/dashboard/trade_filter_store.dart @@ -16,6 +16,7 @@ abstract class TradeFilterStoreBase with Store { displaySimpleSwap = true, displayTrocador = true, displayExolix = true, + displayChainflip = true, displayThorChain = true, displayLetsExchange = true, displayStealthEx = true; @@ -41,6 +42,9 @@ abstract class TradeFilterStoreBase with Store { @observable bool displayExolix; + @observable + bool displayChainflip; + @observable bool displayThorChain; @@ -56,7 +60,8 @@ abstract class TradeFilterStoreBase with Store { displaySideShift && displaySimpleSwap && displayTrocador && - displayExolix && + displayExolix && + displayChainflip && displayThorChain && displayLetsExchange && displayStealthEx; @@ -85,11 +90,15 @@ abstract class TradeFilterStoreBase with Store { case ExchangeProviderDescription.exolix: displayExolix = !displayExolix; break; + case ExchangeProviderDescription.chainflip: + displayChainflip = !displayChainflip; + break; case ExchangeProviderDescription.thorChain: displayThorChain = !displayThorChain; break; case ExchangeProviderDescription.letsExchange: displayLetsExchange = !displayLetsExchange; + break; case ExchangeProviderDescription.stealthEx: displayStealthEx = !displayStealthEx; break; @@ -102,6 +111,7 @@ abstract class TradeFilterStoreBase with Store { displaySimpleSwap = false; displayTrocador = false; displayExolix = false; + displayChainflip = false; displayThorChain = false; displayLetsExchange = false; displayStealthEx = false; @@ -113,6 +123,7 @@ abstract class TradeFilterStoreBase with Store { displaySimpleSwap = true; displayTrocador = true; displayExolix = true; + displayChainflip = true; displayThorChain = true; displayLetsExchange = true; displayStealthEx = true; @@ -141,6 +152,8 @@ abstract class TradeFilterStoreBase with Store { item.trade.provider == ExchangeProviderDescription.simpleSwap) || (displayTrocador && item.trade.provider == ExchangeProviderDescription.trocador) || (displayExolix && item.trade.provider == ExchangeProviderDescription.exolix) || + (displayChainflip && + item.trade.provider == ExchangeProviderDescription.chainflip) || (displayThorChain && item.trade.provider == ExchangeProviderDescription.thorChain) || (displayLetsExchange && diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index 866cb0067..090f4e2b6 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -23,6 +23,10 @@ import 'package:cake_wallet/entities/seed_type.dart'; import 'package:cake_wallet/entities/sort_balance_types.dart'; import 'package:cake_wallet/entities/wallet_list_order_types.dart'; import 'package:cake_wallet/ethereum/ethereum.dart'; +import 'package:cake_wallet/wallet_type_utils.dart'; +import 'package:cake_wallet/wownero/wownero.dart'; +import 'package:cake_wallet/zano/zano.dart'; +import 'package:cw_core/transaction_priority.dart'; import 'package:cake_wallet/exchange/provider/trocador_exchange_provider.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/polygon/polygon.dart'; @@ -114,6 +118,7 @@ abstract class SettingsStoreBase with Store { required this.defaultNanoRep, required this.defaultBananoRep, required this.lookupsTwitter, + required this.lookupsZanoAlias, required this.lookupsMastodon, required this.lookupsYatService, required this.lookupsUnstoppableDomains, @@ -136,6 +141,7 @@ abstract class SettingsStoreBase with Store { TransactionPriority? initialEthereumTransactionPriority, TransactionPriority? initialPolygonTransactionPriority, TransactionPriority? initialBitcoinCashTransactionPriority, + TransactionPriority? initialZanoTransactionPriority, Country? initialCakePayCountry}) : nodes = ObservableMap.of(nodes), powNodes = ObservableMap.of(powNodes), @@ -223,6 +229,9 @@ abstract class SettingsStoreBase with Store { priority[WalletType.bitcoinCash] = initialBitcoinCashTransactionPriority; } + if (initialZanoTransactionPriority != null) { + priority[WalletType.zano] = initialZanoTransactionPriority; + } if (initialCakePayCountry != null) { selectedCakePayCountry = initialCakePayCountry; } @@ -276,6 +285,9 @@ abstract class SettingsStoreBase with Store { case WalletType.polygon: key = PreferencesKey.polygonTransactionPriority; break; + case WalletType.zano: + key = PreferencesKey.zanoTransactionPriority; + break; default: key = null; } @@ -462,6 +474,10 @@ abstract class SettingsStoreBase with Store { (_) => lookupsTwitter, (bool looksUpTwitter) => _sharedPreferences.setBool(PreferencesKey.lookupsTwitter, looksUpTwitter)); + reaction( + (_) => lookupsZanoAlias, + (bool lookupsZanoAlias) => + _sharedPreferences.setBool(PreferencesKey.lookupsZanoAlias, lookupsZanoAlias)); reaction( (_) => lookupsMastodon, @@ -789,6 +805,9 @@ abstract class SettingsStoreBase with Store { @observable bool lookupsTwitter; + @observable + bool lookupsZanoAlias; + @observable bool lookupsMastodon; @@ -916,6 +935,7 @@ abstract class SettingsStoreBase with Store { TransactionPriority? polygonTransactionPriority; TransactionPriority? bitcoinCashTransactionPriority; TransactionPriority? wowneroTransactionPriority; + TransactionPriority? zanoTransactionPriority; if (sharedPreferences.getInt(PreferencesKey.havenTransactionPriority) != null) { havenTransactionPriority = monero?.deserializeMoneroTransactionPriority( @@ -941,6 +961,10 @@ abstract class SettingsStoreBase with Store { wowneroTransactionPriority = wownero?.deserializeWowneroTransactionPriority( raw: sharedPreferences.getInt(PreferencesKey.wowneroTransactionPriority)!); } + if (sharedPreferences.getInt(PreferencesKey.zanoTransactionPriority) != null) { + zanoTransactionPriority = monero?.deserializeMoneroTransactionPriority( + raw: sharedPreferences.getInt(PreferencesKey.zanoTransactionPriority)!); + } moneroTransactionPriority ??= monero?.getDefaultTransactionPriority(); bitcoinTransactionPriority ??= bitcoin?.getMediumTransactionPriority(); @@ -950,6 +974,7 @@ abstract class SettingsStoreBase with Store { bitcoinCashTransactionPriority ??= bitcoinCash?.getDefaultTransactionPriority(); wowneroTransactionPriority ??= wownero?.getDefaultTransactionPriority(); polygonTransactionPriority ??= polygon?.getDefaultTransactionPriority(); + zanoTransactionPriority ??= zano?.getDefaultTransactionPriority(); final currentBalanceDisplayMode = BalanceDisplayMode.deserialize( raw: sharedPreferences.getInt(PreferencesKey.currentBalanceDisplayModeKey)!); @@ -1007,6 +1032,7 @@ abstract class SettingsStoreBase with Store { final defaultNanoRep = sharedPreferences.getString(PreferencesKey.defaultNanoRep) ?? ""; final defaultBananoRep = sharedPreferences.getString(PreferencesKey.defaultBananoRep) ?? ""; final lookupsTwitter = sharedPreferences.getBool(PreferencesKey.lookupsTwitter) ?? true; + final lookupsZanoAlias = sharedPreferences.getBool(PreferencesKey.lookupsZanoAlias) ?? true; final lookupsMastodon = sharedPreferences.getBool(PreferencesKey.lookupsMastodon) ?? true; final lookupsYatService = sharedPreferences.getBool(PreferencesKey.lookupsYatService) ?? true; final lookupsUnstoppableDomains = @@ -1049,6 +1075,8 @@ abstract class SettingsStoreBase with Store { final solanaNodeId = sharedPreferences.getInt(PreferencesKey.currentSolanaNodeIdKey); final tronNodeId = sharedPreferences.getInt(PreferencesKey.currentTronNodeIdKey); final wowneroNodeId = sharedPreferences.getInt(PreferencesKey.currentWowneroNodeIdKey); + final zanoNodeId = sharedPreferences.getInt(PreferencesKey.currentZanoNodeIdKey); + final moneroNode = nodeSource.get(nodeId); final bitcoinElectrumServer = nodeSource.get(bitcoinElectrumServerId); final litecoinElectrumServer = nodeSource.get(litecoinElectrumServerId); @@ -1061,6 +1089,7 @@ abstract class SettingsStoreBase with Store { final solanaNode = nodeSource.get(solanaNodeId); final tronNode = nodeSource.get(tronNodeId); final wowneroNode = nodeSource.get(wowneroNodeId); + final zanoNode = nodeSource.get(zanoNodeId); final packageInfo = await PackageInfo.fromPlatform(); final deviceName = await _getDeviceName() ?? ''; final shouldShowYatPopup = sharedPreferences.getBool(PreferencesKey.shouldShowYatPopup) ?? true; @@ -1142,6 +1171,10 @@ abstract class SettingsStoreBase with Store { nodes[WalletType.wownero] = wowneroNode; } + if (zanoNode != null) { + nodes[WalletType.zano] = zanoNode; + } + final savedSyncMode = SyncMode.all.firstWhere((element) { return element.type.index == (sharedPreferences.getInt(PreferencesKey.syncModeKey) ?? SyncType.sixHours.index); }); @@ -1291,6 +1324,7 @@ abstract class SettingsStoreBase with Store { defaultNanoRep: defaultNanoRep, defaultBananoRep: defaultBananoRep, lookupsTwitter: lookupsTwitter, + lookupsZanoAlias: lookupsZanoAlias, lookupsMastodon: lookupsMastodon, lookupsYatService: lookupsYatService, lookupsUnstoppableDomains: lookupsUnstoppableDomains, @@ -1307,6 +1341,7 @@ abstract class SettingsStoreBase with Store { hasEnabledMwebBefore: hasEnabledMwebBefore, initialMoneroTransactionPriority: moneroTransactionPriority, initialWowneroTransactionPriority: wowneroTransactionPriority, + initialZanoTransactionPriority: zanoTransactionPriority, initialBitcoinTransactionPriority: bitcoinTransactionPriority, initialHavenTransactionPriority: havenTransactionPriority, initialLitecoinTransactionPriority: litecoinTransactionPriority, @@ -1385,6 +1420,10 @@ abstract class SettingsStoreBase with Store { priority[WalletType.bitcoinCash] = bitcoinCash!.deserializeBitcoinCashTransactionPriority( sharedPreferences.getInt(PreferencesKey.bitcoinCashTransactionPriority)!); } + if (zano != null && sharedPreferences.getInt(PreferencesKey.zanoTransactionPriority) != null) { + priority[WalletType.zano] = zano!.deserializeMoneroTransactionPriority( + raw: sharedPreferences.getInt(PreferencesKey.zanoTransactionPriority)!); + } final generateSubaddresses = sharedPreferences.getInt(PreferencesKey.autoGenerateSubaddressStatusKey); @@ -1464,6 +1503,7 @@ abstract class SettingsStoreBase with Store { defaultNanoRep = sharedPreferences.getString(PreferencesKey.defaultNanoRep) ?? ""; defaultBananoRep = sharedPreferences.getString(PreferencesKey.defaultBananoRep) ?? ""; lookupsTwitter = sharedPreferences.getBool(PreferencesKey.lookupsTwitter) ?? true; + lookupsZanoAlias = sharedPreferences.getBool(PreferencesKey.lookupsZanoAlias) ?? true; lookupsMastodon = sharedPreferences.getBool(PreferencesKey.lookupsMastodon) ?? true; lookupsYatService = sharedPreferences.getBool(PreferencesKey.lookupsYatService) ?? true; lookupsUnstoppableDomains = @@ -1494,6 +1534,7 @@ abstract class SettingsStoreBase with Store { final solanaNodeId = sharedPreferences.getInt(PreferencesKey.currentSolanaNodeIdKey); final tronNodeId = sharedPreferences.getInt(PreferencesKey.currentTronNodeIdKey); final wowneroNodeId = sharedPreferences.getInt(PreferencesKey.currentWowneroNodeIdKey); + final zanoNodeId = sharedPreferences.getInt(PreferencesKey.currentZanoNodeIdKey); final moneroNode = nodeSource.get(nodeId); final bitcoinElectrumServer = nodeSource.get(bitcoinElectrumServerId); final litecoinElectrumServer = nodeSource.get(litecoinElectrumServerId); @@ -1501,10 +1542,12 @@ abstract class SettingsStoreBase with Store { final ethereumNode = nodeSource.get(ethereumNodeId); final polygonNode = nodeSource.get(polygonNodeId); final bitcoinCashNode = nodeSource.get(bitcoinCashElectrumServerId); - final nanoNode = nodeSource.get(nanoNodeId); + final nanoNode = nodeSource.get(nanoNodeId); final solanaNode = nodeSource.get(solanaNodeId); final tronNode = nodeSource.get(tronNodeId); final wowneroNode = nodeSource.get(wowneroNodeId); + final zanoNode = nodeSource.get(zanoNodeId); + if (moneroNode != null) { nodes[WalletType.monero] = moneroNode; } @@ -1547,6 +1590,11 @@ abstract class SettingsStoreBase with Store { if (wowneroNode != null) { nodes[WalletType.wownero] = wowneroNode; + + } + + if (zanoNode != null) { + nodes[WalletType.zano] = zanoNode; } // MIGRATED: @@ -1685,6 +1733,8 @@ abstract class SettingsStoreBase with Store { case WalletType.wownero: await _sharedPreferences.setInt(PreferencesKey.currentWowneroNodeIdKey, node.key as int); break; + case WalletType.zano: + await _sharedPreferences.setInt(PreferencesKey.currentZanoNodeIdKey, node.key as int); default: break; } diff --git a/lib/themes/extensions/theme_type_images.dart b/lib/themes/extensions/theme_type_images.dart new file mode 100644 index 000000000..39461e702 --- /dev/null +++ b/lib/themes/extensions/theme_type_images.dart @@ -0,0 +1,14 @@ +import 'package:cake_wallet/themes/theme_base.dart'; + +extension ThemeTypeImages on ThemeType { + String get walletGroupImage { + switch (this) { + case ThemeType.bright: + return 'assets/images/wallet_group_bright.png'; + case ThemeType.light: + return 'assets/images/wallet_group_light.png'; + default: + return 'assets/images/wallet_group_dark.png'; + } + } +} diff --git a/lib/themes/monero_dark_theme.dart b/lib/themes/monero_dark_theme.dart index 1478ba8c5..53f11716b 100644 --- a/lib/themes/monero_dark_theme.dart +++ b/lib/themes/monero_dark_theme.dart @@ -21,6 +21,7 @@ import 'package:cake_wallet/themes/extensions/send_page_theme.dart'; import 'package:cake_wallet/themes/extensions/sync_indicator_theme.dart'; import 'package:cake_wallet/themes/extensions/transaction_trade_theme.dart'; import 'package:cake_wallet/themes/extensions/wallet_list_theme.dart'; +import 'package:cake_wallet/themes/theme_base.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/palette.dart'; import 'package:flutter/material.dart'; @@ -28,6 +29,8 @@ import 'package:flutter/material.dart'; class MoneroDarkTheme extends DarkTheme { MoneroDarkTheme({required int raw}) : super(raw: raw); + @override + ThemeType get type => ThemeType.oled; @override String get title => S.current.monero_dark_theme; @override diff --git a/lib/themes/theme_base.dart b/lib/themes/theme_base.dart index 3bba6f65f..ad5807db0 100644 --- a/lib/themes/theme_base.dart +++ b/lib/themes/theme_base.dart @@ -27,7 +27,7 @@ import 'package:cake_wallet/themes/extensions/transaction_trade_theme.dart'; import 'package:cake_wallet/themes/extensions/wallet_list_theme.dart'; import 'package:flutter/material.dart'; -enum ThemeType { light, bright, dark } +enum ThemeType { light, bright, dark, oled} abstract class ThemeBase { ThemeBase({required this.raw}) { diff --git a/lib/utils/exception_handler.dart b/lib/utils/exception_handler.dart index b949c9968..17e6daed3 100644 --- a/lib/utils/exception_handler.dart +++ b/lib/utils/exception_handler.dart @@ -201,6 +201,7 @@ class ExceptionHandler { "Connection closed before full header was received", "Connection terminated during handshake", "PERMISSION_NOT_GRANTED", + "OS Error: Permission denied", "Failed host lookup:", "CERTIFICATE_VERIFY_FAILED", "Handshake error in client", diff --git a/lib/utils/payment_request.dart b/lib/utils/payment_request.dart index fe0ecf605..42b1d0ff6 100644 --- a/lib/utils/payment_request.dart +++ b/lib/utils/payment_request.dart @@ -9,6 +9,7 @@ class PaymentRequest { var amount = ""; var note = ""; var scheme = ""; + String? walletType; String? callbackUrl; String? callbackMessage; @@ -19,11 +20,11 @@ class PaymentRequest { scheme = uri.scheme; callbackUrl = uri.queryParameters['callback']; callbackMessage = uri.queryParameters['callbackMessage']; + walletType = uri.queryParameters['type']; } if (scheme == "nano-gpt") { - // treat as nano so filling out the address works: - scheme = "nano"; + scheme = walletType ?? "nano"; } if (nano != null) { diff --git a/lib/view_model/advanced_privacy_settings_view_model.dart b/lib/view_model/advanced_privacy_settings_view_model.dart index 85b9dbead..14d7ad566 100644 --- a/lib/view_model/advanced_privacy_settings_view_model.dart +++ b/lib/view_model/advanced_privacy_settings_view_model.dart @@ -54,11 +54,11 @@ abstract class AdvancedPrivacySettingsViewModelBase with Store { case WalletType.wownero: case WalletType.none: case WalletType.haven: + case WalletType.zano: return false; } } - bool get isMoneroSeedTypeOptionsEnabled => [ WalletType.monero, WalletType.wownero, @@ -78,6 +78,9 @@ abstract class AdvancedPrivacySettingsViewModelBase with Store { WalletType.ethereum, WalletType.polygon, WalletType.tron, + WalletType.monero, + WalletType.wownero, + WalletType.zano, ].contains(type); @computed diff --git a/lib/view_model/dashboard/balance_view_model.dart b/lib/view_model/dashboard/balance_view_model.dart index 5ca11e2bb..730f07a93 100644 --- a/lib/view_model/dashboard/balance_view_model.dart +++ b/lib/view_model/dashboard/balance_view_model.dart @@ -111,7 +111,8 @@ abstract class BalanceViewModelBase with Store { bool get isHomeScreenSettingsEnabled => isEVMCompatibleChain(wallet.type) || wallet.type == WalletType.solana || - wallet.type == WalletType.tron; + wallet.type == WalletType.tron || + wallet.type == WalletType.zano; @computed bool get hasAccounts => wallet.type == WalletType.monero || wallet.type == WalletType.wownero; @@ -196,15 +197,14 @@ abstract class BalanceViewModelBase with Store { } } - @computed - String get additionalBalance { - final walletBalance = _walletBalance; + String additionalBalance(CryptoCurrency cryptoCurrency) { + final balance = _currencyBalance(cryptoCurrency); if (displayMode == BalanceDisplayMode.hiddenBalance) { return '0.0'; } - return walletBalance.formattedAdditionalBalance; + return balance.formattedAdditionalBalance; } @computed @@ -216,7 +216,7 @@ abstract class BalanceViewModelBase with Store { key, BalanceRecord( availableBalance: '●●●●●●', - additionalBalance: additionalBalance, + additionalBalance: additionalBalance(key), frozenBalance: '', secondAvailableBalance: '●●●●●●', secondAdditionalBalance: '●●●●●●', @@ -286,10 +286,9 @@ abstract class BalanceViewModelBase with Store { @observable bool mwebEnabled = false; - @computed - bool get hasAdditionalBalance { + bool hasAdditionalBalance(CryptoCurrency currency) { bool isWalletTypeActivated = _hasAdditionalBalanceForWalletType(wallet.type); - bool isNotZeroAmount = additionalBalance != "0.0"; + bool isNotZeroAmount = additionalBalance(currency) != "0.0"; return isWalletTypeActivated && isNotZeroAmount; } @@ -306,6 +305,7 @@ abstract class BalanceViewModelBase with Store { switch (type) { case WalletType.monero: case WalletType.wownero: + case WalletType.zano: return true; default: return false; @@ -382,9 +382,8 @@ abstract class BalanceViewModelBase with Store { return balance; } - @computed - Balance get _walletBalance { - final balance = wallet.balance[wallet.currency]; + Balance _currencyBalance(CryptoCurrency cryptoCurrency) { + final balance = wallet.balance[cryptoCurrency]; if (balance == null) { throw Exception('No balance for ${wallet.currency}'); diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index 8d956d5bd..fa4800c8a 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -52,6 +52,8 @@ import 'package:http/http.dart' as http; import 'package:mobx/mobx.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import '../../themes/theme_base.dart'; + part 'dashboard_view_model.g.dart'; class DashboardViewModel = DashboardViewModelBase with _$DashboardViewModel; @@ -70,7 +72,7 @@ abstract class DashboardViewModelBase with Store { required this.sharedPreferences, required this.keyService}) : hasTradeAction = false, - hasExchangeAction = false, + hasSwapAction = false, isShowFirstYatIntroduction = false, isShowSecondYatIntroduction = false, isShowThirdYatIntroduction = false, @@ -130,6 +132,11 @@ abstract class DashboardViewModelBase with Store { caption: ExchangeProviderDescription.exolix.title, onChanged: () => tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.exolix)), + FilterItem( + value: () => tradeFilterStore.displayChainflip, + caption: ExchangeProviderDescription.chainflip.title, + onChanged: () => + tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.chainflip)), FilterItem( value: () => tradeFilterStore.displayThorChain, caption: ExchangeProviderDescription.thorChain.title, @@ -283,9 +290,7 @@ abstract class DashboardViewModelBase with Store { } _checkMweb(); - reaction((_) => settingsStore.mwebAlwaysScan, (bool value) { - _checkMweb(); - }); + reaction((_) => settingsStore.mwebAlwaysScan, (bool value) => _checkMweb()); } void _checkMweb() { @@ -383,12 +388,13 @@ abstract class DashboardViewModelBase with Store { bool get isTestnet => wallet.type == WalletType.bitcoin && bitcoin!.isTestnet(wallet); @computed - bool get hasRescan => - wallet.type == WalletType.bitcoin || - wallet.type == WalletType.monero || - wallet.type == WalletType.litecoin || - wallet.type == WalletType.wownero || - wallet.type == WalletType.haven; + bool get hasRescan => [ + WalletType.bitcoin, + WalletType.monero, + WalletType.litecoin, + WalletType.wownero, + WalletType.haven + ].contains(wallet.type); @computed bool get isMoneroViewOnly { @@ -476,6 +482,34 @@ abstract class DashboardViewModelBase with Store { @computed bool get hasEnabledMwebBefore => settingsStore.hasEnabledMwebBefore; + @action + double getShadowSpread() { + double spread = 0; + if (settingsStore.currentTheme.type == ThemeType.bright) + spread = 0; + else if (settingsStore.currentTheme.type == ThemeType.light) + spread = 0; + else if (settingsStore.currentTheme.type == ThemeType.dark) + spread = 0; + else if (settingsStore.currentTheme.type == ThemeType.oled) + spread = 0; + return spread; + } + + @action + double getShadowBlur() { + double blur = 0; + if (settingsStore.currentTheme.type == ThemeType.bright) + blur = 0; + else if (settingsStore.currentTheme.type == ThemeType.light) + blur = 0; + else if (settingsStore.currentTheme.type == ThemeType.dark) + blur = 0; + else if (settingsStore.currentTheme.type == ThemeType.oled) + blur = 0; + return blur; + } + @action void setMwebEnabled() { if (!hasMweb) { @@ -526,10 +560,10 @@ abstract class DashboardViewModelBase with Store { void furtherShowYatPopup(bool shouldShow) => settingsStore.shouldShowYatPopup = shouldShow; @computed - bool get isEnabledExchangeAction => settingsStore.exchangeStatus != ExchangeApiMode.disabled; + bool get isEnabledSwapAction => settingsStore.exchangeStatus != ExchangeApiMode.disabled; @observable - bool hasExchangeAction; + bool hasSwapAction; @computed bool get isEnabledTradeAction => !settingsStore.disableTradeOption; @@ -545,30 +579,25 @@ abstract class DashboardViewModelBase with Store { ReactionDisposer? _onMoneroBalanceChangeReaction; @computed - bool get hasPowNodes => wallet.type == WalletType.nano || wallet.type == WalletType.banano; + bool get hasPowNodes => [WalletType.nano, WalletType.banano].contains(wallet.type); @computed bool get hasSignMessages { - if (wallet.isHardwareWallet) { - return false; - } - switch (wallet.type) { - case WalletType.monero: - case WalletType.litecoin: - case WalletType.bitcoin: - case WalletType.bitcoinCash: - case WalletType.ethereum: - case WalletType.polygon: - case WalletType.solana: - case WalletType.nano: - case WalletType.banano: - case WalletType.tron: - case WalletType.wownero: - return true; - case WalletType.haven: - case WalletType.none: - return false; - } + if (wallet.isHardwareWallet) return false; + + return [ + WalletType.monero, + WalletType.litecoin, + WalletType.bitcoin, + WalletType.bitcoinCash, + WalletType.ethereum, + WalletType.polygon, + WalletType.solana, + WalletType.nano, + WalletType.banano, + WalletType.tron, + WalletType.wownero + ].contains(wallet.type); } bool get showRepWarning { @@ -737,7 +766,7 @@ abstract class DashboardViewModelBase with Store { } void updateActions() { - hasExchangeAction = !isHaven; + hasSwapAction = !isHaven; hasTradeAction = !isHaven; } diff --git a/lib/view_model/dashboard/home_settings_view_model.dart b/lib/view_model/dashboard/home_settings_view_model.dart index 0c3a611eb..197d550c3 100644 --- a/lib/view_model/dashboard/home_settings_view_model.dart +++ b/lib/view_model/dashboard/home_settings_view_model.dart @@ -13,6 +13,7 @@ import 'package:cake_wallet/solana/solana.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/tron/tron.dart'; import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart'; +import 'package:cake_wallet/zano/zano.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/erc20_token.dart'; import 'package:cw_core/utils/print_verbose.dart'; @@ -39,6 +40,8 @@ abstract class HomeSettingsViewModelBase with Store { final ObservableSet tokens; + WalletType get walletType => _balanceViewModel.wallet.type; + @observable bool isAddingToken; @@ -108,9 +111,16 @@ abstract class HomeSettingsViewModelBase with Store { await tron!.addTronToken(_balanceViewModel.wallet, token, contractAddress); } + if (_balanceViewModel.wallet.type == WalletType.zano) { + await zano!.addZanoAssetById(_balanceViewModel.wallet, contractAddress); + } + _updateTokensList(); _updateFiatPrices(token); - } finally { + } catch (e) { + throw e; + } + finally { isAddingToken = false; } } @@ -134,6 +144,9 @@ abstract class HomeSettingsViewModelBase with Store { if (_balanceViewModel.wallet.type == WalletType.tron) { await tron!.deleteTronToken(_balanceViewModel.wallet, token); } + if (_balanceViewModel.wallet.type == WalletType.zano) { + await zano!.deleteZanoAsset(_balanceViewModel.wallet, token); + } _updateTokensList(); } finally { isDeletingToken = false; @@ -340,6 +353,10 @@ abstract class HomeSettingsViewModelBase with Store { return await tron!.getTronToken(_balanceViewModel.wallet, contractAddress); } + if (_balanceViewModel.wallet.type == WalletType.zano) { + return await zano!.getZanoAsset(_balanceViewModel.wallet, contractAddress); + } + return null; } @@ -378,6 +395,10 @@ abstract class HomeSettingsViewModelBase with Store { tron!.addTronToken(_balanceViewModel.wallet, token, address); } + if (_balanceViewModel.wallet.type == WalletType.zano) { + await zano!.changeZanoAssetAvailability(_balanceViewModel.wallet, token); + } + _refreshTokensList(); } @@ -432,6 +453,13 @@ abstract class HomeSettingsViewModelBase with Store { .toList() ..sort(_sortFunc)); } + + if (_balanceViewModel.wallet.type == WalletType.zano) { + tokens.addAll(zano!.getZanoAssets(_balanceViewModel.wallet) + .where((element) => _matchesSearchText(element)) + .toList() + ..sort(_sortFunc)); + } } @action @@ -476,6 +504,10 @@ abstract class HomeSettingsViewModelBase with Store { return polygon!.getTokenAddress(asset); } + if (_balanceViewModel.wallet.type == WalletType.zano) { + return zano!.getZanoAssetAddress(asset); + } + // We return null if it's neither Tron, Polygon, Ethereum or Solana wallet (which is actually impossible because we only display home settings for either of these three wallets). return null; } diff --git a/lib/view_model/dashboard/trade_list_item.dart b/lib/view_model/dashboard/trade_list_item.dart index 55ae4e99f..973f5b76f 100644 --- a/lib/view_model/dashboard/trade_list_item.dart +++ b/lib/view_model/dashboard/trade_list_item.dart @@ -18,6 +18,9 @@ class TradeListItem extends ActionListItem { String get tradeFormattedAmount => displayMode == BalanceDisplayMode.hiddenBalance ? '---' : trade.amountFormatted(); + String get tradeFormattedReceiveAmount => + displayMode == BalanceDisplayMode.hiddenBalance ? '---' : trade.receiveAmountFormatted(); + @override DateTime get date => trade.createdAt!; } diff --git a/lib/view_model/dashboard/transaction_list_item.dart b/lib/view_model/dashboard/transaction_list_item.dart index d9b361fd2..864448293 100644 --- a/lib/view_model/dashboard/transaction_list_item.dart +++ b/lib/view_model/dashboard/transaction_list_item.dart @@ -8,6 +8,7 @@ import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cake_wallet/solana/solana.dart'; import 'package:cake_wallet/tron/tron.dart'; import 'package:cake_wallet/wownero/wownero.dart'; +import 'package:cake_wallet/zano/zano.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/transaction_info.dart'; @@ -63,6 +64,7 @@ class TransactionListItem extends ActionListItem with Keyable { switch (balanceViewModel.wallet.type) { case WalletType.monero: case WalletType.haven: + case WalletType.zano: if (transaction.confirmations >= 0 && transaction.confirmations < 10) { return ' (${transaction.confirmations}/10)'; } @@ -103,6 +105,7 @@ class TransactionListItem extends ActionListItem with Keyable { WalletType.haven, WalletType.wownero, WalletType.litecoin, + WalletType.zano, ].contains(balanceViewModel.wallet.type)) { return formattedPendingStatus; } @@ -201,7 +204,6 @@ class TransactionListItem extends ActionListItem with Keyable { price: price, ); break; - case WalletType.tron: final asset = tron!.assetOfTransaction(balanceViewModel.wallet, transaction); final price = balanceViewModel.fiatConvertationStore.prices[asset]; @@ -211,6 +213,17 @@ class TransactionListItem extends ActionListItem with Keyable { price: price, ); break; + case WalletType.zano: + final asset = zano!.assetOfTransaction(balanceViewModel.wallet, transaction); + if (asset == null) { + amount = "0.00"; + break; + } + final price = balanceViewModel.fiatConvertationStore.prices[asset]; + amount = calculateFiatAmountRaw( + cryptoAmount: zano!.formatterIntAmountToDouble(amount: transaction.amount, currency: asset, forFee: false), + price: price); + break; default: break; } diff --git a/lib/view_model/exchange/exchange_trade_view_model.dart b/lib/view_model/exchange/exchange_trade_view_model.dart index 0c8053842..fd8c29f1f 100644 --- a/lib/view_model/exchange/exchange_trade_view_model.dart +++ b/lib/view_model/exchange/exchange_trade_view_model.dart @@ -1,10 +1,11 @@ import 'dart:async'; import 'package:cake_wallet/exchange/exchange_provider_description.dart'; +import 'package:cake_wallet/exchange/provider/chainflip_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/changenow_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/exolix_exchange_provider.dart'; -import 'package:cake_wallet/exchange/provider/quantex_exchange_provider.dart'; +import 'package:cake_wallet/exchange/provider/swaptrade_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/sideshift_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/simpleswap_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/stealth_ex_exchange_provider.dart'; @@ -51,14 +52,18 @@ abstract class ExchangeTradeViewModelBase with Store { case ExchangeProviderDescription.exolix: _provider = ExolixExchangeProvider(); break; - case ExchangeProviderDescription.quantex: - _provider = QuantexExchangeProvider(); + case ExchangeProviderDescription.swapTrade: + _provider = SwapTradeExchangeProvider(); break; case ExchangeProviderDescription.stealthEx: _provider = StealthExExchangeProvider(); + break; case ExchangeProviderDescription.thorChain: _provider = ThorChainExchangeProvider(tradesStore: trades); break; + case ExchangeProviderDescription.chainflip: + _provider = ChainflipExchangeProvider(tradesStore: trades); + break; } _updateItems(); diff --git a/lib/view_model/exchange/exchange_view_model.dart b/lib/view_model/exchange/exchange_view_model.dart index 63e1db6bc..f10860c7d 100644 --- a/lib/view_model/exchange/exchange_view_model.dart +++ b/lib/view_model/exchange/exchange_view_model.dart @@ -4,6 +4,7 @@ import 'dart:convert'; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cake_wallet/core/create_trade_result.dart'; +import 'package:cake_wallet/exchange/provider/chainflip_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/letsexchange_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/stealth_ex_exchange_provider.dart'; import 'package:cw_core/crypto_currency.dart'; @@ -34,7 +35,7 @@ import 'package:cake_wallet/exchange/limits_state.dart'; import 'package:cake_wallet/exchange/provider/changenow_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/exolix_exchange_provider.dart'; -import 'package:cake_wallet/exchange/provider/quantex_exchange_provider.dart'; +import 'package:cake_wallet/exchange/provider/swaptrade_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/sideshift_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/simpleswap_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/thorchain_exchange.provider.dart'; @@ -115,9 +116,9 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with .toList()); _setAvailableProviders(); - _calculateBestRate(); + calculateBestRate(); - bestRateSync = Timer.periodic(Duration(seconds: 10), (timer) => _calculateBestRate()); + bestRateSync = Timer.periodic(Duration(seconds: 10), (timer) => calculateBestRate()); isDepositAddressEnabled = !(depositCurrency == wallet.currency); depositAmount = ''; @@ -144,8 +145,8 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with loadLimits(); reaction((_) => isFixedRateMode, (Object _) { loadLimits(); - _bestRate = 0; - _calculateBestRate(); + bestRate = 0; + calculateBestRate(); }); if (isElectrumWallet) { @@ -159,7 +160,8 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with wallet.type == WalletType.bitcoinCash; bool get hideAddressAfterExchange => - wallet.type == WalletType.monero || wallet.type == WalletType.wownero; + wallet.type == WalletType.monero || + wallet.type == WalletType.wownero; bool _useTorOnly; final Box trades; @@ -172,8 +174,9 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with SideShiftExchangeProvider(), SimpleSwapExchangeProvider(), ThorChainExchangeProvider(tradesStore: trades), + ChainflipExchangeProvider(tradesStore: trades), if (FeatureFlag.isExolixEnabled) ExolixExchangeProvider(), - QuantexExchangeProvider(), + SwapTradeExchangeProvider(), LetsExchangeExchangeProvider(), StealthExExchangeProvider(), TrocadorExchangeProvider( @@ -308,6 +311,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with case WalletType.monero: case WalletType.wownero: case WalletType.haven: + case WalletType.zano: return transactionPriority == monero!.getMoneroTransactionPrioritySlow(); case WalletType.bitcoin: return transactionPriority == bitcoin!.getBitcoinTransactionPrioritySlow(); @@ -334,7 +338,8 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with final ContactListViewModel contactListViewModel; - double _bestRate = 0.0; + @observable + double bestRate = 0.0; late Timer bestRateSync; @@ -366,15 +371,15 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with final _enteredAmount = double.tryParse(amount.replaceAll(',', '.')) ?? 0; - if (_bestRate == 0) { + if (bestRate == 0) { depositAmount = S.current.fetching; - await _calculateBestRate(); + await calculateBestRate(); } _cryptoNumberFormat.maximumFractionDigits = depositMaxDigits; depositAmount = _cryptoNumberFormat - .format(_enteredAmount / _bestRate) + .format(_enteredAmount / bestRate) .toString() .replaceAll(RegExp('\\,'), ''); } @@ -392,15 +397,15 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with final _enteredAmount = double.tryParse(amount.replaceAll(',', '.')) ?? 0; /// in case the best rate was not calculated yet - if (_bestRate == 0) { + if (bestRate == 0) { receiveAmount = S.current.fetching; - await _calculateBestRate(); + await calculateBestRate(); } _cryptoNumberFormat.maximumFractionDigits = receiveMaxDigits; receiveAmount = _cryptoNumberFormat - .format(_bestRate * _enteredAmount) + .format(bestRate * _enteredAmount) .toString() .replaceAll(RegExp('\\,'), ''); } @@ -416,8 +421,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with return true; } - - Future _calculateBestRate() async { + Future calculateBestRate() async { final amount = double.tryParse(isFixedRateMode ? receiveAmount : depositAmount) ?? 1; final _providers = _tradeAvailableProviders @@ -453,7 +457,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with } } } - if (_sortedAvailableProviders.isNotEmpty) _bestRate = _sortedAvailableProviders.keys.first; + if (_sortedAvailableProviders.isNotEmpty) bestRate = _sortedAvailableProviders.keys.first; } @action @@ -533,7 +537,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with if (!(await provider.checkIsAvailable())) continue; - _bestRate = providerRate; + bestRate = providerRate; await changeDepositAmount(amount: depositAmount); final request = TradeRequest( @@ -693,8 +697,8 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with receiveAmount = ''; loadLimits(); _setAvailableProviders(); - _bestRate = 0; - _calculateBestRate(); + bestRate = 0; + calculateBestRate(); } void _initialPairBasedOnWallet() { @@ -747,6 +751,10 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with depositCurrency = CryptoCurrency.wow; receiveCurrency = CryptoCurrency.xmr; break; + case WalletType.zano: + depositCurrency = CryptoCurrency.zano; + receiveCurrency = CryptoCurrency.xmr; + break; case WalletType.none: break; } @@ -785,8 +793,8 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with isFixedRateMode = false; _defineIsReceiveAmountEditable(); loadLimits(); - _bestRate = 0; - _calculateBestRate(); + bestRate = 0; + calculateBestRate(); final Map exchangeProvidersSelection = json.decode(sharedPreferences.getString(PreferencesKey.exchangeProvidersSelection) ?? "{}") @@ -821,6 +829,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with case WalletType.monero: case WalletType.haven: case WalletType.wownero: + case WalletType.zano: _settingsStore.priority[wallet.type] = monero!.getMoneroTransactionPriorityAutomatic(); break; case WalletType.bitcoin: diff --git a/lib/view_model/hardware_wallet/ledger_view_model.dart b/lib/view_model/hardware_wallet/ledger_view_model.dart index 4c084c778..4f80aa698 100644 --- a/lib/view_model/hardware_wallet/ledger_view_model.dart +++ b/lib/view_model/hardware_wallet/ledger_view_model.dart @@ -16,7 +16,6 @@ import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/widgets.dart'; - import 'package:ledger_flutter_plus/ledger_flutter_plus.dart' as sdk; import 'package:mobx/mobx.dart'; import 'package:permission_handler/permission_handler.dart'; @@ -61,20 +60,21 @@ abstract class LedgerViewModelBase with Store { bool bleIsEnabled = false; bool _bleIsInitialized = false; + Future _initBLE() async { if (bleIsEnabled && !_bleIsInitialized) { ledgerPlusBLE = sdk.LedgerInterface.ble( - onPermissionRequest: (_) async { - Map statuses = await [ - Permission.bluetoothScan, - Permission.bluetoothConnect, - Permission.bluetoothAdvertise, - ].request(); + onPermissionRequest: (_) async { + Map statuses = await [ + Permission.bluetoothScan, + Permission.bluetoothConnect, + Permission.bluetoothAdvertise, + ].request(); - return statuses.values.where((status) => status.isDenied).isEmpty; - }, - bleOptions: - sdk.BluetoothOptions(maxScanDuration: Duration(minutes: 5))); + return statuses.values.where((status) => status.isDenied).isEmpty; + }, + bleOptions: sdk.BluetoothOptions(maxScanDuration: Duration(minutes: 5)), + ); _bleIsInitialized = true; } } @@ -92,10 +92,8 @@ abstract class LedgerViewModelBase with Store { Stream scanForUsbDevices() => ledgerPlusUSB.scan(); Future stopScanning() async { - await ledgerPlusBLE.stopScanning(); - if (!Platform.isIOS) { - await ledgerPlusUSB.stopScanning(); - } + if (_bleIsInitialized) await ledgerPlusBLE.stopScanning(); + if (!Platform.isIOS) await ledgerPlusUSB.stopScanning(); } Future connectLedger(sdk.LedgerDevice device, WalletType type) async { @@ -112,8 +110,8 @@ abstract class LedgerViewModelBase with Store { : ledgerPlusUSB; if (_connectionChangeSubscription == null) { - _connectionChangeSubscription = ledger.deviceStateChanges - .listen(_connectionChangeListener); + _connectionChangeSubscription = + ledger.deviceStateChanges.listen(_connectionChangeListener); } _connection = await ledger.connect(device); @@ -125,8 +123,7 @@ abstract class LedgerViewModelBase with Store { bool _isConnecting = true; WalletType? _connectingWalletType; - void _connectionChangeListener( - sdk.BleConnectionState event, ) { + void _connectionChangeListener(sdk.BleConnectionState event) { printV('Ledger Device State Changed: $event'); if (event == sdk.BleConnectionState.disconnected && !_isConnecting) { _connection = null; 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 8b3c70c5e..71f996aff 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 @@ -1,6 +1,7 @@ import 'package:cake_wallet/core/execution_state.dart'; import 'package:cake_wallet/entities/qr_scanner.dart'; import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cw_core/utils/print_verbose.dart'; import 'package:flutter/cupertino.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; @@ -85,6 +86,7 @@ abstract class NodeCreateOrEditViewModelBase with Store { case WalletType.litecoin: case WalletType.bitcoinCash: case WalletType.bitcoin: + case WalletType.zano: return false; } } @@ -213,29 +215,29 @@ abstract class NodeCreateOrEditViewModelBase with Store { bool isCameraPermissionGranted = await PermissionHandler.checkPermission(Permission.camera, context); if (!isCameraPermissionGranted) return; - String code = await presentQRScanner(context); + String? code = await presentQRScanner(context); + if (code == null) throw Exception("Unexpected QR code value: aborted"); if (code.isEmpty) { throw Exception('Unexpected scan QR code value: value is empty'); } + if (!code.contains('://')) code = 'tcp://$code'; + final uri = Uri.tryParse(code); - - if (uri == null) { - throw Exception('Unexpected scan QR code value: Value is invalid'); + if (uri == null || uri.host.isEmpty) { + throw Exception('Invalid QR code: Unable to parse or missing host.'); } - final userInfo = uri.userInfo.split(':'); - - if (userInfo.length < 2) { - throw Exception('Unexpected scan QR code value: Value is invalid'); - } - - final rpcUser = userInfo[0]; - final rpcPassword = userInfo[1]; + final userInfo = uri.userInfo; + final rpcUser = userInfo.length == 2 ? userInfo[0] : ''; + final rpcPassword = userInfo.length == 2 ? userInfo[1] : ''; final ipAddress = uri.host; - final port = uri.port.toString(); + final port = uri.hasPort ? uri.port.toString() : ''; final path = uri.path; + final queryParams = uri.queryParameters; // Currently not used + + await Future.delayed(Duration(milliseconds: 345)); setAddress(ipAddress); setPath(path); diff --git a/lib/view_model/node_list/node_list_view_model.dart b/lib/view_model/node_list/node_list_view_model.dart index 2721fd7b3..71e77eb12 100644 --- a/lib/view_model/node_list/node_list_view_model.dart +++ b/lib/view_model/node_list/node_list_view_model.dart @@ -88,6 +88,9 @@ abstract class NodeListViewModelBase with Store { case WalletType.wownero: node = getWowneroDefaultNode(nodes: _nodeSource); break; + case WalletType.zano: + node = getZanoDefaultNode(nodes: _nodeSource)!; + break; default: throw Exception('Unexpected wallet type: ${_appStore.wallet!.type}'); } diff --git a/lib/view_model/restore/restore_from_qr_vm.dart b/lib/view_model/restore/restore_from_qr_vm.dart index cbdad85b8..f31c93911 100644 --- a/lib/view_model/restore/restore_from_qr_vm.dart +++ b/lib/view_model/restore/restore_from_qr_vm.dart @@ -9,6 +9,7 @@ import 'package:cake_wallet/view_model/restore/restore_mode.dart'; import 'package:cake_wallet/view_model/restore/restore_wallet.dart'; import 'package:cake_wallet/view_model/seed_settings_view_model.dart'; import 'package:cake_wallet/wownero/wownero.dart'; +import 'package:cake_wallet/zano/zano.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/monero/monero.dart'; @@ -110,6 +111,7 @@ abstract class WalletRestorationFromQRVMBase extends WalletCreationVM with Store height: restoreWallet.height ?? 0, mnemonic: restoreWallet.mnemonicSeed ?? '', password: password, + passphrase: restoreWallet.passphrase ?? '', ); case WalletType.bitcoin: case WalletType.litecoin: @@ -180,6 +182,15 @@ abstract class WalletRestorationFromQRVMBase extends WalletCreationVM with Store height: restoreWallet.height ?? 0, mnemonic: restoreWallet.mnemonicSeed ?? '', password: password, + passphrase: restoreWallet.passphrase ?? '', + ); + case WalletType.zano: + return zano!.createZanoRestoreWalletFromSeedCredentials( + name: name, + password: password, + height: height, + mnemonic: restoreWallet.mnemonicSeed ?? '', + passphrase: restoreWallet.passphrase ?? '', ); default: throw Exception('Unexpected type: ${type.toString()}'); 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 c1a19ea57..e28a429a4 100644 --- a/lib/view_model/restore/wallet_restore_from_qr_code.dart +++ b/lib/view_model/restore/wallet_restore_from_qr_code.dart @@ -41,6 +41,9 @@ class WalletRestoreFromQRCode { 'wownero': WalletType.wownero, 'wownero-wallet': WalletType.wownero, 'wownero_wallet': WalletType.wownero, + 'zano': WalletType.zano, + 'zano-wallet': WalletType.zano, + 'zano_wallet': WalletType.zano, }; static bool _containsAssetSpecifier(String code) => _extractWalletType(code) != null; @@ -73,9 +76,7 @@ class WalletRestoreFromQRCode { RegExp _getPattern(int wordCount) => RegExp(r'(?<=\W|^)((?:\w+\s+){' + (wordCount - 1).toString() + r'}\w+)(?=\W|$)'); - List patternCounts = walletType == WalletType.monero || walletType == WalletType.wownero - ? [25, 16, 14, 13] - : [24, 18, 12]; + final List patternCounts = [12, 13, 14, 16, 18, 24, 25, 26]; for (final count in patternCounts) { final pattern = _getPattern(count); @@ -88,7 +89,8 @@ class WalletRestoreFromQRCode { } static Future scanQRCodeForRestoring(BuildContext context) async { - String code = await presentQRScanner(context); + String? code = await presentQRScanner(context); + if (code == null) throw Exception("Unexpected scan QR code value: aborted"); if (code.isEmpty) throw Exception('Unexpected scan QR code value: value is empty'); WalletType? walletType; @@ -119,7 +121,9 @@ class WalletRestoreFromQRCode { queryParameters['seed'] = _extractSeedPhraseFromUrl(code, walletType!); } if (queryParameters['address'] == null) { - queryParameters['address'] = _extractAddressFromUrl(code, walletType!); + try { + queryParameters['address'] = _extractAddressFromUrl(code, walletType!); + } catch (_) {} } Map credentials = {'type': walletType, ...queryParameters, 'raw_qr': code}; @@ -220,7 +224,8 @@ class WalletRestoreFromQRCode { 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["version"] != 0) + throw UnimplementedError("Found view-only restore with unsupported version"); if (codeParsed["primaryAddress"] == null || codeParsed["privateViewKey"] == null || codeParsed["restoreHeight"] == null) { diff --git a/lib/view_model/send/output.dart b/lib/view_model/send/output.dart index f977ef003..121ffa693 100644 --- a/lib/view_model/send/output.dart +++ b/lib/view_model/send/output.dart @@ -10,6 +10,7 @@ import 'package:cake_wallet/solana/solana.dart'; import 'package:cake_wallet/src/screens/send/widgets/extract_address_from_parsed.dart'; import 'package:cake_wallet/tron/tron.dart'; import 'package:cake_wallet/wownero/wownero.dart'; +import 'package:cake_wallet/zano/zano.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:flutter/material.dart'; @@ -112,6 +113,9 @@ abstract class OutputBase with Store { case WalletType.wownero: _amount = wownero!.formatterWowneroParseAmount(amount: _cryptoAmount); break; + case WalletType.zano: + _amount = zano!.formatterParseAmount(amount: _cryptoAmount, currency: cryptoCurrencyHandler()); + break; default: break; } @@ -180,6 +184,10 @@ abstract class OutputBase with Store { if (_wallet.type == WalletType.polygon) { return polygon!.formatterPolygonAmountToDouble(amount: BigInt.from(fee)); } + + if (_wallet.type == WalletType.zano) { + return zano!.formatterIntAmountToDouble(amount: fee, currency: cryptoCurrencyHandler(), forFee: true); + } } catch (e) { printV(e.toString()); } @@ -308,6 +316,9 @@ abstract class OutputBase with Store { case WalletType.wownero: maximumFractionDigits = 11; break; + case WalletType.zano: + maximumFractionDigits = 12; + break; default: break; } diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index cafe89cb1..53c52aa1e 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -20,6 +20,7 @@ import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart'; import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart'; import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_list_view_model.dart'; import 'package:cake_wallet/wownero/wownero.dart'; +import 'package:cake_wallet/zano/zano.dart'; import 'package:cw_core/exceptions.dart'; import 'package:cw_core/transaction_info.dart'; import 'package:cw_core/transaction_priority.dart'; @@ -61,7 +62,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor selectedCryptoCurrency = wallet.currency; hasMultipleTokens = isEVMCompatibleChain(wallet.type) || wallet.type == WalletType.solana || - wallet.type == WalletType.tron; + wallet.type == WalletType.tron || + wallet.type == WalletType.zano; } UnspentCoinsListViewModel unspentCoinsListViewModel; @@ -81,7 +83,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor selectedCryptoCurrency = appStore.wallet!.currency, hasMultipleTokens = isEVMCompatibleChain(appStore.wallet!.type) || appStore.wallet!.type == WalletType.solana || - appStore.wallet!.type == WalletType.tron, + appStore.wallet!.type == WalletType.tron || + appStore.wallet!.type == WalletType.zano, outputs = ObservableList(), _settingsStore = appStore.settingsStore, fiatFromSettings = appStore.settingsStore.fiatCurrency, @@ -576,6 +579,9 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor .createSolanaTransactionCredentials(outputs, currency: selectedCryptoCurrency); case WalletType.tron: return tron!.createTronTransactionCredentials(outputs, currency: selectedCryptoCurrency); + case WalletType.zano: + return zano!.createZanoTransactionCredentials( + outputs: outputs, priority: priority!, currency: selectedCryptoCurrency); default: throw Exception('Unexpected wallet type: ${wallet.type}'); } diff --git a/lib/view_model/settings/display_settings_view_model.dart b/lib/view_model/settings/display_settings_view_model.dart index 69d82eff4..26eb05985 100644 --- a/lib/view_model/settings/display_settings_view_model.dart +++ b/lib/view_model/settings/display_settings_view_model.dart @@ -37,6 +37,9 @@ abstract class DisplaySettingsViewModelBase with Store { @computed bool get disabledFiatApiMode => _settingsStore.fiatApiMode == FiatApiMode.disabled; + @computed + bool get showAddressBookPopup => _settingsStore.showAddressBookPopupEnabled; + @action void setBalanceDisplayMode(BalanceDisplayMode value) => _settingsStore.balanceDisplayMode = value; @@ -66,4 +69,8 @@ abstract class DisplaySettingsViewModelBase with Store { void setShouldShowMarketPlaceInDashbaord(bool value) { _settingsStore.shouldShowMarketPlaceInDashboard = value; } + + @action + void setShowAddressBookPopup(bool value) => _settingsStore.showAddressBookPopupEnabled = value; + } diff --git a/lib/view_model/settings/mweb_settings_view_model.dart b/lib/view_model/settings/mweb_settings_view_model.dart index 11e4c8177..151854a2e 100644 --- a/lib/view_model/settings/mweb_settings_view_model.dart +++ b/lib/view_model/settings/mweb_settings_view_model.dart @@ -3,6 +3,8 @@ import 'dart:io'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/utils/exception_handler.dart'; +import 'package:cw_core/root_dir.dart'; +import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:flutter/widgets.dart'; import 'package:mobx/mobx.dart'; @@ -47,25 +49,6 @@ abstract class MwebSettingsViewModelBase with Store { _settingsStore.mwebAlwaysScan = value; } - Future saveLogsLocally(String filePath) async { - try { - final appSupportPath = (await getApplicationSupportDirectory()).path; - final logsFile = File("$appSupportPath/logs/debug.log"); - if (!logsFile.existsSync()) { - throw Exception('Logs file does not exist'); - } - await logsFile.copy(filePath); - return true; - } catch (e, s) { - ExceptionHandler.onError(FlutterErrorDetails( - exception: e, - stack: s, - library: "Export Logs", - )); - return false; - } - } - Future getAbbreviatedLogs() async { final appSupportPath = (await getApplicationSupportDirectory()).path; final logsFile = File("$appSupportPath/logs/debug.log"); diff --git a/lib/view_model/settings/other_settings_view_model.dart b/lib/view_model/settings/other_settings_view_model.dart index c7a5d0b90..3036e8ae9 100644 --- a/lib/view_model/settings/other_settings_view_model.dart +++ b/lib/view_model/settings/other_settings_view_model.dart @@ -60,10 +60,6 @@ abstract class OtherSettingsViewModelBase with Store { bool get changeRepresentativeEnabled => _wallet.type == WalletType.nano || _wallet.type == WalletType.banano; - @computed - bool get showAddressBookPopup => _settingsStore.showAddressBookPopupEnabled; - - @computed bool get displayTransactionPriority => !(changeRepresentativeEnabled || _wallet.type == WalletType.solana || @@ -118,9 +114,6 @@ abstract class OtherSettingsViewModelBase with Store { return customItem != null ? priorities.indexOf(customItem) : null; } - @action - void setShowAddressBookPopup(bool value) => _settingsStore.showAddressBookPopupEnabled = value; - int? get maxCustomFeeRate { if (_wallet.type == WalletType.bitcoin) { return bitcoin!.getMaxCustomFeeRate(_wallet); diff --git a/lib/view_model/settings/privacy_settings_view_model.dart b/lib/view_model/settings/privacy_settings_view_model.dart index 67f0d88a0..e2c977590 100644 --- a/lib/view_model/settings/privacy_settings_view_model.dart +++ b/lib/view_model/settings/privacy_settings_view_model.dart @@ -1,4 +1,3 @@ -import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart'; import 'package:cake_wallet/entities/exchange_api_mode.dart'; import 'package:cake_wallet/ethereum/ethereum.dart'; @@ -79,6 +78,9 @@ abstract class PrivacySettingsViewModelBase with Store { @computed bool get lookupTwitter => _settingsStore.lookupsTwitter; + @computed + bool get lookupsZanoAlias => _settingsStore.lookupsZanoAlias; + @computed bool get looksUpMastodon => _settingsStore.lookupsMastodon; @@ -127,6 +129,9 @@ abstract class PrivacySettingsViewModelBase with Store { @action void setLookupsTwitter(bool value) => _settingsStore.lookupsTwitter = value; + @action + void setLookupsZanoAlias(bool value) => _settingsStore.lookupsZanoAlias = value; + @action void setLookupsMastodon(bool value) => _settingsStore.lookupsMastodon = value; diff --git a/lib/view_model/support_view_model.dart b/lib/view_model/support_view_model.dart index 69659916f..d48b182b1 100644 --- a/lib/view_model/support_view_model.dart +++ b/lib/view_model/support_view_model.dart @@ -69,9 +69,9 @@ abstract class SupportViewModelBase with Store { linkTitle: 'support@exolix.com', link: 'mailto:support@exolix.com'), LinkListItem( - title: 'Quantex', - icon: 'assets/images/quantex.png', - linkTitle: 'help.myquantex.com', + title: 'SwapTrade', + icon: 'assets/images/swap_trade.png', + linkTitle: 'help.swaptrade.io', link: 'mailto:support@exolix.com'), LinkListItem( title: 'Trocador', diff --git a/lib/view_model/trade_details_view_model.dart b/lib/view_model/trade_details_view_model.dart index db7b979d9..6b509dc22 100644 --- a/lib/view_model/trade_details_view_model.dart +++ b/lib/view_model/trade_details_view_model.dart @@ -1,11 +1,12 @@ import 'dart:async'; import 'package:cake_wallet/exchange/exchange_provider_description.dart'; +import 'package:cake_wallet/exchange/provider/chainflip_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/changenow_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/exolix_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/letsexchange_exchange_provider.dart'; -import 'package:cake_wallet/exchange/provider/quantex_exchange_provider.dart'; +import 'package:cake_wallet/exchange/provider/swaptrade_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/sideshift_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/simpleswap_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/stealth_ex_exchange_provider.dart'; @@ -60,14 +61,17 @@ abstract class TradeDetailsViewModelBase with Store { case ExchangeProviderDescription.thorChain: _provider = ThorChainExchangeProvider(tradesStore: trades); break; - case ExchangeProviderDescription.quantex: - _provider = QuantexExchangeProvider(); + case ExchangeProviderDescription.swapTrade: + _provider = SwapTradeExchangeProvider(); case ExchangeProviderDescription.letsExchange: _provider = LetsExchangeExchangeProvider(); break; case ExchangeProviderDescription.stealthEx: _provider = StealthExExchangeProvider(); break; + case ExchangeProviderDescription.chainflip: + _provider = ChainflipExchangeProvider(tradesStore: trades); + break; } _updateItems(); @@ -92,12 +96,14 @@ abstract class TradeDetailsViewModelBase with Store { return 'https://exolix.com/transaction/${trade.id}'; case ExchangeProviderDescription.thorChain: return 'https://track.ninerealms.com/${trade.id}'; - case ExchangeProviderDescription.quantex: - return 'https://myquantex.com/send/${trade.id}'; + case ExchangeProviderDescription.swapTrade: + return 'https://swaptrade.io/send/${trade.id}'; case ExchangeProviderDescription.letsExchange: return 'https://letsexchange.io/?transactionId=${trade.id}'; case ExchangeProviderDescription.stealthEx: return 'https://stealthex.io/exchange/?id=${trade.id}'; + case ExchangeProviderDescription.chainflip: + return 'https://scan.chainflip.io/channels/${trade.id}'; } return null; } diff --git a/lib/view_model/transaction_details_view_model.dart b/lib/view_model/transaction_details_view_model.dart index 9ec542361..e93c6f8ff 100644 --- a/lib/view_model/transaction_details_view_model.dart +++ b/lib/view_model/transaction_details_view_model.dart @@ -81,6 +81,9 @@ abstract class TransactionDetailsViewModelBase with Store { case WalletType.wownero: _addWowneroListItems(tx, dateFormat); break; + case WalletType.zano: + _addZanoListItems(tx, dateFormat); + break; default: break; } @@ -181,6 +184,8 @@ abstract class TransactionDetailsViewModelBase with Store { return 'https://tronscan.org/#/transaction/${txId}'; case WalletType.wownero: return 'https://explore.wownero.com/tx/${txId}'; + case WalletType.zano: + return 'https://explorer.zano.org/transaction/${txId}'; case WalletType.none: return ''; } @@ -211,6 +216,8 @@ abstract class TransactionDetailsViewModelBase with Store { return S.current.view_transaction_on + 'tronscan.org'; case WalletType.wownero: return S.current.view_transaction_on + 'Wownero.com'; + case WalletType.zano: + return S.current.view_transaction_on + 'explorer.zano.org'; case WalletType.none: return ''; } @@ -776,4 +783,20 @@ abstract class TransactionDetailsViewModelBase with Store { items.addAll(_items); } + + void _addZanoListItems(TransactionInfo tx, DateFormat dateFormat) { + final comment = tx.additionalInfo['comment'] as String?; + items.addAll([ + StandartListItem(title: S.current.transaction_details_transaction_id, value: tx.id), + StandartListItem(title: 'Asset ID', value: tx.additionalInfo['assetId'] as String? ?? "Unknown asset id"), + StandartListItem( + title: S.current.transaction_details_date, value: dateFormat.format(tx.date)), + StandartListItem(title: S.current.transaction_details_height, value: '${tx.height}'), + StandartListItem(title: S.current.transaction_details_amount, value: tx.amountFormatted()), + if (tx.feeFormatted()?.isNotEmpty ?? false) + StandartListItem(title: S.current.transaction_details_fee, value: tx.feeFormatted()!), + if (comment != null && comment.isNotEmpty) + StandartListItem(title: S.current.transaction_details_title, value: comment), + ]); + } } diff --git a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart index 3e399266a..773d8335c 100644 --- a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart +++ b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart @@ -17,6 +17,7 @@ import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/store/yat/yat_store.dart'; import 'package:cake_wallet/tron/tron.dart'; +import 'package:cake_wallet/zano/zano.dart'; import 'package:cake_wallet/utils/list_item.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_account_list_header.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_hidden_list_header.dart'; @@ -205,6 +206,23 @@ class WowneroURI extends PaymentURI { } } +class ZanoURI extends PaymentURI { + ZanoURI({required String amount, required String address}) + : super(amount: amount, address: address); + + @override + String toString() { + var base = 'zano:' + address; + + if (amount.isNotEmpty) { + base += '?amount=${amount.replaceAll(',', '.')}'; + } + + return base; + } +} + + abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewModel with Store { WalletAddressListViewModelBase({ @@ -298,11 +316,12 @@ abstract class WalletAddressListViewModelBase return TronURI(amount: amount, address: address.address); case WalletType.wownero: return WowneroURI(amount: amount, address: address.address); + case WalletType.zano: + return ZanoURI(amount: amount, address: address.address); case WalletType.none: throw Exception('Unexpected type: ${type.toString()}'); } } - @computed ObservableList get items => ObservableList() ..addAll(_baseItems) @@ -479,6 +498,12 @@ abstract class WalletAddressListViewModelBase .contains((addressList[i] as WalletAddressListItem).address); } + if (wallet.type == WalletType.zano) { + final primaryAddress = zano!.getAddress(wallet); + + addressList.add(WalletAddressListItem(isPrimary: true, name: null, address: primaryAddress)); + } + if (searchText.isNotEmpty) { return ObservableList.of(addressList.where((item) { if (item is WalletAddressListItem) { diff --git a/lib/view_model/wallet_creation_vm.dart b/lib/view_model/wallet_creation_vm.dart index 083e076f8..66903b1da 100644 --- a/lib/view_model/wallet_creation_vm.dart +++ b/lib/view_model/wallet_creation_vm.dart @@ -10,6 +10,7 @@ import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/view_model/restore/restore_wallet.dart'; import 'package:cake_wallet/view_model/seed_settings_view_model.dart'; +import 'package:cw_core/exceptions.dart'; import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_base.dart'; @@ -118,7 +119,11 @@ abstract class WalletCreationVMBase with Store { } catch (e, s) { printV("error: $e"); printV("stack: $s"); - state = FailureState(e.toString()); + String message = e.toString(); + if (e is RestoreFromSeedException) { + message = e.message; + } + state = FailureState(message); } } diff --git a/lib/view_model/wallet_keys_view_model.dart b/lib/view_model/wallet_keys_view_model.dart index 81eda7cc8..402764c40 100644 --- a/lib/view_model/wallet_keys_view_model.dart +++ b/lib/view_model/wallet_keys_view_model.dart @@ -1,10 +1,10 @@ import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/haven/haven.dart'; import 'package:cake_wallet/monero/monero.dart'; -import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cake_wallet/src/screens/transaction_details/standart_list_item.dart'; import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/wownero/wownero.dart'; +import 'package:cake_wallet/zano/zano.dart'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/transaction_info.dart'; import 'package:cw_core/wallet_base.dart'; @@ -20,32 +20,29 @@ class WalletKeysViewModel = WalletKeysViewModelBase with _$WalletKeysViewModel; abstract class WalletKeysViewModelBase with Store { WalletKeysViewModelBase(this._appStore) - : title = _appStore.wallet!.type == WalletType.bitcoin || - _appStore.wallet!.type == WalletType.litecoin || - _appStore.wallet!.type == WalletType.bitcoinCash - ? S.current.wallet_seed - : S.current.wallet_keys, + : title = S.current.wallet_keys, + _wallet = _appStore.wallet!, _walletName = _appStore.wallet!.type.name, _restoreHeight = _appStore.wallet!.walletInfo.restoreHeight, _restoreHeightByTransactions = 0, items = ObservableList() { - _populateItems(); + _populateKeysItems(); reaction((_) => _appStore.wallet, (WalletBase? _wallet) { - _populateItems(); + _populateKeysItems(); }); - if (_appStore.wallet!.type == WalletType.monero || - _appStore.wallet!.type == WalletType.haven || - _appStore.wallet!.type == WalletType.wownero) { - final accountTransactions = _getWalletTransactions(_appStore.wallet!); + if (_wallet.type == WalletType.monero || + _wallet.type == WalletType.haven || + _wallet.type == WalletType.wownero) { + final accountTransactions = _getWalletTransactions(_wallet); if (accountTransactions.isNotEmpty) { final incomingAccountTransactions = accountTransactions.where((tx) => tx.direction == TransactionDirection.incoming); if (incomingAccountTransactions.isNotEmpty) { incomingAccountTransactions.toList().sort((a, b) => a.date.compareTo(b.date)); - _restoreHeightByTransactions = _getRestoreHeightByTransactions( - _appStore.wallet!.type, incomingAccountTransactions.first.date); + _restoreHeightByTransactions = + _getRestoreHeightByTransactions(_wallet.type, incomingAccountTransactions.first.date); } } } @@ -54,26 +51,132 @@ abstract class WalletKeysViewModelBase with Store { final ObservableList items; final String title; - + final WalletBase _wallet; final String _walletName; - - AppStore get appStore => _appStore; - final AppStore _appStore; - final int _restoreHeight; int _restoreHeightByTransactions; - void _populateItems() { + AppStore get appStore => _appStore; + + String get seed => _wallet.seed != null ? _wallet.seed! : ''; + + bool get isLegacySeedOnly => + (_wallet.type == WalletType.monero || _wallet.type == WalletType.wownero) && + _wallet.seed != null && + !Polyseed.isValidSeed(_wallet.seed!); + + String get legacySeed { + if ((_wallet.type == WalletType.monero || _wallet.type == WalletType.wownero) && + _wallet.seed != null && + Polyseed.isValidSeed(_wallet.seed!)) { + final langName = PolyseedLang.getByPhrase(_wallet.seed!).nameEnglish; + + if (_wallet.type == WalletType.monero) { + return (_wallet as MoneroWalletBase).seedLegacy(langName); + } else if (_wallet.type == WalletType.wownero) { + return wownero!.getLegacySeed(_wallet, langName); + } + } + return ''; + } + + String get legacyRestoreHeight { + if (_wallet.type == WalletType.monero) { + return monero!.getRestoreHeight(_wallet)?.toString() ?? ''; + } + if (_wallet.type == WalletType.wownero) { + return wownero!.getRestoreHeight(_wallet)?.toString() ?? ''; + } + return ''; + } + + @observable + bool obscurePassphrase = true; + + String get passphrase { + return _wallet.passphrase ?? ''; + } + + /// The Regex split the words based on any whitespace character. + /// + /// Either standard ASCII space (U+0020) or the full-width space character (U+3000) used by the Japanese. + List get seedSplit => seed.isNotEmpty ? seed.split(RegExp(r'\s+')) : []; + + List get legacySeedSplit => legacySeed.isNotEmpty ? legacySeed.split(RegExp(r'\s+')) : []; + + void _populateKeysItems() { items.clear(); - if (_appStore.wallet!.type == WalletType.monero) { - final keys = monero!.getKeys(_appStore.wallet!); + Map? keys; + switch (_wallet.type) { + case WalletType.monero: + keys = monero!.getKeys(_wallet); + break; + case WalletType.haven: + keys = haven!.getKeys(_wallet); + break; + case WalletType.wownero: + keys = wownero!.getKeys(_wallet); + break; + case WalletType.zano: + keys = zano!.getKeys(_wallet); + break; + case WalletType.ethereum: + case WalletType.polygon: + case WalletType.solana: + case WalletType.tron: + items.addAll([ + if (_wallet.privateKey != null) + StandartListItem( + key: ValueKey('${_walletName}_wallet_private_key_item_key'), + title: S.current.private_key, + value: _wallet.privateKey!, + ), + ]); + break; + case WalletType.nano: + case WalletType.banano: + // we always have the hex version of the seed and private key: + items.addAll([ + if (_wallet.hexSeed != null) + StandartListItem( + key: ValueKey('${_walletName}_wallet_hex_seed_key'), + title: S.current.seed_hex_form, + value: _wallet.hexSeed!, + ), + if (_wallet.privateKey != null) + StandartListItem( + key: ValueKey('${_walletName}_wallet_private_key_item_key'), + title: S.current.private_key, + value: _wallet.privateKey!, + ), + ]); + break; + case WalletType.bitcoin: + case WalletType.litecoin: + case WalletType.bitcoinCash: + case WalletType.none: + // final keys = bitcoin!.getWalletKeys(_appStore.wallet!); + // + // items.addAll([ + // if (keys['wif'] != null) + // StandartListItem(title: "WIF", value: keys['wif']!), + // if (keys['privateKey'] != null) + // StandartListItem(title: S.current.private_key, value: keys['privateKey']!), + // if (keys['publicKey'] != null) + // StandartListItem(title: S.current.public_key, value: keys['publicKey']!), + // ]); + break; + } + + if (keys != null) { items.addAll([ if (keys['primaryAddress'] != null) StandartListItem( + key: ValueKey('${_walletName}_wallet_primary_address_item_key'), title: S.current.primary_address, value: keys['primaryAddress']!), if (keys['publicSpendKey'] != null) @@ -100,212 +203,25 @@ abstract class WalletKeysViewModelBase with Store { title: S.current.view_key_private, value: keys['privateViewKey']!, ), - if (_appStore.wallet!.seed!.isNotEmpty) - StandartListItem( - key: ValueKey('${_walletName}_wallet_seed_item_key'), - title: S.current.wallet_seed, - value: _appStore.wallet!.seed!, - ), - ]); - - if (_appStore.wallet?.seed != null && Polyseed.isValidSeed(_appStore.wallet!.seed!)) { - final lang = PolyseedLang.getByPhrase(_appStore.wallet!.seed!); - items.add( - StandartListItem( - key: ValueKey('${_walletName}_wallet_seed_legacy_item_key'), - title: S.current.wallet_seed_legacy, - value: (_appStore.wallet as MoneroWalletBase).seedLegacy(lang.nameEnglish), - ), - ); - } - - final restoreHeight = monero!.getRestoreHeight(_appStore.wallet!); - if (restoreHeight != null) { - items.add( - StandartListItem( - key: ValueKey('${_walletName}_wallet_restore_height_item_key'), - title: S.current.wallet_recovery_height, - value: restoreHeight.toString(), - ), - ); - } - } - - if (_appStore.wallet!.type == WalletType.haven) { - 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'), - title: S.current.spend_key_public, - value: keys['publicSpendKey']!, - ), - if (keys['privateSpendKey'] != null) - StandartListItem( - key: ValueKey('${_walletName}_wallet_private_spend_key_item_key'), - title: S.current.spend_key_private, - value: keys['privateSpendKey']!, - ), - if (keys['publicViewKey'] != null) - StandartListItem( - key: ValueKey('${_walletName}_wallet_public_view_key_item_key'), - title: S.current.view_key_public, - value: keys['publicViewKey']!, - ), - if (keys['privateViewKey'] != null) - StandartListItem( - key: ValueKey('${_walletName}_wallet_private_view_key_item_key'), - title: S.current.view_key_private, - value: keys['privateViewKey']!, - ), - if (_appStore.wallet!.seed!.isNotEmpty) - StandartListItem( - key: ValueKey('${_walletName}_wallet_seed_item_key'), - title: S.current.wallet_seed, - value: _appStore.wallet!.seed!, - ), - ]); - } - - if (_appStore.wallet!.type == WalletType.wownero) { - 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'), - title: S.current.spend_key_public, - value: keys['publicSpendKey']!, - ), - if (keys['privateSpendKey'] != null) - StandartListItem( - key: ValueKey('${_walletName}_wallet_private_spend_key_item_key'), - title: S.current.spend_key_private, - value: keys['privateSpendKey']!, - ), - if (keys['publicViewKey'] != null) - StandartListItem( - key: ValueKey('${_walletName}_wallet_public_view_key_item_key'), - title: S.current.view_key_public, - value: keys['publicViewKey']!, - ), - if (keys['privateViewKey'] != null) - StandartListItem( - key: ValueKey('${_walletName}_wallet_private_view_key_item_key'), - title: S.current.view_key_private, - value: keys['privateViewKey']!, - ), - if (_appStore.wallet!.seed!.isNotEmpty) - StandartListItem( - key: ValueKey('${_walletName}_wallet_seed_item_key'), - title: S.current.wallet_seed, - value: _appStore.wallet!.seed!, - ), - ]); - - if (_appStore.wallet?.seed != null && Polyseed.isValidSeed(_appStore.wallet!.seed!)) { - final lang = PolyseedLang.getByPhrase(_appStore.wallet!.seed!); - items.add( - StandartListItem( - key: ValueKey('${_walletName}_wallet_seed_legacy_item_key'), - title: S.current.wallet_seed_legacy, - value: wownero!.getLegacySeed(_appStore.wallet!, lang.nameEnglish), - ), - ); - } - } - - if (_appStore.wallet!.type == WalletType.bitcoin || - _appStore.wallet!.type == WalletType.litecoin || - _appStore.wallet!.type == WalletType.bitcoinCash) { - // final keys = bitcoin!.getWalletKeys(_appStore.wallet!); - - items.addAll([ - // if (keys['wif'] != null) - // StandartListItem(title: "WIF", value: keys['wif']!), - // if (keys['privateKey'] != null) - // StandartListItem(title: S.current.private_key, value: keys['privateKey']!), - // if (keys['publicKey'] != null) - // StandartListItem(title: S.current.public_key, value: keys['publicKey']!), - StandartListItem( - key: ValueKey('${_walletName}_wallet_seed_item_key'), - title: S.current.wallet_seed, - value: _appStore.wallet!.seed!, - ), - ]); - } - - if (isEVMCompatibleChain(_appStore.wallet!.type) || - _appStore.wallet!.type == WalletType.solana || - _appStore.wallet!.type == WalletType.tron) { - items.addAll([ - if (_appStore.wallet!.privateKey != null) - StandartListItem( - key: ValueKey('${_walletName}_wallet_private_key_item_key'), - title: S.current.private_key, - value: _appStore.wallet!.privateKey!, - ), - if (_appStore.wallet!.seed != null) - StandartListItem( - key: ValueKey('${_walletName}_wallet_seed_item_key'), - title: S.current.wallet_seed, - value: _appStore.wallet!.seed!, - ), - ]); - } - - bool nanoBased = - _appStore.wallet!.type == WalletType.nano || _appStore.wallet!.type == WalletType.banano; - - if (nanoBased) { - // we always have the hex version of the seed and private key: - items.addAll([ - if (_appStore.wallet!.seed != null) - StandartListItem( - key: ValueKey('${_walletName}_wallet_seed_item_key'), - title: S.current.wallet_seed, - value: _appStore.wallet!.seed!, - ), - if (_appStore.wallet!.hexSeed != null) - StandartListItem( - key: ValueKey('${_walletName}_wallet_hex_seed_key'), - title: S.current.seed_hex_form, - value: _appStore.wallet!.hexSeed!, - ), - if (_appStore.wallet!.privateKey != null) - StandartListItem( - key: ValueKey('${_walletName}_wallet_private_key_item_key'), - title: S.current.private_key, - value: _appStore.wallet!.privateKey!, - ), ]); } } Future _currentHeight() async { - if (_appStore.wallet!.type == WalletType.haven) { + if (_wallet.type == WalletType.haven) { return await haven!.getCurrentHeight(); } - if (_appStore.wallet!.type == WalletType.monero) { + if (_wallet.type == WalletType.monero) { return await monero!.getCurrentHeight(); } - if (_appStore.wallet!.type == WalletType.wownero) { + if (_wallet.type == WalletType.wownero) { return await wownero!.getCurrentHeight(); } return null; } String get _scheme { - switch (_appStore.wallet!.type) { + switch (_wallet.type) { case WalletType.monero: return 'monero-wallet'; case WalletType.bitcoin: @@ -330,12 +246,20 @@ abstract class WalletKeysViewModelBase with Store { return 'tron-wallet'; case WalletType.wownero: return 'wownero-wallet'; + case WalletType.zano: + return 'zano-wallet'; default: - throw Exception('Unexpected wallet type: ${_appStore.wallet!.type.toString()}'); + throw Exception('Unexpected wallet type: ${_wallet.type.toString()}'); } } Future get restoreHeight async { + if (_wallet.type == WalletType.monero) { + return monero!.getRestoreHeight(_wallet)?.toString(); + } + if (_wallet.type == WalletType.wownero) { + return wownero!.getRestoreHeight(_wallet)?.toString(); + } if (_restoreHeightByTransactions != 0) return getRoundedRestoreHeight(_restoreHeightByTransactions); if (_restoreHeight != 0) return _restoreHeight.toString(); @@ -349,19 +273,26 @@ abstract class WalletKeysViewModelBase with Store { Future> get _queryParams async { final restoreHeightResult = await restoreHeight; return { - if (_appStore.wallet!.seed != null) 'seed': _appStore.wallet!.seed!, - if (_appStore.wallet!.seed == null && _appStore.wallet!.hexSeed != null) - 'hexSeed': _appStore.wallet!.hexSeed!, - if (_appStore.wallet!.seed == null && _appStore.wallet!.privateKey != null) - 'private_key': _appStore.wallet!.privateKey!, + if (_wallet.seed != null) 'seed': _wallet.seed!, + if (_wallet.seed == null && _wallet.hexSeed != null) 'hexSeed': _wallet.hexSeed!, + if (_wallet.seed == null && _wallet.privateKey != null) 'private_key': _wallet.privateKey!, if (restoreHeightResult != null) ...{'height': restoreHeightResult}, - if (_appStore.wallet!.passphrase != null) 'passphrase': _appStore.wallet!.passphrase! + if (_wallet.passphrase != null) 'passphrase': _wallet.passphrase! }; } - Future get url async => Uri( + Future> get _queryParamsForLegacy async { + final restoreHeightResult = await restoreHeight; + return { + if (legacySeed.isNotEmpty) 'seed': legacySeed, + if (restoreHeightResult != null) ...{'height': restoreHeightResult}, + if ((_wallet.passphrase ?? '') != '') 'passphrase': _wallet.passphrase! + }; + } + + Future getUrl(bool isLegacySeed) async => Uri( scheme: _scheme, - queryParameters: await _queryParams, + queryParameters: isLegacySeed ? await _queryParamsForLegacy : await _queryParams, ); List _getWalletTransactions(WalletBase wallet) { diff --git a/lib/view_model/wallet_new_vm.dart b/lib/view_model/wallet_new_vm.dart index be30811d9..aa933eadc 100644 --- a/lib/view_model/wallet_new_vm.dart +++ b/lib/view_model/wallet_new_vm.dart @@ -1,5 +1,13 @@ import 'package:cake_wallet/core/new_wallet_arguments.dart'; import 'package:cake_wallet/ethereum/ethereum.dart'; +import 'package:cake_wallet/zano/zano.dart'; +import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart'; +import 'package:cake_wallet/solana/solana.dart'; +import 'package:cake_wallet/tron/tron.dart'; +import 'package:cake_wallet/wownero/wownero.dart'; +import 'package:cake_wallet/zano/zano.dart'; +import 'package:hive/hive.dart'; +import 'package:mobx/mobx.dart'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart'; import 'package:cake_wallet/core/wallet_creation_service.dart'; @@ -65,11 +73,16 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store { ? advancedPrivacySettingsViewModel.seedPhraseLength.value : 24; case WalletType.nano: + case WalletType.banano: return seedSettingsViewModel.nanoSeedType == NanoSeedType.bip39 ? advancedPrivacySettingsViewModel.seedPhraseLength.value : 24; - default: + case WalletType.none: return 24; + case WalletType.haven: + return 25; + case WalletType.zano: + return 26; } } @@ -87,6 +100,7 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store { name: name, language: options!.first as String, password: walletPassword, + passphrase: passphrase, isPolyseed: options.last as bool); case WalletType.bitcoin: case WalletType.litecoin: @@ -152,9 +166,16 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store { case WalletType.wownero: return wownero!.createWowneroNewWalletCredentials( name: name, - password: walletPassword, language: options!.first as String, isPolyseed: options.last as bool, + password: walletPassword, + passphrase: passphrase, + ); + case WalletType.zano: + return zano!.createZanoNewWalletCredentials( + name: name, + password: walletPassword, + passphrase: passphrase, ); case WalletType.none: throw Exception('Unexpected type: ${type.toString()}'); diff --git a/lib/view_model/wallet_restore_view_model.dart b/lib/view_model/wallet_restore_view_model.dart index d37b69f74..8a497a605 100644 --- a/lib/view_model/wallet_restore_view_model.dart +++ b/lib/view_model/wallet_restore_view_model.dart @@ -15,6 +15,7 @@ import 'package:cake_wallet/view_model/restore/restore_mode.dart'; import 'package:cake_wallet/view_model/seed_settings_view_model.dart'; import 'package:cake_wallet/view_model/wallet_creation_vm.dart'; import 'package:cake_wallet/wownero/wownero.dart'; +import 'package:cake_wallet/zano/zano.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_credentials.dart'; import 'package:cw_core/wallet_info.dart'; @@ -61,6 +62,7 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { case WalletType.bitcoin: case WalletType.litecoin: case WalletType.bitcoinCash: + case WalletType.zano: case WalletType.none: availableModes = [WalletRestoreMode.seed]; break; @@ -70,8 +72,6 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { } static const moneroSeedMnemonicLength = 25; - static const electrumSeedMnemonicLength = 24; - static const electrumShortSeedMnemonicLength = 12; late List availableModes; final bool hasSeedLanguageSelector; @@ -97,7 +97,7 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { switch (type) { case WalletType.monero: return monero!.createMoneroRestoreWalletFromSeedCredentials( - name: name, height: height, mnemonic: seed, password: password); + name: name, height: height, mnemonic: seed, password: password, passphrase: passphrase??''); case WalletType.bitcoin: case WalletType.litecoin: return bitcoin!.createBitcoinRestoreWalletFromSeedCredentials( @@ -160,8 +160,16 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { name: name, mnemonic: seed, password: password, + passphrase: passphrase??'', height: height, ); + case WalletType.zano: + return zano!.createZanoRestoreWalletFromSeedCredentials( + name: name, + password: password, + height: height, + passphrase: passphrase??'', + mnemonic: seed); case WalletType.none: break; } diff --git a/lib/wownero/cw_wownero.dart b/lib/wownero/cw_wownero.dart index 0e0b00fd4..e20b6fbbf 100644 --- a/lib/wownero/cw_wownero.dart +++ b/lib/wownero/cw_wownero.dart @@ -222,19 +222,21 @@ class CWWownero extends Wownero { WalletCredentials createWowneroRestoreWalletFromSeedCredentials( {required String name, required String password, + required String passphrase, required int height, required String mnemonic}) => WowneroRestoreWalletFromSeedCredentials( - name: name, password: password, height: height, mnemonic: mnemonic); + name: name, password: password, passphrase: passphrase, height: height, mnemonic: mnemonic); @override WalletCredentials createWowneroNewWalletCredentials( {required String name, required String language, required bool isPolyseed, - String? password}) => + String? password, + String? passphrase}) => WowneroNewWalletCredentials( - name: name, password: password, language: language, isPolyseed: isPolyseed); + name: name, password: password, language: language, isPolyseed: isPolyseed, passphrase: passphrase); @override Map getKeys(Object wallet) { @@ -244,10 +246,17 @@ class CWWownero extends Wownero { 'privateSpendKey': keys.privateSpendKey, 'privateViewKey': keys.privateViewKey, 'publicSpendKey': keys.publicSpendKey, - 'publicViewKey': keys.publicViewKey + 'publicViewKey': keys.publicViewKey, + 'passphrase': keys.passphrase }; } + @override + int? getRestoreHeight(Object wallet) { + final wowneroWallet = wallet as WowneroWallet; + return wowneroWallet.restoreHeight; + } + @override Object createWowneroTransactionCreationCredentials( {required List outputs, required TransactionPriority priority}) => diff --git a/lib/zano/cw_zano.dart b/lib/zano/cw_zano.dart new file mode 100644 index 000000000..19fec04e4 --- /dev/null +++ b/lib/zano/cw_zano.dart @@ -0,0 +1,134 @@ +part of 'zano.dart'; + +class CWZano extends Zano { + + List getZanoAssets(WalletBase wallet) => (wallet as ZanoWallet).zanoAssets.values.toList(); + + @override + Future addZanoAssetById(WalletBase wallet, String assetId) async => await (wallet as ZanoWallet).addZanoAssetById(assetId); + + @override + Future changeZanoAssetAvailability(WalletBase wallet, CryptoCurrency token) async => await (wallet as ZanoWallet).changeZanoAssetAvailability(token as ZanoAsset); + + @override + Future deleteZanoAsset(WalletBase wallet, CryptoCurrency token) async => await (wallet as ZanoWallet).deleteZanoAsset(token as ZanoAsset); + + @override + Future getZanoAsset(WalletBase wallet, String assetId) async { + final zanoWallet = wallet as ZanoWallet; + return await zanoWallet.getZanoAsset(assetId); + } + + // @override + // TransactionHistoryBase getTransactionHistory(Object wallet) { + // final zanoWallet = wallet as ZanoWallet; + // return zanoWallet.transactionHistory; + // } + + @override + TransactionPriority getDefaultTransactionPriority() { + return MoneroTransactionPriority.automatic; + } + + @override + TransactionPriority deserializeMoneroTransactionPriority({required int raw}) { + return MoneroTransactionPriority.deserialize(raw: raw); + } + + @override + List getTransactionPriorities() { + return MoneroTransactionPriority.all; + } + + @override + List getWordList(String language) { + assert(language.toLowerCase() == LanguageList.english.toLowerCase()); + return EnglishMnemonics.words; + } + + @override + WalletCredentials createZanoRestoreWalletFromSeedCredentials( + {required String name, required String password, required int height, required String passphrase, required String mnemonic}) { + return ZanoRestoreWalletFromSeedCredentials(name: name, password: password, passphrase: passphrase, height: height, mnemonic: mnemonic); + } + + @override + WalletCredentials createZanoNewWalletCredentials({required String name, required String? password, required String? passphrase}) { + return ZanoNewWalletCredentials(name: name, password: password, passphrase: passphrase); + } + + @override + Map getKeys(Object wallet) { + final zanoWallet = wallet as ZanoWallet; + final keys = zanoWallet.keys; + return { + 'privateSpendKey': keys.privateSpendKey, + 'privateViewKey': keys.privateViewKey, + 'publicSpendKey': keys.publicSpendKey, + 'publicViewKey': keys.publicViewKey + }; + } + + @override + Object createZanoTransactionCredentials({required List outputs, required TransactionPriority priority, required CryptoCurrency currency}) { + return ZanoTransactionCredentials( + outputs: outputs + .map((out) => OutputInfo( + fiatAmount: out.fiatAmount, + cryptoAmount: out.cryptoAmount, + address: out.address, + note: out.note, + sendAll: out.sendAll, + extractedAddress: out.extractedAddress, + isParsedAddress: out.isParsedAddress, + formattedCryptoAmount: out.formattedCryptoAmount)) + .toList(), + priority: priority as MoneroTransactionPriority, + currency: currency, + ); + } + + @override + double formatterIntAmountToDouble({required int amount, required CryptoCurrency currency, required bool forFee}) { + // fee always counted in zano with default decimal points + if (forFee) return ZanoFormatter.intAmountToDouble(amount); + if (currency is ZanoAsset) return ZanoFormatter.intAmountToDouble(amount, currency.decimalPoint); + return ZanoFormatter.intAmountToDouble(amount); + } + + @override + int formatterParseAmount({required String amount, required CryptoCurrency currency}) { + if (currency is ZanoAsset) return ZanoFormatter.parseAmount(amount, currency.decimalPoint); + return ZanoFormatter.parseAmount(amount); + } + + // @override + // int getTransactionInfoAccountId(TransactionInfo tx) { + // final zanoTransactionInfo = tx as ZanoTransactionInfo; + // return zanoTransactionInfo.accountIndex; + // } + + @override + WalletService createZanoWalletService(Box walletInfoSource) { + return ZanoWalletService(walletInfoSource); + } + + @override + CryptoCurrency? assetOfTransaction(WalletBase wallet, TransactionInfo transaction) { + transaction as ZanoTransactionInfo; + if (transaction.tokenSymbol == CryptoCurrency.zano.title) { + return CryptoCurrency.zano; + } + wallet as ZanoWallet; + final asset = wallet.zanoAssets.values.firstWhereOrNull((element) => element?.ticker == transaction.tokenSymbol); + return asset; + } + + String getZanoAssetAddress(CryptoCurrency asset) => (asset as ZanoAsset).assetId; + + @override + String getAddress(WalletBase wallet) => (wallet as ZanoWallet).walletAddresses.address; + + @override + bool validateAddress(String address) => ZanoUtils.validateAddress(address); +} diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt index bfce34c34..0b6f32fd6 100644 --- a/linux/CMakeLists.txt +++ b/linux/CMakeLists.txt @@ -107,10 +107,19 @@ install(CODE " set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") -install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/../scripts/monero_c/release/monero/x86_64-linux-gnu_libwallet2_api_c.so" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" RENAME "monero_libwallet2_api_c.so" +if(CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64") + set(LIB_TRIPLET "aarch64-linux-gnu") +else() + set(LIB_TRIPLET "x86_64-linux-gnu") +endif() + +install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/../scripts/monero_c/release/monero/${LIB_TRIPLET}_libwallet2_api_c.so" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" RENAME "monero_libwallet2_api_c.so" COMPONENT Runtime) -install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/../scripts/monero_c/release/wownero/x86_64-linux-gnu_libwallet2_api_c.so" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" RENAME "wownero_libwallet2_api_c.so" +install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/../scripts/monero_c/release/wownero/${LIB_TRIPLET}_libwallet2_api_c.so" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" RENAME "wownero_libwallet2_api_c.so" + COMPONENT Runtime) + +install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/../scripts/monero_c/release/zano/${LIB_TRIPLET}_libwallet2_api_c.so" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" RENAME "zano_libwallet2_api_c.so" COMPONENT Runtime) install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" diff --git a/model_generator.sh b/model_generator.sh index 1443b0fc9..f3950e2b1 100755 --- a/model_generator.sh +++ b/model_generator.sh @@ -1,7 +1,7 @@ #!/bin/bash set -x -e -for cwcoin in cw_{core,evm,monero,bitcoin,haven,nano,bitcoin_cash,solana,tron,wownero} +for cwcoin in cw_{core,evm,monero,bitcoin,haven,nano,bitcoin_cash,solana,tron,wownero,zano} do if [[ "x$1" == "xasync" ]]; then @@ -10,7 +10,7 @@ do bash -c "cd $cwcoin; flutter pub get; dart run build_runner build --delete-conflicting-outputs; cd .." fi done -for cwcoin in cw_{polygon,ethereum,mwebd}; +for cwcoin in cw_{polygon,ethereum,mweb}; do if [[ "x$1" == "xasync" ]]; then diff --git a/pubspec_base.yaml b/pubspec_base.yaml index be1dba134..1f13af1df 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -17,7 +17,7 @@ dependencies: fast_scanner: git: url: https://github.com/MrCyjaneK/fast_scanner - ref: c5a08720216a508bf1fe3d062ad19d2836545a42 + ref: 69b3276b090fa6ac01b4483ca3adca93a8e615be http: ^1.1.0 path_provider: ^2.0.11 mobx: ^2.1.4 @@ -184,6 +184,7 @@ flutter: - assets/solana_node_list.yml - assets/tron_node_list.yml - assets/wownero_node_list.yml + - assets/zano_node_list.yml - assets/text/ - assets/faq/ - assets/animation/ diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index 6627898cc..4c52b74e9 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -84,6 +84,7 @@ "bitcoin_dark_theme": "موضوع البيتكوين الظلام", "bitcoin_light_theme": "موضوع البيتكوين الخفيفة", "bitcoin_payments_require_1_confirmation": "تتطلب مدفوعات Bitcoin تأكيدًا واحدًا ، والذي قد يستغرق 20 دقيقة أو أكثر. شكرا لصبرك! سيتم إرسال بريد إلكتروني إليك عند تأكيد الدفع.", + "block_height": "ارتفاع كتلة", "block_remaining": "1 كتلة متبقية", "Blocks_remaining": "بلوك متبقي ${status}", "bluetooth": "بلوتوث", @@ -131,6 +132,7 @@ "change_rep": "ﺏﻭﺪﻨﻣ ﺮﻴﻴﻐﺗ", "change_rep_message": "؟ﻦﻴﻠﺜﻤﻤﻟﺍ ﺮﻴﻴﻐﺗ ﺪﻳﺮﺗ ﻚﻧﺃ ﺪﻛﺄﺘﻣ ﺖﻧﺃ ﻞﻫ", "change_rep_successful": "تم تغيير ممثل بنجاح", + "change_selected_exchanges": "تغيير التبادلات المحددة", "change_wallet_alert_content": "هل تريد تغيير المحفظة الحالية إلى ${wallet_name}؟", "change_wallet_alert_title": "تغيير المحفظة الحالية", "choose_a_payment_method": "اختر طريقة الدفع", @@ -296,7 +298,7 @@ "etherscan_history": "Etherscan تاريخ", "event": "ﺙﺪﺣ", "events": "ﺙﺍﺪﺣﻷﺍ", - "exchange": "تبديل", + "exchange": "تبادل", "exchange_incorrect_current_wallet_for_xmr": "إذا كنت ترغب في تبديل XMR من رصيد محفظة الكعكة ، فيرجى التبديل إلى محفظة Monero أولاً.", "exchange_new_template": "قالب جديد", "exchange_provider_unsupported": "${providerName} لم يعد مدعومًا!", @@ -382,6 +384,7 @@ "invalid_password": "رمز مرور خاطئ", "invoice_details": "تفاصيل الفاتورة", "is_percentage": "يكون", + "keys": "مفاتيح", "last_30_days": "آخر 30 يومًا", "learn_more": "اعرف المزيد", "ledger_connection_error": "فشل في الاتصال بك دفتر الأستاذ. حاول مرة اخرى.", @@ -389,6 +392,7 @@ "ledger_error_tx_rejected_by_user": "تم رفض المعاملة على الجهاز", "ledger_error_wrong_app": "يرجى التأكد", "ledger_please_enable_bluetooth": "يرجى تمكين البلوتوث للكشف عن دفتر الأستاذ الخاص بك", + "legacy": "إرث", "light_theme": "فاتح", "litecoin_enable_mweb_sync": "تمكين MWEB المسح الضوئي", "litecoin_mweb": "mweb", @@ -667,6 +671,7 @@ "select_backup_file": "حدد ملف النسخ الاحتياطي", "select_buy_provider_notice": "حدد مزود شراء أعلاه. يمكنك تخطي هذه الشاشة عن طريق تعيين مزود شراء الافتراضي في إعدادات التطبيق.", "select_destination": ".ﻲﻃﺎﻴﺘﺣﻻﺍ ﺦﺴﻨﻟﺍ ﻒﻠﻣ ﺔﻬﺟﻭ ﺪﻳﺪﺤﺗ ءﺎﺟﺮﻟﺍ", + "select_hw_account_below": "الرجاء تحديد حساب الاستعادة أدناه:", "select_sell_provider_notice": ".ﻖﻴﺒﻄﺘﻟﺍ ﺕﺍﺩﺍﺪﻋﺇ ﻲﻓ ﻚﺑ ﺹﺎﺨﻟﺍ ﻲﺿﺍﺮﺘﻓﻻﺍ ﻊﻴﺒﻟﺍ ﺩﻭﺰﻣ ﻦﻴﻴﻌﺗ ﻖﻳﺮﻃ ﻦﻋ ﺔﺷﺎﺸﻟﺍ ﻩﺬﻫ ﻲﻄﺨﺗ", "select_your_country": "الرجاء تحديد بلدك", "sell": "بيع", @@ -697,6 +702,7 @@ "service_health_disabled": "تم تعطيل نشرة صحة الخدمة", "service_health_disabled_message": "هذه هي صفحة نشرة صحة الخدمة ، يمكنك تمكين هذه الصفحة ضمن الإعدادات -> الخصوصية", "set_a_pin": "تعيين دبوس", + "set_up_a_wallet": "قم بإعداد محفظة", "settings": "إعدادات", "settings_all": "الكل", "settings_allow_biometrical_authentication": "السماح بالمصادقة البيومترية", @@ -732,7 +738,7 @@ "share_address": "شارك العنوان", "shared_seed_wallet_groups": "مجموعات محفظة البذور المشتركة", "show": "يعرض", - "show_address_book_popup": "عرض \"إضافة إلى كتاب العناوين\" المنبثقة بعد الإرسال", + "show_address_book_popup": "عرض دفتر العناوين المنبثقة", "show_balance": "اضغط لفترة طويلة لإظهار التوازن", "show_balance_toast": "اضغط لفترة طويلة لإخفاء أو إظهار التوازن", "show_details": "اظهر التفاصيل", @@ -781,6 +787,7 @@ "support_title_guides": "مستندات محفظة كعكة", "support_title_live_chat": "الدعم المباشر", "support_title_other_links": "روابط دعم أخرى", + "swap": "تبديل", "sweeping_wallet": "كنس المحفظة", "sweeping_wallet_alert": "لن يستغرق هذا وقتًا طويلاً. لا تترك هذه الشاشة وإلا فقد يتم فقد أموال سويبت", "switchToETHWallet": "ﻯﺮﺧﺃ ﺓﺮﻣ ﺔﻟﻭﺎﺤﻤﻟﺍﻭ Ethereum ﺔﻈﻔﺤﻣ ﻰﻟﺇ ﻞﻳﺪﺒﺘﻟﺍ ﻰﺟﺮﻳ", @@ -806,6 +813,7 @@ "testnet_coins_no_value": "عملات TestNet ليس لها قيمة", "third_intro_content": "يعيش Yats خارج Cake Wallet أيضًا. يمكن استبدال أي عنوان محفظة على وجه الأرض بـ Yat!", "third_intro_title": "يتماشي Yat بلطف مع الآخرين", + "this_pair_is_not_supported_warning": "لا يتم دعم هذا الزوج مع البورصة (البورصة) المحددة حاليًا. الرجاء تحديد تبادل آخر.", "thorchain_contract_address_not_supported": "لا يدعم Thorchain الإرسال إلى عنوان العقد", "thorchain_taproot_address_not_supported": "لا يدعم مزود Thorchain عناوين Taproot. يرجى تغيير العنوان أو تحديد مزود مختلف.", "time": "${minutes}د ${seconds}س", @@ -929,10 +937,13 @@ "waitFewSecondForTxUpdate": "ﺕﻼﻣﺎﻌﻤﻟﺍ ﻞﺠﺳ ﻲﻓ ﺔﻠﻣﺎﻌﻤﻟﺍ ﺲﻜﻌﻨﺗ ﻰﺘﺣ ﻥﺍﻮﺛ ﻊﻀﺒﻟ ﺭﺎﻈﺘﻧﻻﺍ ﻰﺟﺮﻳ", "wallet": "محفظة", "wallet_group": "مجموعة محفظة", + "wallet_group_description_existing_seed": "لقد اخترت استخدام بذرة موجودة لهذه المحفظة. يمكنك التحقق من البذرة مرة أخرى إذا كنت بحاجة إلى تأكيدها أو كتابتها.", "wallet_group_description_four": "لإنشاء محفظة مع بذرة جديدة تماما.", "wallet_group_description_one": "في محفظة الكيك ، يمكنك إنشاء ملف", + "wallet_group_description_open_wallet": "خلاف ذلك ، يمكنك الاستمرار في فتح المحفظة", "wallet_group_description_three": "لرؤية المحافظ المتاحة و/أو شاشة مجموعات المحفظة. أو اختر", "wallet_group_description_two": "عن طريق اختيار محفظة موجودة لتبادل البذور مع. يمكن أن تحتوي كل مجموعة محفظة على محفظة واحدة من كل نوع من العملة. \n\n يمكنك تحديدها", + "wallet_group_description_view_seed": "يمكنك دائمًا عرض هذه البذرة مرة أخرى تحت", "wallet_group_empty_state_text_one": "يبدو أنه ليس لديك أي مجموعات محفظة متوافقة !\n\n انقر", "wallet_group_empty_state_text_two": "أدناه لجعل واحدة جديدة.", "wallet_keys": "سييد المحفظة / المفاتيح", diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index 6d92a5d45..72f0fb2f2 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -84,6 +84,7 @@ "bitcoin_dark_theme": "Тъмна тема за биткойн", "bitcoin_light_theme": "Лека биткойн тема", "bitcoin_payments_require_1_confirmation": "Плащанията с Bitcoin изискват потвърждение, което може да отнеме 20 минути или повече. Благодарим за търпението! Ще получите имейл, когато плащането е потвърдено.", + "block_height": "Височина на блока", "block_remaining": "1 блок останал", "Blocks_remaining": "${status} оставащи блока", "bluetooth": "Bluetooth", @@ -131,6 +132,7 @@ "change_rep": "Смяна на представител", "change_rep_message": "Сигурни ли сте, че искате да смените представителите?", "change_rep_successful": "Успешно промени представител", + "change_selected_exchanges": "Променете избраните борси", "change_wallet_alert_content": "Искате ли да смените сегашния портфейл на ${wallet_name}?", "change_wallet_alert_title": "Смяна на сегашния портфейл", "choose_a_payment_method": "Изберете начин на плащане", @@ -296,7 +298,7 @@ "etherscan_history": "История на Etherscan", "event": "Събитие", "events": "събития", - "exchange": "Разметка", + "exchange": "Обмен", "exchange_incorrect_current_wallet_for_xmr": "Ако искате да смените XMR от вашия баланс на портфейла на тортата Monero, моля, преминете първо към вашия портфейл Monero.", "exchange_new_template": "Нов шаблон", "exchange_provider_unsupported": "${providerName} вече не се поддържа!", @@ -382,6 +384,7 @@ "invalid_password": "Невалидна парола", "invoice_details": "IДанни за фактура", "is_percentage": "е", + "keys": "Клавиши", "last_30_days": "Последните 30 дни", "learn_more": "Научете още", "ledger_connection_error": "Не успя да се свърже с вашата книга. Моля, опитайте отново.", @@ -389,6 +392,7 @@ "ledger_error_tx_rejected_by_user": "Транзакция, отхвърлена на устройство", "ledger_error_wrong_app": "Моля, уверете се, че сте отворили правилното приложение на вашата книга", "ledger_please_enable_bluetooth": "Моля, активирайте Bluetooth да открие вашата книга", + "legacy": "Наследство", "light_theme": "Светло", "litecoin_enable_mweb_sync": "Активирайте сканирането на MWeb", "litecoin_mweb": "Mweb", @@ -667,6 +671,7 @@ "select_backup_file": "Избор на резервно копие", "select_buy_provider_notice": "Изберете доставчик на покупка по -горе. Можете да пропуснете този екран, като зададете вашия доставчик по подразбиране по подразбиране в настройките на приложението.", "select_destination": "Моля, изберете дестинация за архивния файл.", + "select_hw_account_below": "Моля, изберете кой акаунт да възстановите по -долу:", "select_sell_provider_notice": "Изберете доставчик на продажба по-горе. Можете да пропуснете този екран, като зададете своя доставчик на продажба по подразбиране в настройките на приложението.", "select_your_country": "Моля, изберете вашата страна", "sell": "Продаване", @@ -697,6 +702,7 @@ "service_health_disabled": "Service Health Bulletin е деактивиран", "service_health_disabled_message": "Това е страницата на Bulletin на Service Health, можете да активирате тази страница в Настройки -> Поверителност", "set_a_pin": "Поставете щифт", + "set_up_a_wallet": "Настройте портфейл", "settings": "Настройки", "settings_all": "Всичко", "settings_allow_biometrical_authentication": "Позволяване на биометрично удостоверяване.", @@ -732,7 +738,7 @@ "share_address": "Сподели адрес", "shared_seed_wallet_groups": "Споделени групи за портфейли за семена", "show": "Показване", - "show_address_book_popup": "Показване на изскачането на „Добавяне към адресната книга“ след изпращане", + "show_address_book_popup": "Показване на изскачащ прозорец на адресна книга", "show_balance": "Дълго натиснете, за да покажете баланса", "show_balance_toast": "Дълго натискане, за да се скрие или покаже баланс", "show_details": "Показване на подробностите", @@ -781,6 +787,7 @@ "support_title_guides": "Документи за портфейл за торта", "support_title_live_chat": "Подкрепа на живо", "support_title_other_links": "Други връзки за поддръжка", + "swap": "Разметка", "sweeping_wallet": "Метещ портфейл", "sweeping_wallet_alert": "Това не трябва да отнема много време. Не оставяйте този екран или пометените средства могат да бъдат загубени.", "switchToETHWallet": "Моля, преминете към портфейл Ethereum и опитайте отново", @@ -806,6 +813,7 @@ "testnet_coins_no_value": "Тестовите монети нямат стойност", "third_intro_content": "Yats също живее извън Cake Wallet. Всеки адрес на портфейл може да бъде заменен с Yat!", "third_intro_title": "Yat добре се сработва с други", + "this_pair_is_not_supported_warning": "Тази двойка не се поддържа с избраната в момента борса (ите). Моля, изберете друга размяна.", "thorchain_contract_address_not_supported": "Thorchain не подкрепя изпращането до адрес на договор", "thorchain_taproot_address_not_supported": "Доставчикът на Thorchain не поддържа адреси на TapRoot. Моля, променете адреса или изберете друг доставчик.", "time": "${minutes} мин ${seconds} сек", @@ -929,10 +937,13 @@ "waitFewSecondForTxUpdate": "Моля, изчакайте няколко секунди, докато транзакцията се отрази в историята на транзакциите", "wallet": "Портфейл", "wallet_group": "Група на портфейла", + "wallet_group_description_existing_seed": "Вие сте избрали да използвате съществуващо семе за този портфейл. Можете да проверите отново семето, ако трябва да го потвърдите или запишете.", "wallet_group_description_four": "За да създадете портфейл с изцяло ново семе.", "wallet_group_description_one": "В портфейла за торта можете да създадете a", + "wallet_group_description_open_wallet": "В противен случай можете да продължите да отваряте портфейла", "wallet_group_description_three": "За да видите наличния екран за портфейли и/или групи за портфейли. Или изберете", "wallet_group_description_two": "Чрез избора на съществуващ портфейл, с който да споделите семе. Всяка група за портфейл може да съдържа по един портфейл от всеки тип валута. \n\n Можете да изберете", + "wallet_group_description_view_seed": "Винаги можете да видите това семе отново под", "wallet_group_empty_state_text_one": "Изглежда, че нямате съвместими групи портфейли !\n\n tap", "wallet_group_empty_state_text_two": "по -долу, за да се направи нов.", "wallet_keys": "Seed/keys на портфейла", diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index c41181f56..0bb4bb5a3 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -84,6 +84,7 @@ "bitcoin_dark_theme": "Tmavé téma bitcoinů", "bitcoin_light_theme": "Světlé téma bitcoinů", "bitcoin_payments_require_1_confirmation": "U plateb Bitcoinem je vyžadováno alespoň 1 potvrzení, což může trvat 20 minut i déle. Děkujeme za vaši trpělivost! Až bude platba potvrzena, budete informováni e-mailem.", + "block_height": "Výška bloku", "block_remaining": "1 blok zbývající", "Blocks_remaining": "Zbývá ${status} bloků", "bluetooth": "Bluetooth", @@ -131,6 +132,7 @@ "change_rep": "Změna zástupce", "change_rep_message": "Jste si jisti, že chcete změnit zástupce?", "change_rep_successful": "Úspěšně změnil zástupce", + "change_selected_exchanges": "Změnit vybrané výměny", "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", @@ -296,7 +298,7 @@ "etherscan_history": "Historie Etherscanu", "event": "událost", "events": "Události", - "exchange": "Swap", + "exchange": "Výměna", "exchange_incorrect_current_wallet_for_xmr": "Pokud chcete vyměnit XMR z vaší dortové peněženky Monero Balance, nejprve přepněte na peněženku Monero.", "exchange_new_template": "Nová šablona", "exchange_provider_unsupported": "${providerName} již není podporováno!", @@ -382,6 +384,7 @@ "invalid_password": "Neplatné heslo", "invoice_details": "detaily faktury", "is_percentage": "je", + "keys": "Klíče", "last_30_days": "Posledních 30 dnů", "learn_more": "Zjistit více", "ledger_connection_error": "Nepodařilo se připojit k vaší knize. Prosím zkuste to znovu.", @@ -389,6 +392,7 @@ "ledger_error_tx_rejected_by_user": "Transakce zamítnuta na zařízení", "ledger_error_wrong_app": "Ujistěte se, že se na své knize otevřete správnou aplikaci", "ledger_please_enable_bluetooth": "Umožněte prosím Bluetooth detekovat vaši knihu", + "legacy": "Dědictví", "light_theme": "Světlý", "litecoin_enable_mweb_sync": "Povolit skenování MWeb", "litecoin_mweb": "MWeb", @@ -667,6 +671,7 @@ "select_backup_file": "Vybrat soubor se zálohou", "select_buy_provider_notice": "Vyberte výše uvedeného poskytovatele nákupu. Tuto obrazovku můžete přeskočit nastavením výchozího poskytovatele nákupu v nastavení aplikace.", "select_destination": "Vyberte cíl pro záložní soubor.", + "select_hw_account_below": "Níže vyberte, který účet chcete obnovit:", "select_sell_provider_notice": "Výše vyberte poskytovatele prodeje. Tuto obrazovku můžete přeskočit nastavením výchozího poskytovatele prodeje v nastavení aplikace.", "select_your_country": "Vyberte prosím svou zemi", "sell": "Prodat", @@ -697,6 +702,7 @@ "service_health_disabled": "Bulletin zdraví služeb je deaktivován", "service_health_disabled_message": "Toto je stránka Bulletin Service Health Bulletin, můžete tuto stránku povolit v rámci nastavení -> Ochrana osobních údajů", "set_a_pin": "Nastavte špendlík", + "set_up_a_wallet": "Nastavte peněženku", "settings": "Nastavení", "settings_all": "VŠE", "settings_allow_biometrical_authentication": "Povolit biometrické ověření", @@ -732,7 +738,7 @@ "share_address": "Sdílet adresu", "shared_seed_wallet_groups": "Skupiny sdílených semen", "show": "Show", - "show_address_book_popup": "Po odeslání zobrazíte vyskakovací okno „Přidat do adresáře“", + "show_address_book_popup": "Zobrazit vyskakovací okno", "show_balance": "Dlouhý stisknutí zobrazí rovnováhu", "show_balance_toast": "Dlouhý stiskněte pro skrytí nebo zobrazení rovnováhy", "show_details": "Zobrazit detaily", @@ -781,6 +787,7 @@ "support_title_guides": "Dokumenty peněženky dortu", "support_title_live_chat": "Živá podpora", "support_title_other_links": "Další odkazy na podporu", + "swap": "Swap", "sweeping_wallet": "Zametací peněženka", "sweeping_wallet_alert": "To by nemělo trvat dlouho. Nenechávejte tuto obrazovku, jinak mohou být ztraceny prostředky.", "switchToETHWallet": "Přejděte na peněženku Ethereum a zkuste to znovu", @@ -806,6 +813,7 @@ "testnet_coins_no_value": "Mince TestNet nemají žádnou hodnotu", "third_intro_content": "Yat existuje i mimo Cake Wallet. Jakákoliv adresa peněženky na světě může být nahrazena Yatem!", "third_intro_title": "Yat dobře spolupracuje s ostatními", + "this_pair_is_not_supported_warning": "Tento pár není podporován aktuálně vybranými burzami. Vyberte jinou výměnu.", "thorchain_contract_address_not_supported": "Thorchain nepodporuje odeslání na adresu smlouvy", "thorchain_taproot_address_not_supported": "Poskytovatel Thorchain nepodporuje adresy Taproot. Změňte adresu nebo vyberte jiného poskytovatele.", "time": "${minutes}m ${seconds}s", @@ -929,10 +937,13 @@ "waitFewSecondForTxUpdate": "Počkejte několik sekund, než se transakce projeví v historii transakcí", "wallet": "Peněženka", "wallet_group": "Skupina peněženky", + "wallet_group_description_existing_seed": "Rozhodli jste se použít existující semeno pro tuto peněženku. Semeno můžete znovu ověřit, pokud potřebujete potvrdit nebo zapisovat.", "wallet_group_description_four": "Vytvoření peněženky s zcela novým semenem.", "wallet_group_description_one": "V peněžence dortu můžete vytvořit a", + "wallet_group_description_open_wallet": "Jinak můžete pokračovat v otevírání peněženky", "wallet_group_description_three": "Chcete -li zobrazit dostupnou obrazovku Skupina skupin peněženek a/nebo skupin peněženek. Nebo zvolit", "wallet_group_description_two": "Výběrem existující peněženky pro sdílení semeno. Každá skupina peněženek může obsahovat jednu peněženku každého typu měny. \n\n Můžete si vybrat", + "wallet_group_description_view_seed": "Toto semeno si můžete vždy znovu prohlédnout", "wallet_group_empty_state_text_one": "Vypadá to, že nemáte žádné kompatibilní skupiny peněženky !\n\n", "wallet_group_empty_state_text_two": "Níže vytvořit nový.", "wallet_keys": "Seed/klíče peněženky", diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 1c4151ac1..fe6053d23 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -18,7 +18,7 @@ "add_receiver": "Fügen Sie einen weiteren Empfänger hinzu (optional)", "add_secret_code": "Oder fügen Sie diesen Geheimcode einer Authentifizierungs-App hinzu", "add_tip": "Tipp hinzufügen", - "add_token_disclaimer_check": "Ich habe die Adresse und Informationen zum Token-Vertrag anhand einer seriösen Quelle bestätigt. Das Hinzufügen böswilliger oder falscher Informationen kann zu einem Verlust von Geldern führen.", + "add_token_disclaimer_check": "Ich habe die Adresse und Informationen zum Token-Contract anhand einer seriösen Quelle bestätigt. Das Hinzufügen böswilliger oder falscher Informationen kann zu einem Verlust von Geldern führen.", "add_token_warning": "Bearbeiten oder fügen Sie Token nicht gemäß den Anweisungen von Betrügern hinzu.\nBestätigen Sie Token-Adressen immer mit seriösen Quellen!", "add_value": "Wert hinzufügen", "address": "Adresse", @@ -45,7 +45,7 @@ "already_have_account": "Sie haben bereits ein Konto?", "always": "immer", "amount": "Betrag: ", - "amount_is_below_minimum_limit": "Ihr Saldo nach Gebühren wäre geringer als der für den Austausch benötigte Mindestbetrag (${min})", + "amount_is_below_minimum_limit": "Ihr Saldo nach Gebühren wäre geringer als der für den Tausch benötigte Mindestbetrag (${min})", "amount_is_estimate": "Der empfangene Betrag ist eine Schätzung", "amount_is_guaranteed": "Der Empfangsbetrag ist garantiert", "and": "Und", @@ -76,19 +76,20 @@ "backup": "Sicherung", "backup_file": "Sicherungsdatei", "backup_password": "Passwort sichern", - "balance": "Gleichgewicht", - "balance_page": "Balance-Seite", + "balance": "Saldo", + "balance_page": "Guthaben-Seite", "bill_amount": "Rechnungsbetrag", "billing_address_info": "Wenn Sie nach einer Rechnungsadresse gefragt werden, geben Sie bitte Ihre Lieferadresse an", "biometric_auth_reason": "Scannen Sie Ihren Fingerabdruck zur Authentifizierung", "bitcoin_dark_theme": "Dunkles Bitcoin-Thema", "bitcoin_light_theme": "Bitcoin Light-Thema", "bitcoin_payments_require_1_confirmation": "Bitcoin-Zahlungen erfordern 1 Bestätigung, was 20 Minuten oder länger dauern kann. Danke für Ihre Geduld! Sie erhalten eine E-Mail, wenn die Zahlung bestätigt ist.", + "block_height": "Blockhöhe", "block_remaining": "1 Block verbleibend", "Blocks_remaining": "${status} verbleibende Blöcke", "bluetooth": "Bluetooth", "bright_theme": "Strahlend hell", - "bump_fee": "Beulengebühr", + "bump_fee": "Gebühr erhöhen", "buy": "Kaufen", "buy_alert_content": "Derzeit unterstützen wir nur den Kauf von Bitcoin, Ethereum, Litecoin und Monero. Bitte erstellen Sie Ihr Bitcoin-, Ethereum-, Litecoin- oder Monero-Wallet oder wechseln Sie zu diesem.", "buy_bitcoin": "Bitcoin kaufen", @@ -104,7 +105,7 @@ "cake_pay_subtitle": "Kaufen Sie weltweite Prepaid-Karten und Geschenkkarten", "cake_pay_web_cards_subtitle": "Kaufen Sie weltweit Prepaid-Karten und Geschenkkarten", "cake_pay_web_cards_title": "Cake Pay-Webkarten", - "cake_seeds_save_disclaimer": "Bitte speichern Sie diese Wörter an einem sicheren Ort! Sie benötigen diese Wörter, um Ihre Brieftasche auf einem neuen Gerät wiederherzustellen.", + "cake_seeds_save_disclaimer": "Bitte speichern Sie diese Wörter an einem sicheren Ort! Sie benötigen diese Wörter, um Ihre Wallet auf einem neuen Gerät wiederherzustellen.", "cake_wallet": "Cake Wallet", "cakepay_confirm_no_vpn": "Ich bestätige, dass ich keinen Proxy oder VPN benutze", "cakepay_confirm_purchase": "Kauf bestätigen", @@ -118,7 +119,7 @@ "card_address": "Adresse:", "cardholder_agreement": "Karteninhabervertrag", "cards": "Karten", - "chains": "Ketten", + "chains": "Blockchains", "change": "Ändern", "change_backup_password_alert": "Ihre vorherigen Sicherungsdateien können nicht mit einem neuen Sicherungskennwort importiert werden. Das neue Sicherungskennwort wird nur für neue Sicherungsdateien verwendet. Sind Sie sicher, dass Sie das Sicherungskennwort ändern möchten?", "change_currency": "Währung ändern", @@ -131,6 +132,7 @@ "change_rep": "Change-Beauftragter", "change_rep_message": "Sind Sie sicher, dass Sie den Vertreter wechseln möchten?", "change_rep_successful": "Vertreter erfolgreich gerändert", + "change_selected_exchanges": "Änderung ausgewählter Austausch", "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", @@ -148,7 +150,7 @@ "clearnet_link": "Clearnet-Link", "close": "Schließen", "coin_control": "Coin Control (optional)", - "cold_or_recover_wallet": "Fügen Sie eine schreibgeschützte Brieftasche von Cupcake oder eine kalte Brieftasche hinzu oder erholen Sie sich eine Brieftasche", + "cold_or_recover_wallet": "Fügen Sie eine schreibgeschützte Wallet von Cupcake, eine Cold-Wallet oder eine Papier-Wallet hinzu.", "color_theme": "Farbthema", "commit_transaction_amount_fee": "Transaktion absenden\nBetrag: ${amount}\nGebühr: ${fee}", "confirm": "Bestätigen", @@ -158,7 +160,7 @@ "confirm_fee_deduction_content": "Stimmen Sie zu, die Gebühr von der Ausgabe abzuziehen?", "confirm_passphrase": "Passphrase bestätigen", "confirm_sending": "Senden bestätigen", - "confirm_silent_payments_switch_node": "Ihr aktueller Knoten unterstützt keine stillen Zahlungen \\ NCAKE Wallet wechselt zu einem kompatiblen Knoten, nur zum Scannen", + "confirm_silent_payments_switch_node": "Ihr aktueller Knoten unterstützt keine Silent Payments.\\n\\nCake Wallet wechselt zu einem kompatiblen Knoten, nur zum Scannen", "confirmations": "Bestätigungen", "confirmed": "Bestätigter Saldo", "confirmed_tx": "Bestätigt", @@ -176,16 +178,16 @@ "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.", - "contractName": "Vertragsname", - "contractSymbol": "Vertragssymbol", + "contract_warning": "Diese Contract Adresse wurde als potenziell betrügerisch gekennzeichnet. Bitte fahren Sie mit Vorsicht fort.", + "contractName": "Contract-Name", + "contractSymbol": "Contract-Symbol", "copied_key_to_clipboard": "${key} in Zwischenablage kopiert", "copied_to_clipboard": "In die Zwischenablage kopiert", "copy": "Kopieren", "copy_address": "Adresse kopieren", "copy_id": "ID kopieren", "copyWalletConnectLink": "Kopieren Sie den WalletConnect-Link von dApp und fügen Sie ihn hier ein", - "corrupted_seed_notice": "Die Dateien für diese Brieftasche sind beschädigt und können nicht geöffnet werden. Bitte sehen Sie sich die Saatgutphrase an, speichern Sie sie und stellen Sie die Brieftasche wieder her.\n\nWenn der Wert leer ist, konnte der Samen nicht korrekt wiederhergestellt werden.", + "corrupted_seed_notice": "Die Dateien für diese Wallet sind beschädigt und können nicht geöffnet werden. Bitte sehen Sie sich die Seeds an, speichern Sie sie und stellen Sie die Wallet wieder her.\n\nWenn der Wert leer ist, konnte der Seed nicht korrekt wiederhergestellt werden.", "countries": "Länder", "create_account": "Konto erstellen", "create_backup": "Backup erstellen", @@ -253,7 +255,7 @@ "enable": "Aktivieren", "enable_mempool_api": "Mempool-API für genaue Gebühren und Daten", "enable_replace_by_fee": "Aktivieren Sie Ersatz für Fee", - "enable_silent_payments_scanning": "Scannen Sie stille Zahlungen, bis die Spitze erreicht ist", + "enable_silent_payments_scanning": "Scannen Sie nach Silent Payments ihrer Adresse", "enabled": "Ermöglicht", "enter_amount": "Betrag eingeben", "enter_backup_password": "Sicherungskennwort hier eingeben", @@ -293,11 +295,11 @@ "estimated": "Geschätzt", "estimated_new_fee": "Geschätzte neue Gebühr", "estimated_receive_amount": "Geschätzter Empfangsbetrag", - "etherscan_history": "Etherscan-Geschichte", + "etherscan_history": "Etherscan-Historie", "event": "Ereignis", "events": "Veranstaltungen", "exchange": "Tauschen", - "exchange_incorrect_current_wallet_for_xmr": "Wenn Sie XMR aus Ihrer CakeWallet Monero-Balance tauschen möchten, wechseln Sie zuerst zu Ihrer Monero-Wallet.", + "exchange_incorrect_current_wallet_for_xmr": "Wenn Sie XMR aus Ihrem Cake Wallet Monero-Saldo tauschen möchten, wechseln Sie zuerst zu Ihrer Monero-Wallet.", "exchange_new_template": "Neue Vorlage", "exchange_provider_unsupported": "${providerName} wird nicht mehr unterstützt!", "exchange_result_confirm": "Durch Drücken von \"Bestätigen\" wird ${fetchingLabel} ${from} von Ihrer Wallet namens ${walletName} an die unten angegebene Adresse gesendet. Alternativ können Sie von einer externen Wallet an die unten angegebene Adresse / QR-Code senden.\n\nBitte bestätigen Sie, um fortzufahren, oder gehen Sie zurück, um die Beträge zu ändern.", @@ -333,7 +335,7 @@ "freeze": "Einfrieren", "frequently_asked_questions": "Häufig gestellte Fragen", "frozen": "Gefroren", - "frozen_balance": "Gefrorenes Gleichgewicht", + "frozen_balance": "Gefrorenes Guthaben", "full_balance": "Gesamtguthaben", "gas_exceeds_allowance": "Die durch Transaktion erforderliche Gas übertrifft die Zulage.", "generate_name": "Namen generieren", @@ -371,7 +373,7 @@ "incoming": "Eingehend", "incorrect_seed": "Der eingegebene Text ist ungültig.", "incorrect_seed_option": "Falsch. Bitte versuchen Sie es erneut", - "incorrect_seed_option_back": "Falsch. Bitte stellen Sie sicher, dass Ihr Samen korrekt gespeichert wird, und versuchen Sie es erneut.", + "incorrect_seed_option_back": "Falsch. Bitte stellen Sie sicher, dass Ihr Seed korrekt gespeichert wird, und versuchen Sie es erneut.", "inputs": "Eingänge", "insufficient_funds_for_tx": "Unzureichende Mittel zur erfolgreichen Ausführung der Transaktion.", "insufficient_lamport_for_tx": "Sie haben nicht genug SOL, um die Transaktion und ihre Transaktionsgebühr abzudecken. Bitte fügen Sie Ihrer Wallet mehr Sol hinzu oder reduzieren Sie die SOL-Menge, die Sie senden.", @@ -382,6 +384,7 @@ "invalid_password": "Ungültiges Passwort", "invoice_details": "Rechnungs-Details", "is_percentage": "ist", + "keys": "Schlüssel", "last_30_days": "Letzte 30 Tage", "learn_more": "Erfahren Sie mehr", "ledger_connection_error": "Verbindung zum Ledger gescheitert. Bitte versuche es erneut.", @@ -389,6 +392,7 @@ "ledger_error_tx_rejected_by_user": "Transaktion auf dem Gerät abgelehnt", "ledger_error_wrong_app": "Bitte stellen Sie sicher, dass Sie die richtige App auf Ihrem Ledger geöffnet haben", "ledger_please_enable_bluetooth": "Bitte aktivieren Sie Bluetooth um sich mit Ihren Ledger zu verbinden.", + "legacy": "Vermächtnis", "light_theme": "Hell", "litecoin_enable_mweb_sync": "Aktivieren Sie das MWEB-Scannen", "litecoin_mweb": "MWeb", @@ -398,10 +402,10 @@ "litecoin_mweb_display_card": "MWEB-Karte anzeigen", "litecoin_mweb_enable": "Aktivieren Sie MWeb", "litecoin_mweb_enable_later": "Sie können MWEB unter Anzeigeeinstellungen erneut aktivieren.", - "litecoin_mweb_logs": "MWEB -Protokolle", - "litecoin_mweb_node": "MWEB -Knoten", + "litecoin_mweb_logs": "MWEB-Protokolle", + "litecoin_mweb_node": "MWEB-Knoten", "litecoin_mweb_pegin": "Peg in", - "litecoin_mweb_pegout": "Abstecken", + "litecoin_mweb_pegout": "Peg out", "litecoin_mweb_scanning": "MWEB Scanning", "litecoin_mweb_settings": "MWEB-Einstellungen", "litecoin_mweb_warning": "Durch die Verwendung von MWEB wird zunächst ~ 600 MB Daten heruntergeladen und kann je nach Netzwerkgeschwindigkeit bis zu 30 Minuten dauern. Diese ersten Daten werden nur einmal heruntergeladen und für alle Litecoin-Wallets verfügbar", @@ -477,11 +481,11 @@ "offline": "offline", "ok": "OK", "old_fee": "Alte Gebühr", - "onion_link": "Zwiebel-Link", + "onion_link": "Onion-Link", "online": "online", "onramper_option_description": "Kaufen Sie schnell Krypto mit vielen Zahlungsmethoden. In den meisten Ländern erhältlich. Spreads und Gebühren variieren.", "open_gift_card": "Geschenkkarte öffnen", - "open_wallet": "Offener Brieftasche", + "open_wallet": "Wallet öffnen", "optional_description": "Optionale Beschreibung", "optional_email_hint": "Optionale Benachrichtigungs-E-Mail für den Zahlungsempfänger", "optional_name": "Optionaler Empfängername", @@ -514,8 +518,8 @@ "placeholder_transactions": "Ihre Transaktionen werden hier angezeigt", "please_fill_totp": "Bitte geben Sie den 8-stelligen Code ein, der auf Ihrem anderen Gerät vorhanden ist", "please_make_selection": "Bitte treffen Sie unten eine Auswahl zum Erstellen oder Wiederherstellen Ihrer Wallet.", - "Please_reference_document": "Weitere Informationen finden Sie in den Dokumenten unten.", "please_reference_document": "Bitte verweisen Sie auf die folgenden Dokumente, um weitere Informationen zu erhalten.", + "Please_reference_document": "Weitere Informationen finden Sie in den Dokumenten unten.", "please_select": "Bitte auswählen:", "please_select_backup_file": "Bitte wählen Sie die Sicherungsdatei und geben Sie das Sicherungskennwort ein.", "please_try_to_connect_to_another_node": "Bitte versuchen Sie, sich mit einem anderen Knoten zu verbinden", @@ -544,7 +548,7 @@ "qr_payment_amount": "This QR code contains a payment amount. Do you want to overwrite the current value?", "quantity": "Menge", "question_to_disable_2fa": "Sind Sie sicher, dass Sie Cake 2FA deaktivieren möchten? Für den Zugriff auf die Wallet und bestimmte Funktionen wird kein 2FA-Code mehr benötigt.", - "receivable_balance": "Forderungsbilanz", + "receivable_balance": "Empfangbares Guthaben", "receive": "Empfangen", "receive_amount": "Betrag", "received": "Empfangen", @@ -591,7 +595,7 @@ "restore_description_from_keys": "Stellen Sie Ihr Wallet aus generierten Tastenanschlägen her, die von Ihren privaten Schlüsseln gespeichert wurden", "restore_description_from_seed": "Stellen Sie Ihre Wallet aus den 25 Wörtern oder dem 13-Wort-Kombinationscode wieder her", "restore_description_from_seed_keys": "Stellen Sie Ihr Wallet aus Seed/Schlüsseln wieder her, die Sie sicher aufbewahrt haben", - "restore_existing_wallet": "Bestehende Brieftasche wiederherstellen", + "restore_existing_wallet": "Bestehende Wallet wiederherstellen", "restore_from_date_or_blockheight": "Bitte geben Sie ein Datum ein, das einige Tage vor dem Erstellen dieser Wallet liegt. Oder wenn Sie die Blockhöhe kennen, geben Sie stattdessen diese ein", "restore_from_seed_placeholder": "Seed bitte hier eingeben oder einfügen", "restore_new_seed": "Neuer Seed", @@ -633,7 +637,7 @@ "seed_alert_title": "Achtung", "seed_alert_yes": "Ja, habe ich", "seed_choose": "Seed-Sprache auswählen", - "seed_display_path": "Menü -> Sicherheit und Sicherung -> Schlüssel/Samen anzeigen", + "seed_display_path": "Menü -> Sicherheit und Sicherung -> Schlüssel/Seed anzeigen", "seed_hex_form": "Seed (Hexformat)", "seed_key": "Seed-Schlüssel", "seed_language": "Seed-Sprache", @@ -653,11 +657,11 @@ "seed_language_spanish": "Spanisch", "seed_phrase_length": "Länge der Seed-Phrase", "seed_position_question_one": "Was ist das", - "seed_position_question_two": "Wort Ihrer Samenphrase?", + "seed_position_question_two": "Wort Ihrer Seed-Phrase?", "seed_reminder": "Bitte notieren Sie diese für den Fall, dass Sie Ihr Telefon verlieren oder es kaputtgeht", "seed_share": "Seed teilen", "seed_title": "Seed", - "seed_verified": "Samen verifiziert", + "seed_verified": "Seed verifiziert", "seed_verified_subtext": "Sie können Ihren gespeicherten Saat", "seedtype": "Seedtyp", "seedtype_alert_content": "Das Teilen von Seeds mit anderen Wallet ist nur mit bip39 Seedype möglich.", @@ -668,6 +672,7 @@ "select_backup_file": "Sicherungsdatei auswählen", "select_buy_provider_notice": "Wählen Sie oben einen Anbieter kaufen. Sie können diese Seite überspringen, indem Sie Ihren Standard-Kaufanbieter in den App-Einstellungen festlegen.", "select_destination": "Bitte wählen Sie das Ziel für die Sicherungsdatei aus.", + "select_hw_account_below": "Bitte wählen Sie unten, welches Konto unten wiederhergestellt werden soll:", "select_sell_provider_notice": "Wählen Sie oben einen Verkaufsanbieter aus. Sie können diesen Bildschirm überspringen, indem Sie in den App-Einstellungen Ihren Standard-Verkaufsanbieter festlegen.", "select_your_country": "Bitte wählen Sie Ihr Land aus", "sell": "Verkaufen", @@ -697,7 +702,8 @@ "sent": "Versendet", "service_health_disabled": "Service Health Bulletin ist deaktiviert", "service_health_disabled_message": "Dies ist die Seite \"Service Health Bulletin\", können Sie diese Seite unter Einstellungen -> Privatsphäre aktivieren", - "set_a_pin": "Setzen Sie einen Stift", + "set_a_pin": "PIN erstellen", + "set_up_a_wallet": "Eine Wallet einrichten", "settings": "Einstellungen", "settings_all": "ALLE", "settings_allow_biometrical_authentication": "Biometrische Authentifizierung zulassen", @@ -734,8 +740,8 @@ "shared_seed_wallet_groups": "Gemeinsame Walletsseed Gruppen", "show": "Zeigen", "show_address_book_popup": "Popup \"zum Adressbuch hinzufügen\" nach dem Senden anzeigen", - "show_balance": "Lange Presse, um das Gleichgewicht zu zeigen", - "show_balance_toast": "Lange Presse, um sich zu verbergen oder Gleichgewicht zu zeigen", + "show_balance": "Lange drücken, um das Kontoguthaben zu zeigen", + "show_balance_toast": "Langes drücken, um das Kontoguthaben zu verbergen oder zu zeigen", "show_details": "Details anzeigen", "show_keys": "Seed/Schlüssel anzeigen", "show_market_place": "Marktplatz anzeigen", @@ -750,23 +756,23 @@ "signature_invalid_error": "Die Signatur gilt nicht für die angegebene Nachricht", "signTransaction": "Transaktion unterzeichnen", "signup_for_card_accept_terms": "Melden Sie sich für die Karte an und akzeptieren Sie die Bedingungen.", - "silent_payment": "Stille Zahlung", - "silent_payments": "Stille Zahlungen", - "silent_payments_always_scan": "Setzen Sie stille Zahlungen immer scannen", + "silent_payment": "Silent Payment", + "silent_payments": "Silent Payments", + "silent_payments_always_scan": "Setzen Sie Silent Payments auf immer scannen", "silent_payments_disclaimer": "Neue Adressen sind keine neuen Identitäten. Es ist eine Wiederverwendung einer bestehenden Identität mit einem anderen Etikett.", - "silent_payments_display_card": "Zeigen Sie stille Zahlungskarte", + "silent_payments_display_card": "Silent Payments Karte anzeigen", "silent_payments_scan_from_date": "Scan ab Datum", - "silent_payments_scan_from_date_or_blockheight": "Bitte geben Sie die Blockhöhe ein, die Sie für eingehende stille Zahlungen scannen möchten, oder verwenden Sie stattdessen das Datum. Sie können wählen, ob die Wallet jeden Block scannt oder nur die angegebene Höhe überprüft.", + "silent_payments_scan_from_date_or_blockheight": "Bitte geben Sie die Blockhöhe ein, die Sie für eingehende Silent Payments scannen möchten, oder verwenden Sie stattdessen das Datum. Sie können wählen, ob die Wallet jeden Block scannt oder nur die angegebene Höhe überprüft.", "silent_payments_scan_from_height": "Scan aus der Blockhöhe scannen", "silent_payments_scanned_tip": "Gescannt zum Trinkgeld! (${tip})", - "silent_payments_scanning": "Stille Zahlungen scannen", - "silent_payments_settings": "Einstellungen für stille Zahlungen", + "silent_payments_scanning": "Silent Payments scannen", + "silent_payments_settings": "Einstellungen für Silent Payments", "single_seed_wallets_group": "Einzelne Wallets", "slidable": "Verschiebbar", "solana_create_associated_token_account_exception": "Fehler beim Erstellen des zugehörigen Token -Kontos für die Empfängeradresse.", "solana_no_associated_token_account_exception": "Für diese Adresse ist kein Token -Konto zugeordnet.", - "solana_sign_native_transaction_rent_exception": "Transaktion kann nicht abgeschlossen werden. Unzureichende Sol ließen nach der Transaktion zur Miete gelassen. Bitte geben Sie Ihre SOL -Balance auf oder reduzieren Sie die Menge an SOL, die Sie senden.", - "solana_sign_spl_token_transaction_rent_exception": "Transaktion kann nicht abgeschlossen werden. Unzureichende Sol ließen nach der Transaktion zur Miete gelassen. Bitte geben Sie Ihre SOL -Balance auf.", + "solana_sign_native_transaction_rent_exception": "Transaktion kann nicht abgeschlossen werden. Nach der Transaktion ist nicht genug Sol für die Miete übrig. Bitte stocken Sie Ihr SOL-Guthaben auf oder reduzieren Sie die Menge an SOL, die Sie senden.", + "solana_sign_spl_token_transaction_rent_exception": "Transaktion kann nicht abgeschlossen werden. Nach der Transaktion ist nicht genug Sol für die Miete übrig. Bitte stocken Sie Ihr SOL-Guthaben auf", "sort_by": "Sortiere nach", "spend_key_private": "Spend Key (geheim)", "spend_key_public": "Spend Key (öffentlich)", @@ -779,9 +785,10 @@ "support_description_guides": "Dokumentation und Hilfe für bekannte Probleme", "support_description_live_chat": "Kostenlos und schnell! Ausgebildete Mitarbeiter stehen zur Unterstützung bereit, um zu helfen", "support_description_other_links": "Treten Sie unseren Communities bei oder erreichen Sie uns oder unsere Partner über andere Methoden", - "support_title_guides": "Kuchenbrieftasche Docs", + "support_title_guides": "Cake Wallet Docs", "support_title_live_chat": "Live Support", "support_title_other_links": "Andere Support-Links", + "swap": "Tauschen", "sweeping_wallet": "Wallet leeren", "sweeping_wallet_alert": "Das sollte nicht lange dauern. VERLASSEN SIE DIESEN BILDSCHIRM NICHT, ANDERNFALLS KÖNNEN DIE GELDER VERLOREN GEHEN", "switchToETHWallet": "Bitte wechseln Sie zu einem Ethereum-Wallet und versuchen Sie es erneut", @@ -807,12 +814,13 @@ "testnet_coins_no_value": "Testnet-Münzen haben keinen Wert", "third_intro_content": "Yats leben auch außerhalb von Cake Wallet. Jede Wallet-Adresse auf der Welt kann durch ein Yat ersetzt werden!", "third_intro_title": "Yat spielt gut mit anderen", - "thorchain_contract_address_not_supported": "Thorchain unterstützt das Senden an eine Vertragsadresse nicht", + "this_pair_is_not_supported_warning": "Dieses Paar wird nicht mit den derzeit ausgewählten Börsen unterstützt. Bitte wählen Sie einen anderen Austausch.", + "thorchain_contract_address_not_supported": "Thorchain unterstützt das Senden an eine Smart Contract Adresse nicht", "thorchain_taproot_address_not_supported": "Der Thorchain-Anbieter unterstützt keine Taproot-Adressen. Bitte ändern Sie die Adresse oder wählen Sie einen anderen Anbieter aus.", "time": "${minutes}m ${seconds}s", "tip": "Hinweis:", "today": "Heute", - "token_contract_address": "Token-Vertragsadresse", + "token_contract_address": "Token-Contract-Adresse", "token_decimal": "Token-Dezimalzahl", "token_name": "Token-Name, z. B.: Tether", "token_symbol": "Token-Symbol, z. B.: USDT", @@ -878,7 +886,7 @@ "transaction_sent_notice": "Wenn der Bildschirm nach 1 Minute nicht weitergeht, überprüfen Sie einen Block-Explorer und Ihre E-Mail.", "transactions": "Transaktionen", "transactions_by_date": "Transaktionen nach Datum", - "trongrid_history": "Tronglidgeschichte", + "trongrid_history": "Trongrid-Historie", "trusted": "Vertrauenswürdige", "tx_commit_exception_no_dust_on_change": "Die Transaktion wird diesen Betrag abgelehnt. Mit diesen Münzen können Sie ${min} ohne Veränderung oder ${max} senden, die Änderungen zurückgeben.", "tx_commit_failed": "Transaktionsausschüsse ist fehlgeschlagen. Bitte wenden Sie sich an Support.", @@ -887,9 +895,9 @@ "tx_no_dust_exception": "Die Transaktion wird abgelehnt, indem eine Menge zu klein gesendet wird. Bitte versuchen Sie, die Menge zu erhöhen.", "tx_not_enough_inputs_exception": "Nicht genügend Eingänge verfügbar. Bitte wählen Sie mehr unter Münzkontrolle aus", "tx_rejected_bip68_final": "Die Transaktion hat unbestätigte Inputs und konnte nicht durch Gebühr ersetzt werden.", - "tx_rejected_dust_change": "Transaktion abgelehnt durch Netzwerkregeln, niedriger Änderungsbetrag (Staub). Versuchen Sie, alle zu senden oder die Menge zu reduzieren.", - "tx_rejected_dust_output": "Transaktion durch Netzwerkregeln, niedriger Ausgangsmenge (Staub) abgelehnt. Bitte erhöhen Sie den Betrag.", - "tx_rejected_dust_output_send_all": "Transaktion durch Netzwerkregeln, niedriger Ausgangsmenge (Staub) abgelehnt. Bitte überprüfen Sie den Gleichgewicht der unter Münzkontrolle ausgewählten Münzen.", + "tx_rejected_dust_change": "Transaktion abgelehnt durch Netzwerkregeln, niedriger Änderungsbetrag (Dust). Versuchen Sie, alle zu senden oder die Menge zu reduzieren.", + "tx_rejected_dust_output": "Transaktion durch Netzwerkregeln, niedriger Ausgangsmenge (Dust) abgelehnt. Bitte erhöhen Sie den Betrag.", + "tx_rejected_dust_output_send_all": "Transaktion durch Netzwerkregeln, niedriger Ausgangsmenge (Dust) abgelehnt. Bitte überprüfen Sie das Guthabe der unter Münzkontrolle ausgewählten Münzen.", "tx_rejected_vout_negative": "Nicht genug Guthaben, um die Gebühren dieser Transaktion zu bezahlen. Bitte überprüfen Sie den Restbetrag der Münzen unter Münzkontrolle.", "tx_wrong_balance_exception": "Sie haben nicht genug ${currency}, um diesen Betrag zu senden.", "tx_wrong_balance_with_amount_exception": "Sie haben nicht genug ${currency}, um die Gesamtmenge von ${amount} zu senden", @@ -932,10 +940,13 @@ "waiting_payment_confirmation": "Warte auf Zahlungsbestätigung", "wallet": "Geldbörse", "wallet_group": "Walletgruppe", + "wallet_group_description_existing_seed": "Sie haben für diese Wallet einen vorhandenen Seed verwendet. Sie können den Seed erneut überprüfen, wenn Sie ihn bestätigen oder aufschreiben müssen.", "wallet_group_description_four": "eine Wallet mit einem völlig neuen Seed schaffen.", - "wallet_group_description_one": "In CakeWallet können Sie eine erstellen", + "wallet_group_description_one": "In Cake Wallet können Sie eine erstellen", + "wallet_group_description_open_wallet": "Andernfalls können Sie die Wallet weiter öffnen", "wallet_group_description_three": "Sehen Sie den Bildschirm zur verfügbaren Wallet und/oder Walletgruppen. Oder wählen", "wallet_group_description_two": "Durch die Auswahl einer vorhandenen Wallet, mit der ein Seed geteilt werden kann. Jede Walletgruppe kann eine einzelne Wallet jedes Währungstyps enthalten. \n\n Sie können auswählen", + "wallet_group_description_view_seed": "Sie können diesen Seed immer wieder untersuchen", "wallet_group_empty_state_text_one": "Sieht so aus, als hätten Sie keine kompatiblen Walletgruppen !\n\n TAP", "wallet_group_empty_state_text_two": "unten, um einen neuen zu machen.", "wallet_keys": "Wallet-Seed/-Schlüssel", @@ -963,10 +974,10 @@ "wallets": "Wallets", "warning": "Warnung", "welcome": "Willkommen bei", - "welcome_subtitle_new_wallet": "Wenn Sie frisch anfangen möchten, tippen Sie unten neue Brieftaschen und Sie werden zu den Rennen gehen.", - "welcome_subtitle_restore_wallet": "Wenn Sie über eine vorhandene Brieftasche verfügen, die Sie in Kuchen bringen möchten, wählen Sie einfach die vorhandene Brieftasche wiederherstellen und wir werden Sie durch den Prozess führen.", + "welcome_subtitle_new_wallet": "Wenn Sie frisch anfangen möchten, tippen Sie unten neue Wallet und schon sind Sie bereit.", + "welcome_subtitle_restore_wallet": "Wenn Sie über eine vorhandene Wallet verfügen, die Sie in Cake Wallet öffnen möchten, drücken Sie auf Vorhandene Wallet wiederherstellen und wir werden Sie durch den Prozess führen.", "welcome_to_cakepay": "Willkommen bei Cake Pay!", - "what_is_silent_payments": "Was sind stille Zahlungen?", + "what_is_silent_payments": "Was sind Silent Payments?", "widgets_address": "Adresse", "widgets_or": "oder", "widgets_restore_from_blockheight": "Ab Blockhöhe wiederherstellen", diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 73ed8e15b..9e893cb56 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -84,6 +84,7 @@ "bitcoin_dark_theme": "Bitcoin Dark Theme", "bitcoin_light_theme": "Bitcoin Light Theme", "bitcoin_payments_require_1_confirmation": "Bitcoin payments require 1 confirmation, which can take 20 minutes or longer. Thanks for your patience! You will be emailed when the payment is confirmed.", + "block_height": "Block height", "block_remaining": "1 Block Remaining", "Blocks_remaining": "${status} Blocks Remaining", "bluetooth": "Bluetooth", @@ -131,6 +132,7 @@ "change_rep": "Change Representative", "change_rep_message": "Are you sure that you want to change representatives?", "change_rep_successful": "Successfully changed representative", + "change_selected_exchanges": "Change Selected Exchanges", "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", @@ -296,7 +298,7 @@ "etherscan_history": "Etherscan history", "event": "Event", "events": "Events", - "exchange": "Swap", + "exchange": "Exchange", "exchange_incorrect_current_wallet_for_xmr": "If you want to swap XMR from your Cake Wallet Monero balance, please switch to your Monero wallet first.", "exchange_new_template": "New template", "exchange_provider_unsupported": "${providerName} is no longer supported!", @@ -382,6 +384,7 @@ "invalid_password": "Invalid password", "invoice_details": "Invoice details", "is_percentage": "is", + "keys": "Keys", "last_30_days": "Last 30 days", "learn_more": "Learn More", "ledger_connection_error": "Failed to connect to you Ledger. Please try again.", @@ -389,6 +392,7 @@ "ledger_error_tx_rejected_by_user": "Transaction rejected on device", "ledger_error_wrong_app": "Please make sure you opend the right app on your ledger", "ledger_please_enable_bluetooth": "Please enable Bluetooth to detect your Ledger", + "legacy": "Legacy", "light_theme": "Light", "litecoin_enable_mweb_sync": "Enable MWEB scanning", "litecoin_mweb": "MWEB", @@ -499,6 +503,7 @@ "overwrite_amount": "Overwrite amount", "pairingInvalidEvent": "Pairing Invalid Event", "passphrase": "Passphrase (Optional)", + "passphrase_view_keys": "Passphrase", "passphrases_doesnt_match": "Passphrases do not match, please try again", "password": "Password", "paste": "Paste", @@ -667,6 +672,7 @@ "select_backup_file": "Select backup file", "select_buy_provider_notice": "Select a buy provider above. You can skip this screen by setting your default buy provider in app settings.", "select_destination": "Please select destination for the backup file.", + "select_hw_account_below": "Please select which account to restore below:", "select_sell_provider_notice": "Select a sell provider above. You can skip this screen by setting your default sell provider in app settings.", "select_your_country": "Please select your country", "sell": "Sell", @@ -697,6 +703,7 @@ "service_health_disabled": "Service Health Bulletin is disabled", "service_health_disabled_message": "This is the service health bulletin page, you can enable this page under Settings -> Privacy", "set_a_pin": "Set a PIN", + "set_up_a_wallet": "Set up a Wallet", "settings": "Settings", "settings_all": "ALL", "settings_allow_biometrical_authentication": "Allow biometrical authentication", @@ -732,7 +739,7 @@ "share_address": "Share address", "shared_seed_wallet_groups": "Shared Seed Wallet Groups", "show": "Show", - "show_address_book_popup": "Show 'Add to Address Book' popup after sending", + "show_address_book_popup": "Show Address Book popup", "show_balance": "Long Press to Show Balance", "show_balance_toast": "Long press to hide or show balance", "show_details": "Show Details", @@ -781,6 +788,7 @@ "support_title_guides": "Cake Wallet docs", "support_title_live_chat": "Live support", "support_title_other_links": "Other support links", + "swap": "Swap", "sweeping_wallet": "Sweeping wallet", "sweeping_wallet_alert": "This shouldn’t take long. DO NOT LEAVE THIS SCREEN OR THE SWEPT FUNDS MAY BE LOST.", "switchToETHWallet": "Please switch to an Ethereum wallet and try again", @@ -806,6 +814,7 @@ "testnet_coins_no_value": "Testnet coins have no value", "third_intro_content": "Yats live outside of Cake Wallet, too. Any wallet address on earth can be replaced with a Yat!", "third_intro_title": "Yat plays nicely with others", + "this_pair_is_not_supported_warning": "This pair is not supported with the currently selected exchange(s). Please select another exchange.", "thorchain_contract_address_not_supported": "THORChain does not support sending to a contract address", "thorchain_taproot_address_not_supported": "The ThorChain provider does not support Taproot addresses. Please change the address or select a different provider.", "time": "${minutes}m ${seconds}s", @@ -929,10 +938,13 @@ "waitFewSecondForTxUpdate": "Kindly wait for a few seconds for transaction to reflect in transactions history", "wallet": "Wallet", "wallet_group": "Wallet Group", + "wallet_group_description_existing_seed": "You’ve chosen to use an existing seed for this wallet. You may verify the seed again if you need to confirm or write it down.", "wallet_group_description_four": "to create a wallet with an entirely new seed.", "wallet_group_description_one": "In Cake Wallet, you can create a", + "wallet_group_description_open_wallet": "Otherwise, you can continue to open the wallet", "wallet_group_description_three": "to see the available wallets and/or wallet groups screen. Or choose", "wallet_group_description_two": "by selecting an existing wallet to share a seed with. Each wallet group can contain a single wallet of each currency type.\n\nYou can select", + "wallet_group_description_view_seed": "You can always view this seed again under", "wallet_group_empty_state_text_one": "Looks like you don't have any compatible wallet groups!\n\nTap", "wallet_group_empty_state_text_two": "below to make a new one.", "wallet_keys": "Wallet seed/keys", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index 4f520a2aa..19c13d46b 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -84,6 +84,7 @@ "bitcoin_dark_theme": "Tema oscuro de Bitcoin", "bitcoin_light_theme": "Tema claro de Bitcoin", "bitcoin_payments_require_1_confirmation": "Los pagos de Bitcoin requieren 1 confirmación, que puede demorar 20 minutos o más. ¡Gracias por tu paciencia! Se te enviará un correo electrónico cuando se confirme el pago.", + "block_height": "Altura de bloque", "block_remaining": "1 bloqueo restante", "Blocks_remaining": "${status} Bloques restantes", "bluetooth": "Bluetooth", @@ -131,6 +132,7 @@ "change_rep": "Representante de cambio", "change_rep_message": "¿Estás seguro de que quieres cambiar de representante?", "change_rep_successful": "Representante cambiado con éxito", + "change_selected_exchanges": "Cambiar intercambios seleccionados", "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", @@ -296,7 +298,7 @@ "etherscan_history": "historia de etherscan", "event": "Evento", "events": "Eventos", - "exchange": "Intercambiar", + "exchange": "Intercambio", "exchange_incorrect_current_wallet_for_xmr": "Si desea intercambiar XMR desde su billetera de pastel Monero Balance, primero cambie a su billetera Monero.", "exchange_new_template": "Nueva plantilla", "exchange_provider_unsupported": "¡${providerName} ya no es compatible!", @@ -382,6 +384,7 @@ "invalid_password": "Contraseña invalida", "invoice_details": "Detalles de la factura", "is_percentage": "es", + "keys": "Llaves", "last_30_days": "Últimos 30 días", "learn_more": "Aprende más", "ledger_connection_error": "No se pudo conectar con ledger. Inténtalo de nuevo.", @@ -389,6 +392,7 @@ "ledger_error_tx_rejected_by_user": "Transacción rechazada en el dispositivo", "ledger_error_wrong_app": "Por favor, asegúrate de abrir la aplicación correcta en su libro mayor.", "ledger_please_enable_bluetooth": "Habilita tu Bluetooth para detectar tu ledger", + "legacy": "Legado", "light_theme": "Ligero", "litecoin_enable_mweb_sync": "Habilitar el escaneo mweb", "litecoin_mweb": "Mweb", @@ -668,6 +672,7 @@ "select_backup_file": "Seleccionar archivo de respaldo", "select_buy_provider_notice": "Selecciona un proveedor de compra arriba. Puede omitir esta pantalla configurando su proveedor de compra predeterminado en la configuración de la aplicación.", "select_destination": "Selecciona el destino del archivo de copia de seguridad.", + "select_hw_account_below": "Seleccione qué cuenta restaurar a continuación:", "select_sell_provider_notice": "Selecciona un proveedor de venta arriba. Puede omitir esta pantalla configurando su proveedor de venta predeterminado en la configuración de la aplicación.", "select_your_country": "Seleccione su país", "sell": "Vender", @@ -698,6 +703,7 @@ "service_health_disabled": "El boletín de salud del servicio está deshabilitado", "service_health_disabled_message": "Esta es la página del Boletín de Salud del Servicio, puede habilitar esta página en Configuración -> Privacidad", "set_a_pin": "Establecer un alfiler", + "set_up_a_wallet": "Establecer una billetera", "settings": "Configuraciones", "settings_all": "TODOS", "settings_allow_biometrical_authentication": "Permitir autenticación biométrica", @@ -733,7 +739,7 @@ "share_address": "Compartir dirección", "shared_seed_wallet_groups": "Grupos de billetera de semillas compartidas", "show": "Espectáculo", - "show_address_book_popup": "Mostrar ventana emergente 'Agregar a la libreta de direcciones' después de enviar", + "show_address_book_popup": "Mostrar la ventana emergente de la libreta de direcciones", "show_balance": "Prensa larga para mostrar equilibrio", "show_balance_toast": "Prensa larga para esconder o mostrar equilibrio", "show_details": "Mostrar detalles", @@ -782,6 +788,7 @@ "support_title_guides": "Documentos de billetera de pastel", "support_title_live_chat": "Soporte en tiempo real", "support_title_other_links": "Otros enlaces de soporte", + "swap": "Intercambio", "sweeping_wallet": "Barrer billetera (gastar todos los fondos disponibles)", "sweeping_wallet_alert": "Esto no debería llevar mucho tiempo. NO DEJES ESTA PANTALLA O SE PUEDEN PERDER LOS FONDOS BARRIDOS", "switchToETHWallet": "Cambia a una billetera Ethereum e inténtelo nuevamente.", @@ -807,6 +814,7 @@ "testnet_coins_no_value": "Las monedas de prueba no tienen valor", "third_intro_content": "Los Yats también viven fuera de Cake Wallet. Cualquier dirección de billetera en la tierra se puede reemplazar con un Yat!", "third_intro_title": "Yat juega muy bien con otras", + "this_pair_is_not_supported_warning": "Este par no es compatible con los intercambios (s) actualmente seleccionados. Seleccione otro intercambio.", "thorchain_contract_address_not_supported": "Thorchain no admite enviar a una dirección de contrato", "thorchain_taproot_address_not_supported": "El proveedor de Thorchain no admite las direcciones de Taproot. Cambia la dirección o selecciona un proveedor diferente.", "time": "${minutes}m ${seconds}s", @@ -930,10 +938,13 @@ "waitFewSecondForTxUpdate": "Espera unos segundos para que la transacción se refleje en el historial de transacciones.", "wallet": "Billetera", "wallet_group": "Grupo de billetera", + "wallet_group_description_existing_seed": "Ha elegido usar una semilla existente para esta billetera. Puede verificar la semilla nuevamente si necesita confirmarla o escribirla.", "wallet_group_description_four": "Para crear una billetera con una semilla completamente nueva.", "wallet_group_description_one": "En la billetera de pastel, puedes crear un", + "wallet_group_description_open_wallet": "De lo contrario, puede continuar abriendo la billetera", "wallet_group_description_three": "Para ver las billeteras disponibles y/o la pantalla de grupos de billeteras. O elegir", "wallet_group_description_two": "Seleccionando una billetera existente para compartir una semilla con. Cada grupo de billetera puede contener una sola billetera de cada tipo de moneda. \n\n puedes seleccionar", + "wallet_group_description_view_seed": "Siempre puedes ver esta semilla nuevamente debajo", "wallet_group_empty_state_text_one": "Parece que no tienes ningún grupo de billetera compatible !\n\n toque", "wallet_group_empty_state_text_two": "a continuación para hacer uno nuevo.", "wallet_keys": "Billetera semilla/claves", diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index 69c23ac81..94f4ccac6 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -84,6 +84,7 @@ "bitcoin_dark_theme": "Thème sombre Bitcoin", "bitcoin_light_theme": "Thème léger Bitcoin", "bitcoin_payments_require_1_confirmation": "Les paiements Bitcoin nécessitent 1 confirmation, ce qui peut prendre 20 minutes ou plus. Merci pour votre patience ! Vous serez averti par e-mail lorsque le paiement sera confirmé.", + "block_height": "Hauteur de blocage", "block_remaining": "1 bloc restant", "Blocks_remaining": "Blocs Restants : ${status}", "bluetooth": "Bluetooth", @@ -131,6 +132,7 @@ "change_rep": "Changer de représentant", "change_rep_message": "Êtes-vous sûr de vouloir changer de représentant ?", "change_rep_successful": "Représentant changé avec succès", + "change_selected_exchanges": "Modifier les échanges sélectionné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", @@ -296,7 +298,7 @@ "etherscan_history": "Historique Etherscan", "event": "Événement", "events": "Événements", - "exchange": "Échanger", + "exchange": "Échange", "exchange_incorrect_current_wallet_for_xmr": "Si vous souhaitez échanger des XMR depuis le solde Monero de votre Cake Wallet, veuillez d'abord passer à votre portefeuille Monero.", "exchange_new_template": "Nouveau modèle d'échange", "exchange_provider_unsupported": "${providerName} n'est plus pris en charge !", @@ -382,6 +384,7 @@ "invalid_password": "Mot de passe incorrect", "invoice_details": "Détails de la facture", "is_percentage": "est", + "keys": "Clés", "last_30_days": "30 derniers jours", "learn_more": "En savoir plus", "ledger_connection_error": "Impossible de se connecter à votre Ledger. Veuillez réessayer.", @@ -389,6 +392,7 @@ "ledger_error_tx_rejected_by_user": "Transaction rejetée sur l'appareil", "ledger_error_wrong_app": "Veuillez vous assurer d'ouvrir la bonne application sur votre Ledger", "ledger_please_enable_bluetooth": "Veuillez activer le Bluetooth pour détecter votre Ledger", + "legacy": "Héritage", "light_theme": "Clair", "litecoin_enable_mweb_sync": "Activer la numérisation MWEB", "litecoin_mweb": "Mweb", @@ -667,6 +671,7 @@ "select_backup_file": "Sélectionnez le fichier de sauvegarde", "select_buy_provider_notice": "Sélectionnez un fournisseur d'achat ci-dessus. Vous pouvez ignorer cet écran en définissant votre fournisseur d'achat par défaut dans les paramètres de l'application.", "select_destination": "Veuillez sélectionner la destination du fichier de sauvegarde.", + "select_hw_account_below": "Veuillez sélectionner le compte à restaurer ci-dessous:", "select_sell_provider_notice": "Sélectionnez un fournisseur de vente ci-dessus. Vous pouvez ignorer cet écran en définissant votre fournisseur de vente par défaut dans les paramètres de l'application.", "select_your_country": "Veuillez sélectionner votre pays", "sell": "Vendre", @@ -697,6 +702,7 @@ "service_health_disabled": "Le bulletin de santé du service est désactivé.", "service_health_disabled_message": "Ceci est la page du Bulletin de santé du service, vous pouvez activer cette page sous Paramètres -> Confidentialité", "set_a_pin": "Définir un code PIN", + "set_up_a_wallet": "Configurer un portefeuille", "settings": "Paramètres", "settings_all": "TOUT", "settings_allow_biometrical_authentication": "Autoriser l'authentification biométrique", @@ -732,7 +738,7 @@ "share_address": "Partager l'adresse", "shared_seed_wallet_groups": "Groupes de portefeuilles partagés", "show": "Montrer", - "show_address_book_popup": "Afficher la popup `` Ajouter au carnet d'adresses '' après avoir envoyé", + "show_address_book_popup": "Afficher la fenêtre contextuelle du carnet d'adresses", "show_balance": "Longue presse pour montrer l'équilibre", "show_balance_toast": "Longue appuyez sur pour masquer ou afficher l'équilibre", "show_details": "Afficher les détails", @@ -781,6 +787,7 @@ "support_title_guides": "Docs de portefeuille à gâteau", "support_title_live_chat": "Support en direct", "support_title_other_links": "Autres liens d'assistance", + "swap": "Échanger", "sweeping_wallet": "Portefeuille (wallet) de consolidation", "sweeping_wallet_alert": "Cela ne devrait pas prendre longtemps. NE QUITTEZ PAS CET ÉCRAN OU LES FONDS TRANSFÉRÉS POURRAIENT ÊTRE PERDUS.", "switchToETHWallet": "Veuillez passer à un portefeuille (wallet) Ethereum et réessayer", @@ -806,6 +813,7 @@ "testnet_coins_no_value": "Les pièces TestNet n'ont aucune valeur", "third_intro_content": "Les Yats existent aussi en dehors de Cake Wallet. Toute adresse sur terre peut être remplacée par un Yat !", "third_intro_title": "Yat est universel", + "this_pair_is_not_supported_warning": "Cette paire n'est pas prise en charge avec les échanges actuellement sélectionnés. Veuillez sélectionner un autre échange.", "thorchain_contract_address_not_supported": "Thorchain ne prend pas en charge l'envoi à une adresse de contrat", "thorchain_taproot_address_not_supported": "Le fournisseur de Thorchain ne prend pas en charge les adresses de tapoot. Veuillez modifier l'adresse ou sélectionner un autre fournisseur.", "time": "${minutes}m ${seconds}s", @@ -929,10 +937,13 @@ "waitFewSecondForTxUpdate": "Veuillez attendre quelques secondes pour que la transaction soit reflétée dans l'historique des transactions.", "wallet": "Portefeuille", "wallet_group": "Groupe de portefeuille", + "wallet_group_description_existing_seed": "Vous avez choisi d'utiliser une graine existante pour ce portefeuille. Vous pouvez vérifier à nouveau la graine si vous avez besoin de le confirmer ou de le noter.", "wallet_group_description_four": "Pour créer un portefeuille avec une graine entièrement nouvelle.", "wallet_group_description_one": "Dans Cake Wallet, vous pouvez créer un", + "wallet_group_description_open_wallet": "Sinon, vous pouvez continuer à ouvrir le portefeuille", "wallet_group_description_three": "Pour voir les portefeuilles et / ou les groupes de portefeuilles disponibles. Ou choisir", "wallet_group_description_two": "En sélectionnant un portefeuille existant pour partager une graine avec. Chaque groupe de portefeuille peut contenir un seul portefeuille de chaque type de devise. \n\n Vous pouvez sélectionner", + "wallet_group_description_view_seed": "Vous pouvez toujours revoir cette graine sous", "wallet_group_empty_state_text_one": "On dirait que vous n'avez pas de groupes de portefeuilles compatibles !\n\n Tap", "wallet_group_empty_state_text_two": "Ci-dessous pour en faire un nouveau.", "wallet_keys": "Phrase secrète (seed)/Clefs du portefeuille (wallet)", diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index 07622a841..3c990924b 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -84,6 +84,7 @@ "bitcoin_dark_theme": "Bitcoin Dark Jigo", "bitcoin_light_theme": "Jigon Hasken Bitcoin", "bitcoin_payments_require_1_confirmation": "Akwatin Bitcoin na buɗe 1 sambumbu, da yake za ta samu mintuna 20 ko yawa. Ina kira ga sabuwar lafiya! Zaka sanarwa ta email lokacin da aka samu akwatin samun lambar waya.", + "block_height": "Toshe tsawo", "block_remaining": "1 toshe ragowar", "Blocks_remaining": "${status} Katanga ya rage", "bluetooth": "Bluetooth", @@ -131,6 +132,7 @@ "change_rep": "Canza Wakili", "change_rep_message": "Shin kun tabbata kuna son canza wakilai?", "change_rep_successful": "An samu nasarar canzawa wakilin", + "change_selected_exchanges": "Canza musayar musayar", "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", @@ -296,7 +298,7 @@ "etherscan_history": "Etherscan tarihin kowane zamani", "event": "Lamarin", "events": "Abubuwan da suka faru", - "exchange": "Musya", + "exchange": "Canji", "exchange_incorrect_current_wallet_for_xmr": "Idan kana son canza XMR daga walat ɗin Bed Wallet ɗinka, da fatan za a canza zuwa walat ɗinku na Monero.", "exchange_new_template": "Sabon template", "exchange_provider_unsupported": "${providerName}", @@ -382,6 +384,7 @@ "invalid_password": "Kalmar sirri mara inganci", "invoice_details": "Bayanin wadannan", "is_percentage": "shine", + "keys": "Makullin", "last_30_days": "Kwanaki 30 na ƙarshe", "learn_more": "Ƙara Koyi", "ledger_connection_error": "Ba a yi nasarar haɗawa da ku ba. Da fatan za a sake gwadawa.", @@ -389,6 +392,7 @@ "ledger_error_tx_rejected_by_user": "Ma'amala da aka ƙi akan na'urar", "ledger_error_wrong_app": "Da fatan za a tabbata kun yi amfani da app ɗin dama akan dillalarku", "ledger_please_enable_bluetooth": "Da fatan za a kunna Bluetooth don gano Ledger ɗinku", + "legacy": "Gado", "light_theme": "Haske", "litecoin_enable_mweb_sync": "Kunna binciken Mweb", "litecoin_mweb": "Mweb", @@ -669,6 +673,7 @@ "select_backup_file": "Zaɓi fayil ɗin madadin", "select_buy_provider_notice": "Zaɓi mai ba da kyauta a sama. Zaka iya tsallake wannan allon ta hanyar saita mai ba da isasshen busasshen mai ba da isasshen busasshiyar saiti.", "select_destination": "Da fatan za a zaɓi wurin da za a yi wa madadin fayil ɗin.", + "select_hw_account_below": "Da fatan za a zabi wanda asusun zai gyara a ƙasa:", "select_sell_provider_notice": "Zaɓi mai bada siyarwa a sama. Kuna iya tsallake wannan allon ta saita mai bada siyar da ku a cikin saitunan app.", "select_your_country": "Da fatan za a zabi ƙasarku", "sell": "sayar", @@ -699,6 +704,7 @@ "service_health_disabled": "Ba a kashe Bayar da Kiwon Lafiya", "service_health_disabled_message": "Wannan shafin yanar gizo mai kula da sabis ne, zaka iya kunna wannan shafin a karkashin saiti -> Sirri", "set_a_pin": "Saita PIN", + "set_up_a_wallet": "Kafa walat", "settings": "Saiti", "settings_all": "DUK", "settings_allow_biometrical_authentication": "Bada izinin tantance sawun yatsa", @@ -734,7 +740,7 @@ "share_address": "Raba adireshin", "shared_seed_wallet_groups": "Raba ƙungiya walat", "show": "Nuna", - "show_address_book_popup": "Nuna 'ƙara don magance littafin' Popup bayan aikawa", + "show_address_book_popup": "Nuna littafin littafin adireshi", "show_balance": "Dogon latsawa don nuna ma'auni", "show_balance_toast": "Latsa latsawa don ɓoye ko nuna ma'auni", "show_details": "Nuna Cikakkun bayanai", @@ -783,6 +789,7 @@ "support_title_guides": "Docs Bakin", "support_title_live_chat": "Tallafi na Live", "support_title_other_links": "Sauran hanyoyin tallafi", + "swap": "Musya", "sweeping_wallet": "Kashi na kasa", "sweeping_wallet_alert": "Wannan ba zai samu lokacin mai tsaski. KADA KA SAMU KUNGIYARAN KUHON, ZAMAN DADIN BANKUNCI ZAI HAŘA", "switchToETHWallet": "Da fatan za a canza zuwa walat ɗin Ethereum kuma a sake gwadawa", @@ -808,6 +815,7 @@ "testnet_coins_no_value": "TalkNet tsabar kudi ba su da darajar", "third_intro_content": "Yats suna zaune a wajen Kek Wallet, kuma. Ana iya maye gurbin kowane adireshin walat a duniya da Yat!", "third_intro_title": "Yat yana wasa da kyau tare da wasu", + "this_pair_is_not_supported_warning": "Ba a tallafa wa wannan mazaje tare da zaɓin da aka zaɓa a yanzu ba. Da fatan za a sami wani musayar.", "thorchain_contract_address_not_supported": "Thorchain baya goyon bayan aika zuwa adireshin kwangila", "thorchain_taproot_address_not_supported": "Mai ba da tallafi na ThorChain baya goyan bayan adreshin taproot. Da fatan za a canza adireshin ko zaɓi mai bayarwa daban.", "time": "${minutes}m ${seconds}s", @@ -931,10 +939,13 @@ "waitFewSecondForTxUpdate": "Da fatan za a jira ƴan daƙiƙa don ciniki don yin tunani a tarihin ma'amala", "wallet": "Zabira", "wallet_group": "Wallet kungiyar", + "wallet_group_description_existing_seed": "Kun zaɓi yin amfani da iri ɗaya na data kasance don wannan Wallet.you na iya tabbatar da zuriyar kuma idan kuna buƙatar tabbatarwa ko rubuta shi.", "wallet_group_description_four": "Don ƙirƙirar walat tare da sabon iri.", "wallet_group_description_one": "A cikin walat walat, zaka iya ƙirƙirar", + "wallet_group_description_open_wallet": "In ba haka ba, zaku iya ci gaba da buɗe walat ɗin", "wallet_group_description_three": "Don ganin wallets da / ko allon walat din. Ko zabi", "wallet_group_description_two": "ta hanyar zabar walat mai gudana don raba iri tare da. Kowane rukunin walat na iya ƙunsar watsarin kowane nau'in kuɗi. \n\n Zaka iya zaɓar", + "wallet_group_description_view_seed": "Koyaushe zaka iya duba wannan zuriya", "wallet_group_empty_state_text_one": "Kamar dai ba ku da wata ƙungiya matattara !\n\n Taɓa", "wallet_group_empty_state_text_two": "da ke ƙasa don yin sabo.", "wallet_keys": "Iri/maɓalli na walat", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index 702461323..b7ffd664e 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -84,6 +84,7 @@ "bitcoin_dark_theme": "बिटकॉइन डार्क थीम", "bitcoin_light_theme": "बिटकॉइन लाइट थीम", "bitcoin_payments_require_1_confirmation": "बिटकॉइन भुगतान के लिए 1 पुष्टिकरण की आवश्यकता होती है, जिसमें 20 मिनट या अधिक समय लग सकता है। आपके धैर्य के लिए धन्यवाद! भुगतान की पुष्टि होने पर आपको ईमेल किया जाएगा।", + "block_height": "ब्लॉक ऊंचाई", "block_remaining": "1 ब्लॉक शेष", "Blocks_remaining": "${status} शेष रहते हैं", "bluetooth": "ब्लूटूथ", @@ -131,6 +132,7 @@ "change_rep": "प्रतिनिधि बदलें", "change_rep_message": "क्या आप वाकई प्रतिनिधियों को बदलना चाहते हैं?", "change_rep_successful": "सफलतापूर्वक बदलकर प्रतिनिधि", + "change_selected_exchanges": "चयनित एक्सचेंजों को बदलें", "change_wallet_alert_content": "क्या आप करंट वॉलेट को बदलना चाहते हैं ${wallet_name}?", "change_wallet_alert_title": "वर्तमान बटुआ बदलें", "choose_a_payment_method": "एक भुगतान विधि का चयन करें", @@ -296,7 +298,7 @@ "etherscan_history": "इथरस्कैन इतिहास", "event": "आयोजन", "events": "आयोजन", - "exchange": "बदलना", + "exchange": "अदला-बदली", "exchange_incorrect_current_wallet_for_xmr": "यदि आप अपने केक वॉलेट मोनेरो बैलेंस से XMR को स्वैप करना चाहते हैं, तो कृपया पहले अपने मोनेरो वॉलेट पर स्विच करें।", "exchange_new_template": "नया टेम्पलेट", "exchange_provider_unsupported": "${providerName} अब समर्थित नहीं है!", @@ -382,6 +384,7 @@ "invalid_password": "अवैध पासवर्ड", "invoice_details": "चालान विवरण", "is_percentage": "है", + "keys": "चाबी", "last_30_days": "पिछले 30 दिन", "learn_more": "और अधिक जानें", "ledger_connection_error": "आप लेजर से जुड़ने में विफल रहे। कृपया पुन: प्रयास करें।", @@ -389,6 +392,7 @@ "ledger_error_tx_rejected_by_user": "डिवाइस पर लेनदेन खारिज कर दिया गया", "ledger_error_wrong_app": "कृपया सुनिश्चित करें कि आप अपने लेजर पर सही ऐप को खोलते हैं", "ledger_please_enable_bluetooth": "कृपया अपने बहीखाने का पता लगाने के लिए ब्लूटूथ को सक्षम करें", + "legacy": "परंपरा", "light_theme": "रोशनी", "litecoin_enable_mweb_sync": "MWEB स्कैनिंग सक्षम करें", "litecoin_mweb": "मावली", @@ -669,6 +673,7 @@ "select_backup_file": "बैकअप फ़ाइल का चयन करें", "select_buy_provider_notice": "ऊपर एक खरीद प्रदाता का चयन करें। आप इस स्क्रीन को ऐप सेटिंग्स में अपना डिफ़ॉल्ट बाय प्रदाता सेट करके छोड़ सकते हैं।", "select_destination": "कृपया बैकअप फ़ाइल के लिए गंतव्य का चयन करें।", + "select_hw_account_below": "कृपया नीचे पुनर्स्थापित करने के लिए कौन सा खाता चुनें:", "select_sell_provider_notice": "ऊपर एक विक्रय प्रदाता का चयन करें। आप ऐप सेटिंग में अपना डिफ़ॉल्ट विक्रय प्रदाता सेट करके इस स्क्रीन को छोड़ सकते हैं।", "select_your_country": "कृपया अपने देश का चयन करें", "sell": "बेचना", @@ -699,6 +704,7 @@ "service_health_disabled": "सेवा स्वास्थ्य बुलेटिन अक्षम है", "service_health_disabled_message": "यह सेवा स्वास्थ्य बुलेटिन पृष्ठ है, आप इस पृष्ठ को सेटिंग्स के तहत सक्षम कर सकते हैं -> गोपनीयता", "set_a_pin": "एक पिन सेट करना", + "set_up_a_wallet": "एक बटुआ सेट करें", "settings": "समायोजन", "settings_all": "सब", "settings_allow_biometrical_authentication": "बायोमेट्रिक प्रमाणीकरण की अनुमति दें", @@ -734,9 +740,9 @@ "share_address": "पता साझा करें", "shared_seed_wallet_groups": "साझा बीज बटुए समूह", "show": "दिखाओ", - "show_address_book_popup": "भेजने के बाद 'एड एड्रेस बुक' पॉपअप दिखाएं", + "show_address_book_popup": "पता बुक पॉपअप दिखाएं", "show_balance": "बैलेंस दिखाने के लिए लॉन्ग प्रेस", - "show_balance_toast": "बैलेंस को छिपाने या दिखाने के लिए लॉन्ग प्रेस", + "show_balance_toast": "संतुलन को छिपाने या दिखाने के लिए लॉन्ग प्रेस", "show_details": "विवरण दिखाएं", "show_keys": "बीज / कुंजियाँ दिखाएँ", "show_market_place": "बाज़ार दिखाएँ", @@ -783,6 +789,7 @@ "support_title_guides": "केक बटुए डॉक्स", "support_title_live_chat": "लाइव सहायता", "support_title_other_links": "अन्य समर्थन लिंक", + "swap": "बदलना", "sweeping_wallet": "स्वीपिंग वॉलेट", "sweeping_wallet_alert": "इसमें अधिक समय नहीं लगना चाहिए। इस स्क्रीन को न छोड़ें या स्वैप्ट फंड खो सकते हैं", "switchToETHWallet": "कृपया एथेरियम वॉलेट पर स्विच करें और पुनः प्रयास करें", @@ -808,6 +815,7 @@ "testnet_coins_no_value": "टेस्टनेट सिक्कों का कोई मूल्य नहीं है", "third_intro_content": "Yats Cake Wallet के बाहर भी रहता है। धरती पर किसी भी वॉलेट पते को Yat से बदला जा सकता है!", "third_intro_title": "Yat दूसरों के साथ अच्छा खेलता है", + "this_pair_is_not_supported_warning": "यह जोड़ी वर्तमान में चयनित एक्सचेंज (ओं) के साथ समर्थित नहीं है। कृपया एक और एक्सचेंज चुनें।", "thorchain_contract_address_not_supported": "थोरचेन एक अनुबंध पते पर भेजने का समर्थन नहीं करता है", "thorchain_taproot_address_not_supported": "थोरचेन प्रदाता टैपरोट पते का समर्थन नहीं करता है। कृपया पता बदलें या एक अलग प्रदाता का चयन करें।", "time": "${minutes}m ${seconds}s", @@ -931,10 +939,13 @@ "waitFewSecondForTxUpdate": "लेन-देन इतिहास में लेन-देन प्रतिबिंबित होने के लिए कृपया कुछ सेकंड प्रतीक्षा करें", "wallet": "बटुआ", "wallet_group": "बटुए समूह", + "wallet_group_description_existing_seed": "आपने इस वॉलेट के लिए एक मौजूदा बीज का उपयोग करने के लिए चुना है। यदि आपको इसकी पुष्टि करने या लिखने की आवश्यकता है, तो आप फिर से बीज को सत्यापित कर सकते हैं।", "wallet_group_description_four": "एक पूरी तरह से नए बीज के साथ एक बटुआ बनाने के लिए।", "wallet_group_description_one": "केक बटुए में, आप एक बना सकते हैं", + "wallet_group_description_open_wallet": "अन्यथा, आप बटुए खोलना जारी रख सकते हैं", "wallet_group_description_three": "उपलब्ध वॉलेट और/या वॉलेट समूह स्क्रीन देखने के लिए। या चुनें", "wallet_group_description_two": "एक बीज साझा करने के लिए एक मौजूदा बटुए का चयन करके। प्रत्येक वॉलेट समूह में प्रत्येक मुद्रा प्रकार का एक एकल वॉलेट हो सकता है।\n\nआप चयन कर सकते हैं", + "wallet_group_description_view_seed": "आप हमेशा इस बीज को फिर से देख सकते हैं", "wallet_group_empty_state_text_one": "लगता है कि आपके पास कोई संगत बटुआ समूह नहीं है!\n\nनल", "wallet_group_empty_state_text_two": "नीचे एक नया बनाने के लिए।", "wallet_keys": "बटुआ बीज / चाबियाँ", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index bdefaa4eb..8f7584b5c 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -84,6 +84,7 @@ "bitcoin_dark_theme": "Bitcoin Tamna tema", "bitcoin_light_theme": "Bitcoin Light Theme", "bitcoin_payments_require_1_confirmation": "Bitcoin plaćanja zahtijevaju 1 potvrdu, što može potrajati 20 minuta ili dulje. Hvala na Vašem strpljenju! Dobit ćete e-poruku kada plaćanje bude potvrđeno.", + "block_height": "Visina bloka", "block_remaining": "Preostalo 1 blok", "Blocks_remaining": "${status} preostalih blokova", "bluetooth": "Bluetooth", @@ -131,6 +132,7 @@ "change_rep": "Promijenite reprezentativan", "change_rep_message": "Jeste li sigurni da želite promijeniti predstavnika?", "change_rep_successful": "Uspješno promijenjena reprezentativna", + "change_selected_exchanges": "Promijenite odabrane razmjene", "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", @@ -296,7 +298,7 @@ "etherscan_history": "Etherscan povijest", "event": "Događaj", "events": "Događaji", - "exchange": "Zamjena", + "exchange": "Razmjena", "exchange_incorrect_current_wallet_for_xmr": "Ako želite zamijeniti XMR iz vašeg novčanika za kolač Monero, prvo se prebacite na svoj novčanik Monero.", "exchange_new_template": "Novi predložak", "exchange_provider_unsupported": "${providerName} više nije podržan!", @@ -382,6 +384,7 @@ "invalid_password": "Netočna zaporka", "invoice_details": "Podaci o fakturi", "is_percentage": "je", + "keys": "Ključ", "last_30_days": "Zadnjih 30 dana", "learn_more": "Saznajte više", "ledger_connection_error": "Nije uspio povezati se s knjigom. Molim te pokušaj ponovno.", @@ -389,6 +392,7 @@ "ledger_error_tx_rejected_by_user": "Transakcija odbijena na uređaju", "ledger_error_wrong_app": "Obavezno obavezno otvorite pravu aplikaciju na knjizi", "ledger_please_enable_bluetooth": "Omogućite Bluetooth da otkrije svoju knjigu", + "legacy": "Nasljeđe", "light_theme": "Svijetla", "litecoin_enable_mweb_sync": "Omogućite MWEB skeniranje", "litecoin_mweb": "MWeb", @@ -667,6 +671,7 @@ "select_backup_file": "Odaberite datoteku sigurnosne kopije", "select_buy_provider_notice": "Odaberite gornji davatelj kupnje. Ovaj zaslon možete preskočiti postavljanjem zadanog davatelja usluga kupnje u postavkama aplikacija.", "select_destination": "Odaberite odredište za datoteku sigurnosne kopije.", + "select_hw_account_below": "Molimo odaberite koji će se račun vratiti u nastavku:", "select_sell_provider_notice": "Gore odaberite pružatelja usluga prodaje. Ovaj zaslon možete preskočiti postavljanjem zadanog pružatelja usluga prodaje u postavkama aplikacije.", "select_your_country": "Odaberite svoju zemlju", "sell": "Prodavati", @@ -697,6 +702,7 @@ "service_health_disabled": "Zdravstveni bilten usluge je onemogućen", "service_health_disabled_message": "Ovo je stranica zdravstvenog biltena o usluzi, možete omogućiti ovu stranicu pod postavkama -> privatnost", "set_a_pin": "Postavite pin", + "set_up_a_wallet": "Postavite novčanik", "settings": "Postavke", "settings_all": "SVE", "settings_allow_biometrical_authentication": "Dopusti biometrijsku autentifikaciju", @@ -732,7 +738,7 @@ "share_address": "Podijeli adresu", "shared_seed_wallet_groups": "Zajedničke grupe za sjeme novčanika", "show": "Pokazati", - "show_address_book_popup": "Pokažite \"dodaj u adresar\" skočni prozor nakon slanja", + "show_address_book_popup": "Prikaži Popup adresara", "show_balance": "Dugački pritisak za pokazivanje ravnoteže", "show_balance_toast": "Dugo pritisnite da biste sakrili ili pokazali ravnotežu", "show_details": "Prikaži pojedinosti", @@ -781,6 +787,7 @@ "support_title_guides": "Dokumenti s kolačem kolača", "support_title_live_chat": "Podrška uživo", "support_title_other_links": "Ostale veze za podršku", + "swap": "Mijenjati", "sweeping_wallet": "Čisti novčanik", "sweeping_wallet_alert": "Ovo ne bi trebalo dugo trajati. NE NAPUŠTAJTE OVAJ ZASLON INAČE SE POBREŠENA SREDSTVA MOGU IZGUBITI", "switchToETHWallet": "Prijeđite na Ethereum novčanik i pokušajte ponovno", @@ -806,6 +813,7 @@ "testnet_coins_no_value": "TestNet kovanice nemaju vrijednost", "third_intro_content": "Yats žive i izvan Cake Wallet -a. Bilo koja adresa novčanika na svijetu može se zamijeniti Yat!", "third_intro_title": "Yat se lijepo igra s drugima", + "this_pair_is_not_supported_warning": "Ovaj par nije podržan s trenutno odabranim razmjenama. Molimo odaberite drugu razmjenu.", "thorchain_contract_address_not_supported": "Thorchain ne podržava slanje na adresu ugovora", "thorchain_taproot_address_not_supported": "Thorchain pružatelj ne podržava Taproot adrese. Promijenite adresu ili odaberite drugog davatelja usluga.", "time": "${minutes}m ${seconds}s", @@ -929,10 +937,13 @@ "waitFewSecondForTxUpdate": "Pričekajte nekoliko sekundi da se transakcija prikaže u povijesti transakcija", "wallet": "Novčanik", "wallet_group": "Skupina novčanika", + "wallet_group_description_existing_seed": "Odlučili ste koristiti postojeće sjeme za ovaj novčanik. Možete ponovno provjeriti sjeme ako ga trebate potvrditi ili zapisati.", "wallet_group_description_four": "Da biste stvorili novčanik s potpuno novim sjemenom.", "wallet_group_description_one": "U novčaniku kolača možete stvoriti a", + "wallet_group_description_open_wallet": "Inače možete nastaviti otvarati novčanik", "wallet_group_description_three": "Da biste vidjeli zaslon dostupnih novčanika i/ili grupa novčanika. Ili odaberite", "wallet_group_description_two": "Odabirom postojećeg novčanika s kojim ćete dijeliti sjeme. Svaka grupa novčanika može sadržavati jedan novčanik svake vrste valute. \n\n", + "wallet_group_description_view_seed": "Uvijek možete ponovo pogledati ovo sjeme ispod", "wallet_group_empty_state_text_one": "Izgleda da nemate nikakve kompatibilne grupe novčanika !\n\n", "wallet_group_empty_state_text_two": "Ispod da napravite novi.", "wallet_keys": "Pristupni izraz/ključ novčanika", diff --git a/res/values/strings_hy.arb b/res/values/strings_hy.arb index 85907535b..1aa8676bc 100644 --- a/res/values/strings_hy.arb +++ b/res/values/strings_hy.arb @@ -84,6 +84,7 @@ "bitcoin_dark_theme": "Bitcoin մութ տեսք", "bitcoin_light_theme": "Bitcoin պայծառ տեսք", "bitcoin_payments_require_1_confirmation": "Bitcoin վճարումները պահանջում են 1 հաստատում, որը կարող է տևել 20 րոպե կամ ավելի: Շնորհակալություն ձեր համբերության համար: Դուք էլ. նամակ կստանաք, երբ վճարումը հաստատվի։", + "block_height": "Բլոկի բարձրությունը", "block_remaining": "1 Բլոկ է մնացել", "Blocks_remaining": "${status} Բլոկ է մնացել", "bluetooth": "Bluetooth", @@ -131,6 +132,7 @@ "change_rep": "Փոխել ներկայացուցչին", "change_rep_message": "Վստահ եք, որ ցանկանում եք փոխել ներկայացուցիչներին?", "change_rep_successful": "Ներկայացուցչի փոփոխությունը հաջողությամբ կատարվեց", + "change_selected_exchanges": "Փոխեք ընտրված փոխանակումները", "change_wallet_alert_content": "Ցանկանում եք փոխել ընթացիկ դրամապանակը ${wallet_name}?", "change_wallet_alert_title": "Փոխել ընթացիկ դրամապանակը", "choose_a_payment_method": "Ընտրեք վճարման եղանակ", @@ -296,7 +298,7 @@ "etherscan_history": "Etherscan պատմություն", "event": "Իրադարձություն", "events": "Իրադարձություններ", - "exchange": "Փոխանակել", + "exchange": "Փոխանակում", "exchange_incorrect_current_wallet_for_xmr": "Եթե ​​ցանկանում եք փոխանակել XMR ձեր տորթի դրամապանակից Monero Relandal- ից, խնդրում ենք նախ անցնել ձեր Monero դրամապանակին:", "exchange_new_template": "Նոր տեսակ", "exchange_provider_unsupported": "${providerName} այլևս չի ապահովվում", @@ -382,6 +384,7 @@ "invalid_password": "Սխալ գաղտնաբառ", "invoice_details": "Հաշիվ-ապրանքագրի մանրամասներ", "is_percentage": "կազմում է", + "keys": "Ստեղներ", "last_30_days": "Վերջին 30 օրը", "learn_more": "Տեղեկանալ ավելին", "ledger_connection_error": "Չկարողացանք կապ հաստատել Ledger-ի հետ: Խնդրում ենք փորձել նորից", @@ -389,6 +392,7 @@ "ledger_error_tx_rejected_by_user": "Գործարքը մերժված է օգտատերի կողմից", "ledger_error_wrong_app": "Խնդրում ենք համոզվել, որ դուք բացել եք ճիշտ ծրագիրը ձեր Ledger-ում", "ledger_please_enable_bluetooth": "Խնդրում ենք միացնել Bluetooth-ը ձեր Ledger-ը հայտնաբերելու համար", + "legacy": "Ժառանգություն", "light_theme": "Լուսավոր", "litecoin_enable_mweb_sync": "Միացնել MWEB սկան", "litecoin_mweb": "Մուեբ", @@ -667,6 +671,7 @@ "select_backup_file": "Ընտրել կրկնօրինակ ֆայլ", "select_buy_provider_notice": "Ընտրեք գնման մատակարարը վերևում։ Դուք կարող եք բաց թողնել այս էկրանը ձեր լռելայն գնման մատակարարը հավելվածի կարգավորումներում սահմանելով", "select_destination": "Խնդրում ենք ընտրել կրկնօրինակ ֆայլի նպատակակետը", + "select_hw_account_below": "Խնդրում ենք ընտրել, թե որ հաշիվն է վերականգնել հետեւյալը.", "select_sell_provider_notice": "Ընտրեք վաճառքի մատակարարը վերևում։ Դուք կարող եք բաց թողնել այս էկրանը ձեր լռելայն վաճառքի մատակարարը հավելվածի կարգավորումներում սահմանելով", "select_your_country": "Խնդրում ենք ընտրել ձեր երկիրը", "sell": "Ծախել", @@ -697,6 +702,7 @@ "service_health_disabled": "Ծառայության առողջությունը անջատված է", "service_health_disabled_message": "Սա ծառայության առողջության էջն է, դուք կարող եք այս էջը միացնել Կարգավորումներ -> Գաղտնիություն", "set_a_pin": "Սեփական քորոց սահմանեք", + "set_up_a_wallet": "Ստեղծեք դրամապանակ", "settings": "Կարգավորումներ", "settings_all": "Բոլորը", "settings_allow_biometrical_authentication": "Թույլատրել կենսաչափական վավերացում", @@ -732,7 +738,7 @@ "share_address": "Կիսվել հասցեով", "shared_seed_wallet_groups": "Համօգտագործված սերմերի դրամապանակների խմբեր", "show": "Ցուցահանդես", - "show_address_book_popup": "Show ույց տալ «Ուղարկելուց հետո« Հասցեների գրքի »թռուցիկ", + "show_address_book_popup": "Show ուցադրել հասցեի գրքի թռուցիկ", "show_balance": "Երկար մամուլ, հավասարակշռությունը ցույց տալու համար", "show_balance_toast": "Երկար սեղմեք `հավասարակշռությունը թաքցնելու կամ ցույց տալու համար", "show_details": "Ցուցադրել մանրամասներ", @@ -781,6 +787,7 @@ "support_title_guides": "Տորթ դրամապանակի փաստաթղթեր", "support_title_live_chat": "Անմիջական աջակցություն", "support_title_other_links": "Այլ աջակցության հղումներ", + "swap": "Փոխանակել", "sweeping_wallet": "Դրամապանակը մաքրվում է", "sweeping_wallet_alert": "Սա չի տևի երկար։ Խնդրում ենք չլքել այս էկրանը կամ մաքրված միջոցները կկորչեն։", "switchToETHWallet": "Խնդրում ենք անցնել Ethereum դրամապանակ և փորձել կրկին", @@ -806,6 +813,7 @@ "testnet_coins_no_value": "Testnet արժույթները չունեն արժեք", "third_intro_content": "Yats-ը ապրում է Cake Wallet-ի դրսում ևս: Երկրի ցանկացած դրամապանակի հասցե կարող է փոխարինվել Yat-ով!", "third_intro_title": "Yat-ը լավ է համագործակցում ուրիշների հետ", + "this_pair_is_not_supported_warning": "Այս զույգը չի ապահովվում ներկայումս ընտրված փոխանակման (ներ) ի հետ: Խնդրում ենք ընտրել մեկ այլ փոխանակում:", "thorchain_contract_address_not_supported": "THORChain-ը չի աջակցում պայմանագրի հասցե ուղարկելուն", "thorchain_taproot_address_not_supported": "ThorChain մատակարարը չի աջակցում Taproot հասցեները: Խնդրում ենք փոխել հասցեն կամ ընտրել այլ մատակարար:", "time": "${minutes}ր ${seconds}վ", @@ -929,10 +937,13 @@ "waitFewSecondForTxUpdate": "Խնդրում ենք սպասել մի քանի վայրկյան, որպեսզի գործարքը արտացոլվի գործարքների պատմության մեջ", "wallet": "Դրամապանակ", "wallet_group": "Դրամապանակների խումբ", + "wallet_group_description_existing_seed": "Դուք ընտրել եք օգտագործել այս դրամապանակի համար գոյություն ունեցող սերմը: Կարող եք կրկին հաստատել սերմը, եթե անհրաժեշտ է հաստատել կամ գրել այն:", "wallet_group_description_four": "Ամբողջովին նոր սերմով դրամապանակ ստեղծելու համար:", "wallet_group_description_one": "Տորթի դրամապանակում կարող եք ստեղծել ա", + "wallet_group_description_open_wallet": "Հակառակ դեպքում, դուք կարող եք շարունակել բացել դրամապանակը", "wallet_group_description_three": "Տեսնել առկա դրամապանակներն ու (կամ) դրամապանակների խմբերի էկրանը: Կամ ընտրել", "wallet_group_description_two": "ընտրելով գոյություն ունեցող դրամապանակ `սերմը կիսելու համար: Դրամապանակների յուրաքանչյուր խումբ կարող է պարունակել յուրաքանչյուր արժույթի տեսակի մեկ դրամապանակ:\n\nԿարող եք ընտրել", + "wallet_group_description_view_seed": "Միշտ կարող եք կրկին դիտել այս սերմը ներքեւում", "wallet_group_empty_state_text_one": "Կարծես թե որեւէ համատեղելի դրամապանակի խմբեր չունեք:\n\nԹակել", "wallet_group_empty_state_text_two": "ներքեւում `նորը կազմելու համար:", "wallet_keys": "Դրամապանակի սերմ/բանալիներ", diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index 6c34e78d6..2d32719a8 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -84,6 +84,7 @@ "bitcoin_dark_theme": "Tema Gelap Bitcoin", "bitcoin_light_theme": "Tema Cahaya Bitcoin", "bitcoin_payments_require_1_confirmation": "Pembayaran Bitcoin memerlukan 1 konfirmasi, yang bisa memakan waktu 20 menit atau lebih. Terima kasih atas kesabaran Anda! Anda akan diemail saat pembayaran dikonfirmasi.", + "block_height": "Tinggi blok", "block_remaining": "1 blok tersisa", "Blocks_remaining": "${status} Blok Tersisa", "bluetooth": "Bluetooth", @@ -131,6 +132,7 @@ "change_rep": "Ubah Perwakilan", "change_rep_message": "Apakah Anda yakin ingin mengubah perwakilan?", "change_rep_successful": "Berhasil mengubah perwakilan", + "change_selected_exchanges": "Ubah pertukaran yang dipilih", "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", @@ -296,7 +298,7 @@ "etherscan_history": "Sejarah Etherscan", "event": "Peristiwa", "events": "Acara", - "exchange": "Menukar", + "exchange": "Menukarkan", "exchange_incorrect_current_wallet_for_xmr": "Jika Anda ingin bertukar XMR dari Saldo Monero Dompet Kue Anda, silakan beralih ke Monero Wallet Anda terlebih dahulu.", "exchange_new_template": "Template baru", "exchange_provider_unsupported": "${providerName} tidak lagi didukung!", @@ -382,6 +384,7 @@ "invalid_password": "Kata sandi salah", "invoice_details": "Detail faktur", "is_percentage": "adalah", + "keys": "Kunci", "last_30_days": "30 hari terakhir", "learn_more": "Pelajari Lebih Lanjut", "ledger_connection_error": "Gagal terhubung ke buku besar Anda. Tolong coba lagi.", @@ -389,6 +392,7 @@ "ledger_error_tx_rejected_by_user": "Transaksi ditolak pada perangkat", "ledger_error_wrong_app": "Pastikan Anda membuka aplikasi yang tepat di buku besar Anda", "ledger_please_enable_bluetooth": "Harap aktifkan Bluetooth untuk mendeteksi buku besar Anda", + "legacy": "Warisan", "light_theme": "Terang", "litecoin_enable_mweb_sync": "Aktifkan pemindaian MWEB", "litecoin_mweb": "Mweb", @@ -670,6 +674,7 @@ "select_backup_file": "Pilih file cadangan", "select_buy_provider_notice": "Pilih penyedia beli di atas. Anda dapat melewatkan layar ini dengan mengatur penyedia pembelian default Anda di pengaturan aplikasi.", "select_destination": "Silakan pilih tujuan untuk file cadangan.", + "select_hw_account_below": "Pilih akun mana yang akan dikembalikan di bawah ini:", "select_sell_provider_notice": "Pilih penyedia jual di atas. Anda dapat melewati layar ini dengan mengatur penyedia penjualan default Anda di pengaturan aplikasi.", "select_your_country": "Pilih negara Anda", "sell": "Jual", @@ -700,6 +705,7 @@ "service_health_disabled": "Buletin Kesehatan Layanan dinonaktifkan", "service_health_disabled_message": "Ini adalah halaman Buletin Kesehatan Layanan, Anda dapat mengaktifkan halaman ini di bawah Pengaturan -> Privasi", "set_a_pin": "Atur pin", + "set_up_a_wallet": "Siapkan dompet", "settings": "Pengaturan", "settings_all": "SEMUA", "settings_allow_biometrical_authentication": "Izinkan otentikasi biometrik", @@ -735,7 +741,7 @@ "share_address": "Bagikan alamat", "shared_seed_wallet_groups": "Kelompok dompet benih bersama", "show": "Menunjukkan", - "show_address_book_popup": "Tampilkan popup 'Tambahkan ke Alamat' setelah mengirim", + "show_address_book_popup": "Tampilkan Alamat Buku Popup", "show_balance": "PRESS PANJANG UNTUK MENUNJUKKAN Balance", "show_balance_toast": "Tekan panjang untuk menyembunyikan atau menunjukkan keseimbangan", "show_details": "Tampilkan Rincian", @@ -784,6 +790,7 @@ "support_title_guides": "DOKS DOKO CAKE", "support_title_live_chat": "Dukungan langsung", "support_title_other_links": "Tautan dukungan lainnya", + "swap": "Menukar", "sweeping_wallet": "Dompet menyapu", "sweeping_wallet_alert": "Ini seharusnya tidak memakan waktu lama. Jangan tinggalkan layar ini atau dana swept mungkin hilang.", "switchToETHWallet": "Silakan beralih ke dompet Ethereum dan coba lagi", @@ -809,6 +816,7 @@ "testnet_coins_no_value": "Koin TestNet tidak memiliki nilai", "third_intro_content": "Yats hidup di luar Cake Wallet juga. Setiap alamat dompet di dunia dapat diganti dengan Yat!", "third_intro_title": "Yat bermain baik dengan yang lain", + "this_pair_is_not_supported_warning": "Pasangan ini tidak didukung dengan pertukaran yang saat ini dipilih. Pilih pertukaran lain.", "thorchain_contract_address_not_supported": "Thorchain tidak mendukung pengiriman ke alamat kontrak", "thorchain_taproot_address_not_supported": "Penyedia Thorchain tidak mendukung alamat Taproot. Harap ubah alamatnya atau pilih penyedia yang berbeda.", "time": "${minutes}m ${seconds}s", @@ -932,10 +940,13 @@ "waitFewSecondForTxUpdate": "Mohon tunggu beberapa detik hingga transaksi terlihat di riwayat transaksi", "wallet": "Dompet", "wallet_group": "Kelompok dompet", + "wallet_group_description_existing_seed": "Anda telah memilih untuk menggunakan benih yang ada untuk dompet ini. Anda dapat memverifikasi benih lagi jika Anda perlu mengonfirmasi atau menuliskannya.", "wallet_group_description_four": "Untuk membuat dompet dengan benih yang sama sekali baru.", "wallet_group_description_one": "Di dompet kue, Anda dapat membuat file", + "wallet_group_description_open_wallet": "Jika tidak, Anda dapat terus membuka dompet", "wallet_group_description_three": "Untuk melihat layar dompet dan/atau grup dompet yang tersedia. Atau pilih", "wallet_group_description_two": "dengan memilih dompet yang ada untuk berbagi benih dengan. Setiap grup dompet dapat berisi satu dompet dari setiap jenis mata uang. \n\n Anda dapat memilih", + "wallet_group_description_view_seed": "Anda selalu dapat melihat benih ini lagi di bawah", "wallet_group_empty_state_text_one": "Sepertinya Anda tidak memiliki grup dompet yang kompatibel !\n\n tap", "wallet_group_empty_state_text_two": "di bawah ini untuk membuat yang baru.", "wallet_keys": "Seed/kunci dompet", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index 62c4f8f4b..9bae9e3b0 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -84,6 +84,7 @@ "bitcoin_dark_theme": "Tema oscuro di Bitcoin", "bitcoin_light_theme": "Tema luce Bitcoin", "bitcoin_payments_require_1_confirmation": "I pagamenti in bitcoin richiedono 1 conferma, che può richiedere 20 minuti o più. Grazie per la vostra pazienza! Riceverai un'e-mail quando il pagamento sarà confermato.", + "block_height": "Altezza del blocco", "block_remaining": "1 blocco rimanente", "Blocks_remaining": "${status} Blocchi Rimanenti", "bluetooth": "Bluetooth", @@ -131,6 +132,7 @@ "change_rep": "Cambia rappresentante", "change_rep_message": "Sei sicuro di voler cambiare rappresentante?", "change_rep_successful": "Rappresentante modificato con successo", + "change_selected_exchanges": "Modificare gli scambi selezionati", "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", @@ -383,6 +385,7 @@ "invalid_password": "Password non valida", "invoice_details": "Dettagli della fattura", "is_percentage": "è", + "keys": "Tasti", "last_30_days": "Ultimi 30 giorni", "learn_more": "Impara di più", "ledger_connection_error": "Impossibile connettersi al libro mastro. Per favore riprova.", @@ -390,6 +393,7 @@ "ledger_error_tx_rejected_by_user": "Transazione rifiutata sul dispositivo", "ledger_error_wrong_app": "Assicurati di aprire l'app giusta sul libro mastro", "ledger_please_enable_bluetooth": "Si prega di consentire al Bluetooth di rilevare il libro mastro", + "legacy": "Eredità", "light_theme": "Bianco", "litecoin_enable_mweb_sync": "Abilita la scansione MWeb", "litecoin_mweb": "MWeb", @@ -669,6 +673,7 @@ "select_backup_file": "Seleziona file di backup", "select_buy_provider_notice": "Seleziona un fornitore di acquisto sopra. È possibile saltare questa schermata impostando il provider di acquisto predefinito nelle impostazioni dell'app.", "select_destination": "Seleziona la destinazione per il file di backup.", + "select_hw_account_below": "Seleziona quale account ripristina di seguito:", "select_sell_provider_notice": "Seleziona un fornitore di vendita sopra. Puoi saltare questa schermata impostando il tuo fornitore di vendita predefinito nelle impostazioni dell'app.", "select_your_country": "Seleziona il tuo paese", "sell": "Vendere", @@ -699,6 +704,7 @@ "service_health_disabled": "Il Bollettino sanitario di servizio è disabilitato", "service_health_disabled_message": "Questa è la pagina del Bollettino sanitario del servizio, è possibile abilitare questa pagina in Impostazioni -> Privacy", "set_a_pin": "Imposta un pin", + "set_up_a_wallet": "Imposta un portafoglio", "settings": "Impostazioni", "settings_all": "TUTTO", "settings_allow_biometrical_authentication": "Consenti autenticazione biometrica", @@ -734,7 +740,7 @@ "share_address": "Condividi indirizzo", "shared_seed_wallet_groups": "Gruppi di portafoglio di semi condivisi", "show": "Spettacolo", - "show_address_book_popup": "Mostra il popup \"Aggiungi alla rubrica\" ​​dopo l'invio", + "show_address_book_popup": "Mostra popup della rubrica", "show_balance": "Lunga stampa per mostrare l'equilibrio", "show_balance_toast": "A lungo pressa per nascondere o mostrare l'equilibrio", "show_details": "Mostra dettagli", @@ -783,6 +789,7 @@ "support_title_guides": "Documenti del portafoglio per torta", "support_title_live_chat": "Supporto dal vivo", "support_title_other_links": "Altri collegamenti di supporto", + "swap": "Scambio", "sweeping_wallet": "Portafoglio ampio", "sweeping_wallet_alert": "Questo non dovrebbe richiedere molto tempo. NON LASCIARE QUESTA SCHERMATA O I FONDI SPAZZATI POTREBBERO ANDARE PERSI", "switchToETHWallet": "Passa a un portafoglio Ethereum e riprova", @@ -808,6 +815,7 @@ "testnet_coins_no_value": "Le monete TestNet non hanno valore", "third_intro_content": "Yat può funzionare anche fuori da Cake Wallet. Qualsiasi indirizzo di portafoglio sulla terra può essere sostituito con uno Yat!", "third_intro_title": "Yat gioca bene con gli altri", + "this_pair_is_not_supported_warning": "Questa coppia non è supportata con gli scambi attualmente selezionati. Seleziona un altro scambio.", "thorchain_contract_address_not_supported": "Thorchain non supporta l'invio a un indirizzo contrattuale", "thorchain_taproot_address_not_supported": "Il provider di Thorchain non supporta gli indirizzi di TapRoot. Si prega di modificare l'indirizzo o selezionare un fornitore diverso.", "time": "${minutes}m ${seconds}s", @@ -932,10 +940,13 @@ "waiting_payment_confirmation": "In attesa di conferma del pagamento", "wallet": "Portafoglio", "wallet_group": "Gruppo di portafoglio", + "wallet_group_description_existing_seed": "Hai scelto di utilizzare un seme esistente per questo portafoglio. Puoi verificare di nuovo il seme se devi confermarlo o scriverlo.", "wallet_group_description_four": "Per creare un portafoglio con un seme completamente nuovo.", "wallet_group_description_one": "Nel portafoglio di torte, puoi creare un", + "wallet_group_description_open_wallet": "Altrimenti, puoi continuare ad aprire il portafoglio", "wallet_group_description_three": "Per vedere la schermata di portafogli e/o gruppi di portafogli disponibili. O scegli", "wallet_group_description_two": "Selezionando un portafoglio esistente con cui condividere un seme. Ogni gruppo di portafoglio può contenere un singolo portafoglio di ciascun tipo di valuta. \n\n È possibile selezionare", + "wallet_group_description_view_seed": "Puoi sempre visualizzare di nuovo questo seme sotto", "wallet_group_empty_state_text_one": "Sembra che tu non abbia alcun gruppo di portafoglio compatibile !\n\n TAP", "wallet_group_empty_state_text_two": "Di seguito per crearne uno nuovo.", "wallet_keys": "Seme Portafoglio /chiavi", diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index d589a4166..468549376 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -84,6 +84,7 @@ "bitcoin_dark_theme": "ビットコインダークテーマ", "bitcoin_light_theme": "ビットコインライトテーマ", "bitcoin_payments_require_1_confirmation": "ビットコインの支払いには 1 回の確認が必要で、これには 20 分以上かかる場合があります。お待ち頂きまして、ありがとうございます!支払いが確認されると、メールが送信されます。", + "block_height": "ブロックの高さ", "block_remaining": "残り1ブロック", "Blocks_remaining": "${status} 残りのブロック", "bluetooth": "ブルートゥース", @@ -131,6 +132,7 @@ "change_rep": "代表者の変更", "change_rep_message": "代表者を変更してもよろしいですか?", "change_rep_successful": "代表者の変更に成功しました", + "change_selected_exchanges": "選択した交換を変更します", "change_wallet_alert_content": "現在のウォレットをに変更しますか ${wallet_name}?", "change_wallet_alert_title": "現在のウォレットを変更する", "choose_a_payment_method": "支払い方法を選択します", @@ -296,7 +298,7 @@ "etherscan_history": "イーサスキャンの歴史", "event": "イベント", "events": "イベント", - "exchange": "スワップ", + "exchange": "交換", "exchange_incorrect_current_wallet_for_xmr": "XMRをケーキウォレットモネロバランスから交換したい場合は、最初にMoneroウォレットに切り替えてください。", "exchange_new_template": "新しいテンプレート", "exchange_provider_unsupported": "${providerName}はサポートされなくなりました!", @@ -383,6 +385,7 @@ "invalid_password": "無効なパスワード", "invoice_details": "請求の詳細", "is_percentage": "is", + "keys": "キー", "last_30_days": "過去30日", "learn_more": "もっと詳しく知る", "ledger_connection_error": "元帳に接続できませんでした。もう一度やり直してください。", @@ -390,6 +393,7 @@ "ledger_error_tx_rejected_by_user": "トランザクションはデバイスで拒否されました", "ledger_error_wrong_app": "元帳に適切なアプリを開始するようにしてください", "ledger_please_enable_bluetooth": "Bluetoothが元帳を検出できるようにしてください", + "legacy": "遺産", "light_theme": "光", "litecoin_enable_mweb_sync": "MWEBスキャンを有効にします", "litecoin_mweb": "mweb", @@ -668,6 +672,7 @@ "select_backup_file": "バックアップファイルを選択", "select_buy_provider_notice": "上記の購入プロバイダーを選択してください。デフォルトの購入プロバイダーをアプリ設定で設定して、この画面をスキップできます。", "select_destination": "バックアップファイルの保存先を選択してください。", + "select_hw_account_below": "以下に復元するアカウントを選択してください。", "select_sell_provider_notice": "上記の販売プロバイダーを選択してください。アプリ設定でデフォルトの販売プロバイダーを設定することで、この画面をスキップできます。", "select_your_country": "あなたの国を選択してください", "sell": "売る", @@ -698,6 +703,7 @@ "service_health_disabled": "サービスヘルス速報は無効です", "service_health_disabled_message": "これはService Health Bulletinページです。設定の下でこのページを有効にすることができます - >プライバシー", "set_a_pin": "ピンを設定します", + "set_up_a_wallet": "ウォレットをセットアップします", "settings": "設定", "settings_all": "すべて", "settings_allow_biometrical_authentication": "生体認証を許可する", @@ -733,7 +739,7 @@ "share_address": "住所を共有する", "shared_seed_wallet_groups": "共有シードウォレットグループ", "show": "見せる", - "show_address_book_popup": "送信後に「アドレスブックに追加」ポップアップを表示します", + "show_address_book_popup": "アドレス帳のポップアップを表示します", "show_balance": "バランスを示すためにロングプレス", "show_balance_toast": "バランスを隠したり表示したりするためにロングプレス", "show_details": "詳細を表示", @@ -782,6 +788,7 @@ "support_title_guides": "ケーキウォレットドキュメント", "support_title_live_chat": "ライブサポート", "support_title_other_links": "その他のサポートリンク", + "swap": "スワップ", "sweeping_wallet": "スイープウォレット", "sweeping_wallet_alert": "これには時間がかかりません。この画面から離れないでください。そうしないと、スイープ ファンドが失われる可能性があります", "switchToETHWallet": "イーサリアムウォレットに切り替えてもう一度お試しください", @@ -807,6 +814,7 @@ "testnet_coins_no_value": "テストネットコインには価値がありません", "third_intro_content": "YatsはCakeWalletの外にも住んでいます。 地球上のどのウォレットアドレスもYatに置き換えることができます!", "third_intro_title": "Yatは他の人とうまく遊ぶ", + "this_pair_is_not_supported_warning": "このペアは、現在選択されている取引所ではサポートされていません。別の交換を選択してください。", "thorchain_contract_address_not_supported": "Thorchainは、契約アドレスへの送信をサポートしていません", "thorchain_taproot_address_not_supported": "Thorchainプロバイダーは、TapRootアドレスをサポートしていません。アドレスを変更するか、別のプロバイダーを選択してください。", "time": "${minutes}m ${seconds}s", @@ -930,10 +938,13 @@ "waitFewSecondForTxUpdate": "取引履歴に取引が反映されるまで数秒お待ちください。", "wallet": "財布", "wallet_group": "ウォレットグループ", + "wallet_group_description_existing_seed": "この財布に既存の種子を使用することを選択しました。確認または書き留める必要がある場合は、シードをもう一度確認できます。", "wallet_group_description_four": "まったく新しい種子の財布を作成します。", "wallet_group_description_one": "ケーキウォレットでは、aを作成できます", + "wallet_group_description_open_wallet": "それ以外の場合は、ウォレットを開き続けることができます", "wallet_group_description_three": "利用可能なウォレットおよび/またはウォレットグループの画面を表示します。または選択します", "wallet_group_description_two": "既存のウォレットを選択して種子を共有します。各ウォレットグループには、各通貨タイプの単一のウォレットを含めることができます。\n\n選択できます", + "wallet_group_description_view_seed": "いつでもこの種を再び見ることができます", "wallet_group_empty_state_text_one": "互換性のあるウォレットグループがないようです!\n\nタップ", "wallet_group_empty_state_text_two": "以下に新しいものを作るために。", "wallet_keys": "ウォレットシード/キー", diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index 57163d2b6..51dba77ec 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -84,6 +84,7 @@ "bitcoin_dark_theme": "비트코인 다크 테마", "bitcoin_light_theme": "비트코인 라이트 테마", "bitcoin_payments_require_1_confirmation": "비트코인 결제는 1번의 확인이 필요하며 20분 이상이 소요될 수 있습니다. 기다려 주셔서 감사합니다! 결제가 확인되면 이메일이 전송됩니다.", + "block_height": "블록 높이", "block_remaining": "남은 블록 1 개", "Blocks_remaining": "${status} 남은 블록", "bluetooth": "블루투스", @@ -131,6 +132,7 @@ "change_rep": "대표를 변경하십시오", "change_rep_message": "대표를 바꾸고 싶습니까?", "change_rep_successful": "대리인이 성공적으로 변경되었습니다", + "change_selected_exchanges": "선택된 거래소 변경", "change_wallet_alert_content": "현재 지갑을 다음으로 변경 하시겠습니까 ${wallet_name}?", "change_wallet_alert_title": "현재 지갑 변경", "choose_a_payment_method": "결제 방법을 선택하십시오", @@ -382,6 +384,7 @@ "invalid_password": "유효하지 않은 비밀번호", "invoice_details": "인보이스 세부정보", "is_percentage": "이다", + "keys": "키", "last_30_days": "지난 30일", "learn_more": "더 알아보기", "ledger_connection_error": "원장에 연결하지 못했습니다. 다시 시도하십시오.", @@ -389,6 +392,7 @@ "ledger_error_tx_rejected_by_user": "장치에서 거래가 거부되었습니다", "ledger_error_wrong_app": "원장에서 올바른 앱을 반대하는지 확인하십시오.", "ledger_please_enable_bluetooth": "Bluetooth가 원장을 감지 할 수 있도록하십시오", + "legacy": "유산", "light_theme": "빛", "litecoin_enable_mweb_sync": "mweb 스캔을 활성화합니다", "litecoin_mweb": "mweb", @@ -515,7 +519,6 @@ "please_fill_totp": "다른 기기에 있는 8자리 코드를 입력하세요.", "please_make_selection": "아래에서 선택하십시오 지갑 만들기 또는 복구.", "please_reference_document": "자세한 내용은 아래 문서를 참조하십시오.", - "Please_reference_document": "자세한 내용은 아래 문서를 참조하십시오.", "please_select": "선택 해주세요:", "please_select_backup_file": "백업 파일을 선택하고 백업 암호를 입력하십시오.", "please_try_to_connect_to_another_node": "다른 노드에 연결을 시도하십시오", @@ -668,6 +671,7 @@ "select_backup_file": "백업 파일 선택", "select_buy_provider_notice": "위의 구매 제공자를 선택하십시오. 앱 설정에서 기본 구매 제공자를 설정 하여이 화면을 건너 뛸 수 있습니다.", "select_destination": "백업 파일의 대상을 선택하십시오.", + "select_hw_account_below": "아래를 복원 할 계정을 선택하십시오.", "select_sell_provider_notice": "위에서 판매 공급자를 선택하세요. 앱 설정에서 기본 판매 공급자를 설정하면 이 화면을 건너뛸 수 있습니다.", "select_your_country": "국가를 선택하십시오", "sell": "팔다", @@ -698,6 +702,7 @@ "service_health_disabled": "서비스 건강 게시판이 장애가되었습니다", "service_health_disabled_message": "이것은 서비스 건강 게시판 페이지입니다. 설정 에서이 페이지를 활성화 할 수 있습니다 -> 개인 정보", "set_a_pin": "핀을 설정하십시오", + "set_up_a_wallet": "지갑을 설정하십시오", "settings": "설정", "settings_all": "모든", "settings_allow_biometrical_authentication": "생체 인증 허용", @@ -733,7 +738,7 @@ "share_address": "주소 공유", "shared_seed_wallet_groups": "공유 종자 지갑 그룹", "show": "보여주다", - "show_address_book_popup": "전송 후 '주소 책에 추가'팝업을 표시하십시오", + "show_address_book_popup": "주소록 팝업을 보여주십시오", "show_balance": "균형을 보여주기 위해 긴 언론", "show_balance_toast": "균형을 숨기거나 보여주기 위해 긴 누르십시오", "show_details": "세부정보 표시", @@ -782,6 +787,7 @@ "support_title_guides": "케이크 지갑 문서", "support_title_live_chat": "실시간 지원", "support_title_other_links": "다른 지원 링크", + "swap": "교환", "sweeping_wallet": "스위핑 지갑", "sweeping_wallet_alert": "오래 걸리지 않습니다. 이 화면을 떠나지 마십시오. 그렇지 않으면 스웹트 자금이 손실될 수 있습니다.", "switchToETHWallet": "이더리움 지갑으로 전환한 후 다시 시도해 주세요.", @@ -807,6 +813,7 @@ "testnet_coins_no_value": "Testnet 코인은 가치가 없습니다", "third_intro_content": "Yats는 Cake Wallet 밖에서도 살고 있습니다. 지구상의 모든 지갑 주소는 Yat!", "third_intro_title": "Yat는 다른 사람들과 잘 놉니다.", + "this_pair_is_not_supported_warning": "이 쌍은 현재 선택된 교환으로 지원되지 않습니다. 다른 교환을 선택하십시오.", "thorchain_contract_address_not_supported": "Thorchain은 계약 주소로 보내는 것을 지원하지 않습니다", "thorchain_taproot_address_not_supported": "Thorchain 제공 업체는 Taproot 주소를 지원하지 않습니다. 주소를 변경하거나 다른 공급자를 선택하십시오.", "time": "${minutes}m ${seconds}s", @@ -930,10 +937,13 @@ "waitFewSecondForTxUpdate": "거래 내역에 거래가 반영될 때까지 몇 초 정도 기다려 주세요.", "wallet": "지갑", "wallet_group": "지갑 그룹", + "wallet_group_description_existing_seed": "이 지갑에 기존 씨앗을 사용하기로 선택했습니다. 확인하거나 작성 해야하는 경우 씨앗을 다시 확인할 수 있습니다.", "wallet_group_description_four": "완전히 새로운 씨앗으로 지갑을 만듭니다.", "wallet_group_description_one": "케이크 지갑에서는 a를 만들 수 있습니다", + "wallet_group_description_open_wallet": "그렇지 않으면 지갑을 계속 열 수 있습니다", "wallet_group_description_three": "사용 가능한 지갑 및/또는 지갑 그룹 스크린을 볼 수 있습니다. 또는 선택하십시오", "wallet_group_description_two": "씨앗을 공유 할 기존 지갑을 선택함으로써. 각 지갑 그룹은 각 통화 유형의 단일 지갑을 포함 할 수 있습니다. \n\n", + "wallet_group_description_view_seed": "이 씨앗을 언제든지 다시 볼 수 있습니다", "wallet_group_empty_state_text_one": "호환 지갑 그룹이없는 것 같습니다 !\n\n TAP", "wallet_group_empty_state_text_two": "아래에서 새로운 것을 만들기 위해.", "wallet_keys": "지갑 시드 / 키", diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index b62b659d0..a40b221c4 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -84,6 +84,7 @@ "bitcoin_dark_theme": "Bitcoin Dark Theme", "bitcoin_light_theme": "Bitcoin Light အပြင်အဆင်", "bitcoin_payments_require_1_confirmation": "Bitcoin ငွေပေးချေမှုများသည် မိနစ် 20 သို့မဟုတ် ထို့ထက်ပိုကြာနိုင်သည် 1 အတည်ပြုချက် လိုအပ်သည်။ မင်းရဲ့စိတ်ရှည်မှုအတွက် ကျေးဇူးတင်ပါတယ်။ ငွေပေးချေမှုကို အတည်ပြုပြီးသောအခါ သင့်ထံ အီးမေးလ်ပို့ပါမည်။", + "block_height": "ပိတ်ပင်တားဆီးမှုအမြင့်", "block_remaining": "ကျန်ရှိနေသေးသော block", "Blocks_remaining": "${status} ဘလောက်များ ကျန်နေပါသည်။", "bluetooth": "ဘလူးတုသ်", @@ -131,6 +132,7 @@ "change_rep": "ကိုယ်စားလှယ်ပြောင်းပါ။", "change_rep_message": "ကိုယ်စားလှယ်ပြောင်းလိုသည်မှာ သေချာပါသလား။", "change_rep_successful": "အောင်မြင်စွာကိုယ်စားလှယ်ပြောင်းလဲသွားတယ်", + "change_selected_exchanges": "ရွေးချယ်ထားသောအပြန်အလှန်ဖလှယ်မှုကိုပြောင်းလဲပါ", "change_wallet_alert_content": "လက်ရှိပိုက်ဆံအိတ်ကို ${wallet_name} သို့ ပြောင်းလိုပါသလား။", "change_wallet_alert_title": "လက်ရှိပိုက်ဆံအိတ်ကို ပြောင်းပါ။", "choose_a_payment_method": "ငွေပေးချေမှုနည်းလမ်းကိုရွေးချယ်ပါ", @@ -382,6 +384,7 @@ "invalid_password": "မမှန်ကန်သောစကားဝှက်", "invoice_details": "ပြေစာအသေးစိတ်", "is_percentage": "သည်", + "keys": "သော့များ", "last_30_days": "လွန်ခဲ့သော ရက် 30", "learn_more": "ပိုမိုသိရှိရန်", "ledger_connection_error": "သငျသညျ Ledger နှင့်ချိတ်ဆက်ရန်မအောင်မြင်ပါ။ ကျေးဇူးပြုပြီးထပ်ကြိုးစားပါ", @@ -389,6 +392,7 @@ "ledger_error_tx_rejected_by_user": "ငွေပေးငွေယူ device ကိုအပေါ်ငြင်းပယ်ခဲ့သည်", "ledger_error_wrong_app": "ကျေးဇူးပြု. သင့်လက်ျာအက်ပ်ကိုသင်၏ Ledger တွင်ဖွင့်ရန်သေချာစေပါ", "ledger_please_enable_bluetooth": "သင်၏ Ledger ကိုရှာဖွေရန် Bluetooth ကိုဖွင့်ပါ", + "legacy": "အနေှးမနေသော", "light_theme": "အလင်း", "litecoin_enable_mweb_sync": "mweb scanning ဖွင့်ပါ", "litecoin_mweb": "မင်္ဂလာပါ", @@ -667,6 +671,7 @@ "select_backup_file": "အရန်ဖိုင်ကို ရွေးပါ။", "select_buy_provider_notice": "အပေါ်ကဝယ်သူတစ် ဦး ကိုရွေးချယ်ပါ။ သင်၏ default 0 ယ်သူအား app settings တွင် setting လုပ်ခြင်းဖြင့်ဤ screen ကိုကျော်သွားနိုင်သည်။", "select_destination": "အရန်ဖိုင်အတွက် ဦးတည်ရာကို ရွေးပါ။", + "select_hw_account_below": "အောက်ဖော်ပြပါမည်သည့်အကောင့်ကိုရွေးပါ။", "select_sell_provider_notice": "အထက်ဖော်ပြပါ အရောင်းဝန်ဆောင်မှုပေးသူကို ရွေးပါ။ အက်ပ်ဆက်တင်များတွင် သင်၏မူလရောင်းချပေးသူကို သတ်မှတ်ခြင်းဖြင့် ဤစခရင်ကို ကျော်နိုင်သည်။", "select_your_country": "ကျေးဇူးပြု. သင့်နိုင်ငံကိုရွေးချယ်ပါ", "sell": "ရောင်း", @@ -697,6 +702,7 @@ "service_health_disabled": "ဝန်ဆောင်မှုကျန်းမာရေးစာစောင်အားပိတ်ထားသည်", "service_health_disabled_message": "ဤသည်မှာ 0 န်ဆောင်မှုကျန်းမာရေးစာစောင်စာမျက်နှာတွင်ဤစာမျက်နှာကို Settings အောက်တွင်ဖွင့်ထားနိုင်သည်", "set_a_pin": "PIN နံပါတ်ကိုသတ်မှတ်ပါ", + "set_up_a_wallet": "ပိုက်ဆံအိတ်တစ်ခု set up", "settings": "ဆက်တင်များ", "settings_all": "အားလုံး", "settings_allow_biometrical_authentication": "ဇီဝဗေဒဆိုင်ရာ အထောက်အထားစိစစ်ခြင်းကို ခွင့်ပြုပါ။", @@ -732,7 +738,7 @@ "share_address": "လိပ်စာမျှဝေပါ။", "shared_seed_wallet_groups": "shared မျိုးစေ့ပိုက်ဆံအိတ်အုပ်စုများ", "show": "ပြသ", - "show_address_book_popup": "ပေးပို့ပြီးနောက် 'address book' popup ကိုပြပါ", + "show_address_book_popup": "လိပ်စာစာအုပ် popup ပြပါ", "show_balance": "ချိန်ခွင်လျှာကိုပြသရန်ရှည်လျားသောစာနယ်ဇင်း", "show_balance_toast": "ချိန်ခွင်လျှာကိုဖျောက်ရန်သို့မဟုတ်ပြသရန်ရှည်လျားသောစာနယ်ဇင်း", "show_details": "အသေးစိတ်ပြ", @@ -781,6 +787,7 @@ "support_title_guides": "ကိတ်မုန့်ပိုက်ဆံအိတ်များ", "support_title_live_chat": "တိုက်ရိုက်ပံ့ပိုးမှု", "support_title_other_links": "အခြားအထောက်အပံ့လင့်များ", + "swap": "လဲလှယ်", "sweeping_wallet": "ိုက်ဆံအိတ် တံမြက်လှည်း", "sweeping_wallet_alert": "ဒါက ကြာကြာမခံသင့်ပါဘူး။ ဤစခရင်ကို ချန်မထားပါနှင့် သို့မဟုတ် ထုတ်ယူထားသော ရန်ပုံငွေများ ဆုံးရှုံးနိုင်သည်", "switchToETHWallet": "ကျေးဇူးပြု၍ Ethereum ပိုက်ဆံအိတ်သို့ ပြောင်းပြီး ထပ်စမ်းကြည့်ပါ။", @@ -806,6 +813,7 @@ "testnet_coins_no_value": "Testnet ဒင်္ဂါးပြားတန်ဖိုးမရှိပါ", "third_intro_content": "Yats သည် Cake Wallet အပြင်ဘက်တွင် နေထိုင်ပါသည်။ ကမ္ဘာပေါ်ရှိ မည်သည့်ပိုက်ဆံအိတ်လိပ်စာကို Yat ဖြင့် အစားထိုးနိုင်ပါသည်။", "third_intro_title": "Yat သည် အခြားသူများနှင့် ကောင်းစွာကစားသည်။", + "this_pair_is_not_supported_warning": "ဤစုံတွဲသည်လက်ရှိရွေးချယ်ထားသောလဲလှယ်ခြင်း (များ) ဖြင့်မထောက်ပံ့ပါ။ ကျေးဇူးပြု. အခြားလဲလှယ်မှုကိုရွေးချယ်ပါ။", "thorchain_contract_address_not_supported": "Thorchain သည်စာချုပ်လိပ်စာသို့ပို့ခြင်းမပြုပါ", "thorchain_taproot_address_not_supported": "Thorchain Provider သည် Taproot လိပ်စာများကိုမထောက်ခံပါ။ ကျေးဇူးပြု. လိပ်စာကိုပြောင်းပါသို့မဟုတ်အခြားပံ့ပိုးပေးသူကိုရွေးချယ်ပါ။", "time": "${minutes}m ${seconds}s", @@ -929,10 +937,13 @@ "waitFewSecondForTxUpdate": "ငွေပေးငွေယူ မှတ်တမ်းတွင် ရောင်ပြန်ဟပ်ရန် စက္ကန့်အနည်းငယ်စောင့်ပါ။", "wallet": "ပိုက်ဆံအိတ်", "wallet_group": "ပိုက်ဆံအိတ်အုပ်စု", + "wallet_group_description_existing_seed": "ဒီပိုက်ဆံအိတ်အတွက်ရှိပြီးသားမျိုးစေ့ကိုသုံးဖို့သင်ရွေးချယ်ခဲ့တယ်။ သင်ကအတည်ပြုရန်သို့မဟုတ်ရေးရန်လိုအပ်လျှင်မျိုးစေ့ကိုထပ်မံအတည်ပြုနိုင်သည်။", "wallet_group_description_four": "လုံးဝအသစ်သောမျိုးစေ့နှင့်အတူပိုက်ဆံအိတ်ဖန်တီးရန်။", "wallet_group_description_one": "ကိတ်မုန့်၌, သင်တစ် ဦး ဖန်တီးနိုင်ပါတယ်", + "wallet_group_description_open_wallet": "ဒီလိုမှမဟုတ်ရင်သင်ပိုက်ဆံအိတ်ကိုဆက်ဖွင့်နိုင်တယ်", "wallet_group_description_three": "ရရှိနိုင်သည့်ပိုက်ဆံအိတ်နှင့် / သို့မဟုတ်ပိုက်ဆံအိတ်အုပ်စုများမြင်ကွင်းကိုကြည့်ရှုရန်။ သို့မဟုတ်ရွေးချယ်ပါ", "wallet_group_description_two": "နှင့်အတူမျိုးစေ့ဝေမျှဖို့ရှိပြီးသားပိုက်ဆံအိတ်တစ်ခုရွေးချယ်ခြင်းအားဖြင့်။ ပိုက်ဆံအိတ်အုပ်စုတစ်ခုစီတွင်ငွေကြေးအမျိုးအစားတစ်ခုစီ၏တစ်ခုတည်းသောပိုက်ဆံအိတ်တစ်ခုပါ 0 င်နိုင်သည်။ \n\n သင်ရွေးချယ်နိုင်သည်", + "wallet_group_description_view_seed": "သင်သည်ဤမျိုးစေ့ကိုနောက်တဖန်ရှုမြင်နိုင်သည်", "wallet_group_empty_state_text_one": "သင့်တွင်သဟဇာတဖြစ်သောပိုက်ဆံအိတ်အုပ်စုများမရှိပါ။ !\n\n ကိုအသာပုတ်ပါ", "wallet_group_empty_state_text_two": "အသစ်တစ်ခုကိုတစ်ခုလုပ်ဖို့အောက်တွင်ဖော်ပြထားသော။", "wallet_keys": "ပိုက်ဆံအိတ် အစေ့/သော့များ", diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 3073227e4..00a56d13d 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -84,6 +84,7 @@ "bitcoin_dark_theme": "Bitcoin donker thema", "bitcoin_light_theme": "Bitcoin Light-thema", "bitcoin_payments_require_1_confirmation": "Bitcoin-betalingen vereisen 1 bevestiging, wat 20 minuten of langer kan duren. Dank voor uw geduld! U ontvangt een e-mail wanneer de betaling is bevestigd.", + "block_height": "Blokhoogte", "block_remaining": "1 blok resterend", "Blocks_remaining": "${status} Resterende blokken", "bluetooth": "Bluetooth", @@ -131,6 +132,7 @@ "change_rep": "Vertegenwoordiger wijzigen", "change_rep_message": "Weet u zeker dat u van vertegenwoordiger wilt veranderen?", "change_rep_successful": "Met succes veranderde vertegenwoordiger", + "change_selected_exchanges": "Wijzig geselecteerde uitwisselingen", "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", @@ -296,7 +298,7 @@ "etherscan_history": "Etherscan-geschiedenis", "event": "Evenement", "events": "Evenementen", - "exchange": "Ruil", + "exchange": "Aandelenbeurs", "exchange_incorrect_current_wallet_for_xmr": "Als je XMR uit je cake -portemonnee Monero -balans wilt ruilen, schakel dan eerst over naar je Monero -portemonnee.", "exchange_new_template": "Nieuwe sjabloon", "exchange_provider_unsupported": "${providerName} wordt niet langer ondersteund!", @@ -382,6 +384,7 @@ "invalid_password": "Ongeldig wachtwoord", "invoice_details": "Factuurgegevens", "is_percentage": "is", + "keys": "Sleutels", "last_30_days": "Laatste 30 dagen", "learn_more": "Kom meer te weten", "ledger_connection_error": "Kan geen verbinding maken met u grootboek. Probeer het opnieuw.", @@ -389,6 +392,7 @@ "ledger_error_tx_rejected_by_user": "Transactie afgewezen op apparaat", "ledger_error_wrong_app": "Zorg ervoor dat u de juiste app op uw grootboek opent", "ledger_please_enable_bluetooth": "Schakel Bluetooth in staat om uw grootboek te detecteren", + "legacy": "Nalatenschap", "light_theme": "Licht", "litecoin_enable_mweb_sync": "MWEB -scanning inschakelen", "litecoin_mweb": "Mweb", @@ -667,6 +671,7 @@ "select_backup_file": "Selecteer een back-upbestand", "select_buy_provider_notice": "Selecteer hierboven een koopprovider. U kunt dit scherm overslaan door uw standaard kopenprovider in te stellen in app -instellingen.", "select_destination": "Selecteer de bestemming voor het back-upbestand.", + "select_hw_account_below": "Selecteer welk account u hieronder moet herstellen:", "select_sell_provider_notice": "Selecteer hierboven een verkoopaanbieder. U kunt dit scherm overslaan door uw standaardverkoopprovider in te stellen in de app-instellingen.", "select_your_country": "Selecteer uw land", "sell": "Verkopen", @@ -697,6 +702,7 @@ "service_health_disabled": "Service Health Bulletin is uitgeschakeld", "service_health_disabled_message": "Dit is de Service Health Bulletin -pagina, u kunt deze pagina instellingen inschakelen -> Privacy", "set_a_pin": "Zet een speld", + "set_up_a_wallet": "Zet een portemonnee op", "settings": "Instellingen", "settings_all": "ALLE", "settings_allow_biometrical_authentication": "Biometrische authenticatie toestaan", @@ -732,7 +738,7 @@ "share_address": "Deel adres", "shared_seed_wallet_groups": "Gedeelde zaadportelgroepen", "show": "Show", - "show_address_book_popup": "Toon 'Toevoegen aan adresboek' pop -up na verzenden", + "show_address_book_popup": "Toon adresboek pop -up", "show_balance": "Lange pers om evenwicht te tonen", "show_balance_toast": "Lange pers om evenwicht te verbergen of te tonen", "show_details": "Toon details", @@ -781,6 +787,7 @@ "support_title_guides": "Cake -portemonnee documenten", "support_title_live_chat": "Live ondersteuning", "support_title_other_links": "Andere ondersteuningslinks", + "swap": "Ruil", "sweeping_wallet": "Vegende portemonnee", "sweeping_wallet_alert": "Dit duurt niet lang. VERLAAT DIT SCHERM NIET, ANDERS KAN HET SWEPT-GELD VERLOREN WORDEN", "switchToETHWallet": "Schakel over naar een Ethereum-portemonnee en probeer het opnieuw", @@ -806,6 +813,7 @@ "testnet_coins_no_value": "Testnet -munten hebben geen waarde", "third_intro_content": "Yats wonen ook buiten Cake Wallet. Elk portemonnee-adres op aarde kan worden vervangen door een Yat!", "third_intro_title": "Yat speelt leuk met anderen", + "this_pair_is_not_supported_warning": "Dit paar wordt niet ondersteund met de momenteel geselecteerde uitwisseling (s). Selecteer een andere uitwisseling.", "thorchain_contract_address_not_supported": "Thorchain ondersteunt het verzenden niet naar een contractadres", "thorchain_taproot_address_not_supported": "De Thorchain -provider ondersteunt geen Taprooot -adressen. Wijzig het adres of selecteer een andere provider.", "time": "${minutes}m ${seconds}s", @@ -930,10 +938,13 @@ "waiting_payment_confirmation": "In afwachting van betalingsbevestiging", "wallet": "Portemonnee", "wallet_group": "Portemonnee", + "wallet_group_description_existing_seed": "U hebt ervoor gekozen om een ​​bestaand zaadje voor deze portemonnee te gebruiken. U kunt het zaad opnieuw verifiëren als u het moet bevestigen of opschrijven.", "wallet_group_description_four": "om een ​​portemonnee te maken met een geheel nieuw zaadje.", "wallet_group_description_one": "In cakeballet kun je een", + "wallet_group_description_open_wallet": "Anders kunt u de portemonnee blijven openen", "wallet_group_description_three": "Om de beschikbare portefeuilles en/of portefeuillegroepen te zien. Of kies", "wallet_group_description_two": "Door een bestaande portemonnee te selecteren om een ​​zaadje mee te delen. Elke portemonnee -groep kan een enkele portemonnee van elk valutietype bevatten. \n\n U kunt selecteren", + "wallet_group_description_view_seed": "Je kunt dit zaad altijd opnieuw bekijken", "wallet_group_empty_state_text_one": "Het lijkt erop dat je geen compatibele portemonnee -groepen hebt !\n\n TAP", "wallet_group_empty_state_text_two": "hieronder om een ​​nieuwe te maken.", "wallet_keys": "Portemonnee zaad/sleutels", diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index 6b25f5f33..424860c30 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -84,6 +84,7 @@ "bitcoin_dark_theme": "Ciemny motyw Bitcoina", "bitcoin_light_theme": "Lekki motyw Bitcoin", "bitcoin_payments_require_1_confirmation": "Płatności Bitcoin wymagają 1 potwierdzenia, co może zająć 20 minut lub dłużej. Dziękuję za cierpliwość! Otrzymasz wiadomość e-mail, gdy płatność zostanie potwierdzona.", + "block_height": "Wysokość bloku", "block_remaining": "1 blok pozostałym", "Blocks_remaining": "Pozostało ${status} bloków", "bluetooth": "Bluetooth", @@ -131,6 +132,7 @@ "change_rep": "Zmień przedstawiciela", "change_rep_message": "Czy na pewno chcesz zmienić przedstawiciela?", "change_rep_successful": "Pomyślnie zmienił przedstawiciela", + "change_selected_exchanges": "Zmień wybrane wymiany", "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", @@ -296,7 +298,7 @@ "etherscan_history": "Historia Etherscanu", "event": "Wydarzenie", "events": "Wydarzenia", - "exchange": "Zamieniać", + "exchange": "Giełda", "exchange_incorrect_current_wallet_for_xmr": "Jeśli chcesz zamienić XMR z salda Monero Portfer, najpierw przejdź na portfel Monero.", "exchange_new_template": "Nowy szablon wymiany", "exchange_provider_unsupported": "${providerName} nie jest już obsługiwany!", @@ -382,6 +384,7 @@ "invalid_password": "Nieprawidłowe hasło", "invoice_details": "Dane do faktury", "is_percentage": "jest", + "keys": "Klawiatura", "last_30_days": "Ostatnie 30 dni", "learn_more": "Dowiedz się więcej", "ledger_connection_error": "Nie udało się połączyć z twoją księgą. Proszę spróbuj ponownie.", @@ -389,6 +392,7 @@ "ledger_error_tx_rejected_by_user": "Transakcja odrzucona na urządzeniu", "ledger_error_wrong_app": "Upewnij się, że opisz odpowiednią aplikację na swojej księdze", "ledger_please_enable_bluetooth": "Włącz Bluetooth wykrywanie księgi", + "legacy": "Dziedzictwo", "light_theme": "Jasny", "litecoin_enable_mweb_sync": "Włącz skanowanie MWEB", "litecoin_mweb": "MWEB", @@ -667,6 +671,7 @@ "select_backup_file": "Wybierz plik kopii zapasowej", "select_buy_provider_notice": "Wybierz powyższe dostawcę zakupu. Możesz pominąć ten ekran, ustawiając domyślnego dostawcę zakupu w ustawieniach aplikacji.", "select_destination": "Wybierz miejsce docelowe dla pliku kopii zapasowej.", + "select_hw_account_below": "Wybierz, które konto przywrócić poniżej:", "select_sell_provider_notice": "Wybierz dostawcę sprzedaży powyżej. Możesz pominąć ten ekran, ustawiając domyślnego dostawcę sprzedaży w ustawieniach aplikacji.", "select_your_country": "Wybierz swój kraj", "sell": "Sprzedać", @@ -697,6 +702,7 @@ "service_health_disabled": "Biuletyn zdrowia usług jest wyłączony", "service_health_disabled_message": "To jest strona Biuletynu Zdrowie Service, możesz włączyć tę stronę w Ustawieniach -> Prywatność", "set_a_pin": "Ustaw szpilkę", + "set_up_a_wallet": "Ustaw portfel", "settings": "Ustawienia", "settings_all": "Wszystkie", "settings_allow_biometrical_authentication": "Zezwalaj na uwierzytelnianie biometryczne", @@ -732,7 +738,7 @@ "share_address": "Udostępnij adres", "shared_seed_wallet_groups": "Wspólne grupy portfeli nasion", "show": "Pokazywać", - "show_address_book_popup": "Pokaż wysypkę „Dodaj do książki” po wysłaniu", + "show_address_book_popup": "Pokaż okienko książki adresowej", "show_balance": "Długa prasa, aby pokazać równowagę", "show_balance_toast": "Długa naciśnij, aby ukryć lub pokazać równowagę", "show_details": "Pokaż szczegóły", @@ -781,6 +787,7 @@ "support_title_guides": "Dokumenty portfela ciasta", "support_title_live_chat": "Wsparcie na żywo", "support_title_other_links": "Inne linki wsparcia", + "swap": "Zamieniać", "sweeping_wallet": "Zamiatanie portfela", "sweeping_wallet_alert": "To nie powinno zająć dużo czasu. NIE WYCHODŹ Z TEGO EKRANU, W PRZECIWNYM WYPADKU MOŻE ZOSTAĆ UTRACONA ŚRODKI", "switchToETHWallet": "Przejdź na portfel Ethereum i spróbuj ponownie", @@ -806,6 +813,7 @@ "testnet_coins_no_value": "Monety testowe nie mają wartości", "third_intro_content": "Yats mieszkają również poza Cake Wallet. Każdy adres portfela na ziemi można zastąpić Yat!", "third_intro_title": "Yat ładnie bawi się z innymi", + "this_pair_is_not_supported_warning": "Ta para nie jest obsługiwana z aktualnie wybraną wymianą. Wybierz kolejną wymianę.", "thorchain_contract_address_not_supported": "Thorchain nie wspiera wysyłania na adres umowy", "thorchain_taproot_address_not_supported": "Dostawca Thorchain nie obsługuje adresów TAPROOT. Zmień adres lub wybierz innego dostawcę.", "time": "${minutes}m ${seconds}s", @@ -929,10 +937,13 @@ "waitFewSecondForTxUpdate": "Poczekaj kilka sekund, aż transakcja zostanie odzwierciedlona w historii transakcji", "wallet": "Portfel", "wallet_group": "Grupa portfela", + "wallet_group_description_existing_seed": "Zdecydowałeś się użyć istniejącego ziarna do tego portfela. Możesz ponownie zweryfikować ziarno, jeśli chcesz je potwierdzić lub zapisać.", "wallet_group_description_four": "Aby stworzyć portfel z zupełnie nowym ziarnem.", "wallet_group_description_one": "W portfelu ciasta możesz stworzyć", + "wallet_group_description_open_wallet": "W przeciwnym razie możesz nadal otwierać portfel", "wallet_group_description_three": "Aby zobaczyć dostępny ekran portfeli i/lub grup portfeli. Lub wybierz", "wallet_group_description_two": "Wybierając istniejący portfel do podzielenia nasion. Każda grupa portfela może zawierać pojedynczy portfel każdego typu waluty. \n\n możesz wybrać", + "wallet_group_description_view_seed": "Zawsze możesz ponownie zobaczyć to ziarno pod", "wallet_group_empty_state_text_one": "Wygląda na to, że nie masz żadnych kompatybilnych grup portfeli !\n\n Tap", "wallet_group_empty_state_text_two": "poniżej, aby zrobić nowy.", "wallet_keys": "Klucze portfela", diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index c60ec1142..6a1c7dd76 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -84,6 +84,7 @@ "bitcoin_dark_theme": "Tema escuro Bitcoin", "bitcoin_light_theme": "Tema claro de bitcoin", "bitcoin_payments_require_1_confirmation": "Os pagamentos em Bitcoin exigem 1 confirmação, o que pode levar 20 minutos ou mais. Obrigado pela sua paciência! Você receberá um e-mail quando o pagamento for confirmado.", + "block_height": "Altura do bloco", "block_remaining": "1 bloco restante", "Blocks_remaining": "${status} blocos restantes", "bluetooth": "Bluetooth", @@ -131,6 +132,7 @@ "change_rep": "Alterar representante", "change_rep_message": "Tem certeza de que deseja alterar os representantes?", "change_rep_successful": "Mudou com sucesso o representante", + "change_selected_exchanges": "Altere as trocas selecionadas", "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", @@ -296,7 +298,7 @@ "etherscan_history": "história Etherscan", "event": "Evento", "events": "Eventos", - "exchange": "Trocar", + "exchange": "Intercâmbio", "exchange_incorrect_current_wallet_for_xmr": "Se você deseja trocar o XMR do balanço da carteira de bolo, mude para a sua carteira Monero primeiro.", "exchange_new_template": "Novo modelo", "exchange_provider_unsupported": "${providerName} não é mais suportado!", @@ -382,6 +384,7 @@ "invalid_password": "Senha inválida", "invoice_details": "Detalhes da fatura", "is_percentage": "é", + "keys": "Chaves", "last_30_days": "Últimos 30 dias", "learn_more": "Saber mais", "ledger_connection_error": "Falha ao se conectar ao seu livro. Por favor, tente novamente.", @@ -389,6 +392,7 @@ "ledger_error_tx_rejected_by_user": "Transação rejeitada no dispositivo", "ledger_error_wrong_app": "Por favor, certifique -se de optar pelo aplicativo certo no seu livro", "ledger_please_enable_bluetooth": "Ative o Bluetooth para detectar seu livro", + "legacy": "Legado", "light_theme": "Luz", "litecoin_enable_mweb_sync": "Ativar digitalização do MWEB", "litecoin_mweb": "Mweb", @@ -669,6 +673,7 @@ "select_backup_file": "Selecione o arquivo de backup", "select_buy_provider_notice": "Selecione um provedor de compra acima. Você pode pular esta tela definindo seu provedor de compra padrão nas configurações de aplicativos.", "select_destination": "Selecione o destino para o arquivo de backup.", + "select_hw_account_below": "Selecione qual conta para restaurar abaixo:", "select_sell_provider_notice": "Selecione um fornecedor de venda acima. Você pode pular esta tela definindo seu provedor de venda padrão nas configurações do aplicativo.", "select_your_country": "Selecione seu país", "sell": "Vender", @@ -699,6 +704,7 @@ "service_health_disabled": "O Boletim de Saúde de Serviço está desativado", "service_health_disabled_message": "Esta é a página do Boletim de Saúde de Serviço, você pode ativar esta página em Configurações -> Privacidade", "set_a_pin": "Defina um pino", + "set_up_a_wallet": "Configurar uma carteira", "settings": "Configurações", "settings_all": "Tudo", "settings_allow_biometrical_authentication": "Permitir autenticação biométrica", @@ -734,7 +740,7 @@ "share_address": "Compartilhar endereço", "shared_seed_wallet_groups": "Grupos de carteira de sementes compartilhados", "show": "Mostrar", - "show_address_book_popup": "Mostre pop -up 'Adicionar ao livro de endereços' depois de enviar", + "show_address_book_popup": "Mostrar pop -up de livro de endereços", "show_balance": "Pressione há muito tempo para mostrar o equilíbrio", "show_balance_toast": "Pressione há muito tempo para se esconder ou mostrar equilíbrio", "show_details": "Mostrar detalhes", @@ -783,6 +789,7 @@ "support_title_guides": "Documentos da carteira de bolo", "support_title_live_chat": "Apoio ao vivo", "support_title_other_links": "Outros links de suporte", + "swap": "Trocar", "sweeping_wallet": "Carteira varrendo", "sweeping_wallet_alert": "To nie powinno zająć dużo czasu. NIE WYCHODŹ Z TEGO EKRANU, W PRZECIWNYM WYPADKU MOŻE ZOSTAĆ UTRACONA ŚRODKI", "switchToETHWallet": "Mude para uma carteira Ethereum e tente novamente", @@ -808,6 +815,7 @@ "testnet_coins_no_value": "As moedas de teste não têm valor", "third_intro_content": "Yats também mora fora da Cake Wallet. Qualquer endereço de carteira na Terra pode ser substituído por um Yat!", "third_intro_title": "Yat joga bem com os outros", + "this_pair_is_not_supported_warning": "Este par não é suportado com as trocas (s) selecionadas (s) atualmente selecionadas. Selecione outra troca.", "thorchain_contract_address_not_supported": "Thorchain não suporta o envio para um endereço de contrato", "thorchain_taproot_address_not_supported": "O provedor de Thorchain não suporta endereços de raiz de Tap. Altere o endereço ou selecione um provedor diferente.", "time": "${minutes}m ${seconds}s", @@ -932,10 +940,13 @@ "waiting_payment_confirmation": "Aguardando confirmação de pagamento", "wallet": "Carteira", "wallet_group": "Grupo de carteira", + "wallet_group_description_existing_seed": "Você optou por usar uma semente existente para esta carteira. Você pode verificar a semente novamente se precisar confirmar ou anotá -la.", "wallet_group_description_four": "Para criar uma carteira com uma semente totalmente nova.", "wallet_group_description_one": "Na carteira de bolo, você pode criar um", + "wallet_group_description_open_wallet": "Caso contrário, você pode continuar a abrir a carteira", "wallet_group_description_three": "Para ver as carteiras disponíveis e/ou os grupos de carteiras. Ou escolha", "wallet_group_description_two": "Selecionando uma carteira existente para compartilhar uma semente. Cada grupo de carteira pode conter uma única carteira de cada tipo de moeda. \n\n você pode selecionar", + "wallet_group_description_view_seed": "Você sempre pode ver esta semente novamente em", "wallet_group_empty_state_text_one": "Parece que você não tem nenhum grupo de carteira compatível !\n\n Toque", "wallet_group_empty_state_text_two": "abaixo para fazer um novo.", "wallet_keys": "Semente/chaves da carteira", diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index dca427382..6acb1c989 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -84,6 +84,7 @@ "bitcoin_dark_theme": "Биткойн Темная тема", "bitcoin_light_theme": "Светлая биткойн-тема", "bitcoin_payments_require_1_confirmation": "Биткойн-платежи требуют 1 подтверждения, что может занять 20 минут или дольше. Спасибо тебе за твое терпение! Вы получите электронное письмо, когда платеж будет подтвержден.", + "block_height": "Высота блока", "block_remaining": "1 Блок остался", "Blocks_remaining": "${status} Осталось блоков", "bluetooth": "Bluetooth", @@ -131,6 +132,7 @@ "change_rep": "Изменить представителя", "change_rep_message": "Вы уверены, что хотите сменить представителя?", "change_rep_successful": "Успешно изменил представитель", + "change_selected_exchanges": "Изменить выбранные обмены", "change_wallet_alert_content": "Вы хотите изменить текущий кошелек на ${wallet_name}?", "change_wallet_alert_title": "Изменить текущий кошелек", "choose_a_payment_method": "Выберите способ оплаты", @@ -296,7 +298,7 @@ "etherscan_history": "История Эфириума", "event": "Событие", "events": "События", - "exchange": "Менять", + "exchange": "Обмен", "exchange_incorrect_current_wallet_for_xmr": "Если вы хотите поменять XMR с баланса с кошельком для торта Monero, сначала переключитесь на свой кошелек Monero.", "exchange_new_template": "Новый шаблон", "exchange_provider_unsupported": "${providerName} больше не поддерживается!", @@ -382,6 +384,7 @@ "invalid_password": "Неверный пароль", "invoice_details": "Детали счета", "is_percentage": "есть", + "keys": "Ключи", "last_30_days": "Последние 30 дней", "learn_more": "Узнать больше", "ledger_connection_error": "Не удалось подключиться к вам, книги. Пожалуйста, попробуйте еще раз.", @@ -389,6 +392,7 @@ "ledger_error_tx_rejected_by_user": "Транзакция отклоняется на устройстве", "ledger_error_wrong_app": "Пожалуйста, убедитесь, что вы предлагаете правильное приложение в своей бухгалтерской книге", "ledger_please_enable_bluetooth": "Пожалуйста, включите Bluetooth обнаружить вашу бухгалтерскую книгу", + "legacy": "Наследие", "light_theme": "Светлая", "litecoin_enable_mweb_sync": "Включить MWEB сканирование", "litecoin_mweb": "Мвеб", @@ -668,6 +672,7 @@ "select_backup_file": "Выберите файл резервной копии", "select_buy_provider_notice": "Выберите поставщика покупки выше. Вы можете пропустить этот экран, установив поставщика покупки по умолчанию в настройках приложения.", "select_destination": "Пожалуйста, выберите место для файла резервной копии.", + "select_hw_account_below": "Пожалуйста, выберите, какую учетную запись восстановить ниже:", "select_sell_provider_notice": "Выберите поставщика услуг продажи выше. Вы можете пропустить этот экран, установив поставщика услуг продаж по умолчанию в настройках приложения.", "select_your_country": "Пожалуйста, выберите свою страну", "sell": "Продавать", @@ -698,6 +703,7 @@ "service_health_disabled": "Бюллетень для здоровья обслуживания инвалид", "service_health_disabled_message": "Это страница бюллетени обслуживания услуг, вы можете включить эту страницу в соответствии с настройками -> Конфиденциальность", "set_a_pin": "Установить булавку", + "set_up_a_wallet": "Установить кошелек", "settings": "Настройки", "settings_all": "ВСЕ", "settings_allow_biometrical_authentication": "Включить биометрическую аутентификацию", @@ -733,7 +739,7 @@ "share_address": "Поделиться адресом", "shared_seed_wallet_groups": "Общие группы кошелька семян", "show": "Показывать", - "show_address_book_popup": "Покажите всплывающее окно «Добавить в адрес адреса» после отправки", + "show_address_book_popup": "Показать адресную книгу всплывающее окно", "show_balance": "Длинная пресса, чтобы показать баланс", "show_balance_toast": "Длинная нажавка, чтобы скрыть или показать баланс", "show_details": "Показать детали", @@ -782,6 +788,7 @@ "support_title_guides": "Корт кошелек документов", "support_title_live_chat": "Живая поддержка", "support_title_other_links": "Другие ссылки на поддержку", + "swap": "Менять", "sweeping_wallet": "Подметание кошелька", "sweeping_wallet_alert": "Это не должно занять много времени. НЕ ПОКИДАЙТЕ ЭТОТ ЭКРАН, ИНАЧЕ ВЫЧИСЛЕННЫЕ СРЕДСТВА МОГУТ БЫТЬ ПОТЕРЯНЫ", "switchToETHWallet": "Пожалуйста, переключитесь на кошелек Ethereum и повторите попытку.", @@ -807,6 +814,7 @@ "testnet_coins_no_value": "Монеты теста не имеют значения", "third_intro_content": "Yat находятся за пределами Cake Wallet. Любой адрес кошелька на земле можно заменить на Yat!", "third_intro_title": "Yat хорошо взаимодействует с другими", + "this_pair_is_not_supported_warning": "Эта пара не поддерживается в настоящее время выбранной биржи (ы). Пожалуйста, выберите другой обмен.", "thorchain_contract_address_not_supported": "Thorchain не поддерживает отправку на адрес контракта", "thorchain_taproot_address_not_supported": "Поставщик Thorchain не поддерживает адреса taproot. Пожалуйста, измените адрес или выберите другого поставщика.", "time": "${minutes}мин ${seconds}сек", @@ -930,10 +938,13 @@ "waitFewSecondForTxUpdate": "Пожалуйста, подождите несколько секунд, чтобы транзакция отразилась в истории транзакций.", "wallet": "Кошелек", "wallet_group": "Группа кошелька", + "wallet_group_description_existing_seed": "Вы решили использовать существующее семя для этого кошелька. Вы можете снова проверить семя, если вам нужно подтвердить или записать его.", "wallet_group_description_four": "создать кошелек с совершенно новым семенем.", "wallet_group_description_one": "В кошельке для торта вы можете создать", + "wallet_group_description_open_wallet": "В противном случае вы можете продолжать открывать кошелек", "wallet_group_description_three": "Чтобы увидеть доступные кошельки и/или экраны групп кошельков. Или выберите", "wallet_group_description_two": "выбирая существующий кошелек, чтобы поделиться семенами. Каждая группа кошелька может содержать один кошелек каждого типа валюты. \n\n Вы можете выбрать", + "wallet_group_description_view_seed": "Вы всегда можете просматривать это семя снова под", "wallet_group_empty_state_text_one": "Похоже, у вас нет никаких совместимых групп кошелька !\n\n tap", "wallet_group_empty_state_text_two": "ниже, чтобы сделать новый.", "wallet_keys": "Мнемоническая фраза/ключи кошелька", diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index 838b5e649..393dc3683 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -84,6 +84,7 @@ "bitcoin_dark_theme": "ธีมมืด Bitcoin", "bitcoin_light_theme": "ธีมแสง Bitcoin", "bitcoin_payments_require_1_confirmation": "การชำระเงินด้วย Bitcoin ต้องการการยืนยัน 1 ครั้ง ซึ่งอาจใช้เวลา 20 นาทีหรือนานกว่านั้น ขอบคุณสำหรับความอดทนของคุณ! คุณจะได้รับอีเมลเมื่อการชำระเงินได้รับการยืนยัน", + "block_height": "ความสูงของบล็อก", "block_remaining": "เหลือ 1 บล็อก", "Blocks_remaining": "${status} บล็อกที่เหลืออยู่", "bluetooth": "บลูทู ธ", @@ -131,6 +132,7 @@ "change_rep": "เปลี่ยนผู้แทน", "change_rep_message": "คุณแน่ใจหรือไม่ว่าต้องการเปลี่ยนตัวแทน", "change_rep_successful": "เปลี่ยนตัวแทนสำเร็จ", + "change_selected_exchanges": "เปลี่ยนการแลกเปลี่ยนที่เลือก", "change_wallet_alert_content": "คุณต้องการเปลี่ยนกระเป๋าปัจจุบันเป็น ${wallet_name} หรือไม่?", "change_wallet_alert_title": "เปลี่ยนกระเป๋าปัจจุบัน", "choose_a_payment_method": "เลือกวิธีการชำระเงิน", @@ -382,6 +384,7 @@ "invalid_password": "รหัสผ่านไม่ถูกต้อง", "invoice_details": "รายละเอียดใบแจ้งหนี้", "is_percentage": "เป็น", + "keys": "กุญแจ", "last_30_days": "30 วันล่าสุด", "learn_more": "ศึกษาเพิ่มเติม", "ledger_connection_error": "ไม่สามารถเชื่อมต่อกับบัญชีแยกประเภทของคุณได้ กรุณาลองอีกครั้ง.", @@ -389,6 +392,7 @@ "ledger_error_tx_rejected_by_user": "ธุรกรรมถูกปฏิเสธบนอุปกรณ์", "ledger_error_wrong_app": "โปรดตรวจสอบให้แน่ใจว่าคุณเปิดแอพที่เหมาะสมในบัญชีแยกประเภทของคุณ", "ledger_please_enable_bluetooth": "โปรดเปิดใช้งานบลูทู ธ ในการตรวจจับบัญชีแยกประเภทของคุณ", + "legacy": "มรดก", "light_theme": "สว่าง", "litecoin_enable_mweb_sync": "เปิดใช้งานการสแกน MWEB", "litecoin_mweb": "mweb", @@ -667,6 +671,7 @@ "select_backup_file": "เลือกไฟล์สำรอง", "select_buy_provider_notice": "เลือกผู้ให้บริการซื้อด้านบน คุณสามารถข้ามหน้าจอนี้ได้โดยการตั้งค่าผู้ให้บริการซื้อเริ่มต้นในการตั้งค่าแอป", "select_destination": "โปรดเลือกปลายทางสำหรับไฟล์สำรอง", + "select_hw_account_below": "กรุณาเลือกบัญชีที่จะกู้คืนด้านล่าง:", "select_sell_provider_notice": "เลือกผู้ให้บริการการขายด้านบน คุณสามารถข้ามหน้าจอนี้ได้โดยการตั้งค่าผู้ให้บริการการขายเริ่มต้นในการตั้งค่าแอป", "select_your_country": "กรุณาเลือกประเทศของคุณ", "sell": "ขาย", @@ -697,6 +702,7 @@ "service_health_disabled": "Service Health Bulletin ถูกปิดใช้งาน", "service_health_disabled_message": "นี่คือหน้า Service Health Bulletin คุณสามารถเปิดใช้งานหน้านี้ภายใต้การตั้งค่า -> ความเป็นส่วนตัว", "set_a_pin": "ตั้งพิน", + "set_up_a_wallet": "ตั้งค่ากระเป๋าเงิน", "settings": "การตั้งค่า", "settings_all": "ทั้งหมด", "settings_allow_biometrical_authentication": "อนุญาตให้ใช้การยืนยันตัวตนทางระบบชีวภาพ", @@ -732,7 +738,7 @@ "share_address": "แชร์ที่อยู่", "shared_seed_wallet_groups": "กลุ่มกระเป๋าเงินที่ใช้ร่วมกัน", "show": "แสดง", - "show_address_book_popup": "แสดง 'เพิ่มในสมุดรายชื่อ' ป๊อปอัพหลังจากส่ง", + "show_address_book_popup": "แสดงสมุดที่อยู่ป๊อปอัพ", "show_balance": "กดยาวเพื่อแสดงความสมดุล", "show_balance_toast": "กดนานเพื่อซ่อนหรือแสดงความสมดุล", "show_details": "แสดงรายละเอียด", @@ -781,6 +787,7 @@ "support_title_guides": "เอกสารกระเป๋าเงินเค้ก", "support_title_live_chat": "การสนับสนุนสด", "support_title_other_links": "ลิงค์สนับสนุนอื่น ๆ", + "swap": "แลกเปลี่ยน", "sweeping_wallet": "กวาดกระเป๋าสตางค์", "sweeping_wallet_alert": "การดำเนินการนี้ใช้เวลาไม่นาน อย่าออกจากหน้าจอนี้ มิฉะนั้นเงินที่กวาดไปอาจสูญหาย", "switchToETHWallet": "โปรดเปลี่ยนไปใช้กระเป๋าเงิน Ethereum แล้วลองอีกครั้ง", @@ -806,6 +813,7 @@ "testnet_coins_no_value": "Testnet Coins ไม่มีค่า", "third_intro_content": "Yat อาศัยอยู่นอก Cake Wallet ด้วย ที่อยู่กระเป๋าใดๆ ทั่วโลกสามารถแทนด้วย Yat ได้อีกด้วย!", "third_intro_title": "Yat ปฏิบัติตนอย่างดีกับผู้อื่น", + "this_pair_is_not_supported_warning": "คู่นี้ไม่ได้รับการสนับสนุนด้วยการแลกเปลี่ยนที่เลือกในปัจจุบัน โปรดเลือกการแลกเปลี่ยนอื่น", "thorchain_contract_address_not_supported": "Thorchain ไม่สนับสนุนการส่งไปยังที่อยู่สัญญา", "thorchain_taproot_address_not_supported": "ผู้ให้บริการ Thorchain ไม่รองรับที่อยู่ taproot โปรดเปลี่ยนที่อยู่หรือเลือกผู้ให้บริการอื่น", "time": "${minutes}m ${seconds}s", @@ -929,10 +937,13 @@ "waitFewSecondForTxUpdate": "กรุณารอสักครู่เพื่อให้ธุรกรรมปรากฏในประวัติการทำธุรกรรม", "wallet": "กระเป๋าสตางค์", "wallet_group": "กลุ่มกระเป๋าเงิน", + "wallet_group_description_existing_seed": "คุณเลือกที่จะใช้เมล็ดพันธุ์ที่มีอยู่สำหรับกระเป๋าเงินนี้คุณอาจตรวจสอบเมล็ดได้อีกครั้งหากคุณต้องการยืนยันหรือเขียนลงไป", "wallet_group_description_four": "เพื่อสร้างกระเป๋าเงินที่มีเมล็ดพันธุ์ใหม่ทั้งหมด", "wallet_group_description_one": "ในกระเป๋าเงินเค้กคุณสามารถสร้างไฟล์", + "wallet_group_description_open_wallet": "มิฉะนั้นคุณสามารถเปิดกระเป๋าเงินต่อไปได้", "wallet_group_description_three": "หากต้องการดูกระเป๋าเงินและ/หรือกลุ่มกระเป๋าเงินที่มีอยู่ หรือเลือก", "wallet_group_description_two": "โดยการเลือกกระเป๋าเงินที่มีอยู่เพื่อแบ่งปันเมล็ดด้วย แต่ละกลุ่มกระเป๋าเงินสามารถมีกระเป๋าเงินเดียวของแต่ละประเภทสกุลเงิน \n\n คุณสามารถเลือกได้", + "wallet_group_description_view_seed": "คุณสามารถดูเมล็ดพันธุ์นี้ได้อีกครั้งภายใต้", "wallet_group_empty_state_text_one": "ดูเหมือนว่าคุณจะไม่มีกลุ่มกระเป๋าเงินที่เข้ากันได้ !\n\n แตะ", "wallet_group_empty_state_text_two": "ด้านล่างเพื่อสร้างใหม่", "wallet_keys": "ซีดของกระเป๋า/คีย์", diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index b8d758b52..b0f5d962d 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -84,6 +84,7 @@ "bitcoin_dark_theme": "Bitcoin Dark Theme", "bitcoin_light_theme": "Bitcoin Light Theme", "bitcoin_payments_require_1_confirmation": "Ang mga pagbabayad sa Bitcoin ay nangangailangan ng 1 kumpirmasyon, na maaaring tumagal ng 20 minuto o mas mahaba. Salamat sa iyong pasensya! Mag-email ka kapag nakumpirma ang pagbabayad.", + "block_height": "I -block ang taas", "block_remaining": "1 Bloke ang Natitira", "Blocks_remaining": "Ang natitirang ${status} ay natitira", "bluetooth": "Bluetooth", @@ -131,6 +132,7 @@ "change_rep": "Baguhin ang Representative", "change_rep_message": "Sigurado ka bang nais mong baguhin ang mga representative?", "change_rep_successful": "Matagumpay na nagbago ng representative", + "change_selected_exchanges": "Baguhin ang mga napiling palitan", "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", @@ -382,6 +384,7 @@ "invalid_password": "Di-wastong password", "invoice_details": "Mga detalye ng invoice", "is_percentage": "ay", + "keys": "Mga susi", "last_30_days": "Huling 30 na araw", "learn_more": "Matuto nang higit pa", "ledger_connection_error": "Nabigong kumonekta sa iyong Ledger. Pakisubukang muli.", @@ -389,6 +392,7 @@ "ledger_error_tx_rejected_by_user": "Ang transaksyon ay tinanggihan sa hardware wallet", "ledger_error_wrong_app": "Mangyaring tiyaking pinipili mo ang tamang app sa iyong Ledger", "ledger_please_enable_bluetooth": "Mangyaring paganahin ang Bluetooth upang makita ang iyong Ledger", + "legacy": "Pamana", "light_theme": "Light", "litecoin_enable_mweb_sync": "Paganahin ang pag -scan ng MWeb", "litecoin_mweb": "Mweb", @@ -667,6 +671,7 @@ "select_backup_file": "Piliin ang backup na file", "select_buy_provider_notice": "Pumili ng provider ng pagbili sa itaas. Maaari mong laktawan ang screen na ito sa pamamagitan ng pagtatakda ng iyong default na provider ng pagbili sa mga setting ng app.", "select_destination": "Mangyaring piliin ang patutunguhan para sa backup na file.", + "select_hw_account_below": "Mangyaring piliin kung aling account ang ibabalik sa ibaba:", "select_sell_provider_notice": "Pumili ng provider ng nagbebenta sa itaas. Maaari mong laktawan ang screen na ito sa pamamagitan ng pagtatakda ng iyong default na sell provider sa mga setting ng app.", "select_your_country": "Mangyaring piliin ang iyong bansa", "sell": "Ibenta", @@ -697,6 +702,7 @@ "service_health_disabled": "Hindi pinagana ang Service Health Bulletin", "service_health_disabled_message": "Ito ang pahina ng Service Health Bulletin, maaari mong paganahin ang pahinang ito sa ilalim ng Mga Setting -> Pagkapribado", "set_a_pin": "Magtakda ng isang pin", + "set_up_a_wallet": "Mag -set up ng isang pitaka", "settings": "Mga Setting", "settings_all": "LAHAT", "settings_allow_biometrical_authentication": "Payagan ang biometrical authentication", @@ -732,7 +738,7 @@ "share_address": "Ibahagi ang address", "shared_seed_wallet_groups": "Ibinahaging mga pangkat ng pitaka ng binhi", "show": "Ipakita", - "show_address_book_popup": "Ipakita ang popup na 'Idagdag sa Address Book' pagkatapos magpadala", + "show_address_book_popup": "Ipakita ang Address Book Popup", "show_balance": "Mahabang pindutin upang ipakita ang balanse", "show_balance_toast": "Mahabang pindutin upang itago o ipakita ang balanse", "show_details": "Ipakita ang mga detalye", @@ -781,6 +787,7 @@ "support_title_guides": "Cake wallet doc", "support_title_live_chat": "Live na suporta", "support_title_other_links": "Iba pang mga link sa suporta", + "swap": "Palitan", "sweeping_wallet": "Sweeping wallet", "sweeping_wallet_alert": "Hindi ito dapat magtagal. HUWAG iwanan ang screen na ito o maaaring mawala ang mga pondo.", "switchToETHWallet": "Mangyaring lumipat sa isang Ethereum wallet at subukang muli", @@ -806,6 +813,7 @@ "testnet_coins_no_value": "Ang mga barya ng testnet ay walang halaga", "third_intro_content": "Nabubuhay rin ang Yats sa labas ng Cake Wallet. Anumang wallet address sa mundo ay maaaring palitan ng Yat!", "third_intro_title": "Magaling makipaglaro ang Yat sa iba", + "this_pair_is_not_supported_warning": "Ang pares na ito ay hindi suportado sa kasalukuyang napiling (mga) palitan. Mangyaring pumili ng isa pang palitan.", "thorchain_contract_address_not_supported": "Hindi sinusuportahan ng THORChain ang pagpapadala sa isang address ng kontrata", "thorchain_taproot_address_not_supported": "Ang provider ng THORChain ay hindi sumusuporta sa mga address ng Taproot. Mangyaring baguhin ang address o pumili ng ibang provider.", "time": "${minutes} m ${seconds} s", @@ -929,10 +937,13 @@ "waitFewSecondForTxUpdate": "Mangyaring maghintay ng ilang segundo para makita ang transaksyon sa history ng mga transaksyon", "wallet": "Wallet", "wallet_group": "Group ng Wallet", + "wallet_group_description_existing_seed": "Pinili mong gumamit ng isang umiiral na binhi para sa pitaka na ito. Maaari mong mapatunayan muli ang binhi kung kailangan mong kumpirmahin o isulat ito.", "wallet_group_description_four": "Upang lumikha ng isang pitaka na may ganap na bagong binhi.", "wallet_group_description_one": "Sa cake wallet, maaari kang lumikha ng isang", + "wallet_group_description_open_wallet": "Kung hindi man, maaari mong magpatuloy upang buksan ang pitaka", "wallet_group_description_three": "Upang makita ang magagamit na mga wallets at/o screen ng mga pangkat ng pitaka. O pumili", "wallet_group_description_two": "Sa pamamagitan ng pagpili ng isang umiiral na pitaka upang magbahagi ng isang binhi. Ang bawat pangkat ng pitaka ay maaaring maglaman ng isang solong pitaka ng bawat uri ng pera.\n\nMaaari kang pumili", + "wallet_group_description_view_seed": "Maaari mong palaging tingnan ang binhi na ito sa ilalim", "wallet_group_empty_state_text_one": "Mukhang wala kang anumang mga katugmang pangkat ng pitaka!\n\ntap", "wallet_group_empty_state_text_two": "sa ibaba upang gumawa ng bago.", "wallet_keys": "Wallet seed/keys", diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index 634e0d7bd..600eb5131 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -84,6 +84,7 @@ "bitcoin_dark_theme": "Bitcoin Karanlık Teması", "bitcoin_light_theme": "Bitcoin Hafif Tema", "bitcoin_payments_require_1_confirmation": "Bitcoin ödemeleri, 20 dakika veya daha uzun sürebilen 1 onay gerektirir. Sabrınız için teşekkürler! Ödeme onaylandığında e-posta ile bilgilendirileceksiniz.", + "block_height": "Blok yüksekliği", "block_remaining": "Kalan 1 blok", "Blocks_remaining": "${status} Blok Kaldı", "bluetooth": "Bluetooth", @@ -131,6 +132,7 @@ "change_rep": "Temsilciyi Değiştir", "change_rep_message": "Temsilcileri değiştirmek istediğinizden emin misiniz?", "change_rep_successful": "Temsilciyi başarıyla değiştirdi", + "change_selected_exchanges": "Seçilen borsaları değiştirin", "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", @@ -296,7 +298,7 @@ "etherscan_history": "Etherscan geçmişi", "event": "Etkinlik", "events": "Olaylar", - "exchange": "Takas", + "exchange": "Değişme", "exchange_incorrect_current_wallet_for_xmr": "XMR'yi kek cüzdanı Monero bakiyenizden değiştirmek istiyorsanız, lütfen önce Monero cüzdanınıza geçin.", "exchange_new_template": "Yeni şablon", "exchange_provider_unsupported": "${providerName} artık desteklenmiyor!", @@ -382,6 +384,7 @@ "invalid_password": "Geçersiz şifre", "invoice_details": "fatura detayları", "is_percentage": "is", + "keys": "Anahtar", "last_30_days": "Son 30 gün", "learn_more": "Daha fazla öğren", "ledger_connection_error": "Ledger'e bağlanamadı. Lütfen tekrar deneyin.", @@ -389,6 +392,7 @@ "ledger_error_tx_rejected_by_user": "Cihazda reddedilen işlem", "ledger_error_wrong_app": "Lütfen defterinizde doğru uygulamayı açtığınızdan emin olun", "ledger_please_enable_bluetooth": "Defterinizi algılamak için lütfen Bluetooth'u etkinleştirin", + "legacy": "Miras", "light_theme": "Aydınlık", "litecoin_enable_mweb_sync": "MWEB taramasını etkinleştir", "litecoin_mweb": "Mweb", @@ -667,6 +671,7 @@ "select_backup_file": "Yedek dosyası seç", "select_buy_provider_notice": "Yukarıda bir satın alma sağlayıcısı seçin. App ayarlarında varsayılan satın alma sağlayıcınızı ayarlayarak bu ekranı atlayabilirsiniz.", "select_destination": "Lütfen yedekleme dosyası için hedef seçin.", + "select_hw_account_below": "Lütfen aşağıda hangi hesabı geri yükleyeceğinizi seçin:", "select_sell_provider_notice": "Yukarıdan bir satış sağlayıcısı seçin. Uygulama ayarlarında varsayılan satış sağlayıcınızı ayarlayarak bu ekranı atlayabilirsiniz.", "select_your_country": "Lütfen ülkenizi seçin", "sell": "Satış", @@ -697,6 +702,7 @@ "service_health_disabled": "Service Health Bülten devre dışı bırakıldı", "service_health_disabled_message": "Bu Hizmet Sağlığı Bülten Sayfası, bu sayfayı Ayarlar -> Gizlilik altında etkinleştirebilirsiniz", "set_a_pin": "Bir pim ayarlamak", + "set_up_a_wallet": "Bir cüzdan kurun", "settings": "ayarlar", "settings_all": "HEPSİ", "settings_allow_biometrical_authentication": "Biyometrik doğrulamaya izin ver", @@ -732,7 +738,7 @@ "share_address": "Adresi paylaş", "shared_seed_wallet_groups": "Paylaşılan tohum cüzdan grupları", "show": "Göstermek", - "show_address_book_popup": "Gönderdikten sonra 'adres defterine ekle' açılır", + "show_address_book_popup": "Adres Kitabı Popup'ı Göster", "show_balance": "Dengeyi Göstermek İçin Uzun Basın", "show_balance_toast": "Dengeyi gizlemek veya göstermek için uzun basın", "show_details": "Detayları Göster", @@ -781,6 +787,7 @@ "support_title_guides": "Kek Cüzdan Dokümanlar", "support_title_live_chat": "Canlı destek", "support_title_other_links": "Diğer destek bağlantıları", + "swap": "Takas", "sweeping_wallet": "Süpürme cüzdanı", "sweeping_wallet_alert": "Bu uzun sürmemeli. BU EKRANDAN BIRAKMAYIN YOKSA SÜPÜRÜLEN FONLAR KAYBOLABİLİR", "switchToETHWallet": "Lütfen bir Ethereum cüzdanına geçin ve tekrar deneyin", @@ -806,6 +813,7 @@ "testnet_coins_no_value": "TestNet paralarının değeri yok", "third_intro_content": "Yat'lar Cake Wallet'ın dışında da çalışabilir. Dünya üzerindeki herhangi bir cüzdan adresi Yat ile değiştirilebilir!", "third_intro_title": "Yat diğerleriyle iyi çalışır", + "this_pair_is_not_supported_warning": "Bu çift şu anda seçilen değişim (ler) ile desteklenmemektedir. Lütfen başka bir değişim seçin.", "thorchain_contract_address_not_supported": "Thorchain bir sözleşme adresine göndermeyi desteklemiyor", "thorchain_taproot_address_not_supported": "Thorchain sağlayıcısı Taproot adreslerini desteklemiyor. Lütfen adresi değiştirin veya farklı bir sağlayıcı seçin.", "time": "${minutes}d ${seconds}s", @@ -929,10 +937,13 @@ "waitFewSecondForTxUpdate": "İşlemin işlem geçmişine yansıması için lütfen birkaç saniye bekleyin", "wallet": "Cüzdan", "wallet_group": "Cüzdan grubu", + "wallet_group_description_existing_seed": "Bu cüzdan için mevcut bir tohum kullanmayı seçtiniz. Onaylamanız veya yazmanız gerekiyorsa tohumu tekrar doğrulayabilirsiniz.", "wallet_group_description_four": "Tamamen yeni bir tohumla bir cüzdan oluşturmak için.", "wallet_group_description_one": "Kek cüzdanında bir", + "wallet_group_description_open_wallet": "Aksi takdirde cüzdanı açmaya devam edebilirsiniz", "wallet_group_description_three": "Mevcut cüzdan ve/veya cüzdan grupları ekranını görmek için. Veya seç", "wallet_group_description_two": "Bir tohumu paylaşmak için mevcut bir cüzdan seçerek. Her cüzdan grubu, her para türünün tek bir cüzdanı içerebilir. \n\n Seçebilirsiniz", + "wallet_group_description_view_seed": "Bu tohumu her zaman tekrar görebilirsiniz", "wallet_group_empty_state_text_one": "Herhangi bir uyumlu cüzdan grubunuz yok gibi görünüyor !\n\n TAP", "wallet_group_empty_state_text_two": "Yeni bir tane yapmak için aşağıda.", "wallet_keys": "Cüzdan tohumu/anahtarları", diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index 8469f690b..f9264e505 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -84,6 +84,7 @@ "bitcoin_dark_theme": "Темна тема Bitcoin", "bitcoin_light_theme": "Світла тема Bitcoin", "bitcoin_payments_require_1_confirmation": "Платежі Bitcoin потребують 1 підтвердження, яке може зайняти 20 хвилин або більше. Дякую за Ваше терпіння! Ви отримаєте електронний лист, коли платіж буде підтверджено.", + "block_height": "Висота блоку", "block_remaining": "1 блок, що залишився", "Blocks_remaining": "${status} Залишилось блоків", "bluetooth": "Блюдот", @@ -131,6 +132,7 @@ "change_rep": "Зміна представника", "change_rep_message": "Ви впевнені, що хочете змінити представника?", "change_rep_successful": "Успішно змінив представник", + "change_selected_exchanges": "Змінити вибрані біржі", "change_wallet_alert_content": "Ви хочете змінити поточний гаманець на ${wallet_name}?", "change_wallet_alert_title": "Змінити поточний гаманець", "choose_a_payment_method": "Виберіть метод оплати", @@ -296,7 +298,7 @@ "etherscan_history": "Історія Etherscan", "event": "Подія", "events": "Події", - "exchange": "Обміняти", + "exchange": "Обмін", "exchange_incorrect_current_wallet_for_xmr": "Якщо ви хочете поміняти XMR зі свого балансу для тортів Monero Balance, спочатку перейдіть на свій гаманець Monero.", "exchange_new_template": "Новий шаблон", "exchange_provider_unsupported": "${providerName} більше не підтримується!", @@ -382,6 +384,7 @@ "invalid_password": "Недійсний пароль", "invoice_details": "Реквізити рахунку-фактури", "is_percentage": "є", + "keys": "Ключі", "last_30_days": "Останні 30 днів", "learn_more": "Дізнатися більше", "ledger_connection_error": "Не вдалося підключитися до вас. Будь ласка спробуйте ще раз.", @@ -389,6 +392,7 @@ "ledger_error_tx_rejected_by_user": "Транзакція відхилена на пристрої", "ledger_error_wrong_app": "Будь ласка, переконайтеся, що ви відкриваєте потрібну програму на своїй книзі", "ledger_please_enable_bluetooth": "Будь ласка, ввімкніть Bluetooth виявити свою книгу", + "legacy": "Спадщина", "light_theme": "Світла", "litecoin_enable_mweb_sync": "Увімкнути сканування MWEB", "litecoin_mweb": "Мвеб", @@ -668,6 +672,7 @@ "select_backup_file": "Виберіть файл резервної копії", "select_buy_provider_notice": "Виберіть постачальника купівлі вище. Ви можете пропустити цей екран, встановивши свого постачальника купівлі за замовчуванням у налаштуваннях додатків.", "select_destination": "Виберіть місце призначення для файлу резервної копії.", + "select_hw_account_below": "Виберіть, який рахунок відновити нижче:", "select_sell_provider_notice": "Виберіть вище постачальника послуг продажу. Ви можете пропустити цей екран, встановивши постачальника послуг продажу за умовчанням у налаштуваннях програми.", "select_your_country": "Будь ласка, виберіть свою країну", "sell": "Продати", @@ -698,6 +703,7 @@ "service_health_disabled": "Вісник охорони здоров'я інвалідів", "service_health_disabled_message": "Це сторінка бюлетеня Health Service, ви можете включити цю сторінку в налаштуваннях -> конфіденційність", "set_a_pin": "Встановити PIN", + "set_up_a_wallet": "Встановіть гаманець", "settings": "Налаштування", "settings_all": "ВСІ", "settings_allow_biometrical_authentication": "Включити біометричну аутентифікацію", @@ -733,7 +739,7 @@ "share_address": "Поділитися адресою", "shared_seed_wallet_groups": "Спільні групи насіннєвих гаманців", "show": "Показувати", - "show_address_book_popup": "Показати спливаюче вікно \"Додати до адресної книги\" після надсилання", + "show_address_book_popup": "Показати спливаюче вікно адреси книги", "show_balance": "Довга преса, щоб показати рівновагу", "show_balance_toast": "Довга преса, щоб приховати або показати рівновагу", "show_details": "Показати деталі", @@ -782,6 +788,7 @@ "support_title_guides": "Торт гаманці", "support_title_live_chat": "Жива підтримка", "support_title_other_links": "Інші посилання на підтримку", + "swap": "Обміняти", "sweeping_wallet": "Підмітаня гаманця", "sweeping_wallet_alert": "Це не повинно зайняти багато часу. НЕ ЗАЛИШАЙТЕ ЦЬОГО ЕКРАНУ, АБО КОШТИ МОЖУТЬ БУТИ ВТРАЧЕНІ", "switchToETHWallet": "Перейдіть на гаманець Ethereum і повторіть спробу", @@ -807,6 +814,7 @@ "testnet_coins_no_value": "Монети TestNet не мають значення", "third_intro_content": "Yat знаходиться за межами Cake Wallet. Будь-яку адресу гаманця на землі можна замінити на Yat!", "third_intro_title": "Yat добре взаємодіє з іншими", + "this_pair_is_not_supported_warning": "Ця пара не підтримується на даний момент вибраній біржі. Виберіть інший обмін.", "thorchain_contract_address_not_supported": "Thorchain не підтримує надсилання на адресу контракту", "thorchain_taproot_address_not_supported": "Постачальник Thorchain не підтримує адреси Taproot. Будь ласка, змініть адресу або виберіть іншого постачальника.", "time": "${minutes}хв ${seconds}сек", @@ -930,10 +938,13 @@ "waitFewSecondForTxUpdate": "Будь ласка, зачекайте кілька секунд, поки транзакція відобразиться в історії транзакцій", "wallet": "Гаманець", "wallet_group": "Група гаманців", + "wallet_group_description_existing_seed": "Ви вирішили використовувати існуюче насіння для цього гаманця. Ви можете ще раз перевірити насіння, якщо вам потрібно підтвердити або записати його.", "wallet_group_description_four": "створити гаманець з абсолютно новим насінням.", "wallet_group_description_one": "У гаманці тортів ви можете створити a", + "wallet_group_description_open_wallet": "В іншому випадку ви можете продовжувати відкривати гаманець", "wallet_group_description_three": "Щоб побачити наявні гаманці та/або екран групи гаманців. Або вибрати", "wallet_group_description_two": "Вибираючи існуючий гаманець, щоб поділитися насінням. Кожна група гаманця може містити один гаманець кожного типу валюти. \n\n Ви можете вибрати", + "wallet_group_description_view_seed": "Ви завжди можете переглянути це насіння ще раз під", "wallet_group_empty_state_text_one": "Схоже, у вас немає сумісних груп гаманця !\n\n Торкніться", "wallet_group_empty_state_text_two": "нижче, щоб зробити новий.", "wallet_keys": "Мнемонічна фраза/ключі гаманця", diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index 05b6a365e..2db0a0528 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -84,6 +84,7 @@ "bitcoin_dark_theme": "بٹ کوائن ڈارک تھیم", "bitcoin_light_theme": "بٹ کوائن لائٹ تھیم", "bitcoin_payments_require_1_confirmation": "بٹ کوائن کی ادائیگی میں 1 تصدیق کی ضرورت ہوتی ہے ، جس میں 20 منٹ یا اس سے زیادہ وقت لگ سکتا ہے۔ آپ کے صبر کا شکریہ! ادائیگی کی تصدیق ہونے پر آپ کو ای میل کیا جائے گا۔", + "block_height": "اونچائی کو بلاک کریں", "block_remaining": "1 بلاک باقی", "Blocks_remaining": "${status} بلاکس باقی ہیں۔", "bluetooth": "بلوٹوتھ", @@ -131,6 +132,7 @@ "change_rep": "۔ﮟﯾﺮﮐ ﻞﯾﺪﺒﺗ ﮦﺪﻨﺋﺎﻤﻧ", "change_rep_message": "؟ﮟﯿﮨ ﮯﺘﮨﺎﭼ ﺎﻧﺮﮐ ﻞﯾﺪﺒﺗ ﻮﮐ ﮞﻭﺪﻨﺋﺎﻤﻧ ﯽﻌﻗﺍﻭ ﭖﺁ ﺎﯿﮐ", "change_rep_successful": "نمائندہ کو کامیابی کے ساتھ تبدیل کیا", + "change_selected_exchanges": "منتخب تبادلے کو تبدیل کریں", "change_wallet_alert_content": "کیا آپ موجودہ والیٹ کو ${wallet_name} میں تبدیل کرنا چاہتے ہیں؟", "change_wallet_alert_title": "موجودہ پرس تبدیل کریں۔", "choose_a_payment_method": "ادائیگی کا طریقہ منتخب کریں", @@ -382,6 +384,7 @@ "invalid_password": "غلط پاسورڈ", "invoice_details": "رسید کی تفصیلات", "is_percentage": "ہے", + "keys": "چابیاں", "last_30_days": "آخری 30 دن", "learn_more": "اورجانیے", "ledger_connection_error": "آپ سے لیجر سے رابطہ قائم کرنے میں ناکام۔ دوبارہ کوشش کریں.", @@ -389,6 +392,7 @@ "ledger_error_tx_rejected_by_user": "آلہ پر لین دین کو مسترد کردیا گیا", "ledger_error_wrong_app": "براہ کرم یقینی بنائیں کہ آپ اپنے لیجر پر صحیح ایپ کو کھولتے ہیں", "ledger_please_enable_bluetooth": "براہ کرم بلوٹوتھ کو اپنے لیجر کا پتہ لگانے کے لئے اہل بنائیں", + "legacy": "میراث", "light_theme": "روشنی", "litecoin_enable_mweb_sync": "MWEB اسکیننگ کو فعال کریں", "litecoin_mweb": "MWEB", @@ -669,6 +673,7 @@ "select_backup_file": "بیک اپ فائل کو منتخب کریں۔", "select_buy_provider_notice": "اوپر خریدنے والا خریدنے والا منتخب کریں۔ آپ ایپ کی ترتیبات میں اپنے پہلے سے طے شدہ خریدنے والے کو ترتیب دے کر اس اسکرین کو چھوڑ سکتے ہیں۔", "select_destination": "۔ﮟﯾﺮﮐ ﺏﺎﺨﺘﻧﺍ ﺎﮐ ﻝﺰﻨﻣ ﮯﯿﻟ ﮯﮐ ﻞﺋﺎﻓ ﭖﺍ ﮏﯿﺑ ﻡﺮﮐ ﮦﺍﺮﺑ", + "select_hw_account_below": "براہ کرم ذیل میں کون سا اکاؤنٹ بحال کرنا ہے منتخب کریں:", "select_sell_provider_notice": "۔ﮟﯿﮨ ﮯﺘﮑﺳ ﮌﻮﮭﭼ ﻮﮐ ﻦﯾﺮﮑﺳﺍ ﺱﺍ ﺮﮐ ﮮﺩ ﺐﯿﺗﺮﺗ ﻮﮐ ﮦﺪﻨﻨﮐ ﻢﮨﺍﺮﻓ ﻞﯿﺳ ﭧﻟﺎﻔﯾﮈ ﮯﻨﭘﺍ ﮟﯿﻣ ﺕﺎﺒ", "select_your_country": "براہ کرم اپنے ملک کو منتخب کریں", "sell": "بیچنا", @@ -699,6 +704,7 @@ "service_health_disabled": "سروس ہیلتھ بلیٹن غیر فعال ہے", "service_health_disabled_message": "یہ سروس ہیلتھ بلیٹن پیج ہے ، آپ اس صفحے کو ترتیبات کے تحت اہل بنا سکتے ہیں -> رازداری", "set_a_pin": "ایک پن مرتب کریں", + "set_up_a_wallet": "ایک پرس لگائیں", "settings": "ترتیبات", "settings_all": "تمام", "settings_allow_biometrical_authentication": "بایومیٹریکل تصدیق کی اجازت دیں۔", @@ -734,7 +740,7 @@ "share_address": "پتہ شیئر کریں۔", "shared_seed_wallet_groups": "مشترکہ بیج پرس گروپ", "show": "دکھائیں", - "show_address_book_popup": "بھیجنے کے بعد 'ایڈریس میں شامل کریں کتاب' پاپ اپ دکھائیں", + "show_address_book_popup": "ایڈریس بک پاپ اپ دکھائیں", "show_balance": "توازن ظاہر کرنے کے لئے طویل پریس", "show_balance_toast": "توازن چھپانے یا ظاہر کرنے کے لئے طویل پریس", "show_details": "تفصیلات دکھائیں", @@ -783,6 +789,7 @@ "support_title_guides": "کیک پرس کے دستاویزات", "support_title_live_chat": "براہ راست مدد", "support_title_other_links": "دوسرے سپورٹ لنکس", + "swap": "تبادلہ", "sweeping_wallet": "جھاڑو دینے والا پرس", "sweeping_wallet_alert": "اس میں زیادہ وقت نہیں لینا چاہئے۔ اس اسکرین کو مت چھوڑیں یا بہہ جانے والے فنڈز ضائع ہوسکتے ہیں۔", "switchToETHWallet": "۔ﮟﯾﺮﮐ ﺶﺷﻮﮐ ﮦﺭﺎﺑﻭﺩ ﺭﻭﺍ ﮟﯾﺮﮐ ﭻﺋﻮﺳ ﺮﭘ ﭧﯿﻟﺍﻭ Ethereum ﻡﺮﮐ ﮦﺍﺮﺑ", @@ -808,6 +815,7 @@ "testnet_coins_no_value": "ٹیسٹ نیٹ سکے کی کوئی قیمت نہیں ہے", "third_intro_content": "Yats بھی Cake والیٹ سے باہر رہتے ہیں۔ زمین پر کسی بھی بٹوے کے پتے کو Yat سے تبدیل کیا جا سکتا ہے!", "third_intro_title": "Yat دوسروں کے ساتھ اچھی طرح کھیلتا ہے۔", + "this_pair_is_not_supported_warning": "اس جوڑی کو فی الحال منتخب کردہ تبادلے (زبانیں) کے ساتھ تعاون نہیں کیا گیا ہے۔ براہ کرم دوسرا تبادلہ منتخب کریں۔", "thorchain_contract_address_not_supported": "تھورچین معاہدے کے پتے بھیجنے کی حمایت نہیں کرتا ہے", "thorchain_taproot_address_not_supported": "تھورچین فراہم کنندہ ٹیپروٹ پتے کی حمایت نہیں کرتا ہے۔ براہ کرم پتہ تبدیل کریں یا ایک مختلف فراہم کنندہ کو منتخب کریں۔", "time": "${minutes}m ${seconds}s", @@ -931,10 +939,13 @@ "waitFewSecondForTxUpdate": "۔ﮟﯾﺮﮐ ﺭﺎﻈﺘﻧﺍ ﺎﮐ ﮉﻨﮑﯿﺳ ﺪﻨﭼ ﻡﺮﮐ ﮦﺍﺮﺑ ﮯﯿﻟ ﮯﮐ ﮯﻧﺮﮐ ﯽﺳﺎﮑﻋ ﯽﮐ ﻦﯾﺩ ﻦﯿﻟ ﮟﯿﻣ ﺦﯾﺭﺎﺗ ﯽﮐ ﻦ", "wallet": "پرس", "wallet_group": "پرس گروپ", + "wallet_group_description_existing_seed": "آپ نے اس پرس کے لئے موجودہ بیج استعمال کرنے کا انتخاب کیا ہے۔ اگر آپ کو اس کی تصدیق یا لکھنے کی ضرورت ہے تو آپ دوبارہ بیج کی تصدیق کرسکتے ہیں۔", "wallet_group_description_four": "مکمل طور پر نئے بیج کے ساتھ پرس بنانے کے ل.", "wallet_group_description_one": "کیک پرس میں ، آپ بنا سکتے ہیں", + "wallet_group_description_open_wallet": "بصورت دیگر ، آپ بٹوے کو کھول سکتے ہیں", "wallet_group_description_three": "دستیاب بٹوے اور/یا پرس گروپوں کی اسکرین کو دیکھنے کے لئے۔ یا منتخب کریں", "wallet_group_description_two": "بیج کے ساتھ بانٹنے کے لئے موجودہ پرس کا انتخاب کرکے۔ ہر بٹوے گروپ میں ہر کرنسی کی قسم کا ایک بٹوے شامل ہوسکتا ہے۔ \n\n آپ منتخب کرسکتے ہیں", + "wallet_group_description_view_seed": "آپ ہمیشہ اس بیج کو دوبارہ دیکھ سکتے ہیں", "wallet_group_empty_state_text_one": "ایسا لگتا ہے کہ آپ کے پاس کوئی مطابقت پذیر والیٹ گروپس نہیں ہیں !\n\n نل", "wallet_group_empty_state_text_two": "ایک نیا بنانے کے لئے ذیل میں.", "wallet_keys": "بٹوے کے بیج / چابیاں", diff --git a/res/values/strings_vi.arb b/res/values/strings_vi.arb index 0d1d90a69..f42d26957 100644 --- a/res/values/strings_vi.arb +++ b/res/values/strings_vi.arb @@ -84,6 +84,7 @@ "bitcoin_dark_theme": "Chủ đề Bitcoin tối", "bitcoin_light_theme": "Chủ đề Bitcoin sáng", "bitcoin_payments_require_1_confirmation": "Các khoản thanh toán Bitcoin yêu cầu 1 xác nhận, có thể mất 20 phút hoặc lâu hơn. Cảm ơn bạn đã kiên nhẫn! Bạn sẽ nhận được email khi thanh toán được xác nhận.", + "block_height": "Chiều cao khối", "block_remaining": "1 khối còn lại", "Blocks_remaining": "${status} khối còn lại", "bluetooth": "Bluetooth", @@ -131,6 +132,7 @@ "change_rep": "Thay đổi Đại diện", "change_rep_message": "Bạn có chắc chắn muốn thay đổi đại diện không?", "change_rep_successful": "Thay đổi đại diện thành công", + "change_selected_exchanges": "Thay đổi các trao đổi được chọn", "change_wallet_alert_content": "Bạn có muốn thay đổi ví hiện tại thành ${wallet_name} không?", "change_wallet_alert_title": "Thay đổi ví hiện tại", "choose_account": "Chọn tài khoản", @@ -295,7 +297,7 @@ "etherscan_history": "Lịch sử Etherscan", "event": "Sự kiện", "events": "Các sự kiện", - "exchange": "Tráo đổi", + "exchange": "Trao đổi", "exchange_incorrect_current_wallet_for_xmr": "Nếu bạn muốn trao đổi XMR từ CAPE CAME MONERO BALANCE, vui lòng chuyển sang ví Monero của bạn trước.", "exchange_new_template": "Mẫu mới", "exchange_provider_unsupported": "${providerName} không còn được hỗ trợ nữa!", @@ -381,6 +383,7 @@ "invalid_password": "Mật khẩu không hợp lệ", "invoice_details": "Chi tiết hóa đơn", "is_percentage": "là", + "keys": "Chìa khóa", "last_30_days": "30 ngày gần nhất", "learn_more": "Tìm hiểu thêm", "ledger_connection_error": "Không thể kết nối với Ledger của bạn. Vui lòng thử lại.", @@ -388,6 +391,7 @@ "ledger_error_tx_rejected_by_user": "Giao dịch bị từ chối trên thiết bị", "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", + "legacy": "Di sản", "light_theme": "Chủ đề sáng", "litecoin_enable_mweb_sync": "Bật quét MWEB", "litecoin_mweb": "Mweb", @@ -666,6 +670,7 @@ "select_backup_file": "Chọn tệp sao lưu", "select_buy_provider_notice": "Chọn nhà cung cấp mua ở trên. Bạn có thể bỏ qua màn hình này bằng cách thiết lập nhà cung cấp mua mặc định trong cài đặt ứng dụng.", "select_destination": "Vui lòng chọn đích cho tệp sao lưu.", + "select_hw_account_below": "Vui lòng chọn tài khoản nào để khôi phục bên dưới:", "select_sell_provider_notice": "Chọn nhà cung cấp bán ở trên. Bạn có thể bỏ qua màn hình này bằng cách thiết lập nhà cung cấp bán mặc định trong cài đặt ứng dụng.", "select_your_country": "Vui lòng chọn quốc gia của bạn", "sell": "Bán", @@ -696,6 +701,7 @@ "service_health_disabled": "Thông báo sức khỏe dịch vụ bị vô hiệu hóa", "service_health_disabled_message": "Đây là trang thông báo sức khỏe dịch vụ, bạn có thể kích hoạt trang này trong Cài đặt -> Quyền riêng tư", "set_a_pin": "Đặt một pin", + "set_up_a_wallet": "Thiết lập ví", "settings": "Cài đặt", "settings_all": "TẤT CẢ", "settings_allow_biometrical_authentication": "Cho phép xác thực sinh trắc học", @@ -731,7 +737,7 @@ "share_address": "Chia sẻ địa chỉ", "shared_seed_wallet_groups": "Nhóm ví hạt được chia sẻ", "show": "Trình diễn", - "show_address_book_popup": "Hiển thị cửa sổ bật lên 'Thêm vào sổ địa chỉ' sau khi gửi", + "show_address_book_popup": "Hiển thị cửa sổ bật lên sổ sách địa chỉ", "show_balance": "Báo chí dài để hiển thị sự cân bằng", "show_balance_toast": "Nhấn dài để ẩn hoặc hiển thị sự cân bằng", "show_details": "Hiển thị chi tiết", @@ -780,6 +786,7 @@ "support_title_guides": "Cake Wallet Docs", "support_title_live_chat": "Hỗ trợ trực tiếp", "support_title_other_links": "Liên kết hỗ trợ khác", + "swap": "Tráo đổi", "sweeping_wallet": "Quét ví", "sweeping_wallet_alert": "Việc này không nên mất nhiều thời gian. KHÔNG RỜI KHỎI MÀN HÌNH NÀY HOẶC CÁC KHOẢN TIỀN ĐƯỢC QUÉT CÓ THỂ BỊ MẤT.", "switchToETHWallet": "Vui lòng chuyển sang ví Ethereum và thử lại", @@ -805,6 +812,7 @@ "testnet_coins_no_value": "Tiền tệ testnet không có giá trị", "third_intro_content": "Yats cũng tồn tại ngoài Cake Wallet. Bất kỳ địa chỉ ví nào trên thế giới đều có thể được thay thế bằng một Yat!", "third_intro_title": "Yat tương thích tốt với các đối tượng khác", + "this_pair_is_not_supported_warning": "Cặp này không được hỗ trợ với (các) trao đổi hiện được chọn. Vui lòng chọn một trao đổi khác.", "thorchain_contract_address_not_supported": "THORChain không hỗ trợ gửi đến địa chỉ hợp đồng", "thorchain_taproot_address_not_supported": "Nhà cung cấp ThorChain không hỗ trợ địa chỉ Taproot. Vui lòng thay đổi địa chỉ hoặc chọn nhà cung cấp khác.", "time": "${minutes} phút ${seconds} giây", @@ -928,10 +936,13 @@ "waitFewSecondForTxUpdate": "Vui lòng đợi vài giây để giao dịch được phản ánh trong lịch sử giao dịch", "wallet": "Cái ví", "wallet_group": "Nhóm ví", + "wallet_group_description_existing_seed": "Bạn đã chọn sử dụng một hạt giống hiện có cho ví này. Bạn có thể xác minh lại hạt giống nếu bạn cần xác nhận hoặc viết nó ra.", "wallet_group_description_four": "Để tạo ra một ví với một hạt giống hoàn toàn mới.", "wallet_group_description_one": "Trong ví bánh, bạn có thể tạo", + "wallet_group_description_open_wallet": "Nếu không, bạn có thể tiếp tục mở ví", "wallet_group_description_three": "Để xem ví trên ví và/hoặc màn hình nhóm ví. Hoặc chọn", "wallet_group_description_two": "Bằng cách chọn một ví hiện có để chia sẻ một hạt giống với. Mỗi nhóm ví có thể chứa một ví của mỗi loại tiền tệ. \n\n Bạn có thể chọn", + "wallet_group_description_view_seed": "Bạn luôn có thể xem lại hạt giống này dưới", "wallet_group_empty_state_text_one": "Có vẻ như bạn không có bất kỳ nhóm ví tương thích nào !\n\n Tap", "wallet_group_empty_state_text_two": "Dưới đây để làm một cái mới.", "wallet_keys": "Hạt giống/khóa ví", diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index 1b613d2a4..d9f819a22 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -84,6 +84,7 @@ "bitcoin_dark_theme": "Bitcoin Dark Akori", "bitcoin_light_theme": "Bitcoin Light Akori", "bitcoin_payments_require_1_confirmation": "Àwọn àránṣẹ́ Bitcoin nílò ìjẹ́rìísí kan. Ó lè lo ìṣéjú ogun tàbí ìṣéjú jù. A dúpẹ́ fún sùúrù yín! Ẹ máa gba ímeèlì t'ó bá jẹ́rìísí àránṣẹ́ náà.", + "block_height": "Dènà giga", "block_remaining": "1 bulọọki to ku", "Blocks_remaining": "Àkójọpọ̀ ${status} kikù", "bluetooth": "Bluetooth", @@ -131,6 +132,7 @@ "change_rep": "Yi Aṣoju", "change_rep_message": "Ṣe o da ọ loju pe o fẹ yi awọn aṣoju pada?", "change_rep_successful": "Ni ifijišẹ yipada aṣoju", + "change_selected_exchanges": "Yiyipada awọn paṣipaarọ ti o yan", "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", @@ -297,7 +299,7 @@ "etherscan_history": "Etherscan itan", "event": "Iṣẹlẹ", "events": "Awọn iṣẹlẹ", - "exchange": "Eepo", + "exchange": "Paarọ", "exchange_incorrect_current_wallet_for_xmr": "Ti o ba fẹ lati yi XMR lati dọgba oyinbo oyinbo kekere rẹ ti a fi omi ṣan rẹ, jọwọ yipada si apamọwọ Monrou akọkọ.", "exchange_new_template": "Àwòṣe títun", "exchange_provider_unsupported": "${providerName} ko ni atilẹyin mọ!", @@ -383,6 +385,7 @@ "invalid_password": "Ọrọ igbaniwọle ti ko wulo", "invoice_details": "Iru awọn ẹya ọrọ", "is_percentage": "jẹ́", + "keys": "Awọn bọtini", "last_30_days": "Ọ̀jọ̀ mọ́gbọ̀n tó kọjà", "learn_more": "Túbọ̀ kọ́", "ledger_connection_error": "O kuna lati sopọ mọ ọ. Jọwọ gbiyanju lẹẹkansi.", @@ -390,6 +393,7 @@ "ledger_error_tx_rejected_by_user": "Idunadura kọ lori ẹrọ", "ledger_error_wrong_app": "Jọwọ rii daju pe iwọ yoo sọ app ti o tọ loju omi rẹ", "ledger_please_enable_bluetooth": "Jọwọ jẹ ki Bluetooth lati rii iṣupọ rẹ", + "legacy": "Agbara", "light_theme": "Funfun bí eérú", "litecoin_enable_mweb_sync": "Mu mweb ọlọjẹ", "litecoin_mweb": "Mweb", @@ -668,6 +672,7 @@ "select_backup_file": "Select backup file", "select_buy_provider_notice": "Yan olupese Ra loke. O le skii iboju yii nipa ṣiṣeto olupese rẹ ni awọn eto App.", "select_destination": "Jọwọ yan ibi ti o nlo fun faili afẹyinti.", + "select_hw_account_below": "Jọwọ yan iru iroyin lati mu pada ni isalẹ:", "select_sell_provider_notice": "Yan olupese ti o ta loke. O le foju iboju yii nipa tito olupese iṣẹ tita aiyipada rẹ ni awọn eto app.", "select_your_country": "Jọwọ yan orilẹ-ede rẹ", "sell": "Tà", @@ -698,6 +703,7 @@ "service_health_disabled": "IPỌRỌ IWE TI AGBARA TI O LE RẸ", "service_health_disabled_message": "Eyi ni oju-iwe Iwe itẹlera Iṣẹ Ile-iṣẹ Iṣẹ: O le mu oju-iwe yii ṣiṣẹ labẹ Eto -> Asiri", "set_a_pin": "Ṣeto PIN kan", + "set_up_a_wallet": "Ṣeto apamọwọ kan", "settings": "Awọn aseṣe", "settings_all": "Gbogbo", "settings_allow_biometrical_authentication": "Fi àyè gba ìfẹ̀rílàdí biometrical", @@ -733,7 +739,7 @@ "share_address": "Pín àdírẹ́sì", "shared_seed_wallet_groups": "Awọn ẹgbẹ ti a pin irugbin", "show": "Fihan", - "show_address_book_popup": "Fihan 'ṣafikun si Agbejade Iwe' Lẹhin fifiranṣẹ", + "show_address_book_popup": "Fihan Agbejade Iwe Adirẹsi", "show_balance": "Tẹ Tẹ lati ṣafihan iwọntunwọnsi", "show_balance_toast": "Tẹ Tẹ lati tọju tabi ṣafihan iwọntunwọnsi", "show_details": "Fi ìsọfúnni kékeré hàn", @@ -782,6 +788,7 @@ "support_title_guides": "Awọn iwe apamọwọ oyinbo akara oyinbo", "support_title_live_chat": "Atilẹyin ifiwe", "support_title_other_links": "Awọn ọna asopọ atilẹyin miiran", + "swap": "Eepo", "sweeping_wallet": "Fi owo iwe iwe wofo", "sweeping_wallet_alert": "Yio kọja pada si ikan yii. Kì yoo daadaa leede yii tabi owo ti o ti fi se iwe iwe naa yoo gbe.", "switchToETHWallet": "Jọwọ yipada si apamọwọ Ethereum ki o tun gbiyanju lẹẹkansi", @@ -807,6 +814,7 @@ "testnet_coins_no_value": "Awọn aṣọ irekọja ko ni iye", "third_intro_content": "A sì lè lo Yats níta Cake Wallet. A lè rọ́pò Àdírẹ́sì kankan àpamọ́wọ́ fún Yat!", "third_intro_title": "Àlàáfíà ni Yat àti àwọn ìmíìn jọ wà", + "this_pair_is_not_supported_warning": "Apo yii ko ni atilẹyin pẹlu paṣipaarọ (s) ti a ti yan lọwọlọwọ. Jọwọ yan paṣipaarọ miiran.", "thorchain_contract_address_not_supported": "Thorchain ko ṣe atilẹyin fifiranṣẹ si adirẹsi adehun kan", "thorchain_taproot_address_not_supported": "Olupese Trockchain ko ṣe atilẹyin awọn adirẹsi Taproot. Jọwọ yi adirẹsi pada tabi yan olupese ti o yatọ.", "time": "${minutes}ìṣj ${seconds}ìṣs", @@ -930,10 +938,13 @@ "waitFewSecondForTxUpdate": "Fi inurere duro fun awọn iṣeju diẹ fun idunadura lati ṣe afihan ninu itan-akọọlẹ iṣowo", "wallet": "Ohun apamọwọwọ", "wallet_group": "Ẹgbẹ apamọwọ", + "wallet_group_description_existing_seed": "O ti yan lati lo irugbin ti o wa tẹlẹ fun ogiriina yii.O le rii daju iru naa lẹẹkansii ti o ba nilo lati jẹrisi tabi kọ silẹ.", "wallet_group_description_four": "Lati ṣẹda apamọwọ kan pẹlu irugbin tuntun tuntun.", "wallet_group_description_one": "Ni apamọwọ akara oyinbo, o le ṣẹda a", + "wallet_group_description_open_wallet": "Bibẹẹkọ, o le tẹsiwaju lati ṣii apamọwọ", "wallet_group_description_three": "Lati wo awọn Woleti ti o wa ati / tabi Iboju Wallt. Tabi yan", "wallet_group_description_two": "nipa yiyan apamọwọ ti o wa tẹlẹ lati pin irugbin kan pẹlu. Ẹgbẹ apamọwọ kọọkan le ni apamọwọ kan ti iru owo kọọkan. \n\n O le yan", + "wallet_group_description_view_seed": "O le nigbagbogbo wo irugbin yii lẹẹkansi labẹ", "wallet_group_empty_state_text_one": "O dabi pe o ko ni eyikeyi awọn ẹgbẹ ti o ni ibamu!\n\ntẹ ni kia kia", "wallet_group_empty_state_text_two": "ni isalẹ lati ṣe ọkan titun.", "wallet_keys": "Hóró/kọ́kọ́rọ́ àpamọ́wọ́", diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index 04b653291..20f85d375 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -84,6 +84,7 @@ "bitcoin_dark_theme": "比特币黑暗主题", "bitcoin_light_theme": "比特币浅色主题", "bitcoin_payments_require_1_confirmation": "比特币支付需要 1 次确认,这可能需要 20 分钟或更长时间。谢谢你的耐心!确认付款后,您将收到电子邮件。", + "block_height": "块高度", "block_remaining": "剩下1个块", "Blocks_remaining": "${status} 剩余的块", "bluetooth": "蓝牙", @@ -131,6 +132,7 @@ "change_rep": "变革代表", "change_rep_message": "您确定要更换代表吗?", "change_rep_successful": "成功改变了代表", + "change_selected_exchanges": "更改选定的交换", "change_wallet_alert_content": "您是否想将当前钱包改为 ${wallet_name}?", "change_wallet_alert_title": "更换当前钱包", "choose_a_payment_method": "选择付款方式", @@ -382,6 +384,7 @@ "invalid_password": "无效的密码", "invoice_details": "发票明细", "is_percentage": "是", + "keys": "钥匙", "last_30_days": "过去 30 天", "learn_more": "了解更多", "ledger_connection_error": "无法连接到您的分类帐。请再试一次。", @@ -389,6 +392,7 @@ "ledger_error_tx_rejected_by_user": "交易在设备上拒绝", "ledger_error_wrong_app": "请确保您在分类帐中操作正确的应用程序", "ledger_please_enable_bluetooth": "请启用蓝牙来检测您的分类帐", + "legacy": "遗产", "light_theme": "艳丽", "litecoin_enable_mweb_sync": "启用MWEB扫描", "litecoin_mweb": "MWEB", @@ -667,6 +671,7 @@ "select_backup_file": "选择备份文件", "select_buy_provider_notice": "在上面选择买入提供商。您可以通过在应用程序设置中设置默认的购买提供商来跳过此屏幕。", "select_destination": "请选择备份文件的目的地。", + "select_hw_account_below": "请在下面选择要还原的帐户:", "select_sell_provider_notice": "选择上面的销售提供商。您可以通过在应用程序设置中设置默认销售提供商来跳过此屏幕。", "select_your_country": "请选择你的国家", "sell": "卖", @@ -697,6 +702,7 @@ "service_health_disabled": "服务健康公告被禁用", "service_health_disabled_message": "这是服务健康公告页面,您可以在设置 - >隐私下启用此页面", "set_a_pin": "设置一个别针", + "set_up_a_wallet": "设置钱包", "settings": "设置", "settings_all": "全部", "settings_allow_biometrical_authentication": "允许生物识别认证", @@ -732,7 +738,7 @@ "share_address": "分享地址", "shared_seed_wallet_groups": "共享种子钱包组", "show": "展示", - "show_address_book_popup": "发送后显示“添加到通讯簿”弹出窗口", + "show_address_book_popup": "显示地址簿弹出", "show_balance": "长印刷以显示平衡", "show_balance_toast": "长按以隐藏或显示平衡", "show_details": "显示详细信息", @@ -781,6 +787,7 @@ "support_title_guides": "蛋糕钱包文档", "support_title_live_chat": "实时支持", "support_title_other_links": "其他支持链接", + "swap": "交换", "sweeping_wallet": "扫一扫钱包", "sweeping_wallet_alert": "\n这应该不会花很长时间。请勿离开此屏幕,否则可能会丢失所掠取的资金", "switchToETHWallet": "请切换到以太坊钱包并重试", @@ -806,6 +813,7 @@ "testnet_coins_no_value": "TestNet硬币没有价值", "third_intro_content": "Yats 也住在 Cake Wallet 之外。 地球上任何一個錢包地址都可以用一個Yat來代替!", "third_intro_title": "Yat 和別人玩得很好", + "this_pair_is_not_supported_warning": "当前选择的交换不支持这对。请选择另一个交换。", "thorchain_contract_address_not_supported": "Thorchain不支持发送到合同地址", "thorchain_taproot_address_not_supported": "Thorchain提供商不支持Taproot地址。请更改地址或选择其他提供商。", "time": "${minutes}m ${seconds}s", @@ -929,10 +937,13 @@ "waitFewSecondForTxUpdate": "请等待几秒钟,交易才会反映在交易历史记录中", "wallet": "钱包", "wallet_group": "钱包组", + "wallet_group_description_existing_seed": "您已经选择在此钱包中使用现有种子。如果需要确认或写下来,您可能会再次验证种子。", "wallet_group_description_four": "创建一个带有全新种子的钱包。", "wallet_group_description_one": "在蛋糕钱包中,您可以创建一个", + "wallet_group_description_open_wallet": "否则,您可以继续打开钱包", "wallet_group_description_three": "查看可用的钱包和/或钱包组屏幕。或选择", "wallet_group_description_two": "通过选择现有的钱包与种子共享。每个钱包组都可以包含每种货币类型的单个钱包。\n\n您可以选择", + "wallet_group_description_view_seed": "您可以随时再次在下面查看此种子", "wallet_group_empty_state_text_one": "看起来您没有任何兼容的钱包组!\n\n tap", "wallet_group_empty_state_text_two": "下面是一个新的。", "wallet_keys": "钱包种子/密钥", diff --git a/run-android.sh b/run-android.sh index cb0c34038..feaf0d91d 100755 --- a/run-android.sh +++ b/run-android.sh @@ -1,5 +1,5 @@ #!/bin/bash -source "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/universal_sed.sh" +source "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/functions.sh" # Get the current git branch get_current_branch() { if git rev-parse --git-dir > /dev/null 2>&1; then diff --git a/scripts/android/app_env.sh b/scripts/android/app_env.sh index 45e28379d..8b1d46264 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.19.1" -MONERO_COM_BUILD_NUMBER=110 +MONERO_COM_VERSION="1.20.2" +MONERO_COM_BUILD_NUMBER=114 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.22.1" -CAKEWALLET_BUILD_NUMBER=242 +CAKEWALLET_VERSION="4.23.2" +CAKEWALLET_BUILD_NUMBER=247 CAKEWALLET_BUNDLE_ID="com.cakewallet.cake_wallet" CAKEWALLET_PACKAGE="com.cakewallet.cake_wallet" CAKEWALLET_SCHEME="cakewallet" diff --git a/scripts/android/build_monero_all.sh b/scripts/android/build_monero_all.sh index 71a6b6228..1cec707e6 100755 --- a/scripts/android/build_monero_all.sh +++ b/scripts/android/build_monero_all.sh @@ -1,4 +1,5 @@ #!/bin/bash +source "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/functions.sh" # Usage: env USE_DOCKER= ./build_all.sh @@ -6,11 +7,9 @@ set -x -e cd "$(dirname "$0")" -NPROC="-j$(nproc)" - ../prepare_moneroc.sh -for COIN in monero wownero; +for COIN in monero wownero zano; do pushd ../monero_c for target in {x86_64,aarch64}-linux-android armv7a-linux-androideabi @@ -19,9 +18,9 @@ do then echo "file exist, not building monero_c for ${COIN}/$target."; else - env -i ./build_single.sh ${COIN} $target $NPROC + ./build_single.sh ${COIN} $target -j$MAKE_JOB_COUNT unxz -f ../monero_c/release/${COIN}/${target}_libwallet2_api_c.so.xz fi done popd -done \ No newline at end of file +done diff --git a/scripts/android/copy_monero_deps.sh b/scripts/android/copy_monero_deps.sh index ed8181e6b..be621b683 100755 --- a/scripts/android/copy_monero_deps.sh +++ b/scripts/android/copy_monero_deps.sh @@ -5,6 +5,7 @@ CW_DIR=${WORKDIR}/cake_wallet CW_EXRTERNAL_DIR=${CW_DIR}/cw_shared_external/ios/External/android CW_HAVEN_EXTERNAL_DIR=${CW_DIR}/cw_haven/ios/External/android CW_MONERO_EXTERNAL_DIR=${CW_DIR}/cw_monero/ios/External/android +CW_ZANO_EXTERNAL_DIR=${CW_DIR}/cw_zano/ios/External/android for arch in "aarch" "aarch64" "i686" "x86_64" do @@ -40,5 +41,6 @@ done mkdir -p ${CW_HAVEN_EXTERNAL_DIR}/include mkdir -p ${CW_MONERO_EXTERNAL_DIR}/include +mkdir -p ${CW_ZANO_EXTERNAL_DIR}/include cp $CW_EXRTERNAL_DIR/x86/include/haven/wallet2_api.h ${CW_HAVEN_EXTERNAL_DIR}/include diff --git a/scripts/android/finish_boost.sh b/scripts/android/finish_boost.sh index 3cf656c55..d96de2d72 100755 --- a/scripts/android/finish_boost.sh +++ b/scripts/android/finish_boost.sh @@ -5,5 +5,5 @@ PREFIX=$2 BOOST_SRC_DIR=$3 cd $BOOST_SRC_DIR - -./b2 --build-type=minimal link=static runtime-link=static --with-chrono --with-date_time --with-filesystem --with-program_options --with-regex --with-serialization --with-system --with-thread --with-locale --build-dir=android --stagedir=android toolset=clang threading=multi threadapi=pthread target-os=android -sICONV_PATH=${PREFIX} -j$THREADS install +echo "Building boost" +./b2 --build-type=minimal link=static runtime-link=static --with-chrono --with-date_time --with-timer --with-filesystem --with-program_options --with-regex --with-serialization --with-system --with-thread --with-locale --with-log --build-dir=android --stagedir=android toolset=clang threading=multi threadapi=pthread target-os=android -sICONV_PATH=${PREFIX} -j$THREADS install diff --git a/scripts/android/inject_app_details.sh b/scripts/android/inject_app_details.sh index 7b0d74798..7f0858b18 100755 --- a/scripts/android/inject_app_details.sh +++ b/scripts/android/inject_app_details.sh @@ -1,5 +1,5 @@ #!/bin/bash -source "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/universal_sed.sh" +source "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/functions.sh" if [ -z "$APP_ANDROID_TYPE" ]; then echo "Please set APP_ANDROID_TYPE" exit 1 diff --git a/scripts/android/pubspec_gen.sh b/scripts/android/pubspec_gen.sh index febc4f9e9..5d6a24722 100755 --- a/scripts/android/pubspec_gen.sh +++ b/scripts/android/pubspec_gen.sh @@ -10,7 +10,7 @@ case $APP_ANDROID_TYPE in CONFIG_ARGS="--monero" ;; $CAKEWALLET) - CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana --tron --wownero" + CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana --tron --wownero --zano" if [ "$CW_WITH_HAVEN" = true ];then CONFIG_ARGS="$CONFIG_ARGS --haven" fi diff --git a/scripts/functions.sh b/scripts/functions.sh new file mode 100644 index 000000000..b4b12416a --- /dev/null +++ b/scripts/functions.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +detect_sed() { + if sed --version 2>/dev/null | grep -q "GNU"; then + SED_TYPE="GNU" + else + SED_TYPE="BSD" + fi +} + +universal_sed() { + local expression=$1 + local file=$2 + + if [[ "$SED_TYPE" == "GNU" ]]; then + sed -i "$expression" "$file" + else + sed -i '' "$expression" "$file" + fi +} + +detect_sed + +if [[ "$(uname)" == "Linux" ]]; +then + export MAKE_JOB_COUNT="$(expr $(printf '%s\n%s' $(( $(grep MemTotal: /proc/meminfo | cut -d: -f2 | cut -dk -f1) * 4 / (1048576 * 9) )) $(nproc) | sort -n | head -n1) '|' 1)" +elif [[ "$(uname)" == "Darwin" ]]; +then + export MAKE_JOB_COUNT="$(expr $(printf '%s\n%s' $(( $(sysctl -n hw.memsize) * 4 / (1073741824 * 9) )) $(sysctl -n hw.logicalcpu) | sort -n | head -n1) '|' 1)" +else + # Assume windows eh? + export MAKE_JOB_COUNT="$(expr $(printf '%s\n%s' $(( $(grep MemTotal: /proc/meminfo | cut -d: -f2 | cut -dk -f1) * 4 / (1048576 * 9) )) $(nproc) | sort -n | head -n1) '|' 1)" +fi \ No newline at end of file diff --git a/scripts/ios/app_config.sh b/scripts/ios/app_config.sh index 396ccd7f0..d118370b5 100755 --- a/scripts/ios/app_config.sh +++ b/scripts/ios/app_config.sh @@ -1,5 +1,5 @@ #!/bin/bash -source "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/universal_sed.sh" +source "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/functions.sh" set -x -e MONERO_COM="monero.com" CAKEWALLET="cakewallet" @@ -10,8 +10,7 @@ if [ -z "$APP_IOS_TYPE" ]; then echo "Please set APP_IOS_TYPE" exit 1 fi -./gen_framework.sh -cd .. # go to scipts +cd .. ./gen_android_manifest.sh cd .. # go to root cp -rf ./ios/Runner/InfoBase.plist ./ios/Runner/Info.plist @@ -32,7 +31,7 @@ case $APP_IOS_TYPE in CONFIG_ARGS="--monero" ;; $CAKEWALLET) - CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana --tron --wownero" + CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana --tron --wownero --zano" if [ "$CW_WITH_HAVEN" = true ];then CONFIG_ARGS="$CONFIG_ARGS --haven" fi diff --git a/scripts/ios/app_env.sh b/scripts/ios/app_env.sh index 079b12391..c6d3778f3 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.19.1" -MONERO_COM_BUILD_NUMBER=107 +MONERO_COM_VERSION="1.20.2" +MONERO_COM_BUILD_NUMBER=112 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="4.22.1" -CAKEWALLET_BUILD_NUMBER=289 +CAKEWALLET_VERSION="4.23.2" +CAKEWALLET_BUILD_NUMBER=298 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" HAVEN_NAME="Haven" diff --git a/scripts/ios/build_boost.sh b/scripts/ios/build_boost.sh index e2dc291ee..11dcead3e 100755 --- a/scripts/ios/build_boost.sh +++ b/scripts/ios/build_boost.sh @@ -1,23 +1,42 @@ -#!/bin/sh +#!/bin/bash + +set -e . ./config.sh MIN_IOS_VERSION=10.0 BOOST_URL="https://github.com/cake-tech/Apple-Boost-BuildScript.git" BOOST_DIR_PATH="${EXTERNAL_IOS_SOURCE_DIR}/Apple-Boost-BuildScript" -BOOST_VERSION=1.72.0 +BOOST_VERSION=1.84.0 BOOST_LIBS="random regex graph random chrono thread filesystem system date_time locale serialization program_options" echo "============================ Boost ============================" echo "Cloning Apple-Boost-BuildScript from - $BOOST_URL" -git clone -b build $BOOST_URL $BOOST_DIR_PATH -cd $BOOST_DIR_PATH + +# Check if the directory already exists. +if [ -d "$BOOST_DIR_PATH" ]; then + echo "Boost directory already exists." +else + echo "Cloning Boost from $BOOST_URL" + git clone -b build $BOOST_URL $BOOST_DIR_PATH +fi + +# Verify if the repository was cloned successfully. +if [ -d "$BOOST_DIR_PATH/.git" ]; then + echo "Boost repository cloned successfully." + cd $BOOST_DIR_PATH + git checkout build +else + echo "Failed to clone Boost repository. Exiting." + exit 1 +fi + ./boost.sh -ios \ --min-ios-version ${MIN_IOS_VERSION} \ --boost-libs "${BOOST_LIBS}" \ --boost-version ${BOOST_VERSION} \ --no-framework -mv ${BOOST_DIR_PATH}/build/boost/${BOOST_VERSION}/ios/release/prefix/include/* $EXTERNAL_IOS_INCLUDE_DIR -mv ${BOOST_DIR_PATH}/build/boost/${BOOST_VERSION}/ios/release/prefix/lib/* $EXTERNAL_IOS_LIB_DIR \ No newline at end of file +mv -f ${BOOST_DIR_PATH}/build/boost/${BOOST_VERSION}/ios/release/prefix/include/* $EXTERNAL_IOS_INCLUDE_DIR +mv -f ${BOOST_DIR_PATH}/build/boost/${BOOST_VERSION}/ios/release/prefix/lib/* $EXTERNAL_IOS_LIB_DIR \ No newline at end of file diff --git a/scripts/ios/build_monero_all.sh b/scripts/ios/build_monero_all.sh index a5993d181..69d4f006b 100755 --- a/scripts/ios/build_monero_all.sh +++ b/scripts/ios/build_monero_all.sh @@ -1,4 +1,5 @@ #!/bin/sh +source "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/functions.sh" . ./config.sh # ./install_missing_headers.sh @@ -12,17 +13,22 @@ set -x -e cd "$(dirname "$0")" -NPROC="-j$(sysctl -n hw.logicalcpu)" - ../prepare_moneroc.sh -for COIN in monero wownero; +for COIN in monero wownero zano; do pushd ../monero_c - rm -rf external/ios/build - ./build_single.sh ${COIN} host-apple-ios $NPROC + ./build_single.sh ${COIN} aarch64-apple-ios -j$MAKE_JOB_COUNT + ./build_single.sh ${COIN} aarch64-apple-iossimulator -j$MAKE_JOB_COUNT popd done -unxz -f ../monero_c/release/monero/host-apple-ios_libwallet2_api_c.dylib.xz -unxz -f ../monero_c/release/wownero/host-apple-ios_libwallet2_api_c.dylib.xz +unxz -fk ../monero_c/release/monero/aarch64-apple-ios_libwallet2_api_c.dylib.xz +unxz -fk ../monero_c/release/wownero/aarch64-apple-ios_libwallet2_api_c.dylib.xz +unxz -fk ../monero_c/release/zano/aarch64-apple-ios_libwallet2_api_c.dylib.xz + +unxz -fk ../monero_c/release/monero/aarch64-apple-iossimulator_libwallet2_api_c.dylib.xz +unxz -fk ../monero_c/release/wownero/aarch64-apple-iossimulator_libwallet2_api_c.dylib.xz +unxz -fk ../monero_c/release/zano/aarch64-apple-iossimulator_libwallet2_api_c.dylib.xz + +./gen_framework.sh \ No newline at end of file diff --git a/scripts/ios/build_mwebd.sh b/scripts/ios/build_mwebd.sh index e13c4931c..5bdd32e15 100755 --- a/scripts/ios/build_mwebd.sh +++ b/scripts/ios/build_mwebd.sh @@ -14,7 +14,7 @@ git clone https://github.com/ltcmweb/mwebd cd mwebd git reset --hard 555349415f76a42ec5c76152b64c4ab9aabc448f gomobile bind -target=ios . -mv -fn ./Mwebd.xcframework ../../../ios/ +mv -fn ./Mwebd.xcframework ../../../cw_mweb/ios/ # cleanup: cd .. rm -rf mwebd \ No newline at end of file diff --git a/scripts/ios/build_zano.sh b/scripts/ios/build_zano.sh new file mode 100755 index 000000000..d37ed226d --- /dev/null +++ b/scripts/ios/build_zano.sh @@ -0,0 +1,106 @@ +#!/bin/sh + +. ./config.sh + +ZANO_URL="https://github.com/hyle-team/zano.git" +ZANO_DIR_PATH="${EXTERNAL_IOS_SOURCE_DIR}/zano" +ZANO_VERSION=fde28efdc5d7efe8741dcb0e62ea0aebc805a373 + + +IOS_TOOLCHAIN_DIR_PATH="${EXTERNAL_IOS_SOURCE_DIR}/ios_toolchain" +IOS_TOOLCHAIN_URL="https://github.com/leetal/ios-cmake.git" +IOS_TOOLCHAIN_VERSION=06465b27698424cf4a04a5ca4904d50a3c966c45 + +export NO_DEFAULT_PATH + +BUILD_TYPE=release +PREFIX=${EXTERNAL_IOS_DIR} +DEST_LIB_DIR=${EXTERNAL_IOS_LIB_DIR}/zano +DEST_INCLUDE_DIR=${EXTERNAL_IOS_INCLUDE_DIR}/zano + +ZANO_MOBILE_IOS_BUILD_FOLDER_ARM64="${ZANO_DIR_PATH}/build" +ZANO_MOBILE_IOS_INSTALL_FOLDER_ARM64="${ZANO_DIR_PATH}/install" + +echo "ZANO_URL: $ZANO_URL" +echo "IOS_TOOLCHAIN_DIR_PATH: $IOS_TOOLCHAIN_DIR_PATH" +echo "ZANO_MOBILE_IOS_BUILD_FOLDER_ARM64: $ZANO_MOBILE_IOS_BUILD_FOLDER_ARM64" +echo "ZANO_MOBILE_IOS_INSTALL_FOLDER_ARM64: $ZANO_MOBILE_IOS_INSTALL_FOLDER_ARM64" +echo "PREFIX: $PREFIX" +echo "DEST_LIB_DIR: $DEST_LIB_DIR" +echo "DEST_INCLUDE_DIR: $DEST_INCLUDE_DIR" +echo "ZANO_DIR_PATH: $ZANO_DIR_PATH" + +echo "Cloning ios_toolchain from - $IOS_TOOLCHAIN_URL to - $IOS_TOOLCHAIN_DIR_PATH" +git clone $IOS_TOOLCHAIN_URL $IOS_TOOLCHAIN_DIR_PATH +cd $IOS_TOOLCHAIN_DIR_PATH +git checkout $IOS_TOOLCHAIN_VERSION +git submodule update --init --force +cd .. + +echo "Cloning zano from - $ZANO_URL to - $ZANO_DIR_PATH" +git clone $ZANO_URL $ZANO_DIR_PATH +cd $ZANO_DIR_PATH +git fetch origin +if [ $? -ne 0 ]; then + echo "Failed to perform command" + exit 1 +fi +git checkout $ZANO_VERSION +if [ $? -ne 0 ]; then + echo "Failed to perform command" + exit 1 +fi +git submodule update --init --force +if [ $? -ne 0 ]; then + echo "Failed to perform command" + exit 1 +fi +mkdir -p build +cd .. + + +export CMAKE_INCLUDE_PATH="${PREFIX}/include" +export CMAKE_LIBRARY_PATH="${PREFIX}/lib" + + +rm -rf ${ZANO_MOBILE_IOS_BUILD_FOLDER_ARM64} > /dev/null +rm -rf ${ZANO_MOBILE_IOS_INSTALL_FOLDER_ARM64} > /dev/null + +echo "CMAKE_INCLUDE_PATH: $CMAKE_INCLUDE_PATH" +echo "CMAKE_LIBRARY_PATH: $CMAKE_LIBRARY_PATH" +echo "ROOT_DIR: $ROOT_DIR" + + +cmake -DCMAKE_BUILD_TYPE=$BUILD_TYPE \ + -DCMAKE_TOOLCHAIN_FILE="${IOS_TOOLCHAIN_DIR_PATH}/ios.toolchain.cmake" \ + -DPLATFORM=OS64 \ + -S"${ZANO_DIR_PATH}" \ + -B"${ZANO_MOBILE_IOS_BUILD_FOLDER_ARM64}" \ + -GXcode \ + -DCAKEWALLET=TRUE \ + -DSKIP_BOOST_FATLIB_LIB=TRUE \ + -DCMAKE_SYSTEM_NAME=iOS \ + -DCMAKE_INSTALL_PREFIX="${ZANO_MOBILE_IOS_INSTALL_FOLDER_ARM64}" \ + -DCMAKE_XCODE_ATTRIBUTE_ONLY_ACTIVE_ARCH=NO \ + -DCMAKE_CXX_FLAGS="-Wno-enum-constexpr-conversion" \ + -DDISABLE_TOR=TRUE + +# -DCMAKE_OSX_ARCHITECTURES="arm64" +# -DCMAKE_IOS_INSTALL_COMBINED=YES + +if [ $? -ne 0 ]; then + echo "Failed to perform command" + exit 1 +fi + +cmake --build "${ZANO_MOBILE_IOS_BUILD_FOLDER_ARM64}" --config $BUILD_TYPE --target install -- -j 4 +if [ $? -ne 0 ]; then + echo "Failed to perform command" + exit 1 +fi + +mkdir -p $DEST_LIB_DIR +mkdir -p $DEST_INCLUDE_DIR + +cp ${ZANO_MOBILE_IOS_INSTALL_FOLDER_ARM64}/lib/* $DEST_LIB_DIR +cp ${ZANO_DIR_PATH}/src/wallet/plain_wallet_api.h $DEST_INCLUDE_DIR diff --git a/scripts/ios/build_zano_all.sh b/scripts/ios/build_zano_all.sh new file mode 100755 index 000000000..2e5ef81d5 --- /dev/null +++ b/scripts/ios/build_zano_all.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +. ./config.sh +./install_missing_headers.sh +./build_openssl.sh +./build_boost.sh +./build_zano.sh diff --git a/scripts/ios/gen_framework.sh b/scripts/ios/gen_framework.sh index 5c9bcd228..e1c237f87 100755 --- a/scripts/ios/gen_framework.sh +++ b/scripts/ios/gen_framework.sh @@ -1,31 +1,159 @@ #!/bin/sh -# Assume we are in scripts/ios +set -e + IOS_DIR="$(pwd)/../../ios" -DYLIB_NAME="monero_libwallet2_api_c.dylib" -DYLIB_LINK_PATH="${IOS_DIR}/${DYLIB_NAME}" -FRWK_DIR="${IOS_DIR}/MoneroWallet.framework" +DYLIB_PATH="$(pwd)/../../scripts/monero_c/release" +TMP_DIR="${IOS_DIR}/tmp" -if [ ! -f $DYLIB_LINK_PATH ]; then - echo "Dylib is not found by the link: ${DYLIB_LINK_PATH}" - exit 0 -fi +rm -rf "${IOS_DIR:?}/MoneroWallet.xcframework" "${IOS_DIR:?}/WowneroWallet.xcframework" "${IOS_DIR:?}/ZanoWallet.xcframework" +rm -rf "${IOS_DIR:?}/MoneroWallet.framework" "${IOS_DIR:?}/WowneroWallet.framework" "${IOS_DIR:?}/ZanoWallet.framework" +rm -rf "$TMP_DIR" +mkdir -p "$TMP_DIR" -cd $FRWK_DIR # go to iOS framework dir -lipo -create $DYLIB_LINK_PATH -output MoneroWallet +write_info_plist() { + framework_bundle="$1" + framework_name="$2" + target="$3" + plist_path="${framework_bundle}/Info.plist" -echo "Generated ${FRWK_DIR}" -# also generate for wownero -IOS_DIR="$(pwd)/../../ios" -DYLIB_NAME="wownero_libwallet2_api_c.dylib" -DYLIB_LINK_PATH="${IOS_DIR}/${DYLIB_NAME}" -FRWK_DIR="${IOS_DIR}/WowneroWallet.framework" + if [[ "x$target" = "xiossimulator" ]]; then + platform="iPhoneSimulator" + dtplatformname="iphonesimulator" + dtsdkname="iphonesimulator17.4" + else + platform="iPhoneOS" + dtplatformname="iphoneos" + dtsdkname="iphoneos17.4" + fi -if [ ! -f $DYLIB_LINK_PATH ]; then - echo "Dylib is not found by the link: ${DYLIB_LINK_PATH}" - exit 0 -fi + cat > "$plist_path" < + + + + BuildMachineOSBuild + 23E224 + CFBundleDevelopmentRegion + en + CFBundleExecutable + ${framework_name} + CFBundleIdentifier + com.fotolockr.${framework_name} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${framework_name} + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ??? + CFBundleSupportedPlatforms + + ${platform} + + CFBundleVersion + 1 + DTCompiler + com.apple.compilers.llvm.clang.1_0 + DTPlatformBuild + 21E210 + DTPlatformName + ${dtplatformname} + DTPlatformVersion + 17.4 + DTSDKBuild + 21E210 + DTSDKName + ${dtsdkname} + DTXcode + 1530 + DTXcodeBuild + 15E204a + MinimumOSVersion + 16.0 + UIDeviceFamily + + 1 + 2 + + UIRequiredDeviceCapabilities + + arm64 + + + +EOF + plutil -convert binary1 "$plist_path" +} -cd $FRWK_DIR # go to iOS framework dir -lipo -create $DYLIB_LINK_PATH -output WowneroWallet +create_framework() { + wallet="$1" + framework_name="$2" + target="$3" + out_dir="$4" -echo "Generated ${FRWK_DIR}" + echo "Creating ${framework_name}.framework for target ${target} in ${out_dir}..." + + framework_bundle="${out_dir}/${framework_name}.framework" + + rm -rf "$framework_bundle" + mkdir -p "$framework_bundle" + + input_dylib="${DYLIB_PATH}/${wallet}/aarch64-apple-${target}_libwallet2_api_c.dylib" + if [[ ! -f "$input_dylib" ]]; then + echo "Error: Input dylib not found: $input_dylib" + exit 1 + fi + + lipo -create "$input_dylib" -output "${framework_bundle}/${framework_name}" + echo "Created binary: ${framework_bundle}/${framework_name}" + + write_info_plist "$framework_bundle" "$framework_name" "$target" +} + +create_xcframework() { + framework_name="$1" + device_framework="$2" + simulator_framework="$3" + xcframework_output="$4" + + echo "Creating ${xcframework_output} by bundling:" + echo " Device framework: ${device_framework}" + echo " Simulator framework: ${simulator_framework}" + + xcodebuild -create-xcframework \ + -framework "$device_framework" \ + -framework "$simulator_framework" \ + -output "$xcframework_output" + + echo "Created XCFramework: ${xcframework_output}" +} + +wallets=("monero" "wownero" "zano") +framework_names=("MoneroWallet" "WowneroWallet" "ZanoWallet") + +for i in "${!wallets[@]}"; do + wallet="${wallets[$i]}" + framework_name="${framework_names[$i]}" + + device_out="${TMP_DIR}/${framework_name}_device" + simulator_out="${TMP_DIR}/${framework_name}_simulator" + rm -rf "$device_out" "$simulator_out" + mkdir -p "$device_out" "$simulator_out" + + create_framework "$wallet" "$framework_name" "ios" "$device_out" + create_framework "$wallet" "$framework_name" "iossimulator" "$simulator_out" + + device_framework="${device_out}/${framework_name}.framework" + simulator_framework="${simulator_out}/${framework_name}.framework" + xcframework_output="${IOS_DIR}/${framework_name}.xcframework" + rm -rf "$xcframework_output" + + create_xcframework "$framework_name" "$device_framework" "$simulator_framework" "$xcframework_output" +done + +echo "All XCFrameworks created successfully." + +rm -rf "$TMP_DIR" diff --git a/scripts/ios/setup.sh b/scripts/ios/setup.sh index abe8435ae..b17b3718b 100755 --- a/scripts/ios/setup.sh +++ b/scripts/ios/setup.sh @@ -13,16 +13,21 @@ fi libtool -static -o libboost.a ./libboost_*.a libtool -static -o libhaven.a ./haven/*.a libtool -static -o libmonero.a ./monero/*.a +libtool -static -o libzano.a ./zano/*.a CW_HAVEN_EXTERNAL_LIB=../../../../../cw_haven/ios/External/ios/lib CW_HAVEN_EXTERNAL_INCLUDE=../../../../../cw_haven/ios/External/ios/include CW_MONERO_EXTERNAL_LIB=../../../../../cw_monero/ios/External/ios/lib CW_MONERO_EXTERNAL_INCLUDE=../../../../../cw_monero/ios/External/ios/include +CW_ZANO_EXTERNAL_LIB=../../../../../cw_zano/ios/External/ios/lib +CW_ZANO_EXTERNAL_INCLUDE=../../../../../cw_zano/ios/External/ios/include mkdir -p $CW_HAVEN_EXTERNAL_INCLUDE mkdir -p $CW_MONERO_EXTERNAL_INCLUDE +mkdir -p $CW_ZANO_EXTERNAL_INCLUDE mkdir -p $CW_HAVEN_EXTERNAL_LIB mkdir -p $CW_MONERO_EXTERNAL_LIB +mkdir -p $CW_ZANO_EXTERNAL_LIB ln ./libboost.a ${CW_HAVEN_EXTERNAL_LIB}/libboost.a ln ./libcrypto.a ${CW_HAVEN_EXTERNAL_LIB}/libcrypto.a @@ -37,4 +42,10 @@ ln ./libssl.a ${CW_MONERO_EXTERNAL_LIB}/libssl.a ln ./libsodium.a ${CW_MONERO_EXTERNAL_LIB}/libsodium.a ln ./libunbound.a ${CW_MONERO_EXTERNAL_LIB}/libunbound.a cp ./libmonero.a $CW_MONERO_EXTERNAL_LIB -cp ../include/monero/* $CW_MONERO_EXTERNAL_INCLUDE \ No newline at end of file +cp ../include/monero/* $CW_MONERO_EXTERNAL_INCLUDE + +ln ./libboost.a ${CW_ZANO_EXTERNAL_LIB}/libboost.a +ln ./libcrypto.a ${CW_ZANO_EXTERNAL_LIB}/libcrypto.a +ln ./libssl.a ${CW_ZANO_EXTERNAL_LIB}/libssl.a +cp ./libzano.a $CW_ZANO_EXTERNAL_LIB +cp ../include/zano/* $CW_ZANO_EXTERNAL_INCLUDE diff --git a/scripts/linux/.fvmrc b/scripts/linux/.fvmrc new file mode 100644 index 000000000..262e5e837 --- /dev/null +++ b/scripts/linux/.fvmrc @@ -0,0 +1,3 @@ +{ + "flutter": "3.24.0" +} \ No newline at end of file diff --git a/scripts/linux/.gitignore b/scripts/linux/.gitignore new file mode 100644 index 000000000..9e366fe3b --- /dev/null +++ b/scripts/linux/.gitignore @@ -0,0 +1,3 @@ + +# FVM Version Cache +.fvm/ \ No newline at end of file diff --git a/scripts/linux/Dockerfile.linux b/scripts/linux/Dockerfile.linux index c8f4d3bde..b41873cf5 100644 --- a/scripts/linux/Dockerfile.linux +++ b/scripts/linux/Dockerfile.linux @@ -53,6 +53,8 @@ RUN set -o xtrace \ && apt-get install -y udev qemu-kvm libvirt-daemon-system libvirt-clients bridge-utils \ # for linux tests && apt-get install -y xvfb network-manager ffmpeg x11-utils \ + # for aarch64-linux-gnu + && apt-get install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu \ && rm -rf /var/lib/apt/lists/* \ && sh -c 'echo "en_US.UTF-8 UTF-8" > /etc/locale.gen' \ && locale-gen \ diff --git a/scripts/linux/app_config.sh b/scripts/linux/app_config.sh index 0fc41bd0f..5d9d8597b 100755 --- a/scripts/linux/app_config.sh +++ b/scripts/linux/app_config.sh @@ -1,5 +1,5 @@ #!/bin/bash -source "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/universal_sed.sh" +source "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/functions.sh" CAKEWALLET="cakewallet" DIR=`pwd` diff --git a/scripts/linux/app_env.sh b/scripts/linux/app_env.sh index 5103eeccc..325d2b335 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.12.1" -CAKEWALLET_BUILD_NUMBER=43 +CAKEWALLET_VERSION="1.13.2" +CAKEWALLET_BUILD_NUMBER=47 if ! [[ " ${TYPES[*]} " =~ " ${APP_LINUX_TYPE} " ]]; then echo "Wrong app type." diff --git a/scripts/linux/build_monero_all.sh b/scripts/linux/build_monero_all.sh index 558423219..7113d88ef 100755 --- a/scripts/linux/build_monero_all.sh +++ b/scripts/linux/build_monero_all.sh @@ -1,23 +1,22 @@ #!/bin/bash +source "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/functions.sh" set -x -e cd "$(dirname "$0")" -NPROC="-j$(nproc)" - ../prepare_moneroc.sh -for COIN in monero wownero; +for COIN in monero wownero zano; do pushd ../monero_c - for target in x86_64-linux-gnu + for target in x86_64-linux-gnu # aarch64-linux-gnu do if [[ -f "release/${COIN}/${target}_libwallet2_api_c.so" ]]; then echo "file exist, not building monero_c for ${COIN}/$target."; else - ./build_single.sh ${COIN} $target $NPROC + ./build_single.sh ${COIN} $target -j$MAKE_JOB_COUNT unxz -f ../monero_c/release/${COIN}/${target}_libwallet2_api_c.so.xz fi done diff --git a/scripts/macos/app_config.sh b/scripts/macos/app_config.sh index 452205dd9..bb4750803 100755 --- a/scripts/macos/app_config.sh +++ b/scripts/macos/app_config.sh @@ -1,5 +1,5 @@ #!/bin/bash -source "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/universal_sed.sh" +source "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/functions.sh" MONERO_COM="monero.com" CAKEWALLET="cakewallet" diff --git a/scripts/macos/app_env.sh b/scripts/macos/app_env.sh index af2b6ab1e..f554d4d01 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.9.1" -MONERO_COM_BUILD_NUMBER=40 +MONERO_COM_VERSION="1.10.2" +MONERO_COM_BUILD_NUMBER=44 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="1.15.1" -CAKEWALLET_BUILD_NUMBER=101 +CAKEWALLET_VERSION="1.16.2" +CAKEWALLET_BUILD_NUMBER=105 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" if ! [[ " ${TYPES[*]} " =~ " ${APP_MACOS_TYPE} " ]]; then diff --git a/scripts/macos/build_monero_all.sh b/scripts/macos/build_monero_all.sh index edc8efe81..f46674242 100755 --- a/scripts/macos/build_monero_all.sh +++ b/scripts/macos/build_monero_all.sh @@ -22,11 +22,7 @@ then popd done else - if [[ "x$1" == "xuniversal" ]]; then ARCHS=(x86_64 arm64) - else - ARCHS=$(uname -m) - fi for COIN in monero wownero; do MONERO_LIBS="" @@ -34,24 +30,16 @@ else for ARCH in "${ARCHS[@]}"; do if [[ "$ARCH" == "arm64" ]]; then - export HOMEBREW_PREFIX=/opt/homebrew - HOST="aarch64-host-apple-darwin" + HOST="aarch64-apple-darwin" else - export HOMEBREW_PREFIX=/usr/local - HOST="${ARCH}-host-apple-darwin" + HOST="x86_64-apple-darwin" fi MONERO_LIBS="$MONERO_LIBS -arch ${ARCH} ${MONEROC_RELEASE_DIR}/${HOST}_libwallet2_api_c.dylib" WOWNERO_LIBS="$WOWNERO_LIBS -arch ${ARCH} ${WOWNEROC_RELEASE_DIR}/${HOST}_libwallet2_api_c.dylib" - if [[ ! $(uname -m) == $ARCH ]]; then - PRC="arch -${ARCH}" - else - PRC="" - fi - pushd ../monero_c - $PRC ./build_single.sh ${COIN} ${HOST} $NPROC + ./build_single.sh ${COIN} ${HOST} -j$MAKE_JOB_COUNT unxz -f ./release/${COIN}/${HOST}_libwallet2_api_c.dylib.xz popd done diff --git a/scripts/macos/gen_common.sh b/scripts/macos/gen_common.sh index d1d40edc9..72ff638b6 100755 --- a/scripts/macos/gen_common.sh +++ b/scripts/macos/gen_common.sh @@ -1,5 +1,5 @@ #!/bin/sh -source "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/universal_sed.sh" +source "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/functions.sh" gen_podspec() { ARCH=$1 CW_PLUGIN_DIR="`pwd`/../../cw_monero/macos" diff --git a/scripts/prepare_moneroc.sh b/scripts/prepare_moneroc.sh index c0de33f6f..ec2cb4908 100755 --- a/scripts/prepare_moneroc.sh +++ b/scripts/prepare_moneroc.sh @@ -8,24 +8,23 @@ if [[ ! -d "monero_c/.git" ]]; then git clone https://github.com/mrcyjanek/monero_c --branch master monero_c cd monero_c - git checkout af5277f96073917185864d3596e82b67bee54e78 + git checkout 65608c09e9093f1cd42c6afd8e9131016c82574b git reset --hard git submodule update --init --force --recursive ./apply_patches.sh monero ./apply_patches.sh wownero + ./apply_patches.sh zano else cd monero_c fi -if [[ ! -f "monero/.patch-applied" ]]; -then - ./apply_patches.sh monero -fi - -if [[ ! -f "wownero/.patch-applied" ]]; -then - ./apply_patches.sh wownero -fi +for coin in monero wownero zano; +do + if [[ ! -f "$coin/.patch-applied" ]]; + then + ./apply_patches.sh $coin + fi +done cd .. echo "monero_c source prepared". diff --git a/scripts/universal_sed.sh b/scripts/universal_sed.sh deleted file mode 100644 index d8a95684e..000000000 --- a/scripts/universal_sed.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/bash - -detect_sed() { - if sed --version 2>/dev/null | grep -q "GNU"; then - SED_TYPE="GNU" - else - SED_TYPE="BSD" - fi -} - -universal_sed() { - local expression=$1 - local file=$2 - - if [[ "$SED_TYPE" == "GNU" ]]; then - sed -i "$expression" "$file" - else - sed -i '' "$expression" "$file" - fi -} - -detect_sed diff --git a/scripts/windows/build_all.sh b/scripts/windows/build_all.sh index e77f6edb5..90a60dac7 100755 --- a/scripts/windows/build_all.sh +++ b/scripts/windows/build_all.sh @@ -1,4 +1,6 @@ +#!/bin/bash set -x -e +source "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/functions.sh" cd "$(dirname "$0")" @@ -16,19 +18,18 @@ pushd ../monero_c set +e command -v sudo && export SUDO=sudo set -e - NPROC="-j$(nproc)" if [[ ! "x$USE_DOCKER" == "x" ]]; then for COIN in monero wownero; do - $SUDO docker run --platform linux/amd64 -v$HOME/.cache/ccache:/root/.ccache -v$PWD:$PWD -w $PWD --rm -it git.mrcyjanek.net/mrcyjanek/debian:buster bash -c "git config --global --add safe.directory '*'; apt update; apt install -y ccache gcc-mingw-w64-x86-64 g++-mingw-w64-x86-64 gperf libtinfo5; ./build_single.sh ${COIN} x86_64-w64-mingw32 $NPROC" - # $SUDO docker run --platform linux/amd64 -v$HOME/.cache/ccache:/root/.ccache -v$PWD:$PWD -w $PWD --rm -it git.mrcyjanek.net/mrcyjanek/debian:buster bash -c "git config --global --add safe.directory '*'; apt update; apt install -y ccache gcc-mingw-w64-i686 g++-mingw-w64-i686 gperf libtinfo5; ./build_single.sh ${COIN} i686-w64-mingw32 $NPROC" + $SUDO docker run --platform linux/amd64 -v$HOME/.cache/ccache:/root/.ccache -v$PWD:$PWD -w $PWD --rm -it git.mrcyjanek.net/mrcyjanek/debian:buster bash -c "git config --global --add safe.directory '*'; apt update; apt install -y ccache gcc-mingw-w64-x86-64 g++-mingw-w64-x86-64 gperf libtinfo5; ./build_single.sh ${COIN} x86_64-w64-mingw32 -j$MAKE_JOB_COUNT" + # $SUDO docker run --platform linux/amd64 -v$HOME/.cache/ccache:/root/.ccache -v$PWD:$PWD -w $PWD --rm -it git.mrcyjanek.net/mrcyjanek/debian:buster bash -c "git config --global --add safe.directory '*'; apt update; apt install -y ccache gcc-mingw-w64-i686 g++-mingw-w64-i686 gperf libtinfo5; ./build_single.sh ${COIN} i686-w64-mingw32 -j$MAKE_JOB_COUNT" done else for COIN in monero wownero; do - $SUDO ./build_single.sh ${COIN} x86_64-w64-mingw32 $NPROC - # $SUDO ./build_single.sh ${COIN} i686-w64-mingw32 $NPROC + $SUDO ./build_single.sh ${COIN} x86_64-w64-mingw32 -j$MAKE_JOB_COUNT + # $SUDO ./build_single.sh ${COIN} i686-w64-mingw32 -j$MAKE_JOB_COUNT done fi popd diff --git a/scripts/windows/build_exe_installer.iss b/scripts/windows/build_exe_installer.iss index 950800896..b2dd60130 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.3.1" +#define MyAppVersion "0.4.2" #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 69e876f45..e1ad3b52d 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -10,6 +10,7 @@ const polygonOutputPath = 'lib/polygon/polygon.dart'; const solanaOutputPath = 'lib/solana/solana.dart'; const tronOutputPath = 'lib/tron/tron.dart'; const wowneroOutputPath = 'lib/wownero/wownero.dart'; +const zanoOutputPath = 'lib/zano/zano.dart'; const walletTypesPath = 'lib/wallet_types.g.dart'; const secureStoragePath = 'lib/core/secure_storage.dart'; const pubspecDefaultPath = 'pubspec_default.yaml'; @@ -28,6 +29,7 @@ Future main(List args) async { final hasSolana = args.contains('${prefix}solana'); final hasTron = args.contains('${prefix}tron'); final hasWownero = args.contains('${prefix}wownero'); + final hasZano = args.contains('${prefix}zano'); final excludeFlutterSecureStorage = args.contains('${prefix}excludeFlutterSecureStorage'); await generateBitcoin(hasBitcoin); @@ -40,6 +42,7 @@ Future main(List args) async { await generateSolana(hasSolana); await generateTron(hasTron); await generateWownero(hasWownero); + await generateZano(hasZano); // await generateBanano(hasEthereum); await generatePubspec( @@ -55,6 +58,7 @@ Future main(List args) async { hasSolana: hasSolana, hasTron: hasTron, hasWownero: hasWownero, + hasZano: hasZano, ); await generateWalletTypes( hasMonero: hasMonero, @@ -68,6 +72,7 @@ Future main(List args) async { hasSolana: hasSolana, hasTron: hasTron, hasWownero: hasWownero, + hasZano: hasZano, ); await injectSecureStorage(!excludeFlutterSecureStorage); } @@ -400,9 +405,9 @@ abstract class Monero { required String password, required String language, required int height}); - WalletCredentials createMoneroRestoreWalletFromSeedCredentials({required String name, required String password, required int height, required String mnemonic}); + WalletCredentials createMoneroRestoreWalletFromSeedCredentials({required String name, required String password, required String passphrase, 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}); + WalletCredentials createMoneroNewWalletCredentials({required String name, required String language, required bool isPolyseed, required String? passphrase, String? password}); Map getKeys(Object wallet); int? getRestoreHeight(Object wallet); Object createMoneroTransactionCreationCredentials({required List outputs, required TransactionPriority priority}); @@ -590,8 +595,9 @@ abstract class Wownero { required String password, required String language, required int height}); - WalletCredentials createWowneroRestoreWalletFromSeedCredentials({required String name, required String password, required int height, required String mnemonic}); - WalletCredentials createWowneroNewWalletCredentials({required String name, required String language, required bool isPolyseed, String? password}); + WalletCredentials createWowneroRestoreWalletFromSeedCredentials({required String name, required String password, required String passphrase, required int height, required String mnemonic}); + WalletCredentials createWowneroNewWalletCredentials({required String name, required String language, required bool isPolyseed, String? password, String? passphrase}); + int? getRestoreHeight(Object wallet); Map getKeys(Object wallet); Object createWowneroTransactionCreationCredentials({required List outputs, required TransactionPriority priority}); Object createWowneroTransactionCreationCredentialsRaw({required List outputs, required TransactionPriority priority}); @@ -1401,6 +1407,77 @@ abstract class Tron { await outputFile.writeAsString(output); } +Future generateZano(bool hasImplementation) async { + final outputFile = File(zanoOutputPath); + const zanoCommonHeaders = """ +import 'package:cake_wallet/utils/language_list.dart'; +import 'package:cake_wallet/view_model/send/output.dart'; +import 'package:collection/collection.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/monero_transaction_priority.dart'; +import 'package:cw_core/output_info.dart'; +import 'package:cw_core/transaction_history.dart'; +import 'package:cw_core/transaction_info.dart'; +import 'package:cw_core/transaction_priority.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_credentials.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/wallet_service.dart'; +import 'package:cw_core/zano_asset.dart'; +import 'package:hive/hive.dart'; +"""; + const zanoCWHeaders = """ +import 'package:cw_zano/mnemonics/english.dart'; +import 'package:cw_zano/model/zano_transaction_credentials.dart'; +import 'package:cw_zano/model/zano_transaction_info.dart'; +import 'package:cw_zano/zano_formatter.dart'; +import 'package:cw_zano/zano_wallet.dart'; +import 'package:cw_zano/zano_wallet_service.dart'; +import 'package:cw_zano/zano_utils.dart'; +"""; + const zanoCwPart = "part 'cw_zano.dart';"; + const zanoContent = """ +abstract class Zano { + TransactionPriority getDefaultTransactionPriority(); + TransactionPriority deserializeMoneroTransactionPriority({required int raw}); + List getTransactionPriorities(); + List getWordList(String language); + + WalletCredentials createZanoRestoreWalletFromSeedCredentials({required String name, required String password, required String passphrase, required int height, required String mnemonic}); + WalletCredentials createZanoNewWalletCredentials({required String name, required String? password, required String? passphrase}); + Map getKeys(Object wallet); + Object createZanoTransactionCredentials({required List outputs, required TransactionPriority priority, required CryptoCurrency currency}); + double formatterIntAmountToDouble({required int amount, required CryptoCurrency currency, required bool forFee}); + int formatterParseAmount({required String amount, required CryptoCurrency currency}); + WalletService createZanoWalletService(Box walletInfoSource); + CryptoCurrency? assetOfTransaction(WalletBase wallet, TransactionInfo tx); + List getZanoAssets(WalletBase wallet); + String getZanoAssetAddress(CryptoCurrency asset); + Future changeZanoAssetAvailability(WalletBase wallet, CryptoCurrency token); + Future addZanoAssetById(WalletBase wallet, String assetId); + Future deleteZanoAsset(WalletBase wallet, CryptoCurrency token); + Future getZanoAsset(WalletBase wallet, String contractAddress); + String getAddress(WalletBase wallet); + bool validateAddress(String address); +} +"""; + const zanoEmptyDefinition = 'Zano? zano;\n'; + const zanoCWDefinition = 'Zano? zano = CWZano();\n'; + + final output = '$zanoCommonHeaders\n' + + (hasImplementation ? '$zanoCWHeaders\n' : '\n') + + (hasImplementation ? '$zanoCwPart\n\n' : '\n') + + (hasImplementation ? zanoCWDefinition : zanoEmptyDefinition) + + '\n' + + zanoContent; + + if (outputFile.existsSync()) { + await outputFile.delete(); + } + + await outputFile.writeAsString(output); +} + Future generatePubspec({ required bool hasMonero, required bool hasBitcoin, @@ -1414,6 +1491,7 @@ Future generatePubspec({ required bool hasSolana, required bool hasTron, required bool hasWownero, + required bool hasZano, }) async { const cwCore = """ cw_core: @@ -1478,6 +1556,10 @@ Future generatePubspec({ cw_wownero: path: ./cw_wownero """; + const cwZano = """ + cw_zano: + path: ./cw_zano + """; final inputFile = File(pubspecOutputPath); final inputText = await inputFile.readAsString(); final inputLines = inputText.split('\n'); @@ -1539,6 +1621,10 @@ Future generatePubspec({ output += '\n$cwWownero'; } + if (hasZano) { + output += '\n$cwZano'; + } + final outputLines = output.split('\n'); inputLines.insertAll(dependenciesIndex + 1, outputLines); final outputContent = inputLines.join('\n'); @@ -1563,6 +1649,7 @@ Future generateWalletTypes({ required bool hasSolana, required bool hasTron, required bool hasWownero, + required bool hasZano, }) async { final walletTypesFile = File(walletTypesPath); @@ -1610,6 +1697,10 @@ Future generateWalletTypes({ outputContent += '\tWalletType.nano,\n'; } + if (hasZano) { + outputContent += '\tWalletType.zano,\n'; + } + if (hasBanano) { outputContent += '\tWalletType.banano,\n'; } diff --git a/tool/download_moneroc_prebuilds.dart b/tool/download_moneroc_prebuilds.dart index 5169ea687..8909ca8a8 100644 --- a/tool/download_moneroc_prebuilds.dart +++ b/tool/download_moneroc_prebuilds.dart @@ -23,7 +23,8 @@ final List triplets = [ // "host-apple-darwin", // not available on CI (yet) // "x86_64-host-apple-darwin", // not available on CI (yet) "aarch64-host-apple-darwin", // apple silicon macbooks (local builds) - "host-apple-ios", + "aarch64-apple-ios", + "aarch64-apple-iossimulator", ]; Future main() async { @@ -43,11 +44,13 @@ Future main() async { final url = asset["browser_download_url"] as String; printV("- downloading $localFilename"); await _dio.download(url, localFilename); - printV(" extracting $localFilename"); - final inputStream = InputFileStream(localFilename); - final archive = XZDecoder().decodeBuffer(inputStream); - final outputStream = OutputFileStream(localFilename.replaceAll(".xz", "")); - outputStream.writeBytes(archive); + if (localFilename.endsWith(".xz")) { + printV(" extracting $localFilename"); + final inputStream = InputFileStream(localFilename); + final archive = XZDecoder().decodeBuffer(inputStream); + final outputStream = OutputFileStream(localFilename.replaceAll(".xz", "")); + outputStream.writeBytes(archive); + } } } if (Platform.isMacOS) { diff --git a/tool/utils/secret_key.dart b/tool/utils/secret_key.dart index 5c316c54b..48614900a 100644 --- a/tool/utils/secret_key.dart +++ b/tool/utils/secret_key.dart @@ -38,7 +38,7 @@ class SecretKey { SecretKey('moralisApiKey', () => ''), SecretKey('ankrApiKey', () => ''), SecretKey('chainStackApiKey', () => ''), - SecretKey('quantexExchangeMarkup', () => ''), + SecretKey('swapTradeExchangeMarkup', () => ''), SecretKey('seeds', () => ''), SecretKey('testCakePayApiKey', () => ''), SecretKey('cakePayApiKey', () => ''), @@ -75,6 +75,8 @@ class SecretKey { SecretKey('stealthExBearerToken', () => ''), SecretKey('stealthExAdditionalFeePercent', () => ''), SecretKey('moneroTestWalletBlockHeight', () => ''), + SecretKey('chainflipApiKey', () => ''), + SecretKey('chainflipAffiliateFee', () => ''), ]; static final evmChainsSecrets = [ diff --git a/windows/CMakeLists.txt b/windows/CMakeLists.txt index 09bc8cfb6..231aaaf19 100644 --- a/windows/CMakeLists.txt +++ b/windows/CMakeLists.txt @@ -87,8 +87,6 @@ install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/../scripts/monero_c/release/monero/x8 install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/../scripts/monero_c/release/wownero/x86_64-w64-mingw32_libwallet2_api_c.dll" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" RENAME "wownero_libwallet2_api_c.dll" COMPONENT Runtime) -install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/../scripts/monero_c/release/monero/x86_64-w64-mingw32_libpolyseed.dll" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" RENAME "libpolyseed.dll" - COMPONENT Runtime) install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/../scripts/monero_c/release/monero/x86_64-w64-mingw32_libssp-0.dll" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" RENAME "libssp-0.dll" COMPONENT Runtime)