diff --git a/.github/workflows/cache_dependencies.yml b/.github/workflows/cache_dependencies.yml index 902a44a42..c0042bf5c 100644 --- a/.github/workflows/cache_dependencies.yml +++ b/.github/workflows/cache_dependencies.yml @@ -23,9 +23,10 @@ jobs: docker-images: true - uses: actions/checkout@v2 - - uses: actions/setup-java@v1 + - uses: actions/setup-java@v2 with: - java-version: "17.x" + distribution: "temurin" + java-version: "17" - name: Configure placeholder git details run: | git config --global user.email "CI@cakewallet.com" diff --git a/.github/workflows/pr_test_build_android.yml b/.github/workflows/pr_test_build_android.yml index 5dbf1610f..c9021fac0 100644 --- a/.github/workflows/pr_test_build_android.yml +++ b/.github/workflows/pr_test_build_android.yml @@ -13,6 +13,9 @@ on: jobs: PR_test_build: runs-on: ubuntu-20.04 + strategy: + matrix: + api-level: [29] env: STORE_PASS: test@cake_wallet KEY_PASS: test@cake_wallet @@ -39,9 +42,10 @@ jobs: docker-images: true - uses: actions/checkout@v2 - - uses: actions/setup-java@v1 + - uses: actions/setup-java@v2 with: - java-version: "17.x" + distribution: "temurin" + java-version: "17" - name: Configure placeholder git details run: | git config --global user.email "CI@cakewallet.com" @@ -92,6 +96,25 @@ jobs: cd /opt/android/cake_wallet flutter pub get + + - name: Install go and gomobile + run: | + # install go > 1.23: + wget https://go.dev/dl/go1.23.1.linux-amd64.tar.gz + sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go1.23.1.linux-amd64.tar.gz + export PATH=$PATH:/usr/local/go/bin + export PATH=$PATH:~/go/bin + go install golang.org/x/mobile/cmd/gomobile@latest + gomobile init + + - name: Build mwebd + run: | + # paths are reset after each step, so we need to set them again: + export PATH=$PATH:/usr/local/go/bin + export PATH=$PATH:~/go/bin + cd /opt/android/cake_wallet/scripts/android/ + ./build_mwebd.sh --dont-install + - name: Generate KeyStore run: | cd /opt/android/cake_wallet/android/app @@ -148,6 +171,8 @@ jobs: echo "const fiatApiKey = '${{ secrets.FIAT_API_KEY }}';" >> lib/.secrets.g.dart echo "const payfuraApiKey = '${{ secrets.PAYFURA_API_KEY }}';" >> lib/.secrets.g.dart echo "const ankrApiKey = '${{ secrets.ANKR_API_KEY }}';" >> lib/.secrets.g.dart + echo "const etherScanApiKey = '${{ secrets.ETHER_SCAN_API_KEY }}';" >> lib/.secrets.g.dart + echo "const polygonScanApiKey = '${{ secrets.POLYGON_SCAN_API_KEY }}';" >> lib/.secrets.g.dart echo "const etherScanApiKey = '${{ secrets.ETHER_SCAN_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart echo "const moralisApiKey = '${{ secrets.MORALIS_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart echo "const chatwootWebsiteToken = '${{ secrets.CHATWOOT_WEBSITE_TOKEN }}';" >> lib/.secrets.g.dart @@ -167,6 +192,10 @@ jobs: echo "const nanoNowNodesApiKey = '${{ secrets.NANO_NOW_NODES_API_KEY }}';" >> cw_nano/lib/.secrets.g.dart echo "const tronGridApiKey = '${{ secrets.TRON_GRID_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart echo "const tronNowNodesApiKey = '${{ secrets.TRON_NOW_NODES_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart + echo "const letsExchangeBearerToken = '${{ secrets.LETS_EXCHANGE_TOKEN }}';" >> lib/.secrets.g.dart + echo "const letsExchangeAffiliateId = '${{ secrets.LETS_EXCHANGE_AFFILIATE_ID }}';" >> lib/.secrets.g.dart + echo "const stealthExBearerToken = '${{ secrets.STEALTH_EX_BEARER_TOKEN }}';" >> lib/.secrets.g.dart + echo "const stealthExAdditionalFeePercent = '${{ secrets.STEALTH_EX_ADDITIONAL_FEE_PERCENT }}';" >> lib/.secrets.g.dart - name: Rename app run: | @@ -195,6 +224,7 @@ jobs: cd /opt/android/cake_wallet/build/app/outputs/flutter-apk mkdir test-apk cp app-arm64-v8a-release.apk test-apk/${{env.BRANCH_NAME}}.apk + cp app-x86_64-release.apk test-apk/${{env.BRANCH_NAME}}_x86.apk - name: Upload Artifact uses: kittaakos/upload-artifact-as-is@v0 diff --git a/.github/workflows/pr_test_build_linux.yml b/.github/workflows/pr_test_build_linux.yml index 7935dd177..5ea0cb377 100644 --- a/.github/workflows/pr_test_build_linux.yml +++ b/.github/workflows/pr_test_build_linux.yml @@ -89,6 +89,25 @@ jobs: cd /opt/android/cake_wallet flutter pub get + - name: Install go and gomobile + run: | + # install go > 1.23: + wget https://go.dev/dl/go1.23.1.linux-amd64.tar.gz + sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go1.23.1.linux-amd64.tar.gz + export PATH=$PATH:/usr/local/go/bin + export PATH=$PATH:~/go/bin + go install golang.org/x/mobile/cmd/gomobile@latest + gomobile init + + - name: Build mwebd + run: | + # paths are reset after each step, so we need to set them again: + export PATH=$PATH:/usr/local/go/bin + export PATH=$PATH:~/go/bin + # build mwebd: + cd /opt/android/cake_wallet/scripts/android/ + ./build_mwebd.sh --dont-install + - name: Generate localization run: | cd /opt/android/cake_wallet @@ -125,6 +144,8 @@ jobs: echo "const sideShiftAffiliateId = '${{ secrets.SIDE_SHIFT_AFFILIATE_ID }}';" >> lib/.secrets.g.dart echo "const simpleSwapApiKey = '${{ secrets.SIMPLE_SWAP_API_KEY }}';" >> lib/.secrets.g.dart echo "const simpleSwapApiKeyDesktop = '${{ secrets.SIMPLE_SWAP_API_KEY_DESKTOP }}';" >> lib/.secrets.g.dart + echo "const polygonScanApiKey = '${{ secrets.POLYGON_SCAN_API_KEY }}';" >> lib/.secrets.g.dart + echo "const etherScanApiKey = '${{ secrets.ETHER_SCAN_API_KEY }}';" >> lib/.secrets.g.dart echo "const onramperApiKey = '${{ secrets.ONRAMPER_API_KEY }}';" >> lib/.secrets.g.dart echo "const anypayToken = '${{ secrets.ANY_PAY_TOKEN }}';" >> lib/.secrets.g.dart echo "const ioniaClientId = '${{ secrets.IONIA_CLIENT_ID }}';" >> lib/.secrets.g.dart @@ -154,6 +175,10 @@ jobs: echo "const nanoNowNodesApiKey = '${{ secrets.NANO_NOW_NODES_API_KEY }}';" >> cw_nano/lib/.secrets.g.dart echo "const tronGridApiKey = '${{ secrets.TRON_GRID_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart echo "const tronNowNodesApiKey = '${{ secrets.TRON_NOW_NODES_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart + echo "const letsExchangeBearerToken = '${{ secrets.LETS_EXCHANGE_TOKEN }}';" >> lib/.secrets.g.dart + echo "const letsExchangeAffiliateId = '${{ secrets.LETS_EXCHANGE_AFFILIATE_ID }}';" >> lib/.secrets.g.dart + echo "const stealthExBearerToken = '${{ secrets.STEALTH_EX_BEARER_TOKEN }}';" >> lib/.secrets.g.dart + echo "const stealthExAdditionalFeePercent = '${{ secrets.STEALTH_EX_ADDITIONAL_FEE_PERCENT }}';" >> lib/.secrets.g.dart - name: Rename app run: | diff --git a/.gitignore b/.gitignore index 8336ca512..970241189 100644 --- a/.gitignore +++ b/.gitignore @@ -171,6 +171,9 @@ macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png macos/Runner/Configs/AppInfo.xcconfig + +integration_test/playground.dart + # Monero.dart (Monero_C) scripts/monero_c # iOS generated framework bin diff --git a/README.md b/README.md index 6e507bfcd..1c28f92a2 100644 --- a/README.md +++ b/README.md @@ -161,7 +161,9 @@ The only parts to be translated, if needed, are the values m and s after the var 4. Add the language to `lib/entities/language_service.dart` under both `supportedLocales` and `localeCountryCode`. Use the name of the language in the local language and in English in parentheses after for `supportedLocales`. Use the [ISO 3166-1 alpha-3 code](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-3) for `localeCountryCode`. You must choose one country, so choose the country with the most native speakers of this language or is otherwise best associated with this language. -5. Add a relevant flag to `assets/images/flags/XXXX.png`, replacing XXXX with the 3 digit localeCountryCode. The image must be 42x26 pixels with a 3 pixels of transparent margin on all 4 sides. You can resize the flag with [paint.net](https://www.getpaint.net/) to 36x20 pixels, expand the canvas to 42x26 pixels with the flag anchored in the middle, and then manually delete the 3 pixels on each side to make transparent. Or you can use another program like Photoshop. +5. Add a relevant flag to `assets/images/flags/XXXX.png`, replacing XXXX with the 3 letters localeCountryCode. The image must be 42x26 pixels with a 3 pixels of transparent margin on all 4 sides. You can resize the flag with [paint.net](https://www.getpaint.net/) to 36x20 pixels, expand the canvas to 42x26 pixels with the flag anchored in the middle, and then manually delete the 3 pixels on each side to make transparent. Or you can use another program like Photoshop. + +6. Add the new language code to `tool/utils/translation/translation_constants.dart` ## Add a new fiat currency diff --git a/analysis_options.yaml b/analysis_options.yaml index 396904041..be68a4f26 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,5 +1,6 @@ include: package:lints/recommended.yaml + analyzer: exclude: [ build/**, @@ -10,7 +11,18 @@ analyzer: lib/generated/*.dart, cw_monero/ios/External/**, cw_shared_external/**, - shared_external/**] + shared_external/**, + lib/bitcoin/cw_bitcoin.dart, + lib/bitcoin_cash/cw_bitcoin_cash.dart, + lib/ethereum/cw_ethereum.dart, + lib/haven/cw_haven.dart, + lib/monero/cw_monero.dart, + lib/nano/cw_nano.dart, + lib/polygon/cw_polygon.dart, + lib/solana/cw_solana.dart, + lib/tron/cw_tron.dart, + lib/wownero/cw_wownero.dart, + ] language: strict-casts: true strict-raw-types: true @@ -72,4 +84,4 @@ linter: # - unawaited_futures # - unnecessary_getters_setters # - unrelated_type_equality_checks -# - valid_regexps \ No newline at end of file +# - valid_regexps diff --git a/android/app/src/main/AndroidManifestBase.xml b/android/app/src/main/AndroidManifestBase.xml index b03c8a925..98bbb1236 100644 --- a/android/app/src/main/AndroidManifestBase.xml +++ b/android/app/src/main/AndroidManifestBase.xml @@ -1,35 +1,30 @@ - - - - - + + - - + - - - + + + - - - + + + + - + + + + - + + - - - diff --git a/assets/images/flags/are.png b/assets/images/flags/are.png index ae68c4ff2..2df30486a 100644 Binary files a/assets/images/flags/are.png and b/assets/images/flags/are.png differ diff --git a/assets/images/flags/arg.png b/assets/images/flags/arg.png index c5bd233d2..d87458455 100644 Binary files a/assets/images/flags/arg.png and b/assets/images/flags/arg.png differ diff --git a/assets/images/flags/arm.png b/assets/images/flags/arm.png new file mode 100644 index 000000000..0e4c356e2 Binary files /dev/null and b/assets/images/flags/arm.png differ diff --git a/assets/images/flags/aus.png b/assets/images/flags/aus.png index c8837731c..2364fa9ef 100644 Binary files a/assets/images/flags/aus.png and b/assets/images/flags/aus.png differ diff --git a/assets/images/flags/bgd.png b/assets/images/flags/bgd.png index 0f8c5cfe5..802233d1c 100644 Binary files a/assets/images/flags/bgd.png and b/assets/images/flags/bgd.png differ diff --git a/assets/images/flags/bgr.png b/assets/images/flags/bgr.png index a89509f1f..fc53406b4 100644 Binary files a/assets/images/flags/bgr.png and b/assets/images/flags/bgr.png differ diff --git a/assets/images/flags/bra.png b/assets/images/flags/bra.png index ecac6f5a3..6c4ba8968 100644 Binary files a/assets/images/flags/bra.png and b/assets/images/flags/bra.png differ diff --git a/assets/images/flags/cad.png b/assets/images/flags/cad.png index 106cea5b9..a96ea16f6 100644 Binary files a/assets/images/flags/cad.png and b/assets/images/flags/cad.png differ diff --git a/assets/images/flags/che.png b/assets/images/flags/che.png index 427db0fbc..eab82b708 100644 Binary files a/assets/images/flags/che.png and b/assets/images/flags/che.png differ diff --git a/assets/images/flags/chl.png b/assets/images/flags/chl.png index 73a38f406..4328f0d08 100644 Binary files a/assets/images/flags/chl.png and b/assets/images/flags/chl.png differ diff --git a/assets/images/flags/chn.png b/assets/images/flags/chn.png index 7a03dd26e..d326f7afa 100644 Binary files a/assets/images/flags/chn.png and b/assets/images/flags/chn.png differ diff --git a/assets/images/flags/col.png b/assets/images/flags/col.png index 9a0fc6ac1..798cd6374 100644 Binary files a/assets/images/flags/col.png and b/assets/images/flags/col.png differ diff --git a/assets/images/flags/czk.png b/assets/images/flags/czk.png index a6c13a773..e4578e86c 100644 Binary files a/assets/images/flags/czk.png and b/assets/images/flags/czk.png differ diff --git a/assets/images/flags/deu.png b/assets/images/flags/deu.png index 95b88a0ea..9bebd76f3 100644 Binary files a/assets/images/flags/deu.png and b/assets/images/flags/deu.png differ diff --git a/assets/images/flags/dnk.png b/assets/images/flags/dnk.png index 69dd1b2b8..e02d42bfe 100644 Binary files a/assets/images/flags/dnk.png and b/assets/images/flags/dnk.png differ diff --git a/assets/images/flags/egy.png b/assets/images/flags/egy.png index 062ee21cf..692e7de78 100644 Binary files a/assets/images/flags/egy.png and b/assets/images/flags/egy.png differ diff --git a/assets/images/flags/esp.png b/assets/images/flags/esp.png index 0193a6a44..6b12aff49 100644 Binary files a/assets/images/flags/esp.png and b/assets/images/flags/esp.png differ diff --git a/assets/images/flags/eur.png b/assets/images/flags/eur.png index 1312b0200..2500968a9 100644 Binary files a/assets/images/flags/eur.png and b/assets/images/flags/eur.png differ diff --git a/assets/images/flags/fra.png b/assets/images/flags/fra.png index 91dce8ff2..2f432f4af 100644 Binary files a/assets/images/flags/fra.png and b/assets/images/flags/fra.png differ diff --git a/assets/images/flags/gbr.png b/assets/images/flags/gbr.png index 151f06db5..8b53f8851 100644 Binary files a/assets/images/flags/gbr.png and b/assets/images/flags/gbr.png differ diff --git a/assets/images/flags/gha.png b/assets/images/flags/gha.png index 8d6801e81..6e38331bb 100644 Binary files a/assets/images/flags/gha.png and b/assets/images/flags/gha.png differ diff --git a/assets/images/flags/gtm.png b/assets/images/flags/gtm.png index 2083ad806..8841a352a 100644 Binary files a/assets/images/flags/gtm.png and b/assets/images/flags/gtm.png differ diff --git a/assets/images/flags/hau.png b/assets/images/flags/hau.png index 7583b5daf..2bfb0a71c 100644 Binary files a/assets/images/flags/hau.png and b/assets/images/flags/hau.png differ diff --git a/assets/images/flags/hkg.png b/assets/images/flags/hkg.png index 85925604e..8c4cd78ae 100644 Binary files a/assets/images/flags/hkg.png and b/assets/images/flags/hkg.png differ diff --git a/assets/images/flags/hrv.png b/assets/images/flags/hrv.png index 9c87c5d0e..43c936b1e 100644 Binary files a/assets/images/flags/hrv.png and b/assets/images/flags/hrv.png differ diff --git a/assets/images/flags/hun.png b/assets/images/flags/hun.png index 9722561a8..397910aa3 100644 Binary files a/assets/images/flags/hun.png and b/assets/images/flags/hun.png differ diff --git a/assets/images/flags/idn.png b/assets/images/flags/idn.png index 52c965921..d1f01d8b5 100644 Binary files a/assets/images/flags/idn.png and b/assets/images/flags/idn.png differ diff --git a/assets/images/flags/ind.png b/assets/images/flags/ind.png index ef721a2aa..45d4a7109 100644 Binary files a/assets/images/flags/ind.png and b/assets/images/flags/ind.png differ diff --git a/assets/images/flags/irn.png b/assets/images/flags/irn.png index 151a03919..ec59e48c3 100644 Binary files a/assets/images/flags/irn.png and b/assets/images/flags/irn.png differ diff --git a/assets/images/flags/isl.png b/assets/images/flags/isl.png index ed545e905..d24f29abb 100644 Binary files a/assets/images/flags/isl.png and b/assets/images/flags/isl.png differ diff --git a/assets/images/flags/isr.png b/assets/images/flags/isr.png index 9f815dcbd..f28dff1ad 100644 Binary files a/assets/images/flags/isr.png and b/assets/images/flags/isr.png differ diff --git a/assets/images/flags/ita.png b/assets/images/flags/ita.png index 768f5a181..8af7190b8 100644 Binary files a/assets/images/flags/ita.png and b/assets/images/flags/ita.png differ diff --git a/assets/images/flags/jpn.png b/assets/images/flags/jpn.png index a13ef4178..b5c6fb4c4 100644 Binary files a/assets/images/flags/jpn.png and b/assets/images/flags/jpn.png differ diff --git a/assets/images/flags/kor.png b/assets/images/flags/kor.png index 36e867ea8..ccc181522 100644 Binary files a/assets/images/flags/kor.png and b/assets/images/flags/kor.png differ diff --git a/assets/images/flags/mar.png b/assets/images/flags/mar.png index 65b31c892..dda3a0131 100644 Binary files a/assets/images/flags/mar.png and b/assets/images/flags/mar.png differ diff --git a/assets/images/flags/mex.png b/assets/images/flags/mex.png index 9531a3ea2..f81102912 100644 Binary files a/assets/images/flags/mex.png and b/assets/images/flags/mex.png differ diff --git a/assets/images/flags/mmr.png b/assets/images/flags/mmr.png index 7fc6e1661..c2f9fec10 100644 Binary files a/assets/images/flags/mmr.png and b/assets/images/flags/mmr.png differ diff --git a/assets/images/flags/mys.png b/assets/images/flags/mys.png index 022476291..d151a3e4f 100644 Binary files a/assets/images/flags/mys.png and b/assets/images/flags/mys.png differ diff --git a/assets/images/flags/nga.png b/assets/images/flags/nga.png index ebfd82449..26efc073e 100644 Binary files a/assets/images/flags/nga.png and b/assets/images/flags/nga.png differ diff --git a/assets/images/flags/nld.png b/assets/images/flags/nld.png index 62dbc2058..220937426 100644 Binary files a/assets/images/flags/nld.png and b/assets/images/flags/nld.png differ diff --git a/assets/images/flags/nor.png b/assets/images/flags/nor.png index bd226c0a6..dfb43c5d5 100644 Binary files a/assets/images/flags/nor.png and b/assets/images/flags/nor.png differ diff --git a/assets/images/flags/nzl.png b/assets/images/flags/nzl.png index 11c6ade9c..306fdc778 100644 Binary files a/assets/images/flags/nzl.png and b/assets/images/flags/nzl.png differ diff --git a/assets/images/flags/pak.png b/assets/images/flags/pak.png index 1462650e4..b8c966ea1 100644 Binary files a/assets/images/flags/pak.png and b/assets/images/flags/pak.png differ diff --git a/assets/images/flags/phl.png b/assets/images/flags/phl.png index b453f3933..3c2ce7bf3 100644 Binary files a/assets/images/flags/phl.png and b/assets/images/flags/phl.png differ diff --git a/assets/images/flags/pol.png b/assets/images/flags/pol.png index 30d5a9371..1188eccd1 100644 Binary files a/assets/images/flags/pol.png and b/assets/images/flags/pol.png differ diff --git a/assets/images/flags/prt.png b/assets/images/flags/prt.png index ff5a25fa9..268676679 100644 Binary files a/assets/images/flags/prt.png and b/assets/images/flags/prt.png differ diff --git a/assets/images/flags/rou.png b/assets/images/flags/rou.png index 49b36b438..db1e24ca2 100644 Binary files a/assets/images/flags/rou.png and b/assets/images/flags/rou.png differ diff --git a/assets/images/flags/rus.png b/assets/images/flags/rus.png index 2633dcbd0..460c7b813 100644 Binary files a/assets/images/flags/rus.png and b/assets/images/flags/rus.png differ diff --git a/assets/images/flags/saf.png b/assets/images/flags/saf.png index 3b9cbded8..9accc6b5f 100644 Binary files a/assets/images/flags/saf.png and b/assets/images/flags/saf.png differ diff --git a/assets/images/flags/sau.png b/assets/images/flags/sau.png index 97951983a..255dabedd 100644 Binary files a/assets/images/flags/sau.png and b/assets/images/flags/sau.png differ diff --git a/assets/images/flags/sgp.png b/assets/images/flags/sgp.png index 5782ea144..a677561d4 100644 Binary files a/assets/images/flags/sgp.png and b/assets/images/flags/sgp.png differ diff --git a/assets/images/flags/swe.png b/assets/images/flags/swe.png index ef73086f6..e5ee36d2f 100644 Binary files a/assets/images/flags/swe.png and b/assets/images/flags/swe.png differ diff --git a/assets/images/flags/tha.png b/assets/images/flags/tha.png index 1bdb04d00..a99cd4d48 100644 Binary files a/assets/images/flags/tha.png and b/assets/images/flags/tha.png differ diff --git a/assets/images/flags/tur.png b/assets/images/flags/tur.png index 166c6313a..e86b5a85e 100644 Binary files a/assets/images/flags/tur.png and b/assets/images/flags/tur.png differ diff --git a/assets/images/flags/twn.png b/assets/images/flags/twn.png index 4af8ba78d..34a2b37db 100644 Binary files a/assets/images/flags/twn.png and b/assets/images/flags/twn.png differ diff --git a/assets/images/flags/ukr.png b/assets/images/flags/ukr.png index 61071e338..c4fe57fcc 100644 Binary files a/assets/images/flags/ukr.png and b/assets/images/flags/ukr.png differ diff --git a/assets/images/flags/usa.png b/assets/images/flags/usa.png index a8c44ce75..30fc880b7 100644 Binary files a/assets/images/flags/usa.png and b/assets/images/flags/usa.png differ diff --git a/assets/images/flags/ven.png b/assets/images/flags/ven.png index fcc25ef2b..c189b0545 100644 Binary files a/assets/images/flags/ven.png and b/assets/images/flags/ven.png differ diff --git a/assets/images/flags/vnm.png b/assets/images/flags/vnm.png index 3cbbf878f..d313c9912 100644 Binary files a/assets/images/flags/vnm.png and b/assets/images/flags/vnm.png differ diff --git a/assets/images/hardware_wallet/ledger_flex.png b/assets/images/hardware_wallet/ledger_flex.png new file mode 100644 index 000000000..fa39f241f Binary files /dev/null and b/assets/images/hardware_wallet/ledger_flex.png differ diff --git a/assets/images/hardware_wallet/ledger_nano_s.png b/assets/images/hardware_wallet/ledger_nano_s.png new file mode 100644 index 000000000..02777aeb6 Binary files /dev/null and b/assets/images/hardware_wallet/ledger_nano_s.png differ diff --git a/assets/images/hardware_wallet/ledger_nano_x.png b/assets/images/hardware_wallet/ledger_nano_x.png new file mode 100644 index 000000000..e9328a5ef Binary files /dev/null and b/assets/images/hardware_wallet/ledger_nano_x.png differ diff --git a/assets/images/hardware_wallet/ledger_stax.png b/assets/images/hardware_wallet/ledger_stax.png new file mode 100644 index 000000000..06c9c848e Binary files /dev/null and b/assets/images/hardware_wallet/ledger_stax.png differ diff --git a/assets/images/ledger_nano.png b/assets/images/ledger_nano.png deleted file mode 100644 index bb61ba175..000000000 Binary files a/assets/images/ledger_nano.png and /dev/null differ diff --git a/assets/images/letsexchange_icon.svg b/assets/images/letsexchange_icon.svg new file mode 100644 index 000000000..104b43a6b --- /dev/null +++ b/assets/images/letsexchange_icon.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/assets/images/mweb_logo.png b/assets/images/mweb_logo.png new file mode 100644 index 000000000..92317203e Binary files /dev/null and b/assets/images/mweb_logo.png differ diff --git a/assets/images/nanogpt.png b/assets/images/nanogpt.png new file mode 100644 index 000000000..958400452 Binary files /dev/null and b/assets/images/nanogpt.png differ diff --git a/assets/images/stealthex.png b/assets/images/stealthex.png new file mode 100644 index 000000000..311d47b74 Binary files /dev/null and b/assets/images/stealthex.png differ diff --git a/assets/images/ton_icon.png b/assets/images/ton_icon.png new file mode 100644 index 000000000..90f9968cc Binary files /dev/null and b/assets/images/ton_icon.png differ diff --git a/assets/images/wallet_group_bright.png b/assets/images/wallet_group_bright.png new file mode 100644 index 000000000..263361db6 Binary files /dev/null and b/assets/images/wallet_group_bright.png differ diff --git a/assets/images/wallet_group_dark.png b/assets/images/wallet_group_dark.png new file mode 100644 index 000000000..7cd08d2cd Binary files /dev/null and b/assets/images/wallet_group_dark.png differ diff --git a/assets/images/wallet_group_light.png b/assets/images/wallet_group_light.png new file mode 100644 index 000000000..7827971e7 Binary files /dev/null and b/assets/images/wallet_group_light.png differ diff --git a/assets/node_list.yml b/assets/node_list.yml index bc7a9dc4a..d04a9a2e8 100644 --- a/assets/node_list.yml +++ b/assets/node_list.yml @@ -17,6 +17,3 @@ - uri: node.community.rino.io:18081 is_default: false -- - uri: node.moneroworld.com:18089 - is_default: false diff --git a/assets/solana_node_list.yml b/assets/solana_node_list.yml index e3ff9138e..e5641d3f8 100644 --- a/assets/solana_node_list.yml +++ b/assets/solana_node_list.yml @@ -4,4 +4,7 @@ useSSL: true - uri: api.mainnet-beta.solana.com:443 + useSSL: true +- + uri: solana-rpc.publicnode.com:443 useSSL: true \ No newline at end of file diff --git a/assets/text/Monerocom_Release_Notes.txt b/assets/text/Monerocom_Release_Notes.txt index 11a3ad803..613ea4281 100644 --- a/assets/text/Monerocom_Release_Notes.txt +++ b/assets/text/Monerocom_Release_Notes.txt @@ -1,3 +1,3 @@ -Scan and verify messages -Synchronization enhancements +Monero enhancements +Introducing StealthEx and LetxExchange Bug fixes \ No newline at end of file diff --git a/assets/text/Release_Notes.txt b/assets/text/Release_Notes.txt index 11a3ad803..61aafb6e4 100644 --- a/assets/text/Release_Notes.txt +++ b/assets/text/Release_Notes.txt @@ -1,3 +1,7 @@ -Scan and verify messages -Synchronization enhancements +Added Litecoin MWEB +Added wallet groups +Silent Payment enhancements for speed & reliability +Monero enhancements +Introducing StealthEx and LetxExchange +Additional ERC20 tokens scam detection Bug fixes \ No newline at end of file diff --git a/build-guide-linux.md b/build-guide-linux.md index e0158945b..50ecc76fe 100644 --- a/build-guide-linux.md +++ b/build-guide-linux.md @@ -15,7 +15,7 @@ These steps will help you configure and execute a build of CakeWallet from its s ### 1. Installing Package Dependencies -CakeWallet requires some packages to be install on your build system. You may easily install them on your build system with the following command: +CakeWallet requires some packages to be installed on your build system. You may easily install them on your build system with the following command: `$ sudo apt install build-essential cmake pkg-config git curl autoconf libtool` @@ -145,7 +145,7 @@ Path to executable file will be: # Flatpak -For package the built application into flatpak you need fistly to install `flatpak` and `flatpak-builder`: +For package the built application into flatpak you need firstly to install `flatpak` and `flatpak-builder`: `$ sudo apt install flatpak flatpak-builder` diff --git a/cw_bitcoin/lib/address_to_output_script.dart b/cw_bitcoin/lib/address_to_output_script.dart deleted file mode 100644 index 892f7a0d6..000000000 --- a/cw_bitcoin/lib/address_to_output_script.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'dart:typed_data'; -import 'package:bitcoin_base/bitcoin_base.dart' as bitcoin; - -List addressToOutputScript(String address, bitcoin.BasedUtxoNetwork network) { - try { - if (network == bitcoin.BitcoinCashNetwork.mainnet) { - return bitcoin.BitcoinCashAddress(address).baseAddress.toScriptPubKey().toBytes(); - } - return bitcoin.addressToOutputScript(address: address, network: network); - } catch (err) { - print(err); - return Uint8List(0); - } -} diff --git a/cw_bitcoin/lib/bitcoin_address_record.dart b/cw_bitcoin/lib/bitcoin_address_record.dart index bf36e6fb9..7e4b5f58f 100644 --- a/cw_bitcoin/lib/bitcoin_address_record.dart +++ b/cw_bitcoin/lib/bitcoin_address_record.dart @@ -1,7 +1,6 @@ import 'dart:convert'; import 'package:bitcoin_base/bitcoin_base.dart'; -import 'package:cw_bitcoin/script_hash.dart' as sh; abstract class BaseBitcoinAddressRecord { BaseBitcoinAddressRecord( @@ -65,8 +64,8 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord { required super.type, String? scriptHash, required super.network, - }) : scriptHash = - scriptHash ?? (network != null ? sh.scriptHash(address, network: network) : null); + }) : scriptHash = scriptHash ?? + (network != null ? BitcoinAddressUtils.scriptHash(address, network: network) : null); factory BitcoinAddressRecord.fromJSON(String jsonSource, {BasedUtxoNetwork? network}) { final decoded = json.decode(jsonSource) as Map; @@ -92,7 +91,7 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord { String getScriptHash(BasedUtxoNetwork network) { if (scriptHash != null) return scriptHash!; - scriptHash = sh.scriptHash(address, network: network); + scriptHash = BitcoinAddressUtils.scriptHash(address, network: network); return scriptHash!; } diff --git a/cw_bitcoin/lib/bitcoin_hardware_wallet_service.dart b/cw_bitcoin/lib/bitcoin_hardware_wallet_service.dart index de339175d..a02c51c69 100644 --- a/cw_bitcoin/lib/bitcoin_hardware_wallet_service.dart +++ b/cw_bitcoin/lib/bitcoin_hardware_wallet_service.dart @@ -5,30 +5,31 @@ import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:cw_bitcoin/utils.dart'; import 'package:cw_core/hardware/hardware_account_data.dart'; import 'package:ledger_bitcoin/ledger_bitcoin.dart'; -import 'package:ledger_flutter/ledger_flutter.dart'; +import 'package:ledger_flutter_plus/ledger_flutter_plus.dart'; class BitcoinHardwareWalletService { - BitcoinHardwareWalletService(this.ledger, this.device); + BitcoinHardwareWalletService(this.ledgerConnection); - final Ledger ledger; - final LedgerDevice device; + final LedgerConnection ledgerConnection; - Future> getAvailableAccounts({int index = 0, int limit = 5}) async { - final bitcoinLedgerApp = BitcoinLedgerApp(ledger); + Future> getAvailableAccounts( + {int index = 0, int limit = 5}) async { + final bitcoinLedgerApp = BitcoinLedgerApp(ledgerConnection); - final masterFp = await bitcoinLedgerApp.getMasterFingerprint(device); - print(masterFp); + final masterFp = await bitcoinLedgerApp.getMasterFingerprint(); final accounts = []; final indexRange = List.generate(limit, (i) => i + index); for (final i in indexRange) { final derivationPath = "m/84'/0'/$i'"; - final xpub = await bitcoinLedgerApp.getXPubKey(device, derivationPath: derivationPath); + final xpub = + await bitcoinLedgerApp.getXPubKey(derivationPath: derivationPath); Bip32Slip10Secp256k1 hd = Bip32Slip10Secp256k1.fromExtendedKey(xpub).childKey(Bip32KeyIndex(0)); - final address = generateP2WPKHAddress(hd: hd, index: 0, network: BitcoinNetwork.mainnet); + final address = generateP2WPKHAddress( + hd: hd, index: 0, network: BitcoinNetwork.mainnet); accounts.add(HardwareAccountData( address: address, diff --git a/cw_bitcoin/lib/bitcoin_mnemonic.dart b/cw_bitcoin/lib/bitcoin_mnemonic.dart index 905aece28..21ff3891e 100644 --- a/cw_bitcoin/lib/bitcoin_mnemonic.dart +++ b/cw_bitcoin/lib/bitcoin_mnemonic.dart @@ -1,12 +1,14 @@ import 'dart:convert'; import 'dart:math'; import 'dart:typed_data'; + import 'package:crypto/crypto.dart'; import 'package:cryptography/cryptography.dart' as cryptography; import 'package:cw_core/sec_random_native.dart'; import 'package:cw_core/utils/text_normalizer.dart'; const segwit = '100'; +const mweb = 'eb'; final wordlist = englishWordlist; double logBase(num x, num base) => log(x) / log(base); @@ -59,11 +61,7 @@ void maskBytes(Uint8List bytes, int bits) { } } -String bufferToBin(Uint8List data) { - final q1 = data.map((e) => e.toRadixString(2).padLeft(8, '0')); - final q2 = q1.join(''); - return q2; -} +String bufferToBin(Uint8List data) => data.map((e) => e.toRadixString(2).padLeft(8, '0')).join(''); String encode(Uint8List data) { final dataBitLen = data.length * 8; @@ -112,22 +110,23 @@ Future checkIfMnemonicIsElectrum2(String mnemonic) async { Future getMnemonicHash(String mnemonic) async { final hmacSha512 = Hmac(sha512, utf8.encode('Seed version')); final digest = hmacSha512.convert(utf8.encode(normalizeText(mnemonic))); - final hx = digest.toString(); - return hx; + return digest.toString(); } -Future mnemonicToSeedBytes(String mnemonic, {String prefix = segwit}) async { +Future mnemonicToSeedBytes(String mnemonic, + {String prefix = segwit, String passphrase = ''}) async { final pbkdf2 = cryptography.Pbkdf2(macAlgorithm: cryptography.Hmac.sha512(), iterations: 2048, bits: 512); final text = normalizeText(mnemonic); - // pbkdf2.deriveKey(secretKey: secretKey, nonce: nonce) + final passphraseBytes = utf8.encode(normalizeText(passphrase)); final key = await pbkdf2.deriveKey( - secretKey: cryptography.SecretKey(text.codeUnits), nonce: 'electrum'.codeUnits); + secretKey: cryptography.SecretKey(text.codeUnits), + nonce: [...'electrum'.codeUnits, ...passphraseBytes]); final bytes = await key.extractBytes(); return Uint8List.fromList(bytes); } -bool matchesAnyPrefix(String mnemonic) => prefixMatches(mnemonic, [segwit]).any((el) => el); +bool matchesAnyPrefix(String mnemonic) => prefixMatches(mnemonic, [segwit, mweb]).any((el) => el); bool validateMnemonic(String mnemonic, {String prefix = segwit}) { try { diff --git a/cw_bitcoin_cash/lib/src/mnemonic.dart b/cw_bitcoin/lib/bitcoin_mnemonics_bip39.dart similarity index 65% rename from cw_bitcoin_cash/lib/src/mnemonic.dart rename to cw_bitcoin/lib/bitcoin_mnemonics_bip39.dart index 7aac1d5c4..ff02e875c 100644 --- a/cw_bitcoin_cash/lib/src/mnemonic.dart +++ b/cw_bitcoin/lib/bitcoin_mnemonics_bip39.dart @@ -7,5 +7,6 @@ class MnemonicBip39 { static String generate({int strength = 128}) => bip39.generateMnemonic(strength: strength); /// Create root seed from mnemonic - static Uint8List toSeed(String mnemonic) => bip39.mnemonicToSeed(mnemonic); + static Uint8List toSeed(String mnemonic, {String? passphrase}) => + bip39.mnemonicToSeed(mnemonic, passphrase: passphrase ?? ''); } diff --git a/cw_bitcoin/lib/bitcoin_receive_page_option.dart b/cw_bitcoin/lib/bitcoin_receive_page_option.dart index 851be5965..01c31b84a 100644 --- a/cw_bitcoin/lib/bitcoin_receive_page_option.dart +++ b/cw_bitcoin/lib/bitcoin_receive_page_option.dart @@ -7,6 +7,7 @@ class BitcoinReceivePageOption implements ReceivePageOption { static const p2tr = BitcoinReceivePageOption._('Taproot (P2TR)'); static const p2wsh = BitcoinReceivePageOption._('Segwit (P2WSH)'); static const p2pkh = BitcoinReceivePageOption._('Legacy (P2PKH)'); + static const mweb = BitcoinReceivePageOption._('MWEB'); static const silent_payments = BitcoinReceivePageOption._('Silent Payments'); @@ -27,6 +28,11 @@ class BitcoinReceivePageOption implements ReceivePageOption { BitcoinReceivePageOption.p2pkh ]; + static const allLitecoin = [ + BitcoinReceivePageOption.p2wpkh, + BitcoinReceivePageOption.mweb, + ]; + BitcoinAddressType toType() { switch (this) { case BitcoinReceivePageOption.p2tr: @@ -39,6 +45,8 @@ class BitcoinReceivePageOption implements ReceivePageOption { return P2shAddressType.p2wshInP2sh; case BitcoinReceivePageOption.silent_payments: return SilentPaymentsAddresType.p2sp; + case BitcoinReceivePageOption.mweb: + return SegwitAddresType.mweb; case BitcoinReceivePageOption.p2wpkh: default: return SegwitAddresType.p2wpkh; @@ -51,6 +59,8 @@ class BitcoinReceivePageOption implements ReceivePageOption { return BitcoinReceivePageOption.p2tr; case SegwitAddresType.p2wsh: return BitcoinReceivePageOption.p2wsh; + case SegwitAddresType.mweb: + return BitcoinReceivePageOption.mweb; case P2pkhAddressType.p2pkh: return BitcoinReceivePageOption.p2pkh; case P2shAddressType.p2wshInP2sh: diff --git a/cw_bitcoin/lib/bitcoin_transaction_credentials.dart b/cw_bitcoin/lib/bitcoin_transaction_credentials.dart index bda7c39ae..01e905fb0 100644 --- a/cw_bitcoin/lib/bitcoin_transaction_credentials.dart +++ b/cw_bitcoin/lib/bitcoin_transaction_credentials.dart @@ -1,11 +1,13 @@ import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; import 'package:cw_core/output_info.dart'; +import 'package:cw_core/unspent_coin_type.dart'; class BitcoinTransactionCredentials { BitcoinTransactionCredentials(this.outputs, - {required this.priority, this.feeRate}); + {required this.priority, this.feeRate, this.coinTypeToSpendFrom = UnspentCoinType.any}); final List outputs; final BitcoinTransactionPriority? priority; final int? feeRate; + final UnspentCoinType coinTypeToSpendFrom; } diff --git a/cw_bitcoin/lib/bitcoin_transaction_priority.dart b/cw_bitcoin/lib/bitcoin_transaction_priority.dart index 7c4dcfd5f..d1f45a545 100644 --- a/cw_bitcoin/lib/bitcoin_transaction_priority.dart +++ b/cw_bitcoin/lib/bitcoin_transaction_priority.dart @@ -87,7 +87,7 @@ class LitecoinTransactionPriority extends BitcoinTransactionPriority { } @override - String get units => 'Latoshi'; + String get units => 'Litoshi'; @override String toString() { diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index e2e537ee8..908897845 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -5,13 +5,13 @@ 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/bitcoin_mnemonic.dart'; +import 'package:cw_bitcoin/psbt_transaction_builder.dart'; import 'package:cw_core/encryption_file_utils.dart'; import 'package:cw_bitcoin/electrum_derivations.dart'; import 'package:cw_bitcoin/bitcoin_wallet_addresses.dart'; import 'package:cw_bitcoin/electrum_balance.dart'; import 'package:cw_bitcoin/electrum_wallet.dart'; import 'package:cw_bitcoin/electrum_wallet_snapshot.dart'; -import 'package:cw_bitcoin/psbt_transaction_builder.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/wallet_info.dart'; @@ -19,7 +19,7 @@ import 'package:cw_core/wallet_keys_file.dart'; import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; import 'package:ledger_bitcoin/ledger_bitcoin.dart'; -import 'package:ledger_flutter/ledger_flutter.dart'; +import 'package:ledger_flutter_plus/ledger_flutter_plus.dart'; import 'package:mobx/mobx.dart'; part 'bitcoin_wallet.g.dart'; @@ -61,8 +61,9 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { initialBalance: initialBalance, seedBytes: seedBytes, encryptionFileUtils: encryptionFileUtils, - currency: - networkParam == BitcoinNetwork.testnet ? CryptoCurrency.tbtc : CryptoCurrency.btc, + currency: networkParam == BitcoinNetwork.testnet + ? CryptoCurrency.tbtc + : CryptoCurrency.btc, alwaysScan: alwaysScan, ) { // in a standard BIP44 wallet, mainHd derivation path = m/84'/0'/0'/0 (account 0, index unspecified here) @@ -80,11 +81,14 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { mainHd: hd, sideHd: accountHD.childKey(Bip32KeyIndex(1)), network: networkParam ?? network, - masterHd: seedBytes != null ? Bip32Slip10Secp256k1.fromSeed(seedBytes) : null, + masterHd: + seedBytes != null ? Bip32Slip10Secp256k1.fromSeed(seedBytes) : null, + isHardwareWallet: walletInfo.isHardwareWallet, ); autorun((_) { - this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress; + this.walletAddresses.isEnabledAutoGenerateSubaddress = + this.isEnabledAutoGenerateSubaddress; }); } @@ -115,7 +119,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { break; case DerivationType.electrum: default: - seedBytes = await mnemonicToSeedBytes(mnemonic); + seedBytes = await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? ""); break; } return BitcoinWallet( @@ -185,8 +189,10 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { walletInfo.derivationInfo ??= DerivationInfo(); // set the default if not present: - walletInfo.derivationInfo!.derivationPath ??= snp?.derivationPath ?? electrum_path; - walletInfo.derivationInfo!.derivationType ??= snp?.derivationType ?? DerivationType.electrum; + walletInfo.derivationInfo!.derivationPath ??= + snp?.derivationPath ?? electrum_path; + walletInfo.derivationInfo!.derivationType ??= + snp?.derivationType ?? DerivationType.electrum; Uint8List? seedBytes = null; final mnemonic = keysData.mnemonic; @@ -195,7 +201,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { if (mnemonic != null) { switch (walletInfo.derivationInfo!.derivationType) { case DerivationType.electrum: - seedBytes = await mnemonicToSeedBytes(mnemonic); + seedBytes = await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? ""); break; case DerivationType.bip39: default: @@ -228,15 +234,14 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { ); } - Ledger? _ledger; - LedgerDevice? _ledgerDevice; + LedgerConnection? _ledgerConnection; BitcoinLedgerApp? _bitcoinLedgerApp; - void setLedger(Ledger setLedger, LedgerDevice setLedgerDevice) { - _ledger = setLedger; - _ledgerDevice = setLedgerDevice; - _bitcoinLedgerApp = - BitcoinLedgerApp(_ledger!, derivationPath: walletInfo.derivationInfo!.derivationPath!); + @override + void setLedgerConnection(LedgerConnection connection) { + _ledgerConnection = connection; + _bitcoinLedgerApp = BitcoinLedgerApp(_ledgerConnection!, + derivationPath: walletInfo.derivationInfo!.derivationPath!); } @override @@ -251,12 +256,14 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { BitcoinOrdering inputOrdering = BitcoinOrdering.bip69, BitcoinOrdering outputOrdering = BitcoinOrdering.bip69, }) async { - final masterFingerprint = await _bitcoinLedgerApp!.getMasterFingerprint(_ledgerDevice!); + final masterFingerprint = await _bitcoinLedgerApp!.getMasterFingerprint(); final psbtReadyInputs = []; for (final utxo in utxos) { - final rawTx = await electrumClient.getTransactionHex(hash: utxo.utxo.txHash); - final publicKeyAndDerivationPath = publicKeys[utxo.ownerDetails.address.pubKeyHash()]!; + final rawTx = + await electrumClient.getTransactionHex(hash: utxo.utxo.txHash); + final publicKeyAndDerivationPath = + publicKeys[utxo.ownerDetails.address.pubKeyHash()]!; psbtReadyInputs.add(PSBTReadyUtxoWithAddress( utxo: utxo.utxo, @@ -268,10 +275,10 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { )); } - final psbt = - PSBTTransactionBuild(inputs: psbtReadyInputs, outputs: outputs, enableRBF: enableRBF); + final psbt = PSBTTransactionBuild( + inputs: psbtReadyInputs, outputs: outputs, enableRBF: enableRBF); - final rawHex = await _bitcoinLedgerApp!.signPsbt(_ledgerDevice!, psbt: psbt.psbt); + final rawHex = await _bitcoinLedgerApp!.signPsbt(psbt: psbt.psbt); return BtcTransaction.fromRaw(BytesUtils.toHexString(rawHex)); } @@ -279,14 +286,16 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { Future signMessage(String message, {String? address = null}) async { if (walletInfo.isHardwareWallet) { final addressEntry = address != null - ? walletAddresses.allAddresses.firstWhere((element) => element.address == address) + ? walletAddresses.allAddresses + .firstWhere((element) => element.address == address) : null; final index = addressEntry?.index ?? 0; final isChange = addressEntry?.isHidden == true ? 1 : 0; final accountPath = walletInfo.derivationInfo?.derivationPath; - final derivationPath = accountPath != null ? "$accountPath/$isChange/$index" : null; + final derivationPath = + accountPath != null ? "$accountPath/$isChange/$index" : null; - final signature = await _bitcoinLedgerApp!.signMessage(_ledgerDevice!, + final signature = await _bitcoinLedgerApp!.signMessage( message: ascii.encode(message), signDerivationPath: derivationPath); return base64Encode(signature); } diff --git a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart index c177ab7c6..2749a9a02 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart @@ -15,6 +15,7 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S required super.mainHd, required super.sideHd, required super.network, + required super.isHardwareWallet, super.initialAddresses, super.initialRegularAddressIndex, super.initialChangeAddressIndex, diff --git a/cw_bitcoin/lib/bitcoin_wallet_creation_credentials.dart b/cw_bitcoin/lib/bitcoin_wallet_creation_credentials.dart index 91b8e4ae2..a1b1418b8 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_creation_credentials.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_creation_credentials.dart @@ -3,17 +3,24 @@ import 'package:cw_core/wallet_credentials.dart'; import 'package:cw_core/wallet_info.dart'; class BitcoinNewWalletCredentials extends WalletCredentials { - BitcoinNewWalletCredentials( - {required String name, - WalletInfo? walletInfo, - String? password, - DerivationType? derivationType, - String? derivationPath}) - : super( + BitcoinNewWalletCredentials({ + required String name, + WalletInfo? walletInfo, + String? password, + DerivationType? derivationType, + String? derivationPath, + String? passphrase, + this.mnemonic, + String? parentAddress, + }) : super( name: name, walletInfo: walletInfo, password: password, + passphrase: passphrase, + parentAddress: parentAddress, ); + + final String? mnemonic; } class BitcoinRestoreWalletFromSeedCredentials extends WalletCredentials { diff --git a/cw_bitcoin/lib/bitcoin_wallet_service.dart b/cw_bitcoin/lib/bitcoin_wallet_service.dart index d6d97f3de..06f2082e4 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_service.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_service.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cw_bitcoin/bitcoin_mnemonic.dart'; +import 'package:cw_bitcoin/bitcoin_mnemonics_bip39.dart'; import 'package:cw_bitcoin/mnemonic_is_incorrect_exception.dart'; import 'package:cw_bitcoin/bitcoin_wallet_creation_credentials.dart'; import 'package:cw_core/encryption_file_utils.dart'; @@ -35,8 +36,21 @@ class BitcoinWalletService extends WalletService< final network = isTestnet == true ? BitcoinNetwork.testnet : BitcoinNetwork.mainnet; credentials.walletInfo?.network = network.value; + final String mnemonic; + switch ( credentials.walletInfo?.derivationInfo?.derivationType) { + case DerivationType.bip39: + final strength = credentials.seedPhraseLength == 24 ? 256 : 128; + + mnemonic = credentials.mnemonic ?? await MnemonicBip39.generate(strength: strength); + break; + case DerivationType.electrum: + default: + mnemonic = await generateElectrumMnemonic(); + break; + } + final wallet = await BitcoinWalletBase.create( - mnemonic: await generateElectrumMnemonic(), + mnemonic: mnemonic, password: credentials.password!, passphrase: credentials.passphrase, walletInfo: credentials.walletInfo!, diff --git a/cw_bitcoin/lib/electrum.dart b/cw_bitcoin/lib/electrum.dart index 6ac8edd1a..a18c038fa 100644 --- a/cw_bitcoin/lib/electrum.dart +++ b/cw_bitcoin/lib/electrum.dart @@ -4,7 +4,6 @@ import 'dart:io'; import 'dart:typed_data'; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cw_bitcoin/bitcoin_amount_format.dart'; -import 'package:cw_bitcoin/script_hash.dart'; import 'package:flutter/foundation.dart'; import 'package:rxdart/rxdart.dart'; @@ -48,6 +47,7 @@ class ElectrumClient { final Map _tasks; Map get tasks => _tasks; final Map _errors; + ConnectionStatus _connectionStatus = ConnectionStatus.disconnected; bool _isConnected; Timer? _aliveTimer; String unterminatedString; @@ -57,17 +57,19 @@ class ElectrumClient { Future connectToUri(Uri uri, {bool? useSSL}) async { this.uri = uri; - this.useSSL = useSSL; - await connect(host: uri.host, port: uri.port, useSSL: useSSL); + if (useSSL != null) { + this.useSSL = useSSL; + } + await connect(host: uri.host, port: uri.port); } - Future connect({required String host, required int port, bool? useSSL}) async { + Future connect({required String host, required int port}) async { _setConnectionStatus(ConnectionStatus.connecting); try { await socket?.close(); - socket = null; } catch (_) {} + socket = null; try { if (useSSL == false || (useSSL == null && uri.toString().contains("btc-electrum"))) { @@ -80,16 +82,28 @@ class ElectrumClient { onBadCertificate: (_) => true, ); } - } catch (_) { - _setConnectionStatus(ConnectionStatus.failed); + } catch (e) { + if (e is HandshakeException) { + useSSL = !(useSSL ?? false); + } + + if (_connectionStatus != ConnectionStatus.connecting) { + _setConnectionStatus(ConnectionStatus.failed); + } + return; } if (socket == null) { - _setConnectionStatus(ConnectionStatus.failed); + if (_connectionStatus != ConnectionStatus.connecting) { + _setConnectionStatus(ConnectionStatus.failed); + } + return; } - _setConnectionStatus(ConnectionStatus.connected); + + // use ping to determine actual connection status since we could've just not timed out yet: + // _setConnectionStatus(ConnectionStatus.connected); socket!.listen( (Uint8List event) { @@ -103,26 +117,24 @@ class ElectrumClient { _parseResponse(message); } } catch (e) { - print(e.toString()); + print("socket.listen: $e"); } }, onError: (Object error) { - socket = null; final errorMsg = error.toString(); print(errorMsg); unterminatedString = ''; - - final currentHost = socket?.address.host; - final isErrorForCurrentHost = errorMsg.contains(" ${currentHost} "); - - if (currentHost != null && isErrorForCurrentHost) - _setConnectionStatus(ConnectionStatus.failed); }, onDone: () { + print("SOCKET CLOSED!!!!!"); unterminatedString = ''; - if (host == socket?.address.host) { - socket = null; - _setConnectionStatus(ConnectionStatus.disconnected); + try { + if (host == socket?.address.host || socket == null) { + _setConnectionStatus(ConnectionStatus.disconnected); + socket?.destroy(); + } + } catch (e) { + print("onDone: $e"); } }, cancelOnError: true, @@ -167,7 +179,7 @@ class ElectrumClient { unterminatedString = ''; } } catch (e) { - print(e.toString()); + print("parse $e"); } } @@ -180,7 +192,7 @@ class ElectrumClient { try { await callWithTimeout(method: 'server.ping'); _setConnectionStatus(ConnectionStatus.connected); - } on RequestFailedTimeoutException catch (_) { + } catch (_) { _setConnectionStatus(ConnectionStatus.disconnected); } } @@ -220,25 +232,6 @@ class ElectrumClient { return []; }); - Future>> getListUnspentWithAddress( - String address, BasedUtxoNetwork network) => - call( - method: 'blockchain.scripthash.listunspent', - params: [scriptHash(address, network: network)]).then((dynamic result) { - if (result is List) { - return result.map((dynamic val) { - if (val is Map) { - val['address'] = address; - return val; - } - - return {}; - }).toList(); - } - - return []; - }); - Future>> getListUnspent(String scriptHash) => call(method: 'blockchain.scripthash.listunspent', params: [scriptHash]) .then((dynamic result) { @@ -275,16 +268,12 @@ class ElectrumClient { try { final result = await callWithTimeout( method: 'blockchain.transaction.get', params: [hash, verbose], timeout: 10000); - if (result is Map) { - return result; - } + return result; } on RequestFailedTimeoutException catch (_) { return {}; } catch (e) { - print("getTransaction: ${e.toString()}"); return {}; } - return {}; } Future> getTransactionVerbose({required String hash}) => @@ -329,9 +318,8 @@ class ElectrumClient { await call(method: 'blockchain.block.get_header', params: [height]) as Map; BehaviorSubject? tweaksSubscribe({required int height, required int count}) { - _id += 1; return subscribe( - id: 'blockchain.tweaks.subscribe:${height + count}', + id: 'blockchain.tweaks.subscribe', method: 'blockchain.tweaks.subscribe', params: [height, count, false], ); @@ -436,7 +424,6 @@ class ElectrumClient { {required String id, required String method, List params = const []}) { try { if (socket == null) { - _setConnectionStatus(ConnectionStatus.failed); return null; } final subscription = BehaviorSubject(); @@ -445,7 +432,7 @@ class ElectrumClient { return subscription; } catch (e) { - print(e.toString()); + print("subscribe $e"); return null; } } @@ -453,7 +440,6 @@ class ElectrumClient { Future call( {required String method, List params = const [], Function(int)? idCallback}) async { if (socket == null) { - _setConnectionStatus(ConnectionStatus.failed); return null; } final completer = Completer(); @@ -467,10 +453,9 @@ class ElectrumClient { } Future callWithTimeout( - {required String method, List params = const [], int timeout = 4000}) async { + {required String method, List params = const [], int timeout = 5000}) async { try { if (socket == null) { - _setConnectionStatus(ConnectionStatus.failed); return null; } final completer = Completer(); @@ -486,7 +471,8 @@ class ElectrumClient { return completer.future; } catch (e) { - print(e.toString()); + print("callWithTimeout $e"); + rethrow; } } @@ -523,6 +509,12 @@ class ElectrumClient { void _methodHandler({required String method, required Map request}) { switch (method) { + case 'blockchain.headers.subscribe': + final params = request['params'] as List; + final id = 'blockchain.headers.subscribe'; + + _tasks[id]?.subject?.add(params.last); + break; case 'blockchain.scripthash.subscribe': final params = request['params'] as List; final scripthash = params.first as String?; @@ -545,7 +537,14 @@ class ElectrumClient { void _setConnectionStatus(ConnectionStatus status) { onConnectionStatusChange?.call(status); + _connectionStatus = status; _isConnected = status == ConnectionStatus.connected; + if (!_isConnected) { + try { + socket?.destroy(); + } catch (_) {} + socket = null; + } } void _handleResponse(Map response) { diff --git a/cw_bitcoin/lib/electrum_balance.dart b/cw_bitcoin/lib/electrum_balance.dart index 15d6843d8..4e37f40b1 100644 --- a/cw_bitcoin/lib/electrum_balance.dart +++ b/cw_bitcoin/lib/electrum_balance.dart @@ -7,7 +7,14 @@ class ElectrumBalance extends Balance { required this.confirmed, required this.unconfirmed, required this.frozen, - }) : super(confirmed, unconfirmed); + this.secondConfirmed = 0, + this.secondUnconfirmed = 0, + }) : super( + confirmed, + unconfirmed, + secondAvailable: secondConfirmed, + secondAdditional: secondUnconfirmed, + ); static ElectrumBalance? fromJSON(String? jsonSource) { if (jsonSource == null) { @@ -25,9 +32,12 @@ class ElectrumBalance extends Balance { int confirmed; int unconfirmed; final int frozen; + int secondConfirmed = 0; + int secondUnconfirmed = 0; @override - String get formattedAvailableBalance => bitcoinAmountToString(amount: confirmed - frozen); + String get formattedAvailableBalance => + bitcoinAmountToString(amount: confirmed - frozen); @override String get formattedAdditionalBalance => bitcoinAmountToString(amount: unconfirmed); @@ -38,6 +48,21 @@ class ElectrumBalance extends Balance { return frozenFormatted == '0.0' ? '' : frozenFormatted; } - String toJSON() => - json.encode({'confirmed': confirmed, 'unconfirmed': unconfirmed, 'frozen': frozen}); + @override + String get formattedSecondAvailableBalance => bitcoinAmountToString(amount: secondConfirmed); + + @override + String get formattedSecondAdditionalBalance => bitcoinAmountToString(amount: secondUnconfirmed); + + @override + String get formattedFullAvailableBalance => + bitcoinAmountToString(amount: confirmed + secondConfirmed - frozen); + + String toJSON() => json.encode({ + 'confirmed': confirmed, + 'unconfirmed': unconfirmed, + 'frozen': frozen, + 'secondConfirmed': secondConfirmed, + 'secondUnconfirmed': secondUnconfirmed + }); } diff --git a/cw_bitcoin/lib/electrum_derivations.dart b/cw_bitcoin/lib/electrum_derivations.dart index 749e5c7af..81a3626d2 100644 --- a/cw_bitcoin/lib/electrum_derivations.dart +++ b/cw_bitcoin/lib/electrum_derivations.dart @@ -109,5 +109,4 @@ Map> electrum_derivations = { ], }; - -String electrum_path = electrum_derivations[DerivationType.electrum]!.first.derivationPath!; \ No newline at end of file +String electrum_path = electrum_derivations[DerivationType.electrum]!.first.derivationPath!; diff --git a/cw_bitcoin/lib/electrum_transaction_history.dart b/cw_bitcoin/lib/electrum_transaction_history.dart index 806f813dd..b688f097b 100644 --- a/cw_bitcoin/lib/electrum_transaction_history.dart +++ b/cw_bitcoin/lib/electrum_transaction_history.dart @@ -76,8 +76,13 @@ abstract class ElectrumTransactionHistoryBase final val = entry.value; if (val is Map) { - final tx = ElectrumTransactionInfo.fromJson(val, walletInfo.type); - _update(tx); + // removing transactions with invalid date + if (val['date'] == 1168650000) { + transactions.remove(entry.key); + } else { + final tx = ElectrumTransactionInfo.fromJson(val, walletInfo.type); + _update(tx); + } } }); diff --git a/cw_bitcoin/lib/electrum_transaction_info.dart b/cw_bitcoin/lib/electrum_transaction_info.dart index ea4a3de33..1ab7799e3 100644 --- a/cw_bitcoin/lib/electrum_transaction_info.dart +++ b/cw_bitcoin/lib/electrum_transaction_info.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cw_bitcoin/address_from_output.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; @@ -7,10 +9,12 @@ import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/transaction_info.dart'; import 'package:cw_core/format_amount.dart'; import 'package:cw_core/wallet_type.dart'; +import 'package:hex/hex.dart'; class ElectrumTransactionBundle { ElectrumTransactionBundle(this.originalTransaction, {required this.ins, required this.confirmations, this.time}); + final BtcTransaction originalTransaction; final List ins; final int? time; @@ -19,20 +23,25 @@ class ElectrumTransactionBundle { class ElectrumTransactionInfo extends TransactionInfo { List? unspents; + bool isReceivedSilentPayment; - ElectrumTransactionInfo(this.type, - {required String id, - int? height, - required int amount, - int? fee, - List? inputAddresses, - List? outputAddresses, - required TransactionDirection direction, - required bool isPending, - required DateTime date, - required int confirmations, - String? to, - this.unspents}) { + ElectrumTransactionInfo( + this.type, { + required String id, + int? height, + required int amount, + int? fee, + List? inputAddresses, + List? outputAddresses, + required TransactionDirection direction, + required bool isPending, + bool isReplaced = false, + required DateTime date, + required int confirmations, + String? to, + this.unspents, + this.isReceivedSilentPayment = false, + }) { this.id = id; this.height = height; this.amount = amount; @@ -42,6 +51,7 @@ class ElectrumTransactionInfo extends TransactionInfo { this.direction = direction; this.date = date; this.isPending = isPending; + this.isReplaced = isReplaced; this.confirmations = confirmations; this.to = to; } @@ -90,6 +100,7 @@ class ElectrumTransactionInfo extends TransactionInfo { id: id, height: height, isPending: false, + isReplaced: false, fee: fee, direction: direction, amount: amount, @@ -125,7 +136,24 @@ class ElectrumTransactionInfo extends TransactionInfo { for (final out in bundle.originalTransaction.outputs) { totalOutAmount += out.amount.toInt(); final addressExists = addresses.contains(addressFromOutputScript(out.scriptPubKey, network)); - outputAddresses.add(addressFromOutputScript(out.scriptPubKey, network)); + final address = addressFromOutputScript(out.scriptPubKey, network); + + if (address.isNotEmpty) outputAddresses.add(address); + + // Check if the script contains OP_RETURN + final script = out.scriptPubKey.script; + if (script.contains('OP_RETURN')) { + final index = script.indexOf('OP_RETURN'); + if (index + 1 <= script.length) { + try { + final opReturnData = script[index + 1].toString(); + final decodedString = utf8.decode(HEX.decode(opReturnData)); + outputAddresses.add('OP_RETURN:$decodedString'); + } catch (_) { + outputAddresses.add('OP_RETURN:'); + } + } + } if (addressExists) { receivedAmounts.add(out.amount.toInt()); @@ -148,6 +176,7 @@ class ElectrumTransactionInfo extends TransactionInfo { id: bundle.originalTransaction.txId(), height: height, isPending: bundle.confirmations == 0, + isReplaced: false, inputAddresses: inputAddresses, outputAddresses: outputAddresses, fee: fee, @@ -171,6 +200,7 @@ class ElectrumTransactionInfo extends TransactionInfo { direction: parseTransactionDirectionFromInt(data['direction'] as int), date: DateTime.fromMillisecondsSinceEpoch(data['date'] as int), isPending: data['isPending'] as bool, + isReplaced: data['isReplaced'] as bool? ?? false, confirmations: data['confirmations'] as int, inputAddresses: inputAddresses.isEmpty ? [] : inputAddresses.map((e) => e.toString()).toList(), @@ -181,6 +211,7 @@ class ElectrumTransactionInfo extends TransactionInfo { .map((unspent) => BitcoinSilentPaymentsUnspent.fromJSON(null, unspent as Map)) .toList(), + isReceivedSilentPayment: data['isReceivedSilentPayment'] as bool? ?? false, ); } @@ -212,6 +243,7 @@ class ElectrumTransactionInfo extends TransactionInfo { direction: direction, date: date, isPending: isPending, + isReplaced: isReplaced ?? false, inputAddresses: inputAddresses, outputAddresses: outputAddresses, confirmations: info.confirmations); @@ -225,16 +257,18 @@ class ElectrumTransactionInfo extends TransactionInfo { m['direction'] = direction.index; m['date'] = date.millisecondsSinceEpoch; m['isPending'] = isPending; + m['isReplaced'] = isReplaced; m['confirmations'] = confirmations; m['fee'] = fee; m['to'] = to; m['unspents'] = unspents?.map((e) => e.toJson()).toList() ?? []; m['inputAddresses'] = inputAddresses; m['outputAddresses'] = outputAddresses; + m['isReceivedSilentPayment'] = isReceivedSilentPayment; return m; } String toString() { - return 'ElectrumTransactionInfo(id: $id, height: $height, amount: $amount, fee: $fee, direction: $direction, date: $date, isPending: $isPending, confirmations: $confirmations, to: $to, unspent: $unspents)'; + return 'ElectrumTransactionInfo(id: $id, height: $height, amount: $amount, fee: $fee, direction: $direction, date: $date, isPending: $isPending, isReplaced: $isReplaced, confirmations: $confirmations, to: $to, unspent: $unspents, inputAddresses: $inputAddresses, outputAddresses: $outputAddresses)'; } } diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 28e21953a..08b94d7dd 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -2,10 +2,10 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:isolate'; -import 'dart:math'; import 'package:bitcoin_base/bitcoin_base.dart'; -import 'package:cw_core/encryption_file_utils.dart'; +import 'package:cw_bitcoin/bitcoin_wallet.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:collection/collection.dart'; import 'package:cw_bitcoin/address_from_output.dart'; @@ -23,9 +23,10 @@ import 'package:cw_bitcoin/electrum_transaction_info.dart'; import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; import 'package:cw_bitcoin/exceptions.dart'; import 'package:cw_bitcoin/pending_bitcoin_transaction.dart'; -import 'package:cw_bitcoin/script_hash.dart'; import 'package:cw_bitcoin/utils.dart'; import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/encryption_file_utils.dart'; +import 'package:cw_core/get_height_by_date.dart'; import 'package:cw_core/node.dart'; import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/pending_transaction.dart'; @@ -37,22 +38,24 @@ import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_keys_file.dart'; import 'package:cw_core/wallet_type.dart'; -import 'package:cw_core/get_height_by_date.dart'; +import 'package:cw_core/unspent_coin_type.dart'; import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; +import 'package:ledger_flutter_plus/ledger_flutter_plus.dart' as ledger; import 'package:mobx/mobx.dart'; import 'package:rxdart/subjects.dart'; import 'package:sp_scanner/sp_scanner.dart'; +import 'package:hex/hex.dart'; +import 'package:http/http.dart' as http; part 'electrum_wallet.g.dart'; class ElectrumWallet = ElectrumWalletBase with _$ElectrumWallet; -const int TWEAKS_COUNT = 25; - -abstract class ElectrumWalletBase - extends WalletBase - with Store, WalletKeysFile { +abstract class ElectrumWalletBase extends WalletBase< + ElectrumBalance, + ElectrumTransactionHistory, + ElectrumTransactionInfo> with Store, WalletKeysFile { ElectrumWalletBase({ required String password, required WalletInfo walletInfo, @@ -68,8 +71,8 @@ abstract class ElectrumWalletBase ElectrumBalance? initialBalance, CryptoCurrency? currency, this.alwaysScan, - }) : accountHD = - getAccountHDWallet(currency, network, seedBytes, xpub, walletInfo.derivationInfo), + }) : accountHD = getAccountHDWallet( + currency, network, seedBytes, xpub, walletInfo.derivationInfo), syncStatus = NotConnectedSyncStatus(), _password = password, _feeRates = [], @@ -100,41 +103,69 @@ abstract class ElectrumWalletBase ); reaction((_) => syncStatus, _syncStatusReaction); + + sharedPrefs.complete(SharedPreferences.getInstance()); } - static Bip32Slip10Secp256k1 getAccountHDWallet(CryptoCurrency? currency, BasedUtxoNetwork network, - Uint8List? seedBytes, String? xpub, DerivationInfo? derivationInfo) { + static Bip32Slip10Secp256k1 getAccountHDWallet( + CryptoCurrency? currency, + BasedUtxoNetwork network, + Uint8List? seedBytes, + String? xpub, + DerivationInfo? derivationInfo) { if (seedBytes == null && xpub == null) { throw Exception( "To create a Wallet you need either a seed or an xpub. This should not happen"); } if (seedBytes != null) { - return currency == CryptoCurrency.bch - ? bitcoinCashHDWallet(seedBytes) - : Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath( - _hardenedDerivationPath(derivationInfo?.derivationPath ?? electrum_path)) + switch (currency) { + case CryptoCurrency.btc: + case CryptoCurrency.ltc: + case CryptoCurrency.tbtc: + return Bip32Slip10Secp256k1.fromSeed(seedBytes, getKeyNetVersion(network)) + .derivePath(_hardenedDerivationPath( + derivationInfo?.derivationPath ?? electrum_path)) as Bip32Slip10Secp256k1; + case CryptoCurrency.bch: + return bitcoinCashHDWallet(seedBytes); + default: + throw Exception("Unsupported currency"); + } } - return Bip32Slip10Secp256k1.fromExtendedKey(xpub!); + return Bip32Slip10Secp256k1.fromExtendedKey( + xpub!, getKeyNetVersion(network)); } static Bip32Slip10Secp256k1 bitcoinCashHDWallet(Uint8List seedBytes) => - Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath("m/44'/145'/0'") as Bip32Slip10Secp256k1; + Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath("m/44'/145'/0'") + as Bip32Slip10Secp256k1; static int estimatedTransactionSize(int inputsCount, int outputsCounts) => inputsCount * 68 + outputsCounts * 34 + 10; + static Bip32KeyNetVersions? getKeyNetVersion(BasedUtxoNetwork network) { + switch (network) { + case LitecoinNetwork.mainnet: + return Bip44Conf.litecoinMainNet.altKeyNetVer; + default: + return null; + } + } + bool? alwaysScan; final Bip32Slip10Secp256k1 accountHD; final String? _mnemonic; Bip32Slip10Secp256k1 get hd => accountHD.childKey(Bip32KeyIndex(0)); + Bip32Slip10Secp256k1 get sideHd => accountHD.childKey(Bip32KeyIndex(1)); final EncryptionFileUtils encryptionFileUtils; + + @override final String? passphrase; @override @@ -155,15 +186,20 @@ abstract class ElectrumWalletBase @observable SyncStatus syncStatus; - Set get addressesSet => walletAddresses.allAddresses.map((addr) => addr.address).toSet(); + Set get addressesSet => walletAddresses.allAddresses + .where((element) => element.type != SegwitAddresType.mweb) + .map((addr) => addr.address) + .toSet(); List get scriptHashes => walletAddresses.addressesByReceiveType - .map((addr) => scriptHash(addr.address, network: network)) + .where((addr) => RegexUtils.addressTypeFromStr(addr.address, network) is! MwebAddress) + .map((addr) => (addr as BitcoinAddressRecord).getScriptHash(network)) .toList(); List get publicScriptHashes => walletAddresses.allAddresses .where((addr) => !addr.isHidden) - .map((addr) => scriptHash(addr.address, network: network)) + .where((addr) => RegexUtils.addressTypeFromStr(addr.address, network) is! MwebAddress) + .map((addr) => addr.getScriptHash(network)) .toList(); String get xpub => accountHD.publicKey.toExtended; @@ -192,12 +228,19 @@ abstract class ElectrumWalletBase bool _isTryingToConnect = false; + Completer sharedPrefs = Completer(); + + Future checkIfMempoolAPIIsEnabled() async { + bool isMempoolAPIEnabled = (await sharedPrefs.future).getBool("use_mempool_fee_api") ?? true; + return isMempoolAPIEnabled; + } + @action Future setSilentPaymentsScanning(bool active) async { silentPaymentsScanningActive = active; if (active) { - syncStatus = StartingScanSyncStatus(); + syncStatus = AttemptingScanSyncStatus(); final tip = await getUpdatedChainTip(); @@ -225,7 +268,7 @@ abstract class ElectrumWalletBase int? _currentChainTip; Future getCurrentChainTip() async { - if (_currentChainTip != null) { + if ((_currentChainTip ?? 0) > 0) { return _currentChainTip!; } _currentChainTip = await electrumClient.getCurrentBlockChainTip() ?? 0; @@ -261,8 +304,8 @@ abstract class ElectrumWalletBase Future? _isolate; void Function(FlutterErrorDetails)? _onError; - Timer? _reconnectTimer; Timer? _autoSaveTimer; + StreamSubscription? _receiveStream; Timer? _updateFeeRateTimer; static const int _autoSaveInterval = 1; @@ -276,12 +319,8 @@ abstract class ElectrumWalletBase } @action - Future _setListeners( - int height, { - int? chainTipParam, - bool? doSingleScan, - bool? usingSupportedNode, - }) async { + Future _setListeners(int height, {int? chainTipParam, bool? doSingleScan}) async { + if (this is! BitcoinWallet) return; final chainTip = chainTipParam ?? await getUpdatedChainTip(); if (chainTip == height) { @@ -289,7 +328,7 @@ abstract class ElectrumWalletBase return; } - syncStatus = StartingScanSyncStatus(); + syncStatus = AttemptingScanSyncStatus(); if (_isolate != null) { final runningIsolate = await _isolate!; @@ -318,7 +357,8 @@ abstract class ElectrumWalletBase isSingleScan: doSingleScan ?? false, )); - await for (var message in receivePort) { + _receiveStream?.cancel(); + _receiveStream = receivePort.listen((var message) async { if (message is Map) { for (final map in message.entries) { final txid = map.key; @@ -381,10 +421,16 @@ abstract class ElectrumWalletBase nodeSupportsSilentPayments = false; } - syncStatus = message.syncStatus; + if (message.syncStatus is SyncingSyncStatus) { + var status = message.syncStatus as SyncingSyncStatus; + syncStatus = SyncingSyncStatus(status.blocksLeft, status.ptc); + } else { + syncStatus = message.syncStatus; + } + await walletInfo.updateRestoreHeight(message.height); } - } + }); } void _updateSilentAddressRecord(BitcoinSilentPaymentsUnspent unspent) { @@ -414,15 +460,19 @@ abstract class ElectrumWalletBase @override Future startSync() async { try { + if (syncStatus is SyncronizingSyncStatus) { + return; + } + syncStatus = SyncronizingSyncStatus(); if (hasSilentPaymentsScanning) { await _setInitialHeight(); } - await _subscribeForUpdates(); - + await subscribeForUpdates(); await updateTransactions(); + await updateAllUnspents(); await updateBalance(); await updateFeeRates(); @@ -437,13 +487,35 @@ abstract class ElectrumWalletBase } } catch (e, stacktrace) { print(stacktrace); - print(e.toString()); + print("startSync $e"); syncStatus = FailedSyncStatus(); } } @action Future updateFeeRates() async { + if (await checkIfMempoolAPIIsEnabled()) { + try { + final response = + await http.get(Uri.parse("http://mempool.cakewallet.com:8999/api/v1/fees/recommended")); + + final result = json.decode(response.body) as Map; + final slowFee = (result['economyFee'] as num?)?.toInt() ?? 0; + int mediumFee = (result['hourFee'] as num?)?.toInt() ?? 0; + int fastFee = (result['fastestFee'] as num?)?.toInt() ?? 0; + if (slowFee == mediumFee) { + mediumFee++; + } + while (fastFee <= mediumFee) { + fastFee++; + } + _feeRates = [slowFee, mediumFee, fastFee]; + return; + } catch (e) { + print(e); + } + } + final feeRates = await electrumClient.feeRates(network: network); if (feeRates != [0, 0, 0]) { _feeRates = feeRates; @@ -513,13 +585,15 @@ abstract class ElectrumWalletBase try { syncStatus = ConnectingSyncStatus(); + await _receiveStream?.cancel(); await electrumClient.close(); electrumClient.onConnectionStatusChange = _onConnectionStatusChange; await electrumClient.connectToUri(node.uri, useSSL: node.useSSL); - } catch (e) { - print(e.toString()); + } catch (e, stacktrace) { + print(stacktrace); + print("connectToNode $e"); syncStatus = FailedSyncStatus(); } } @@ -533,6 +607,7 @@ abstract class ElectrumWalletBase required int credentialsAmount, required bool paysToSilentPayment, int? inputsCount, + UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any, }) { List utxos = []; List vinOutpoints = []; @@ -543,7 +618,20 @@ abstract class ElectrumWalletBase bool spendsUnconfirmedTX = false; int leftAmount = credentialsAmount; - final availableInputs = unspentCoins.where((utx) => utx.isSending && !utx.isFrozen).toList(); + final availableInputs = unspentCoins.where((utx) { + if (!utx.isSending || utx.isFrozen) { + return false; + } + + switch (coinTypeToSpendFrom) { + case UnspentCoinType.mweb: + return utx.bitcoinAddressRecord.type == SegwitAddresType.mweb; + case UnspentCoinType.nonMweb: + return utx.bitcoinAddressRecord.type != SegwitAddresType.mweb; + case UnspentCoinType.any: + return true; + } + }).toList(); final unconfirmedCoins = availableInputs.where((utx) => utx.confirmations == 0).toList(); for (int i = 0; i < availableInputs.length; i++) { @@ -560,12 +648,13 @@ abstract class ElectrumWalletBase allInputsAmount += utx.value; leftAmount = leftAmount - utx.value; - final address = addressTypeFromStr(utx.address, network); + final address = RegexUtils.addressTypeFromStr(utx.address, network); ECPrivate? privkey; bool? isSilentPayment = false; - final hd = - utx.bitcoinAddressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd; + final hd = utx.bitcoinAddressRecord.isHidden + ? walletAddresses.sideHd + : walletAddresses.mainHd; if (utx.bitcoinAddressRecord is BitcoinSilentPaymentAddressRecord) { final unspentAddress = utx.bitcoinAddressRecord as BitcoinSilentPaymentAddressRecord; @@ -650,33 +739,24 @@ abstract class ElectrumWalletBase String? memo, int credentialsAmount = 0, bool hasSilentPayment = false, + UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any, }) async { final utxoDetails = _createUTXOS( sendAll: true, credentialsAmount: credentialsAmount, paysToSilentPayment: hasSilentPayment, + coinTypeToSpendFrom: coinTypeToSpendFrom, ); - int estimatedSize; - if (network is BitcoinCashNetwork) { - estimatedSize = ForkedTransactionBuilder.estimateTransactionSize( - utxos: utxoDetails.utxos, - outputs: outputs, - network: network as BitcoinCashNetwork, - memo: memo, - ); - } else { - estimatedSize = BitcoinTransactionBuilder.estimateTransactionSize( - utxos: utxoDetails.utxos, - outputs: outputs, - network: network, - memo: memo, - inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos, - vinOutpoints: utxoDetails.vinOutpoints, - ); - } - - int fee = feeAmountWithFeeRate(feeRate, 0, 0, size: estimatedSize); + int fee = await calcFee( + utxos: utxoDetails.utxos, + outputs: outputs, + network: network, + memo: memo, + feeRate: feeRate, + inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos, + vinOutpoints: utxoDetails.vinOutpoints, + ); if (fee == 0) { throw BitcoinTransactionNoFeeException(); @@ -727,17 +807,20 @@ abstract class ElectrumWalletBase Future estimateTxForAmount( int credentialsAmount, List outputs, + List updatedOutputs, int feeRate, { int? inputsCount, String? memo, bool? useUnconfirmed, bool hasSilentPayment = false, + UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any, }) async { final utxoDetails = _createUTXOS( sendAll: false, credentialsAmount: credentialsAmount, inputsCount: inputsCount, paysToSilentPayment: hasSilentPayment, + coinTypeToSpendFrom: coinTypeToSpendFrom, ); final spendingAllCoins = utxoDetails.availableInputs.length == utxoDetails.utxos.length; @@ -753,58 +836,86 @@ abstract class ElectrumWalletBase return estimateTxForAmount( credentialsAmount, outputs, + updatedOutputs, feeRate, inputsCount: utxoDetails.utxos.length + 1, memo: memo, hasSilentPayment: hasSilentPayment, + coinTypeToSpendFrom: coinTypeToSpendFrom, ); } throw BitcoinTransactionWrongBalanceException(); } - final changeAddress = await walletAddresses.getChangeAddress(); - final address = addressTypeFromStr(changeAddress, network); + final changeAddress = await walletAddresses.getChangeAddress( + inputs: utxoDetails.availableInputs, + outputs: updatedOutputs, + ); + final address = RegexUtils.addressTypeFromStr(changeAddress.address, network); + updatedOutputs.add(BitcoinOutput( + address: address, + value: BigInt.from(amountLeftForChangeAndFee), + isChange: true, + )); outputs.add(BitcoinOutput( address: address, value: BigInt.from(amountLeftForChangeAndFee), + isChange: true, )); - int estimatedSize; - if (network is BitcoinCashNetwork) { - estimatedSize = ForkedTransactionBuilder.estimateTransactionSize( - utxos: utxoDetails.utxos, - outputs: outputs, - network: network as BitcoinCashNetwork, - memo: memo, - ); - } else { - estimatedSize = BitcoinTransactionBuilder.estimateTransactionSize( - utxos: utxoDetails.utxos, - outputs: outputs, - network: network, - memo: memo, - inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos, - vinOutpoints: utxoDetails.vinOutpoints, - ); - } + // Get Derivation path for change Address since it is needed in Litecoin and BitcoinCash hardware Wallets + final changeDerivationPath = + "${_hardenedDerivationPath(walletInfo.derivationInfo?.derivationPath ?? "m/0'")}" + "/${changeAddress.isHidden ? "1" : "0"}" + "/${changeAddress.index}"; + utxoDetails.publicKeys[address.pubKeyHash()] = + PublicKeyWithDerivationPath('', changeDerivationPath); - int fee = feeAmountWithFeeRate(feeRate, 0, 0, size: estimatedSize); + // calcFee updates the silent payment outputs to calculate the tx size accounting + // for taproot addresses, but if more inputs are needed to make up for fees, + // the silent payment outputs need to be recalculated for the new inputs + var temp = outputs.map((output) => output).toList(); + int fee = await calcFee( + utxos: utxoDetails.utxos, + // Always take only not updated bitcoin outputs here so for every estimation + // the SP outputs are re-generated to the proper taproot addresses + outputs: temp, + network: network, + memo: memo, + feeRate: feeRate, + inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos, + vinOutpoints: utxoDetails.vinOutpoints, + ); + + updatedOutputs.clear(); + updatedOutputs.addAll(temp); if (fee == 0) { throw BitcoinTransactionNoFeeException(); } int amount = credentialsAmount; - final lastOutput = outputs.last; + final lastOutput = updatedOutputs.last; final amountLeftForChange = amountLeftForChangeAndFee - fee; if (!_isBelowDust(amountLeftForChange)) { // Here, lastOutput already is change, return the amount left without the fee to the user's address. - outputs[outputs.length - 1] = - BitcoinOutput(address: lastOutput.address, value: BigInt.from(amountLeftForChange)); + updatedOutputs[updatedOutputs.length - 1] = BitcoinOutput( + address: lastOutput.address, + value: BigInt.from(amountLeftForChange), + isSilentPayment: lastOutput.isSilentPayment, + isChange: true, + ); + outputs[outputs.length - 1] = BitcoinOutput( + address: lastOutput.address, + value: BigInt.from(amountLeftForChange), + isSilentPayment: lastOutput.isSilentPayment, + isChange: true, + ); } else { // If has change that is lower than dust, will end up with tx rejected by network rules, so estimate again without the added change + updatedOutputs.removeLast(); outputs.removeLast(); // Still has inputs to spend before failing @@ -812,17 +923,21 @@ abstract class ElectrumWalletBase return estimateTxForAmount( credentialsAmount, outputs, + updatedOutputs, feeRate, inputsCount: utxoDetails.utxos.length + 1, memo: memo, + hasSilentPayment: hasSilentPayment, useUnconfirmed: useUnconfirmed ?? spendingAllConfirmedCoins, + coinTypeToSpendFrom: coinTypeToSpendFrom, ); } final estimatedSendAll = await estimateSendAllTx( - outputs, + updatedOutputs, feeRate, memo: memo, + coinTypeToSpendFrom: coinTypeToSpendFrom, ); if (estimatedSendAll.amount == credentialsAmount) { @@ -844,7 +959,7 @@ abstract class ElectrumWalletBase final totalAmount = amount + fee; - if (totalAmount > balance[currency]!.confirmed) { + if (totalAmount > (balance[currency]!.confirmed + balance[currency]!.secondConfirmed)) { throw BitcoinTransactionWrongBalanceException(); } @@ -852,15 +967,18 @@ abstract class ElectrumWalletBase if (spendingAllCoins) { throw BitcoinTransactionWrongBalanceException(); } else { + updatedOutputs.removeLast(); outputs.removeLast(); return estimateTxForAmount( credentialsAmount, outputs, + updatedOutputs, feeRate, inputsCount: utxoDetails.utxos.length + 1, memo: memo, useUnconfirmed: useUnconfirmed ?? spendingAllConfirmedCoins, hasSilentPayment: hasSilentPayment, + coinTypeToSpendFrom: coinTypeToSpendFrom, ); } } @@ -879,6 +997,37 @@ abstract class ElectrumWalletBase ); } + Future calcFee({ + required List utxos, + required List outputs, + required BasedUtxoNetwork network, + String? memo, + required int feeRate, + List? inputPrivKeyInfos, + List? vinOutpoints, + }) async { + int estimatedSize; + if (network is BitcoinCashNetwork) { + estimatedSize = ForkedTransactionBuilder.estimateTransactionSize( + utxos: utxos, + outputs: outputs, + network: network, + memo: memo, + ); + } else { + estimatedSize = BitcoinTransactionBuilder.estimateTransactionSize( + utxos: utxos, + outputs: outputs, + network: network, + memo: memo, + inputPrivKeyInfos: inputPrivKeyInfos, + vinOutpoints: vinOutpoints, + ); + } + + return feeAmountWithFeeRate(feeRate, 0, 0, size: estimatedSize); + } + @override Future createTransaction(Object credentials) async { try { @@ -887,6 +1036,7 @@ abstract class ElectrumWalletBase final hasMultiDestination = transactionCredentials.outputs.length > 1; final sendAll = !hasMultiDestination && transactionCredentials.outputs.first.sendAll; final memo = transactionCredentials.outputs.first.memo; + final coinTypeToSpendFrom = transactionCredentials.coinTypeToSpendFrom; int credentialsAmount = 0; bool hasSilentPayment = false; @@ -906,18 +1056,27 @@ abstract class ElectrumWalletBase credentialsAmount += outputAmount; - final address = - addressTypeFromStr(out.isParsedAddress ? out.extractedAddress! : out.address, network); + final address = RegexUtils.addressTypeFromStr( + out.isParsedAddress ? out.extractedAddress! : out.address, network); + final isSilentPayment = address is SilentPaymentAddress; - if (address is SilentPaymentAddress) { + if (isSilentPayment) { hasSilentPayment = true; } if (sendAll) { // The value will be changed after estimating the Tx size and deducting the fee from the total to be sent - outputs.add(BitcoinOutput(address: address, value: BigInt.from(0))); + outputs.add(BitcoinOutput( + address: address, + value: BigInt.from(0), + isSilentPayment: isSilentPayment, + )); } else { - outputs.add(BitcoinOutput(address: address, value: BigInt.from(outputAmount))); + outputs.add(BitcoinOutput( + address: address, + value: BigInt.from(outputAmount), + isSilentPayment: isSilentPayment, + )); } } @@ -926,28 +1085,34 @@ abstract class ElectrumWalletBase : feeRate(transactionCredentials.priority!); EstimatedTxResult estimatedTx; + final updatedOutputs = + outputs.map((e) => BitcoinOutput(address: e.address, value: e.value)).toList(); + if (sendAll) { estimatedTx = await estimateSendAllTx( - outputs, + updatedOutputs, feeRateInt, memo: memo, credentialsAmount: credentialsAmount, hasSilentPayment: hasSilentPayment, + coinTypeToSpendFrom: coinTypeToSpendFrom, ); } else { estimatedTx = await estimateTxForAmount( credentialsAmount, outputs, + updatedOutputs, feeRateInt, memo: memo, hasSilentPayment: hasSilentPayment, + coinTypeToSpendFrom: coinTypeToSpendFrom, ); } if (walletInfo.isHardwareWallet) { final transaction = await buildHardwareWalletTransaction( utxos: estimatedTx.utxos, - outputs: outputs, + outputs: updatedOutputs, publicKeys: estimatedTx.publicKeys, fee: BigInt.from(estimatedTx.fee), network: network, @@ -977,7 +1142,7 @@ abstract class ElectrumWalletBase if (network is BitcoinCashNetwork) { txb = ForkedTransactionBuilder( utxos: estimatedTx.utxos, - outputs: outputs, + outputs: updatedOutputs, fee: BigInt.from(estimatedTx.fee), network: network, memo: estimatedTx.memo, @@ -987,7 +1152,7 @@ abstract class ElectrumWalletBase } else { txb = BitcoinTransactionBuilder( utxos: estimatedTx.utxos, - outputs: outputs, + outputs: updatedOutputs, fee: BigInt.from(estimatedTx.fee), network: network, memo: estimatedTx.memo, @@ -1047,6 +1212,7 @@ abstract class ElectrumWalletBase hasChange: estimatedTx.hasChange, isSendAll: estimatedTx.isSendAll, hasTaprootInputs: hasTaprootInputs, + utxos: estimatedTx.utxos, )..addListener((transaction) async { transactionHistory.addOne(transaction); if (estimatedTx.spendsSilentPayment) { @@ -1057,6 +1223,9 @@ abstract class ElectrumWalletBase }); } + unspentCoins + .removeWhere((utxo) => estimatedTx.utxos.any((e) => e.utxo.txHash == utxo.hash)); + await updateBalance(); }); } catch (e) { @@ -1064,6 +1233,9 @@ abstract class ElectrumWalletBase } } + void setLedgerConnection(ledger.LedgerConnection connection) => + throw UnimplementedError(); + Future buildHardwareWalletTransaction({ required List outputs, required BigInt fee, @@ -1092,6 +1264,8 @@ abstract class ElectrumWalletBase 'derivationPath': walletInfo.derivationInfo?.derivationPath, 'silent_addresses': walletAddresses.silentAddresses.map((addr) => addr.toJSON()).toList(), 'silent_address_index': walletAddresses.currentSilentAddressIndex.toString(), + 'mweb_addresses': walletAddresses.mwebAddresses.map((addr) => addr.toJSON()).toList(), + 'alwaysScan': alwaysScan, }); int feeRate(TransactionPriority priority) { @@ -1203,19 +1377,15 @@ abstract class ElectrumWalletBase @action @override - Future rescan({ - required int height, - int? chainTip, - ScanData? scanData, - bool? doSingleScan, - }) async { + Future rescan({required int height, bool? doSingleScan}) async { silentPaymentsScanningActive = true; _setListeners(height, doSingleScan: doSingleScan); } @override - Future close() async { + Future close({required bool shouldCleanup}) async { try { + await _receiveStream?.cancel(); await electrumClient.close(); } catch (_) {} _autoSaveTimer?.cancel(); @@ -1235,77 +1405,70 @@ abstract class ElectrumWalletBase }); } - await Future.wait(walletAddresses.allAddresses.map((address) async { + // Set the balance of all non-silent payment and non-mweb addresses to 0 before updating + walletAddresses.allAddresses + .where((element) => element.type != SegwitAddresType.mweb) + .forEach((addr) { + if (addr is! BitcoinSilentPaymentAddressRecord) addr.balance = 0; + }); + + await Future.wait(walletAddresses.allAddresses + .where((element) => element.type != SegwitAddresType.mweb) + .map((address) async { updatedUnspentCoins.addAll(await fetchUnspent(address)); })); unspentCoins = updatedUnspentCoins; - if (unspentCoinsInfo.isEmpty) { - unspentCoins.forEach((coin) => _addCoinInfo(coin)); + if (unspentCoinsInfo.length != updatedUnspentCoins.length) { + unspentCoins.forEach((coin) => addCoinInfo(coin)); return; } - if (unspentCoins.isNotEmpty) { - unspentCoins.forEach((coin) { - final coinInfoList = unspentCoinsInfo.values.where((element) => - element.walletId.contains(id) && - element.hash.contains(coin.hash) && - element.vout == coin.vout); - - if (coinInfoList.isNotEmpty) { - final coinInfo = coinInfoList.first; - - coin.isFrozen = coinInfo.isFrozen; - coin.isSending = coinInfo.isSending; - coin.note = coinInfo.note; - if (coin.bitcoinAddressRecord is! BitcoinSilentPaymentAddressRecord) - coin.bitcoinAddressRecord.balance += coinInfo.value; - } else { - _addCoinInfo(coin); - } - }); - } - + await updateCoins(unspentCoins); await _refreshUnspentCoinsInfo(); } - @action - Future updateUnspents(BitcoinAddressRecord address) async { - final newUnspentCoins = await fetchUnspent(address); - - if (newUnspentCoins.isNotEmpty) { - unspentCoins.addAll(newUnspentCoins); - - newUnspentCoins.forEach((coin) { - final coinInfoList = unspentCoinsInfo.values.where( - (element) => - element.walletId.contains(id) && - element.hash.contains(coin.hash) && - element.vout == coin.vout, - ); - - if (coinInfoList.isNotEmpty) { - final coinInfo = coinInfoList.first; - - coin.isFrozen = coinInfo.isFrozen; - coin.isSending = coinInfo.isSending; - coin.note = coinInfo.note; - if (coin.bitcoinAddressRecord is! BitcoinSilentPaymentAddressRecord) - coin.bitcoinAddressRecord.balance += coinInfo.value; - } else { - _addCoinInfo(coin); - } - }); + Future updateCoins(List newUnspentCoins) async { + if (newUnspentCoins.isEmpty) { + return; } + + newUnspentCoins.forEach((coin) { + final coinInfoList = unspentCoinsInfo.values.where( + (element) => + element.walletId.contains(id) && + element.hash.contains(coin.hash) && + element.vout == coin.vout, + ); + + if (coinInfoList.isNotEmpty) { + final coinInfo = coinInfoList.first; + + coin.isFrozen = coinInfo.isFrozen; + coin.isSending = coinInfo.isSending; + coin.note = coinInfo.note; + if (coin.bitcoinAddressRecord is! BitcoinSilentPaymentAddressRecord) + coin.bitcoinAddressRecord.balance += coinInfo.value; + } else { + addCoinInfo(coin); + } + }); + } + + @action + Future updateUnspentsForAddress(BitcoinAddressRecord address) async { + final newUnspentCoins = await fetchUnspent(address); + await updateCoins(newUnspentCoins); } @action Future> fetchUnspent(BitcoinAddressRecord address) async { - final unspents = await electrumClient.getListUnspent(address.getScriptHash(network)); - + List> unspents = []; List updatedUnspentCoins = []; + unspents = await electrumClient.getListUnspent(address.getScriptHash(network)); + await Future.wait(unspents.map((unspent) async { try { final coin = BitcoinUnspent.fromJSON(address, unspent); @@ -1321,7 +1484,7 @@ abstract class ElectrumWalletBase } @action - Future _addCoinInfo(BitcoinUnspent coin) async { + Future addCoinInfo(BitcoinUnspent coin) async { final newInfo = UnspentCoinsInfo( walletId: id, hash: coin.hash, @@ -1359,30 +1522,21 @@ abstract class ElectrumWalletBase await unspentCoinsInfo.deleteAll(keys); } } catch (e) { - print(e.toString()); + print("refreshUnspentCoinsInfo $e"); } } - Future canReplaceByFee(String hash) async { - final verboseTransaction = await electrumClient.getTransactionVerbose(hash: hash); + int transactionVSize(String transactionHex) => BtcTransaction.fromRaw(transactionHex).getVSize(); - final String? transactionHex; - int confirmations = 0; - - if (verboseTransaction.isEmpty) { - transactionHex = await electrumClient.getTransactionHex(hash: hash); - } else { - confirmations = verboseTransaction['confirmations'] as int? ?? 0; - transactionHex = verboseTransaction['hex'] as String?; + Future canReplaceByFee(ElectrumTransactionInfo tx) async { + try { + final bundle = await getTransactionExpanded(hash: tx.txHash); + _updateInputsAndOutputs(tx, bundle); + if (bundle.confirmations > 0) return null; + return bundle.originalTransaction.canReplaceByFee ? bundle.originalTransaction.toHex() : null; + } catch (e) { + return null; } - - if (confirmations > 0) return false; - - if (transactionHex == null || transactionHex.isEmpty) { - return false; - } - - return BtcTransaction.fromRaw(transactionHex).canReplaceByFee; } Future isChangeSufficientForFee(String txId, int newFee) async { @@ -1423,6 +1577,7 @@ abstract class ElectrumWalletBase List privateKeys = []; var allInputsAmount = 0; + String? memo; // Add inputs for (var i = 0; i < bundle.originalTransaction.inputs.length; i++) { @@ -1436,9 +1591,11 @@ abstract class ElectrumWalletBase final addressRecord = walletAddresses.allAddresses.firstWhere((element) => element.address == address); - final btcAddress = addressTypeFromStr(addressRecord.address, network); + final btcAddress = RegexUtils.addressTypeFromStr(addressRecord.address, network); final privkey = generateECPrivate( - hd: addressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd, + hd: addressRecord.isHidden + ? walletAddresses.sideHd + : walletAddresses.mainHd, index: addressRecord.index, network: network); @@ -1458,53 +1615,82 @@ abstract class ElectrumWalletBase ); } - int totalOutAmount = bundle.originalTransaction.outputs - .fold(0, (previousValue, element) => previousValue + element.amount.toInt()); - - var currentFee = allInputsAmount - totalOutAmount; - int remainingFee = newFee - currentFee; - + // Create a list of available outputs final outputs = []; - - // Add outputs and deduct the fees from it - for (int i = bundle.originalTransaction.outputs.length - 1; i >= 0; i--) { - final out = bundle.originalTransaction.outputs[i]; - final address = addressFromOutputScript(out.scriptPubKey, network); - final btcAddress = addressTypeFromStr(address, network); - - int newAmount; - if (out.amount.toInt() >= remainingFee) { - newAmount = out.amount.toInt() - remainingFee; - remainingFee = 0; - - // if new amount of output is less than dust amount, then don't add this output as well - if (newAmount <= _dustAmount) { - continue; + for (final out in bundle.originalTransaction.outputs) { + // Check if the script contains OP_RETURN + final script = out.scriptPubKey.script; + if (script.contains('OP_RETURN') && memo == null) { + final index = script.indexOf('OP_RETURN'); + if (index + 1 <= script.length) { + try { + final opReturnData = script[index + 1].toString(); + memo = utf8.decode(HEX.decode(opReturnData)); + continue; + } catch (_) { + throw Exception('Cannot decode OP_RETURN data'); + } } - } else { - remainingFee -= out.amount.toInt(); - continue; } - outputs.add(BitcoinOutput(address: btcAddress, value: BigInt.from(newAmount))); + final address = addressFromOutputScript(out.scriptPubKey, network); + final btcAddress = RegexUtils.addressTypeFromStr(address, network); + outputs.add(BitcoinOutput(address: btcAddress, value: BigInt.from(out.amount.toInt()))); } + // Calculate the total amount and fees + int totalOutAmount = + outputs.fold(0, (previousValue, output) => previousValue + output.value.toInt()); + int currentFee = allInputsAmount - totalOutAmount; + int remainingFee = newFee - currentFee; + + if (remainingFee <= 0) { + throw Exception("New fee must be higher than the current fee."); + } + + // Deduct Remaining Fee from Main Outputs + if (remainingFee > 0) { + for (int i = outputs.length - 1; i >= 0; i--) { + int outputAmount = outputs[i].value.toInt(); + + if (outputAmount > _dustAmount) { + int deduction = (outputAmount - _dustAmount >= remainingFee) + ? remainingFee + : outputAmount - _dustAmount; + outputs[i] = BitcoinOutput( + address: outputs[i].address, value: BigInt.from(outputAmount - deduction)); + remainingFee -= deduction; + + if (remainingFee <= 0) break; + } + } + } + + // Final check if the remaining fee couldn't be deducted + if (remainingFee > 0) { + throw Exception("Not enough funds to cover the fee."); + } + + // Identify all change outputs final changeAddresses = walletAddresses.allAddresses.where((element) => element.isHidden); + final List changeOutputs = outputs + .where((output) => changeAddresses + .any((element) => element.address == output.address.toAddress(network))) + .toList(); - // look for a change address in the outputs - final changeOutput = outputs.firstWhereOrNull((output) => - changeAddresses.any((element) => element.address == output.address.toAddress(network))); + int totalChangeAmount = + changeOutputs.fold(0, (sum, output) => sum + output.value.toInt()); - // deduct the change amount from the output amount - if (changeOutput != null) { - totalOutAmount -= changeOutput.value.toInt(); - } + // The final amount that the receiver will receive + int sendingAmount = allInputsAmount - newFee - totalChangeAmount; final txb = BitcoinTransactionBuilder( utxos: utxos, outputs: outputs, fee: BigInt.from(newFee), network: network, + memo: memo, + outputOrdering: BitcoinOrdering.none, enableRBF: true, ); @@ -1527,12 +1713,19 @@ abstract class ElectrumWalletBase transaction, type, electrumClient: electrumClient, - amount: totalOutAmount, + amount: sendingAmount, fee: newFee, network: network, - hasChange: changeOutput != null, + hasChange: changeOutputs.isNotEmpty, feeRate: newFee.toString(), )..addListener((transaction) async { + transactionHistory.transactions.values.forEach((tx) { + if (tx.id == hash) { + tx.isReplaced = true; + tx.isPending = false; + transactionHistory.addOne(tx); + } + }); transactionHistory.addOne(transaction); await updateBalance(); }); @@ -1544,8 +1737,6 @@ abstract class ElectrumWalletBase Future getTransactionExpanded( {required String hash, int? height}) async { String transactionHex; - // TODO: time is not always available, and calculating it from height is not always accurate. - // Add settings to choose API provider and use and http server instead of electrum for this. int? time; int? confirmations; @@ -1553,6 +1744,31 @@ abstract class ElectrumWalletBase if (verboseTransaction.isEmpty) { transactionHex = await electrumClient.getTransactionHex(hash: hash); + + if (height != null && height > 0 && await checkIfMempoolAPIIsEnabled()) { + try { + final blockHash = await http.get( + Uri.parse( + "http://mempool.cakewallet.com:8999/api/v1/block-height/$height", + ), + ); + + if (blockHash.statusCode == 200 && + blockHash.body.isNotEmpty && + jsonDecode(blockHash.body) != null) { + final blockResponse = await http.get( + Uri.parse( + "http://mempool.cakewallet.com:8999/api/v1/block/${blockHash.body}", + ), + ); + if (blockResponse.statusCode == 200 && + blockResponse.body.isNotEmpty && + jsonDecode(blockResponse.body)['timestamp'] != null) { + time = int.parse(jsonDecode(blockResponse.body)['timestamp'].toString()); + } + } + } catch (_) {} + } } else { transactionHex = verboseTransaction['hex'] as String; time = verboseTransaction['time'] as int?; @@ -1560,8 +1776,9 @@ abstract class ElectrumWalletBase } if (height != null) { - if (time == null) { - time = (getDateByBitcoinHeight(height).millisecondsSinceEpoch / 1000).round(); + if (time == null && height > 0) { + time = (getDateByBitcoinHeight(height).millisecondsSinceEpoch / 1000) + .round(); } if (confirmations == null) { @@ -1623,12 +1840,14 @@ abstract class ElectrumWalletBase final Map historiesWithDetails = {}; if (type == WalletType.bitcoin) { - await Future.wait(ADDRESS_TYPES + await Future.wait(BITCOIN_ADDRESS_TYPES .map((type) => fetchTransactionsForAddressType(historiesWithDetails, type))); } else if (type == WalletType.bitcoinCash) { - await fetchTransactionsForAddressType(historiesWithDetails, P2pkhAddressType.p2pkh); + await Future.wait(BITCOIN_CASH_ADDRESS_TYPES + .map((type) => fetchTransactionsForAddressType(historiesWithDetails, type))); } else if (type == WalletType.litecoin) { - await fetchTransactionsForAddressType(historiesWithDetails, SegwitAddresType.p2wpkh); + await Future.wait(LITECOIN_ADDRESS_TYPES + .map((type) => fetchTransactionsForAddressType(historiesWithDetails, type))); } transactionHistory.transactions.values.forEach((tx) async { @@ -1650,7 +1869,7 @@ abstract class ElectrumWalletBase return historiesWithDetails; } catch (e) { - print(e.toString()); + print("fetchTransactions $e"); return {}; } } @@ -1662,7 +1881,8 @@ abstract class ElectrumWalletBase final addressesByType = walletAddresses.allAddresses.where((addr) => addr.type == type); final hiddenAddresses = addressesByType.where((addr) => addr.isHidden == true); final receiveAddresses = addressesByType.where((addr) => addr.isHidden == false); - + walletAddresses.hiddenAddresses.addAll(hiddenAddresses.map((e) => e.address)); + await walletAddresses.saveAddressesInBox(); await Future.wait(addressesByType.map((addressRecord) async { final history = await _fetchAddressHistory(addressRecord, await getCurrentChainTip()); @@ -1685,7 +1905,7 @@ abstract class ElectrumWalletBase matchedAddresses.toList(), addressRecord.isHidden, (address) async { - await _subscribeForUpdates(); + await subscribeForUpdates(); return _fetchAddressHistory(address, await getCurrentChainTip()) .then((history) => history.isNotEmpty ? address.address : null); }, @@ -1704,6 +1924,8 @@ abstract class ElectrumWalletBase Future> _fetchAddressHistory( BitcoinAddressRecord addressRecord, int? currentHeight) async { + String txid = ""; + try { final Map historiesWithDetails = {}; @@ -1713,7 +1935,7 @@ abstract class ElectrumWalletBase addressRecord.setAsUsed(); await Future.wait(history.map((transaction) async { - final txid = transaction['tx_hash'] as String; + txid = transaction['tx_hash'] as String; final height = transaction['height'] as int; final storedTx = transactionHistory.transactions[txid]; @@ -1721,7 +1943,9 @@ abstract class ElectrumWalletBase if (height > 0) { storedTx.height = height; // the tx's block itself is the first confirmation so add 1 - if (currentHeight != null) storedTx.confirmations = currentHeight - height + 1; + if ((currentHeight ?? 0) > 0) { + storedTx.confirmations = currentHeight! - height + 1; + } storedTx.isPending = storedTx.confirmations == 0; } @@ -1744,22 +1968,31 @@ abstract class ElectrumWalletBase } return historiesWithDetails; - } catch (e) { - print(e.toString()); + } catch (e, stacktrace) { + _onError?.call(FlutterErrorDetails( + exception: "$txid - $e", + stack: stacktrace, + library: this.runtimeType.toString(), + )); return {}; } } Future updateTransactions() async { + print("updateTransactions() called!"); try { if (_isTransactionUpdating) { return; } await getCurrentChainTip(); - transactionHistory.transactions.values.forEach((tx) async { - if (tx.unspents != null && tx.unspents!.isNotEmpty && tx.height != null && tx.height! > 0) { - tx.confirmations = await getCurrentChainTip() - tx.height! + 1; + transactionHistory.transactions.values.forEach((tx) { + if (tx.unspents != null && + tx.unspents!.isNotEmpty && + tx.height != null && + tx.height! > 0 && + (_currentChainTip ?? 0) > 0) { + tx.confirmations = _currentChainTip! - tx.height! + 1; } }); @@ -1774,24 +2007,36 @@ abstract class ElectrumWalletBase } } - Future _subscribeForUpdates() async { + Future subscribeForUpdates() async { final unsubscribedScriptHashes = walletAddresses.allAddresses.where( - (address) => !_scripthashesUpdateSubject.containsKey(address.getScriptHash(network)), + (address) => + !_scripthashesUpdateSubject.containsKey(address.getScriptHash(network)) && + address.type != SegwitAddresType.mweb, ); await Future.wait(unsubscribedScriptHashes.map((address) async { final sh = address.getScriptHash(network); - await _scripthashesUpdateSubject[sh]?.close(); - _scripthashesUpdateSubject[sh] = await electrumClient.scripthashUpdate(sh); + if (!(_scripthashesUpdateSubject[sh]?.isClosed ?? true)) { + try { + await _scripthashesUpdateSubject[sh]?.close(); + } catch (e) { + print("failed to close: $e"); + } + } + try { + _scripthashesUpdateSubject[sh] = await electrumClient.scripthashUpdate(sh); + } catch (e) { + print("failed scripthashUpdate: $e"); + } _scripthashesUpdateSubject[sh]?.listen((event) async { try { - await updateUnspents(address); + await updateUnspentsForAddress(address); await updateBalance(); await _fetchAddressHistory(address, await getCurrentChainTip()); } catch (e, s) { - print(e.toString()); + print("sub error: $e"); _onError?.call(FlutterErrorDetails( exception: e, stack: s, @@ -1802,12 +2047,14 @@ abstract class ElectrumWalletBase })); } - Future _fetchBalances() async { - final addresses = walletAddresses.allAddresses.toList(); + Future fetchBalances() async { + final addresses = walletAddresses.allAddresses + .where((address) => RegexUtils.addressTypeFromStr(address.address, network) is! MwebAddress) + .toList(); final balanceFutures = >>[]; for (var i = 0; i < addresses.length; i++) { final addressRecord = addresses[i]; - final sh = scriptHash(addressRecord.address, network: network); + final sh = addressRecord.getScriptHash(network); final balanceFuture = electrumClient.getBalance(sh); balanceFutures.add(balanceFuture); } @@ -1816,6 +2063,18 @@ abstract class ElectrumWalletBase var totalConfirmed = 0; var totalUnconfirmed = 0; + unspentCoinsInfo.values.forEach((info) { + unspentCoins.forEach((element) { + if (element.hash == info.hash && + element.vout == info.vout && + info.isFrozen && + element.bitcoinAddressRecord.address == info.address && + element.value == info.value) { + totalFrozen += element.value; + } + }); + }); + if (hasSilentPaymentsScanning) { // Add values from unspent coins that are not fetched by the address list // i.e. scanned silent payments @@ -1841,32 +2100,25 @@ abstract class ElectrumWalletBase totalConfirmed += confirmed; totalUnconfirmed += unconfirmed; + addressRecord.balance = confirmed + unconfirmed; if (confirmed > 0 || unconfirmed > 0) { addressRecord.setAsUsed(); } } return ElectrumBalance( - confirmed: totalConfirmed, unconfirmed: totalUnconfirmed, frozen: totalFrozen); + confirmed: totalConfirmed, + unconfirmed: totalUnconfirmed, + frozen: totalFrozen, + ); } Future updateBalance() async { - balance[currency] = await _fetchBalances(); + print("updateBalance() called!"); + balance[currency] = await fetchBalances(); await save(); } - String getChangeAddress() { - const minCountOfHiddenAddresses = 5; - final random = Random(); - var addresses = walletAddresses.allAddresses.where((addr) => addr.isHidden).toList(); - - if (addresses.length < minCountOfHiddenAddresses) { - addresses = walletAddresses.allAddresses.toList(); - } - - return addresses[random.nextInt(addresses.length)].address; - } - @override void setExceptionHandler(void Function(FlutterErrorDetails) onError) => _onError = onError; @@ -1915,7 +2167,7 @@ abstract class ElectrumWalletBase List possibleRecoverIds = [0, 1]; - final baseAddress = addressTypeFromStr(address, network); + final baseAddress = RegexUtils.addressTypeFromStr(address, network); for (int recoveryId in possibleRecoverIds) { final pubKey = sig.recoverPublicKey(messageHash, Curves.generatorSecp256k1, recoveryId); @@ -1971,6 +2223,7 @@ abstract class ElectrumWalletBase @action void _onConnectionStatusChange(ConnectionStatus status) { + switch (status) { case ConnectionStatus.connected: if (syncStatus is NotConnectedSyncStatus || @@ -1982,19 +2235,26 @@ abstract class ElectrumWalletBase break; case ConnectionStatus.disconnected: - syncStatus = NotConnectedSyncStatus(); + if (syncStatus is! NotConnectedSyncStatus) { + syncStatus = NotConnectedSyncStatus(); + } break; case ConnectionStatus.failed: - syncStatus = LostConnectionSyncStatus(); + if (syncStatus is! LostConnectionSyncStatus) { + syncStatus = LostConnectionSyncStatus(); + } break; case ConnectionStatus.connecting: - syncStatus = ConnectingSyncStatus(); + if (syncStatus is! ConnectingSyncStatus) { + syncStatus = ConnectingSyncStatus(); + } break; default: } } void _syncStatusReaction(SyncStatus syncStatus) async { + print("SYNC_STATUS_CHANGE: ${syncStatus}"); if (syncStatus is SyncingSyncStatus) { return; } @@ -2007,9 +2267,9 @@ abstract class ElectrumWalletBase _isTryingToConnect = true; - _reconnectTimer?.cancel(); - _reconnectTimer = Timer(Duration(seconds: 10), () { - if (this.syncStatus is! SyncedSyncStatus && this.syncStatus is! SyncedTipSyncStatus) { + Timer(Duration(seconds: 5), () { + if (this.syncStatus is NotConnectedSyncStatus || + this.syncStatus is LostConnectionSyncStatus) { this.electrumClient.connectToUri( node!.uri, useSSL: node!.useSSL ?? false, @@ -2026,6 +2286,54 @@ abstract class ElectrumWalletBase }); } } + + void _updateInputsAndOutputs(ElectrumTransactionInfo tx, ElectrumTransactionBundle bundle) { + tx.inputAddresses = tx.inputAddresses?.where((address) => address.isNotEmpty).toList(); + + if (tx.inputAddresses == null || + tx.inputAddresses!.isEmpty || + tx.outputAddresses == null || + tx.outputAddresses!.isEmpty) { + List inputAddresses = []; + List outputAddresses = []; + + for (int i = 0; i < bundle.originalTransaction.inputs.length; i++) { + final input = bundle.originalTransaction.inputs[i]; + final inputTransaction = bundle.ins[i]; + final vout = input.txIndex; + final outTransaction = inputTransaction.outputs[vout]; + final address = addressFromOutputScript(outTransaction.scriptPubKey, network); + + if (address.isNotEmpty) inputAddresses.add(address); + } + + for (int i = 0; i < bundle.originalTransaction.outputs.length; i++) { + final out = bundle.originalTransaction.outputs[i]; + final address = addressFromOutputScript(out.scriptPubKey, network); + + if (address.isNotEmpty) outputAddresses.add(address); + + // Check if the script contains OP_RETURN + final script = out.scriptPubKey.script; + if (script.contains('OP_RETURN')) { + final index = script.indexOf('OP_RETURN'); + if (index + 1 <= script.length) { + try { + final opReturnData = script[index + 1].toString(); + final decodedString = utf8.decode(HEX.decode(opReturnData)); + outputAddresses.add('OP_RETURN:$decodedString'); + } catch (_) { + outputAddresses.add('OP_RETURN:'); + } + } + } + } + tx.inputAddresses = inputAddresses; + tx.outputAddresses = outputAddresses; + + transactionHistory.addOne(tx); + } + } } class ScanNode { @@ -2092,21 +2400,22 @@ Future startRefresh(ScanData scanData) async { BehaviorSubject? tweaksSubscription = null; - final syncingStatus = scanData.isSingleScan - ? SyncingSyncStatus(1, 0) - : SyncingSyncStatus.fromHeightValues(scanData.chainTip, initialSyncHeight, syncHeight); - - // Initial status UI update, send how many blocks left to scan - scanData.sendPort.send(SyncResponse(syncHeight, syncingStatus)); - final electrumClient = scanData.electrumClient; await electrumClient.connectToUri( scanData.node?.uri ?? Uri.parse("tcp://electrs.cakewallet.com:50001"), useSSL: scanData.node?.useSSL ?? false, ); + int getCountPerRequest(int syncHeight) { + if (scanData.isSingleScan) { + return 1; + } + + final amountLeft = scanData.chainTip - syncHeight + 1; + return amountLeft; + } + if (tweaksSubscription == null) { - final count = scanData.isSingleScan ? 1 : TWEAKS_COUNT; final receiver = Receiver( scanData.silentAddress.b_scan.toHex(), scanData.silentAddress.B_spend.toHex(), @@ -2115,16 +2424,45 @@ Future startRefresh(ScanData scanData) async { scanData.labelIndexes.length, ); - tweaksSubscription = await electrumClient.tweaksSubscribe(height: syncHeight, count: count); - tweaksSubscription?.listen((t) async { - final tweaks = t as Map; + // Initial status UI update, send how many blocks in total to scan + final initialCount = getCountPerRequest(syncHeight); + scanData.sendPort.send(SyncResponse(syncHeight, StartingScanSyncStatus(syncHeight))); - if (tweaks["message"] != null) { + tweaksSubscription = await electrumClient.tweaksSubscribe( + height: syncHeight, + count: initialCount, + ); + + Future listenFn(t) async { + final tweaks = t as Map; + final msg = tweaks["message"]; + // success or error msg + final noData = msg != null; + + if (noData) { // re-subscribe to continue receiving messages, starting from the next unscanned height - electrumClient.tweaksSubscribe(height: syncHeight + 1, count: count); + final nextHeight = syncHeight + 1; + final nextCount = getCountPerRequest(nextHeight); + + if (nextCount > 0) { + tweaksSubscription?.close(); + + final nextTweaksSubscription = electrumClient.tweaksSubscribe( + height: nextHeight, + count: nextCount, + ); + nextTweaksSubscription?.listen(listenFn); + } + return; } + // Continuous status UI update, send how many blocks left to scan + final syncingStatus = scanData.isSingleScan + ? SyncingSyncStatus(1, 0) + : SyncingSyncStatus.fromHeightValues(scanData.chainTip, initialSyncHeight, syncHeight); + scanData.sendPort.send(SyncResponse(syncHeight, syncingStatus)); + final blockHeight = tweaks.keys.first; final tweakHeight = int.parse(blockHeight); @@ -2159,11 +2497,13 @@ Future startRefresh(ScanData scanData) async { fee: 0, direction: TransactionDirection.incoming, isPending: false, + isReplaced: false, date: scanData.network == BitcoinNetwork.mainnet ? getDateByBitcoinHeight(tweakHeight) : DateTime.now(), confirmations: scanData.chainTip - tweakHeight + 1, unspents: [], + isReceivedSilentPayment: true, ); addToWallet.forEach((label, value) { @@ -2218,16 +2558,6 @@ Future startRefresh(ScanData scanData) async { } catch (_) {} syncHeight = tweakHeight; - scanData.sendPort.send( - SyncResponse( - syncHeight, - SyncingSyncStatus.fromHeightValues( - scanData.chainTip, - initialSyncHeight, - syncHeight, - ), - ), - ); if (tweakHeight >= scanData.chainTip || scanData.isSingleScan) { if (tweakHeight >= scanData.chainTip) @@ -2243,7 +2573,9 @@ Future startRefresh(ScanData scanData) async { await tweaksSubscription!.close(); await electrumClient.close(); } - }); + } + + tweaksSubscription?.listen(listenFn); } if (tweaksSubscription == null) { @@ -2273,6 +2605,8 @@ class EstimatedTxResult { final int fee; final int amount; final bool spendsSilentPayment; + + // final bool sendsToSilentPayment; final bool hasChange; final bool isSendAll; final String? memo; @@ -2286,31 +2620,6 @@ class PublicKeyWithDerivationPath { final String publicKey; } -BitcoinBaseAddress addressTypeFromStr(String address, BasedUtxoNetwork network) { - if (network is BitcoinCashNetwork) { - if (!address.startsWith("bitcoincash:") && - (address.startsWith("q") || address.startsWith("p"))) { - address = "bitcoincash:$address"; - } - - return BitcoinCashAddress(address).baseAddress; - } - - if (P2pkhAddress.regex.hasMatch(address)) { - return P2pkhAddress.fromAddress(address: address, network: network); - } else if (P2shAddress.regex.hasMatch(address)) { - return P2shAddress.fromAddress(address: address, network: network); - } else if (P2wshAddress.regex.hasMatch(address)) { - return P2wshAddress.fromAddress(address: address, network: network); - } else if (P2trAddress.regex.hasMatch(address)) { - return P2trAddress.fromAddress(address: address, network: network); - } else if (SilentPaymentAddress.regex.hasMatch(address)) { - return SilentPaymentAddress.fromAddress(address); - } else { - return P2wpkhAddress.fromAddress(address: address, network: network); - } -} - BitcoinAddressType _getScriptType(BitcoinBaseAddress type) { if (type is P2pkhAddress) { return P2pkhAddressType.p2pkh; @@ -2320,6 +2629,8 @@ BitcoinAddressType _getScriptType(BitcoinBaseAddress type) { return SegwitAddresType.p2wsh; } else if (type is P2trAddress) { return SegwitAddresType.p2tr; + } else if (type is MwebAddress) { + return SegwitAddresType.mweb; } else if (type is SilentPaymentsAddresType) { return SilentPaymentsAddresType.p2sp; } else { diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index 9900995eb..14b9a1e7a 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -1,6 +1,9 @@ +import 'dart:io' show Platform; + 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/bitcoin_unspent.dart'; import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; @@ -10,7 +13,7 @@ part 'electrum_wallet_addresses.g.dart'; class ElectrumWalletAddresses = ElectrumWalletAddressesBase with _$ElectrumWalletAddresses; -const List ADDRESS_TYPES = [ +const List BITCOIN_ADDRESS_TYPES = [ SegwitAddresType.p2wpkh, P2pkhAddressType.p2pkh, SegwitAddresType.p2tr, @@ -18,19 +21,31 @@ const List ADDRESS_TYPES = [ P2shAddressType.p2wshInP2sh, ]; +const List LITECOIN_ADDRESS_TYPES = [ + SegwitAddresType.p2wpkh, + SegwitAddresType.mweb, +]; + +const List BITCOIN_CASH_ADDRESS_TYPES = [ + P2pkhAddressType.p2pkh, +]; + abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { ElectrumWalletAddressesBase( WalletInfo walletInfo, { required this.mainHd, required this.sideHd, required this.network, + required this.isHardwareWallet, List? initialAddresses, Map? initialRegularAddressIndex, Map? initialChangeAddressIndex, List? initialSilentAddresses, int initialSilentAddressIndex = 0, + List? initialMwebAddresses, Bip32Slip10Secp256k1? masterHd, BitcoinAddressType? initialAddressPageType, + }) : _addresses = ObservableList.of((initialAddresses ?? []).toSet()), addressesByReceiveType = ObservableList.of(([]).toSet()), @@ -49,6 +64,8 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { silentAddresses = ObservableList.of( (initialSilentAddresses ?? []).toSet()), currentSilentAddressIndex = initialSilentAddressIndex, + mwebAddresses = + ObservableList.of((initialMwebAddresses ?? []).toSet()), super(walletInfo) { if (masterHd != null) { silentAddress = SilentPaymentOwner.fromPrivateKeys( @@ -87,13 +104,17 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { static const gap = 20; final ObservableList _addresses; - late ObservableList addressesByReceiveType; + final ObservableList addressesByReceiveType; final ObservableList receiveAddresses; final ObservableList changeAddresses; + // TODO: add this variable in `bitcoin_wallet_addresses` and just add a cast in cw_bitcoin to use it final ObservableList silentAddresses; + // TODO: add this variable in `litecoin_wallet_addresses` and just add a cast in cw_bitcoin to use it + final ObservableList mwebAddresses; final BasedUtxoNetwork network; final Bip32Slip10Secp256k1 mainHd; final Bip32Slip10Secp256k1 sideHd; + final bool isHardwareWallet; @observable SilentPaymentOwner? silentAddress; @@ -149,6 +170,9 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { @override set address(String addr) { + if (addr == "Silent Payments" && SilentPaymentsAddresType.p2sp != addressPageType) { + return; + } if (addressPageType == SilentPaymentsAddresType.p2sp) { final selected = silentAddresses.firstWhere((addressRecord) => addressRecord.address == addr); @@ -160,12 +184,17 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { } return; } - - final addressRecord = _addresses.firstWhere((addressRecord) => addressRecord.address == addr); + try { + final addressRecord = _addresses.firstWhere( + (addressRecord) => addressRecord.address == addr, + ); previousAddressRecord = addressRecord; receiveAddresses.remove(addressRecord); receiveAddresses.insert(0, addressRecord); + } catch (e) { + print("ElectrumWalletAddressBase: set address ($addr): $e"); + } } @override @@ -213,14 +242,20 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { if (walletInfo.type == WalletType.bitcoinCash) { await _generateInitialAddresses(type: P2pkhAddressType.p2pkh); } else if (walletInfo.type == WalletType.litecoin) { - await _generateInitialAddresses(); + await _generateInitialAddresses(type: SegwitAddresType.p2wpkh); + if ((Platform.isAndroid || Platform.isIOS) && !isHardwareWallet) { + await _generateInitialAddresses(type: SegwitAddresType.mweb); + } } else if (walletInfo.type == WalletType.bitcoin) { await _generateInitialAddresses(); - await _generateInitialAddresses(type: P2pkhAddressType.p2pkh); - await _generateInitialAddresses(type: P2shAddressType.p2wshInP2sh); - await _generateInitialAddresses(type: SegwitAddresType.p2tr); - await _generateInitialAddresses(type: SegwitAddresType.p2wsh); + if (!isHardwareWallet) { + await _generateInitialAddresses(type: P2pkhAddressType.p2pkh); + await _generateInitialAddresses(type: P2shAddressType.p2wshInP2sh); + await _generateInitialAddresses(type: SegwitAddresType.p2tr); + await _generateInitialAddresses(type: SegwitAddresType.p2wsh); + } } + updateAddressesByMatch(); updateReceiveAddresses(); updateChangeAddresses(); @@ -237,7 +272,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { } @action - Future getChangeAddress() async { + Future getChangeAddress({List? inputs, List? outputs, bool isPegIn = false}) async { updateChangeAddresses(); if (changeAddresses.isEmpty) { @@ -252,7 +287,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { } updateChangeAddresses(); - final address = changeAddresses[currentChangeAddressIndex].address; + final address = changeAddresses[currentChangeAddressIndex]; currentChangeAddressIndex += 1; return address; } @@ -296,7 +331,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { ); silentAddresses.add(address); - updateAddressesByMatch(); + Future.delayed(Duration.zero, () => updateAddressesByMatch()); return address; } @@ -313,16 +348,114 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { network: network, ); _addresses.add(address); - updateAddressesByMatch(); + Future.delayed(Duration.zero, () => updateAddressesByMatch()); return address; } - String getAddress( - {required int index, - required Bip32Slip10Secp256k1 hd, - BitcoinAddressType? addressType}) => + String getAddress({ + required int index, + required Bip32Slip10Secp256k1 hd, + BitcoinAddressType? addressType, + }) => ''; + Future getAddressAsync({ + required int index, + required Bip32Slip10Secp256k1 hd, + BitcoinAddressType? addressType, + }) async => + getAddress(index: index, hd: hd, addressType: addressType); + + void addBitcoinAddressTypes() { + final lastP2wpkh = _addresses + .where((addressRecord) => + _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2wpkh)) + .toList() + .last; + if (lastP2wpkh.address != address) { + addressesMap[lastP2wpkh.address] = 'P2WPKH'; + } else { + addressesMap[address] = 'Active - P2WPKH'; + } + + final lastP2pkh = _addresses.firstWhere( + (addressRecord) => _isUnusedReceiveAddressByType(addressRecord, P2pkhAddressType.p2pkh)); + if (lastP2pkh.address != address) { + addressesMap[lastP2pkh.address] = 'P2PKH'; + } else { + addressesMap[address] = 'Active - P2PKH'; + } + + final lastP2sh = _addresses.firstWhere((addressRecord) => + _isUnusedReceiveAddressByType(addressRecord, P2shAddressType.p2wshInP2sh)); + if (lastP2sh.address != address) { + addressesMap[lastP2sh.address] = 'P2SH'; + } else { + addressesMap[address] = 'Active - P2SH'; + } + + final lastP2tr = _addresses.firstWhere( + (addressRecord) => _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2tr)); + if (lastP2tr.address != address) { + addressesMap[lastP2tr.address] = 'P2TR'; + } else { + addressesMap[address] = 'Active - P2TR'; + } + + final lastP2wsh = _addresses.firstWhere( + (addressRecord) => _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2wsh)); + if (lastP2wsh.address != address) { + addressesMap[lastP2wsh.address] = 'P2WSH'; + } else { + addressesMap[address] = 'Active - P2WSH'; + } + + silentAddresses.forEach((addressRecord) { + if (addressRecord.type != SilentPaymentsAddresType.p2sp || addressRecord.isHidden) { + return; + } + + if (addressRecord.address != address) { + addressesMap[addressRecord.address] = addressRecord.name.isEmpty + ? "Silent Payments" + : "Silent Payments - " + addressRecord.name; + } else { + addressesMap[address] = 'Active - Silent Payments'; + } + }); + } + + void addLitecoinAddressTypes() { + final lastP2wpkh = _addresses + .where((addressRecord) => + _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2wpkh)) + .toList() + .last; + if (lastP2wpkh.address != address) { + addressesMap[lastP2wpkh.address] = 'P2WPKH'; + } else { + addressesMap[address] = 'Active - P2WPKH'; + } + + final lastMweb = _addresses.firstWhere( + (addressRecord) => _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.mweb)); + if (lastMweb.address != address) { + addressesMap[lastMweb.address] = 'MWEB'; + } else { + addressesMap[address] = 'Active - MWEB'; + } + } + + void addBitcoinCashAddressTypes() { + final lastP2pkh = _addresses.firstWhere( + (addressRecord) => _isUnusedReceiveAddressByType(addressRecord, P2pkhAddressType.p2pkh)); + if (lastP2pkh.address != address) { + addressesMap[lastP2pkh.address] = 'P2PKH'; + } else { + addressesMap[address] = 'Active - P2PKH'; + } + } + @override Future updateAddressesInBox() async { try { @@ -334,66 +467,23 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { allAddressesMap[addressRecord.address] = addressRecord.name; }); - final lastP2wpkh = _addresses - .where((addressRecord) => - _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2wpkh)) - .toList() - .last; - if (lastP2wpkh.address != address) { - addressesMap[lastP2wpkh.address] = 'P2WPKH'; - } else { - addressesMap[address] = 'Active - P2WPKH'; + switch (walletInfo.type) { + case WalletType.bitcoin: + addBitcoinAddressTypes(); + break; + case WalletType.litecoin: + addLitecoinAddressTypes(); + break; + case WalletType.bitcoinCash: + addBitcoinCashAddressTypes(); + break; + default: + break; } - final lastP2pkh = _addresses.firstWhere( - (addressRecord) => _isUnusedReceiveAddressByType(addressRecord, P2pkhAddressType.p2pkh)); - if (lastP2pkh.address != address) { - addressesMap[lastP2pkh.address] = 'P2PKH'; - } else { - addressesMap[address] = 'Active - P2PKH'; - } - - final lastP2sh = _addresses.firstWhere((addressRecord) => - _isUnusedReceiveAddressByType(addressRecord, P2shAddressType.p2wshInP2sh)); - if (lastP2sh.address != address) { - addressesMap[lastP2sh.address] = 'P2SH'; - } else { - addressesMap[address] = 'Active - P2SH'; - } - - final lastP2tr = _addresses.firstWhere( - (addressRecord) => _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2tr)); - if (lastP2tr.address != address) { - addressesMap[lastP2tr.address] = 'P2TR'; - } else { - addressesMap[address] = 'Active - P2TR'; - } - - final lastP2wsh = _addresses.firstWhere( - (addressRecord) => _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2wsh)); - if (lastP2wsh.address != address) { - addressesMap[lastP2wsh.address] = 'P2WSH'; - } else { - addressesMap[address] = 'Active - P2WSH'; - } - - silentAddresses.forEach((addressRecord) { - if (addressRecord.type != SilentPaymentsAddresType.p2sp || addressRecord.isHidden) { - return; - } - - if (addressRecord.address != address) { - addressesMap[addressRecord.address] = addressRecord.name.isEmpty - ? "Silent Payments" - : "Silent Payments - " + addressRecord.name; - } else { - addressesMap[address] = 'Active - Silent Payments'; - } - }); - await saveAddressesInBox(); } catch (e) { - print(e.toString()); + print("updateAddresses $e"); } } @@ -410,6 +500,11 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { foundAddress = addressRecord; } }); + mwebAddresses.forEach((addressRecord) { + if (addressRecord.address == address) { + foundAddress = addressRecord; + } + }); if (foundAddress != null) { foundAddress!.setNewName(label); @@ -510,7 +605,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { for (var i = startIndex; i < count + startIndex; i++) { final address = BitcoinAddressRecord( - getAddress(index: i, hd: _getHd(isHidden), addressType: type ?? addressPageType), + await getAddressAsync(index: i, hd: _getHd(isHidden), addressType: type ?? addressPageType), index: i, isHidden: isHidden, type: type ?? addressPageType, @@ -540,15 +635,28 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { updateAddressesByMatch(); } + @action + void addMwebAddresses(Iterable addresses) { + final addressesSet = this.mwebAddresses.toSet(); + addressesSet.addAll(addresses); + this.mwebAddresses.clear(); + this.mwebAddresses.addAll(addressesSet); + updateAddressesByMatch(); + } + void _validateAddresses() { - _addresses.forEach((element) { + _addresses.forEach((element) async { + if (element.type == SegwitAddresType.mweb) { + // this would add a ton of startup lag for mweb addresses since we have 1000 of them + return; + } if (!element.isHidden && element.address != - getAddress(index: element.index, hd: mainHd, addressType: element.type)) { + await getAddressAsync(index: element.index, hd: mainHd, addressType: element.type)) { element.isHidden = true; } else if (element.isHidden && element.address != - getAddress(index: element.index, hd: sideHd, addressType: element.type)) { + await getAddressAsync(index: element.index, hd: sideHd, addressType: element.type)) { element.isHidden = false; } }); @@ -567,7 +675,9 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { } Bip32Slip10Secp256k1 _getHd(bool isHidden) => isHidden ? sideHd : mainHd; + bool _isAddressByType(BitcoinAddressRecord addr, BitcoinAddressType type) => addr.type == type; + bool _isUnusedReceiveAddressByType(BitcoinAddressRecord addr, BitcoinAddressType type) => !addr.isHidden && !addr.isUsed && addr.type == type; diff --git a/cw_bitcoin/lib/electrum_wallet_snapshot.dart b/cw_bitcoin/lib/electrum_wallet_snapshot.dart index fa58be238..990719089 100644 --- a/cw_bitcoin/lib/electrum_wallet_snapshot.dart +++ b/cw_bitcoin/lib/electrum_wallet_snapshot.dart @@ -23,6 +23,8 @@ class ElectrumWalletSnapshot { required this.addressPageType, required this.silentAddresses, required this.silentAddressIndex, + required this.mwebAddresses, + required this.alwaysScan, this.passphrase, this.derivationType, this.derivationPath, @@ -44,6 +46,9 @@ class ElectrumWalletSnapshot { List addresses; List silentAddresses; + List mwebAddresses; + bool alwaysScan; + ElectrumBalance balance; Map regularAddressIndex; Map changeAddressIndex; @@ -51,15 +56,16 @@ class ElectrumWalletSnapshot { DerivationType? derivationType; String? derivationPath; - static Future load( - EncryptionFileUtils encryptionFileUtils, String name, WalletType type, String password, BasedUtxoNetwork network) async { + static Future load(EncryptionFileUtils encryptionFileUtils, String name, + WalletType type, String password, BasedUtxoNetwork network) async { final path = await pathForWallet(name: name, type: type); final jsonSource = await encryptionFileUtils.read(path: path, password: password); final data = json.decode(jsonSource) as Map; - final addressesTmp = data['addresses'] as List? ?? []; final mnemonic = data['mnemonic'] as String?; final xpub = data['xpub'] as String?; final passphrase = data['passphrase'] as String? ?? ''; + + final addressesTmp = data['addresses'] as List? ?? []; final addresses = addressesTmp .whereType() .map((addr) => BitcoinAddressRecord.fromJSON(addr, network: network)) @@ -71,6 +77,14 @@ class ElectrumWalletSnapshot { .map((addr) => BitcoinSilentPaymentAddressRecord.fromJSON(addr, network: network)) .toList(); + final mwebAddressTmp = data['mweb_addresses'] as List? ?? []; + final mwebAddresses = mwebAddressTmp + .whereType() + .map((addr) => BitcoinAddressRecord.fromJSON(addr, network: network)) + .toList(); + + final alwaysScan = data['alwaysScan'] as bool? ?? false; + final balance = ElectrumBalance.fromJSON(data['balance'] as String?) ?? ElectrumBalance(confirmed: 0, unconfirmed: 0, frozen: 0); var regularAddressIndexByType = {SegwitAddresType.p2wpkh.toString(): 0}; @@ -113,6 +127,8 @@ class ElectrumWalletSnapshot { derivationPath: derivationPath, silentAddresses: silentAddresses, silentAddressIndex: silentAddressIndex, + mwebAddresses: mwebAddresses, + alwaysScan: alwaysScan, ); } } diff --git a/cw_bitcoin/lib/exceptions.dart b/cw_bitcoin/lib/exceptions.dart index 3307bfeed..9bdb66eef 100644 --- a/cw_bitcoin/lib/exceptions.dart +++ b/cw_bitcoin/lib/exceptions.dart @@ -3,6 +3,11 @@ import 'package:cw_core/exceptions.dart'; class BitcoinTransactionWrongBalanceException extends TransactionWrongBalanceException { BitcoinTransactionWrongBalanceException({super.amount}) : super(CryptoCurrency.btc); + + @override + String toString() { + return "BitcoinTransactionWrongBalanceException: $amount, $currency"; + } } class BitcoinTransactionNoInputsException extends TransactionNoInputsException {} @@ -13,10 +18,20 @@ class BitcoinTransactionNoDustException extends TransactionNoDustException {} class BitcoinTransactionNoDustOnChangeException extends TransactionNoDustOnChangeException { BitcoinTransactionNoDustOnChangeException(super.max, super.min); + + @override + String toString() { + return "BitcoinTransactionNoDustOnChangeException: max: $max, min: $min"; + } } class BitcoinTransactionCommitFailed extends TransactionCommitFailed { BitcoinTransactionCommitFailed({super.errorMessage}); + + @override + String toString() { + return errorMessage??"unknown error"; + } } class BitcoinTransactionCommitFailedDustChange extends TransactionCommitFailedDustChange {} @@ -30,4 +45,6 @@ class BitcoinTransactionCommitFailedVoutNegative extends TransactionCommitFailed class BitcoinTransactionCommitFailedBIP68Final extends TransactionCommitFailedBIP68Final {} +class BitcoinTransactionCommitFailedLessThanMin extends TransactionCommitFailedLessThanMin {} + class BitcoinTransactionSilentPaymentsNotSupported extends TransactionInputNotSupported {} diff --git a/cw_bitcoin/lib/litecoin_hardware_wallet_service.dart b/cw_bitcoin/lib/litecoin_hardware_wallet_service.dart new file mode 100644 index 000000000..62840933c --- /dev/null +++ b/cw_bitcoin/lib/litecoin_hardware_wallet_service.dart @@ -0,0 +1,46 @@ +import 'dart:async'; + +import 'package:bitcoin_base/bitcoin_base.dart'; +import 'package:blockchain_utils/blockchain_utils.dart'; +import 'package:cw_bitcoin/utils.dart'; +import 'package:cw_core/hardware/hardware_account_data.dart'; +import 'package:ledger_flutter_plus/ledger_flutter_plus.dart'; +import 'package:ledger_litecoin/ledger_litecoin.dart'; + +class LitecoinHardwareWalletService { + LitecoinHardwareWalletService(this.ledgerConnection); + + final LedgerConnection ledgerConnection; + + Future> getAvailableAccounts( + {int index = 0, int limit = 5}) async { + final litecoinLedgerApp = LitecoinLedgerApp(ledgerConnection); + + await litecoinLedgerApp.getVersion(); + + final accounts = []; + final indexRange = List.generate(limit, (i) => i + index); + final xpubVersion = Bip44Conf.litecoinMainNet.altKeyNetVer; + + for (final i in indexRange) { + final derivationPath = "m/84'/2'/$i'"; + final xpub = await litecoinLedgerApp.getXPubKey( + accountsDerivationPath: derivationPath, + xPubVersion: int.parse(hex.encode(xpubVersion.public), radix: 16)); + final hd = Bip32Slip10Secp256k1.fromExtendedKey(xpub, xpubVersion) + .childKey(Bip32KeyIndex(0)); + + final address = generateP2WPKHAddress( + hd: hd, index: 0, network: LitecoinNetwork.mainnet); + + accounts.add(HardwareAccountData( + address: address, + accountIndex: i, + derivationPath: derivationPath, + xpub: xpub, + )); + } + + return accounts; + } +} diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index 890d98342..1fb39c878 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -1,14 +1,33 @@ +import 'dart:async'; import 'dart:convert'; +import 'dart:typed_data'; +import 'package:convert/convert.dart' as convert; +import 'dart:math'; +import 'package:collection/collection.dart'; +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_mweb/mwebd.pbgrpc.dart'; +import 'package:fixnum/fixnum.dart'; +import 'package:bip39/bip39.dart' as bip39; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:blockchain_utils/signer/ecdsa_signing_key.dart'; -import 'package:bip39/bip39.dart' as bip39; import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_mnemonic.dart'; import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; +import 'package:cw_bitcoin/bitcoin_unspent.dart'; +import 'package:cw_bitcoin/electrum_transaction_info.dart'; +import 'package:cw_bitcoin/pending_bitcoin_transaction.dart'; +import 'package:cw_bitcoin/utils.dart'; +import 'package:cw_bitcoin/electrum_derivations.dart'; import 'package:cw_core/encryption_file_utils.dart'; import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/pending_transaction.dart'; +import 'package:cw_core/sync_status.dart'; +import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_bitcoin/electrum_balance.dart'; import 'package:cw_bitcoin/electrum_wallet.dart'; @@ -18,8 +37,13 @@ import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_keys_file.dart'; import 'package:flutter/foundation.dart'; +import 'package:grpc/grpc.dart'; import 'package:hive/hive.dart'; +import 'package:ledger_flutter_plus/ledger_flutter_plus.dart'; +import 'package:ledger_litecoin/ledger_litecoin.dart'; import 'package:mobx/mobx.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:cw_mweb/cw_mweb.dart'; import 'package:bitcoin_base/src/crypto/keypair/sign_utils.dart'; import 'package:pointycastle/ecc/api.dart'; import 'package:pointycastle/ecc/curves/secp256k1.dart'; @@ -30,41 +54,105 @@ class LitecoinWallet = LitecoinWalletBase with _$LitecoinWallet; abstract class LitecoinWalletBase extends ElectrumWallet with Store { LitecoinWalletBase({ - required String mnemonic, required String password, required WalletInfo walletInfo, required Box unspentCoinsInfo, - required Uint8List seedBytes, required EncryptionFileUtils encryptionFileUtils, + Uint8List? seedBytes, + String? mnemonic, + String? xpub, + String? passphrase, String? addressPageType, List? initialAddresses, + List? initialMwebAddresses, ElectrumBalance? initialBalance, Map? initialRegularAddressIndex, Map? initialChangeAddressIndex, + int? initialMwebHeight, + bool? alwaysScan, }) : super( - mnemonic: mnemonic, - password: password, - walletInfo: walletInfo, - unspentCoinsInfo: unspentCoinsInfo, - network: LitecoinNetwork.mainnet, - initialAddresses: initialAddresses, - initialBalance: initialBalance, - seedBytes: seedBytes, - encryptionFileUtils: encryptionFileUtils, - currency: CryptoCurrency.ltc) { + mnemonic: mnemonic, + password: password, + xpub: xpub, + walletInfo: walletInfo, + unspentCoinsInfo: unspentCoinsInfo, + network: LitecoinNetwork.mainnet, + initialAddresses: initialAddresses, + initialBalance: initialBalance, + seedBytes: seedBytes, + encryptionFileUtils: encryptionFileUtils, + currency: CryptoCurrency.ltc, + alwaysScan: alwaysScan, + ) { + if (seedBytes != null) { + mwebHd = Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath( + "m/1000'") as Bip32Slip10Secp256k1; + mwebEnabled = alwaysScan ?? false; + } else { + mwebHd = null; + mwebEnabled = false; + } walletAddresses = LitecoinWalletAddresses( walletInfo, initialAddresses: initialAddresses, initialRegularAddressIndex: initialRegularAddressIndex, initialChangeAddressIndex: initialChangeAddressIndex, + initialMwebAddresses: initialMwebAddresses, mainHd: hd, sideHd: accountHD.childKey(Bip32KeyIndex(1)), network: network, + mwebHd: mwebHd, + mwebEnabled: mwebEnabled, + isHardwareWallet: walletInfo.isHardwareWallet, ); autorun((_) { this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress; }); + reaction((_) => mwebSyncStatus, (status) async { + if (mwebSyncStatus is FailedSyncStatus) { + // we failed to connect to mweb, check if we are connected to the litecoin node: + late int nodeHeight; + try { + nodeHeight = await electrumClient.getCurrentBlockChainTip() ?? 0; + } catch (_) { + nodeHeight = 0; + } + + if (nodeHeight == 0) { + // we aren't connected to the litecoin node, so the current electrum_wallet reactions will take care of this case for us + } else { + // we're connected to the litecoin node, but we failed to connect to mweb, try again after a few seconds: + await CwMweb.stop(); + await Future.delayed(const Duration(seconds: 5)); + startSync(); + } + } else if (mwebSyncStatus is SyncingSyncStatus) { + syncStatus = mwebSyncStatus; + } else if (mwebSyncStatus is SyncronizingSyncStatus) { + if (syncStatus is! SyncronizingSyncStatus) { + syncStatus = mwebSyncStatus; + } + } else if (mwebSyncStatus is SyncedSyncStatus) { + if (syncStatus is! SyncedSyncStatus) { + syncStatus = mwebSyncStatus; + } + } + }); } + late final Bip32Slip10Secp256k1? mwebHd; + late final Box mwebUtxosBox; + Timer? _syncTimer; + Timer? _feeRatesTimer; + Timer? _processingTimer; + StreamSubscription? _utxoStream; + late bool mwebEnabled; + bool processingUtxos = false; + + @observable + SyncStatus mwebSyncStatus = NotConnectedSyncStatus(); + + List get scanSecret => mwebHd!.childKey(Bip32KeyIndex(0x80000000)).privateKey.privKey.raw; + List get spendSecret => mwebHd!.childKey(Bip32KeyIndex(0x80000001)).privateKey.privKey.raw; static Future create( {required String mnemonic, @@ -75,6 +163,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { String? passphrase, String? addressPageType, List? initialAddresses, + List? initialMwebAddresses, ElectrumBalance? initialBalance, Map? initialRegularAddressIndex, Map? initialChangeAddressIndex}) async { @@ -89,7 +178,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { break; case DerivationType.electrum: default: - seedBytes = await mnemonicToSeedBytes(mnemonic); + seedBytes = await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? ""); break; } return LitecoinWallet( @@ -98,8 +187,10 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfo, initialAddresses: initialAddresses, + initialMwebAddresses: initialMwebAddresses, initialBalance: initialBalance, encryptionFileUtils: encryptionFileUtils, + passphrase: passphrase, seedBytes: seedBytes, initialRegularAddressIndex: initialRegularAddressIndex, initialChangeAddressIndex: initialChangeAddressIndex, @@ -107,12 +198,14 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { ); } - static Future open( - {required String name, - required WalletInfo walletInfo, - required Box unspentCoinsInfo, - required String password, - required EncryptionFileUtils encryptionFileUtils}) async { + static Future open({ + required String name, + required WalletInfo walletInfo, + required Box unspentCoinsInfo, + required String password, + required bool alwaysScan, + required EncryptionFileUtils encryptionFileUtils, + }) async { final hasKeysFile = await WalletKeysFile.hasKeysFile(name, walletInfo.type); ElectrumWalletSnapshot? snp = null; @@ -143,18 +236,620 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { ); } + walletInfo.derivationInfo ??= DerivationInfo(); + + // set the default if not present: + walletInfo.derivationInfo!.derivationPath ??= snp?.derivationPath ?? electrum_path; + walletInfo.derivationInfo!.derivationType ??= snp?.derivationType ?? DerivationType.electrum; + + Uint8List? seedBytes = null; + final mnemonic = keysData.mnemonic; + final passphrase = keysData.passphrase; + + if (mnemonic != null) { + switch (walletInfo.derivationInfo?.derivationType) { + case DerivationType.bip39: + seedBytes = await bip39.mnemonicToSeed( + mnemonic, + passphrase: passphrase ?? "", + ); + break; + case DerivationType.electrum: + default: + seedBytes = await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? ""); + break; + } + } + return LitecoinWallet( - mnemonic: keysData.mnemonic!, + mnemonic: keysData.mnemonic, + xpub: keysData.xPub, password: password, walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfo, initialAddresses: snp?.addresses, + initialMwebAddresses: snp?.mwebAddresses, initialBalance: snp?.balance, - seedBytes: await mnemonicToSeedBytes(keysData.mnemonic!), + seedBytes: seedBytes, + passphrase: passphrase, encryptionFileUtils: encryptionFileUtils, initialRegularAddressIndex: snp?.regularAddressIndex, initialChangeAddressIndex: snp?.changeAddressIndex, addressPageType: snp?.addressPageType, + alwaysScan: snp?.alwaysScan, + ); + } + + Future waitForMwebAddresses() async { + print("waitForMwebAddresses() called!"); + // ensure that we have the full 1000 mweb addresses generated before continuing: + // should no longer be needed, but leaving here just in case + await (walletAddresses as LitecoinWalletAddresses).ensureMwebAddressUpToIndexExists(1020); + } + + @action + @override + Future startSync() async { + print("startSync() called!"); + print("STARTING SYNC - MWEB ENABLED: $mwebEnabled"); + if (!mwebEnabled) { + try { + // in case we're switching from a litecoin wallet that had mweb enabled + CwMweb.stop(); + } catch (_) {} + super.startSync(); + return; + } + + if (mwebSyncStatus is SyncronizingSyncStatus) { + return; + } + + print("STARTING SYNC - MWEB ENABLED: $mwebEnabled"); + _syncTimer?.cancel(); + try { + mwebSyncStatus = SyncronizingSyncStatus(); + try { + await subscribeForUpdates(); + } catch (e) { + print("failed to subcribe for updates: $e"); + } + updateFeeRates(); + _feeRatesTimer?.cancel(); + _feeRatesTimer = + Timer.periodic(const Duration(minutes: 1), (timer) async => await updateFeeRates()); + + print("START SYNC FUNCS"); + await waitForMwebAddresses(); + await processMwebUtxos(); + await updateTransactions(); + await updateUnspent(); + await updateBalance(); + print("DONE SYNC FUNCS"); + } catch (e, s) { + print("mweb sync failed: $e $s"); + mwebSyncStatus = FailedSyncStatus(error: "mweb sync failed: $e"); + return; + } + + _syncTimer = Timer.periodic(const Duration(milliseconds: 3000), (timer) async { + if (mwebSyncStatus is FailedSyncStatus) { + _syncTimer?.cancel(); + return; + } + + final nodeHeight = + await electrumClient.getCurrentBlockChainTip() ?? 0; // current block height of our node + + if (nodeHeight == 0) { + // we aren't connected to the ltc node yet + if (mwebSyncStatus is! NotConnectedSyncStatus) { + mwebSyncStatus = FailedSyncStatus(error: "litecoin node isn't connected"); + } + return; + } + + final resp = await CwMweb.status(StatusRequest()); + + try { + if (resp.blockHeaderHeight < nodeHeight) { + int h = resp.blockHeaderHeight; + mwebSyncStatus = SyncingSyncStatus(nodeHeight - h, h / nodeHeight); + } else if (resp.mwebHeaderHeight < nodeHeight) { + int h = resp.mwebHeaderHeight; + mwebSyncStatus = SyncingSyncStatus(nodeHeight - h, h / nodeHeight); + } else if (resp.mwebUtxosHeight < nodeHeight) { + mwebSyncStatus = SyncingSyncStatus(1, 0.999); + } else { + if (resp.mwebUtxosHeight > walletInfo.restoreHeight) { + await walletInfo.updateRestoreHeight(resp.mwebUtxosHeight); + await checkMwebUtxosSpent(); + // update the confirmations for each transaction: + for (final transaction in transactionHistory.transactions.values) { + if (transaction.isPending) continue; + int txHeight = transaction.height ?? resp.mwebUtxosHeight; + final confirmations = (resp.mwebUtxosHeight - txHeight) + 1; + if (transaction.confirmations == confirmations) continue; + if (transaction.confirmations == 0) { + updateBalance(); + } + transaction.confirmations = confirmations; + transactionHistory.addOne(transaction); + } + await transactionHistory.save(); + } + + // prevent unnecessary reaction triggers: + if (mwebSyncStatus is! SyncedSyncStatus) { + // mwebd is synced, but we could still be processing incoming utxos: + if (!processingUtxos) { + mwebSyncStatus = SyncedSyncStatus(); + } + } + return; + } + } catch (e) { + print("error syncing: $e"); + mwebSyncStatus = FailedSyncStatus(error: e.toString()); + } + }); + } + + @action + @override + Future stopSync() async { + print("stopSync() called!"); + _syncTimer?.cancel(); + _utxoStream?.cancel(); + _feeRatesTimer?.cancel(); + await CwMweb.stop(); + print("stopped syncing!"); + } + + Future initMwebUtxosBox() async { + final boxName = "${walletInfo.name.replaceAll(" ", "_")}_${MwebUtxo.boxName}"; + + mwebUtxosBox = await CakeHive.openBox(boxName); + } + + @override + Future renameWalletFiles(String newWalletName) async { + // rename the hive box: + final oldBoxName = "${walletInfo.name.replaceAll(" ", "_")}_${MwebUtxo.boxName}"; + final newBoxName = "${newWalletName.replaceAll(" ", "_")}_${MwebUtxo.boxName}"; + + final oldBox = await CakeHive.openBox(oldBoxName); + mwebUtxosBox = await CakeHive.openBox(newBoxName); + for (final key in oldBox.keys) { + await mwebUtxosBox.put(key, oldBox.get(key)!); + } + oldBox.deleteFromDisk(); + + await super.renameWalletFiles(newWalletName); + } + + @action + @override + Future rescan({ + required int height, + int? chainTip, + ScanData? scanData, + bool? doSingleScan, + bool? usingElectrs, + }) async { + _syncTimer?.cancel(); + await walletInfo.updateRestoreHeight(height); + + // go through mwebUtxos and clear any that are above the new restore height: + if (height == 0) { + await mwebUtxosBox.clear(); + transactionHistory.clear(); + } else { + for (final utxo in mwebUtxosBox.values) { + if (utxo.height > height) { + await mwebUtxosBox.delete(utxo.outputId); + } + } + // TODO: remove transactions that are above the new restore height! + } + + // reset coin balances and txCount to 0: + unspentCoins.forEach((coin) { + if (coin.bitcoinAddressRecord is! BitcoinSilentPaymentAddressRecord) + coin.bitcoinAddressRecord.balance = 0; + coin.bitcoinAddressRecord.txCount = 0; + }); + + for (var addressRecord in walletAddresses.allAddresses) { + addressRecord.balance = 0; + addressRecord.txCount = 0; + } + + await startSync(); + } + + @override + Future init() async { + await super.init(); + await initMwebUtxosBox(); + } + + Future handleIncoming(MwebUtxo utxo) async { + print("handleIncoming() called!"); + final status = await CwMweb.status(StatusRequest()); + var date = DateTime.now(); + var confirmations = 0; + if (utxo.height > 0) { + date = DateTime.fromMillisecondsSinceEpoch(utxo.blockTime * 1000); + confirmations = status.blockHeaderHeight - utxo.height + 1; + } + var tx = transactionHistory.transactions.values + .firstWhereOrNull((tx) => tx.outputAddresses?.contains(utxo.outputId) ?? false); + + if (tx == null) { + tx = ElectrumTransactionInfo( + WalletType.litecoin, + id: utxo.outputId, + height: utxo.height, + amount: utxo.value.toInt(), + fee: 0, + direction: TransactionDirection.incoming, + isPending: utxo.height == 0, + date: date, + confirmations: confirmations, + inputAddresses: [], + outputAddresses: [utxo.outputId], + isReplaced: false, + ); + } + + // don't update the confirmations if the tx is updated by electrum: + if (tx.confirmations == 0 || utxo.height != 0) { + tx.height = utxo.height; + tx.isPending = utxo.height == 0; + tx.confirmations = confirmations; + } + + bool isNew = transactionHistory.transactions[tx.id] == null; + + if (!(tx.outputAddresses?.contains(utxo.address) ?? false)) { + tx.outputAddresses?.add(utxo.address); + isNew = true; + } + + if (isNew) { + final addressRecord = walletAddresses.allAddresses + .firstWhereOrNull((addressRecord) => addressRecord.address == utxo.address); + if (addressRecord == null) { + print("we don't have this address in the wallet! ${utxo.address}"); + return; + } + + // update the txCount: + addressRecord.txCount++; + addressRecord.balance += utxo.value.toInt(); + addressRecord.setAsUsed(); + } + + transactionHistory.addOne(tx); + + if (isNew) { + // update the unconfirmed balance when a new tx is added: + // we do this after adding the tx to the history so that sub address balances are updated correctly + // (since that calculation is based on the tx history) + await updateBalance(); + } + } + + Future processMwebUtxos() async { + print("processMwebUtxos() called!"); + if (!mwebEnabled) { + return; + } + + int restoreHeight = walletInfo.restoreHeight; + print("SCANNING FROM HEIGHT: $restoreHeight"); + final req = UtxosRequest(scanSecret: scanSecret, fromHeight: restoreHeight); + + // process new utxos as they come in: + await _utxoStream?.cancel(); + ResponseStream? responseStream = await CwMweb.utxos(req); + if (responseStream == null) { + throw Exception("failed to get utxos stream!"); + } + _utxoStream = responseStream.listen((Utxo sUtxo) async { + // we're processing utxos, so our balance could still be innacurate: + if (mwebSyncStatus is! SyncronizingSyncStatus && mwebSyncStatus is! SyncingSyncStatus) { + mwebSyncStatus = SyncronizingSyncStatus(); + processingUtxos = true; + _processingTimer?.cancel(); + _processingTimer = Timer.periodic(const Duration(seconds: 2), (timer) async { + processingUtxos = false; + timer.cancel(); + }); + } + + final utxo = MwebUtxo( + address: sUtxo.address, + blockTime: sUtxo.blockTime, + height: sUtxo.height, + outputId: sUtxo.outputId, + value: sUtxo.value.toInt(), + ); + + if (mwebUtxosBox.containsKey(utxo.outputId)) { + // we've already stored this utxo, skip it: + // but do update the utxo height if it's somehow different: + final existingUtxo = mwebUtxosBox.get(utxo.outputId); + if (existingUtxo!.height != utxo.height) { + print( + "updating utxo height for $utxo.outputId: ${existingUtxo.height} -> ${utxo.height}"); + existingUtxo.height = utxo.height; + await mwebUtxosBox.put(utxo.outputId, existingUtxo); + } + return; + } + + await updateUnspent(); + await updateBalance(); + + final mwebAddrs = (walletAddresses as LitecoinWalletAddresses).mwebAddrs; + + // don't process utxos with addresses that are not in the mwebAddrs list: + if (utxo.address.isNotEmpty && !mwebAddrs.contains(utxo.address)) { + return; + } + + await mwebUtxosBox.put(utxo.outputId, utxo); + + await handleIncoming(utxo); + }); + } + + Future checkMwebUtxosSpent() async { + if (!mwebEnabled) { + return; + } + + final pendingOutgoingTransactions = transactionHistory.transactions.values + .where((tx) => tx.direction == TransactionDirection.outgoing && tx.isPending); + + // check if any of the pending outgoing transactions are now confirmed: + bool updatedAny = false; + for (final tx in pendingOutgoingTransactions) { + updatedAny = await isConfirmed(tx) || updatedAny; + } + + // get output ids of all the mweb utxos that have > 0 height: + final outputIds = + mwebUtxosBox.values.where((utxo) => utxo.height > 0).map((utxo) => utxo.outputId).toList(); + + final resp = await CwMweb.spent(SpentRequest(outputId: outputIds)); + final spent = resp.outputId; + if (spent.isEmpty) { + return; + } + + final status = await CwMweb.status(StatusRequest()); + final height = await electrumClient.getCurrentBlockChainTip(); + if (height == null || status.blockHeaderHeight != height) return; + if (status.mwebUtxosHeight != height) return; // we aren't synced + int amount = 0; + Set inputAddresses = {}; + var output = convert.AccumulatorSink(); + var input = sha256.startChunkedConversion(output); + + for (final outputId in spent) { + final utxo = mwebUtxosBox.get(outputId); + await mwebUtxosBox.delete(outputId); + if (utxo == null) continue; + final addressRecord = walletAddresses.allAddresses + .firstWhere((addressRecord) => addressRecord.address == utxo.address); + if (!inputAddresses.contains(utxo.address)) { + addressRecord.txCount++; + } + addressRecord.balance -= utxo.value.toInt(); + amount += utxo.value.toInt(); + inputAddresses.add(utxo.address); + input.add(hex.decode(outputId)); + } + + if (inputAddresses.isEmpty) return; + input.close(); + var digest = output.events.single; + final tx = ElectrumTransactionInfo( + WalletType.litecoin, + id: digest.toString(), + height: height, + amount: amount, + fee: 0, + direction: TransactionDirection.outgoing, + isPending: false, + date: DateTime.fromMillisecondsSinceEpoch(status.blockTime * 1000), + confirmations: 1, + inputAddresses: inputAddresses.toList(), + outputAddresses: [], + isReplaced: false, + ); + + transactionHistory.addOne(tx); + await transactionHistory.save(); + + if (updatedAny) { + await updateBalance(); + } + } + + // checks if a pending transaction is now confirmed, and updates the tx info accordingly: + Future isConfirmed(ElectrumTransactionInfo tx) async { + if (!mwebEnabled) return false; + if (!tx.isPending) return false; + + final outputId = [], target = {}; + final isHash = RegExp(r'^[a-f0-9]{64}$').hasMatch; + final spendingOutputIds = tx.inputAddresses?.where(isHash) ?? []; + final payingToOutputIds = tx.outputAddresses?.where(isHash) ?? []; + outputId.addAll(spendingOutputIds); + outputId.addAll(payingToOutputIds); + target.addAll(spendingOutputIds); + + for (final outputId in payingToOutputIds) { + final spendingTx = transactionHistory.transactions.values + .firstWhereOrNull((tx) => tx.inputAddresses?.contains(outputId) ?? false); + if (spendingTx != null && !spendingTx.isPending) { + target.add(outputId); + } + } + + if (outputId.isEmpty) { + return false; + } + + final resp = await CwMweb.spent(SpentRequest(outputId: outputId)); + if (!setEquals(resp.outputId.toSet(), target)) { + return false; + } + + final status = await CwMweb.status(StatusRequest()); + tx.height = status.mwebUtxosHeight; + tx.confirmations = 1; + tx.isPending = false; + await transactionHistory.save(); + return true; + } + + Future updateUnspent() async { + print("updateUnspent() called!"); + await checkMwebUtxosSpent(); + await updateAllUnspents(); + } + + @override + @action + Future updateAllUnspents() async { + if (!mwebEnabled) { + await super.updateAllUnspents(); + return; + } + + // add the mweb unspents to the list: + List mwebUnspentCoins = []; + // update mweb unspents: + final mwebAddrs = (walletAddresses as LitecoinWalletAddresses).mwebAddrs; + mwebUtxosBox.keys.forEach((dynamic oId) { + final String outputId = oId as String; + final utxo = mwebUtxosBox.get(outputId); + if (utxo == null) { + return; + } + if (utxo.address.isEmpty) { + // not sure if a bug or a special case but we definitely ignore these + return; + } + final addressRecord = walletAddresses.allAddresses + .firstWhereOrNull((addressRecord) => addressRecord.address == utxo.address); + + if (addressRecord == null) { + print("utxo contains an address that is not in the wallet: ${utxo.address}"); + return; + } + final unspent = BitcoinUnspent( + addressRecord, + outputId, + utxo.value.toInt(), + mwebAddrs.indexOf(utxo.address), + ); + if (unspent.vout == 0) { + unspent.isChange = true; + } + mwebUnspentCoins.add(unspent); + }); + + // copy coin control attributes to mwebCoins: + await updateCoins(mwebUnspentCoins); + // get regular ltc unspents (this resets unspentCoins): + await super.updateAllUnspents(); + // add the mwebCoins: + unspentCoins.addAll(mwebUnspentCoins); + } + + @override + Future fetchBalances() async { + final balance = await super.fetchBalances(); + if (!mwebEnabled) { + return balance; + } + + // update unspent balances: + await updateUnspent(); + + int confirmed = balance.confirmed; + int unconfirmed = balance.unconfirmed; + int confirmedMweb = 0; + int unconfirmedMweb = 0; + try { + mwebUtxosBox.values.forEach((utxo) { + if (utxo.height > 0) { + confirmedMweb += utxo.value.toInt(); + } else { + unconfirmedMweb += utxo.value.toInt(); + } + }); + if (unconfirmedMweb > 0) { + unconfirmedMweb = -1 * (confirmedMweb - unconfirmedMweb); + } + } catch (_) {} + + for (var addressRecord in walletAddresses.allAddresses) { + addressRecord.balance = 0; + addressRecord.txCount = 0; + } + + unspentCoins.forEach((coin) { + final coinInfoList = unspentCoinsInfo.values.where( + (element) => + element.walletId.contains(id) && + element.hash.contains(coin.hash) && + element.vout == coin.vout, + ); + + if (coinInfoList.isNotEmpty) { + final coinInfo = coinInfoList.first; + + coin.isFrozen = coinInfo.isFrozen; + coin.isSending = coinInfo.isSending; + coin.note = coinInfo.note; + if (coin.bitcoinAddressRecord is! BitcoinSilentPaymentAddressRecord) + coin.bitcoinAddressRecord.balance += coinInfo.value; + } else { + super.addCoinInfo(coin); + } + }); + + // update the txCount for each address using the tx history, since we can't rely on mwebd + // to have an accurate count, we should just keep it in sync with what we know from the tx history: + for (final tx in transactionHistory.transactions.values) { + // if (tx.isPending) continue; + if (tx.inputAddresses == null || tx.outputAddresses == null) { + continue; + } + final txAddresses = tx.inputAddresses! + tx.outputAddresses!; + for (final address in txAddresses) { + final addressRecord = walletAddresses.allAddresses + .firstWhereOrNull((addressRecord) => addressRecord.address == address); + if (addressRecord == null) { + continue; + } + addressRecord.txCount++; + } + } + + return ElectrumBalance( + confirmed: confirmed, + unconfirmed: unconfirmed, + frozen: balance.frozen, + secondConfirmed: confirmedMweb, + secondUnconfirmed: unconfirmedMweb, ); } @@ -174,6 +869,248 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { return 0; } + @override + Future calcFee({ + required List utxos, + required List outputs, + required BasedUtxoNetwork network, + String? memo, + required int feeRate, + List? inputPrivKeyInfos, + List? vinOutpoints, + }) async { + final spendsMweb = utxos.any((utxo) => utxo.utxo.scriptType == SegwitAddresType.mweb); + final paysToMweb = outputs + .any((output) => output.toOutput.scriptPubKey.getAddressType() == SegwitAddresType.mweb); + if (!spendsMweb && !paysToMweb) { + return await super.calcFee( + utxos: utxos, + outputs: outputs, + network: network, + memo: memo, + feeRate: feeRate, + inputPrivKeyInfos: inputPrivKeyInfos, + vinOutpoints: vinOutpoints, + ); + } + + 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( + script: outputs[0].toOutput.scriptPubKey, value: utxos.sumOfUtxosValue()) + ]; + } + + // https://github.com/ltcmweb/mwebd?tab=readme-ov-file#fee-estimation + final preOutputSum = + outputs.fold(BigInt.zero, (acc, output) => acc + output.toOutput.amount); + final fee = utxos.sumOfUtxosValue() - preOutputSum; + final txb = + BitcoinTransactionBuilder(utxos: utxos, outputs: outputs, fee: fee, network: network); + final resp = await CwMweb.create(CreateRequest( + rawTx: txb.buildTransaction((a, b, c, d) => '').toBytes(), + scanSecret: scanSecret, + spendSecret: spendSecret, + feeRatePerKb: Int64(feeRate * 1000), + dryRun: true)); + final tx = BtcTransaction.fromRaw(hex.encode(resp.rawTx)); + final posUtxos = utxos + .where((utxo) => tx.inputs + .any((input) => input.txId == utxo.utxo.txHash && input.txIndex == utxo.utxo.vout)) + .toList(); + final posOutputSum = tx.outputs.fold(0, (acc, output) => acc + output.amount.toInt()); + final mwebInputSum = utxos.sumOfUtxosValue() - posUtxos.sumOfUtxosValue(); + final expectedPegin = max(0, (preOutputSum - mwebInputSum).toInt()); + var feeIncrease = posOutputSum - expectedPegin; + if (expectedPegin > 0 && fee == BigInt.zero) { + feeIncrease += await super.calcFee( + utxos: posUtxos, + outputs: tx.outputs + .map((output) => + BitcoinScriptOutput(script: output.scriptPubKey, value: output.amount)) + .toList(), + network: network, + memo: memo, + feeRate: feeRate) + + feeRate * 41; + } + return fee.toInt() + feeIncrease; + } + + @override + Future createTransaction(Object credentials) async { + try { + var tx = await super.createTransaction(credentials) as PendingBitcoinTransaction; + tx.isMweb = mwebEnabled; + + if (!mwebEnabled) { + tx.changeAddressOverride = + (await (walletAddresses as LitecoinWalletAddresses) + .getChangeAddress(isPegIn: false)) + .address; + return tx; + } + await waitForMwebAddresses(); + + final resp = await CwMweb.create(CreateRequest( + rawTx: hex.decode(tx.hex), + scanSecret: scanSecret, + spendSecret: spendSecret, + feeRatePerKb: Int64.parseInt(tx.feeRate) * 1000, + )); + final tx2 = BtcTransaction.fromRaw(hex.encode(resp.rawTx)); + + // check if the transaction doesn't contain any mweb inputs or outputs: + final transactionCredentials = credentials as BitcoinTransactionCredentials; + + bool hasMwebInput = false; + bool hasMwebOutput = false; + + for (final output in transactionCredentials.outputs) { + if (output.extractedAddress?.toLowerCase().contains("mweb") ?? false) { + hasMwebOutput = true; + break; + } + if (output.address.toLowerCase().contains("mweb")) { + hasMwebOutput = true; + break; + } + } + + // check if mweb inputs are used: + for (final utxo in tx.utxos) { + if (utxo.utxo.scriptType == SegwitAddresType.mweb) { + hasMwebInput = true; + } + } + + bool isPegIn = !hasMwebInput && hasMwebOutput; + bool isRegular = !hasMwebInput && !hasMwebOutput; + tx.changeAddressOverride = + (await (walletAddresses as LitecoinWalletAddresses) + .getChangeAddress(isPegIn: isPegIn || isRegular)) + .address; + if (!hasMwebInput && !hasMwebOutput) { + tx.isMweb = false; + return tx; + } + + // check if any of the inputs of this transaction are hog-ex: + // this list is only non-mweb inputs: + tx2.inputs.forEach((txInput) { + bool isHogEx = true; + + final utxo = unspentCoins + .firstWhere((utxo) => utxo.hash == txInput.txId && utxo.vout == txInput.txIndex); + + // TODO: detect actual hog-ex inputs + + if (!isHogEx) { + return; + } + + int confirmations = utxo.confirmations ?? 0; + if (confirmations < 6) { + throw Exception( + "A transaction input has less than 6 confirmations, please try again later."); + } + }); + + tx.hexOverride = tx2 + .copyWith( + witnesses: tx2.inputs.asMap().entries.map((e) { + final utxo = unspentCoins + .firstWhere((utxo) => utxo.hash == e.value.txId && utxo.vout == e.value.txIndex); + final key = generateECPrivate( + hd: utxo.bitcoinAddressRecord.isHidden + ? walletAddresses.sideHd + : walletAddresses.mainHd, + index: utxo.bitcoinAddressRecord.index, + network: network); + final digest = tx2.getTransactionSegwitDigit( + txInIndex: e.key, + script: key.getPublic().toP2pkhAddress().toScriptPubKey(), + amount: BigInt.from(utxo.value), + ); + return TxWitnessInput(stack: [key.signInput(digest), key.getPublic().toHex()]); + }).toList()) + .toHex(); + tx.outputAddresses = resp.outputId; + + return tx + ..addListener((transaction) async { + final addresses = {}; + transaction.inputAddresses?.forEach((id) async { + final utxo = mwebUtxosBox.get(id); + await mwebUtxosBox.delete(id); // gets deleted in checkMwebUtxosSpent + if (utxo == null) return; + final addressRecord = walletAddresses.allAddresses + .firstWhere((addressRecord) => addressRecord.address == utxo.address); + if (!addresses.contains(utxo.address)) { + addresses.add(utxo.address); + } + addressRecord.balance -= utxo.value.toInt(); + }); + transaction.inputAddresses?.addAll(addresses); + + transactionHistory.addOne(transaction); + await updateUnspent(); + await updateBalance(); + }); + } catch (e, s) { + print(e); + print(s); + if (e.toString().contains("commit failed")) { + print(e); + throw Exception("Transaction commit failed (no peers responded), please try again."); + } + rethrow; + } + } + + @override + Future save() async { + await super.save(); + } + + @override + Future close({required bool shouldCleanup}) async { + _utxoStream?.cancel(); + _feeRatesTimer?.cancel(); + _syncTimer?.cancel(); + _processingTimer?.cancel(); + if (shouldCleanup) { + try { + await stopSync(); + } catch (_) {} + } + await super.close(shouldCleanup: shouldCleanup); + } + + Future setMwebEnabled(bool enabled) async { + if (mwebEnabled == enabled) { + return; + } + + alwaysScan = enabled; + mwebEnabled = enabled; + (walletAddresses as LitecoinWalletAddresses).mwebEnabled = enabled; + await save(); + try { + await stopSync(); + } catch (_) {} + await startSync(); + } + + Future getStatusRequest() async { + final resp = await CwMweb.status(StatusRequest()); + return resp; + } + @override Future signMessage(String message, {String? address = null}) async { final index = address != null @@ -271,7 +1208,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { List possibleRecoverIds = [0, 1]; - final baseAddress = addressTypeFromStr(address, network); + final baseAddress = RegexUtils.addressTypeFromStr(address, network); for (int recoveryId in possibleRecoverIds) { final pubKey = sig.recoverPublicKey(messageHash, Curves.generatorSecp256k1, recoveryId); @@ -296,4 +1233,64 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { return false; } + + LedgerConnection? _ledgerConnection; + LitecoinLedgerApp? _litecoinLedgerApp; + + @override + void setLedgerConnection(LedgerConnection connection) { + _ledgerConnection = connection; + _litecoinLedgerApp = + LitecoinLedgerApp(_ledgerConnection!, derivationPath: walletInfo.derivationInfo!.derivationPath!); + } + + @override + Future buildHardwareWalletTransaction({ + required List outputs, + required BigInt fee, + required BasedUtxoNetwork network, + required List utxos, + required Map publicKeys, + String? memo, + bool enableRBF = false, + BitcoinOrdering inputOrdering = BitcoinOrdering.bip69, + BitcoinOrdering outputOrdering = BitcoinOrdering.bip69, + }) async { + final readyInputs = []; + for (final utxo in utxos) { + final rawTx = await electrumClient.getTransactionHex(hash: utxo.utxo.txHash); + final publicKeyAndDerivationPath = publicKeys[utxo.ownerDetails.address.pubKeyHash()]!; + + readyInputs.add(LedgerTransaction( + rawTx: rawTx, + outputIndex: utxo.utxo.vout, + ownerPublicKey: Uint8List.fromList(hex.decode(publicKeyAndDerivationPath.publicKey)), + ownerDerivationPath: publicKeyAndDerivationPath.derivationPath, + // sequence: enableRBF ? 0x1 : 0xffffffff, + sequence: 0xffffffff, + )); + } + + String? changePath; + for (final output in outputs) { + final maybeChangePath = publicKeys[(output as BitcoinOutput).address.pubKeyHash()]; + if (maybeChangePath != null) changePath ??= maybeChangePath.derivationPath; + } + + + final rawHex = await _litecoinLedgerApp!.createTransaction( + inputs: readyInputs, + outputs: outputs + .map((e) => TransactionOutput.fromBigInt( + (e as BitcoinOutput).value, Uint8List.fromList(e.address.toScriptPubKey().toBytes()))) + .toList(), + changePath: changePath, + sigHashType: 0x01, + additionals: ["bech32"], + isSegWit: true, + useTrustedInputForSegwit: true + ); + + return BtcTransaction.fromRaw(rawHex); + } } diff --git a/cw_bitcoin/lib/litecoin_wallet_addresses.dart b/cw_bitcoin/lib/litecoin_wallet_addresses.dart index 6945db445..c55f5fc76 100644 --- a/cw_bitcoin/lib/litecoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/litecoin_wallet_addresses.dart @@ -1,29 +1,206 @@ +import 'dart:async'; +import 'dart:io' show Platform; +import 'dart:typed_data'; + 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/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/wallet_info.dart'; +import 'package:cw_mweb/cw_mweb.dart'; +import 'package:flutter/foundation.dart'; import 'package:mobx/mobx.dart'; part 'litecoin_wallet_addresses.g.dart'; -class LitecoinWalletAddresses = LitecoinWalletAddressesBase with _$LitecoinWalletAddresses; +class LitecoinWalletAddresses = LitecoinWalletAddressesBase + with _$LitecoinWalletAddresses; -abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with Store { +abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses + with Store { LitecoinWalletAddressesBase( WalletInfo walletInfo, { required super.mainHd, required super.sideHd, required super.network, + required super.isHardwareWallet, + required this.mwebHd, + required this.mwebEnabled, super.initialAddresses, + super.initialMwebAddresses, super.initialRegularAddressIndex, super.initialChangeAddressIndex, - }) : super(walletInfo); + }) : super(walletInfo) { + for (int i = 0; i < mwebAddresses.length; i++) { + mwebAddrs.add(mwebAddresses[i].address); + } + print("initialized with ${mwebAddrs.length} mweb addresses"); + } + + final Bip32Slip10Secp256k1? mwebHd; + bool mwebEnabled; + int mwebTopUpIndex = 1000; + List mwebAddrs = []; + bool generating = false; + + List get scanSecret => + mwebHd!.childKey(Bip32KeyIndex(0x80000000)).privateKey.privKey.raw; + List get spendPubkey => + mwebHd!.childKey(Bip32KeyIndex(0x80000001)).publicKey.pubKey.compressed; @override - String getAddress( - {required int index, - required Bip32Slip10Secp256k1 hd, - BitcoinAddressType? addressType}) => - generateP2WPKHAddress(hd: hd, index: index, network: network); + Future init() async { + if (!isHardwareWallet) await initMwebAddresses(); + await super.init(); + } + + @computed + @override + List get allAddresses { + return List.from(super.allAddresses)..addAll(mwebAddresses); + } + + Future ensureMwebAddressUpToIndexExists(int index) async { + if (Platform.isLinux || Platform.isMacOS || Platform.isWindows) { + return null; + } + + Uint8List scan = Uint8List.fromList(scanSecret); + Uint8List spend = Uint8List.fromList(spendPubkey); + + if (index < mwebAddresses.length && index < mwebAddrs.length) { + return; + } + + while (generating) { + print("generating....."); + // this function was called multiple times in multiple places: + await Future.delayed(const Duration(milliseconds: 100)); + } + + print("Generating MWEB addresses up to index $index"); + generating = true; + try { + while (mwebAddrs.length <= (index + 1)) { + final addresses = + await CwMweb.addresses(scan, spend, mwebAddrs.length, mwebAddrs.length + 50); + print("generated up to index ${mwebAddrs.length}"); + // sleep for a bit to avoid making the main thread unresponsive: + await Future.delayed(Duration(milliseconds: 200)); + mwebAddrs.addAll(addresses!); + } + } catch (_) {} + generating = false; + print("Done generating MWEB addresses len: ${mwebAddrs.length}"); + + // ensure mweb addresses are up to date: + // This is the Case if the Litecoin Wallet is a hardware Wallet + if (mwebHd == null) return; + + if (mwebAddresses.length < mwebAddrs.length) { + List addressRecords = mwebAddrs + .asMap() + .entries + .map((e) => BitcoinAddressRecord( + e.value, + index: e.key, + type: SegwitAddresType.mweb, + network: network, + )) + .toList(); + addMwebAddresses(addressRecords); + print("set ${addressRecords.length} mweb addresses"); + } + } + + Future initMwebAddresses() async { + if (mwebAddrs.length < 1000) { + await ensureMwebAddressUpToIndexExists(20); + return; + } + } + + @override + String getAddress({ + required int index, + required Bip32Slip10Secp256k1 hd, + BitcoinAddressType? addressType, + }) { + if (addressType == SegwitAddresType.mweb) { + return hd == sideHd ? mwebAddrs[0] : mwebAddrs[index + 1]; + } + return generateP2WPKHAddress(hd: hd, index: index, network: network); + } + + @override + Future getAddressAsync({ + required int index, + required Bip32Slip10Secp256k1 hd, + BitcoinAddressType? addressType, + }) async { + if (addressType == SegwitAddresType.mweb) { + await ensureMwebAddressUpToIndexExists(index); + } + return getAddress(index: index, hd: hd, addressType: addressType); + } + + @action + @override + Future getChangeAddress( + {List? inputs, List? outputs, bool isPegIn = false}) async { + // use regular change address on peg in, otherwise use mweb for change address: + + if (!mwebEnabled || isPegIn) { + return super.getChangeAddress(); + } + + if (inputs != null && outputs != null) { + // check if this is a PEGIN: + bool outputsToMweb = false; + bool comesFromMweb = false; + + for (var i = 0; i < outputs.length; i++) { + // TODO: probably not the best way to tell if this is an mweb address + // (but it doesn't contain the "mweb" text at this stage) + if (outputs[i].address.toAddress(network).length > 110) { + outputsToMweb = true; + } + } + + inputs.forEach((element) { + if (!element.isSending || element.isFrozen) { + return; + } + if (element.address.contains("mweb")) { + comesFromMweb = true; + } + }); + + bool isPegIn = !comesFromMweb && outputsToMweb; + + if (isPegIn && mwebEnabled) { + return super.getChangeAddress(); + } + + // use regular change address if it's not an mweb tx: + if (!comesFromMweb && !outputsToMweb) { + return super.getChangeAddress(); + } + } + + if (mwebEnabled) { + await ensureMwebAddressUpToIndexExists(1); + return BitcoinAddressRecord( + mwebAddrs[0], + index: 0, + type: SegwitAddresType.mweb, + network: network, + ); + } + + return super.getChangeAddress(); + } } diff --git a/cw_bitcoin/lib/litecoin_wallet_service.dart b/cw_bitcoin/lib/litecoin_wallet_service.dart index a46b12a2e..7cc266f5b 100644 --- a/cw_bitcoin/lib/litecoin_wallet_service.dart +++ b/cw_bitcoin/lib/litecoin_wallet_service.dart @@ -1,9 +1,11 @@ import 'dart:io'; +import 'package:bitcoin_base/bitcoin_base.dart'; +import 'package:cw_bitcoin/bitcoin_mnemonics_bip39.dart'; +import 'package:cw_bitcoin/mnemonic_is_incorrect_exception.dart'; import 'package:cw_core/encryption_file_utils.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:hive/hive.dart'; import 'package:cw_bitcoin/bitcoin_mnemonic.dart'; -import 'package:cw_bitcoin/mnemonic_is_incorrect_exception.dart'; import 'package:cw_bitcoin/bitcoin_wallet_creation_credentials.dart'; import 'package:cw_bitcoin/litecoin_wallet.dart'; import 'package:cw_core/wallet_service.dart'; @@ -13,16 +15,19 @@ import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:collection/collection.dart'; import 'package:bip39/bip39.dart' as bip39; +import 'package:path_provider/path_provider.dart'; class LitecoinWalletService extends WalletService< BitcoinNewWalletCredentials, BitcoinRestoreWalletFromSeedCredentials, BitcoinRestoreWalletFromWIFCredentials, - BitcoinNewWalletCredentials> { - LitecoinWalletService(this.walletInfoSource, this.unspentCoinsInfoSource, this.isDirect); + BitcoinRestoreWalletFromHardware> { + LitecoinWalletService( + this.walletInfoSource, this.unspentCoinsInfoSource, this.alwaysScan, this.isDirect); final Box walletInfoSource; final Box unspentCoinsInfoSource; + final bool alwaysScan; final bool isDirect; @override @@ -30,8 +35,21 @@ class LitecoinWalletService extends WalletService< @override Future create(BitcoinNewWalletCredentials credentials, {bool? isTestnet}) async { + final String mnemonic; + switch (credentials.walletInfo?.derivationInfo?.derivationType) { + case DerivationType.bip39: + final strength = credentials.seedPhraseLength == 24 ? 256 : 128; + + mnemonic = credentials.mnemonic ?? await MnemonicBip39.generate(strength: strength); + break; + case DerivationType.electrum: + default: + mnemonic = await generateElectrumMnemonic(); + break; + } + final wallet = await LitecoinWalletBase.create( - mnemonic: await generateElectrumMnemonic(), + mnemonic: mnemonic, password: credentials.password!, passphrase: credentials.passphrase, walletInfo: credentials.walletInfo!, @@ -50,6 +68,7 @@ class LitecoinWalletService extends WalletService< @override Future openWallet(String name, String password) async { + final walletInfo = walletInfoSource.values .firstWhereOrNull((info) => info.id == WalletBase.idFor(name, getType()))!; @@ -59,6 +78,7 @@ class LitecoinWalletService extends WalletService< name: name, walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfoSource, + alwaysScan: alwaysScan, encryptionFileUtils: encryptionFileUtilsFor(isDirect), ); await wallet.init(); @@ -71,6 +91,7 @@ class LitecoinWalletService extends WalletService< name: name, walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfoSource, + alwaysScan: alwaysScan, encryptionFileUtils: encryptionFileUtilsFor(isDirect), ); await wallet.init(); @@ -84,6 +105,23 @@ class LitecoinWalletService extends WalletService< final walletInfo = walletInfoSource.values .firstWhereOrNull((info) => info.id == WalletBase.idFor(wallet, getType()))!; await walletInfoSource.delete(walletInfo.key); + + // if there are no more litecoin wallets left, cleanup the neutrino db and other files created by mwebd: + if (walletInfoSource.values.where((info) => info.type == WalletType.litecoin).isEmpty) { + final appDirPath = (await getApplicationSupportDirectory()).path; + File neturinoDb = File('$appDirPath/neutrino.db'); + File blockHeaders = File('$appDirPath/block_headers.bin'); + File regFilterHeaders = File('$appDirPath/reg_filter_headers.bin'); + if (neturinoDb.existsSync()) { + neturinoDb.deleteSync(); + } + if (blockHeaders.existsSync()) { + blockHeaders.deleteSync(); + } + if (regFilterHeaders.existsSync()) { + regFilterHeaders.deleteSync(); + } + } } @override @@ -95,6 +133,7 @@ class LitecoinWalletService extends WalletService< name: currentName, walletInfo: currentWalletInfo, unspentCoinsInfo: unspentCoinsInfoSource, + alwaysScan: alwaysScan, encryptionFileUtils: encryptionFileUtilsFor(isDirect), ); @@ -109,9 +148,23 @@ class LitecoinWalletService extends WalletService< } @override - Future restoreFromHardwareWallet(BitcoinNewWalletCredentials credentials) { - throw UnimplementedError( - "Restoring a Litecoin wallet from a hardware wallet is not yet supported!"); + Future restoreFromHardwareWallet(BitcoinRestoreWalletFromHardware credentials, + {bool? isTestnet}) async { + final network = isTestnet == true ? LitecoinNetwork.testnet : LitecoinNetwork.mainnet; + credentials.walletInfo?.network = network.value; + credentials.walletInfo?.derivationInfo?.derivationPath = + credentials.hwAccountData.derivationPath; + + final wallet = await LitecoinWallet( + password: credentials.password!, + xpub: credentials.hwAccountData.xpub, + walletInfo: credentials.walletInfo!, + unspentCoinsInfo: unspentCoinsInfoSource, + encryptionFileUtils: encryptionFileUtilsFor(isDirect), + ); + await wallet.save(); + await wallet.init(); + return wallet; } @override diff --git a/cw_bitcoin/lib/pending_bitcoin_transaction.dart b/cw_bitcoin/lib/pending_bitcoin_transaction.dart index 6a4cd1741..5ed84dbf4 100644 --- a/cw_bitcoin/lib/pending_bitcoin_transaction.dart +++ b/cw_bitcoin/lib/pending_bitcoin_transaction.dart @@ -1,11 +1,15 @@ +import 'package:grpc/grpc.dart'; import 'package:cw_bitcoin/exceptions.dart'; import 'package:bitcoin_base/bitcoin_base.dart'; +import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:cw_core/pending_transaction.dart'; import 'package:cw_bitcoin/electrum.dart'; import 'package:cw_bitcoin/bitcoin_amount_format.dart'; import 'package:cw_bitcoin/electrum_transaction_info.dart'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/wallet_type.dart'; +import 'package:cw_mweb/cw_mweb.dart'; +import 'package:cw_mweb/mwebd.pb.dart'; class PendingBitcoinTransaction with PendingTransaction { PendingBitcoinTransaction( @@ -19,6 +23,8 @@ class PendingBitcoinTransaction with PendingTransaction { required this.hasChange, this.isSendAll = false, this.hasTaprootInputs = false, + this.isMweb = false, + this.utxos = const [], }) : _listeners = []; final WalletType type; @@ -28,15 +34,21 @@ class PendingBitcoinTransaction with PendingTransaction { final int fee; final String feeRate; final BasedUtxoNetwork? network; - final bool hasChange; final bool isSendAll; + final bool hasChange; final bool hasTaprootInputs; + List utxos; + bool isMweb; + String? changeAddressOverride; + String? idOverride; + String? hexOverride; + List? outputAddresses; @override - String get id => _tx.txId(); + String get id => idOverride ?? _tx.txId(); @override - String get hex => _tx.serialize(); + String get hex => hexOverride ?? _tx.serialize(); @override String get amountFormatted => bitcoinAmountToString(amount: amount); @@ -47,10 +59,25 @@ class PendingBitcoinTransaction with PendingTransaction { @override int? get outputCount => _tx.outputs.length; + List get outputs => _tx.outputs; + + bool get hasSilentPayment => _tx.hasSilentPayment; + + PendingChange? get change { + try { + final change = _tx.outputs.firstWhere((out) => out.isChange); + if (changeAddressOverride != null) { + return PendingChange(changeAddressOverride!, BtcUtils.fromSatoshi(change.amount)); + } + return PendingChange(change.scriptPubKey.toAddress(), BtcUtils.fromSatoshi(change.amount)); + } catch (_) { + return null; + } + } + final List _listeners; - @override - Future commit() async { + Future _commit() async { int? callId; final result = await electrumClient.broadcastTransaction( @@ -78,11 +105,36 @@ class PendingBitcoinTransaction with PendingTransaction { throw BitcoinTransactionCommitFailedBIP68Final(); } + if (error.contains("min fee not met")) { + throw BitcoinTransactionCommitFailedLessThanMin(); + } + throw BitcoinTransactionCommitFailed(errorMessage: error); } throw BitcoinTransactionCommitFailed(); } + } + + Future _ltcCommit() async { + try { + final stub = await CwMweb.stub(); + final resp = await stub.broadcast(BroadcastRequest(rawTx: BytesUtils.fromHexString(hex))); + idOverride = resp.txid; + } on GrpcError catch (e) { + throw BitcoinTransactionCommitFailed(errorMessage: e.message); + } catch (e) { + throw BitcoinTransactionCommitFailed(errorMessage: "Unknown error: ${e.toString()}"); + } + } + + @override + Future commit() async { + if (isMweb) { + await _ltcCommit(); + } else { + await _commit(); + } _listeners.forEach((listener) => listener(transactionInfo())); } @@ -97,6 +149,9 @@ class PendingBitcoinTransaction with PendingTransaction { direction: TransactionDirection.outgoing, date: DateTime.now(), isPending: true, + isReplaced: false, confirmations: 0, + inputAddresses: _tx.inputs.map((input) => input.txId).toList(), + outputAddresses: outputAddresses, fee: fee); } diff --git a/cw_bitcoin/lib/psbt_transaction_builder.dart b/cw_bitcoin/lib/psbt_transaction_builder.dart index d8d2c9fac..81efb792e 100644 --- a/cw_bitcoin/lib/psbt_transaction_builder.dart +++ b/cw_bitcoin/lib/psbt_transaction_builder.dart @@ -16,10 +16,6 @@ class PSBTTransactionBuild { for (var i = 0; i < inputs.length; i++) { final input = inputs[i]; - print(input.utxo.isP2tr()); - print(input.utxo.isSegwit()); - print(input.utxo.isP2shSegwit()); - psbt.setInputPreviousTxId(i, Uint8List.fromList(hex.decode(input.utxo.txHash).reversed.toList())); psbt.setInputOutputIndex(i, input.utxo.vout); psbt.setInputSequence(i, enableRBF ? 0x1 : 0xffffffff); diff --git a/cw_bitcoin/lib/script_hash.dart b/cw_bitcoin/lib/script_hash.dart deleted file mode 100644 index 2130fcbbe..000000000 --- a/cw_bitcoin/lib/script_hash.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:crypto/crypto.dart'; -import 'package:cw_bitcoin/address_to_output_script.dart'; -import 'package:bitcoin_base/bitcoin_base.dart' as bitcoin; - -String scriptHash(String address, {required bitcoin.BasedUtxoNetwork network}) { - final outputScript = addressToOutputScript(address, network); - final parts = sha256.convert(outputScript).toString().split(''); - var res = ''; - - for (var i = parts.length - 1; i >= 0; i--) { - final char = parts[i]; - i--; - final nextChar = parts[i]; - res += nextChar; - res += char; - } - - return res; -} diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock index cdb88946e..5cba9b734 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -17,6 +17,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.7.0" + archive: + dependency: transitive + description: + name: archive + sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d + url: "https://pub.dev" + source: hosted + version: "3.6.1" args: dependency: transitive description: @@ -29,10 +37,10 @@ packages: dependency: transitive description: name: asn1lib - sha256: "58082b3f0dca697204dbab0ef9ff208bfaea7767ea771076af9a343488428dda" + sha256: "6b151826fcc95ff246cd219a0bf4c753ea14f4081ad71c61939becf3aba27f70" url: "https://pub.dev" source: hosted - version: "1.5.3" + version: "1.5.5" async: dependency: transitive description: @@ -41,6 +49,15 @@ packages: url: "https://pub.dev" source: hosted version: "2.11.0" + bech32: + dependency: "direct main" + description: + path: "." + ref: HEAD + resolved-ref: "05755063b593aa6cca0a4820a318e0ce17de6192" + url: "https://github.com/cake-tech/bech32.git" + source: git + version: "0.2.2" bip32: dependency: transitive description: @@ -70,8 +87,8 @@ packages: dependency: "direct overridden" description: path: "." - ref: cake-update-v5 - resolved-ref: ff2b10eb27b0254ce4518d054332d97d77d9b380 + ref: cake-update-v8 + resolved-ref: fc045a11db3d85d806ca67f75e8b916c706745a2 url: "https://github.com/cake-tech/bitcoin_base" source: git version: "4.7.0" @@ -84,6 +101,14 @@ packages: url: "https://github.com/cake-tech/blockchain_utils" source: git version: "3.3.0" + bluez: + dependency: transitive + description: + name: bluez + sha256: "203a1924e818a9dd74af2b2c7a8f375ab8e5edf0e486bba8f90a0d8a17ed9fce" + url: "https://pub.dev" + source: hosted + version: "0.8.2" boolean_selector: dependency: transitive description: @@ -260,6 +285,13 @@ packages: relative: true source: path version: "0.0.1" + cw_mweb: + dependency: "direct main" + description: + path: "../cw_mweb" + relative: true + source: path + version: "0.0.1" dart_style: dependency: transitive description: @@ -276,6 +308,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.3" + dbus: + dependency: transitive + description: + name: dbus + sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac" + url: "https://pub.dev" + source: hosted + version: "0.7.10" encrypt: dependency: transitive description: @@ -293,29 +333,29 @@ packages: source: hosted version: "1.3.1" ffi: - dependency: transitive + dependency: "direct overridden" description: name: ffi - sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" + sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.0" ffigen: dependency: transitive description: name: ffigen - sha256: d3e76c2ad48a4e7f93a29a162006f00eba46ce7c08194a77bb5c5e97d1b5ff0a + sha256: "3e12e80ccb6539bb3917217bb6f32709220efb737de0d0fa8736da0b7cb507da" url: "https://pub.dev" source: hosted - version: "8.0.2" + version: "12.0.0" file: dependency: transitive description: name: file - sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 url: "https://pub.dev" source: hosted - version: "6.1.4" + version: "7.0.1" fixnum: dependency: transitive description: @@ -337,19 +377,24 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.1+1" - flutter_reactive_ble: - dependency: transitive - description: - name: flutter_reactive_ble - sha256: "247e2efa76de203d1ba11335c13754b5b9d0504b5423e5b0c93a600f016b24e0" - url: "https://pub.dev" - source: hosted - version: "5.3.1" flutter_test: dependency: "direct dev" description: flutter source: sdk version: "0.0.0" + flutter_web_bluetooth: + dependency: transitive + description: + name: flutter_web_bluetooth + sha256: "52ce64f65d7321c4bf6abfe9dac02fb888731339a5e0ad6de59fb916c20c9f02" + url: "https://pub.dev" + source: hosted + version: "0.2.3" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" frontend_server_client: dependency: transitive description: @@ -358,14 +403,6 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0" - functional_data: - dependency: transitive - description: - name: functional_data - sha256: "76d17dc707c40e552014f5a49c0afcc3f1e3f05e800cd6b7872940bfe41a5039" - url: "https://pub.dev" - source: hosted - version: "1.2.0" glob: dependency: transitive description: @@ -374,6 +411,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" + google_identity_services_web: + dependency: transitive + description: + name: google_identity_services_web + sha256: "5be191523702ba8d7a01ca97c17fca096822ccf246b0a9f11923a6ded06199b6" + url: "https://pub.dev" + source: hosted + version: "0.3.1+4" + googleapis_auth: + dependency: transitive + description: + name: googleapis_auth + sha256: befd71383a955535060acde8792e7efc11d2fccd03dd1d3ec434e85b68775938 + url: "https://pub.dev" + source: hosted + version: "1.6.0" graphs: dependency: transitive description: @@ -382,6 +435,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.1" + grpc: + dependency: "direct main" + description: + name: grpc + sha256: e93ee3bce45c134bf44e9728119102358c7cd69de7832d9a874e2e74eb8cab40 + url: "https://pub.dev" + source: hosted + version: "3.2.4" hex: dependency: transitive description: @@ -414,6 +475,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.2" + http2: + dependency: transitive + description: + name: http2 + sha256: "9ced024a160b77aba8fb8674e38f70875e321d319e6f303ec18e87bd5a4b0c1d" + url: "https://pub.dev" + source: hosted + version: "2.3.0" http_multi_server: dependency: transitive description: @@ -489,29 +558,37 @@ packages: ledger_bitcoin: dependency: "direct main" description: - path: "." + path: "packages/ledger-bitcoin" ref: HEAD - resolved-ref: f819d37e235e239c315e93856abbf5e5d3b71dab - url: "https://github.com/cake-tech/ledger-bitcoin" + resolved-ref: dbb5c4956949dc734af3fc8febdbabed89da72aa + url: "https://github.com/cake-tech/ledger-flutter-plus-plugins" source: git - version: "0.0.2" - ledger_flutter: + version: "0.0.3" + ledger_flutter_plus: dependency: "direct main" description: - path: "." - ref: cake-v3 - resolved-ref: "66469ff9dffe2417c70ae7287c9d76d2fe7157a4" - url: "https://github.com/cake-tech/ledger-flutter.git" - source: git - version: "1.0.2" - ledger_usb: - dependency: transitive - description: - name: ledger_usb - sha256: "52c92d03a4cffe06c82921c8e2f79f3cdad6e1cf78e1e9ca35444196ff8f14c2" + name: ledger_flutter_plus + sha256: ea3ed586e1697776dacf42ac979095f1ca3bd143bf007cbe5c78e09cb6943f42 url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.2.5" + ledger_litecoin: + dependency: "direct main" + description: + path: "packages/ledger-litecoin" + ref: HEAD + resolved-ref: dbb5c4956949dc734af3fc8febdbabed89da72aa + url: "https://github.com/cake-tech/ledger-flutter-plus-plugins" + source: git + version: "0.0.2" + ledger_usb_plus: + dependency: transitive + description: + name: ledger_usb_plus + sha256: "21cc5d976cf7edb3518bd2a0c4164139cbb0817d2e4f2054707fc4edfdf9ce87" + url: "https://pub.dev" + source: hosted + version: "1.0.4" logging: dependency: transitive description: @@ -548,10 +625,10 @@ packages: dependency: transitive description: name: mime - sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" + sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a" url: "https://pub.dev" source: hosted - version: "1.0.5" + version: "1.0.6" mobx: dependency: "direct main" description: @@ -640,6 +717,14 @@ packages: 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: @@ -657,13 +742,13 @@ packages: source: hosted version: "2.1.8" pointycastle: - dependency: transitive + dependency: "direct overridden" description: name: pointycastle - sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe" + sha256: "43ac87de6e10afabc85c445745a7b799e04de84cebaa4fd7bf55a5e1e9604d29" url: "https://pub.dev" source: hosted - version: "3.9.1" + version: "3.7.4" pool: dependency: transitive description: @@ -673,13 +758,13 @@ packages: source: hosted version: "1.5.1" protobuf: - dependency: transitive + dependency: "direct overridden" description: name: protobuf - sha256: "01dd9bd0fa02548bf2ceee13545d4a0ec6046459d847b6b061d8a27237108a08" + sha256: "68645b24e0716782e58948f8467fd42a880f255096a821f9e7d0ec625b00c84d" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "3.1.0" provider: dependency: transitive description: @@ -708,34 +793,74 @@ packages: dependency: transitive description: name: quiver - sha256: b1c1ac5ce6688d77f65f3375a9abb9319b3cb32486bdc7a1e0fdf004d7ba4e47 + sha256: ea0b925899e64ecdfbf9c7becb60d5b50e706ade44a85b2363be2a22d88117d2 url: "https://pub.dev" source: hosted - version: "3.2.1" - reactive_ble_mobile: - dependency: transitive - description: - name: reactive_ble_mobile - sha256: "9ec2b4c9c725e439950838d551579750060258fbccd5536d0543b4d07d225798" - url: "https://pub.dev" - source: hosted - version: "5.3.1" - reactive_ble_platform_interface: - dependency: transitive - description: - name: reactive_ble_platform_interface - sha256: "632c92401a2d69c9b94bd48f8fd47488a7013f3d1f9b291884350291a4a81813" - url: "https://pub.dev" - source: hosted - version: "5.3.1" + version: "3.2.2" rxdart: dependency: "direct main" description: name: rxdart - sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb" + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" url: "https://pub.dev" source: hosted - version: "0.27.7" + version: "0.28.0" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180 + url: "https://pub.dev" + source: hosted + version: "2.2.3" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "1ee8bf911094a1b592de7ab29add6f826a7331fb854273d55918693d5364a1f2" + url: "https://pub.dev" + source: hosted + version: "2.2.2" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "07e050c7cd39bad516f8d64c455f04508d09df104be326d8c02551590a0d513d" + url: "https://pub.dev" + source: hosted + version: "2.5.3" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: "59dc807b94d29d52ddbb1b3c0d3b9d0a67fc535a64e62a5542c8db0513fcb6c2" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" shelf: dependency: transitive description: @@ -794,8 +919,8 @@ packages: description: path: "." ref: "sp_v4.0.0" - resolved-ref: "3b8ae38592c0584f53560071dc18bc570758fe13" - url: "https://github.com/rafael-xmr/sp_scanner" + resolved-ref: ca1add293bd1e06920aa049b655832da50d0dab2 + url: "https://github.com/cake-tech/sp_scanner" source: git version: "0.0.1" stack_trace: @@ -870,6 +995,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.2" + universal_ble: + dependency: transitive + description: + name: universal_ble + sha256: "0dfbd6b64bff3ad61ed7a895c232530d9614e9b01ab261a74433a43267edb7f3" + url: "https://pub.dev" + source: hosted + version: "0.12.0" + universal_platform: + dependency: transitive + description: + name: universal_platform + sha256: "64e16458a0ea9b99260ceb5467a214c1f298d647c659af1bff6d3bf82536b1ec" + url: "https://pub.dev" + source: hosted + version: "1.1.0" unorm_dart: dependency: transitive description: @@ -922,10 +1063,18 @@ packages: dependency: transitive description: name: xdg_directories - sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 + url: "https://pub.dev" + source: hosted + version: "6.5.0" yaml: dependency: transitive description: @@ -944,4 +1093,4 @@ packages: version: "2.2.1" sdks: dart: ">=3.3.0 <4.0.0" - flutter: ">=3.16.6" + flutter: ">=3.19.0" diff --git a/cw_bitcoin/pubspec.yaml b/cw_bitcoin/pubspec.yaml index 2af1ac54e..9f1cee67d 100644 --- a/cw_bitcoin/pubspec.yaml +++ b/cw_bitcoin/pubspec.yaml @@ -17,27 +17,38 @@ dependencies: mobx: ^2.0.7+4 flutter_mobx: ^2.0.6+1 intl: ^0.18.0 + shared_preferences: ^2.0.15 cw_core: path: ../cw_core bitbox: git: url: https://github.com/cake-tech/bitbox-flutter.git ref: Add-Support-For-OP-Return-data - rxdart: ^0.27.5 + rxdart: ^0.28.0 cryptography: ^2.0.5 blockchain_utils: git: url: https://github.com/cake-tech/blockchain_utils ref: cake-update-v2 - ledger_flutter: ^1.0.1 - ledger_bitcoin: - git: - url: https://github.com/cake-tech/ledger-bitcoin + cw_mweb: + path: ../cw_mweb + grpc: ^3.2.4 sp_scanner: git: - url: https://github.com/rafael-xmr/sp_scanner + url: https://github.com/cake-tech/sp_scanner ref: sp_v4.0.0 - + bech32: + git: + url: https://github.com/cake-tech/bech32.git + ledger_flutter_plus: ^1.4.1 + ledger_bitcoin: + git: + url: https://github.com/cake-tech/ledger-flutter-plus-plugins + path: packages/ledger-bitcoin + ledger_litecoin: + git: + url: https://github.com/cake-tech/ledger-flutter-plus-plugins + path: packages/ledger-litecoin dev_dependencies: flutter_test: @@ -48,15 +59,14 @@ dev_dependencies: hive_generator: ^1.1.3 dependency_overrides: - ledger_flutter: - git: - url: https://github.com/cake-tech/ledger-flutter.git - ref: cake-v3 watcher: ^1.1.0 + protobuf: ^3.1.0 bitcoin_base: git: url: https://github.com/cake-tech/bitcoin_base - ref: cake-update-v5 + ref: cake-update-v8 + pointycastle: 3.7.4 + ffi: 2.1.0 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_base.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_base.dart index 4699b1649..950b440e7 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_base.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_base.dart @@ -3,5 +3,4 @@ export 'bitcoin_cash_wallet_addresses.dart'; export 'bitcoin_cash_wallet_creation_credentials.dart'; export 'bitcoin_cash_wallet_service.dart'; export 'exceptions/exceptions.dart'; -export 'mnemonic.dart'; export 'bitcoin_cash_address_utils.dart'; diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart index 5659528c0..d55914dcd 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart @@ -1,13 +1,14 @@ import 'package:bitbox/bitbox.dart' as bitbox; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; -import 'package:cw_core/encryption_file_utils.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; +import 'package:cw_bitcoin/bitcoin_mnemonics_bip39.dart'; import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; import 'package:cw_bitcoin/electrum_balance.dart'; import 'package:cw_bitcoin/electrum_wallet.dart'; import 'package:cw_bitcoin/electrum_wallet_snapshot.dart'; import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/encryption_file_utils.dart'; import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/wallet_info.dart'; @@ -30,6 +31,7 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { required Box unspentCoinsInfo, required Uint8List seedBytes, required EncryptionFileUtils encryptionFileUtils, + String? passphrase, BitcoinAddressType? addressPageType, List? initialAddresses, ElectrumBalance? initialBalance, @@ -45,7 +47,8 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { initialBalance: initialBalance, seedBytes: seedBytes, currency: CryptoCurrency.bch, - encryptionFileUtils: encryptionFileUtils) { + encryptionFileUtils: encryptionFileUtils, + passphrase: passphrase) { walletAddresses = BitcoinCashWalletAddresses( walletInfo, initialAddresses: initialAddresses, @@ -55,6 +58,7 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { sideHd: accountHD.childKey(Bip32KeyIndex(1)), network: network, initialAddressPageType: addressPageType, + isHardwareWallet: walletInfo.isHardwareWallet, ); autorun((_) { this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress; @@ -67,6 +71,7 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { required WalletInfo walletInfo, required Box unspentCoinsInfo, required EncryptionFileUtils encryptionFileUtils, + String? passphrase, String? addressPageType, List? initialAddresses, ElectrumBalance? initialBalance, @@ -79,11 +84,12 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { unspentCoinsInfo: unspentCoinsInfo, initialAddresses: initialAddresses, initialBalance: initialBalance, - seedBytes: await MnemonicBip39.toSeed(mnemonic), + seedBytes: MnemonicBip39.toSeed(mnemonic, passphrase: passphrase), encryptionFileUtils: encryptionFileUtils, initialRegularAddressIndex: initialRegularAddressIndex, initialChangeAddressIndex: initialChangeAddressIndex, addressPageType: P2pkhAddressType.p2pkh, + passphrase: passphrase, ); } @@ -150,11 +156,12 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { } }).toList(), initialBalance: snp?.balance, - seedBytes: await MnemonicBip39.toSeed(keysData.mnemonic!), + seedBytes: await MnemonicBip39.toSeed(keysData.mnemonic!, passphrase: keysData.passphrase), encryptionFileUtils: encryptionFileUtils, initialRegularAddressIndex: snp?.regularAddressIndex, initialChangeAddressIndex: snp?.changeAddressIndex, addressPageType: P2pkhAddressType.p2pkh, + passphrase: keysData.passphrase, ); } diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart index 7342dc7f5..fe0ebc828 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart @@ -15,6 +15,7 @@ abstract class BitcoinCashWalletAddressesBase extends ElectrumWalletAddresses wi required super.mainHd, required super.sideHd, required super.network, + required super.isHardwareWallet, super.initialAddresses, super.initialRegularAddressIndex, super.initialChangeAddressIndex, diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_creation_credentials.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_creation_credentials.dart index 017040c5d..af93cdbf8 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_creation_credentials.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_creation_credentials.dart @@ -2,17 +2,31 @@ import 'package:cw_core/wallet_credentials.dart'; import 'package:cw_core/wallet_info.dart'; class BitcoinCashNewWalletCredentials extends WalletCredentials { - BitcoinCashNewWalletCredentials({required String name, WalletInfo? walletInfo, String? password}) - : super(name: name, walletInfo: walletInfo, password: password); + BitcoinCashNewWalletCredentials({ + required String name, + WalletInfo? walletInfo, + String? password, + String? passphrase, + this.mnemonic, + String? parentAddress, + }) : super( + name: name, + walletInfo: walletInfo, + password: password, + passphrase: passphrase, + parentAddress: parentAddress + ); + final String? mnemonic; } class BitcoinCashRestoreWalletFromSeedCredentials extends WalletCredentials { - BitcoinCashRestoreWalletFromSeedCredentials( - {required String name, - required String password, - required this.mnemonic, - WalletInfo? walletInfo}) - : super(name: name, password: password, walletInfo: walletInfo); + BitcoinCashRestoreWalletFromSeedCredentials({ + required String name, + required String password, + required this.mnemonic, + WalletInfo? walletInfo, + String? passphrase, + }) : super(name: name, password: password, walletInfo: walletInfo, passphrase: passphrase); final String mnemonic; } diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_service.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_service.dart index a970be261..d14dc582d 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_service.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_service.dart @@ -1,6 +1,8 @@ import 'dart:io'; import 'package:bip39/bip39.dart'; +import 'package:collection/collection.dart'; +import 'package:cw_bitcoin/bitcoin_mnemonics_bip39.dart'; import 'package:cw_bitcoin_cash/cw_bitcoin_cash.dart'; import 'package:cw_core/encryption_file_utils.dart'; import 'package:cw_core/pathForWallet.dart'; @@ -9,7 +11,6 @@ import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_service.dart'; import 'package:cw_core/wallet_type.dart'; -import 'package:collection/collection.dart'; import 'package:hive/hive.dart'; class BitcoinCashWalletService extends WalletService< @@ -35,11 +36,12 @@ class BitcoinCashWalletService extends WalletService< final strength = credentials.seedPhraseLength == 24 ? 256 : 128; final wallet = await BitcoinCashWalletBase.create( - mnemonic: await MnemonicBip39.generate(strength: strength), + mnemonic: credentials.mnemonic ?? MnemonicBip39.generate(strength: strength), password: credentials.password!, walletInfo: credentials.walletInfo!, unspentCoinsInfo: unspentCoinsInfoSource, encryptionFileUtils: encryptionFileUtilsFor(isDirect), + passphrase: credentials.passphrase, ); await wallet.save(); await wallet.init(); @@ -54,11 +56,11 @@ class BitcoinCashWalletService extends WalletService< try { final wallet = await BitcoinCashWalletBase.open( - password: password, - name: name, - walletInfo: walletInfo, - unspentCoinsInfo: unspentCoinsInfoSource, - encryptionFileUtils: encryptionFileUtilsFor(isDirect), + password: password, + name: name, + walletInfo: walletInfo, + unspentCoinsInfo: unspentCoinsInfoSource, + encryptionFileUtils: encryptionFileUtilsFor(isDirect), ); await wallet.init(); saveBackup(name); @@ -66,11 +68,11 @@ class BitcoinCashWalletService extends WalletService< } catch (_) { await restoreWalletFilesFromBackup(name); final wallet = await BitcoinCashWalletBase.open( - password: password, - name: name, - walletInfo: walletInfo, - unspentCoinsInfo: unspentCoinsInfoSource, - encryptionFileUtils: encryptionFileUtilsFor(isDirect), + password: password, + name: name, + walletInfo: walletInfo, + unspentCoinsInfo: unspentCoinsInfoSource, + encryptionFileUtils: encryptionFileUtilsFor(isDirect), ); await wallet.init(); return wallet; @@ -130,7 +132,9 @@ class BitcoinCashWalletService extends WalletService< mnemonic: credentials.mnemonic, walletInfo: credentials.walletInfo!, unspentCoinsInfo: unspentCoinsInfoSource, - encryptionFileUtils: encryptionFileUtilsFor(isDirect)); + encryptionFileUtils: encryptionFileUtilsFor(isDirect), + passphrase: credentials.passphrase + ); await wallet.save(); await wallet.init(); return wallet; diff --git a/cw_bitcoin_cash/lib/src/pending_bitcoin_cash_transaction.dart b/cw_bitcoin_cash/lib/src/pending_bitcoin_cash_transaction.dart index 6d2ab4696..e1fa9d6e0 100644 --- a/cw_bitcoin_cash/lib/src/pending_bitcoin_cash_transaction.dart +++ b/cw_bitcoin_cash/lib/src/pending_bitcoin_cash_transaction.dart @@ -82,5 +82,7 @@ class PendingBitcoinCashTransaction with PendingTransaction { date: DateTime.now(), isPending: true, confirmations: 0, - fee: fee); + fee: fee, + isReplaced: false, + ); } diff --git a/cw_bitcoin_cash/pubspec.yaml b/cw_bitcoin_cash/pubspec.yaml index 64bd38b1d..cd1e52f51 100644 --- a/cw_bitcoin_cash/pubspec.yaml +++ b/cw_bitcoin_cash/pubspec.yaml @@ -42,7 +42,7 @@ dependency_overrides: bitcoin_base: git: url: https://github.com/cake-tech/bitcoin_base - ref: cake-update-v5 + ref: cake-update-v8 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/cw_core/lib/balance.dart b/cw_core/lib/balance.dart index 431aff515..7350c80f1 100644 --- a/cw_core/lib/balance.dart +++ b/cw_core/lib/balance.dart @@ -1,13 +1,17 @@ abstract class Balance { - const Balance(this.available, this.additional); + const Balance(this.available, this.additional, {this.secondAvailable, this.secondAdditional}); final int available; final int additional; + final int? secondAvailable; + final int? secondAdditional; + String get formattedAvailableBalance; - String get formattedAdditionalBalance; - String get formattedUnAvailableBalance => ''; + String get formattedSecondAvailableBalance => ''; + String get formattedSecondAdditionalBalance => ''; + String get formattedFullAvailableBalance => formattedAvailableBalance; } diff --git a/cw_core/lib/crypto_currency.dart b/cw_core/lib/crypto_currency.dart index cf7a73b3c..0280bb45a 100644 --- a/cw_core/lib/crypto_currency.dart +++ b/cw_core/lib/crypto_currency.dart @@ -106,6 +106,7 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen CryptoCurrency.usdcTrc20, CryptoCurrency.tbtc, CryptoCurrency.wow, + CryptoCurrency.ton, ]; static const havenCurrencies = [ @@ -174,11 +175,11 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen static const zen = CryptoCurrency(title: 'ZEN', fullName: 'Horizen', raw: 44, name: 'zen', iconPath: 'assets/images/zen_icon.png', decimals: 8); static const xvg = CryptoCurrency(title: 'XVG', fullName: 'Verge', raw: 45, name: 'xvg', iconPath: 'assets/images/xvg_icon.png', decimals: 8); - static const usdcpoly = CryptoCurrency(title: 'USDC', tag: 'POLY', fullName: 'USD Coin', raw: 46, name: 'usdcpoly', iconPath: 'assets/images/usdc_icon.png', decimals: 6); + static const usdcpoly = CryptoCurrency(title: 'USDC', tag: 'POL', fullName: 'USD Coin', raw: 46, name: 'usdcpoly', iconPath: 'assets/images/usdc_icon.png', decimals: 6); static const dcr = CryptoCurrency(title: 'DCR', fullName: 'Decred', raw: 47, name: 'dcr', iconPath: 'assets/images/dcr_icon.png', decimals: 8); static const kmd = CryptoCurrency(title: 'KMD', fullName: 'Komodo', raw: 48, name: 'kmd', iconPath: 'assets/images/kmd_icon.png', decimals: 8); static const mana = CryptoCurrency(title: 'MANA', tag: 'ETH', fullName: 'Decentraland', raw: 49, name: 'mana', iconPath: 'assets/images/mana_icon.png', decimals: 18); - static const maticpoly = CryptoCurrency(title: 'MATIC', tag: 'POLY', fullName: 'Polygon', raw: 50, name: 'maticpoly', iconPath: 'assets/images/matic_icon.png', decimals: 18); + static const maticpoly = CryptoCurrency(title: 'POL', tag: 'POL', fullName: 'Polygon', raw: 50, name: 'maticpoly', iconPath: 'assets/images/matic_icon.png', decimals: 18); static const matic = CryptoCurrency(title: 'MATIC', tag: 'ETH', fullName: 'Polygon', raw: 51, name: 'matic', iconPath: 'assets/images/matic_icon.png', decimals: 18); static const mkr = CryptoCurrency(title: 'MKR', tag: 'ETH', fullName: 'Maker', raw: 52, name: 'mkr', iconPath: 'assets/images/mkr_icon.png', decimals: 18); static const near = CryptoCurrency(title: 'NEAR', fullName: 'NEAR Protocol', raw: 53, name: 'near', iconPath: 'assets/images/near_icon.png', decimals: 24); @@ -215,14 +216,15 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen static const dydx = CryptoCurrency(title: 'DYDX', tag: 'ETH', fullName: 'dYdX', raw: 84, name: 'dydx', iconPath: 'assets/images/dydx_icon.png', decimals: 18); static const steth = CryptoCurrency(title: 'STETH', tag: 'ETH', fullName: 'Lido Staked Ethereum', raw: 85, name: 'steth', iconPath: 'assets/images/steth_icon.png', decimals: 18); static const banano = CryptoCurrency(title: 'BAN', fullName: 'Banano', raw: 86, name: 'banano', iconPath: 'assets/images/nano_icon.png', decimals: 29); - static const usdtPoly = CryptoCurrency(title: 'USDT', tag: 'POLY', fullName: 'Tether USD (PoS)', raw: 87, name: 'usdtpoly', iconPath: 'assets/images/usdt_icon.png', decimals: 6); - static const usdcEPoly = CryptoCurrency(title: 'USDC.E', tag: 'POLY', fullName: 'USD Coin (PoS)', raw: 88, name: 'usdcepoly', iconPath: 'assets/images/usdc_icon.png', decimals: 6); + static const usdtPoly = CryptoCurrency(title: 'USDT', tag: 'POL', fullName: 'Tether USD (PoS)', raw: 87, name: 'usdtpoly', iconPath: 'assets/images/usdt_icon.png', decimals: 6); + static const usdcEPoly = CryptoCurrency(title: 'USDC.E', tag: 'POL', fullName: 'USD Coin (PoS)', raw: 88, name: 'usdcepoly', iconPath: 'assets/images/usdc_icon.png', decimals: 6); static const kaspa = CryptoCurrency(title: 'KAS', fullName: 'Kaspa', raw: 89, name: 'kas', iconPath: 'assets/images/kaspa_icon.png', decimals: 8); static const digibyte = CryptoCurrency(title: 'DGB', fullName: 'DigiByte', raw: 90, name: 'dgb', iconPath: 'assets/images/digibyte.png', decimals: 8); static const usdtSol = CryptoCurrency(title: 'USDT', tag: 'SOL', fullName: 'USDT Tether', raw: 91, name: 'usdtsol', iconPath: 'assets/images/usdt_icon.png', decimals: 6); static const usdcTrc20 = CryptoCurrency(title: 'USDC', tag: 'TRX', fullName: 'USDC Coin', raw: 92, name: 'usdctrc20', iconPath: 'assets/images/usdc_icon.png', decimals: 6); 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 final Map _rawCurrencyMap = diff --git a/cw_core/lib/exceptions.dart b/cw_core/lib/exceptions.dart index dccacd799..80bdd2886 100644 --- a/cw_core/lib/exceptions.dart +++ b/cw_core/lib/exceptions.dart @@ -24,6 +24,11 @@ class TransactionCommitFailed implements Exception { final String? errorMessage; TransactionCommitFailed({this.errorMessage}); + + @override + String toString() { + return errorMessage??"unknown error"; + } } class TransactionCommitFailedDustChange implements Exception {} @@ -36,4 +41,6 @@ class TransactionCommitFailedVoutNegative implements Exception {} class TransactionCommitFailedBIP68Final implements Exception {} +class TransactionCommitFailedLessThanMin implements Exception {} + class TransactionInputNotSupported implements Exception {} diff --git a/cw_core/lib/get_height_by_date.dart b/cw_core/lib/get_height_by_date.dart index 204f03d62..2b0b77a89 100644 --- a/cw_core/lib/get_height_by_date.dart +++ b/cw_core/lib/get_height_by_date.dart @@ -267,6 +267,16 @@ const bitcoinDates = { "2023-01": 769810, }; +Future getBitcoinHeightByDateAPI({required DateTime date}) async { + final response = await http.get( + Uri.parse( + "http://mempool.cakewallet.com:8999/api/v1/mining/blocks/timestamp/${(date.millisecondsSinceEpoch / 1000).round()}", + ), + ); + + return jsonDecode(response.body)['height'] as int; +} + int getBitcoinHeightByDate({required DateTime date}) { String dateKey = '${date.year}-${date.month.toString().padLeft(2, '0')}'; final closestKey = bitcoinDates.keys @@ -300,6 +310,11 @@ DateTime getDateByBitcoinHeight(int height) { return estimatedDate; } +int getLtcHeightByDate({required DateTime date}) { + // TODO: use the proxy layer to get the height with a binary search of blocked header heights + return 0; +} + // TODO: enhance all of this global const lists const wowDates = { "2023-12": 583048, @@ -377,4 +392,3 @@ int getWowneroHeightByDate({required DateTime date}) { return wowDates[closestKey] ?? 0; } - diff --git a/cw_core/lib/hardware/device_connection_type.dart b/cw_core/lib/hardware/device_connection_type.dart index 99fd5b1f0..9a3069552 100644 --- a/cw_core/lib/hardware/device_connection_type.dart +++ b/cw_core/lib/hardware/device_connection_type.dart @@ -8,6 +8,7 @@ enum DeviceConnectionType { [bool isIOS = false]) { switch (walletType) { case WalletType.bitcoin: + case WalletType.litecoin: case WalletType.ethereum: case WalletType.polygon: if (isIOS) return [DeviceConnectionType.ble]; diff --git a/cw_core/lib/hive_type_ids.dart b/cw_core/lib/hive_type_ids.dart index 4da616a79..6432c484b 100644 --- a/cw_core/lib/hive_type_ids.dart +++ b/cw_core/lib/hive_type_ids.dart @@ -18,3 +18,4 @@ const SPL_TOKEN_TYPE_ID = 16; 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; \ No newline at end of file diff --git a/cw_core/lib/mweb_utxo.dart b/cw_core/lib/mweb_utxo.dart new file mode 100644 index 000000000..f8dfab395 --- /dev/null +++ b/cw_core/lib/mweb_utxo.dart @@ -0,0 +1,33 @@ +import 'package:cw_core/hive_type_ids.dart'; +import 'package:hive/hive.dart'; + +part 'mweb_utxo.g.dart'; + +@HiveType(typeId: MWEB_UTXO_TYPE_ID) +class MwebUtxo extends HiveObject { + MwebUtxo({ + required this.height, + required this.value, + required this.address, + required this.outputId, + required this.blockTime, + }); + + static const typeId = MWEB_UTXO_TYPE_ID; + static const boxName = 'MwebUtxo'; + + @HiveField(0) + int height; + + @HiveField(1) + int value; + + @HiveField(2) + String address; + + @HiveField(3) + String outputId; + + @HiveField(4) + int blockTime; +} diff --git a/cw_core/lib/node.dart b/cw_core/lib/node.dart index 85c61de15..e19d2a54b 100644 --- a/cw_core/lib/node.dart +++ b/cw_core/lib/node.dart @@ -239,12 +239,15 @@ class Node extends HiveObject with Keyable { // you try to communicate with it Future requestElectrumServer() async { try { + final Socket socket; if (useSSL == true) { - await SecureSocket.connect(uri.host, uri.port, + socket = await SecureSocket.connect(uri.host, uri.port, timeout: Duration(seconds: 5), onBadCertificate: (_) => true); } else { - await Socket.connect(uri.host, uri.port, timeout: Duration(seconds: 5)); + socket = await Socket.connect(uri.host, uri.port, timeout: Duration(seconds: 5)); } + + socket.destroy(); return true; } catch (_) { return false; diff --git a/cw_core/lib/pending_transaction.dart b/cw_core/lib/pending_transaction.dart index 642db9c2c..0a6103a5f 100644 --- a/cw_core/lib/pending_transaction.dart +++ b/cw_core/lib/pending_transaction.dart @@ -1,3 +1,10 @@ +class PendingChange { + final String address; + final String amount; + + PendingChange(this.address, this.amount); +} + mixin PendingTransaction { String get id; String get amountFormatted; @@ -5,6 +12,7 @@ mixin PendingTransaction { String? feeRate; String get hex; int? get outputCount => null; + PendingChange? change; Future commit(); } diff --git a/cw_core/lib/subaddress.dart b/cw_core/lib/subaddress.dart index 8571544a9..4dba7c836 100644 --- a/cw_core/lib/subaddress.dart +++ b/cw_core/lib/subaddress.dart @@ -1,12 +1,22 @@ class Subaddress { - Subaddress({required this.id, required this.address, required this.label}); + Subaddress({ + required this.id, + required this.address, + required this.label, + this.balance = null, + this.txCount = null, + }); Subaddress.fromMap(Map map) : this.id = map['id'] == null ? 0 : int.parse(map['id'] as String), this.address = (map['address'] ?? '') as String, - this.label = (map['label'] ?? '') as String; + this.label = (map['label'] ?? '') as String, + this.balance = (map['balance'] ?? '') as String?, + this.txCount = (map['txCount'] ?? '') as int?; final int id; final String address; final String label; + final String? balance; + final int? txCount; } diff --git a/cw_core/lib/sync_status.dart b/cw_core/lib/sync_status.dart index ea015340c..7d6b0a285 100644 --- a/cw_core/lib/sync_status.dart +++ b/cw_core/lib/sync_status.dart @@ -4,6 +4,9 @@ abstract class SyncStatus { } class StartingScanSyncStatus extends SyncStatus { + StartingScanSyncStatus(this.beginHeight); + + final int beginHeight; @override double progress() => 0.0; } @@ -59,7 +62,18 @@ class AttemptingSyncStatus extends SyncStatus { double progress() => 0.0; } -class FailedSyncStatus extends NotConnectedSyncStatus {} +class AttemptingScanSyncStatus extends SyncStatus { + @override + double progress() => 0.0; +} + +class FailedSyncStatus extends NotConnectedSyncStatus { + String? error; + FailedSyncStatus({this.error}); + + @override + String toString() => error ?? super.toString(); +} class ConnectingSyncStatus extends SyncStatus { @override diff --git a/cw_core/lib/transaction_info.dart b/cw_core/lib/transaction_info.dart index 971e4ecdb..9d0c968d8 100644 --- a/cw_core/lib/transaction_info.dart +++ b/cw_core/lib/transaction_info.dart @@ -18,6 +18,7 @@ abstract class TransactionInfo extends Object with Keyable { String? to; String? from; String? evmSignatureName; + bool? isReplaced; List? inputAddresses; List? outputAddresses; diff --git a/cw_core/lib/unspent_coin_type.dart b/cw_core/lib/unspent_coin_type.dart new file mode 100644 index 000000000..a042610fc --- /dev/null +++ b/cw_core/lib/unspent_coin_type.dart @@ -0,0 +1 @@ +enum UnspentCoinType { mweb, nonMweb, any } \ No newline at end of file diff --git a/cw_core/lib/wallet_addresses.dart b/cw_core/lib/wallet_addresses.dart index e987b5d0e..714d229d9 100644 --- a/cw_core/lib/wallet_addresses.dart +++ b/cw_core/lib/wallet_addresses.dart @@ -1,26 +1,58 @@ import 'package:cw_core/address_info.dart'; import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/wallet_type.dart'; abstract class WalletAddresses { WalletAddresses(this.walletInfo) : addressesMap = {}, allAddressesMap = {}, - addressInfos = {}; + addressInfos = {}, + usedAddresses = {}, + hiddenAddresses = walletInfo.hiddenAddresses?.toSet() ?? {}, + manualAddresses = walletInfo.manualAddresses?.toSet() ?? {}; final WalletInfo walletInfo; String get address; - String? get primaryAddress => null; + String get latestAddress { + if (walletInfo.type == WalletType.monero || walletInfo.type == WalletType.wownero) { + if (addressesMap.keys.length == 0) return address; + return addressesMap[addressesMap.keys.last] ?? address; + } + return _localAddress ?? address; + } - set address(String address); + String get primaryAddress => address; + + String? _localAddress; + + set address(String address) => _localAddress = address; + + String get addressForExchange => address; Map addressesMap; Map allAddressesMap; + Map get usableAddressesMap { + final tmp = addressesMap.map((key, value) => MapEntry(key, value)); // copy address map + tmp.removeWhere((key, value) => hiddenAddresses.contains(key) || manualAddresses.contains(key)); + return tmp; + } + + Map get usableAllAddressesMap { + final tmp = allAddressesMap.map((key, value) => MapEntry(key, value)); // copy address map + tmp.removeWhere((key, value) => hiddenAddresses.contains(key) || manualAddresses.contains(key)); + return tmp; + } + Map> addressInfos; - Set usedAddresses = {}; + Set usedAddresses; + + Set hiddenAddresses; + + Set manualAddresses; Future init(); @@ -32,6 +64,8 @@ abstract class WalletAddresses { walletInfo.addresses = addressesMap; walletInfo.addressInfos = addressInfos; walletInfo.usedAddresses = usedAddresses.toList(); + walletInfo.hiddenAddresses = hiddenAddresses.toList(); + walletInfo.manualAddresses = manualAddresses.toList(); if (walletInfo.isInBox) { await walletInfo.save(); diff --git a/cw_core/lib/wallet_base.dart b/cw_core/lib/wallet_base.dart index f55816bb7..112a20852 100644 --- a/cw_core/lib/wallet_base.dart +++ b/cw_core/lib/wallet_base.dart @@ -46,6 +46,8 @@ abstract class WalletBase null; + String? get passphrase => null; + Object get keys; WalletAddresses get walletAddresses; @@ -65,6 +67,8 @@ abstract class WalletBase startSync(); + Future stopSync() async {} + Future createTransaction(Object credentials); int calculateEstimatedFee(TransactionPriority priority, int? amount); @@ -79,7 +83,7 @@ abstract class WalletBase rescan({required int height}); - void close(); + Future close({required bool shouldCleanup}); Future changePassword(String password); diff --git a/cw_core/lib/wallet_credentials.dart b/cw_core/lib/wallet_credentials.dart index 30ae2546c..55c24bf37 100644 --- a/cw_core/lib/wallet_credentials.dart +++ b/cw_core/lib/wallet_credentials.dart @@ -10,6 +10,7 @@ abstract class WalletCredentials { this.passphrase, this.derivationInfo, this.hardwareWalletType, + this.parentAddress, }) { if (this.walletInfo != null && derivationInfo != null) { this.walletInfo!.derivationInfo = derivationInfo; @@ -18,6 +19,7 @@ abstract class WalletCredentials { final String name; final int? height; + String? parentAddress; int? seedPhraseLength; String? password; String? passphrase; diff --git a/cw_core/lib/wallet_info.dart b/cw_core/lib/wallet_info.dart index ff0c011bb..bd035e30a 100644 --- a/cw_core/lib/wallet_info.dart +++ b/cw_core/lib/wallet_info.dart @@ -80,6 +80,7 @@ class WalletInfo extends HiveObject { this.showIntroCakePayCard, this.derivationInfo, this.hardwareWalletType, + this.parentAddress, ) : _yatLastUsedAddressController = StreamController.broadcast(); factory WalletInfo.external({ @@ -97,6 +98,7 @@ class WalletInfo extends HiveObject { String yatLastUsedAddressRaw = '', DerivationInfo? derivationInfo, HardwareWalletType? hardwareWalletType, + String? parentAddress, }) { return WalletInfo( id, @@ -113,6 +115,7 @@ class WalletInfo extends HiveObject { showIntroCakePayCard, derivationInfo, hardwareWalletType, + parentAddress, ); } @@ -184,6 +187,18 @@ class WalletInfo extends HiveObject { @HiveField(21) HardwareWalletType? hardwareWalletType; + @HiveField(22) + String? parentAddress; + + @HiveField(23) + List? hiddenAddresses; + + @HiveField(24) + List? manualAddresses; + + + + String get yatLastUsedAddress => yatLastUsedAddressRaw ?? ''; set yatLastUsedAddress(String address) { diff --git a/cw_core/pubspec.lock b/cw_core/pubspec.lock index e905af2d9..c2bdda5f1 100644 --- a/cw_core/pubspec.lock +++ b/cw_core/pubspec.lock @@ -230,10 +230,10 @@ packages: dependency: "direct main" description: name: file - sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" url: "https://pub.dev" source: hosted - version: "6.1.4" + version: "7.0.0" fixnum: dependency: transitive description: diff --git a/cw_core/pubspec.yaml b/cw_core/pubspec.yaml index 4497a709d..070779caa 100644 --- a/cw_core/pubspec.yaml +++ b/cw_core/pubspec.yaml @@ -13,7 +13,7 @@ dependencies: flutter: sdk: flutter http: ^1.1.0 - file: ^6.1.4 + file: ^7.0.0 path_provider: ^2.0.11 mobx: ^2.0.7+4 flutter_mobx: ^2.0.6+1 diff --git a/cw_ethereum/lib/ethereum_wallet.dart b/cw_ethereum/lib/ethereum_wallet.dart index 51aeab5e1..765ace052 100644 --- a/cw_ethereum/lib/ethereum_wallet.dart +++ b/cw_ethereum/lib/ethereum_wallet.dart @@ -27,6 +27,7 @@ class EthereumWallet extends EVMChainWallet { super.initialBalance, super.privateKey, required super.encryptionFileUtils, + super.passphrase, }) : super(nativeCurrency: CryptoCurrency.eth); @override @@ -142,7 +143,7 @@ class EthereumWallet extends EVMChainWallet { if (!hasKeysFile) rethrow; } - final balance = EVMChainERC20Balance.fromJSON(data?['balance'] as String) ?? + final balance = EVMChainERC20Balance.fromJSON(data?['balance'] as String?) ?? EVMChainERC20Balance(BigInt.zero); final WalletKeysData keysData; @@ -150,8 +151,9 @@ class EthereumWallet extends EVMChainWallet { if (!hasKeysFile) { final mnemonic = data!['mnemonic'] as String?; final privateKey = data['private_key'] as String?; + final passphrase = data['passphrase'] as String?; - keysData = WalletKeysData(mnemonic: mnemonic, privateKey: privateKey); + keysData = WalletKeysData(mnemonic: mnemonic, privateKey: privateKey, passphrase: passphrase); } else { keysData = await WalletKeysFile.readKeysFile( name, @@ -166,6 +168,7 @@ class EthereumWallet extends EVMChainWallet { password: password, mnemonic: keysData.mnemonic, privateKey: keysData.privateKey, + passphrase: keysData.passphrase, initialBalance: balance, client: EthereumClient(), encryptionFileUtils: encryptionFileUtils, diff --git a/cw_ethereum/lib/ethereum_wallet_service.dart b/cw_ethereum/lib/ethereum_wallet_service.dart index 84fc0a277..858416055 100644 --- a/cw_ethereum/lib/ethereum_wallet_service.dart +++ b/cw_ethereum/lib/ethereum_wallet_service.dart @@ -21,12 +21,13 @@ class EthereumWalletService extends EVMChainWalletService { Future create(EVMChainNewWalletCredentials credentials, {bool? isTestnet}) async { final strength = credentials.seedPhraseLength == 24 ? 256 : 128; - final mnemonic = bip39.generateMnemonic(strength: strength); + final mnemonic = credentials.mnemonic ?? bip39.generateMnemonic(strength: strength); final wallet = EthereumWallet( walletInfo: credentials.walletInfo!, mnemonic: mnemonic, password: credentials.password!, + passphrase: credentials.passphrase, client: client, encryptionFileUtils: encryptionFileUtilsFor(isDirect), ); @@ -144,6 +145,7 @@ class EthereumWalletService extends EVMChainWalletService { password: credentials.password!, mnemonic: credentials.mnemonic, walletInfo: credentials.walletInfo!, + passphrase: credentials.passphrase, client: client, encryptionFileUtils: encryptionFileUtilsFor(isDirect), ); diff --git a/cw_evm/lib/evm_chain_hardware_wallet_service.dart b/cw_evm/lib/evm_chain_hardware_wallet_service.dart index 6f0d11f2e..d8f67c641 100644 --- a/cw_evm/lib/evm_chain_hardware_wallet_service.dart +++ b/cw_evm/lib/evm_chain_hardware_wallet_service.dart @@ -2,26 +2,26 @@ import 'dart:async'; import 'package:cw_core/hardware/hardware_account_data.dart'; import 'package:ledger_ethereum/ledger_ethereum.dart'; -import 'package:ledger_flutter/ledger_flutter.dart'; +import 'package:ledger_flutter_plus/ledger_flutter_plus.dart'; class EVMChainHardwareWalletService { - EVMChainHardwareWalletService(this.ledger, this.device); + EVMChainHardwareWalletService(this.ledgerConnection); - final Ledger ledger; - final LedgerDevice device; + final LedgerConnection ledgerConnection; - Future> getAvailableAccounts({int index = 0, int limit = 5}) async { - final ethereumLedgerApp = EthereumLedgerApp(ledger); + Future> getAvailableAccounts( + {int index = 0, int limit = 5}) async { + final ethereumLedgerApp = EthereumLedgerApp(ledgerConnection); - final version = await ethereumLedgerApp.getVersion(device); + await ethereumLedgerApp.getVersion(); final accounts = []; final indexRange = List.generate(limit, (i) => i + index); for (final i in indexRange) { final derivationPath = "m/44'/60'/$i'/0/0"; - final address = - await ethereumLedgerApp.getAccounts(device, accountsDerivationPath: derivationPath); + final address = await ethereumLedgerApp.getAccounts( + accountsDerivationPath: derivationPath); accounts.add(HardwareAccountData( address: address.first, diff --git a/cw_evm/lib/evm_chain_transaction_credentials.dart b/cw_evm/lib/evm_chain_transaction_credentials.dart index 02927cb4d..5b5bdf170 100644 --- a/cw_evm/lib/evm_chain_transaction_credentials.dart +++ b/cw_evm/lib/evm_chain_transaction_credentials.dart @@ -1,7 +1,6 @@ import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/output_info.dart'; import 'package:cw_evm/evm_chain_transaction_priority.dart'; -import 'package:ledger_flutter/ledger_flutter.dart'; class EVMChainTransactionCredentials { EVMChainTransactionCredentials( diff --git a/cw_evm/lib/evm_chain_wallet.dart b/cw_evm/lib/evm_chain_wallet.dart index 0ade2215e..cfaf39d98 100644 --- a/cw_evm/lib/evm_chain_wallet.dart +++ b/cw_evm/lib/evm_chain_wallet.dart @@ -70,6 +70,7 @@ abstract class EVMChainWalletBase required String password, EVMChainERC20Balance? initialBalance, required this.encryptionFileUtils, + this.passphrase, }) : syncStatus = const NotConnectedSyncStatus(), _password = password, _mnemonic = mnemonic, @@ -178,6 +179,7 @@ abstract class EVMChainWalletBase mnemonic: _mnemonic, privateKey: _hexPrivateKey, password: _password, + passphrase: passphrase, ); walletAddresses.address = _evmChainPrivateKey.address.hexEip55; } @@ -262,7 +264,7 @@ abstract class EVMChainWalletBase } @override - void close() { + Future close({required bool shouldCleanup}) async { _client.stop(); _transactionsUpdateTimer?.cancel(); _updateFeesTimer?.cancel(); @@ -545,6 +547,7 @@ abstract class EVMChainWalletBase 'mnemonic': _mnemonic, 'private_key': privateKey, 'balance': balance[currency]!.toJSON(), + 'passphrase': passphrase, }); Future _updateBalance() async { @@ -574,15 +577,19 @@ abstract class EVMChainWalletBase } } - Future getPrivateKey( - {String? mnemonic, String? privateKey, required String password}) async { + Future getPrivateKey({ + String? mnemonic, + String? privateKey, + required String password, + String? passphrase, + }) async { assert(mnemonic != null || privateKey != null); if (privateKey != null) { return EthPrivateKey.fromHex(privateKey); } - final seed = bip39.mnemonicToSeed(mnemonic!); + final seed = bip39.mnemonicToSeed(mnemonic!, passphrase: passphrase ?? ''); final root = bip32.BIP32.fromSeed(seed); @@ -716,4 +723,7 @@ abstract class EVMChainWalletBase @override String get password => _password; + + @override + final String? passphrase; } diff --git a/cw_evm/lib/evm_chain_wallet_addresses.dart b/cw_evm/lib/evm_chain_wallet_addresses.dart index 4615d79ed..7dd501cc5 100644 --- a/cw_evm/lib/evm_chain_wallet_addresses.dart +++ b/cw_evm/lib/evm_chain_wallet_addresses.dart @@ -17,6 +17,9 @@ abstract class EVMChainWalletAddressesBase extends WalletAddresses with Store { @observable String address; + @override + String get primaryAddress => address; + @override Future init() async { address = walletInfo.address; diff --git a/cw_evm/lib/evm_chain_wallet_creation_credentials.dart b/cw_evm/lib/evm_chain_wallet_creation_credentials.dart index e8a13cbb9..5075e6289 100644 --- a/cw_evm/lib/evm_chain_wallet_creation_credentials.dart +++ b/cw_evm/lib/evm_chain_wallet_creation_credentials.dart @@ -3,17 +3,26 @@ import 'package:cw_core/wallet_credentials.dart'; import 'package:cw_core/wallet_info.dart'; class EVMChainNewWalletCredentials extends WalletCredentials { - EVMChainNewWalletCredentials({required String name, WalletInfo? walletInfo, String? password}) - : super(name: name, walletInfo: walletInfo, password: password); + EVMChainNewWalletCredentials({ + required super.name, + super.walletInfo, + super.password, + super.parentAddress, + this.mnemonic, + super.passphrase, + }); + + final String? mnemonic; } class EVMChainRestoreWalletFromSeedCredentials extends WalletCredentials { EVMChainRestoreWalletFromSeedCredentials({ - required String name, - required String password, + required super.name, + required super.password, required this.mnemonic, - WalletInfo? walletInfo, - }) : super(name: name, password: password, walletInfo: walletInfo); + super.walletInfo, + super.passphrase, + }); final String mnemonic; } diff --git a/cw_evm/lib/evm_ledger_credentials.dart b/cw_evm/lib/evm_ledger_credentials.dart index 0d8de1736..a0b7788dc 100644 --- a/cw_evm/lib/evm_ledger_credentials.dart +++ b/cw_evm/lib/evm_ledger_credentials.dart @@ -1,17 +1,16 @@ import 'dart:async'; import 'dart:typed_data'; -import 'package:cw_core/hardware/device_not_connected_exception.dart'; +import 'package:cw_core/hardware/device_not_connected_exception.dart' + as exception; import 'package:ledger_ethereum/ledger_ethereum.dart'; -import 'package:ledger_flutter/ledger_flutter.dart'; +import 'package:ledger_flutter_plus/ledger_flutter_plus.dart'; import 'package:web3dart/crypto.dart'; import 'package:web3dart/web3dart.dart'; class EvmLedgerCredentials extends CredentialsWithKnownAddress { final String _address; - Ledger? ledger; - LedgerDevice? ledgerDevice; EthereumLedgerApp? ethereumLedgerApp; EvmLedgerCredentials(this._address); @@ -19,25 +18,25 @@ class EvmLedgerCredentials extends CredentialsWithKnownAddress { @override EthereumAddress get address => EthereumAddress.fromHex(_address); - void setLedger(Ledger setLedger, [LedgerDevice? setLedgerDevice, String? derivationPath]) { - ledger = setLedger; - ledgerDevice = setLedgerDevice; - ethereumLedgerApp = - EthereumLedgerApp(ledger!, derivationPath: derivationPath ?? "m/44'/60'/0'/0/0"); + void setLedgerConnection(LedgerConnection connection, + [String? derivationPath]) { + ethereumLedgerApp = EthereumLedgerApp(connection, + derivationPath: derivationPath ?? "m/44'/60'/0'/0/0"); } @override - MsgSignature signToEcSignature(Uint8List payload, {int? chainId, bool isEIP1559 = false}) => - throw UnimplementedError("EvmLedgerCredentials.signToEcSignature"); + MsgSignature signToEcSignature(Uint8List payload, + {int? chainId, bool isEIP1559 = false}) => + throw UnimplementedError("EvmLedgerCredentials.signToEcSignature"); @override Future signToSignature(Uint8List payload, {int? chainId, bool isEIP1559 = false}) async { - if (ledgerDevice == null && ledger?.devices.isNotEmpty != true) { - throw DeviceNotConnectedException(); + if (ethereumLedgerApp == null) { + throw exception.DeviceNotConnectedException(); } - final sig = await ethereumLedgerApp!.signTransaction(device, payload); + final sig = await ethereumLedgerApp!.signTransaction(payload); final v = sig[0].toInt(); final r = bytesToHex(sig.sublist(1, 1 + 32)); @@ -65,14 +64,16 @@ class EvmLedgerCredentials extends CredentialsWithKnownAddress { chainIdV = chainId != null ? (parity + (chainId * 2 + 35)) : parity; } - return MsgSignature(BigInt.parse(r, radix: 16), BigInt.parse(s, radix: 16), chainIdV); + return MsgSignature( + BigInt.parse(r, radix: 16), BigInt.parse(s, radix: 16), chainIdV); } @override - Future signPersonalMessage(Uint8List payload, {int? chainId}) async { - if (isNotConnected) throw DeviceNotConnectedException(); + Future signPersonalMessage(Uint8List payload, + {int? chainId}) async { + if (isNotConnected) throw exception.DeviceNotConnectedException(); - final sig = await ethereumLedgerApp!.signMessage(device, payload); + final sig = await ethereumLedgerApp!.signMessage(payload); final r = sig.sublist(1, 1 + 32); final s = sig.sublist(1 + 32, 1 + 32 + 32); @@ -84,20 +85,22 @@ class EvmLedgerCredentials extends CredentialsWithKnownAddress { @override Uint8List signPersonalMessageToUint8List(Uint8List payload, {int? chainId}) => - throw UnimplementedError("EvmLedgerCredentials.signPersonalMessageToUint8List"); + throw UnimplementedError( + "EvmLedgerCredentials.signPersonalMessageToUint8List"); - Future provideERC20Info(String erc20ContractAddress, int chainId) async { - if (isNotConnected) throw DeviceNotConnectedException(); + Future provideERC20Info( + String erc20ContractAddress, int chainId) async { + if (isNotConnected) throw exception.DeviceNotConnectedException(); try { - await ethereumLedgerApp!.getAndProvideERC20TokenInformation(device, + await ethereumLedgerApp!.getAndProvideERC20TokenInformation( erc20ContractAddress: erc20ContractAddress, chainId: chainId); - } on LedgerException catch (e) { - if (e.errorCode != -28672) rethrow; + } catch (e) { + print(e); + rethrow; + // if (e.errorCode != -28672) rethrow; } } - bool get isNotConnected => (ledgerDevice ?? ledger?.devices.firstOrNull) == null; - - LedgerDevice get device => ledgerDevice ?? ledger!.devices.first; + bool get isNotConnected => ethereumLedgerApp == null || ethereumLedgerApp!.connection.isDisconnected; } diff --git a/cw_evm/pubspec.yaml b/cw_evm/pubspec.yaml index 3e12834b1..326ff4dc9 100644 --- a/cw_evm/pubspec.yaml +++ b/cw_evm/pubspec.yaml @@ -25,20 +25,17 @@ dependencies: mobx: ^2.0.7+4 cw_core: path: ../cw_core - ledger_flutter: ^1.0.1 + ledger_flutter_plus: ^1.4.1 ledger_ethereum: git: - url: https://github.com/cake-tech/ledger-ethereum.git + url: https://github.com/cake-tech/ledger-flutter-plus-plugins + path: packages/ledger-ethereum dependency_overrides: web3dart: git: url: https://github.com/cake-tech/web3dart.git ref: cake - ledger_flutter: - git: - url: https://github.com/cake-tech/ledger-flutter.git - ref: cake-v3 watcher: ^1.1.0 dev_dependencies: diff --git a/cw_haven/android/build.gradle b/cw_haven/android/build.gradle index 87e8df641..d29c31d4e 100644 --- a/cw_haven/android/build.gradle +++ b/cw_haven/android/build.gradle @@ -35,7 +35,6 @@ android { } externalNativeBuild { cmake { - path "CMakeLists.txt" } } } diff --git a/cw_haven/lib/api/exceptions/setup_wallet_exception.dart b/cw_haven/lib/api/exceptions/setup_wallet_exception.dart index b6e0c1f18..9d985665b 100644 --- a/cw_haven/lib/api/exceptions/setup_wallet_exception.dart +++ b/cw_haven/lib/api/exceptions/setup_wallet_exception.dart @@ -2,4 +2,9 @@ class SetupWalletException implements Exception { SetupWalletException({required this.message}); final String message; + + @override + String toString() { + return message; + } } \ No newline at end of file diff --git a/cw_haven/lib/haven_wallet.dart b/cw_haven/lib/haven_wallet.dart index 317d9dc65..06a838100 100644 --- a/cw_haven/lib/haven_wallet.dart +++ b/cw_haven/lib/haven_wallet.dart @@ -106,7 +106,7 @@ abstract class HavenWalletBase Future? updateBalance() => null; @override - void close() { + Future close({required bool shouldCleanup}) async { _listener?.stop(); _onAccountChangeReaction?.reaction.dispose(); _autoSaveTimer?.cancel(); diff --git a/cw_haven/lib/haven_wallet_addresses.dart b/cw_haven/lib/haven_wallet_addresses.dart index eeeb763cf..06de44dff 100644 --- a/cw_haven/lib/haven_wallet_addresses.dart +++ b/cw_haven/lib/haven_wallet_addresses.dart @@ -1,6 +1,7 @@ import 'package:cw_core/wallet_addresses_with_account.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/account.dart'; +import 'package:cw_haven/api/wallet.dart'; import 'package:cw_haven/haven_account_list.dart'; import 'package:cw_haven/haven_subaddress_list.dart'; import 'package:cw_core/subaddress.dart'; @@ -36,7 +37,7 @@ abstract class HavenWalletAddressesBase extends WalletAddressesWithAccount init() async { accountList.update(); - account = accountList.accounts.first; + account = accountList.accounts.isEmpty ? Account(id: 0, label: "Primary address") : accountList.accounts.first; updateSubaddressList(accountIndex: account?.id ?? 0); await updateAddressesInBox(); } @@ -81,8 +82,9 @@ abstract class HavenWalletAddressesBase extends WalletAddressesWithAccount message; } \ No newline at end of file diff --git a/cw_monero/lib/api/exceptions/wallet_restore_from_keys_exception.dart b/cw_monero/lib/api/exceptions/wallet_restore_from_keys_exception.dart index c6b6c6ef7..6c461ee4c 100644 --- a/cw_monero/lib/api/exceptions/wallet_restore_from_keys_exception.dart +++ b/cw_monero/lib/api/exceptions/wallet_restore_from_keys_exception.dart @@ -2,4 +2,7 @@ class WalletRestoreFromKeysException implements Exception { WalletRestoreFromKeysException({required this.message}); final String message; + + @override + String toString() => message; } \ No newline at end of file diff --git a/cw_monero/lib/api/exceptions/wallet_restore_from_seed_exception.dart b/cw_monero/lib/api/exceptions/wallet_restore_from_seed_exception.dart index 004cd7958..904a7e767 100644 --- a/cw_monero/lib/api/exceptions/wallet_restore_from_seed_exception.dart +++ b/cw_monero/lib/api/exceptions/wallet_restore_from_seed_exception.dart @@ -2,4 +2,7 @@ class WalletRestoreFromSeedException implements Exception { WalletRestoreFromSeedException({required this.message}); final String message; + + @override + String toString() => message; } \ No newline at end of file diff --git a/cw_monero/lib/api/subaddress_list.dart b/cw_monero/lib/api/subaddress_list.dart index e5145692d..5db8a5d5f 100644 --- a/cw_monero/lib/api/subaddress_list.dart +++ b/cw_monero/lib/api/subaddress_list.dart @@ -1,5 +1,6 @@ import 'package:cw_monero/api/account_list.dart'; +import 'package:cw_monero/api/transaction_history.dart'; import 'package:cw_monero/api/wallet.dart'; import 'package:monero/monero.dart' as monero; @@ -14,6 +15,10 @@ class SubaddressInfoMetadata { SubaddressInfoMetadata? subaddress = null; +String getRawLabel({required int accountIndex, required int addressIndex}) { + return monero.Wallet_getSubaddressLabel(wptr!, accountIndex: accountIndex, addressIndex: addressIndex); +} + void refreshSubaddresses({required int accountIndex}) { try { isUpdating = true; @@ -29,31 +34,94 @@ class Subaddress { Subaddress({ required this.addressIndex, required this.accountIndex, + required this.received, + required this.txCount, }); - String get address => monero.Wallet_address( - wptr!, - accountIndex: accountIndex, - addressIndex: addressIndex, - ); + late String address = getAddress( + accountIndex: accountIndex, + addressIndex: addressIndex, + ); final int addressIndex; final int accountIndex; - String get label => monero.Wallet_getSubaddressLabel(wptr!, accountIndex: accountIndex, addressIndex: addressIndex); + final int received; + final int txCount; + String get label { + final localLabel = monero.Wallet_getSubaddressLabel(wptr!, accountIndex: accountIndex, addressIndex: addressIndex); + if (localLabel.startsWith("#$addressIndex")) return localLabel; // don't duplicate the ID if it was user-providen + return "#$addressIndex ${localLabel}".trim(); + } } +class TinyTransactionDetails { + TinyTransactionDetails({ + required this.address, + required this.amount, + }); + final List address; + final int amount; +} + +int lastWptr = 0; +int lastTxCount = 0; +List ttDetails = []; + List getAllSubaddresses() { + txhistory = monero.Wallet_history(wptr!); + final txCount = monero.TransactionHistory_count(txhistory!); + if (lastTxCount != txCount && lastWptr != wptr!.address) { + final List newttDetails = []; + lastTxCount = txCount; + lastWptr = wptr!.address; + for (var i = 0; i < txCount; i++) { + final tx = monero.TransactionHistory_transaction(txhistory!, index: i); + if (monero.TransactionInfo_direction(tx) == monero.TransactionInfo_Direction.Out) continue; + final subaddrs = monero.TransactionInfo_subaddrIndex(tx).split(","); + final account = monero.TransactionInfo_subaddrAccount(tx); + newttDetails.add(TinyTransactionDetails( + address: List.generate(subaddrs.length, (index) => getAddress(accountIndex: account, addressIndex: int.tryParse(subaddrs[index])??0)), + amount: monero.TransactionInfo_amount(tx), + )); + } + ttDetails.clear(); + ttDetails.addAll(newttDetails); + } final size = monero.Wallet_numSubaddresses(wptr!, accountIndex: subaddress!.accountIndex); final list = List.generate(size, (index) { + final ttDetailsLocal = ttDetails.where((element) { + final address = getAddress( + accountIndex: subaddress!.accountIndex, + addressIndex: index, + ); + if (element.address.contains(address)) return true; + return false; + }).toList(); + int received = 0; + for (var i = 0; i < ttDetailsLocal.length; i++) { + received += ttDetailsLocal[i].amount; + } return Subaddress( accountIndex: subaddress!.accountIndex, addressIndex: index, + received: received, + txCount: ttDetailsLocal.length, ); }).reversed.toList(); if (list.length == 0) { - list.add(Subaddress(addressIndex: subaddress!.accountIndex, accountIndex: 0)); + list.add( + Subaddress( + addressIndex: subaddress!.accountIndex, + accountIndex: 0, + received: 0, + txCount: 0, + )); } return list; } +int numSubaddresses(int subaccountIndex) { + return monero.Wallet_numSubaddresses(wptr!, accountIndex: subaccountIndex); +} + void addSubaddressSync({required int accountIndex, required String label}) { monero.Wallet_addSubaddress(wptr!, accountIndex: accountIndex, label: label); refreshSubaddresses(accountIndex: accountIndex); diff --git a/cw_monero/lib/api/transaction_history.dart b/cw_monero/lib/api/transaction_history.dart index b416e1b4e..a308b682e 100644 --- a/cw_monero/lib/api/transaction_history.dart +++ b/cw_monero/lib/api/transaction_history.dart @@ -5,32 +5,42 @@ import 'package:cw_monero/api/account_list.dart'; import 'package:cw_monero/api/exceptions/creation_transaction_exception.dart'; import 'package:cw_monero/api/monero_output.dart'; import 'package:cw_monero/api/structs/pending_transaction.dart'; +import 'package:cw_monero/api/wallet.dart'; import 'package:ffi/ffi.dart'; import 'package:monero/monero.dart' as monero; import 'package:monero/src/generated_bindings_monero.g.dart' as monero_gen; +import 'package:mutex/mutex.dart'; String getTxKey(String txId) { return monero.Wallet_getTxKey(wptr!, txid: txId); } - +final txHistoryMutex = Mutex(); monero.TransactionHistory? txhistory; - -void refreshTransactions() { +bool isRefreshingTx = false; +Future refreshTransactions() async { + if (isRefreshingTx == true) return; + isRefreshingTx = true; txhistory ??= monero.Wallet_history(wptr!); - monero.TransactionHistory_refresh(txhistory!); + final ptr = txhistory!.address; + await txHistoryMutex.acquire(); + await Isolate.run(() { + monero.TransactionHistory_refresh(Pointer.fromAddress(ptr)); + }); + txHistoryMutex.release(); + isRefreshingTx = false; } int countOfTransactions() => monero.TransactionHistory_count(txhistory!); -List getAllTransactions() { +Future> getAllTransactions() async { List dummyTxs = []; - + + await txHistoryMutex.acquire(); txhistory ??= monero.Wallet_history(wptr!); - monero.TransactionHistory_refresh(txhistory!); int size = countOfTransactions(); final list = List.generate(size, (index) => Transaction(txInfo: monero.TransactionHistory_transaction(txhistory!, index: index))); - + txHistoryMutex.release(); final accts = monero.Wallet_numSubaddressAccounts(wptr!); for (var i = 0; i < accts; i++) { final fullBalance = monero.Wallet_balance(wptr!, accountIndex: i); @@ -45,6 +55,8 @@ List getAllTransactions() { confirmations: 0, blockheight: 0, accountIndex: i, + addressIndex: 0, + addressIndexList: [0], paymentId: "", amount: fullBalance - availBalance, isSpend: false, @@ -138,11 +150,17 @@ PendingTransactionDescription createTransactionMultDestSync( int accountIndex = 0, List preferredInputs = const []}) { + final dstAddrs = outputs.map((e) => e.address).toList(); + final amounts = outputs.map((e) => monero.Wallet_amountFromString(e.amount)).toList(); + + // print("multDest: dstAddrs: $dstAddrs"); + // print("multDest: amounts: $amounts"); + final txptr = monero.Wallet_createTransactionMultDest( wptr!, - dstAddr: outputs.map((e) => e.address).toList(), + dstAddr: dstAddrs, isSweepAll: false, - amounts: outputs.map((e) => monero.Wallet_amountFromString(e.amount)).toList(), + amounts: amounts, mixinCount: 0, pendingTransactionPriority: priorityRaw, subaddr_account: accountIndex, @@ -245,19 +263,28 @@ Future createTransactionMultDest( class Transaction { final String displayLabel; - String subaddressLabel = monero.Wallet_getSubaddressLabel(wptr!, accountIndex: 0, addressIndex: 0); - late final String address = monero.Wallet_address( + late final String subaddressLabel = monero.Wallet_getSubaddressLabel( wptr!, - accountIndex: 0, - addressIndex: 0, + accountIndex: accountIndex, + addressIndex: addressIndex, ); + late final String address = getAddress( + accountIndex: accountIndex, + addressIndex: addressIndex, + ); + late final List addressList = List.generate(addressIndexList.length, (index) => + getAddress( + accountIndex: accountIndex, + addressIndex: addressIndexList[index], + )); final String description; final int fee; final int confirmations; late final bool isPending = confirmations < 10; final int blockheight; - final int addressIndex = 0; + final int addressIndex; final int accountIndex; + final List addressIndexList; final String paymentId; final int amount; final bool isSpend; @@ -303,11 +330,22 @@ class Transaction { amount = monero.TransactionInfo_amount(txInfo), paymentId = monero.TransactionInfo_paymentId(txInfo), accountIndex = monero.TransactionInfo_subaddrAccount(txInfo), + addressIndex = int.tryParse(monero.TransactionInfo_subaddrIndex(txInfo).split(", ")[0]) ?? 0, + addressIndexList = monero.TransactionInfo_subaddrIndex(txInfo).split(", ").map((e) => int.tryParse(e) ?? 0).toList(), blockheight = monero.TransactionInfo_blockHeight(txInfo), confirmations = monero.TransactionInfo_confirmations(txInfo), fee = monero.TransactionInfo_fee(txInfo), description = monero.TransactionInfo_description(txInfo), - key = monero.Wallet_getTxKey(wptr!, txid: monero.TransactionInfo_hash(txInfo)); + key = getTxKey(txInfo); + + static String getTxKey(monero.TransactionInfo txInfo) { + final txKey = monero.Wallet_getTxKey(wptr!, txid: monero.TransactionInfo_hash(txInfo)); + final status = monero.Wallet_status(wptr!); + if (status != 0) { + return ""; + } + return txKey; + } Transaction.dummy({ required this.displayLabel, @@ -316,6 +354,8 @@ class Transaction { required this.confirmations, required this.blockheight, required this.accountIndex, + required this.addressIndexList, + required this.addressIndex, required this.paymentId, required this.amount, required this.isSpend, diff --git a/cw_monero/lib/api/wallet.dart b/cw_monero/lib/api/wallet.dart index b493e536e..8e03cff3e 100644 --- a/cw_monero/lib/api/wallet.dart +++ b/cw_monero/lib/api/wallet.dart @@ -66,9 +66,20 @@ String getSeedLegacy(String? language) { return legacy; } -String getAddress({int accountIndex = 0, int addressIndex = 0}) => - monero.Wallet_address(wptr!, +Map>> addressCache = {}; + +String getAddress({int accountIndex = 0, int addressIndex = 0}) { + // print("getaddress: ${accountIndex}/${addressIndex}: ${monero.Wallet_numSubaddresses(wptr!, accountIndex: accountIndex)}: ${monero.Wallet_address(wptr!, accountIndex: accountIndex, addressIndex: addressIndex)}"); + while (monero.Wallet_numSubaddresses(wptr!, accountIndex: accountIndex)-1 < addressIndex) { + print("adding subaddress"); + monero.Wallet_addSubaddress(wptr!, accountIndex: accountIndex); + } + addressCache[wptr!.address] ??= {}; + addressCache[wptr!.address]![accountIndex] ??= {}; + addressCache[wptr!.address]![accountIndex]![addressIndex] ??= monero.Wallet_address(wptr!, accountIndex: accountIndex, addressIndex: addressIndex); + return addressCache[wptr!.address]![accountIndex]![addressIndex]!; +} int getFullBalance({int accountIndex = 0}) => monero.Wallet_balance(wptr!, accountIndex: accountIndex); diff --git a/cw_monero/lib/api/wallet_manager.dart b/cw_monero/lib/api/wallet_manager.dart index f06fe3e66..dca7586fb 100644 --- a/cw_monero/lib/api/wallet_manager.dart +++ b/cw_monero/lib/api/wallet_manager.dart @@ -123,7 +123,7 @@ void restoreWalletFromKeysSync( int nettype = 0, int restoreHeight = 0}) { txhistory = null; - final newWptr = spendKey != "" + var newWptr = (spendKey != "") ? monero.WalletManager_createDeterministicWalletFromSpendKey( wmPtr, path: path, @@ -149,6 +149,32 @@ void restoreWalletFromKeysSync( message: monero.Wallet_errorString(newWptr)); } + // CW-712 - Try to restore deterministic wallet first, if the view key doesn't + // match the view key provided + if (spendKey != "") { + final viewKeyRestored = monero.Wallet_secretViewKey(newWptr); + if (viewKey != viewKeyRestored && viewKey != "") { + monero.WalletManager_closeWallet(wmPtr, newWptr, false); + File(path).deleteSync(); + File(path+".keys").deleteSync(); + newWptr = monero.WalletManager_createWalletFromKeys( + wmPtr, + path: path, + password: password, + restoreHeight: restoreHeight, + addressString: address, + viewKeyString: viewKey, + spendKeyString: spendKey, + nettype: 0, + ); + final status = monero.Wallet_status(newWptr); + if (status != 0) { + throw WalletRestoreFromKeysException( + message: monero.Wallet_errorString(newWptr)); + } + } + } + wptr = newWptr; openedWalletsByPath[path] = wptr!; diff --git a/cw_monero/lib/monero_subaddress_list.dart b/cw_monero/lib/monero_subaddress_list.dart index c35afb282..c20b23b5e 100644 --- a/cw_monero/lib/monero_subaddress_list.dart +++ b/cw_monero/lib/monero_subaddress_list.dart @@ -1,6 +1,7 @@ import 'package:cw_core/subaddress.dart'; import 'package:cw_monero/api/coins_info.dart'; import 'package:cw_monero/api/subaddress_list.dart' as subaddress_list; +import 'package:cw_monero/api/wallet.dart'; import 'package:flutter/services.dart'; import 'package:mobx/mobx.dart'; @@ -54,18 +55,12 @@ abstract class MoneroSubaddressListBase with Store { final address = s.address; final label = s.label; final id = s.addressIndex; - final hasDefaultAddressName = - label.toLowerCase() == 'Primary account'.toLowerCase() || - label.toLowerCase() == 'Untitled account'.toLowerCase(); - final isPrimaryAddress = id == 0 && hasDefaultAddressName; return Subaddress( id: id, address: address, - label: isPrimaryAddress - ? 'Primary address' - : hasDefaultAddressName - ? '' - : label); + balance: (s.received/1e12).toStringAsFixed(6), + txCount: s.txCount, + label: label); }).toList(); } @@ -103,6 +98,9 @@ abstract class MoneroSubaddressListBase with Store { required List usedAddresses, }) async { _usedAddresses.addAll(usedAddresses); + final _all = _usedAddresses.toSet().toList(); + _usedAddresses.clear(); + _usedAddresses.addAll(_all); if (_isUpdating) { return; } @@ -124,7 +122,8 @@ abstract class MoneroSubaddressListBase with Store { Future> _getAllUnusedAddresses( {required int accountIndex, required String label}) async { final allAddresses = subaddress_list.getAllSubaddresses(); - if (allAddresses.isEmpty || _usedAddresses.contains(allAddresses.last)) { + // first because addresses come in reversed order. + if (allAddresses.isEmpty || _usedAddresses.contains(allAddresses.first.address)) { final isAddressUnused = await _newSubaddress(accountIndex: accountIndex, label: label); if (!isAddressUnused) { return await _getAllUnusedAddresses(accountIndex: accountIndex, label: label); @@ -139,12 +138,13 @@ abstract class MoneroSubaddressListBase with Store { return Subaddress( id: id, address: address, + balance: (s.received/1e12).toStringAsFixed(6), + txCount: s.txCount, label: id == 0 && label.toLowerCase() == 'Primary account'.toLowerCase() ? 'Primary address' : label); - }) - .toList(); + }).toList().reversed.toList(); } Future _newSubaddress({required int accountIndex, required String label}) async { diff --git a/cw_monero/lib/monero_wallet.dart b/cw_monero/lib/monero_wallet.dart index f5fa0ec7e..0ae2202ba 100644 --- a/cw_monero/lib/monero_wallet.dart +++ b/cw_monero/lib/monero_wallet.dart @@ -13,11 +13,9 @@ import 'package:cw_core/monero_transaction_priority.dart'; import 'package:cw_core/monero_wallet_keys.dart'; import 'package:cw_core/monero_wallet_utils.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_direction.dart'; -import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_info.dart'; @@ -61,7 +59,7 @@ abstract class MoneroWalletBase extends WalletBase isEnabledAutoGenerateSubaddress, (bool enabled) { _updateSubAddress(enabled, account: walletAddresses.account); }); + _onTxHistoryChangeReaction = reaction((_) => transactionHistory, (__) { + _updateSubAddress(isEnabledAutoGenerateSubaddress, account: walletAddresses.account); + }); } static const int _autoSaveInterval = 30; @@ -130,6 +131,7 @@ abstract class MoneroWalletBase extends WalletBase await save()); + // update transaction details after restore + walletAddresses.subaddressList.update(accountIndex: walletAddresses.account?.id??0); } @override Future? updateBalance() => null; @override - void close() async { + Future close({required bool shouldCleanup}) async { _listener?.stop(); _onAccountChangeReaction?.reaction.dispose(); + _onTxHistoryChangeReaction?.reaction.dispose(); _autoSaveTimer?.cancel(); } @@ -580,7 +585,7 @@ abstract class MoneroWalletBase extends WalletBase> fetchTransactions() async { transaction_history.refreshTransactions(); - return _getAllTransactionsOfAccount(walletAddresses.account?.id) + return (await _getAllTransactionsOfAccount(walletAddresses.account?.id)) .fold>( {}, (Map acc, MoneroTransactionInfo tx) { @@ -596,8 +601,8 @@ abstract class MoneroWalletBase extends WalletBase monero_wallet.getSubaddressLabel(accountIndex, addressIndex); - List _getAllTransactionsOfAccount(int? accountIndex) => - transaction_history - .getAllTransactions() + Future> _getAllTransactionsOfAccount(int? accountIndex) async => + (await transaction_history + .getAllTransactions()) .map( (row) => MoneroTransactionInfo( row.hash, diff --git a/cw_monero/lib/monero_wallet_addresses.dart b/cw_monero/lib/monero_wallet_addresses.dart index d4f22e46f..ae78132d3 100644 --- a/cw_monero/lib/monero_wallet_addresses.dart +++ b/cw_monero/lib/monero_wallet_addresses.dart @@ -3,6 +3,8 @@ import 'package:cw_core/address_info.dart'; import 'package:cw_core/subaddress.dart'; import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_info.dart'; +import 'package:cw_monero/api/subaddress_list.dart' as subaddress_list; +import 'package:cw_monero/api/transaction_history.dart'; import 'package:cw_monero/api/wallet.dart'; import 'package:cw_monero/monero_account_list.dart'; import 'package:cw_monero/monero_subaddress_list.dart'; @@ -27,6 +29,33 @@ abstract class MoneroWalletAddressesBase extends WalletAddresses with Store { @observable String address; + @override + String get primaryAddress => getAddress(accountIndex: account?.id ?? 0, addressIndex: 0); + + @override + String get latestAddress { + var addressIndex = subaddress_list.numSubaddresses(account?.id??0) - 1; + var address = getAddress(accountIndex: account?.id??0, addressIndex: addressIndex); + while (hiddenAddresses.contains(address)) { + addressIndex++; + address = getAddress(accountIndex: account?.id??0, addressIndex: addressIndex); + subaddressList.update(accountIndex: account?.id??0); + } + return address; + } + + @override + String get addressForExchange { + var addressIndex = subaddress_list.numSubaddresses(account?.id??0) - 1; + var address = getAddress(accountIndex: account?.id??0, addressIndex: addressIndex); + while (hiddenAddresses.contains(address) || manualAddresses.contains(address) || subaddress_list.getRawLabel(accountIndex: account?.id??0, addressIndex: addressIndex).isNotEmpty) { + addressIndex++; + address = getAddress(accountIndex: account?.id??0, addressIndex: addressIndex); + subaddressList.update(accountIndex: account?.id??0); + } + return address; + } + @observable Account? account; @@ -37,10 +66,12 @@ abstract class MoneroWalletAddressesBase extends WalletAddresses with Store { MoneroAccountList accountList; + Set usedAddresses = Set(); + @override Future init() async { accountList.update(); - account = accountList.accounts.first; + account = accountList.accounts.isEmpty ? Account(id: 0, label: "Primary address") : accountList.accounts.first; updateSubaddressList(accountIndex: account?.id ?? 0); await updateAddressesInBox(); } @@ -89,8 +120,9 @@ abstract class MoneroWalletAddressesBase extends WalletAddresses with Store { void updateSubaddressList({required int accountIndex}) { subaddressList.update(accountIndex: accountIndex); - subaddress = subaddressList.subaddresses.first; - address = subaddress!.address; + address = subaddressList.subaddresses.isNotEmpty + ? subaddressList.subaddresses.first.address + : getAddress(); } Future updateUsedSubaddress() async { @@ -109,7 +141,10 @@ abstract class MoneroWalletAddressesBase extends WalletAddresses with Store { accountIndex: accountIndex, defaultLabel: defaultLabel, usedAddresses: usedAddresses.toList()); - subaddress = (subaddressList.subaddresses.isEmpty) ? Subaddress(id: 0, address: address, label: defaultLabel) : subaddressList.subaddresses.last; + subaddress = (subaddressList.subaddresses.isEmpty) ? Subaddress(id: 0, address: address, label: defaultLabel, balance: '0', txCount: 0) : subaddressList.subaddresses.last; + if (num.tryParse(subaddress!.balance??'0') != 0) { + getAddress(accountIndex: accountIndex, addressIndex: (subaddress?.id??0)+1); + } address = subaddress!.address; } diff --git a/cw_monero/lib/monero_wallet_service.dart b/cw_monero/lib/monero_wallet_service.dart index f9973f430..63ad17583 100644 --- a/cw_monero/lib/monero_wallet_service.dart +++ b/cw_monero/lib/monero_wallet_service.dart @@ -9,11 +9,9 @@ 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_monero/api/exceptions/wallet_opening_exception.dart'; import 'package:cw_monero/api/wallet_manager.dart' as monero_wallet_manager; import 'package:cw_monero/api/wallet_manager.dart'; import 'package:cw_monero/monero_wallet.dart'; -import 'package:flutter/widgets.dart'; import 'package:hive/hive.dart'; import 'package:polyseed/polyseed.dart'; import 'package:monero/monero.dart' as monero; @@ -120,7 +118,6 @@ class MoneroWalletService extends WalletService< @override Future openWallet(String name, String password, {bool? retryOnFailure}) async { - MoneroWallet? wallet; try { final path = await pathForWallet(name: name, type: getType()); @@ -140,48 +137,17 @@ class MoneroWalletService extends WalletService< if (!isValid) { await restoreOrResetWalletFiles(name); - wallet.close(); + wallet.close(shouldCleanup: false); return openWallet(name, password); } await wallet.init(); return wallet; - } catch (e, s) { + } catch (e) { // TODO: Implement Exception for wallet list service. - final bool isBadAlloc = e.toString().contains('bad_alloc') || - (e is WalletOpeningException && - (e.message == 'std::bad_alloc' || e.message.contains('bad_alloc'))); - - final bool doesNotCorrespond = e.toString().contains('does not correspond') || - (e is WalletOpeningException && e.message.contains('does not correspond')); - - final bool isMissingCacheFilesIOS = e.toString().contains('basic_string') || - (e is WalletOpeningException && e.message.contains('basic_string')); - - final bool isMissingCacheFilesAndroid = e.toString().contains('input_stream') || - e.toString().contains('input stream error') || - (e is WalletOpeningException && - (e.message.contains('input_stream') || e.message.contains('input stream error'))); - - final bool invalidSignature = e.toString().contains('invalid signature') || - (e is WalletOpeningException && e.message.contains('invalid signature')); - - final bool invalidPassword = e.toString().contains('invalid password') || - (e is WalletOpeningException && e.message.contains('invalid password')); - - if (!isBadAlloc && - !doesNotCorrespond && - !isMissingCacheFilesIOS && - !isMissingCacheFilesAndroid && - !invalidSignature && - !invalidPassword && - wallet != null && - wallet.onError != null) { - wallet.onError!(FlutterErrorDetails(exception: e, stack: s)); - } - if (invalidPassword || retryOnFailure == false) { + if (retryOnFailure == false) { rethrow; } diff --git a/cw_monero/pubspec.lock b/cw_monero/pubspec.lock index 13c70cfad..ee1d48df1 100644 --- a/cw_monero/pubspec.lock +++ b/cw_monero/pubspec.lock @@ -237,10 +237,10 @@ packages: dependency: transitive description: name: file - sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" url: "https://pub.dev" source: hosted - version: "6.1.4" + version: "7.0.0" fixnum: dependency: transitive description: @@ -463,8 +463,8 @@ packages: dependency: "direct main" description: path: "impls/monero.dart" - ref: bcb328a4956105dc182afd0ce2e48fe263f5f20b - resolved-ref: bcb328a4956105dc182afd0ce2e48fe263f5f20b + ref: "6eb571ea498ed7b854934785f00fabfd0dadf75b" + resolved-ref: "6eb571ea498ed7b854934785f00fabfd0dadf75b" url: "https://github.com/mrcyjanek/monero_c" source: git version: "0.0.0" diff --git a/cw_monero/pubspec.yaml b/cw_monero/pubspec.yaml index 3053977de..cb1f5519f 100644 --- a/cw_monero/pubspec.yaml +++ b/cw_monero/pubspec.yaml @@ -25,7 +25,7 @@ dependencies: monero: git: url: https://github.com/mrcyjanek/monero_c - ref: bcb328a4956105dc182afd0ce2e48fe263f5f20b # monero_c hash + ref: 6eb571ea498ed7b854934785f00fabfd0dadf75b # monero_c hash path: impls/monero.dart mutex: ^3.1.0 diff --git a/cw_mweb/.gitignore b/cw_mweb/.gitignore new file mode 100644 index 000000000..96486fd93 --- /dev/null +++ b/cw_mweb/.gitignore @@ -0,0 +1,30 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ diff --git a/cw_mweb/.metadata b/cw_mweb/.metadata new file mode 100644 index 000000000..606303914 --- /dev/null +++ b/cw_mweb/.metadata @@ -0,0 +1,36 @@ +# 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. + +version: + revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + channel: stable + +project_type: plugin + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + - platform: android + create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + - platform: ios + create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + - platform: macos + create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/cw_mweb/CHANGELOG.md b/cw_mweb/CHANGELOG.md new file mode 100644 index 000000000..41cc7d819 --- /dev/null +++ b/cw_mweb/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/cw_mweb/LICENSE b/cw_mweb/LICENSE new file mode 100644 index 000000000..ba75c69f7 --- /dev/null +++ b/cw_mweb/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/cw_mweb/README.md b/cw_mweb/README.md new file mode 100644 index 000000000..8a839b1ec --- /dev/null +++ b/cw_mweb/README.md @@ -0,0 +1,15 @@ +# cw_mweb + +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 development, view the +[online documentation](https://flutter.dev/docs), which offers tutorials, +samples, guidance on mobile development, and a full API reference. + diff --git a/cw_mweb/analysis_options.yaml b/cw_mweb/analysis_options.yaml new file mode 100644 index 000000000..a5744c1cf --- /dev/null +++ b/cw_mweb/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/cw_mweb/android/.gitignore b/cw_mweb/android/.gitignore new file mode 100644 index 000000000..881f3d95c --- /dev/null +++ b/cw_mweb/android/.gitignore @@ -0,0 +1,10 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures +/libs +.cxx diff --git a/cw_mweb/android/build.gradle b/cw_mweb/android/build.gradle new file mode 100644 index 000000000..7e67b98ad --- /dev/null +++ b/cw_mweb/android/build.gradle @@ -0,0 +1,76 @@ +group 'com.cakewallet.mweb' +version '1.0-SNAPSHOT' + +buildscript { + ext.kotlin_version = '1.7.10' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:7.3.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.allprojects { + repositories { + flatDir { + dirs project(':cw_mweb').file('libs') + } + } +} + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion 31 + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + test.java.srcDirs += 'src/test/kotlin' + } + + defaultConfig { + minSdkVersion 16 + } + + dependencies { + testImplementation 'org.jetbrains.kotlin:kotlin-test' + testImplementation 'org.mockito:mockito-core:5.0.0' + } + + testOptions { + unitTests.all { + useJUnitPlatform() + + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } + } +} + +dependencies { + implementation (name: 'mwebd', ext: 'aar') +} diff --git a/cw_mweb/android/settings.gradle b/cw_mweb/android/settings.gradle new file mode 100644 index 000000000..88fbd66fb --- /dev/null +++ b/cw_mweb/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'cw_mweb' diff --git a/cw_mweb/android/src/main/AndroidManifest.xml b/cw_mweb/android/src/main/AndroidManifest.xml new file mode 100644 index 000000000..fd3746a8c --- /dev/null +++ b/cw_mweb/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + diff --git a/cw_mweb/android/src/main/kotlin/com/cakewallet/mweb/CwMwebPlugin.kt b/cw_mweb/android/src/main/kotlin/com/cakewallet/mweb/CwMwebPlugin.kt new file mode 100644 index 000000000..3ab24ee12 --- /dev/null +++ b/cw_mweb/android/src/main/kotlin/com/cakewallet/mweb/CwMwebPlugin.kt @@ -0,0 +1,66 @@ +package com.cakewallet.mweb + +import androidx.annotation.NonNull + +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.MethodCallHandler +import io.flutter.plugin.common.MethodChannel.Result + +import mwebd.Mwebd +import mwebd.Server + +/** CwMwebPlugin */ +class CwMwebPlugin: FlutterPlugin, MethodCallHandler { + /// The MethodChannel that will the communication between Flutter and native Android + /// + /// This local reference serves to register the plugin with the Flutter Engine and unregister it + /// when the Flutter Engine is detached from the Activity + private lateinit var channel : MethodChannel + private var server: Server? = null + private var port: Long? = null + + override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + channel = MethodChannel(flutterPluginBinding.binaryMessenger, "cw_mweb") + channel.setMethodCallHandler(this) + } + + override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { + if (call.method == "start") { + server?.stop() + val dataDir = call.argument("dataDir") ?: "" + val nodeUri = call.argument("nodeUri") ?: "" + server = server ?: Mwebd.newServer("", dataDir, nodeUri) + port = server?.start(0) + result.success(port) + } else if (call.method == "stop") { + server?.stop() + server = null + port = null + result.success(null) + } else if (call.method == "address") { + // val scanSecret: ByteArray = call.argument("scanSecret") ?: ByteArray(0) + // val spendPub: ByteArray = call.argument("spendPub") ?: ByteArray(0) + // val index: Int = call.argument("index") ?: 0 + // val res = Mwebd.address(scanSecret, spendPub, index) + // result.success(res) + } else if (call.method == "addresses") { + val scanSecret: ByteArray = call.argument("scanSecret") ?: ByteArray(0) + val spendPub: ByteArray = call.argument("spendPub") ?: ByteArray(0) + val fromIndex: Int = call.argument("fromIndex") ?: 0 + val toIndex: Int = call.argument("toIndex") ?: 0 + val res = Mwebd.addresses(scanSecret, spendPub, fromIndex, toIndex) + result.success(res) + } else { + result.notImplemented() + } + } + + override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { + channel.setMethodCallHandler(null) + server?.stop() + server = null + port = null + } +} diff --git a/cw_mweb/ios/.gitignore b/cw_mweb/ios/.gitignore new file mode 100644 index 000000000..0c885071e --- /dev/null +++ b/cw_mweb/ios/.gitignore @@ -0,0 +1,38 @@ +.idea/ +.vagrant/ +.sconsign.dblite +.svn/ + +.DS_Store +*.swp +profile + +DerivedData/ +build/ +GeneratedPluginRegistrant.h +GeneratedPluginRegistrant.m + +.generated/ + +*.pbxuser +*.mode1v3 +*.mode2v3 +*.perspectivev3 + +!default.pbxuser +!default.mode1v3 +!default.mode2v3 +!default.perspectivev3 + +xcuserdata + +*.moved-aside + +*.pyc +*sync/ +Icon? +.tags* + +/Flutter/Generated.xcconfig +/Flutter/ephemeral/ +/Flutter/flutter_export_environment.sh \ No newline at end of file diff --git a/cw_mweb/ios/Assets/.gitkeep b/cw_mweb/ios/Assets/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/cw_mweb/ios/Classes/CwMwebPlugin.swift b/cw_mweb/ios/Classes/CwMwebPlugin.swift new file mode 100644 index 000000000..55b380829 --- /dev/null +++ b/cw_mweb/ios/Classes/CwMwebPlugin.swift @@ -0,0 +1,100 @@ +import Flutter +import UIKit +import Mwebd + +public class CwMwebPlugin: NSObject, FlutterPlugin { +public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel(name: "cw_mweb", binaryMessenger: registrar.messenger()) + let instance = CwMwebPlugin() + registrar.addMethodCallDelegate(instance, channel: channel) + } + + private static var server: MwebdServer? + private static var port: Int = 0 + private static var dataDir: String? + private static var nodeUri: String? + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + switch call.method { + case "getPlatformVersion": + result("iOS " + UIDevice.current.systemVersion) + break + case "start": + stopServer() + let args = call.arguments as? [String: String] + let dataDir = args?["dataDir"] + let nodeUri = args?["nodeUri"] + CwMwebPlugin.dataDir = dataDir + CwMwebPlugin.nodeUri = nodeUri + startServer(result: result) + break + case "stop": + stopServer() + result(nil) + break + // case "address": + // let args = call.arguments as! [String: Any] + // let scanSecret = args["scanSecret"] as! FlutterStandardTypedData + // let spendPub = args["spendPub"] as! FlutterStandardTypedData + // let index = args["index"] as! Int32 + + // let scanSecretData = scanSecret.data + // let spendPubData = spendPub.data + // result(MwebdAddress(scanSecretData, spendPubData, index)) + // break + case "addresses": + let args = call.arguments as! [String: Any] + let scanSecret = args["scanSecret"] as! FlutterStandardTypedData + let spendPub = args["spendPub"] as! FlutterStandardTypedData + let fromIndex = args["fromIndex"] as! Int32 + let toIndex = args["toIndex"] as! Int32 + + let scanSecretData = scanSecret.data + let spendPubData = spendPub.data + result(MwebdAddresses(scanSecretData, spendPubData, fromIndex, toIndex)) + break + default: + result(FlutterMethodNotImplemented) + break + } + } + + private func startServer(result: @escaping FlutterResult) { + if CwMwebPlugin.server == nil { + var error: NSError? + CwMwebPlugin.server = MwebdNewServer("", CwMwebPlugin.dataDir, CwMwebPlugin.nodeUri, &error) + + if let server = CwMwebPlugin.server { + do { + print("Starting server...") + try server.start(0, ret0_: &CwMwebPlugin.port) + print("Server started successfully on port: \(CwMwebPlugin.port)") + result(CwMwebPlugin.port) + } catch let startError as NSError { + print("Server Start Error: \(startError.localizedDescription)") + result(FlutterError(code: "Server Start Error", message: startError.localizedDescription, details: nil)) + } + } else if let error = error { + print("Server Creation Error: \(error.localizedDescription)") + result(FlutterError(code: "Server Creation Error", message: error.localizedDescription, details: nil)) + } else { + print("Unknown Error: Failed to create server") + result(FlutterError(code: "Unknown Error", message: "Failed to create server", details: nil)) + } + } else { + print("Server already running on port: \(CwMwebPlugin.port)") + result(CwMwebPlugin.port) + } + } + + private func stopServer() { + print("Stopping server") + CwMwebPlugin.server?.stop() + CwMwebPlugin.server = nil + CwMwebPlugin.port = 0 + } + + deinit { + stopServer() + } +} diff --git a/cw_mweb/ios/cw_mweb.podspec b/cw_mweb/ios/cw_mweb.podspec new file mode 100644 index 000000000..4a1903bae --- /dev/null +++ b/cw_mweb/ios/cw_mweb.podspec @@ -0,0 +1,26 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint cw_mweb.podspec` to validate before publishing. +# +Pod::Spec.new do |s| + s.name = 'cw_mweb' + s.version = '0.0.1' + s.summary = 'A new Flutter plugin project.' + s.description = <<-DESC +A new Flutter plugin project. + DESC + s.homepage = 'http://example.com' + s.license = { :file => '../LICENSE' } + s.author = { 'Your Company' => 'email@example.com' } + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.dependency 'Flutter' + s.platform = :ios, '11.0' + + # 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.preserve_paths = 'Mwebd.xcframework/**/*' + +end diff --git a/cw_mweb/lib/cw_mweb.dart b/cw_mweb/lib/cw_mweb.dart new file mode 100644 index 000000000..a1a592fb8 --- /dev/null +++ b/cw_mweb/lib/cw_mweb.dart @@ -0,0 +1,200 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:developer'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:grpc/grpc.dart'; +import 'package:path_provider/path_provider.dart'; +import 'cw_mweb_platform_interface.dart'; +import 'mwebd.pbgrpc.dart'; + +class CwMweb { + static RpcClient? _rpcClient; + static ClientChannel? _clientChannel; + static int? _port; + static const TIMEOUT_DURATION = Duration(seconds: 5); + static Timer? logTimer; + + static void readFileWithTimer(String filePath) { + final file = File(filePath); + int lastLength = 0; + + logTimer?.cancel(); + logTimer = Timer.periodic(const Duration(seconds: 1), (timer) async { + try { + final currentLength = await file.length(); + + if (currentLength != lastLength) { + final fileStream = file.openRead(lastLength, currentLength); + final newLines = await fileStream.transform(utf8.decoder).join(); + lastLength = currentLength; + log(newLines); + } + } on GrpcError catch (e) { + log('Caught grpc error: ${e.message}'); + } catch (e) { + log('The mwebd debug log probably is not initialized yet.'); + } + }); + } + + static Future _initializeClient() async { + print("_initializeClient() called!"); + final appDir = await getApplicationSupportDirectory(); + const ltcNodeUri = "ltc-electrum.cakewallet.com:9333"; + + String debugLogPath = "${appDir.path}/logs/debug.log"; + readFileWithTimer(debugLogPath); + + _port = await CwMwebPlatform.instance.start(appDir.path, ltcNodeUri); + if (_port == null || _port == 0) { + throw Exception("Failed to start server"); + } + log("Attempting to connect to server on port: $_port"); + + // wait for the server to finish starting up before we try to connect to it: + await Future.delayed(const Duration(seconds: 8)); + + _clientChannel = ClientChannel('127.0.0.1', port: _port!, channelShutdownHandler: () { + _rpcClient = null; + log("Channel is shutting down!"); + }, + options: const ChannelOptions( + credentials: ChannelCredentials.insecure(), + keepAlive: ClientKeepAliveOptions(permitWithoutCalls: true), + )); + _rpcClient = RpcClient(_clientChannel!); + } + + static Future stub({int maxRetries = 3}) async { + for (int i = 0; i < maxRetries; i++) { + try { + if (_rpcClient == null) { + await _initializeClient(); + } + final status = await _rpcClient! + .status(StatusRequest(), options: CallOptions(timeout: TIMEOUT_DURATION)); + if (status.blockTime == 0) { + throw Exception("blockTime shouldn't be 0! (this connection is likely broken)"); + } + return _rpcClient!; + } on GrpcError catch (e) { + log("Attempt $i failed: $e"); + log('Caught grpc error: ${e.message}'); + _rpcClient = null; + // necessary if the database isn't open: + await stop(); + await Future.delayed(const Duration(seconds: 3)); + } catch (e) { + log("Attempt $i failed: $e"); + _rpcClient = null; + await stop(); + await Future.delayed(const Duration(seconds: 3)); + } + } + throw Exception("Failed to connect after $maxRetries attempts"); + } + + static Future stop() async { + try { + await CwMwebPlatform.instance.stop(); + await cleanup(); + } on GrpcError catch (e) { + log('Caught grpc error: ${e.message}'); + } catch (e) { + log("Error stopping server: $e"); + } + } + + static Future address(Uint8List scanSecret, Uint8List spendPub, int index) async { + try { + return (await CwMwebPlatform.instance.addresses(scanSecret, spendPub, index, index + 1)) + ?.split(',') + .first; + } on GrpcError catch (e) { + log('Caught grpc error: ${e.message}'); + } catch (e) { + log("Error getting address: $e"); + } + return null; + } + + static Future?> addresses( + Uint8List scanSecret, Uint8List spendPub, int fromIndex, int toIndex) async { + try { + return (await CwMwebPlatform.instance.addresses(scanSecret, spendPub, fromIndex, toIndex)) + ?.split(','); + } on GrpcError catch (e) { + log('Caught grpc error: ${e.message}'); + } catch (e) { + log("Error getting addresses: $e"); + } + return null; + } + + static Future cleanup() async { + try { + await _clientChannel?.terminate(); + } catch (_) {} + _rpcClient = null; + _clientChannel = null; + _port = null; + } + + // wrappers that handle the connection issues: + static Future spent(SpentRequest request) async { + log("mweb.spent() called"); + try { + _rpcClient = await stub(); + return await _rpcClient!.spent(request, options: CallOptions(timeout: TIMEOUT_DURATION)); + } on GrpcError catch (e) { + log('Caught grpc error: ${e.message}'); + } catch (e) { + log("Error getting spent: $e"); + } + return SpentResponse(); + } + + static Future status(StatusRequest request) async { + log("mweb.status() called"); + try { + _rpcClient = await stub(); + return await _rpcClient!.status(request, options: CallOptions(timeout: TIMEOUT_DURATION)); + } on GrpcError catch (e) { + log('Caught grpc error: ${e.message}'); + } catch (e) { + log("Error getting status: $e"); + } + return StatusResponse(); + } + + static Future create(CreateRequest request) async { + log("mweb.create() called"); + try { + _rpcClient = await stub(); + return await _rpcClient!.create(request, options: CallOptions(timeout: TIMEOUT_DURATION)); + } on GrpcError catch (e) { + log('Caught grpc error: ${e.message}'); + } catch (e) { + log("Error getting create: $e"); + } + return CreateResponse(); + } + + static Future?> utxos(UtxosRequest request) async { + log("mweb.utxos() called"); + try { + _rpcClient = await stub(); + final resp = _rpcClient! + .utxos(request, options: CallOptions(timeout: const Duration(days: 1000 * 365))); + log("got utxo stream"); + return resp; + } on GrpcError catch (e) { + log('Caught grpc error: ${e.message}'); + } catch (e) { + log("Error getting utxos: $e"); + } + return null; + } +} diff --git a/cw_mweb/lib/cw_mweb_method_channel.dart b/cw_mweb/lib/cw_mweb_method_channel.dart new file mode 100644 index 000000000..1aeb1939e --- /dev/null +++ b/cw_mweb/lib/cw_mweb_method_channel.dart @@ -0,0 +1,58 @@ +import 'dart:io' show Platform; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; + +import 'cw_mweb_platform_interface.dart'; + +/// An implementation of [CwMwebPlatform] that uses method channels. +class MethodChannelCwMweb extends CwMwebPlatform { + /// The method channel used to interact with the native platform. + @visibleForTesting + final methodChannel = const MethodChannel('cw_mweb'); + + @override + Future start(String dataDir, String nodeUri) async { + if (Platform.isLinux || Platform.isMacOS || Platform.isWindows) { + return null; + } + final result = + await methodChannel.invokeMethod('start', {'dataDir': dataDir, 'nodeUri': nodeUri}); + return result; + } + + @override + Future stop() async { + if (Platform.isLinux || Platform.isMacOS || Platform.isWindows) { + return; + } + await methodChannel.invokeMethod('stop'); + } + + @override + Future address(Uint8List scanSecret, Uint8List spendPub, int index) async { + if (Platform.isLinux || Platform.isMacOS || Platform.isWindows) { + return null; + } + final result = await methodChannel.invokeMethod('address', { + 'scanSecret': scanSecret, + 'spendPub': spendPub, + 'index': index, + }); + return result; + } + + @override + Future addresses(Uint8List scanSecret, Uint8List spendPub, int fromIndex, int toIndex) async { + if (Platform.isLinux || Platform.isMacOS || Platform.isWindows) { + return null; + } + final result = await methodChannel.invokeMethod('addresses', { + 'scanSecret': scanSecret, + 'spendPub': spendPub, + 'fromIndex': fromIndex, + 'toIndex': toIndex, + }); + return result; + } +} diff --git a/cw_mweb/lib/cw_mweb_platform_interface.dart b/cw_mweb/lib/cw_mweb_platform_interface.dart new file mode 100644 index 000000000..6ec22bf56 --- /dev/null +++ b/cw_mweb/lib/cw_mweb_platform_interface.dart @@ -0,0 +1,43 @@ +import 'dart:typed_data'; + +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +import 'cw_mweb_method_channel.dart'; + +abstract class CwMwebPlatform extends PlatformInterface { + /// Constructs a CwMwebPlatform. + CwMwebPlatform() : super(token: _token); + + static final Object _token = Object(); + + static CwMwebPlatform _instance = MethodChannelCwMweb(); + + /// The default instance of [CwMwebPlatform] to use. + /// + /// Defaults to [MethodChannelCwMweb]. + static CwMwebPlatform get instance => _instance; + + /// Platform-specific implementations should set this with their own + /// platform-specific class that extends [CwMwebPlatform] when + /// they register themselves. + static set instance(CwMwebPlatform instance) { + PlatformInterface.verifyToken(instance, _token); + _instance = instance; + } + + Future start(String dataDir, String nodeUri) { + throw UnimplementedError('start() has not been implemented.'); + } + + Future stop() { + throw UnimplementedError('stop() has not been implemented.'); + } + + Future address(Uint8List scanSecret, Uint8List spendPub, int index) { + throw UnimplementedError('address(int) has not been implemented.'); + } + + Future addresses(Uint8List scanSecret, Uint8List spendPub, int fromIndex, int toIndex) { + throw UnimplementedError('addresses has not been implemented.'); + } +} diff --git a/cw_mweb/lib/mwebd.pb.dart b/cw_mweb/lib/mwebd.pb.dart new file mode 100644 index 000000000..d0dd486c0 --- /dev/null +++ b/cw_mweb/lib/mwebd.pb.dart @@ -0,0 +1,801 @@ +// +// Generated code. Do not modify. +// source: mwebd.proto +// +// @dart = 2.12 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names, library_prefixes +// ignore_for_file: non_constant_identifier_names, prefer_final_fields +// ignore_for_file: unnecessary_import, unnecessary_this, unused_import + +import 'dart:core' as $core; + +import 'package:fixnum/fixnum.dart' as $fixnum; +import 'package:protobuf/protobuf.dart' as $pb; + +class StatusRequest extends $pb.GeneratedMessage { + factory StatusRequest() => create(); + StatusRequest._() : super(); + factory StatusRequest.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory StatusRequest.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'StatusRequest', createEmptyInstance: create) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + StatusRequest clone() => StatusRequest()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + StatusRequest copyWith(void Function(StatusRequest) updates) => super.copyWith((message) => updates(message as StatusRequest)) as StatusRequest; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static StatusRequest create() => StatusRequest._(); + StatusRequest createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static StatusRequest getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static StatusRequest? _defaultInstance; +} + +class StatusResponse extends $pb.GeneratedMessage { + factory StatusResponse({ + $core.int? blockHeaderHeight, + $core.int? mwebHeaderHeight, + $core.int? mwebUtxosHeight, + $core.int? blockTime, + }) { + final $result = create(); + if (blockHeaderHeight != null) { + $result.blockHeaderHeight = blockHeaderHeight; + } + if (mwebHeaderHeight != null) { + $result.mwebHeaderHeight = mwebHeaderHeight; + } + if (mwebUtxosHeight != null) { + $result.mwebUtxosHeight = mwebUtxosHeight; + } + if (blockTime != null) { + $result.blockTime = blockTime; + } + return $result; + } + StatusResponse._() : super(); + factory StatusResponse.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory StatusResponse.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'StatusResponse', createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'blockHeaderHeight', $pb.PbFieldType.O3) + ..a<$core.int>(2, _omitFieldNames ? '' : 'mwebHeaderHeight', $pb.PbFieldType.O3) + ..a<$core.int>(3, _omitFieldNames ? '' : 'mwebUtxosHeight', $pb.PbFieldType.O3) + ..a<$core.int>(4, _omitFieldNames ? '' : 'blockTime', $pb.PbFieldType.OU3) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + StatusResponse clone() => StatusResponse()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + StatusResponse copyWith(void Function(StatusResponse) updates) => super.copyWith((message) => updates(message as StatusResponse)) as StatusResponse; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static StatusResponse create() => StatusResponse._(); + StatusResponse createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static StatusResponse getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static StatusResponse? _defaultInstance; + + @$pb.TagNumber(1) + $core.int get blockHeaderHeight => $_getIZ(0); + @$pb.TagNumber(1) + set blockHeaderHeight($core.int v) { $_setSignedInt32(0, v); } + @$pb.TagNumber(1) + $core.bool hasBlockHeaderHeight() => $_has(0); + @$pb.TagNumber(1) + void clearBlockHeaderHeight() => clearField(1); + + @$pb.TagNumber(2) + $core.int get mwebHeaderHeight => $_getIZ(1); + @$pb.TagNumber(2) + set mwebHeaderHeight($core.int v) { $_setSignedInt32(1, v); } + @$pb.TagNumber(2) + $core.bool hasMwebHeaderHeight() => $_has(1); + @$pb.TagNumber(2) + void clearMwebHeaderHeight() => clearField(2); + + @$pb.TagNumber(3) + $core.int get mwebUtxosHeight => $_getIZ(2); + @$pb.TagNumber(3) + set mwebUtxosHeight($core.int v) { $_setSignedInt32(2, v); } + @$pb.TagNumber(3) + $core.bool hasMwebUtxosHeight() => $_has(2); + @$pb.TagNumber(3) + void clearMwebUtxosHeight() => clearField(3); + + @$pb.TagNumber(4) + $core.int get blockTime => $_getIZ(3); + @$pb.TagNumber(4) + set blockTime($core.int v) { $_setUnsignedInt32(3, v); } + @$pb.TagNumber(4) + $core.bool hasBlockTime() => $_has(3); + @$pb.TagNumber(4) + void clearBlockTime() => clearField(4); +} + +class UtxosRequest extends $pb.GeneratedMessage { + factory UtxosRequest({ + $core.int? fromHeight, + $core.List<$core.int>? scanSecret, + }) { + final $result = create(); + if (fromHeight != null) { + $result.fromHeight = fromHeight; + } + if (scanSecret != null) { + $result.scanSecret = scanSecret; + } + return $result; + } + UtxosRequest._() : super(); + factory UtxosRequest.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory UtxosRequest.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'UtxosRequest', createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'fromHeight', $pb.PbFieldType.O3) + ..a<$core.List<$core.int>>(2, _omitFieldNames ? '' : 'scanSecret', $pb.PbFieldType.OY) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + UtxosRequest clone() => UtxosRequest()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + UtxosRequest copyWith(void Function(UtxosRequest) updates) => super.copyWith((message) => updates(message as UtxosRequest)) as UtxosRequest; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static UtxosRequest create() => UtxosRequest._(); + UtxosRequest createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static UtxosRequest getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static UtxosRequest? _defaultInstance; + + @$pb.TagNumber(1) + $core.int get fromHeight => $_getIZ(0); + @$pb.TagNumber(1) + set fromHeight($core.int v) { $_setSignedInt32(0, v); } + @$pb.TagNumber(1) + $core.bool hasFromHeight() => $_has(0); + @$pb.TagNumber(1) + void clearFromHeight() => clearField(1); + + @$pb.TagNumber(2) + $core.List<$core.int> get scanSecret => $_getN(1); + @$pb.TagNumber(2) + set scanSecret($core.List<$core.int> v) { $_setBytes(1, v); } + @$pb.TagNumber(2) + $core.bool hasScanSecret() => $_has(1); + @$pb.TagNumber(2) + void clearScanSecret() => clearField(2); +} + +class Utxo extends $pb.GeneratedMessage { + factory Utxo({ + $core.int? height, + $fixnum.Int64? value, + $core.String? address, + $core.String? outputId, + $core.int? blockTime, + }) { + final $result = create(); + if (height != null) { + $result.height = height; + } + if (value != null) { + $result.value = value; + } + if (address != null) { + $result.address = address; + } + if (outputId != null) { + $result.outputId = outputId; + } + if (blockTime != null) { + $result.blockTime = blockTime; + } + return $result; + } + Utxo._() : super(); + factory Utxo.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory Utxo.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Utxo', createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'height', $pb.PbFieldType.O3) + ..a<$fixnum.Int64>(2, _omitFieldNames ? '' : 'value', $pb.PbFieldType.OU6, defaultOrMaker: $fixnum.Int64.ZERO) + ..aOS(3, _omitFieldNames ? '' : 'address') + ..aOS(4, _omitFieldNames ? '' : 'outputId') + ..a<$core.int>(5, _omitFieldNames ? '' : 'blockTime', $pb.PbFieldType.OU3) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + Utxo clone() => Utxo()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Utxo copyWith(void Function(Utxo) updates) => super.copyWith((message) => updates(message as Utxo)) as Utxo; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Utxo create() => Utxo._(); + Utxo createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Utxo getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Utxo? _defaultInstance; + + @$pb.TagNumber(1) + $core.int get height => $_getIZ(0); + @$pb.TagNumber(1) + set height($core.int v) { $_setSignedInt32(0, v); } + @$pb.TagNumber(1) + $core.bool hasHeight() => $_has(0); + @$pb.TagNumber(1) + void clearHeight() => clearField(1); + + @$pb.TagNumber(2) + $fixnum.Int64 get value => $_getI64(1); + @$pb.TagNumber(2) + set value($fixnum.Int64 v) { $_setInt64(1, v); } + @$pb.TagNumber(2) + $core.bool hasValue() => $_has(1); + @$pb.TagNumber(2) + void clearValue() => clearField(2); + + @$pb.TagNumber(3) + $core.String get address => $_getSZ(2); + @$pb.TagNumber(3) + set address($core.String v) { $_setString(2, v); } + @$pb.TagNumber(3) + $core.bool hasAddress() => $_has(2); + @$pb.TagNumber(3) + void clearAddress() => clearField(3); + + @$pb.TagNumber(4) + $core.String get outputId => $_getSZ(3); + @$pb.TagNumber(4) + set outputId($core.String v) { $_setString(3, v); } + @$pb.TagNumber(4) + $core.bool hasOutputId() => $_has(3); + @$pb.TagNumber(4) + void clearOutputId() => clearField(4); + + @$pb.TagNumber(5) + $core.int get blockTime => $_getIZ(4); + @$pb.TagNumber(5) + set blockTime($core.int v) { $_setUnsignedInt32(4, v); } + @$pb.TagNumber(5) + $core.bool hasBlockTime() => $_has(4); + @$pb.TagNumber(5) + void clearBlockTime() => clearField(5); +} + +class AddressRequest extends $pb.GeneratedMessage { + factory AddressRequest({ + $core.int? fromIndex, + $core.int? toIndex, + $core.List<$core.int>? scanSecret, + $core.List<$core.int>? spendPubkey, + }) { + final $result = create(); + if (fromIndex != null) { + $result.fromIndex = fromIndex; + } + if (toIndex != null) { + $result.toIndex = toIndex; + } + if (scanSecret != null) { + $result.scanSecret = scanSecret; + } + if (spendPubkey != null) { + $result.spendPubkey = spendPubkey; + } + return $result; + } + AddressRequest._() : super(); + factory AddressRequest.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory AddressRequest.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'AddressRequest', createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'fromIndex', $pb.PbFieldType.OU3) + ..a<$core.int>(2, _omitFieldNames ? '' : 'toIndex', $pb.PbFieldType.OU3) + ..a<$core.List<$core.int>>(3, _omitFieldNames ? '' : 'scanSecret', $pb.PbFieldType.OY) + ..a<$core.List<$core.int>>(4, _omitFieldNames ? '' : 'spendPubkey', $pb.PbFieldType.OY) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + AddressRequest clone() => AddressRequest()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + AddressRequest copyWith(void Function(AddressRequest) updates) => super.copyWith((message) => updates(message as AddressRequest)) as AddressRequest; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static AddressRequest create() => AddressRequest._(); + AddressRequest createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static AddressRequest getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static AddressRequest? _defaultInstance; + + @$pb.TagNumber(1) + $core.int get fromIndex => $_getIZ(0); + @$pb.TagNumber(1) + set fromIndex($core.int v) { $_setUnsignedInt32(0, v); } + @$pb.TagNumber(1) + $core.bool hasFromIndex() => $_has(0); + @$pb.TagNumber(1) + void clearFromIndex() => clearField(1); + + @$pb.TagNumber(2) + $core.int get toIndex => $_getIZ(1); + @$pb.TagNumber(2) + set toIndex($core.int v) { $_setUnsignedInt32(1, v); } + @$pb.TagNumber(2) + $core.bool hasToIndex() => $_has(1); + @$pb.TagNumber(2) + void clearToIndex() => clearField(2); + + @$pb.TagNumber(3) + $core.List<$core.int> get scanSecret => $_getN(2); + @$pb.TagNumber(3) + set scanSecret($core.List<$core.int> v) { $_setBytes(2, v); } + @$pb.TagNumber(3) + $core.bool hasScanSecret() => $_has(2); + @$pb.TagNumber(3) + void clearScanSecret() => clearField(3); + + @$pb.TagNumber(4) + $core.List<$core.int> get spendPubkey => $_getN(3); + @$pb.TagNumber(4) + set spendPubkey($core.List<$core.int> v) { $_setBytes(3, v); } + @$pb.TagNumber(4) + $core.bool hasSpendPubkey() => $_has(3); + @$pb.TagNumber(4) + void clearSpendPubkey() => clearField(4); +} + +class AddressResponse extends $pb.GeneratedMessage { + factory AddressResponse({ + $core.Iterable<$core.String>? address, + }) { + final $result = create(); + if (address != null) { + $result.address.addAll(address); + } + return $result; + } + AddressResponse._() : super(); + factory AddressResponse.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory AddressResponse.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'AddressResponse', createEmptyInstance: create) + ..pPS(1, _omitFieldNames ? '' : 'address') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + AddressResponse clone() => AddressResponse()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + AddressResponse copyWith(void Function(AddressResponse) updates) => super.copyWith((message) => updates(message as AddressResponse)) as AddressResponse; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static AddressResponse create() => AddressResponse._(); + AddressResponse createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static AddressResponse getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static AddressResponse? _defaultInstance; + + @$pb.TagNumber(1) + $core.List<$core.String> get address => $_getList(0); +} + +class SpentRequest extends $pb.GeneratedMessage { + factory SpentRequest({ + $core.Iterable<$core.String>? outputId, + }) { + final $result = create(); + if (outputId != null) { + $result.outputId.addAll(outputId); + } + return $result; + } + SpentRequest._() : super(); + factory SpentRequest.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory SpentRequest.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'SpentRequest', createEmptyInstance: create) + ..pPS(1, _omitFieldNames ? '' : 'outputId') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + SpentRequest clone() => SpentRequest()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + SpentRequest copyWith(void Function(SpentRequest) updates) => super.copyWith((message) => updates(message as SpentRequest)) as SpentRequest; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static SpentRequest create() => SpentRequest._(); + SpentRequest createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static SpentRequest getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static SpentRequest? _defaultInstance; + + @$pb.TagNumber(1) + $core.List<$core.String> get outputId => $_getList(0); +} + +class SpentResponse extends $pb.GeneratedMessage { + factory SpentResponse({ + $core.Iterable<$core.String>? outputId, + }) { + final $result = create(); + if (outputId != null) { + $result.outputId.addAll(outputId); + } + return $result; + } + SpentResponse._() : super(); + factory SpentResponse.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory SpentResponse.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'SpentResponse', createEmptyInstance: create) + ..pPS(1, _omitFieldNames ? '' : 'outputId') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + SpentResponse clone() => SpentResponse()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + SpentResponse copyWith(void Function(SpentResponse) updates) => super.copyWith((message) => updates(message as SpentResponse)) as SpentResponse; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static SpentResponse create() => SpentResponse._(); + SpentResponse createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static SpentResponse getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static SpentResponse? _defaultInstance; + + @$pb.TagNumber(1) + $core.List<$core.String> get outputId => $_getList(0); +} + +class CreateRequest extends $pb.GeneratedMessage { + factory CreateRequest({ + $core.List<$core.int>? rawTx, + $core.List<$core.int>? scanSecret, + $core.List<$core.int>? spendSecret, + $fixnum.Int64? feeRatePerKb, + $core.bool? dryRun, + }) { + final $result = create(); + if (rawTx != null) { + $result.rawTx = rawTx; + } + if (scanSecret != null) { + $result.scanSecret = scanSecret; + } + if (spendSecret != null) { + $result.spendSecret = spendSecret; + } + if (feeRatePerKb != null) { + $result.feeRatePerKb = feeRatePerKb; + } + if (dryRun != null) { + $result.dryRun = dryRun; + } + return $result; + } + CreateRequest._() : super(); + factory CreateRequest.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory CreateRequest.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'CreateRequest', createEmptyInstance: create) + ..a<$core.List<$core.int>>(1, _omitFieldNames ? '' : 'rawTx', $pb.PbFieldType.OY) + ..a<$core.List<$core.int>>(2, _omitFieldNames ? '' : 'scanSecret', $pb.PbFieldType.OY) + ..a<$core.List<$core.int>>(3, _omitFieldNames ? '' : 'spendSecret', $pb.PbFieldType.OY) + ..a<$fixnum.Int64>(4, _omitFieldNames ? '' : 'feeRatePerKb', $pb.PbFieldType.OU6, defaultOrMaker: $fixnum.Int64.ZERO) + ..aOB(5, _omitFieldNames ? '' : 'dryRun') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + CreateRequest clone() => CreateRequest()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + CreateRequest copyWith(void Function(CreateRequest) updates) => super.copyWith((message) => updates(message as CreateRequest)) as CreateRequest; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static CreateRequest create() => CreateRequest._(); + CreateRequest createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static CreateRequest getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static CreateRequest? _defaultInstance; + + @$pb.TagNumber(1) + $core.List<$core.int> get rawTx => $_getN(0); + @$pb.TagNumber(1) + set rawTx($core.List<$core.int> v) { $_setBytes(0, v); } + @$pb.TagNumber(1) + $core.bool hasRawTx() => $_has(0); + @$pb.TagNumber(1) + void clearRawTx() => clearField(1); + + @$pb.TagNumber(2) + $core.List<$core.int> get scanSecret => $_getN(1); + @$pb.TagNumber(2) + set scanSecret($core.List<$core.int> v) { $_setBytes(1, v); } + @$pb.TagNumber(2) + $core.bool hasScanSecret() => $_has(1); + @$pb.TagNumber(2) + void clearScanSecret() => clearField(2); + + @$pb.TagNumber(3) + $core.List<$core.int> get spendSecret => $_getN(2); + @$pb.TagNumber(3) + set spendSecret($core.List<$core.int> v) { $_setBytes(2, v); } + @$pb.TagNumber(3) + $core.bool hasSpendSecret() => $_has(2); + @$pb.TagNumber(3) + void clearSpendSecret() => clearField(3); + + @$pb.TagNumber(4) + $fixnum.Int64 get feeRatePerKb => $_getI64(3); + @$pb.TagNumber(4) + set feeRatePerKb($fixnum.Int64 v) { $_setInt64(3, v); } + @$pb.TagNumber(4) + $core.bool hasFeeRatePerKb() => $_has(3); + @$pb.TagNumber(4) + void clearFeeRatePerKb() => clearField(4); + + @$pb.TagNumber(5) + $core.bool get dryRun => $_getBF(4); + @$pb.TagNumber(5) + set dryRun($core.bool v) { $_setBool(4, v); } + @$pb.TagNumber(5) + $core.bool hasDryRun() => $_has(4); + @$pb.TagNumber(5) + void clearDryRun() => clearField(5); +} + +class CreateResponse extends $pb.GeneratedMessage { + factory CreateResponse({ + $core.List<$core.int>? rawTx, + $core.Iterable<$core.String>? outputId, + }) { + final $result = create(); + if (rawTx != null) { + $result.rawTx = rawTx; + } + if (outputId != null) { + $result.outputId.addAll(outputId); + } + return $result; + } + CreateResponse._() : super(); + factory CreateResponse.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory CreateResponse.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'CreateResponse', createEmptyInstance: create) + ..a<$core.List<$core.int>>(1, _omitFieldNames ? '' : 'rawTx', $pb.PbFieldType.OY) + ..pPS(2, _omitFieldNames ? '' : 'outputId') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + CreateResponse clone() => CreateResponse()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + CreateResponse copyWith(void Function(CreateResponse) updates) => super.copyWith((message) => updates(message as CreateResponse)) as CreateResponse; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static CreateResponse create() => CreateResponse._(); + CreateResponse createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static CreateResponse getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static CreateResponse? _defaultInstance; + + @$pb.TagNumber(1) + $core.List<$core.int> get rawTx => $_getN(0); + @$pb.TagNumber(1) + set rawTx($core.List<$core.int> v) { $_setBytes(0, v); } + @$pb.TagNumber(1) + $core.bool hasRawTx() => $_has(0); + @$pb.TagNumber(1) + void clearRawTx() => clearField(1); + + @$pb.TagNumber(2) + $core.List<$core.String> get outputId => $_getList(1); +} + +class BroadcastRequest extends $pb.GeneratedMessage { + factory BroadcastRequest({ + $core.List<$core.int>? rawTx, + }) { + final $result = create(); + if (rawTx != null) { + $result.rawTx = rawTx; + } + return $result; + } + BroadcastRequest._() : super(); + factory BroadcastRequest.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory BroadcastRequest.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'BroadcastRequest', createEmptyInstance: create) + ..a<$core.List<$core.int>>(1, _omitFieldNames ? '' : 'rawTx', $pb.PbFieldType.OY) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + BroadcastRequest clone() => BroadcastRequest()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + BroadcastRequest copyWith(void Function(BroadcastRequest) updates) => super.copyWith((message) => updates(message as BroadcastRequest)) as BroadcastRequest; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static BroadcastRequest create() => BroadcastRequest._(); + BroadcastRequest createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static BroadcastRequest getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static BroadcastRequest? _defaultInstance; + + @$pb.TagNumber(1) + $core.List<$core.int> get rawTx => $_getN(0); + @$pb.TagNumber(1) + set rawTx($core.List<$core.int> v) { $_setBytes(0, v); } + @$pb.TagNumber(1) + $core.bool hasRawTx() => $_has(0); + @$pb.TagNumber(1) + void clearRawTx() => clearField(1); +} + +class BroadcastResponse extends $pb.GeneratedMessage { + factory BroadcastResponse({ + $core.String? txid, + }) { + final $result = create(); + if (txid != null) { + $result.txid = txid; + } + return $result; + } + BroadcastResponse._() : super(); + factory BroadcastResponse.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory BroadcastResponse.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'BroadcastResponse', createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'txid') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + BroadcastResponse clone() => BroadcastResponse()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + BroadcastResponse copyWith(void Function(BroadcastResponse) updates) => super.copyWith((message) => updates(message as BroadcastResponse)) as BroadcastResponse; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static BroadcastResponse create() => BroadcastResponse._(); + BroadcastResponse createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static BroadcastResponse getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static BroadcastResponse? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get txid => $_getSZ(0); + @$pb.TagNumber(1) + set txid($core.String v) { $_setString(0, v); } + @$pb.TagNumber(1) + $core.bool hasTxid() => $_has(0); + @$pb.TagNumber(1) + void clearTxid() => clearField(1); +} + + +const _omitFieldNames = $core.bool.fromEnvironment('protobuf.omit_field_names'); +const _omitMessageNames = $core.bool.fromEnvironment('protobuf.omit_message_names'); diff --git a/cw_mweb/lib/mwebd.pbgrpc.dart b/cw_mweb/lib/mwebd.pbgrpc.dart new file mode 100644 index 000000000..6bc48cfdf --- /dev/null +++ b/cw_mweb/lib/mwebd.pbgrpc.dart @@ -0,0 +1,159 @@ +// +// Generated code. Do not modify. +// source: mwebd.proto +// +// @dart = 2.12 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names, library_prefixes +// ignore_for_file: non_constant_identifier_names, prefer_final_fields +// ignore_for_file: unnecessary_import, unnecessary_this, unused_import + +import 'dart:async' as $async; +import 'dart:core' as $core; + +import 'package:grpc/service_api.dart' as $grpc; +import 'package:protobuf/protobuf.dart' as $pb; + +import 'mwebd.pb.dart' as $0; + +export 'mwebd.pb.dart'; + +@$pb.GrpcServiceName('Rpc') +class RpcClient extends $grpc.Client { + static final _$status = $grpc.ClientMethod<$0.StatusRequest, $0.StatusResponse>( + '/Rpc/Status', + ($0.StatusRequest value) => value.writeToBuffer(), + ($core.List<$core.int> value) => $0.StatusResponse.fromBuffer(value)); + static final _$utxos = $grpc.ClientMethod<$0.UtxosRequest, $0.Utxo>( + '/Rpc/Utxos', + ($0.UtxosRequest value) => value.writeToBuffer(), + ($core.List<$core.int> value) => $0.Utxo.fromBuffer(value)); + static final _$addresses = $grpc.ClientMethod<$0.AddressRequest, $0.AddressResponse>( + '/Rpc/Addresses', + ($0.AddressRequest value) => value.writeToBuffer(), + ($core.List<$core.int> value) => $0.AddressResponse.fromBuffer(value)); + static final _$spent = $grpc.ClientMethod<$0.SpentRequest, $0.SpentResponse>( + '/Rpc/Spent', + ($0.SpentRequest value) => value.writeToBuffer(), + ($core.List<$core.int> value) => $0.SpentResponse.fromBuffer(value)); + static final _$create = $grpc.ClientMethod<$0.CreateRequest, $0.CreateResponse>( + '/Rpc/Create', + ($0.CreateRequest value) => value.writeToBuffer(), + ($core.List<$core.int> value) => $0.CreateResponse.fromBuffer(value)); + static final _$broadcast = $grpc.ClientMethod<$0.BroadcastRequest, $0.BroadcastResponse>( + '/Rpc/Broadcast', + ($0.BroadcastRequest value) => value.writeToBuffer(), + ($core.List<$core.int> value) => $0.BroadcastResponse.fromBuffer(value)); + + RpcClient($grpc.ClientChannel channel, + {$grpc.CallOptions? options, + $core.Iterable<$grpc.ClientInterceptor>? interceptors}) + : super(channel, options: options, + interceptors: interceptors); + + $grpc.ResponseFuture<$0.StatusResponse> status($0.StatusRequest request, {$grpc.CallOptions? options}) { + return $createUnaryCall(_$status, request, options: options); + } + + $grpc.ResponseStream<$0.Utxo> utxos($0.UtxosRequest request, {$grpc.CallOptions? options}) { + return $createStreamingCall(_$utxos, $async.Stream.fromIterable([request]), options: options); + } + + $grpc.ResponseFuture<$0.AddressResponse> addresses($0.AddressRequest request, {$grpc.CallOptions? options}) { + return $createUnaryCall(_$addresses, request, options: options); + } + + $grpc.ResponseFuture<$0.SpentResponse> spent($0.SpentRequest request, {$grpc.CallOptions? options}) { + return $createUnaryCall(_$spent, request, options: options); + } + + $grpc.ResponseFuture<$0.CreateResponse> create($0.CreateRequest request, {$grpc.CallOptions? options}) { + return $createUnaryCall(_$create, request, options: options); + } + + $grpc.ResponseFuture<$0.BroadcastResponse> broadcast($0.BroadcastRequest request, {$grpc.CallOptions? options}) { + return $createUnaryCall(_$broadcast, request, options: options); + } +} + +@$pb.GrpcServiceName('Rpc') +abstract class RpcServiceBase extends $grpc.Service { + $core.String get $name => 'Rpc'; + + RpcServiceBase() { + $addMethod($grpc.ServiceMethod<$0.StatusRequest, $0.StatusResponse>( + 'Status', + status_Pre, + false, + false, + ($core.List<$core.int> value) => $0.StatusRequest.fromBuffer(value), + ($0.StatusResponse value) => value.writeToBuffer())); + $addMethod($grpc.ServiceMethod<$0.UtxosRequest, $0.Utxo>( + 'Utxos', + utxos_Pre, + false, + true, + ($core.List<$core.int> value) => $0.UtxosRequest.fromBuffer(value), + ($0.Utxo value) => value.writeToBuffer())); + $addMethod($grpc.ServiceMethod<$0.AddressRequest, $0.AddressResponse>( + 'Addresses', + addresses_Pre, + false, + false, + ($core.List<$core.int> value) => $0.AddressRequest.fromBuffer(value), + ($0.AddressResponse value) => value.writeToBuffer())); + $addMethod($grpc.ServiceMethod<$0.SpentRequest, $0.SpentResponse>( + 'Spent', + spent_Pre, + false, + false, + ($core.List<$core.int> value) => $0.SpentRequest.fromBuffer(value), + ($0.SpentResponse value) => value.writeToBuffer())); + $addMethod($grpc.ServiceMethod<$0.CreateRequest, $0.CreateResponse>( + 'Create', + create_Pre, + false, + false, + ($core.List<$core.int> value) => $0.CreateRequest.fromBuffer(value), + ($0.CreateResponse value) => value.writeToBuffer())); + $addMethod($grpc.ServiceMethod<$0.BroadcastRequest, $0.BroadcastResponse>( + 'Broadcast', + broadcast_Pre, + false, + false, + ($core.List<$core.int> value) => $0.BroadcastRequest.fromBuffer(value), + ($0.BroadcastResponse value) => value.writeToBuffer())); + } + + $async.Future<$0.StatusResponse> status_Pre($grpc.ServiceCall call, $async.Future<$0.StatusRequest> request) async { + return status(call, await request); + } + + $async.Stream<$0.Utxo> utxos_Pre($grpc.ServiceCall call, $async.Future<$0.UtxosRequest> request) async* { + yield* utxos(call, await request); + } + + $async.Future<$0.AddressResponse> addresses_Pre($grpc.ServiceCall call, $async.Future<$0.AddressRequest> request) async { + return addresses(call, await request); + } + + $async.Future<$0.SpentResponse> spent_Pre($grpc.ServiceCall call, $async.Future<$0.SpentRequest> request) async { + return spent(call, await request); + } + + $async.Future<$0.CreateResponse> create_Pre($grpc.ServiceCall call, $async.Future<$0.CreateRequest> request) async { + return create(call, await request); + } + + $async.Future<$0.BroadcastResponse> broadcast_Pre($grpc.ServiceCall call, $async.Future<$0.BroadcastRequest> request) async { + return broadcast(call, await request); + } + + $async.Future<$0.StatusResponse> status($grpc.ServiceCall call, $0.StatusRequest request); + $async.Stream<$0.Utxo> utxos($grpc.ServiceCall call, $0.UtxosRequest request); + $async.Future<$0.AddressResponse> addresses($grpc.ServiceCall call, $0.AddressRequest request); + $async.Future<$0.SpentResponse> spent($grpc.ServiceCall call, $0.SpentRequest request); + $async.Future<$0.CreateResponse> create($grpc.ServiceCall call, $0.CreateRequest request); + $async.Future<$0.BroadcastResponse> broadcast($grpc.ServiceCall call, $0.BroadcastRequest request); +} diff --git a/cw_mweb/macos/Classes/CwMwebPlugin.swift b/cw_mweb/macos/Classes/CwMwebPlugin.swift new file mode 100644 index 000000000..9c0dabd40 --- /dev/null +++ b/cw_mweb/macos/Classes/CwMwebPlugin.swift @@ -0,0 +1,19 @@ +import Cocoa +import FlutterMacOS + +public class CwMwebPlugin: NSObject, FlutterPlugin { + public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel(name: "cw_mweb", binaryMessenger: registrar.messenger) + let instance = CwMwebPlugin() + registrar.addMethodCallDelegate(instance, channel: channel) + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + switch call.method { + case "getPlatformVersion": + result("macOS " + ProcessInfo.processInfo.operatingSystemVersionString) + default: + result(FlutterMethodNotImplemented) + } + } +} diff --git a/cw_mweb/macos/cw_mweb.podspec b/cw_mweb/macos/cw_mweb.podspec new file mode 100644 index 000000000..8fadcced9 --- /dev/null +++ b/cw_mweb/macos/cw_mweb.podspec @@ -0,0 +1,23 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint cw_mweb.podspec` to validate before publishing. +# +Pod::Spec.new do |s| + s.name = 'cw_mweb' + s.version = '0.0.1' + s.summary = 'A new Flutter plugin project.' + s.description = <<-DESC +A new Flutter plugin project. + DESC + s.homepage = 'http://example.com' + s.license = { :file => '../LICENSE' } + s.author = { 'Your Company' => 'email@example.com' } + + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.dependency 'FlutterMacOS' + + s.platform = :osx, '10.11' + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } + s.swift_version = '5.0' +end diff --git a/cw_mweb/pubspec.yaml b/cw_mweb/pubspec.yaml new file mode 100644 index 000000000..cfe43c70b --- /dev/null +++ b/cw_mweb/pubspec.yaml @@ -0,0 +1,76 @@ +name: cw_mweb +description: A new Flutter plugin project. +version: 0.0.1 +homepage: + +environment: + sdk: '>=3.0.6 <4.0.0' + flutter: ">=3.3.0" + +dependencies: + flutter: + sdk: flutter + grpc: ^3.2.4 + path_provider: ^2.1.2 + plugin_platform_interface: ^2.0.2 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^2.0.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 packages. +flutter: + # This section identifies this Flutter project as a plugin project. + # The 'pluginClass' specifies the class (in Java, Kotlin, Swift, Objective-C, etc.) + # which should be registered in the plugin registry. This is required for + # using method channels. + # The Android 'package' specifies package in which the registered class is. + # This is required for using method channels on Android. + # The 'ffiPlugin' specifies that native code should be built and bundled. + # This is required for using `dart:ffi`. + # All these are used by the tooling to maintain consistency when + # adding or updating assets for this project. + plugin: + platforms: + android: + package: com.cakewallet.mweb + pluginClass: CwMwebPlugin + ios: + pluginClass: CwMwebPlugin + macos: + pluginClass: CwMwebPlugin + + # 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/cw_nano/lib/nano_client.dart b/cw_nano/lib/nano_client.dart index 478a6c125..7f8e1d0a9 100644 --- a/cw_nano/lib/nano_client.dart +++ b/cw_nano/lib/nano_client.dart @@ -466,21 +466,25 @@ class NanoClient { blocks = blocks as Map; - // confirm all receivable blocks: - for (final blockHash in blocks.keys) { - final block = blocks[blockHash]; - final String amountRaw = block["amount"] as String; - await receiveBlock( - blockHash: blockHash, - amountRaw: amountRaw, - privateKey: privateKey, - destinationAddress: destinationAddress, - ); - // a bit of a hack: - await Future.delayed(const Duration(seconds: 2)); + try { + // confirm all receivable blocks: + for (final blockHash in blocks.keys) { + final block = blocks[blockHash]; + final String amountRaw = block["amount"] as String; + await receiveBlock( + blockHash: blockHash, + amountRaw: amountRaw, + privateKey: privateKey, + destinationAddress: destinationAddress, + ); + // a bit of a hack: + await Future.delayed(const Duration(seconds: 2)); + } + return blocks.keys.length; + } catch (_) { + // we failed to confirm all receivable blocks for w/e reason (PoW / node outage / etc) + return 0; } - - return blocks.keys.length; } void stop() {} diff --git a/cw_nano/lib/nano_wallet.dart b/cw_nano/lib/nano_wallet.dart index 700710c2e..f89c595c9 100644 --- a/cw_nano/lib/nano_wallet.dart +++ b/cw_nano/lib/nano_wallet.dart @@ -42,6 +42,7 @@ abstract class NanoWalletBase required String password, NanoBalance? initialBalance, required EncryptionFileUtils encryptionFileUtils, + this.passphrase, }) : syncStatus = NotConnectedSyncStatus(), _password = password, _mnemonic = mnemonic, @@ -148,7 +149,7 @@ abstract class NanoWalletBase Future changePassword(String password) => throw UnimplementedError("changePassword"); @override - void close() { + Future close({required bool shouldCleanup}) async { _client.stop(); _receiveTimer?.cancel(); } @@ -548,4 +549,7 @@ abstract class NanoWalletBase } return await NanoSignatures.verifyMessage(message, signature, address); } + + @override + final String? passphrase; } diff --git a/cw_nano/lib/nano_wallet_addresses.dart b/cw_nano/lib/nano_wallet_addresses.dart index cc532d2c7..e8eae7737 100644 --- a/cw_nano/lib/nano_wallet_addresses.dart +++ b/cw_nano/lib/nano_wallet_addresses.dart @@ -18,6 +18,9 @@ abstract class NanoWalletAddressesBase extends WalletAddresses with Store { @observable String address; + @override + String get primaryAddress => address; + @observable NanoAccount? account; diff --git a/cw_nano/lib/nano_wallet_creation_credentials.dart b/cw_nano/lib/nano_wallet_creation_credentials.dart index 4ee79ce48..59789aec7 100644 --- a/cw_nano/lib/nano_wallet_creation_credentials.dart +++ b/cw_nano/lib/nano_wallet_creation_credentials.dart @@ -4,13 +4,21 @@ import 'package:cw_core/wallet_info.dart'; class NanoNewWalletCredentials extends WalletCredentials { NanoNewWalletCredentials({ required String name, + WalletInfo? walletInfo, String? password, DerivationType? derivationType, + this.mnemonic, + String? parentAddress, + String? passphrase, }) : super( name: name, password: password, - derivationInfo: DerivationInfo(derivationType: derivationType), + walletInfo: walletInfo, + parentAddress: parentAddress, + passphrase: passphrase, ); + + final String? mnemonic; } class NanoRestoreWalletFromSeedCredentials extends WalletCredentials { @@ -19,10 +27,12 @@ class NanoRestoreWalletFromSeedCredentials extends WalletCredentials { required this.mnemonic, String? password, required DerivationType derivationType, + String? passphrase, }) : super( name: name, password: password, derivationInfo: DerivationInfo(derivationType: derivationType), + passphrase: passphrase, ); final String mnemonic; diff --git a/cw_nano/lib/nano_wallet_service.dart b/cw_nano/lib/nano_wallet_service.dart index ac3d6581a..5c073c9c0 100644 --- a/cw_nano/lib/nano_wallet_service.dart +++ b/cw_nano/lib/nano_wallet_service.dart @@ -14,8 +14,11 @@ import 'package:bip39/bip39.dart' as bip39; import 'package:nanodart/nanodart.dart'; import 'package:nanoutil/nanoutil.dart'; -class NanoWalletService extends WalletService { +class NanoWalletService extends WalletService< + NanoNewWalletCredentials, + NanoRestoreWalletFromSeedCredentials, + NanoRestoreWalletFromKeysCredentials, + NanoNewWalletCredentials> { NanoWalletService(this.walletInfoSource, this.isDirect); final Box walletInfoSource; @@ -29,12 +32,18 @@ class NanoWalletService extends WalletService create(NanoNewWalletCredentials credentials, {bool? isTestnet}) async { - // nano standard: - String seedKey = NanoSeeds.generateSeed(); - String mnemonic = NanoDerivations.standardSeedToMnemonic(seedKey); - - // ensure default if not present: - credentials.walletInfo!.derivationInfo ??= DerivationInfo(derivationType: DerivationType.nano); + final String mnemonic; + switch (credentials.walletInfo?.derivationInfo?.derivationType) { + case DerivationType.nano: + String seedKey = NanoSeeds.generateSeed(); + mnemonic = credentials.mnemonic ?? NanoDerivations.standardSeedToMnemonic(seedKey); + break; + case DerivationType.bip39: + default: + final strength = credentials.seedPhraseLength == 24 ? 256 : 128; + mnemonic = credentials.mnemonic ?? bip39.generateMnemonic(strength: strength); + break; + } final wallet = NanoWallet( walletInfo: credentials.walletInfo!, @@ -86,7 +95,8 @@ class NanoWalletService extends WalletService restoreFromKeys(NanoRestoreWalletFromKeysCredentials credentials, {bool? isTestnet}) async { + Future restoreFromKeys(NanoRestoreWalletFromKeysCredentials credentials, + {bool? isTestnet}) async { if (credentials.seedKey.contains(' ')) { throw Exception("Invalid key!"); } else { @@ -106,6 +116,13 @@ class NanoWalletService extends WalletService restoreFromHardwareWallet(NanoNewWalletCredentials credentials) { - throw UnimplementedError("Restoring a Nano wallet from a hardware wallet is not yet supported!"); + throw UnimplementedError( + "Restoring a Nano wallet from a hardware wallet is not yet supported!"); } @override - Future restoreFromSeed(NanoRestoreWalletFromSeedCredentials credentials, {bool? isTestnet}) async { + Future restoreFromSeed(NanoRestoreWalletFromSeedCredentials credentials, + {bool? isTestnet}) async { if (credentials.mnemonic.contains(' ')) { if (!bip39.validateMnemonic(credentials.mnemonic)) { throw nm.NanoMnemonicIsIncorrectException(); diff --git a/cw_nano/pubspec.lock b/cw_nano/pubspec.lock index ef9de14f9..dd955e344 100644 --- a/cw_nano/pubspec.lock +++ b/cw_nano/pubspec.lock @@ -277,10 +277,10 @@ packages: dependency: transitive description: name: file - sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" url: "https://pub.dev" source: hosted - version: "6.1.4" + version: "7.0.0" fixnum: dependency: transitive description: diff --git a/cw_polygon/lib/polygon_wallet.dart b/cw_polygon/lib/polygon_wallet.dart index eb59a746e..f9aff16c3 100644 --- a/cw_polygon/lib/polygon_wallet.dart +++ b/cw_polygon/lib/polygon_wallet.dart @@ -27,6 +27,7 @@ class PolygonWallet extends EVMChainWallet { super.privateKey, required super.client, required super.encryptionFileUtils, + super.passphrase, }) : super(nativeCurrency: CryptoCurrency.maticpoly); @override @@ -128,8 +129,9 @@ class PolygonWallet extends EVMChainWallet { if (!hasKeysFile) { final mnemonic = data!['mnemonic'] as String?; final privateKey = data['private_key'] as String?; + final passphrase = data['passphrase'] as String?; - keysData = WalletKeysData(mnemonic: mnemonic, privateKey: privateKey); + keysData = WalletKeysData(mnemonic: mnemonic, privateKey: privateKey, passphrase: passphrase); } else { keysData = await WalletKeysFile.readKeysFile( name, @@ -144,6 +146,7 @@ class PolygonWallet extends EVMChainWallet { password: password, mnemonic: keysData.mnemonic, privateKey: keysData.privateKey, + passphrase: keysData.passphrase, initialBalance: balance, client: PolygonClient(), encryptionFileUtils: encryptionFileUtils, diff --git a/cw_polygon/lib/polygon_wallet_service.dart b/cw_polygon/lib/polygon_wallet_service.dart index 4efc312f7..994912e8d 100644 --- a/cw_polygon/lib/polygon_wallet_service.dart +++ b/cw_polygon/lib/polygon_wallet_service.dart @@ -24,12 +24,13 @@ class PolygonWalletService extends EVMChainWalletService { Future create(EVMChainNewWalletCredentials credentials, {bool? isTestnet}) async { final strength = credentials.seedPhraseLength == 24 ? 256 : 128; - final mnemonic = bip39.generateMnemonic(strength: strength); + final mnemonic = credentials.mnemonic ?? bip39.generateMnemonic(strength: strength); final wallet = PolygonWallet( walletInfo: credentials.walletInfo!, mnemonic: mnemonic, password: credentials.password!, + passphrase: credentials.passphrase, client: client, encryptionFileUtils: encryptionFileUtilsFor(isDirect), ); @@ -125,6 +126,7 @@ class PolygonWalletService extends EVMChainWalletService { password: credentials.password!, mnemonic: credentials.mnemonic, walletInfo: credentials.walletInfo!, + passphrase: credentials.passphrase, client: client, encryptionFileUtils: encryptionFileUtilsFor(isDirect), ); diff --git a/cw_solana/lib/solana_wallet.dart b/cw_solana/lib/solana_wallet.dart index 4e69db3b8..da082e833 100644 --- a/cw_solana/lib/solana_wallet.dart +++ b/cw_solana/lib/solana_wallet.dart @@ -33,7 +33,6 @@ import 'package:solana/base58.dart'; import 'package:solana/metaplex.dart' as metaplex; import 'package:solana/solana.dart'; import 'package:solana/src/crypto/ed25519_hd_keypair.dart'; -import 'package:cryptography/cryptography.dart'; part 'solana_wallet.g.dart'; @@ -49,6 +48,7 @@ abstract class SolanaWalletBase required String password, SolanaBalance? initialBalance, required this.encryptionFileUtils, + this.passphrase, }) : syncStatus = const NotConnectedSyncStatus(), _password = password, _mnemonic = mnemonic, @@ -179,7 +179,7 @@ abstract class SolanaWalletBase Future changePassword(String password) => throw UnimplementedError("changePassword"); @override - void close() { + Future close({required bool shouldCleanup}) async { _client.stop(); _transactionsUpdateTimer?.cancel(); } @@ -632,4 +632,7 @@ abstract class SolanaWalletBase @override String get password => _password; + + @override + final String? passphrase; } diff --git a/cw_solana/lib/solana_wallet_addresses.dart b/cw_solana/lib/solana_wallet_addresses.dart index 97a76fb99..19eb91fa1 100644 --- a/cw_solana/lib/solana_wallet_addresses.dart +++ b/cw_solana/lib/solana_wallet_addresses.dart @@ -14,6 +14,9 @@ abstract class SolanaWalletAddressesBase extends WalletAddresses with Store { @override String address; + @override + String get primaryAddress => address; + @override Future init() async { address = walletInfo.address; diff --git a/cw_solana/lib/solana_wallet_creation_credentials.dart b/cw_solana/lib/solana_wallet_creation_credentials.dart index 5b4fa1774..121ef2b44 100644 --- a/cw_solana/lib/solana_wallet_creation_credentials.dart +++ b/cw_solana/lib/solana_wallet_creation_credentials.dart @@ -2,17 +2,36 @@ import 'package:cw_core/wallet_credentials.dart'; import 'package:cw_core/wallet_info.dart'; class SolanaNewWalletCredentials extends WalletCredentials { - SolanaNewWalletCredentials({required String name, WalletInfo? walletInfo, String? password}) - : super(name: name, walletInfo: walletInfo, password: password); + SolanaNewWalletCredentials({ + required String name, + WalletInfo? walletInfo, + String? password, + String? parentAddress, + this.mnemonic, + String? passphrase, + }) : super( + name: name, + walletInfo: walletInfo, + password: password, + parentAddress: parentAddress, + passphrase: passphrase, + ); + final String? mnemonic; } class SolanaRestoreWalletFromSeedCredentials extends WalletCredentials { - SolanaRestoreWalletFromSeedCredentials( - {required String name, - required String password, - required this.mnemonic, - WalletInfo? walletInfo}) - : super(name: name, password: password, walletInfo: walletInfo); + SolanaRestoreWalletFromSeedCredentials({ + required String name, + required String password, + required this.mnemonic, + WalletInfo? walletInfo, + String? passphrase, + }) : super( + name: name, + password: password, + walletInfo: walletInfo, + passphrase: passphrase, + ); final String mnemonic; } diff --git a/cw_solana/lib/solana_wallet_service.dart b/cw_solana/lib/solana_wallet_service.dart index 7461be33b..aff75373e 100644 --- a/cw_solana/lib/solana_wallet_service.dart +++ b/cw_solana/lib/solana_wallet_service.dart @@ -27,7 +27,7 @@ class SolanaWalletService extends WalletService create(SolanaNewWalletCredentials credentials, {bool? isTestnet}) async { final strength = credentials.seedPhraseLength == 24 ? 256 : 128; - final mnemonic = bip39.generateMnemonic(strength: strength); + final mnemonic = credentials.mnemonic ?? bip39.generateMnemonic(strength: strength); final wallet = SolanaWallet( walletInfo: credentials.walletInfo!, diff --git a/cw_tron/lib/tron_wallet.dart b/cw_tron/lib/tron_wallet.dart index f5841d894..3cd8bfc99 100644 --- a/cw_tron/lib/tron_wallet.dart +++ b/cw_tron/lib/tron_wallet.dart @@ -47,6 +47,7 @@ abstract class TronWalletBase required String password, TronBalance? initialBalance, required this.encryptionFileUtils, + this.passphrase, }) : syncStatus = const NotConnectedSyncStatus(), _password = password, _mnemonic = mnemonic, @@ -113,6 +114,7 @@ abstract class TronWalletBase mnemonic: _mnemonic, privateKey: _hexPrivateKey, password: _password, + passphrase: passphrase, ); _tronPublicKey = _tronPrivateKey.publicKey(); @@ -149,8 +151,9 @@ abstract class TronWalletBase if (!hasKeysFile) { final mnemonic = data!['mnemonic'] as String?; final privateKey = data['private_key'] as String?; + final passphrase = data['passphrase'] as String?; - keysData = WalletKeysData(mnemonic: mnemonic, privateKey: privateKey); + keysData = WalletKeysData(mnemonic: mnemonic, privateKey: privateKey, passphrase: passphrase); } else { keysData = await WalletKeysFile.readKeysFile( name, @@ -165,6 +168,7 @@ abstract class TronWalletBase password: password, mnemonic: keysData.mnemonic, privateKey: keysData.privateKey, + passphrase: keysData.passphrase, initialBalance: balance, encryptionFileUtils: encryptionFileUtils, ); @@ -190,12 +194,13 @@ abstract class TronWalletBase String? mnemonic, String? privateKey, required String password, + String? passphrase, }) async { assert(mnemonic != null || privateKey != null); if (privateKey != null) return TronPrivateKey(privateKey); - final seed = bip39.mnemonicToSeed(mnemonic!); + final seed = bip39.mnemonicToSeed(mnemonic!, passphrase: passphrase ?? ''); // Derive a TRON private key from the seed final bip44 = Bip44.fromSeed(seed, Bip44Coins.tron); @@ -212,7 +217,7 @@ abstract class TronWalletBase Future changePassword(String password) => throw UnimplementedError("changePassword"); @override - void close() => _transactionsUpdateTimer?.cancel(); + Future close({required bool shouldCleanup}) async => _transactionsUpdateTimer?.cancel(); @action @override @@ -463,6 +468,7 @@ abstract class TronWalletBase 'mnemonic': _mnemonic, 'private_key': privateKey, 'balance': balance[currency]!.toJSON(), + 'passphrase': passphrase, }); Future _updateBalance() async { @@ -607,4 +613,7 @@ abstract class TronWalletBase @override String get password => _password; + + @override + final String? passphrase; } diff --git a/cw_tron/lib/tron_wallet_addresses.dart b/cw_tron/lib/tron_wallet_addresses.dart index 35939de26..095f97fa9 100644 --- a/cw_tron/lib/tron_wallet_addresses.dart +++ b/cw_tron/lib/tron_wallet_addresses.dart @@ -17,6 +17,9 @@ abstract class TronWalletAddressesBase extends WalletAddresses with Store { @observable String address; + @override + String get primaryAddress => address; + @override Future init() async { address = walletInfo.address; diff --git a/cw_tron/lib/tron_wallet_creation_credentials.dart b/cw_tron/lib/tron_wallet_creation_credentials.dart index ed5e1c164..fd9066acd 100644 --- a/cw_tron/lib/tron_wallet_creation_credentials.dart +++ b/cw_tron/lib/tron_wallet_creation_credentials.dart @@ -2,17 +2,37 @@ import 'package:cw_core/wallet_credentials.dart'; import 'package:cw_core/wallet_info.dart'; class TronNewWalletCredentials extends WalletCredentials { - TronNewWalletCredentials({required String name, WalletInfo? walletInfo, String? password}) - : super(name: name, walletInfo: walletInfo, password: password); + TronNewWalletCredentials({ + required String name, + WalletInfo? walletInfo, + String? password, + this.mnemonic, + String? parentAddress, + String? passphrase, + }) : super( + name: name, + walletInfo: walletInfo, + password: password, + parentAddress: parentAddress, + passphrase: passphrase, + ); + + final String? mnemonic; } class TronRestoreWalletFromSeedCredentials extends WalletCredentials { - TronRestoreWalletFromSeedCredentials( - {required String name, - required String password, - required this.mnemonic, - WalletInfo? walletInfo}) - : super(name: name, password: password, walletInfo: walletInfo); + TronRestoreWalletFromSeedCredentials({ + required String name, + required String password, + required this.mnemonic, + WalletInfo? walletInfo, + String? passphrase, + }) : super( + name: name, + password: password, + walletInfo: walletInfo, + passphrase: passphrase, + ); final String mnemonic; } diff --git a/cw_tron/lib/tron_wallet_service.dart b/cw_tron/lib/tron_wallet_service.dart index dacef439a..c732a87c4 100644 --- a/cw_tron/lib/tron_wallet_service.dart +++ b/cw_tron/lib/tron_wallet_service.dart @@ -33,18 +33,16 @@ class TronWalletService extends WalletService< WalletType getType() => WalletType.tron; @override - Future create( - TronNewWalletCredentials credentials, { - bool? isTestnet, - }) async { + Future create(TronNewWalletCredentials credentials, {bool? isTestnet}) async { final strength = credentials.seedPhraseLength == 24 ? 256 : 128; - final mnemonic = bip39.generateMnemonic(strength: strength); + final mnemonic = credentials.mnemonic ?? bip39.generateMnemonic(strength: strength); final wallet = TronWallet( walletInfo: credentials.walletInfo!, mnemonic: mnemonic, password: credentials.password!, + passphrase: credentials.passphrase, encryptionFileUtils: encryptionFileUtilsFor(isDirect), ); @@ -120,6 +118,7 @@ class TronWalletService extends WalletService< password: credentials.password!, mnemonic: credentials.mnemonic, walletInfo: credentials.walletInfo!, + passphrase: credentials.passphrase, encryptionFileUtils: encryptionFileUtilsFor(isDirect), ); diff --git a/cw_wownero/lib/api/exceptions/setup_wallet_exception.dart b/cw_wownero/lib/api/exceptions/setup_wallet_exception.dart index b6e0c1f18..c0d25c62f 100644 --- a/cw_wownero/lib/api/exceptions/setup_wallet_exception.dart +++ b/cw_wownero/lib/api/exceptions/setup_wallet_exception.dart @@ -2,4 +2,7 @@ class SetupWalletException implements Exception { SetupWalletException({required this.message}); final String message; + + @override + String toString() => message; } \ No newline at end of file diff --git a/cw_wownero/lib/api/exceptions/wallet_restore_from_keys_exception.dart b/cw_wownero/lib/api/exceptions/wallet_restore_from_keys_exception.dart index c6b6c6ef7..ad576faa2 100644 --- a/cw_wownero/lib/api/exceptions/wallet_restore_from_keys_exception.dart +++ b/cw_wownero/lib/api/exceptions/wallet_restore_from_keys_exception.dart @@ -2,4 +2,6 @@ class WalletRestoreFromKeysException implements Exception { WalletRestoreFromKeysException({required this.message}); final String message; + + String toString() => message; } \ No newline at end of file diff --git a/cw_wownero/lib/api/subaddress_list.dart b/cw_wownero/lib/api/subaddress_list.dart index d8c91a584..b07f24c4a 100644 --- a/cw_wownero/lib/api/subaddress_list.dart +++ b/cw_wownero/lib/api/subaddress_list.dart @@ -1,4 +1,5 @@ import 'package:cw_wownero/api/account_list.dart'; +import 'package:cw_wownero/api/transaction_history.dart'; import 'package:cw_wownero/api/wallet.dart'; import 'package:monero/wownero.dart' as wownero; @@ -28,27 +29,75 @@ class Subaddress { Subaddress({ required this.addressIndex, required this.accountIndex, + required this.txCount, + required this.received, }); - String get address => wownero.Wallet_address( - wptr!, - accountIndex: accountIndex, - addressIndex: addressIndex, - ); + late String address = getAddress( + accountIndex: accountIndex, + addressIndex: addressIndex, + ); final int addressIndex; final int accountIndex; String get label => wownero.Wallet_getSubaddressLabel(wptr!, accountIndex: accountIndex, addressIndex: addressIndex); + final int txCount; + final int received; } +class TinyTransactionDetails { + TinyTransactionDetails({ + required this.address, + required this.amount, + }); + final List address; + final int amount; +} + +int lastWptr = 0; +int lastTxCount = 0; +List ttDetails = []; + List getAllSubaddresses() { + txhistory = wownero.Wallet_history(wptr!); + final txCount = wownero.TransactionHistory_count(txhistory!); + if (lastTxCount != txCount && lastWptr != wptr!.address) { + final List newttDetails = []; + lastTxCount = txCount; + lastWptr = wptr!.address; + for (var i = 0; i < txCount; i++) { + final tx = wownero.TransactionHistory_transaction(txhistory!, index: i); + final subaddrs = wownero.TransactionInfo_subaddrIndex(tx).split(","); + final account = wownero.TransactionInfo_subaddrAccount(tx); + newttDetails.add(TinyTransactionDetails( + address: List.generate(subaddrs.length, (index) => getAddress(accountIndex: account, addressIndex: int.tryParse(subaddrs[index])??0)), + amount: wownero.TransactionInfo_amount(tx), + )); + } + ttDetails.clear(); + ttDetails.addAll(newttDetails); + } final size = wownero.Wallet_numSubaddresses(wptr!, accountIndex: subaddress!.accountIndex); final list = List.generate(size, (index) { + final ttDetailsLocal = ttDetails.where((element) { + final address = getAddress( + accountIndex: subaddress!.accountIndex, + addressIndex: index, + ); + if (address == element.address) return true; + return false; + }).toList(); + int received = 0; + for (var i = 0; i < ttDetailsLocal.length; i++) { + received += ttDetailsLocal[i].amount; + } return Subaddress( accountIndex: subaddress!.accountIndex, addressIndex: index, + received: received, + txCount: ttDetailsLocal.length, ); }).reversed.toList(); if (list.isEmpty) { - list.add(Subaddress(addressIndex: 0, accountIndex: subaddress!.accountIndex)); + list.add(Subaddress(addressIndex: 0, accountIndex: subaddress!.accountIndex, txCount: 0, received: 0)); } return list; } @@ -58,6 +107,10 @@ void addSubaddressSync({required int accountIndex, required String label}) { refreshSubaddresses(accountIndex: accountIndex); } +int numSubaddresses(int subaccountIndex) { + return wownero.Wallet_numSubaddresses(wptr!, accountIndex: subaccountIndex); +} + void setLabelForSubaddressSync( {required int accountIndex, required int addressIndex, required String label}) { wownero.Wallet_setSubaddressLabel(wptr!, accountIndex: accountIndex, addressIndex: addressIndex, label: label); diff --git a/cw_wownero/lib/api/transaction_history.dart b/cw_wownero/lib/api/transaction_history.dart index a1e1e3c9b..6b0923e83 100644 --- a/cw_wownero/lib/api/transaction_history.dart +++ b/cw_wownero/lib/api/transaction_history.dart @@ -3,6 +3,7 @@ import 'dart:isolate'; import 'package:cw_wownero/api/account_list.dart'; import 'package:cw_wownero/api/exceptions/creation_transaction_exception.dart'; +import 'package:cw_wownero/api/wallet.dart'; import 'package:cw_wownero/api/wownero_output.dart'; import 'package:cw_wownero/api/structs/pending_transaction.dart'; import 'package:ffi/ffi.dart'; @@ -16,9 +17,16 @@ String getTxKey(String txId) { wownero.TransactionHistory? txhistory; -void refreshTransactions() { +bool isRefreshingTx = false; +Future refreshTransactions() async { + if (isRefreshingTx == true) return; + isRefreshingTx = true; txhistory ??= wownero.Wallet_history(wptr!); - wownero.TransactionHistory_refresh(txhistory!); + final ptr = txhistory!.address; + await Isolate.run(() { + wownero.TransactionHistory_refresh(Pointer.fromAddress(ptr)); + }); + isRefreshingTx = false; } int countOfTransactions() => wownero.TransactionHistory_count(txhistory!); @@ -45,6 +53,8 @@ List getAllTransactions() { confirmations: 0, blockheight: 0, accountIndex: i, + addressIndex: 0, + addressIndexList: [0], paymentId: "", amount: fullBalance - availBalance, isSpend: false, @@ -243,23 +253,28 @@ Future createTransactionMultDest( class Transaction { final String displayLabel; - String subaddressLabel = wownero.Wallet_getSubaddressLabel(wptr!, accountIndex: 0, addressIndex: 0); - late final String address = wownero.Wallet_address( - wptr!, - accountIndex: 0, - addressIndex: 0, + late final String subaddressLabel = wownero.Wallet_getSubaddressLabel(wptr!, accountIndex: accountIndex, addressIndex: addressIndex); + late final String address = getAddress( + accountIndex: accountIndex, + addressIndex: addressIndex, ); + late final List addressList = List.generate(addressIndexList.length, (index) => + getAddress( + accountIndex: accountIndex, + addressIndex: addressIndexList[index], + )); final String description; final int fee; final int confirmations; late final bool isPending = confirmations < 3; final int blockheight; - final int addressIndex = 0; + final int addressIndex; final int accountIndex; + final List addressIndexList; final String paymentId; final int amount; final bool isSpend; - late DateTime timeStamp; + late final DateTime timeStamp; late final bool isConfirmed = !isPending; final String hash; final String key; @@ -301,6 +316,8 @@ class Transaction { amount = wownero.TransactionInfo_amount(txInfo), paymentId = wownero.TransactionInfo_paymentId(txInfo), accountIndex = wownero.TransactionInfo_subaddrAccount(txInfo), + addressIndex = int.tryParse(wownero.TransactionInfo_subaddrIndex(txInfo).split(", ")[0]) ?? 0, + addressIndexList = wownero.TransactionInfo_subaddrIndex(txInfo).split(", ").map((e) => int.tryParse(e) ?? 0).toList(), blockheight = wownero.TransactionInfo_blockHeight(txInfo), confirmations = wownero.TransactionInfo_confirmations(txInfo), fee = wownero.TransactionInfo_fee(txInfo), @@ -314,6 +331,8 @@ class Transaction { required this.confirmations, required this.blockheight, required this.accountIndex, + required this.addressIndex, + required this.addressIndexList, required this.paymentId, required this.amount, required this.isSpend, diff --git a/cw_wownero/lib/api/wallet.dart b/cw_wownero/lib/api/wallet.dart index 56f54dfac..baf9c8960 100644 --- a/cw_wownero/lib/api/wallet.dart +++ b/cw_wownero/lib/api/wallet.dart @@ -67,10 +67,19 @@ String getSeedLegacy(String? language) { } return legacy; } +Map>> addressCache = {}; -String getAddress({int accountIndex = 0, int addressIndex = 1}) => - wownero.Wallet_address(wptr!, +String getAddress({int accountIndex = 0, int addressIndex = 1}) { + while (wownero.Wallet_numSubaddresses(wptr!, accountIndex: accountIndex)-1 < addressIndex) { + print("adding subaddress"); + wownero.Wallet_addSubaddress(wptr!, accountIndex: accountIndex); + } + addressCache[wptr!.address] ??= {}; + addressCache[wptr!.address]![accountIndex] ??= {}; + addressCache[wptr!.address]![accountIndex]![addressIndex] ??= wownero.Wallet_address(wptr!, accountIndex: accountIndex, addressIndex: addressIndex); + return addressCache[wptr!.address]![accountIndex]![addressIndex]!; +} int getFullBalance({int accountIndex = 0}) => wownero.Wallet_balance(wptr!, accountIndex: accountIndex); diff --git a/cw_wownero/lib/api/wallet_manager.dart b/cw_wownero/lib/api/wallet_manager.dart index 660433ba6..ed6d86823 100644 --- a/cw_wownero/lib/api/wallet_manager.dart +++ b/cw_wownero/lib/api/wallet_manager.dart @@ -140,7 +140,7 @@ void restoreWalletFromKeysSync( int nettype = 0, int restoreHeight = 0}) { txhistory = null; - final newWptr = spendKey != "" + var newWptr = (spendKey != "") ? wownero.WalletManager_createDeterministicWalletFromSpendKey( wmPtr, path: path, @@ -165,7 +165,31 @@ void restoreWalletFromKeysSync( throw WalletRestoreFromKeysException( message: wownero.Wallet_errorString(newWptr)); } - + // CW-712 - Try to restore deterministic wallet first, if the view key doesn't + // match the view key provided + if (spendKey != "") { + final viewKeyRestored = wownero.Wallet_secretViewKey(newWptr); + if (viewKey != viewKeyRestored && viewKey != "") { + wownero.WalletManager_closeWallet(wmPtr, newWptr, false); + File(path).deleteSync(); + File(path+".keys").deleteSync(); + newWptr = wownero.WalletManager_createWalletFromKeys( + wmPtr, + path: path, + password: password, + restoreHeight: restoreHeight, + addressString: address, + viewKeyString: viewKey, + spendKeyString: spendKey, + nettype: 0, + ); + final status = wownero.Wallet_status(newWptr); + if (status != 0) { + throw WalletRestoreFromKeysException( + message: wownero.Wallet_errorString(newWptr)); + } + } + } wptr = newWptr; openedWalletsByPath[path] = wptr!; diff --git a/cw_wownero/lib/wownero_subaddress_list.dart b/cw_wownero/lib/wownero_subaddress_list.dart index 61fd09ef9..2ef4f2045 100644 --- a/cw_wownero/lib/wownero_subaddress_list.dart +++ b/cw_wownero/lib/wownero_subaddress_list.dart @@ -1,6 +1,7 @@ import 'package:cw_core/subaddress.dart'; import 'package:cw_wownero/api/coins_info.dart'; import 'package:cw_wownero/api/subaddress_list.dart' as subaddress_list; +import 'package:cw_wownero/api/wallet.dart'; import 'package:flutter/services.dart'; import 'package:mobx/mobx.dart'; @@ -61,6 +62,8 @@ abstract class WowneroSubaddressListBase with Store { return Subaddress( id: id, address: address, + balance: (s.received/1e12).toStringAsFixed(6), + txCount: s.txCount, label: isPrimaryAddress ? 'Primary address' : hasDefaultAddressName @@ -103,6 +106,9 @@ abstract class WowneroSubaddressListBase with Store { required List usedAddresses, }) async { _usedAddresses.addAll(usedAddresses); + final _all = _usedAddresses.toSet().toList(); + _usedAddresses.clear(); + _usedAddresses.addAll(_all); if (_isUpdating) { return; } @@ -140,6 +146,8 @@ abstract class WowneroSubaddressListBase with Store { return Subaddress( id: id, address: address, + balance: (s.received/1e12).toStringAsFixed(6), + txCount: s.txCount, label: id == 0 && label.toLowerCase() == 'Primary account'.toLowerCase() ? 'Primary address' diff --git a/cw_wownero/lib/wownero_wallet.dart b/cw_wownero/lib/wownero_wallet.dart index c3f4bcb69..331957d67 100644 --- a/cw_wownero/lib/wownero_wallet.dart +++ b/cw_wownero/lib/wownero_wallet.dart @@ -59,7 +59,7 @@ abstract class WowneroWalletBase _isTransactionUpdating = false, _hasSyncAfterStartup = false, _password = password, - isEnabledAutoGenerateSubaddress = false, + isEnabledAutoGenerateSubaddress = true, syncStatus = NotConnectedSyncStatus(), unspentCoins = [], this.unspentCoinsInfo = unspentCoinsInfo, @@ -82,6 +82,10 @@ abstract class WowneroWalletBase reaction((_) => isEnabledAutoGenerateSubaddress, (bool enabled) { _updateSubAddress(enabled, account: walletAddresses.account); }); + + _onTxHistoryChangeReaction = reaction((_) => transactionHistory, (__) { + _updateSubAddress(isEnabledAutoGenerateSubaddress, account: walletAddresses.account); + }); } static const int _autoSaveInterval = 30; @@ -123,6 +127,7 @@ abstract class WowneroWalletBase wownero_wallet.SyncListener? _listener; ReactionDisposer? _onAccountChangeReaction; + ReactionDisposer? _onTxHistoryChangeReaction; bool _isTransactionUpdating; bool _hasSyncAfterStartup; Timer? _autoSaveTimer; @@ -155,9 +160,10 @@ abstract class WowneroWalletBase Future? updateBalance() => null; @override - void close() async { + Future close({required bool shouldCleanup}) async { _listener?.stop(); _onAccountChangeReaction?.reaction.dispose(); + _onTxHistoryChangeReaction?.reaction.dispose(); _autoSaveTimer?.cancel(); } @@ -564,8 +570,8 @@ abstract class WowneroWalletBase } _isTransactionUpdating = true; - transactionHistory.clear(); final transactions = await fetchTransactions(); + transactionHistory.clear(); transactionHistory.addMany(transactions); await transactionHistory.save(); _isTransactionUpdating = false; diff --git a/cw_wownero/lib/wownero_wallet_addresses.dart b/cw_wownero/lib/wownero_wallet_addresses.dart index 9eeb182eb..eed81eb45 100644 --- a/cw_wownero/lib/wownero_wallet_addresses.dart +++ b/cw_wownero/lib/wownero_wallet_addresses.dart @@ -3,6 +3,8 @@ import 'package:cw_core/address_info.dart'; import 'package:cw_core/subaddress.dart'; import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_info.dart'; +import 'package:cw_wownero/api/transaction_history.dart'; +import 'package:cw_wownero/api/subaddress_list.dart' as subaddress_list; import 'package:cw_wownero/api/wallet.dart'; import 'package:cw_wownero/wownero_account_list.dart'; import 'package:cw_wownero/wownero_subaddress_list.dart'; @@ -27,6 +29,30 @@ abstract class WowneroWalletAddressesBase extends WalletAddresses with Store { @observable String address; + @override + String get primaryAddress => getAddress(accountIndex: account?.id ?? 0, addressIndex: 0); + + @override + String get latestAddress { + var addressIndex = subaddress_list.numSubaddresses(account?.id??0) - 1; + var address = getAddress(accountIndex: account?.id??0, addressIndex: addressIndex); + while (hiddenAddresses.contains(address)) { + addressIndex++; + address = getAddress(accountIndex: account?.id??0, addressIndex: addressIndex); + } + return address; + } + + @override + String get addressForExchange { + var addressIndex = subaddress_list.numSubaddresses(account?.id??0) - 1; + var address = getAddress(accountIndex: account?.id??0, addressIndex: addressIndex); + while (hiddenAddresses.contains(address) || manualAddresses.contains(address)) { + addressIndex++; + address = getAddress(accountIndex: account?.id??0, addressIndex: addressIndex); + } + return address; + } @observable Account? account; @@ -36,11 +62,14 @@ abstract class WowneroWalletAddressesBase extends WalletAddresses with Store { WowneroSubaddressList subaddressList; WowneroAccountList accountList; + + @override + Set usedAddresses = Set(); @override Future init() async { accountList.update(); - account = accountList.accounts.first; + account = accountList.accounts.isEmpty ? Account(id: 0, label: "Primary address") : accountList.accounts.first; updateSubaddressList(accountIndex: account?.id ?? 0); await updateAddressesInBox(); } @@ -89,8 +118,9 @@ abstract class WowneroWalletAddressesBase extends WalletAddresses with Store { void updateSubaddressList({required int accountIndex}) { subaddressList.update(accountIndex: accountIndex); - subaddress = subaddressList.subaddresses.first; - address = subaddress!.address; + address = subaddressList.subaddresses.isNotEmpty + ? subaddressList.subaddresses.first.address + : getAddress(); } Future updateUsedSubaddress() async { @@ -109,7 +139,7 @@ abstract class WowneroWalletAddressesBase extends WalletAddresses with Store { accountIndex: accountIndex, defaultLabel: defaultLabel, usedAddresses: usedAddresses.toList()); - subaddress = (subaddressList.subaddresses.isEmpty) ? Subaddress(id: 0, address: address, label: defaultLabel) : subaddressList.subaddresses.last; + subaddress = (subaddressList.subaddresses.isEmpty) ? Subaddress(id: 0, address: address, label: defaultLabel, balance: '0', txCount: 0) : subaddressList.subaddresses.last; address = subaddress!.address; } diff --git a/cw_wownero/lib/wownero_wallet_service.dart b/cw_wownero/lib/wownero_wallet_service.dart index 286bfccd0..6f0fafc88 100644 --- a/cw_wownero/lib/wownero_wallet_service.dart +++ b/cw_wownero/lib/wownero_wallet_service.dart @@ -134,7 +134,7 @@ class WowneroWalletService extends WalletService< if (!isValid) { await restoreOrResetWalletFiles(name); - wallet.close(); + wallet.close(shouldCleanup: false); return openWallet(name, password); } diff --git a/cw_wownero/pubspec.lock b/cw_wownero/pubspec.lock index 58831085f..c90340800 100644 --- a/cw_wownero/pubspec.lock +++ b/cw_wownero/pubspec.lock @@ -237,10 +237,10 @@ packages: dependency: transitive description: name: file - sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" url: "https://pub.dev" source: hosted - version: "6.1.4" + version: "7.0.0" fixnum: dependency: transitive description: @@ -463,8 +463,8 @@ packages: dependency: "direct main" description: path: "impls/monero.dart" - ref: bcb328a4956105dc182afd0ce2e48fe263f5f20b - resolved-ref: bcb328a4956105dc182afd0ce2e48fe263f5f20b + ref: "6eb571ea498ed7b854934785f00fabfd0dadf75b" + resolved-ref: "6eb571ea498ed7b854934785f00fabfd0dadf75b" url: "https://github.com/mrcyjanek/monero_c" source: git version: "0.0.0" diff --git a/cw_wownero/pubspec.yaml b/cw_wownero/pubspec.yaml index b9306711d..6943e60c3 100644 --- a/cw_wownero/pubspec.yaml +++ b/cw_wownero/pubspec.yaml @@ -25,7 +25,7 @@ dependencies: monero: git: url: https://github.com/mrcyjanek/monero_c - ref: bcb328a4956105dc182afd0ce2e48fe263f5f20b # monero_c hash + ref: 6eb571ea498ed7b854934785f00fabfd0dadf75b # monero_c hash path: impls/monero.dart mutex: ^3.1.0 diff --git a/how_to_add_new_wallet_type.md b/how_to_add_new_wallet_type.md index 917e87cf4..d71e181e0 100644 --- a/how_to_add_new_wallet_type.md +++ b/how_to_add_new_wallet_type.md @@ -5,7 +5,7 @@ **N:B** Throughout this guide, `walletx` refers to the specific wallet type you want to add. If you're adding `BNB` to CakeWallet, then `walletx` for you here is `bnb`. **Core Folder/Files Setup** -- Idenitify your core component/package (major project component), which would power the integration e.g web3dart, solana, onchain etc +- Identify your core component/package (major project component), which would power the integration e.g web3dart, solana, onchain etc - Add a new entry to `WalletType` class in `cw_core/wallet_type.dart`. - Fill out the necessary information in the various functions in the files, concerning the wallet name, the native currency type, symbol etc. - Go to `cw_core/lib/currency_for_wallet_type.dart`, in the `currencyForWalletType` function, add a case for `walletx`, returning the native cryptocurrency for `walletx`. @@ -144,7 +144,7 @@ You can add as many node entries as desired. } } -- Next, we’ll write the function to change walletX current node to default. An handy function we would make use of later on. Add a new preference key in `lib/entities/preference_key.dart` with the format `PreferencesKey.currentWalletXNodeIdKey`, we’ll use it to identify the current node id. +- Next, we’ll write the function to change walletX current node to default. A handy function we would make use of later on. Add a new preference key in `lib/entities/preference_key.dart` with the format `PreferencesKey.currentWalletXNodeIdKey`, we’ll use it to identify the current node id. Future changeWalletXCurrentNodeToDefault( {required SharedPreferences sharedPreferences, required Box nodes}) async { @@ -228,7 +228,7 @@ Now you can run the codebase and successfully create a wallet for type walletX s **Balance Screen** - Go to `lib/view_model/dashboard/balance_view_model.dart` -- Modify the function to adjust the way the balance is being display on the app: `isHomeScreenSettingsEnabled` +- Modify the function to adjust the way the balance is being displayed on the app: `isHomeScreenSettingsEnabled` - Add a case to the `availableBalanceLabel` getter to modify the text being displayed (Available or confirmed) - Same for `additionalBalanceLabel` - Next, go to `lib/reactions/fiat_rate_update.dart` diff --git a/howto-build-ios.md b/howto-build-ios.md index 418fbc96b..544d4359e 100644 --- a/howto-build-ios.md +++ b/howto-build-ios.md @@ -57,7 +57,7 @@ Proceed into the source code before proceeding with the next steps: ### 7. Execute Build & Setup Commands for Cake Wallet -We need to generate project settings like app name, app icon, package name, etc. For this need to setup environment variables and configure project files. +We need to generate project settings like app name, app icon, package name, etc. For this, we need to setup environment variables and configure project files. Please pick what app you want to build: cakewallet or monero.com. @@ -92,7 +92,7 @@ Then we need to generate localization files and mobx models. `$ flutter build ios --release` -Then you can open `ios/Runner.xcworkspace` with Xcode and you can to archive the application. +Then you can open `ios/Runner.xcworkspace` with Xcode and you can archive the application. Or if you want to run to connected device: diff --git a/howto-build-windows.md b/howto-build-windows.md index 796cb3cc8..504f8f785 100644 --- a/howto-build-windows.md +++ b/howto-build-windows.md @@ -22,7 +22,7 @@ Then install `Desktop development with C++` packages via Visual Studio 2022, or - `C++ 2022 Redistributable Update` - `C++ core desktop features` - `MVC v143 - VS 2022 C++ x64/x86 build tools` -- `C++ CMake tools for Windwos` +- `C++ CMake tools for Windows` - `Testing tools core features - Build Tools` - `C++ AddressSanitizer`. @@ -38,7 +38,7 @@ For building monero dependencies, it is required to install Windows WSL (https:/ ### 5. Pull Cake Wallet source code -You can downlaod CakeWallet source code from our [GitHub repository](github.com/cake-tech/cake_wallet) via git: +You can download CakeWallet source code from our [GitHub repository](github.com/cake-tech/cake_wallet) via git: `$ git clone https://github.com/cake-tech/cake_wallet.git --branch MrCyjaneK-cyjan-monerodart` OR you can download it as [Zip archive](https://github.com/cake-tech/cake_wallet/archive/refs/heads/MrCyjaneK-cyjan-monerodart.zip) @@ -52,6 +52,6 @@ For that you need to run the shell (bash - typically same named utility should b To configure the application, open the directory where you have downloaded or unarchived Cake Wallet sources and run `cakewallet.bat`. Or if you used WSL and have active shell session you can run `$ ./cakewallet.sh` script in `scripts/windows` which will run `cakewallet.bat` in WSL. -After execution of `cakewallet.bat` you should to get `Cake Wallet.zip` in project root directory which will contains `CakeWallet.exe` file and another needed files for run the application. Now you can extract files from `Cake Wallet.zip` archive and run the application. +After execution of `cakewallet.bat` you should to get `Cake Wallet.zip` in project root directory which will contain `CakeWallet.exe` file and another needed files for run the application. Now you can extract files from `Cake Wallet.zip` archive and run the application. Copyright (c) 2024 Cake Labs LLC. diff --git a/integration_test/components/common_test_cases.dart b/integration_test/components/common_test_cases.dart new file mode 100644 index 000000000..2e2991804 --- /dev/null +++ b/integration_test/components/common_test_cases.dart @@ -0,0 +1,96 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +class CommonTestCases { + WidgetTester tester; + CommonTestCases(this.tester); + + Future isSpecificPage() async { + await tester.pumpAndSettle(); + hasType(); + } + + Future tapItemByKey(String key, {bool shouldPumpAndSettle = true}) async { + final widget = find.byKey(ValueKey(key)); + await tester.tap(widget); + shouldPumpAndSettle ? await tester.pumpAndSettle() : await tester.pump(); + } + + Future tapItemByFinder(Finder finder, {bool shouldPumpAndSettle = true}) async { + await tester.tap(finder); + shouldPumpAndSettle ? await tester.pumpAndSettle() : await tester.pump(); + } + + void hasText(String text, {bool hasWidget = true}) { + final textWidget = find.text(text); + expect(textWidget, hasWidget ? findsOneWidget : findsNothing); + } + + void hasType() { + final typeWidget = find.byType(T); + expect(typeWidget, findsOneWidget); + } + + void hasValueKey(String key) { + final typeWidget = find.byKey(ValueKey(key)); + expect(typeWidget, findsOneWidget); + } + + Future swipePage({bool swipeRight = true}) async { + await tester.drag(find.byType(PageView), Offset(swipeRight ? -300 : 300, 0)); + await tester.pumpAndSettle(); + } + + Future swipeByPageKey({required String key, bool swipeRight = true}) async { + await tester.drag(find.byKey(ValueKey(key)), Offset(swipeRight ? -300 : 300, 0)); + await tester.pumpAndSettle(); + } + + Future goBack() async { + tester.printToConsole('Routing back to previous screen'); + final NavigatorState navigator = tester.state(find.byType(Navigator)); + navigator.pop(); + await tester.pumpAndSettle(); + } + + Future scrollUntilVisible(String childKey, String parentScrollableKey, + {double delta = 300}) async { + final scrollableWidget = find.descendant( + of: find.byKey(Key(parentScrollableKey)), + matching: find.byType(Scrollable), + ); + + final isAlreadyVisibile = isWidgetVisible(find.byKey(ValueKey(childKey))); + + if (isAlreadyVisibile) return; + + await tester.scrollUntilVisible( + find.byKey(ValueKey(childKey)), + delta, + scrollable: scrollableWidget, + ); + } + + bool isWidgetVisible(Finder finder) { + try { + final Element element = finder.evaluate().single; + final RenderBox renderBox = element.renderObject as RenderBox; + return renderBox.paintBounds + .shift(renderBox.localToGlobal(Offset.zero)) + .overlaps(tester.binding.renderViews.first.paintBounds); + } catch (e) { + return false; + } + } + + Future enterText(String text, String editableTextKey) async { + final editableTextWidget = find.byKey(ValueKey((editableTextKey))); + + await tester.enterText(editableTextWidget, text); + + await tester.pumpAndSettle(); + } + + Future defaultSleepTime({int seconds = 2}) async => + await Future.delayed(Duration(seconds: seconds)); +} diff --git a/integration_test/components/common_test_constants.dart b/integration_test/components/common_test_constants.dart new file mode 100644 index 000000000..d8381973e --- /dev/null +++ b/integration_test/components/common_test_constants.dart @@ -0,0 +1,13 @@ +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/wallet_type.dart'; + +class CommonTestConstants { + static final pin = [0, 8, 0, 1]; + static final String sendTestAmount = '0.00008'; + static final String exchangeTestAmount = '8'; + static final WalletType testWalletType = WalletType.solana; + static final String testWalletName = 'Integrated Testing Wallet'; + static final CryptoCurrency testReceiveCurrency = CryptoCurrency.sol; + static final CryptoCurrency testDepositCurrency = CryptoCurrency.usdtSol; + static final String testWalletAddress = 'An2Y2fsUYKfYvN1zF89GAqR1e6GUMBg3qA83Y5ZWDf8L'; +} diff --git a/integration_test/components/common_test_flows.dart b/integration_test/components/common_test_flows.dart new file mode 100644 index 000000000..807509de9 --- /dev/null +++ b/integration_test/components/common_test_flows.dart @@ -0,0 +1,101 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:cake_wallet/.secrets.g.dart' as secrets; +import 'package:cake_wallet/main.dart' as app; + +import '../robots/disclaimer_page_robot.dart'; +import '../robots/new_wallet_type_page_robot.dart'; +import '../robots/restore_from_seed_or_key_robot.dart'; +import '../robots/restore_options_page_robot.dart'; +import '../robots/setup_pin_code_robot.dart'; +import '../robots/welcome_page_robot.dart'; +import 'common_test_cases.dart'; +import 'common_test_constants.dart'; + +class CommonTestFlows { + CommonTestFlows(this._tester) + : _commonTestCases = CommonTestCases(_tester), + _welcomePageRobot = WelcomePageRobot(_tester), + _setupPinCodeRobot = SetupPinCodeRobot(_tester), + _disclaimerPageRobot = DisclaimerPageRobot(_tester), + _newWalletTypePageRobot = NewWalletTypePageRobot(_tester), + _restoreOptionsPageRobot = RestoreOptionsPageRobot(_tester), + _restoreFromSeedOrKeysPageRobot = RestoreFromSeedOrKeysPageRobot(_tester); + + final WidgetTester _tester; + final CommonTestCases _commonTestCases; + + final WelcomePageRobot _welcomePageRobot; + final SetupPinCodeRobot _setupPinCodeRobot; + final DisclaimerPageRobot _disclaimerPageRobot; + final NewWalletTypePageRobot _newWalletTypePageRobot; + final RestoreOptionsPageRobot _restoreOptionsPageRobot; + final RestoreFromSeedOrKeysPageRobot _restoreFromSeedOrKeysPageRobot; + + Future startAppFlow(Key key) async { + await app.main(topLevelKey: ValueKey('send_flow_test_app_key')); + + await _tester.pumpAndSettle(); + + // --------- Disclaimer Page ------------ + // Tap checkbox to accept disclaimer + await _disclaimerPageRobot.tapDisclaimerCheckbox(); + + // Tap accept button + await _disclaimerPageRobot.tapAcceptButton(); + } + + Future restoreWalletThroughSeedsFlow() async { + await _welcomeToRestoreFromSeedsPath(); + await _restoreFromSeeds(); + } + + Future restoreWalletThroughKeysFlow() async { + await _welcomeToRestoreFromSeedsPath(); + await _restoreFromKeys(); + } + + Future _welcomeToRestoreFromSeedsPath() async { + // --------- Welcome Page --------------- + await _welcomePageRobot.navigateToRestoreWalletPage(); + + // ----------- Restore Options Page ----------- + // Route to restore from seeds page to continue flow + await _restoreOptionsPageRobot.navigateToRestoreFromSeedsPage(); + + // ----------- SetupPinCode Page ------------- + // Confirm initial defaults - Widgets to be displayed etc + await _setupPinCodeRobot.isSetupPinCodePage(); + + await _setupPinCodeRobot.enterPinCode(CommonTestConstants.pin, true); + await _setupPinCodeRobot.enterPinCode(CommonTestConstants.pin, false); + await _setupPinCodeRobot.tapSuccessButton(); + + // ----------- NewWalletType Page ------------- + // Confirm scroll behaviour works properly + await _newWalletTypePageRobot + .findParticularWalletTypeInScrollableList(CommonTestConstants.testWalletType); + + // Select a wallet and route to next page + await _newWalletTypePageRobot.selectWalletType(CommonTestConstants.testWalletType); + await _newWalletTypePageRobot.onNextButtonPressed(); + } + + Future _restoreFromSeeds() async { + // ----------- RestoreFromSeedOrKeys Page ------------- + await _restoreFromSeedOrKeysPageRobot.enterWalletNameText(CommonTestConstants.testWalletName); + await _restoreFromSeedOrKeysPageRobot.enterSeedPhraseForWalletRestore(secrets.solanaTestWalletSeeds); + await _restoreFromSeedOrKeysPageRobot.onRestoreWalletButtonPressed(); + } + + Future _restoreFromKeys() async { + await _commonTestCases.swipePage(); + await _commonTestCases.defaultSleepTime(); + + await _restoreFromSeedOrKeysPageRobot.enterWalletNameText(CommonTestConstants.testWalletName); + + await _restoreFromSeedOrKeysPageRobot.enterSeedPhraseForWalletRestore(''); + await _restoreFromSeedOrKeysPageRobot.onRestoreWalletButtonPressed(); + } +} diff --git a/integration_test/funds_related_tests.dart b/integration_test/funds_related_tests.dart new file mode 100644 index 000000000..9d97d47f8 --- /dev/null +++ b/integration_test/funds_related_tests.dart @@ -0,0 +1,84 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import 'components/common_test_constants.dart'; +import 'components/common_test_flows.dart'; +import 'robots/auth_page_robot.dart'; +import 'robots/dashboard_page_robot.dart'; +import 'robots/exchange_confirm_page_robot.dart'; +import 'robots/exchange_page_robot.dart'; +import 'robots/exchange_trade_page_robot.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + DashboardPageRobot dashboardPageRobot; + ExchangePageRobot exchangePageRobot; + ExchangeConfirmPageRobot exchangeConfirmPageRobot; + AuthPageRobot authPageRobot; + ExchangeTradePageRobot exchangeTradePageRobot; + CommonTestFlows commonTestFlows; + + group('Startup Test', () { + testWidgets('Test for Exchange flow using Restore Wallet - Exchanging USDT(Sol) to SOL', + (tester) async { + authPageRobot = AuthPageRobot(tester); + exchangePageRobot = ExchangePageRobot(tester); + dashboardPageRobot = DashboardPageRobot(tester); + exchangeTradePageRobot = ExchangeTradePageRobot(tester); + exchangeConfirmPageRobot = ExchangeConfirmPageRobot(tester); + commonTestFlows = CommonTestFlows(tester); + + await commonTestFlows.startAppFlow(ValueKey('funds_exchange_test_app_key')); + + await commonTestFlows.restoreWalletThroughSeedsFlow(); + + // ----------- RestoreFromSeedOrKeys Page ------------- + await dashboardPageRobot.navigateToExchangePage(); + + // ----------- Exchange Page ------------- + await exchangePageRobot.isExchangePage(); + exchangePageRobot.hasResetButton(); + await exchangePageRobot.displayBothExchangeCards(); + exchangePageRobot.confirmRightComponentsDisplayOnDepositExchangeCards(); + exchangePageRobot.confirmRightComponentsDisplayOnReceiveExchangeCards(); + + await exchangePageRobot.selectDepositCurrency(CommonTestConstants.testDepositCurrency); + await exchangePageRobot.selectReceiveCurrency(CommonTestConstants.testReceiveCurrency); + + await exchangePageRobot.enterDepositAmount(CommonTestConstants.exchangeTestAmount); + await exchangePageRobot.enterDepositRefundAddress( + depositAddress: CommonTestConstants.testWalletAddress); + + await exchangePageRobot.enterReceiveAddress(CommonTestConstants.testWalletAddress); + + await exchangePageRobot.onExchangeButtonPressed(); + + await exchangePageRobot.handleErrors(CommonTestConstants.exchangeTestAmount); + + final onAuthPage = authPageRobot.onAuthPage(); + if (onAuthPage) { + await authPageRobot.enterPinCode(CommonTestConstants.pin, false); + } + + // ----------- Exchange Confirm Page ------------- + await exchangeConfirmPageRobot.isExchangeConfirmPage(); + + exchangeConfirmPageRobot.confirmComponentsOfTradeDisplayProperly(); + await exchangeConfirmPageRobot.confirmCopyTradeIdToClipBoardWorksProperly(); + await exchangeConfirmPageRobot.onSavedTradeIdButtonPressed(); + + // ----------- Exchange Trade Page ------------- + await exchangeTradePageRobot.isExchangeTradePage(); + exchangeTradePageRobot.hasInformationDialog(); + await exchangeTradePageRobot.onGotItButtonPressed(); + + await exchangeTradePageRobot.onConfirmSendingButtonPressed(); + + await exchangeTradePageRobot.handleConfirmSendResult(); + + await exchangeTradePageRobot.onSendButtonOnConfirmSendingDialogPressed(); + }); + }); +} diff --git a/integration_test/helpers/mocks.dart b/integration_test/helpers/mocks.dart new file mode 100644 index 000000000..01259bcc8 --- /dev/null +++ b/integration_test/helpers/mocks.dart @@ -0,0 +1,25 @@ +import 'package:cake_wallet/core/auth_service.dart'; +import 'package:cake_wallet/core/secure_storage.dart'; +import 'package:cake_wallet/store/app_store.dart'; +import 'package:cake_wallet/store/authentication_store.dart'; +import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cake_wallet/store/wallet_list_store.dart'; +import 'package:cake_wallet/view_model/link_view_model.dart'; +import 'package:hive/hive.dart'; +import 'package:mocktail/mocktail.dart'; + +class MockAppStore extends Mock implements AppStore{} +class MockAuthService extends Mock implements AuthService{} +class MockSettingsStore extends Mock implements SettingsStore {} +class MockAuthenticationStore extends Mock implements AuthenticationStore{} +class MockWalletListStore extends Mock implements WalletListStore{} + + + +class MockLinkViewModel extends Mock implements LinkViewModel {} + +class MockHiveInterface extends Mock implements HiveInterface {} + +class MockHiveBox extends Mock implements Box {} + +class MockSecureStorage extends Mock implements SecureStorage{} \ No newline at end of file diff --git a/integration_test/helpers/test_helpers.dart b/integration_test/helpers/test_helpers.dart new file mode 100644 index 000000000..979ff60ff --- /dev/null +++ b/integration_test/helpers/test_helpers.dart @@ -0,0 +1,100 @@ +import 'package:cake_wallet/core/auth_service.dart'; +import 'package:cake_wallet/core/secure_storage.dart'; +import 'package:cake_wallet/di.dart'; +import 'package:cake_wallet/store/app_store.dart'; +import 'package:cake_wallet/store/authentication_store.dart'; +import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cake_wallet/store/wallet_list_store.dart'; +import 'package:cake_wallet/view_model/link_view_model.dart'; +import 'package:hive/hive.dart'; +import 'package:mocktail/mocktail.dart'; + +import 'mocks.dart'; + +class TestHelpers { + static void setup() { + // Fallback values can also be declared here + registerDependencies(); + } + + static void registerDependencies() { + getAndRegisterAppStore(); + getAndRegisterAuthService(); + getAndRegisterSettingsStore(); + getAndRegisterAuthenticationStore(); + getAndRegisterWalletListStore(); + + getAndRegisterLinkViewModel(); + getAndRegisterSecureStorage(); + getAndRegisterHiveInterface(); + } + + static MockSettingsStore getAndRegisterSettingsStore() { + _removeRegistrationIfExists(); + final service = MockSettingsStore(); + getIt.registerSingleton(service); + return service; + } + + static MockAppStore getAndRegisterAppStore() { + _removeRegistrationIfExists(); + final service = MockAppStore(); + final settingsStore = getAndRegisterSettingsStore(); + + when(() => service.settingsStore).thenAnswer((invocation) => settingsStore); + getIt.registerSingleton(service); + return service; + } + + static MockAuthService getAndRegisterAuthService() { + _removeRegistrationIfExists(); + final service = MockAuthService(); + getIt.registerSingleton(service); + return service; + } + + static MockAuthenticationStore getAndRegisterAuthenticationStore() { + _removeRegistrationIfExists(); + final service = MockAuthenticationStore(); + when(() => service.state).thenReturn(AuthenticationState.uninitialized); + getIt.registerSingleton(service); + return service; + } + + static MockWalletListStore getAndRegisterWalletListStore() { + _removeRegistrationIfExists(); + final service = MockWalletListStore(); + getIt.registerSingleton(service); + return service; + } + + static MockLinkViewModel getAndRegisterLinkViewModel() { + _removeRegistrationIfExists(); + final service = MockLinkViewModel(); + getIt.registerSingleton(service); + return service; + } + + static MockHiveInterface getAndRegisterHiveInterface() { + _removeRegistrationIfExists(); + final service = MockHiveInterface(); + final box = MockHiveBox(); + getIt.registerSingleton(service); + return service; + } + + static MockSecureStorage getAndRegisterSecureStorage() { + _removeRegistrationIfExists(); + final service = MockSecureStorage(); + getIt.registerSingleton(service); + return service; + } + + static void _removeRegistrationIfExists() { + if (getIt.isRegistered()) { + getIt.unregister(); + } + } + + static void tearDown() => getIt.reset(); +} diff --git a/integration_test/integration_response_data.json b/integration_test/integration_response_data.json new file mode 100644 index 000000000..ec747fa47 --- /dev/null +++ b/integration_test/integration_response_data.json @@ -0,0 +1 @@ +null \ No newline at end of file diff --git a/integration_test/robots/auth_page_robot.dart b/integration_test/robots/auth_page_robot.dart new file mode 100644 index 000000000..6358d4398 --- /dev/null +++ b/integration_test/robots/auth_page_robot.dart @@ -0,0 +1,30 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/screens/auth/auth_page.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../components/common_test_cases.dart'; +import 'pin_code_widget_robot.dart'; + +class AuthPageRobot extends PinCodeWidgetRobot { + AuthPageRobot(this.tester) + : commonTestCases = CommonTestCases(tester), + super(tester); + + final WidgetTester tester; + late CommonTestCases commonTestCases; + + bool onAuthPage() { + final hasPinButtons = find.byKey(ValueKey('pin_code_button_3_key')); + final hasPin = hasPinButtons.tryEvaluate(); + return hasPin; + } + + Future isAuthPage() async { + await commonTestCases.isSpecificPage(); + } + + void hasTitle() { + commonTestCases.hasText(S.current.setup_pin); + } +} diff --git a/integration_test/robots/dashboard_page_robot.dart b/integration_test/robots/dashboard_page_robot.dart new file mode 100644 index 000000000..fc917c3b2 --- /dev/null +++ b/integration_test/robots/dashboard_page_robot.dart @@ -0,0 +1,75 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/screens/dashboard/dashboard_page.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../components/common_test_cases.dart'; + +class DashboardPageRobot { + DashboardPageRobot(this.tester) : commonTestCases = CommonTestCases(tester); + + final WidgetTester tester; + late CommonTestCases commonTestCases; + + Future isDashboardPage() async { + await commonTestCases.isSpecificPage(); + } + + void confirmServiceUpdateButtonDisplays() { + commonTestCases.hasValueKey('dashboard_page_services_update_button_key'); + } + + void confirmSyncIndicatorButtonDisplays() { + commonTestCases.hasValueKey('dashboard_page_sync_indicator_button_key'); + } + + void confirmMenuButtonDisplays() { + commonTestCases.hasValueKey('dashboard_page_wallet_menu_button_key'); + } + + Future confirmRightCryptoAssetTitleDisplaysPerPageView(WalletType type, + {bool isHaven = false}) async { + //Balance Page + final walletName = walletTypeToString(type); + final assetName = isHaven ? '$walletName Assets' : walletName; + commonTestCases.hasText(assetName); + + // Swipe to Cake features Page + await commonTestCases.swipeByPageKey(key: 'dashboard_page_view_key', swipeRight: false); + await commonTestCases.defaultSleepTime(); + commonTestCases.hasText('Cake ${S.current.features}'); + + // Swipe back to balance + await commonTestCases.swipeByPageKey(key: 'dashboard_page_view_key'); + await commonTestCases.defaultSleepTime(); + + // Swipe to Transactions Page + await commonTestCases.swipeByPageKey(key: 'dashboard_page_view_key'); + await commonTestCases.defaultSleepTime(); + commonTestCases.hasText(S.current.transactions); + + // Swipe back to balance + await commonTestCases.swipeByPageKey(key: 'dashboard_page_view_key', swipeRight: false); + await commonTestCases.defaultSleepTime(seconds: 5); + } + + Future navigateToBuyPage() async { + await commonTestCases.tapItemByKey('dashboard_page_${S.current.buy}_action_button_key'); + } + + Future navigateToSendPage() async { + await commonTestCases.tapItemByKey('dashboard_page_${S.current.send}_action_button_key'); + } + + Future navigateToSellPage() async { + await commonTestCases.tapItemByKey('dashboard_page_${S.current.sell}_action_button_key'); + } + + Future navigateToReceivePage() async { + await commonTestCases.tapItemByKey('dashboard_page_${S.current.receive}_action_button_key'); + } + + Future navigateToExchangePage() async { + await commonTestCases.tapItemByKey('dashboard_page_${S.current.exchange}_action_button_key'); + } +} diff --git a/integration_test/robots/disclaimer_page_robot.dart b/integration_test/robots/disclaimer_page_robot.dart new file mode 100644 index 000000000..18861fc29 --- /dev/null +++ b/integration_test/robots/disclaimer_page_robot.dart @@ -0,0 +1,39 @@ +import 'package:cake_wallet/src/screens/disclaimer/disclaimer_page.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../components/common_test_cases.dart'; + +class DisclaimerPageRobot { + DisclaimerPageRobot(this.tester) : commonTestCases = CommonTestCases(tester); + + final WidgetTester tester; + late CommonTestCases commonTestCases; + + Future isDisclaimerPage() async { + await commonTestCases.isSpecificPage(); + } + + void hasCheckIcon(bool hasBeenTapped) { + // The checked Icon should not be available initially, until user taps the checkbox + final checkIcon = find.byKey(ValueKey('disclaimer_check_icon_key')); + expect(checkIcon, hasBeenTapped ? findsOneWidget : findsNothing); + } + + void hasDisclaimerCheckbox() { + final checkBox = find.byKey(ValueKey('disclaimer_check_key')); + expect(checkBox, findsOneWidget); + } + + Future tapDisclaimerCheckbox() async { + await commonTestCases.tapItemByKey('disclaimer_check_key'); + + await commonTestCases.defaultSleepTime(); + } + + Future tapAcceptButton() async { + await commonTestCases.tapItemByKey('disclaimer_accept_button_key'); + + await commonTestCases.defaultSleepTime(); + } +} diff --git a/integration_test/robots/exchange_confirm_page_robot.dart b/integration_test/robots/exchange_confirm_page_robot.dart new file mode 100644 index 000000000..160fd9dfb --- /dev/null +++ b/integration_test/robots/exchange_confirm_page_robot.dart @@ -0,0 +1,45 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/screens/exchange_trade/exchange_confirm_page.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../components/common_test_cases.dart'; + +class ExchangeConfirmPageRobot { + ExchangeConfirmPageRobot(this.tester) : commonTestCases = CommonTestCases(tester); + + final WidgetTester tester; + late CommonTestCases commonTestCases; + + Future isExchangeConfirmPage() async { + await commonTestCases.isSpecificPage(); + } + + void confirmComponentsOfTradeDisplayProperly() { + final ExchangeConfirmPage exchangeConfirmPage = tester.widget(find.byType(ExchangeConfirmPage)); + final trade = exchangeConfirmPage.trade; + + commonTestCases.hasText(trade.id); + commonTestCases.hasText('${trade.provider.title} ${S.current.trade_id}'); + + commonTestCases.hasValueKey('exchange_confirm_page_saved_id_button_key'); + commonTestCases.hasValueKey('exchange_confirm_page_copy_to_clipboard_button_key'); + } + + Future confirmCopyTradeIdToClipBoardWorksProperly() async { + final ExchangeConfirmPage exchangeConfirmPage = tester.widget(find.byType(ExchangeConfirmPage)); + final trade = exchangeConfirmPage.trade; + + await commonTestCases.tapItemByKey('exchange_confirm_page_copy_to_clipboard_button_key'); + + ClipboardData? clipboardData = await Clipboard.getData('text/plain'); + + expect(clipboardData?.text, trade.id); + } + + Future onSavedTradeIdButtonPressed() async { + await tester.pumpAndSettle(); + await commonTestCases.defaultSleepTime(); + await commonTestCases.tapItemByKey('exchange_confirm_page_saved_id_button_key'); + } +} diff --git a/integration_test/robots/exchange_page_robot.dart b/integration_test/robots/exchange_page_robot.dart new file mode 100644 index 000000000..b439e4791 --- /dev/null +++ b/integration_test/robots/exchange_page_robot.dart @@ -0,0 +1,330 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/screens/exchange/exchange_page.dart'; +import 'package:cake_wallet/src/screens/exchange/widgets/present_provider_picker.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../components/common_test_cases.dart'; + +class ExchangePageRobot { + ExchangePageRobot(this.tester) : commonTestCases = CommonTestCases(tester); + + final WidgetTester tester; + late CommonTestCases commonTestCases; + + Future isExchangePage() async { + await commonTestCases.isSpecificPage(); + await commonTestCases.defaultSleepTime(); + } + + void hasResetButton() { + commonTestCases.hasText(S.current.reset); + } + + void displaysPresentProviderPicker() { + commonTestCases.hasType(); + } + + Future displayBothExchangeCards() async { + final ExchangePage exchangeCard = tester.widget( + find.byType(ExchangePage), + ); + + final depositKey = exchangeCard.depositKey; + final receiveKey = exchangeCard.receiveKey; + + final depositExchangeCard = find.byKey(depositKey); + expect(depositExchangeCard, findsOneWidget); + + final receiveExchangeCard = find.byKey(receiveKey); + expect(receiveExchangeCard, findsOneWidget); + } + + void confirmRightComponentsDisplayOnDepositExchangeCards() { + ExchangePage exchangePage = tester.widget(find.byType(ExchangePage)); + final exchangeViewModel = exchangePage.exchangeViewModel; + final depositCardPrefix = 'deposit_exchange_card'; + + commonTestCases.hasValueKey('${depositCardPrefix}_title_key'); + commonTestCases.hasValueKey('${depositCardPrefix}_currency_picker_button_key'); + commonTestCases.hasValueKey('${depositCardPrefix}_selected_currency_text_key'); + commonTestCases.hasValueKey('${depositCardPrefix}_amount_textfield_key'); + + exchangePage.depositKey.currentState!.changeLimits(min: '0.1'); + + commonTestCases.hasValueKey('${depositCardPrefix}_min_limit_text_key'); + + final initialCurrency = exchangeViewModel.depositCurrency; + if (initialCurrency.tag != null) { + commonTestCases.hasValueKey('${depositCardPrefix}_selected_currency_tag_text_key'); + } + + if (exchangeViewModel.hasAllAmount) { + commonTestCases.hasValueKey('${depositCardPrefix}_send_all_button_key'); + } + + if (exchangeViewModel.isMoneroWallet) { + commonTestCases.hasValueKey('${depositCardPrefix}_address_book_button_key'); + } + + if (exchangeViewModel.isDepositAddressEnabled) { + commonTestCases.hasValueKey('${depositCardPrefix}_editable_address_textfield_key'); + } else { + commonTestCases.hasValueKey('${depositCardPrefix}_non_editable_address_textfield_key'); + commonTestCases.hasValueKey('${depositCardPrefix}_copy_refund_address_button_key'); + } + + // commonTestCases.hasValueKey('${depositCardPrefix}_max_limit_text_key'); + } + + void confirmRightComponentsDisplayOnReceiveExchangeCards() { + ExchangePage exchangePage = tester.widget(find.byType(ExchangePage)); + final exchangeViewModel = exchangePage.exchangeViewModel; + final receiveCardPrefix = 'receive_exchange_card'; + + commonTestCases.hasValueKey('${receiveCardPrefix}_title_key'); + commonTestCases.hasValueKey('${receiveCardPrefix}_currency_picker_button_key'); + commonTestCases.hasValueKey('${receiveCardPrefix}_selected_currency_text_key'); + commonTestCases.hasValueKey('${receiveCardPrefix}_amount_textfield_key'); + commonTestCases.hasValueKey('${receiveCardPrefix}_min_limit_text_key'); + + final initialCurrency = exchangeViewModel.receiveCurrency; + if (initialCurrency.tag != null) { + commonTestCases.hasValueKey('${receiveCardPrefix}_selected_currency_tag_text_key'); + } + + if (exchangeViewModel.hasAllAmount) { + commonTestCases.hasValueKey('${receiveCardPrefix}_send_all_button_key'); + } + + if (exchangeViewModel.isMoneroWallet) { + commonTestCases.hasValueKey('${receiveCardPrefix}_address_book_button_key'); + } + + commonTestCases.hasValueKey('${receiveCardPrefix}_editable_address_textfield_key'); + } + + Future selectDepositCurrency(CryptoCurrency depositCurrency) async { + final depositPrefix = 'deposit_exchange_card'; + final currencyPickerKey = '${depositPrefix}_currency_picker_button_key'; + final currencyPickerDialogKey = '${depositPrefix}_currency_picker_dialog_button_key'; + + await commonTestCases.tapItemByKey(currencyPickerKey); + commonTestCases.hasValueKey(currencyPickerDialogKey); + + ExchangePage exchangePage = tester.widget(find.byType(ExchangePage)); + final exchangeViewModel = exchangePage.exchangeViewModel; + + if (depositCurrency == exchangeViewModel.depositCurrency) { + await commonTestCases.defaultSleepTime(); + await commonTestCases + .tapItemByKey('picker_items_index_${depositCurrency.name}_selected_item_button_key'); + return; + } + + await commonTestCases.scrollUntilVisible( + 'picker_items_index_${depositCurrency.name}_button_key', + 'picker_scrollbar_key', + ); + await commonTestCases.defaultSleepTime(); + + await commonTestCases.tapItemByKey('picker_items_index_${depositCurrency.name}_button_key'); + } + + Future selectReceiveCurrency(CryptoCurrency receiveCurrency) async { + final receivePrefix = 'receive_exchange_card'; + final currencyPickerKey = '${receivePrefix}_currency_picker_button_key'; + final currencyPickerDialogKey = '${receivePrefix}_currency_picker_dialog_button_key'; + + await commonTestCases.tapItemByKey(currencyPickerKey); + commonTestCases.hasValueKey(currencyPickerDialogKey); + + ExchangePage exchangePage = tester.widget(find.byType(ExchangePage)); + final exchangeViewModel = exchangePage.exchangeViewModel; + + if (receiveCurrency == exchangeViewModel.receiveCurrency) { + await commonTestCases + .tapItemByKey('picker_items_index_${receiveCurrency.name}_selected_item_button_key'); + return; + } + + await commonTestCases.scrollUntilVisible( + 'picker_items_index_${receiveCurrency.name}_button_key', + 'picker_scrollbar_key', + ); + await commonTestCases.defaultSleepTime(); + + await commonTestCases.tapItemByKey('picker_items_index_${receiveCurrency.name}_button_key'); + } + + Future enterDepositAmount(String amount) async { + await commonTestCases.enterText(amount, 'deposit_exchange_card_amount_textfield_key'); + } + + Future enterDepositRefundAddress({String? depositAddress}) async { + ExchangePage exchangePage = tester.widget(find.byType(ExchangePage)); + final exchangeViewModel = exchangePage.exchangeViewModel; + + if (exchangeViewModel.isDepositAddressEnabled && depositAddress != null) { + await commonTestCases.enterText( + depositAddress, 'deposit_exchange_card_editable_address_textfield_key'); + } + } + + Future enterReceiveAddress(String receiveAddress) async { + await commonTestCases.enterText( + receiveAddress, + 'receive_exchange_card_editable_address_textfield_key', + ); + await commonTestCases.defaultSleepTime(); + } + + Future onExchangeButtonPressed() async { + await commonTestCases.tapItemByKey('exchange_page_exchange_button_key'); + await commonTestCases.defaultSleepTime(); + } + + bool hasMaxLimitError() { + final maxErrorText = find.text(S.current.error_text_input_above_maximum_limit); + + bool hasMaxError = maxErrorText.tryEvaluate(); + + return hasMaxError; + } + + bool hasMinLimitError() { + final minErrorText = find.text(S.current.error_text_input_below_minimum_limit); + + bool hasMinError = minErrorText.tryEvaluate(); + + return hasMinError; + } + + bool hasTradeCreationFailureError() { + final tradeCreationFailureDialogButton = + find.byKey(ValueKey('exchange_page_trade_creation_failure_dialog_button_key')); + + bool hasTradeCreationFailure = tradeCreationFailureDialogButton.tryEvaluate(); + tester.printToConsole('Trade not created error: $hasTradeCreationFailure'); + return hasTradeCreationFailure; + } + + Future onTradeCreationFailureDialogButtonPressed() async { + await commonTestCases.tapItemByKey('exchange_page_trade_creation_failure_dialog_button_key'); + } + + /// Handling Trade Failure Errors or errors shown through the Failure Dialog. + /// + /// Simulating the user's flow and response when this error comes up. + /// Examples are: + /// - No provider can handle this trade error, + /// - Trade amount below limit error. + Future _handleTradeCreationFailureErrors() async { + bool isTradeCreationFailure = false; + + isTradeCreationFailure = hasTradeCreationFailureError(); + + int maxRetries = 20; + int retries = 0; + + while (isTradeCreationFailure && retries < maxRetries) { + await tester.pump(); + + await onTradeCreationFailureDialogButtonPressed(); + + await commonTestCases.defaultSleepTime(seconds: 5); + + await onExchangeButtonPressed(); + + isTradeCreationFailure = hasTradeCreationFailureError(); + retries++; + } + } + + /// Handles the min limit error. + /// + /// Simulates the user's flow and response when it comes up. + /// + /// Has a max retry of 20 times. + Future _handleMinLimitError(String initialAmount) async { + bool isMinLimitError = false; + + isMinLimitError = hasMinLimitError(); + + double amount; + + amount = double.parse(initialAmount); + + int maxRetries = 20; + int retries = 0; + + while (isMinLimitError && retries < maxRetries) { + amount++; + tester.printToConsole('Amount: $amount'); + + enterDepositAmount(amount.toString()); + + await commonTestCases.defaultSleepTime(); + + await onExchangeButtonPressed(); + + isMinLimitError = hasMinLimitError(); + + retries++; + } + + if (retries >= maxRetries) { + tester.printToConsole('Max retries reached for minLimit Error. Exiting loop.'); + } + } + + /// Handles the max limit error. + /// + /// Simulates the user's flow and response when it comes up. + /// + /// Has a max retry of 20 times. + Future _handleMaxLimitError(String initialAmount) async { + bool isMaxLimitError = false; + + isMaxLimitError = hasMaxLimitError(); + + double amount; + + amount = double.parse(initialAmount); + + int maxRetries = 20; + int retries = 0; + + while (isMaxLimitError && retries < maxRetries) { + amount++; + tester.printToConsole('Amount: $amount'); + + enterDepositAmount(amount.toString()); + + await commonTestCases.defaultSleepTime(); + + await onExchangeButtonPressed(); + + isMaxLimitError = hasMaxLimitError(); + + retries++; + } + + if (retries >= maxRetries) { + tester.printToConsole('Max retries reached for maxLimit Error. Exiting loop.'); + } + } + + Future handleErrors(String initialAmount) async { + await tester.pumpAndSettle(); + + await _handleMinLimitError(initialAmount); + + await _handleMaxLimitError(initialAmount); + + await _handleTradeCreationFailureErrors(); + + await commonTestCases.defaultSleepTime(); + } +} diff --git a/integration_test/robots/exchange_trade_page_robot.dart b/integration_test/robots/exchange_trade_page_robot.dart new file mode 100644 index 000000000..5708b6fae --- /dev/null +++ b/integration_test/robots/exchange_trade_page_robot.dart @@ -0,0 +1,152 @@ +import 'dart:async'; + +import 'package:cake_wallet/core/execution_state.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/screens/exchange_trade/exchange_trade_page.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../components/common_test_cases.dart'; + +class ExchangeTradePageRobot { + ExchangeTradePageRobot(this.tester) : commonTestCases = CommonTestCases(tester); + + final WidgetTester tester; + late CommonTestCases commonTestCases; + + Future isExchangeTradePage() async { + await commonTestCases.isSpecificPage(); + } + + void hasInformationDialog() { + commonTestCases.hasValueKey('information_page_dialog_key'); + } + + Future onGotItButtonPressed() async { + await commonTestCases.tapItemByKey('information_page_got_it_button_key'); + await commonTestCases.defaultSleepTime(); + } + + Future onConfirmSendingButtonPressed() async { + tester.printToConsole('Now confirming sending'); + + await commonTestCases.tapItemByKey( + 'exchange_trade_page_confirm_sending_button_key', + shouldPumpAndSettle: false, + ); + + final Completer completer = Completer(); + + // Loop to wait for the async operation to complete + while (true) { + await Future.delayed(Duration(seconds: 1)); + + final ExchangeTradeState state = tester.state(find.byType(ExchangeTradeForm)); + final execState = state.widget.exchangeTradeViewModel.sendViewModel.state; + + bool isDone = execState is ExecutedSuccessfullyState; + bool isFailed = execState is FailureState; + + tester.printToConsole('isDone: $isDone'); + tester.printToConsole('isFailed: $isFailed'); + + if (isDone || isFailed) { + tester.printToConsole( + isDone ? 'Completer is done' : 'Completer is done though operation failed'); + completer.complete(); + await tester.pump(); + break; + } else { + tester.printToConsole('Completer is not done'); + await tester.pump(); + } + } + + await expectLater(completer.future, completes); + + tester.printToConsole('Done confirming sending'); + + await commonTestCases.defaultSleepTime(seconds: 4); + } + + Future onSendButtonOnConfirmSendingDialogPressed() async { + tester.printToConsole('Send Button on Confirm Dialog Triggered'); + await commonTestCases.defaultSleepTime(seconds: 4); + + final sendText = find.text(S.current.send); + bool hasText = sendText.tryEvaluate(); + + if (hasText) { + await commonTestCases.tapItemByFinder(sendText); + + await commonTestCases.defaultSleepTime(seconds: 4); + } + } + + Future onCancelButtonOnConfirmSendingDialogPressed() async { + tester.printToConsole('Cancel Button on Confirm Dialog Triggered'); + + await commonTestCases.tapItemByKey( + 'exchange_trade_page_confirm_sending_dialog_cancel_button_key', + ); + + await commonTestCases.defaultSleepTime(); + } + + Future onSendFailureDialogButtonPressed() async { + await commonTestCases.defaultSleepTime(seconds: 6); + + tester.printToConsole('Send Button Failure Dialog Triggered'); + + await commonTestCases.tapItemByKey('exchange_trade_page_send_failure_dialog_button_key'); + } + + Future hasErrorWhileSending() async { + await tester.pump(); + + tester.printToConsole('Checking if there is an error'); + + final errorDialog = find.byKey( + ValueKey('exchange_trade_page_send_failure_dialog_button_key'), + ); + + bool hasError = errorDialog.tryEvaluate(); + + tester.printToConsole('Has error: $hasError'); + + return hasError; + } + + Future handleConfirmSendResult() async { + bool hasError = false; + + hasError = await hasErrorWhileSending(); + + int maxRetries = 20; + int retries = 0; + + while (hasError && retries < maxRetries) { + tester.printToConsole('hasErrorInLoop: $hasError'); + await tester.pump(); + + await onSendFailureDialogButtonPressed(); + tester.printToConsole('Failure button tapped'); + + await commonTestCases.defaultSleepTime(); + + await onConfirmSendingButtonPressed(); + tester.printToConsole('Confirm sending button tapped'); + + hasError = await hasErrorWhileSending(); + + retries++; + } + + if (!hasError) { + tester.printToConsole('No error, proceeding with flow'); + await tester.pump(); + } + + await commonTestCases.defaultSleepTime(); + } +} diff --git a/integration_test/robots/new_wallet_type_page_robot.dart b/integration_test/robots/new_wallet_type_page_robot.dart new file mode 100644 index 000000000..89fc8d390 --- /dev/null +++ b/integration_test/robots/new_wallet_type_page_robot.dart @@ -0,0 +1,59 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/screens/new_wallet/new_wallet_type_page.dart'; +import 'package:cake_wallet/themes/theme_base.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../components/common_test_cases.dart'; + +class NewWalletTypePageRobot { + NewWalletTypePageRobot(this.tester) : commonTestCases = CommonTestCases(tester); + + final WidgetTester tester; + late CommonTestCases commonTestCases; + + Future isNewWalletTypePage() async { + await commonTestCases.isSpecificPage(); + } + + void displaysCorrectTitle(bool isCreate) { + commonTestCases.hasText( + isCreate ? S.current.wallet_list_create_new_wallet : S.current.wallet_list_restore_wallet, + ); + } + + void hasWalletTypeForm() { + commonTestCases.hasType(); + } + + void displaysCorrectImage(ThemeType type) { + final walletTypeImage = Image.asset('assets/images/wallet_type.png').image; + final walletTypeLightImage = Image.asset('assets/images/wallet_type_light.png').image; + + find.image( + type == ThemeType.dark ? walletTypeImage : walletTypeLightImage, + ); + } + + Future findParticularWalletTypeInScrollableList(WalletType type) async { + final scrollableWidget = find.descendant( + of: find.byKey(Key('new_wallet_type_scrollable_key')), + matching: find.byType(Scrollable), + ); + + await tester.scrollUntilVisible( + find.byKey(ValueKey('new_wallet_type_${type.name}_button_key')), + 300, + scrollable: scrollableWidget, + ); + } + + Future selectWalletType(WalletType type) async { + await commonTestCases.tapItemByKey('new_wallet_type_${type.name}_button_key'); + } + + Future onNextButtonPressed() async { + await commonTestCases.tapItemByKey('new_wallet_type_next_button_key'); + } +} diff --git a/integration_test/robots/pin_code_widget_robot.dart b/integration_test/robots/pin_code_widget_robot.dart new file mode 100644 index 000000000..b6805e9e0 --- /dev/null +++ b/integration_test/robots/pin_code_widget_robot.dart @@ -0,0 +1,38 @@ +import 'package:cake_wallet/src/screens/pin_code/pin_code_widget.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../components/common_test_cases.dart'; + +class PinCodeWidgetRobot { + PinCodeWidgetRobot(this.tester) : commonTestCases = CommonTestCases(tester); + + final WidgetTester tester; + late CommonTestCases commonTestCases; + + void hasPinCodeWidget() { + final pinCodeWidget = find.bySubtype(); + expect(pinCodeWidget, findsOneWidget); + } + + void hasNumberButtonsVisible() { + // Confirmation for buttons 1-9 + for (var i = 1; i < 10; i++) { + commonTestCases.hasValueKey('pin_code_button_${i}_key'); + } + + // Confirmation for 0 button + commonTestCases.hasValueKey('pin_code_button_0_key'); + } + + Future pushPinButton(int index) async { + await commonTestCases.tapItemByKey('pin_code_button_${index}_key'); + } + + Future enterPinCode(List pinCode, bool isFirstEntry) async { + for (int pin in pinCode) { + await pushPinButton(pin); + } + + await commonTestCases.defaultSleepTime(); + } +} diff --git a/integration_test/robots/restore_from_seed_or_key_robot.dart b/integration_test/robots/restore_from_seed_or_key_robot.dart new file mode 100644 index 000000000..43a65095d --- /dev/null +++ b/integration_test/robots/restore_from_seed_or_key_robot.dart @@ -0,0 +1,89 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/screens/restore/wallet_restore_page.dart'; +import 'package:cake_wallet/src/widgets/validable_annotated_editable_text.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../components/common_test_cases.dart'; + +class RestoreFromSeedOrKeysPageRobot { + RestoreFromSeedOrKeysPageRobot(this.tester) : commonTestCases = CommonTestCases(tester); + + final WidgetTester tester; + late CommonTestCases commonTestCases; + + Future isRestoreFromSeedKeyPage() async { + await commonTestCases.isSpecificPage(); + } + + Future confirmViewComponentsDisplayProperlyPerPageView() async { + commonTestCases.hasText(S.current.wallet_name); + commonTestCases.hasText(S.current.enter_seed_phrase); + commonTestCases.hasText(S.current.restore_title_from_seed); + + commonTestCases.hasValueKey('wallet_restore_from_seed_wallet_name_textfield_key'); + commonTestCases.hasValueKey('wallet_restore_from_seed_wallet_name_refresh_button_key'); + commonTestCases.hasValueKey('wallet_restore_from_seed_wallet_seeds_paste_button_key'); + commonTestCases.hasValueKey('wallet_restore_from_seed_wallet_seeds_textfield_key'); + + commonTestCases.hasText(S.current.private_key, hasWidget: false); + commonTestCases.hasText(S.current.restore_title_from_keys, hasWidget: false); + + await commonTestCases.swipePage(); + await commonTestCases.defaultSleepTime(); + + commonTestCases.hasText(S.current.wallet_name); + commonTestCases.hasText(S.current.private_key); + commonTestCases.hasText(S.current.restore_title_from_keys); + + commonTestCases.hasText(S.current.enter_seed_phrase, hasWidget: false); + commonTestCases.hasText(S.current.restore_title_from_seed, hasWidget: false); + + await commonTestCases.swipePage(swipeRight: false); + } + + void confirmRestoreButtonDisplays() { + commonTestCases.hasValueKey('wallet_restore_seed_or_key_restore_button_key'); + } + + void confirmAdvancedSettingButtonDisplays() { + commonTestCases.hasValueKey('wallet_restore_advanced_settings_button_key'); + } + + Future enterWalletNameText(String walletName, {bool isSeedFormEntry = true}) async { + await commonTestCases.enterText( + walletName, + 'wallet_restore_from_${isSeedFormEntry ? 'seed' : 'keys'}_wallet_name_textfield_key', + ); + } + + Future selectWalletNameFromAvailableOptions({bool isSeedFormEntry = true}) async { + await commonTestCases.tapItemByKey( + 'wallet_restore_from_${isSeedFormEntry ? 'seed' : 'keys'}_wallet_name_refresh_button_key', + ); + } + + Future enterSeedPhraseForWalletRestore(String text) async { + ValidatableAnnotatedEditableTextState seedTextState = + await tester.state(find.byType(ValidatableAnnotatedEditableText)); + + seedTextState.widget.controller.text = text; + await tester.pumpAndSettle(); + } + + Future onPasteSeedPhraseButtonPressed() async { + await commonTestCases.tapItemByKey('wallet_restore_from_seed_wallet_seeds_paste_button_key'); + } + + Future enterPrivateKeyForWalletRestore(String privateKey) async { + await commonTestCases.enterText( + privateKey, + 'wallet_restore_from_key_private_key_textfield_key', + ); + await tester.pumpAndSettle(); + } + + Future onRestoreWalletButtonPressed() async { + await commonTestCases.tapItemByKey('wallet_restore_seed_or_key_restore_button_key'); + await commonTestCases.defaultSleepTime(); + } +} diff --git a/integration_test/robots/restore_options_page_robot.dart b/integration_test/robots/restore_options_page_robot.dart new file mode 100644 index 000000000..b3cefc90c --- /dev/null +++ b/integration_test/robots/restore_options_page_robot.dart @@ -0,0 +1,42 @@ +import 'package:cake_wallet/src/screens/restore/restore_options_page.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../components/common_test_cases.dart'; + +class RestoreOptionsPageRobot { + RestoreOptionsPageRobot(this.tester) : commonTestCases = CommonTestCases(tester); + + final WidgetTester tester; + late CommonTestCases commonTestCases; + + Future isRestoreOptionsPage() async { + await commonTestCases.isSpecificPage(); + } + + void hasRestoreOptionsButton() { + commonTestCases.hasValueKey('restore_options_from_seeds_button_key'); + commonTestCases.hasValueKey('restore_options_from_backup_button_key'); + commonTestCases.hasValueKey('restore_options_from_hardware_wallet_button_key'); + commonTestCases.hasValueKey('restore_options_from_qr_button_key'); + } + + Future navigateToRestoreFromSeedsPage() async { + await commonTestCases.tapItemByKey('restore_options_from_seeds_button_key'); + await commonTestCases.defaultSleepTime(); + } + + Future navigateToRestoreFromBackupPage() async { + await commonTestCases.tapItemByKey('restore_options_from_backup_button_key'); + await commonTestCases.defaultSleepTime(); + } + + Future navigateToRestoreFromHardwareWalletPage() async { + await commonTestCases.tapItemByKey('restore_options_from_hardware_wallet_button_key'); + await commonTestCases.defaultSleepTime(); + } + + Future backAndVerify() async { + await commonTestCases.goBack(); + await isRestoreOptionsPage(); + } +} diff --git a/integration_test/robots/send_page_robot.dart b/integration_test/robots/send_page_robot.dart new file mode 100644 index 000000000..971556620 --- /dev/null +++ b/integration_test/robots/send_page_robot.dart @@ -0,0 +1,366 @@ +import 'dart:async'; + +import 'package:cake_wallet/core/execution_state.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/screens/send/send_page.dart'; +import 'package:cake_wallet/view_model/send/send_view_model_state.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/transaction_priority.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../components/common_test_cases.dart'; +import '../components/common_test_constants.dart'; +import 'auth_page_robot.dart'; + +class SendPageRobot { + SendPageRobot({required this.tester}) + : commonTestCases = CommonTestCases(tester), + authPageRobot = AuthPageRobot(tester); + + WidgetTester tester; + CommonTestCases commonTestCases; + AuthPageRobot authPageRobot; + + Future isSendPage() async { + await commonTestCases.isSpecificPage(); + } + + void hasTitle() { + commonTestCases.hasText(S.current.send); + } + + void confirmViewComponentsDisplayProperly() { + SendPage sendPage = tester.widget(find.byType(SendPage)); + final sendViewModel = sendPage.sendViewModel; + + commonTestCases.hasValueKey('send_page_address_textfield_key'); + commonTestCases.hasValueKey('send_page_note_textfield_key'); + commonTestCases.hasValueKey('send_page_amount_textfield_key'); + commonTestCases.hasValueKey('send_page_add_template_button_key'); + + if (sendViewModel.hasMultipleTokens) { + commonTestCases.hasValueKey('send_page_currency_picker_button_key'); + } + + if (!sendViewModel.isBatchSending) { + commonTestCases.hasValueKey('send_page_send_all_button_key'); + } + + if (!sendViewModel.isFiatDisabled) { + commonTestCases.hasValueKey('send_page_fiat_amount_textfield_key'); + } + + if (sendViewModel.hasFees) { + commonTestCases.hasValueKey('send_page_select_fee_priority_button_key'); + } + + if (sendViewModel.hasCoinControl) { + commonTestCases.hasValueKey('send_page_unspent_coin_button_key'); + } + + if (sendViewModel.hasCurrecyChanger) { + commonTestCases.hasValueKey('send_page_change_asset_button_key'); + } + + if (sendViewModel.sendTemplateViewModel.hasMultiRecipient) { + commonTestCases.hasValueKey('send_page_add_receiver_button_key'); + } + } + + Future selectReceiveCurrency(CryptoCurrency receiveCurrency) async { + final currencyPickerKey = 'send_page_currency_picker_button_key'; + final currencyPickerDialogKey = 'send_page_currency_picker_dialog_button_key'; + + await commonTestCases.tapItemByKey(currencyPickerKey); + commonTestCases.hasValueKey(currencyPickerDialogKey); + + SendPage sendPage = tester.widget(find.byType(SendPage)); + final sendViewModel = sendPage.sendViewModel; + + if (receiveCurrency == sendViewModel.selectedCryptoCurrency) { + await commonTestCases + .tapItemByKey('picker_items_index_${receiveCurrency.name}_selected_item_button_key'); + return; + } + + await commonTestCases.scrollUntilVisible( + 'picker_items_index_${receiveCurrency.name}_button_key', + 'picker_scrollbar_key', + ); + await commonTestCases.defaultSleepTime(); + + await commonTestCases.tapItemByKey('picker_items_index_${receiveCurrency.name}_button_key'); + } + + Future enterReceiveAddress(String receiveAddress) async { + await commonTestCases.enterText(receiveAddress, 'send_page_address_textfield_key'); + await commonTestCases.defaultSleepTime(); + } + + Future enterAmount(String amount) async { + await commonTestCases.enterText(amount, 'send_page_amount_textfield_key'); + } + + Future selectTransactionPriority({TransactionPriority? priority}) async { + SendPage sendPage = tester.widget(find.byType(SendPage)); + final sendViewModel = sendPage.sendViewModel; + + if (!sendViewModel.hasFees || priority == null) return; + + final transactionPriorityPickerKey = 'send_page_select_fee_priority_button_key'; + await commonTestCases.tapItemByKey(transactionPriorityPickerKey); + + if (priority == sendViewModel.transactionPriority) { + await commonTestCases + .tapItemByKey('picker_items_index_${priority.title}_selected_item_button_key'); + return; + } + + await commonTestCases.scrollUntilVisible( + 'picker_items_index_${priority.title}_button_key', + 'picker_scrollbar_key', + ); + await commonTestCases.defaultSleepTime(); + + await commonTestCases.tapItemByKey('picker_items_index_${priority.title}_button_key'); + } + + Future onSendButtonPressed() async { + tester.printToConsole('Pressing send'); + + await commonTestCases.tapItemByKey( + 'send_page_send_button_key', + shouldPumpAndSettle: false, + ); + + await _waitForSendTransactionCompletion(); + + await commonTestCases.defaultSleepTime(); + } + + Future _waitForSendTransactionCompletion() async { + await tester.pump(); + final Completer completer = Completer(); + + // Loop to wait for the async operation to complete + while (true) { + await Future.delayed(Duration(seconds: 1)); + + tester.printToConsole('Before _handleAuth'); + + await _handleAuthPage(); + + tester.printToConsole('After _handleAuth'); + + await tester.pump(); + + final sendPage = tester.widget(find.byType(SendPage)); + final state = sendPage.sendViewModel.state; + + await tester.pump(); + + bool isDone = state is ExecutedSuccessfullyState; + bool isFailed = state is FailureState; + + tester.printToConsole('isDone: $isDone'); + tester.printToConsole('isFailed: $isFailed'); + + if (isDone || isFailed) { + tester.printToConsole( + isDone ? 'Completer is done' : 'Completer is done though operation failed', + ); + completer.complete(); + await tester.pump(); + break; + } else { + tester.printToConsole('Completer is not done'); + await tester.pump(); + } + } + + await expectLater(completer.future, completes); + + tester.printToConsole('Done confirming sending operation'); + } + + Future _handleAuthPage() async { + tester.printToConsole('Inside _handleAuth'); + await tester.pump(); + tester.printToConsole('starting auth checks'); + + final authPage = authPageRobot.onAuthPage(); + + tester.printToConsole('hasAuth:$authPage'); + + if (authPage) { + await tester.pump(); + tester.printToConsole('Starting inner _handleAuth loop checks'); + + try { + await authPageRobot.enterPinCode(CommonTestConstants.pin, false); + tester.printToConsole('Auth done'); + + await tester.pump(); + + tester.printToConsole('Auth pump done'); + } catch (e) { + tester.printToConsole('Auth failed, retrying'); + await tester.pump(); + _handleAuthPage(); + } + } + } + + Future handleSendResult() async { + tester.printToConsole('Inside handle function'); + + bool hasError = false; + + hasError = await hasErrorWhileSending(); + + tester.printToConsole('Has an Error in the handle: $hasError'); + + int maxRetries = 20; + int retries = 0; + + while (hasError && retries < maxRetries) { + tester.printToConsole('hasErrorInLoop: $hasError'); + await tester.pump(); + + await onSendFailureDialogButtonPressed(); + tester.printToConsole('Failure button tapped'); + + await commonTestCases.defaultSleepTime(); + + await onSendButtonPressed(); + tester.printToConsole('Send button tapped'); + + hasError = await hasErrorWhileSending(); + + retries++; + } + + if (!hasError) { + tester.printToConsole('No error, proceeding with flow'); + await tester.pump(); + } + + await commonTestCases.defaultSleepTime(); + } + + //* ------ On Sending Failure ------------ + Future hasErrorWhileSending() async { + await tester.pump(); + + tester.printToConsole('Checking if there is an error'); + + final errorDialog = find.byKey(ValueKey('send_page_send_failure_dialog_button_key')); + + bool hasError = errorDialog.tryEvaluate(); + + tester.printToConsole('Has error: $hasError'); + + return hasError; + } + + Future onSendFailureDialogButtonPressed() async { + await commonTestCases.defaultSleepTime(); + + tester.printToConsole('Send Button Failure Dialog Triggered'); + + await commonTestCases.tapItemByKey('send_page_send_failure_dialog_button_key'); + } + + //* ------ On Sending Success ------------ + Future onSendButtonOnConfirmSendingDialogPressed() async { + tester.printToConsole('Inside confirm sending dialog: For sending'); + await commonTestCases.defaultSleepTime(); + await tester.pump(); + + final sendText = find.text(S.current.send).last; + bool hasText = sendText.tryEvaluate(); + tester.printToConsole('Has Text: $hasText'); + + if (hasText) { + await commonTestCases.tapItemByFinder(sendText, shouldPumpAndSettle: false); + // Loop to wait for the operation to commit transaction + await _waitForCommitTransactionCompletion(); + + await commonTestCases.defaultSleepTime(seconds: 4); + } else { + await commonTestCases.defaultSleepTime(); + await tester.pump(); + onSendButtonOnConfirmSendingDialogPressed(); + } + } + + Future _waitForCommitTransactionCompletion() async { + final Completer completer = Completer(); + + while (true) { + await Future.delayed(Duration(seconds: 1)); + + final sendPage = tester.widget(find.byType(SendPage)); + final state = sendPage.sendViewModel.state; + + bool isDone = state is TransactionCommitted; + bool isFailed = state is FailureState; + + tester.printToConsole('isDone: $isDone'); + tester.printToConsole('isFailed: $isFailed'); + + if (isDone || isFailed) { + tester.printToConsole( + isDone ? 'Completer is done' : 'Completer is done though operation failed', + ); + completer.complete(); + await tester.pump(); + break; + } else { + tester.printToConsole('Completer is not done'); + await tester.pump(); + } + } + + await expectLater(completer.future, completes); + + tester.printToConsole('Done Committing Transaction'); + } + + Future onCancelButtonOnConfirmSendingDialogPressed() async { + tester.printToConsole('Inside confirm sending dialog: For canceling'); + await commonTestCases.defaultSleepTime(seconds: 4); + + final cancelText = find.text(S.current.cancel); + bool hasText = cancelText.tryEvaluate(); + + if (hasText) { + await commonTestCases.tapItemByFinder(cancelText); + + await commonTestCases.defaultSleepTime(seconds: 4); + } + } + + //* ---- Add Contact Dialog On Send Successful Dialog ----- + Future onSentDialogPopUp() async { + SendPage sendPage = tester.widget(find.byType(SendPage)); + final sendViewModel = sendPage.sendViewModel; + + final newContactAddress = sendPage.newContactAddress ?? sendViewModel.newContactAddress(); + if (newContactAddress != null) { + await _onAddContactButtonOnSentDialogPressed(); + } + + await commonTestCases.defaultSleepTime(); + } + + Future _onAddContactButtonOnSentDialogPressed() async { + await commonTestCases.tapItemByKey('send_page_sent_dialog_add_contact_button_key'); + } + + // ignore: unused_element + Future _onIgnoreButtonOnSentDialogPressed() async { + await commonTestCases.tapItemByKey('send_page_sent_dialog_ignore_button_key'); + } +} \ No newline at end of file diff --git a/integration_test/robots/setup_pin_code_robot.dart b/integration_test/robots/setup_pin_code_robot.dart new file mode 100644 index 000000000..0888aac30 --- /dev/null +++ b/integration_test/robots/setup_pin_code_robot.dart @@ -0,0 +1,28 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/screens/setup_pin_code/setup_pin_code.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../components/common_test_cases.dart'; +import 'pin_code_widget_robot.dart'; + +class SetupPinCodeRobot extends PinCodeWidgetRobot { + SetupPinCodeRobot(this.tester) + : commonTestCases = CommonTestCases(tester), + super(tester); + + final WidgetTester tester; + late CommonTestCases commonTestCases; + + Future isSetupPinCodePage() async { + await commonTestCases.isSpecificPage(); + } + + void hasTitle() { + commonTestCases.hasText(S.current.setup_pin); + } + + Future tapSuccessButton() async { + await commonTestCases.tapItemByKey('setup_pin_code_success_button_key'); + await commonTestCases.defaultSleepTime(); + } +} diff --git a/integration_test/robots/welcome_page_robot.dart b/integration_test/robots/welcome_page_robot.dart new file mode 100644 index 000000000..510f63556 --- /dev/null +++ b/integration_test/robots/welcome_page_robot.dart @@ -0,0 +1,40 @@ +import 'package:cake_wallet/src/screens/welcome/welcome_page.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../components/common_test_cases.dart'; + +class WelcomePageRobot { + WelcomePageRobot(this.tester) : commonTestCases = CommonTestCases(tester); + + final WidgetTester tester; + late CommonTestCases commonTestCases; + + Future isWelcomePage() async { + await commonTestCases.isSpecificPage(); + } + + void confirmActionButtonsDisplay() { + final createNewWalletButton = find.byKey(ValueKey('welcome_page_create_new_wallet_button_key')); + + final restoreWalletButton = find.byKey(ValueKey('welcome_page_restore_wallet_button_key')); + + expect(createNewWalletButton, findsOneWidget); + expect(restoreWalletButton, findsOneWidget); + } + + Future navigateToCreateNewWalletPage() async { + await commonTestCases.tapItemByKey('welcome_page_create_new_wallet_button_key'); + await commonTestCases.defaultSleepTime(); + } + + Future navigateToRestoreWalletPage() async { + await commonTestCases.tapItemByKey('welcome_page_restore_wallet_button_key'); + await commonTestCases.defaultSleepTime(); + } + + Future backAndVerify() async { + await commonTestCases.goBack(); + await isWelcomePage(); + } +} diff --git a/integration_test/test_suites/exchange_flow_test.dart b/integration_test/test_suites/exchange_flow_test.dart new file mode 100644 index 000000000..6c993634c --- /dev/null +++ b/integration_test/test_suites/exchange_flow_test.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import '../components/common_test_constants.dart'; +import '../components/common_test_flows.dart'; +import '../robots/auth_page_robot.dart'; +import '../robots/dashboard_page_robot.dart'; +import '../robots/exchange_confirm_page_robot.dart'; +import '../robots/exchange_page_robot.dart'; +import '../robots/exchange_trade_page_robot.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + AuthPageRobot authPageRobot; + CommonTestFlows commonTestFlows; + ExchangePageRobot exchangePageRobot; + DashboardPageRobot dashboardPageRobot; + ExchangeTradePageRobot exchangeTradePageRobot; + ExchangeConfirmPageRobot exchangeConfirmPageRobot; + + group('Exchange Flow Tests', () { + testWidgets('Exchange flow', (tester) async { + authPageRobot = AuthPageRobot(tester); + commonTestFlows = CommonTestFlows(tester); + exchangePageRobot = ExchangePageRobot(tester); + dashboardPageRobot = DashboardPageRobot(tester); + exchangeTradePageRobot = ExchangeTradePageRobot(tester); + exchangeConfirmPageRobot = ExchangeConfirmPageRobot(tester); + + await commonTestFlows.startAppFlow(ValueKey('exchange_app_test_key')); + await commonTestFlows.restoreWalletThroughSeedsFlow(); + await dashboardPageRobot.navigateToExchangePage(); + + // ----------- Exchange Page ------------- + await exchangePageRobot.selectDepositCurrency(CommonTestConstants.testDepositCurrency); + await exchangePageRobot.selectReceiveCurrency(CommonTestConstants.testReceiveCurrency); + + await exchangePageRobot.enterDepositAmount(CommonTestConstants.exchangeTestAmount); + await exchangePageRobot.enterDepositRefundAddress( + depositAddress: CommonTestConstants.testWalletAddress, + ); + await exchangePageRobot.enterReceiveAddress(CommonTestConstants.testWalletAddress); + + await exchangePageRobot.onExchangeButtonPressed(); + + await exchangePageRobot.handleErrors(CommonTestConstants.exchangeTestAmount); + + final onAuthPage = authPageRobot.onAuthPage(); + if (onAuthPage) { + await authPageRobot.enterPinCode(CommonTestConstants.pin, false); + } + + await exchangeConfirmPageRobot.onSavedTradeIdButtonPressed(); + await exchangeTradePageRobot.onGotItButtonPressed(); + }); + }); +} diff --git a/integration_test/test_suites/send_flow_test.dart b/integration_test/test_suites/send_flow_test.dart new file mode 100644 index 000000000..38ac1574f --- /dev/null +++ b/integration_test/test_suites/send_flow_test.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import '../components/common_test_constants.dart'; +import '../components/common_test_flows.dart'; +import '../robots/dashboard_page_robot.dart'; +import '../robots/send_page_robot.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + SendPageRobot sendPageRobot; + CommonTestFlows commonTestFlows; + DashboardPageRobot dashboardPageRobot; + + group('Send Flow Tests', () { + testWidgets('Send flow', (tester) async { + commonTestFlows = CommonTestFlows(tester); + sendPageRobot = SendPageRobot(tester: tester); + dashboardPageRobot = DashboardPageRobot(tester); + + await commonTestFlows.startAppFlow(ValueKey('send_test_app_key')); + await commonTestFlows.restoreWalletThroughSeedsFlow(); + await dashboardPageRobot.navigateToSendPage(); + + await sendPageRobot.enterReceiveAddress(CommonTestConstants.testWalletAddress); + await sendPageRobot.selectReceiveCurrency(CommonTestConstants.testReceiveCurrency); + await sendPageRobot.enterAmount(CommonTestConstants.sendTestAmount); + await sendPageRobot.selectTransactionPriority(); + + await sendPageRobot.onSendButtonPressed(); + + await sendPageRobot.handleSendResult(); + + await sendPageRobot.onSendButtonOnConfirmSendingDialogPressed(); + + await sendPageRobot.onSentDialogPopUp(); + }); + }); +} diff --git a/ios/.gitignore b/ios/.gitignore index e96ef602b..8ded86f14 100644 --- a/ios/.gitignore +++ b/ios/.gitignore @@ -30,3 +30,5 @@ Runner/GeneratedPluginRegistrant.* !default.mode2v3 !default.pbxuser !default.perspectivev3 + +Mwebd.xcframework \ No newline at end of file diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 212d1ec1c..322ef6f86 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -3,7 +3,6 @@ PODS: - Flutter - MTBBarcodeScanner - SwiftProtobuf - - BigInt (5.2.0) - connectivity_plus (0.0.1): - Flutter - ReachabilitySwift @@ -27,6 +26,8 @@ PODS: - cw_haven/Sodium (0.0.1): - cw_shared_external - Flutter + - cw_mweb (0.0.1): + - Flutter - cw_shared_external (0.0.1): - cw_shared_external/Boost (= 0.0.1) - cw_shared_external/OpenSSL (= 0.0.1) @@ -97,10 +98,10 @@ PODS: - Toast - in_app_review (0.2.0): - Flutter + - integration_test (0.0.1): + - Flutter - MTBBarcodeScanner (5.0.11) - OrderedSet (5.0.0) - - package_info (0.0.1): - - Flutter - package_info_plus (0.4.5): - Flutter - path_provider_foundation (0.0.1): @@ -126,14 +127,11 @@ PODS: - FlutterMacOS - sp_scanner (0.0.1): - Flutter - - SwiftProtobuf (1.26.0) + - SwiftProtobuf (1.27.1) - SwiftyGif (5.4.5) - Toast (4.1.1) - uni_links (0.0.1): - Flutter - - UnstoppableDomainsResolution (4.0.0): - - BigInt - - CryptoSwift - url_launcher_ios (0.0.1): - Flutter - wakelock_plus (0.0.1): @@ -146,6 +144,7 @@ DEPENDENCIES: - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) - CryptoSwift - cw_haven (from `.symlinks/plugins/cw_haven/ios`) + - cw_mweb (from `.symlinks/plugins/cw_mweb/ios`) - cw_shared_external (from `.symlinks/plugins/cw_shared_external/ios`) - device_display_brightness (from `.symlinks/plugins/device_display_brightness/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) @@ -158,7 +157,7 @@ DEPENDENCIES: - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`) - in_app_review (from `.symlinks/plugins/in_app_review/ios`) - - package_info (from `.symlinks/plugins/package_info/ios`) + - integration_test (from `.symlinks/plugins/integration_test/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) @@ -168,14 +167,12 @@ DEPENDENCIES: - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - sp_scanner (from `.symlinks/plugins/sp_scanner/ios`) - uni_links (from `.symlinks/plugins/uni_links/ios`) - - UnstoppableDomainsResolution (~> 4.0.0) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`) - workmanager (from `.symlinks/plugins/workmanager/ios`) SPEC REPOS: https://github.com/CocoaPods/Specs.git: - - BigInt - CryptoSwift - DKImagePickerController - DKPhotoGallery @@ -187,7 +184,6 @@ SPEC REPOS: - SwiftProtobuf - SwiftyGif - Toast - - UnstoppableDomainsResolution EXTERNAL SOURCES: barcode_scan2: @@ -196,6 +192,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/connectivity_plus/ios" cw_haven: :path: ".symlinks/plugins/cw_haven/ios" + cw_mweb: + :path: ".symlinks/plugins/cw_mweb/ios" cw_shared_external: :path: ".symlinks/plugins/cw_shared_external/ios" device_display_brightness: @@ -220,8 +218,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/fluttertoast/ios" in_app_review: :path: ".symlinks/plugins/in_app_review/ios" - package_info: - :path: ".symlinks/plugins/package_info/ios" + integration_test: + :path: ".symlinks/plugins/integration_test/ios" package_info_plus: :path: ".symlinks/plugins/package_info_plus/ios" path_provider_foundation: @@ -249,10 +247,10 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: barcode_scan2: 0af2bb63c81b4565aab6cd78278e4c0fa136dbb0 - BigInt: f668a80089607f521586bbe29513d708491ef2f7 connectivity_plus: bf0076dd84a130856aa636df1c71ccaff908fa1d CryptoSwift: c63a805d8bb5e5538e88af4e44bb537776af11ea cw_haven: b3e54e1fbe7b8e6fda57a93206bc38f8e89b898a + cw_mweb: 87af74f9659fed0c1a2cbfb44413f1070e79e3ae cw_shared_external: 2972d872b8917603478117c9957dfca611845a92 device_display_brightness: 1510e72c567a1f6ce6ffe393dcd9afd1426034f7 device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6 @@ -267,9 +265,9 @@ SPEC CHECKSUMS: flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be fluttertoast: 48c57db1b71b0ce9e6bba9f31c940ff4b001293c in_app_review: 318597b3a06c22bb46dc454d56828c85f444f99d + integration_test: 13825b8a9334a850581300559b8839134b124670 MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c - package_info: 873975fc26034f0b863a300ad47e7f1ac6c7ec62 package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6 @@ -281,15 +279,14 @@ SPEC CHECKSUMS: share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 sp_scanner: eaa617fa827396b967116b7f1f43549ca62e9a12 - SwiftProtobuf: 5e8349171e7c2f88f5b9e683cb3cb79d1dc780b3 + SwiftProtobuf: b109bd17979d7993a84da14b1e1fdd8b0ded934a SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 Toast: 1f5ea13423a1e6674c4abdac5be53587ae481c4e uni_links: d97da20c7701486ba192624d99bffaaffcfc298a - UnstoppableDomainsResolution: c3c67f4d0a5e2437cb00d4bd50c2e00d6e743841 url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe wakelock_plus: 78ec7c5b202cab7761af8e2b2b3d0671be6c4ae1 workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6 -PODFILE CHECKSUM: a2fe518be61cdbdc5b0e2da085ab543d556af2d3 +PODFILE CHECKSUM: e448f662d4c41f0c0b1ccbb78afd57dbf895a597 COCOAPODS: 1.15.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 688fa2c39..09c75feee 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -14,12 +14,12 @@ 2193F104374FA2746CE8945B /* ResourceHelper.swift in Resources */ = {isa = PBXBuildFile; fileRef = 78D25C60B94E9D9E48D52E5E /* ResourceHelper.swift */; settings = {ASSET_TAGS = (BreezSDK, ); }; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 495FEFF9B395392FED3425DE /* TaskProtocol.swift in Resources */ = {isa = PBXBuildFile; fileRef = 0F42D8065219E0653321EE2B /* TaskProtocol.swift */; settings = {ASSET_TAGS = (BreezSDK, ); }; }; - 4DFD1BB54A3A50573E19A583 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3C663361C56EBB242598F609 /* Pods_Runner.framework */; }; 525A2200C6C2A43EDC5C8FC5 /* BreezSDKConnector.swift in Resources */ = {isa = PBXBuildFile; fileRef = 1FB06A93B13D606F06B3924D /* BreezSDKConnector.swift */; settings = {ASSET_TAGS = (BreezSDK, ); }; }; 6909E1D79C9986ADF2DE41E9 /* LnurlPayInvoice.swift in Resources */ = {isa = PBXBuildFile; fileRef = DCEA540E3586164FB47AD13E /* LnurlPayInvoice.swift */; settings = {ASSET_TAGS = (BreezSDK, ); }; }; 724FDA327BF191BC29DCAA2E /* Constants.swift in Resources */ = {isa = PBXBuildFile; fileRef = 0CCA7ADAD6FF9185EBBB2BCA /* Constants.swift */; settings = {ASSET_TAGS = (BreezSDK, ); }; }; 73138617307FA4F838D21D62 /* ServiceLogger.swift in Resources */ = {isa = PBXBuildFile; fileRef = F42258C3697CFE3C8C8D1933 /* ServiceLogger.swift */; settings = {ASSET_TAGS = (BreezSDK, ); }; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 8B1F4FCAA5EB9F3A83D32D5F /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D7CD6B6020744E8FA471915D /* Pods_Runner.framework */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; @@ -28,6 +28,7 @@ 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, ); }; }; + 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, ); }; }; D3AD73A327249AFE8F016A51 /* BreezSDK.swift in Resources */ = {isa = PBXBuildFile; fileRef = ABD6FCBB0F4244B090459128 /* BreezSDK.swift */; settings = {ASSET_TAGS = (BreezSDK, ); }; }; @@ -49,6 +50,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 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 = ""; }; @@ -56,13 +58,11 @@ 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 = ""; }; 0F42D8065219E0653321EE2B /* TaskProtocol.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TaskProtocol.swift; path = "../.symlinks/plugins/breez_sdk/ios/bindings-swift/Sources/BreezSDK/TaskProtocol.swift"; sourceTree = ""; }; - 11F9FC13F9EE2A705B213FA9 /* 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 = ""; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 1F083F2041D1F553F2AF8B62 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 1FB06A93B13D606F06B3924D /* BreezSDKConnector.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BreezSDKConnector.swift; path = "../.symlinks/plugins/breez_sdk/ios/bindings-swift/Sources/BreezSDK/BreezSDKConnector.swift"; sourceTree = ""; }; + 28F61114229803070973270D /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 3C663361C56EBB242598F609 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 41102141140E57B1DC27FBA1 /* SDKNotificationService.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SDKNotificationService.swift; path = "../.symlinks/plugins/breez_sdk/ios/bindings-swift/Sources/BreezSDK/SDKNotificationService.swift"; sourceTree = ""; }; 58C22CBD8C22B9D6023D59F8 /* LnurlPayInfo.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = LnurlPayInfo.swift; path = "../.symlinks/plugins/breez_sdk/ios/bindings-swift/Sources/BreezSDK/Task/LnurlPayInfo.swift"; sourceTree = ""; }; 5AFFEBFC279AD49C00F906A4 /* wakeLock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = wakeLock.swift; sourceTree = ""; }; @@ -82,9 +82,12 @@ 9D2F2C9F2555316C95EE7EA3 /* RedeemSwap.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RedeemSwap.swift; path = "../.symlinks/plugins/breez_sdk/ios/bindings-swift/Sources/BreezSDK/Task/RedeemSwap.swift"; sourceTree = ""; }; 9F46EE5D2BC11178009318F5 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; 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 = ""; }; - AD0937B0140D5A4C24E73BEA /* 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 = ""; }; 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 = ""; }; + 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 = ""; }; F42258C3697CFE3C8C8D1933 /* ServiceLogger.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ServiceLogger.swift; path = "../.symlinks/plugins/breez_sdk/ios/bindings-swift/Sources/BreezSDK/ServiceLogger.swift"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -94,7 +97,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 4DFD1BB54A3A50573E19A583 /* Pods_Runner.framework in Frameworks */, + CEAFE4A02C53926F009FF3AD /* libresolv.tbd in Frameworks */, + 8B1F4FCAA5EB9F3A83D32D5F /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -104,8 +108,10 @@ 06957875428D0F5AAE053765 /* Frameworks */ = { isa = PBXGroup; children = ( + CEAFE49D2C539250009FF3AD /* Mwebd.xcframework */, + C58D93382C00FAC6004BCF69 /* libresolv.tbd */, 0C9986A3251A932F00D566FD /* CryptoSwift.framework */, - 3C663361C56EBB242598F609 /* Pods_Runner.framework */, + D7CD6B6020744E8FA471915D /* Pods_Runner.framework */, ); name = Frameworks; sourceTree = ""; @@ -131,10 +137,10 @@ 84389F1A05D5860790D82820 /* Pods */ = { isa = PBXGroup; children = ( - 11F9FC13F9EE2A705B213FA9 /* Pods-Runner.debug.xcconfig */, - 1F083F2041D1F553F2AF8B62 /* Pods-Runner.release.xcconfig */, - AD0937B0140D5A4C24E73BEA /* Pods-Runner.profile.xcconfig */, 0B80439B9064C9708DDB0ADA /* breez_sdk-OnDemandResources */, + 014D7E4DBCFD76DDE652A4D9 /* Pods-Runner.debug.xcconfig */, + 28F61114229803070973270D /* Pods-Runner.release.xcconfig */, + D139E30AEB36740C21C00A9E /* Pods-Runner.profile.xcconfig */, ); path = Pods; sourceTree = ""; @@ -216,14 +222,14 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - B91154210ADCED81FBF06A85 /* [CP] Check Pods Manifest.lock */, + 11278EDF4D5DB437B3FDB787 /* [CP] Check Pods Manifest.lock */, CE5E8A222BEE19C700608EA1 /* CopyFiles */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - 32D0076A9969C0C38D68AF62 /* [CP] Embed Pods Frameworks */, + F6F67323547956BC4F7B67F1 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -299,21 +305,26 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 32D0076A9969C0C38D68AF62 /* [CP] Embed Pods Frameworks */ = { + 11278EDF4D5DB437B3FDB787 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - name = "[CP] Embed Pods Frameworks"; + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { @@ -347,26 +358,21 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n"; }; - B91154210ADCED81FBF06A85 /* [CP] Check Pods Manifest.lock */ = { + F6F67323547956BC4F7B67F1 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; + name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 402f6556b..477d47f7b 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -110,12 +110,12 @@ import workmanager private func makeSecure() { if (!self.window.subviews.contains(textField)) { + let view = UIView(frame: CGRect(x: 0, y: 0, width: textField.frame.self.width, height: textField.frame.self.height)) self.window.addSubview(textField) - textField.centerYAnchor.constraint(equalTo: self.window.centerYAnchor).isActive = true - textField.centerXAnchor.constraint(equalTo: self.window.centerXAnchor).isActive = true self.window.layer.superlayer?.addSublayer(textField.layer) - textField.layer.sublayers?.first?.addSublayer(self.window.layer) + textField.layer.sublayers?.last!.addSublayer(self.window.layer) + textField.leftView = view + textField.leftViewMode = .always } } - -} +} \ No newline at end of file diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index cf322bd72..fd88fe62d 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -28,9 +28,22 @@ class CWBitcoin extends Bitcoin { name: name, password: password, wif: wif, walletInfo: walletInfo); @override - WalletCredentials createBitcoinNewWalletCredentials( - {required String name, WalletInfo? walletInfo, String? password}) => - BitcoinNewWalletCredentials(name: name, walletInfo: walletInfo, password: password); + WalletCredentials createBitcoinNewWalletCredentials({ + required String name, + WalletInfo? walletInfo, + String? password, + String? passphrase, + String? mnemonic, + String? parentAddress, + }) => + BitcoinNewWalletCredentials( + name: name, + walletInfo: walletInfo, + password: password, + passphrase: passphrase, + mnemonic: mnemonic, + parentAddress: parentAddress, + ); @override WalletCredentials createBitcoinHardwareWalletCredentials( @@ -93,34 +106,33 @@ class CWBitcoin extends Bitcoin { } @override - Object createBitcoinTransactionCredentials(List outputs, - {required TransactionPriority priority, int? feeRate}) { + Object createBitcoinTransactionCredentials( + List outputs, { + required TransactionPriority priority, + int? feeRate, + UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any, + }) { final bitcoinFeeRate = priority == BitcoinTransactionPriority.custom && feeRate != null ? feeRate : null; return BitcoinTransactionCredentials( - 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, - memo: out.memo)) - .toList(), - priority: priority as BitcoinTransactionPriority, - feeRate: bitcoinFeeRate); + 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, + memo: out.memo)) + .toList(), + priority: priority as BitcoinTransactionPriority, + feeRate: bitcoinFeeRate, + coinTypeToSpendFrom: coinTypeToSpendFrom, + ); } - @override - Object createBitcoinTransactionCredentialsRaw(List outputs, - {TransactionPriority? priority, required int feeRate}) => - BitcoinTransactionCredentials(outputs, - priority: priority != null ? priority as BitcoinTransactionPriority : null, - feeRate: feeRate); - @override @computed List getSubAddresses(Object wallet) { @@ -192,9 +204,19 @@ class CWBitcoin extends Bitcoin { (priority as BitcoinTransactionPriority).labelWithRate(rate, customRate); @override - List getUnspents(Object wallet) { + List getUnspents(Object wallet, + {UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any}) { final bitcoinWallet = wallet as ElectrumWallet; - return bitcoinWallet.unspentCoins; + return bitcoinWallet.unspentCoins.where((element) { + switch (coinTypeToSpendFrom) { + case UnspentCoinType.mweb: + return element.bitcoinAddressRecord.type == SegwitAddresType.mweb; + case UnspentCoinType.nonMweb: + return element.bitcoinAddressRecord.type != SegwitAddresType.mweb; + case UnspentCoinType.any: + return true; + } + }).toList(); } Future updateUnspents(Object wallet) async { @@ -207,9 +229,9 @@ class CWBitcoin extends Bitcoin { return BitcoinWalletService(walletInfoSource, unspentCoinSource, alwaysScan, isDirect); } - WalletService createLitecoinWalletService( - Box walletInfoSource, Box unspentCoinSource, bool isDirect) { - return LitecoinWalletService(walletInfoSource, unspentCoinSource, isDirect); + WalletService createLitecoinWalletService(Box walletInfoSource, + Box unspentCoinSource, bool alwaysScan, bool isDirect) { + return LitecoinWalletService(walletInfoSource, unspentCoinSource, alwaysScan, isDirect); } @override @@ -248,6 +270,16 @@ class CWBitcoin extends Bitcoin { @override List getBitcoinReceivePageOptions() => BitcoinReceivePageOption.all; + @override + List getLitecoinReceivePageOptions() { + if (Platform.isLinux || Platform.isMacOS || Platform.isWindows) { + return BitcoinReceivePageOption.allLitecoin + .where((element) => element != BitcoinReceivePageOption.mweb) + .toList(); + } + return BitcoinReceivePageOption.allLitecoin; + } + @override BitcoinAddressType getBitcoinAddressType(ReceivePageOption option) { switch (option) { @@ -259,6 +291,8 @@ class CWBitcoin extends Bitcoin { return SegwitAddresType.p2tr; case BitcoinReceivePageOption.p2wsh: return SegwitAddresType.p2wsh; + case BitcoinReceivePageOption.mweb: + return SegwitAddresType.mweb; case BitcoinReceivePageOption.p2wpkh: default: return SegwitAddresType.p2wpkh; @@ -315,7 +349,7 @@ class CWBitcoin extends Bitcoin { for (DerivationType dType in electrum_derivations.keys) { late Uint8List seedBytes; if (dType == DerivationType.electrum) { - seedBytes = await mnemonicToSeedBytes(mnemonic); + seedBytes = await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? ""); } else if (dType == DerivationType.bip39) { seedBytes = bip39.mnemonicToSeed(mnemonic, passphrase: passphrase ?? ''); } @@ -360,23 +394,25 @@ class CWBitcoin extends Bitcoin { continue; } - final sh = scriptHash(address, network: network); + final sh = BitcoinAddressUtils.scriptHash(address, network: network); final history = await electrumClient.getHistory(sh); final balance = await electrumClient.getBalance(sh); - dInfoCopy.balance = balance.entries.first.value.toString(); + dInfoCopy.balance = balance.entries.firstOrNull?.value.toString() ?? "0"; dInfoCopy.address = address; dInfoCopy.transactionsCount = history.length; list.add(dInfoCopy); - } catch (e) { - print(e); + } catch (e, s) { + print("derivationInfoError: $e"); + print("derivationInfoStack: $s"); } } } // sort the list such that derivations with the most transactions are first: list.sort((a, b) => b.transactionsCount.compareTo(a.transactionsCount)); + return list; } @@ -398,9 +434,16 @@ class CWBitcoin extends Bitcoin { } @override - Future canReplaceByFee(Object wallet, String transactionHash) async { + Future canReplaceByFee(Object wallet, Object transactionInfo) async { final bitcoinWallet = wallet as ElectrumWallet; - return bitcoinWallet.canReplaceByFee(transactionHash); + final tx = transactionInfo as ElectrumTransactionInfo; + return bitcoinWallet.canReplaceByFee(tx); + } + + @override + int getTransactionVSize(Object wallet, String transactionHex) { + final bitcoinWallet = wallet as ElectrumWallet; + return bitcoinWallet.transactionVSize(transactionHex); } @override @@ -444,18 +487,30 @@ class CWBitcoin extends Bitcoin { } @override - void setLedger(WalletBase wallet, Ledger ledger, LedgerDevice device) { - (wallet as BitcoinWallet).setLedger(ledger, device); + void setLedgerConnection(WalletBase wallet, ledger.LedgerConnection connection) { + (wallet as ElectrumWallet).setLedgerConnection(connection); } @override - Future> getHardwareWalletAccounts(LedgerViewModel ledgerVM, + Future> getHardwareWalletBitcoinAccounts(LedgerViewModel ledgerVM, {int index = 0, int limit = 5}) async { - final hardwareWalletService = BitcoinHardwareWalletService(ledgerVM.ledger, ledgerVM.device); + final hardwareWalletService = BitcoinHardwareWalletService(ledgerVM.connection); try { return hardwareWalletService.getAvailableAccounts(index: index, limit: limit); - } on LedgerException catch (err) { - print(err.message); + } catch (err) { + print(err); + throw err; + } + } + + @override + Future> getHardwareWalletLitecoinAccounts(LedgerViewModel ledgerVM, + {int index = 0, int limit = 5}) async { + final hardwareWalletService = LitecoinHardwareWalletService(ledgerVM.connection); + try { + return hardwareWalletService.getAvailableAccounts(index: index, limit: limit); + } catch (err) { + print(err); throw err; } } @@ -520,7 +575,23 @@ class CWBitcoin extends Bitcoin { } @override - int getHeightByDate({required DateTime date}) => getBitcoinHeightByDate(date: date); + Future checkIfMempoolAPIIsEnabled(Object wallet) async { + final bitcoinWallet = wallet as ElectrumWallet; + return await bitcoinWallet.checkIfMempoolAPIIsEnabled(); + } + + @override + Future getHeightByDate({required DateTime date, bool? bitcoinMempoolAPIEnabled}) async { + if (bitcoinMempoolAPIEnabled ?? false) { + try { + return await getBitcoinHeightByDateAPI(date: date); + } catch (_) {} + } + return await getBitcoinHeightByDate(date: date); + } + + @override + int getLitecoinHeightByDate({required DateTime date}) => getLtcHeightByDate(date: date); @override Future rescan(Object wallet, {required int height, bool? doSingleScan}) async { @@ -545,4 +616,94 @@ class CWBitcoin extends Bitcoin { final bitcoinWallet = wallet as ElectrumWallet; await bitcoinWallet.updateFeeRates(); } + + @override + Future setMwebEnabled(Object wallet, bool enabled) async { + final litecoinWallet = wallet as LitecoinWallet; + litecoinWallet.setMwebEnabled(enabled); + } + + @override + bool getMwebEnabled(Object wallet) { + final litecoinWallet = wallet as LitecoinWallet; + return litecoinWallet.mwebEnabled; + } + + List updateOutputs(PendingTransaction pendingTransaction, List outputs) { + final pendingTx = pendingTransaction as PendingBitcoinTransaction; + + if (!pendingTx.hasSilentPayment) { + return outputs; + } + + final updatedOutputs = outputs.map((output) { + try { + final pendingOut = pendingTx.outputs[outputs.indexOf(output)]; + final updatedOutput = output; + + updatedOutput.stealthAddress = P2trAddress.fromScriptPubkey(script: pendingOut.scriptPubKey) + .toAddress(BitcoinNetwork.mainnet); + return updatedOutput; + } catch (_) {} + + return output; + }).toList(); + + return updatedOutputs; + } + + @override + bool txIsReceivedSilentPayment(TransactionInfo txInfo) { + final tx = txInfo as ElectrumTransactionInfo; + return tx.isReceivedSilentPayment; + } + + @override + bool txIsMweb(TransactionInfo txInfo) { + final tx = txInfo as ElectrumTransactionInfo; + + List inputAddresses = tx.inputAddresses ?? []; + List outputAddresses = tx.outputAddresses ?? []; + bool inputAddressesContainMweb = false; + bool outputAddressesContainMweb = false; + + for (var address in inputAddresses) { + if (address.toLowerCase().contains('mweb')) { + inputAddressesContainMweb = true; + break; + } + } + + for (var address in outputAddresses) { + if (address.toLowerCase().contains('mweb')) { + outputAddressesContainMweb = true; + break; + } + } + + // TODO: this could be improved: + return inputAddressesContainMweb || outputAddressesContainMweb; + } + + String? getUnusedMwebAddress(Object wallet) { + try { + final electrumWallet = wallet as ElectrumWallet; + final mwebAddress = + electrumWallet.walletAddresses.mwebAddresses.firstWhere((element) => !element.isUsed); + return mwebAddress.address; + } catch (_) { + return null; + } + } + + String? getUnusedSegwitAddress(Object wallet) { + try { + final electrumWallet = wallet as ElectrumWallet; + final segwitAddress = electrumWallet.walletAddresses.allAddresses + .firstWhere((element) => !element.isUsed && element.type == SegwitAddresType.p2wpkh); + return segwitAddress.address; + } catch (_) { + return null; + } + } } diff --git a/lib/bitcoin_cash/cw_bitcoin_cash.dart b/lib/bitcoin_cash/cw_bitcoin_cash.dart index fcb34a286..b74448703 100644 --- a/lib/bitcoin_cash/cw_bitcoin_cash.dart +++ b/lib/bitcoin_cash/cw_bitcoin_cash.dart @@ -15,14 +15,24 @@ class CWBitcoinCash extends BitcoinCash { required String name, WalletInfo? walletInfo, String? password, + String? passphrase, + String? mnemonic, + String? parentAddress, }) => - BitcoinCashNewWalletCredentials(name: name, walletInfo: walletInfo, password: password); + BitcoinCashNewWalletCredentials( + name: name, + walletInfo: walletInfo, + password: password, + passphrase: passphrase, + parentAddress: parentAddress, + mnemonic: mnemonic, + ); @override WalletCredentials createBitcoinCashRestoreWalletFromSeedCredentials( - {required String name, required String mnemonic, required String password}) => + {required String name, required String mnemonic, required String password, String? passphrase}) => BitcoinCashRestoreWalletFromSeedCredentials( - name: name, mnemonic: mnemonic, password: password); + name: name, mnemonic: mnemonic, password: password, passphrase: passphrase); @override TransactionPriority deserializeBitcoinCashTransactionPriority(int raw) => diff --git a/lib/buy/moonpay/moonpay_provider.dart b/lib/buy/moonpay/moonpay_provider.dart index c89fb222f..67ee75d7c 100644 --- a/lib/buy/moonpay/moonpay_provider.dart +++ b/lib/buy/moonpay/moonpay_provider.dart @@ -298,7 +298,9 @@ class MoonPayProvider extends BuyProvider { String _normalizeCurrency(CryptoCurrency currency) { if (currency == CryptoCurrency.maticpoly) { - return "MATIC_POLYGON"; + return "POL_POLYGON"; + } else if (currency == CryptoCurrency.matic) { + return "POL"; } return currency.toString().toLowerCase(); diff --git a/lib/core/address_validator.dart b/lib/core/address_validator.dart index 92a735481..21726fab8 100644 --- a/lib/core/address_validator.dart +++ b/lib/core/address_validator.dart @@ -5,32 +5,44 @@ import 'package:cake_wallet/solana/solana.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/erc20_token.dart'; +const BEFORE_REGEX = '(^|\\s)'; +const AFTER_REGEX = '(\$|\\s)'; + class AddressValidator extends TextValidator { AddressValidator({required CryptoCurrency type}) : super( errorMessage: S.current.error_text_address, - useAdditionalValidation: type == CryptoCurrency.btc - ? (String txt) => validateAddress(address: txt, network: BitcoinNetwork.mainnet) + useAdditionalValidation: type == CryptoCurrency.btc || type == CryptoCurrency.ltc + ? (String txt) => BitcoinAddressUtils.validateAddress( + address: txt, + network: type == CryptoCurrency.btc + ? BitcoinNetwork.mainnet + : LitecoinNetwork.mainnet, + ) : null, pattern: getPattern(type), length: getLength(type)); static String getPattern(CryptoCurrency type) { + var pattern = ""; if (type is Erc20Token) { - return '0x[0-9a-zA-Z]'; + pattern = '0x[0-9a-zA-Z]+'; } switch (type) { case CryptoCurrency.xmr: - return '^4[0-9a-zA-Z]{94}\$|^8[0-9a-zA-Z]{94}\$|^[0-9a-zA-Z]{106}\$'; + pattern = '4[0-9a-zA-Z]{94}|8[0-9a-zA-Z]{94}|[0-9a-zA-Z]{106}'; case CryptoCurrency.ada: - return '^[0-9a-zA-Z]{59}\$|^[0-9a-zA-Z]{92}\$|^[0-9a-zA-Z]{104}\$' - '|^[0-9a-zA-Z]{105}\$|^addr1[0-9a-zA-Z]{98}\$'; + pattern = '[0-9a-zA-Z]{59}|[0-9a-zA-Z]{92}|[0-9a-zA-Z]{104}' + '|[0-9a-zA-Z]{105}|addr1[0-9a-zA-Z]{98}'; case CryptoCurrency.btc: - return '^${P2pkhAddress.regex.pattern}\$|^${P2shAddress.regex.pattern}\$|^${P2wpkhAddress.regex.pattern}\$|${P2trAddress.regex.pattern}\$|^${P2wshAddress.regex.pattern}\$|^${SilentPaymentAddress.regex.pattern}\$'; + pattern = + '${P2pkhAddress.regex.pattern}|${P2shAddress.regex.pattern}|${RegExp(r'(bc|tb)1q[ac-hj-np-z02-9]{25,39}}').pattern}|${P2trAddress.regex.pattern}|${P2wshAddress.regex.pattern}|${SilentPaymentAddress.regex.pattern}'; + case CryptoCurrency.ltc: + pattern = '^${RegExp(r'ltc1q[ac-hj-np-z02-9]{25,39}').pattern}\$|^${MwebAddress.regex.pattern}\$'; case CryptoCurrency.nano: - return '[0-9a-zA-Z_]'; + pattern = '[0-9a-zA-Z_]+'; case CryptoCurrency.banano: - return '[0-9a-zA-Z_]'; + pattern = '[0-9a-zA-Z_]+'; case CryptoCurrency.usdc: case CryptoCurrency.usdcpoly: case CryptoCurrency.usdtPoly: @@ -66,11 +78,11 @@ class AddressValidator extends TextValidator { case CryptoCurrency.dydx: case CryptoCurrency.steth: case CryptoCurrency.shib: - return '0x[0-9a-zA-Z]'; + pattern = '0x[0-9a-zA-Z]+'; case CryptoCurrency.xrp: - return '^[0-9a-zA-Z]{34}\$|^X[0-9a-zA-Z]{46}\$'; + pattern = '[0-9a-zA-Z]{34}|X[0-9a-zA-Z]{46}'; case CryptoCurrency.xhv: - return '^hvx|hvi|hvs[0-9a-zA-Z]'; + pattern = 'hvx|hvi|hvs[0-9a-zA-Z]+'; case CryptoCurrency.xag: case CryptoCurrency.xau: case CryptoCurrency.xaud: @@ -92,40 +104,41 @@ class AddressValidator extends TextValidator { case CryptoCurrency.dash: case CryptoCurrency.eos: case CryptoCurrency.wow: - return '[0-9a-zA-Z]'; + pattern = '[0-9a-zA-Z]+'; case CryptoCurrency.bch: - return '^(?!bitcoincash:)[0-9a-zA-Z]*\$|^(?!bitcoincash:)q|p[0-9a-zA-Z]{41}\$|^(?!bitcoincash:)q|p[0-9a-zA-Z]{42}\$|^bitcoincash:q|p[0-9a-zA-Z]{41}\$|^bitcoincash:q|p[0-9a-zA-Z]{42}\$'; + pattern = + '(?!bitcoincash:)[0-9a-zA-Z]*|(?!bitcoincash:)q|p[0-9a-zA-Z]{41}|(?!bitcoincash:)q|p[0-9a-zA-Z]{42}|bitcoincash:q|p[0-9a-zA-Z]{41}|bitcoincash:q|p[0-9a-zA-Z]{42}'; case CryptoCurrency.bnb: - return '[0-9a-zA-Z]'; - case CryptoCurrency.ltc: - return '^(?!(ltc|LTC)1)[0-9a-zA-Z]*\$|(^LTC1[A-Z0-9]*\$)|(^ltc1[a-z0-9]*\$)'; + pattern = '[0-9a-zA-Z]+'; case CryptoCurrency.hbar: - return '[0-9a-zA-Z.]'; + pattern = '[0-9a-zA-Z.]+'; case CryptoCurrency.zaddr: - return '^zs[0-9a-zA-Z]{75}'; + pattern = 'zs[0-9a-zA-Z]{75}'; case CryptoCurrency.zec: - return '^t1[0-9a-zA-Z]{33}\$|^t3[0-9a-zA-Z]{33}\$'; + pattern = 't1[0-9a-zA-Z]{33}|t3[0-9a-zA-Z]{33}'; case CryptoCurrency.dcr: - return 'D[ksecS]([0-9a-zA-Z])+'; + pattern = 'D[ksecS]([0-9a-zA-Z])+'; case CryptoCurrency.rvn: - return '[Rr]([1-9a-km-zA-HJ-NP-Z]){33}'; + pattern = '[Rr]([1-9a-km-zA-HJ-NP-Z]){33}'; case CryptoCurrency.near: - return '[0-9a-f]{64}'; + pattern = '[0-9a-f]{64}'; case CryptoCurrency.rune: - return 'thor1[0-9a-z]{38}'; + pattern = 'thor1[0-9a-z]{38}'; case CryptoCurrency.scrt: - return 'secret1[0-9a-z]{38}'; + pattern = 'secret1[0-9a-z]{38}'; case CryptoCurrency.stx: - return 'S[MP][0-9a-zA-Z]+'; + pattern = 'S[MP][0-9a-zA-Z]+'; case CryptoCurrency.kmd: - return 'R[0-9a-zA-Z]{33}'; + pattern = 'R[0-9a-zA-Z]{33}'; case CryptoCurrency.pivx: - return 'D([1-9a-km-zA-HJ-NP-Z]){33}'; + pattern = 'D([1-9a-km-zA-HJ-NP-Z]){33}'; case CryptoCurrency.btcln: - return '^(lnbc|LNBC)([0-9]{1,}[a-zA-Z0-9]+)'; + pattern = '(lnbc|LNBC)([0-9]{1,}[a-zA-Z0-9]+)'; default: - return '[0-9a-zA-Z]'; + pattern = '[0-9a-zA-Z]+'; } + + return '$BEFORE_REGEX($pattern)$AFTER_REGEX'; } static List? getLength(CryptoCurrency type) { @@ -146,6 +159,8 @@ class AddressValidator extends TextValidator { return null; case CryptoCurrency.btc: return null; + case CryptoCurrency.ltc: + return null; case CryptoCurrency.dash: return [34]; case CryptoCurrency.eos: @@ -192,8 +207,6 @@ class AddressValidator extends TextValidator { return [42, 43, 44, 54, 55]; case CryptoCurrency.bnb: return [42]; - case CryptoCurrency.ltc: - return [34, 43, 63]; case CryptoCurrency.nano: return [64, 65]; case CryptoCurrency.banano: @@ -266,56 +279,54 @@ class AddressValidator extends TextValidator { } static String? getAddressFromStringPattern(CryptoCurrency type) { + String? pattern = null; + switch (type) { case CryptoCurrency.xmr: case CryptoCurrency.wow: - return '([^0-9a-zA-Z]|^)4[0-9a-zA-Z]{94}([^0-9a-zA-Z]|\$)' - '|([^0-9a-zA-Z]|^)8[0-9a-zA-Z]{94}([^0-9a-zA-Z]|\$)' - '|([^0-9a-zA-Z]|^)[0-9a-zA-Z]{106}([^0-9a-zA-Z]|\$)'; + pattern = '(4[0-9a-zA-Z]{94})' + '|(8[0-9a-zA-Z]{94})' + '|([0-9a-zA-Z]{106})'; case CryptoCurrency.btc: - return '([^0-9a-zA-Z]|^)([1mn][a-km-zA-HJ-NP-Z1-9]{25,34})([^0-9a-zA-Z]|\$)' //P2pkhAddress type - '|([^0-9a-zA-Z]|^)([23][a-km-zA-HJ-NP-Z1-9]{25,34})([^0-9a-zA-Z]|\$)' //P2shAddress type - '|([^0-9a-zA-Z]|^)((bc|tb)1q[ac-hj-np-z02-9]{25,39})([^0-9a-zA-Z]|\$)' //P2wpkhAddress type - '|([^0-9a-zA-Z]|^)((bc|tb)1q[ac-hj-np-z02-9]{40,80})([^0-9a-zA-Z]|\$)' //P2wshAddress type - '|([^0-9a-zA-Z]|^)((bc|tb)1p([ac-hj-np-z02-9]{39}|[ac-hj-np-z02-9]{59}|[ac-hj-np-z02-9]{8,89}))([^0-9a-zA-Z]|\$)' //P2trAddress type - '|${SilentPaymentAddress.regex.pattern}\$'; - + pattern = + '${P2pkhAddress.regex.pattern}|${P2shAddress.regex.pattern}|${P2wpkhAddress.regex.pattern}|${P2trAddress.regex.pattern}|${P2wshAddress.regex.pattern}|${SilentPaymentAddress.regex.pattern}'; case CryptoCurrency.ltc: - return '([^0-9a-zA-Z]|^)^L[a-zA-Z0-9]{26,33}([^0-9a-zA-Z]|\$)' + pattern = '([^0-9a-zA-Z]|^)^L[a-zA-Z0-9]{26,33}([^0-9a-zA-Z]|\$)' '|([^0-9a-zA-Z]|^)[LM][a-km-zA-HJ-NP-Z1-9]{26,33}([^0-9a-zA-Z]|\$)' - '|([^0-9a-zA-Z]|^)ltc[a-zA-Z0-9]{26,45}([^0-9a-zA-Z]|\$)'; + '|([^0-9a-zA-Z]|^)ltc[a-zA-Z0-9]{26,45}([^0-9a-zA-Z]|\$)' + '|([^0-9a-zA-Z]|^)((ltc|t)mweb1q[ac-hj-np-z02-9]{90,120})([^0-9a-zA-Z]|\$)'; case CryptoCurrency.eth: - return '0x[0-9a-zA-Z]{42}'; case CryptoCurrency.maticpoly: - return '0x[0-9a-zA-Z]{42}'; + pattern = '0x[0-9a-zA-Z]+'; case CryptoCurrency.nano: - return 'nano_[0-9a-zA-Z]{60}'; + pattern = 'nano_[0-9a-zA-Z]{60}'; case CryptoCurrency.banano: - return 'ban_[0-9a-zA-Z]{60}'; + pattern = 'ban_[0-9a-zA-Z]{60}'; case CryptoCurrency.bch: - return 'bitcoincash:q[0-9a-zA-Z]{41}([^0-9a-zA-Z]|\$)' - '|bitcoincash:q[0-9a-zA-Z]{42}([^0-9a-zA-Z]|\$)' - '|([^0-9a-zA-Z]|^)q[0-9a-zA-Z]{41}([^0-9a-zA-Z]|\$)' - '|([^0-9a-zA-Z]|^)q[0-9a-zA-Z]{42}([^0-9a-zA-Z]|\$)'; + pattern = '(bitcoincash:)?q[0-9a-zA-Z]{41,42}'; case CryptoCurrency.sol: - return '([^0-9a-zA-Z]|^)[1-9A-HJ-NP-Za-km-z]{43,44}([^0-9a-zA-Z]|\$)'; + pattern = '[1-9A-HJ-NP-Za-km-z]+'; case CryptoCurrency.trx: - return '(T|t)[1-9A-HJ-NP-Za-km-z]{33}'; + pattern = '(T|t)[1-9A-HJ-NP-Za-km-z]{33}'; default: if (type.tag == CryptoCurrency.eth.title) { - return '0x[0-9a-zA-Z]{42}'; + pattern = '0x[0-9a-zA-Z]{42}'; } if (type.tag == CryptoCurrency.maticpoly.tag) { - return '0x[0-9a-zA-Z]{42}'; + pattern = '0x[0-9a-zA-Z]{42}'; } if (type.tag == CryptoCurrency.sol.title) { - return '([^0-9a-zA-Z]|^)[1-9A-HJ-NP-Za-km-z]{43,44}([^0-9a-zA-Z]|\$)'; + pattern = '[1-9A-HJ-NP-Za-km-z]{43,44}'; } if (type.tag == CryptoCurrency.trx.title) { - return '(T|t)[1-9A-HJ-NP-Za-km-z]{33}'; + pattern = '(T|t)[1-9A-HJ-NP-Za-km-z]{33}'; } - - return null; } + + if (pattern != null) { + return "$BEFORE_REGEX($pattern)$AFTER_REGEX"; + } + + return null; } } diff --git a/lib/core/backup_service.dart b/lib/core/backup_service.dart index 42e24d3c7..d65530eb5 100644 --- a/lib/core/backup_service.dart +++ b/lib/core/backup_service.dart @@ -2,14 +2,14 @@ import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; import 'package:cake_wallet/core/secure_storage.dart'; +import 'package:cake_wallet/entities/get_encryption_key.dart'; +import 'package:cake_wallet/entities/transaction_description.dart'; import 'package:cake_wallet/themes/theme_list.dart'; import 'package:cw_core/root_dir.dart'; import 'package:cake_wallet/utils/device_info.dart'; -import 'package:cw_core/root_dir.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; -import 'package:path_provider/path_provider.dart'; import 'package:cryptography/cryptography.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:archive/archive_io.dart'; @@ -24,8 +24,8 @@ import 'package:cake_wallet/wallet_types.g.dart'; import 'package:cake_backup/backup.dart' as cake_backup; class BackupService { - BackupService( - this._secureStorage, this._walletInfoSource, this._keyService, this._sharedPreferences) + BackupService(this._secureStorage, this._walletInfoSource, this._transactionDescriptionBox, + this._keyService, this._sharedPreferences) : _cipher = Cryptography.instance.chacha20Poly1305Aead(), _correctWallets = []; @@ -38,6 +38,7 @@ class BackupService { final SecureStorage _secureStorage; final SharedPreferences _sharedPreferences; final Box _walletInfoSource; + final Box _transactionDescriptionBox; final KeyService _keyService; List _correctWallets; @@ -86,6 +87,13 @@ class BackupService { final preferencesDump = await _exportPreferencesJSON(); final preferencesDumpFile = File('${tmpDir.path}/~_preferences_dump_TMP'); final keychainDumpFile = File('${tmpDir.path}/~_keychain_dump_TMP'); + final transactionDescriptionDumpFile = + File('${tmpDir.path}/~_transaction_descriptions_dump_TMP'); + + final transactionDescriptionData = _transactionDescriptionBox + .toMap() + .map((key, value) => MapEntry(key.toString(), value.toJson())); + final transactionDescriptionDump = jsonEncode(transactionDescriptionData); if (tmpDir.existsSync()) { tmpDir.deleteSync(recursive: true); @@ -98,7 +106,15 @@ class BackupService { if (entity.path == archivePath || entity.path == tmpDir.path) { return; } - + final filename = entity.absolute; + for (var ignore in ignoreFiles) { + final filename = entity.absolute.path; + if (filename.endsWith(ignore) && !filename.contains("wallets/")) { + print("ignoring backup file: $filename"); + return; + } + } + print("restoring: $filename"); if (entity.statSync().type == FileSystemEntityType.directory) { zipEncoder.addDirectory(Directory(entity.path)); } else { @@ -107,8 +123,10 @@ class BackupService { }); await keychainDumpFile.writeAsBytes(keychainDump.toList()); await preferencesDumpFile.writeAsString(preferencesDump); + await transactionDescriptionDumpFile.writeAsString(transactionDescriptionDump); await zipEncoder.addFile(preferencesDumpFile, '~_preferences_dump'); await zipEncoder.addFile(keychainDumpFile, '~_keychain_dump'); + await zipEncoder.addFile(transactionDescriptionDumpFile, '~_transaction_descriptions_dump'); zipEncoder.close(); final content = File(archivePath).readAsBytesSync(); @@ -121,45 +139,61 @@ class BackupService { final decryptedData = await _decryptV1(data, password, nonce); final zip = ZipDecoder().decodeBytes(decryptedData); - zip.files.forEach((file) { + for (var file in zip.files) { final filename = file.name; if (file.isFile) { final content = file.content as List; File('${appDir.path}/' + filename) ..createSync(recursive: true) - ..writeAsBytesSync(content); + ..writeAsBytesSync(content, flush: true); } else { Directory('${appDir.path}/' + filename)..create(recursive: true); } - }); + }; await _verifyWallets(); await _importKeychainDumpV1(password, nonce: nonce); await _importPreferencesDump(); } + // checked with .endsWith - so this should be the last part of the filename + static const ignoreFiles = [ + "flutter_assets/kernel_blob.bin", + "flutter_assets/vm_snapshot_data", + "flutter_assets/isolate_snapshot_data", + ".lock", + ]; + Future _importBackupV2(Uint8List data, String password) async { final appDir = await getAppDir(); final decryptedData = await _decryptV2(data, password); final zip = ZipDecoder().decodeBytes(decryptedData); - zip.files.forEach((file) { + outer: + for (var file in zip.files) { final filename = file.name; - + for (var ignore in ignoreFiles) { + if (filename.endsWith(ignore) && !filename.contains("wallets/")) { + print("ignoring backup file: $filename"); + continue outer; + } + } + print("restoring: $filename"); if (file.isFile) { final content = file.content as List; File('${appDir.path}/' + filename) ..createSync(recursive: true) - ..writeAsBytesSync(content); + ..writeAsBytesSync(content, flush: true); } else { Directory('${appDir.path}/' + filename)..create(recursive: true); } - }); + }; await _verifyWallets(); await _importKeychainDumpV2(password); await _importPreferencesDump(); + await _importTransactionDescriptionDump(); } Future _verifyWallets() async { @@ -184,6 +218,31 @@ class BackupService { return await CakeHive.openBox(WalletInfo.boxName); } + Future _importTransactionDescriptionDump() async { + final appDir = await getAppDir(); + final transactionDescriptionFile = File('${appDir.path}/~_transaction_descriptions_dump'); + + if (!transactionDescriptionFile.existsSync()) { + return; + } + + final jsonData = + json.decode(transactionDescriptionFile.readAsStringSync()) as Map; + final descriptionsMap = jsonData.map((key, value) => + MapEntry(key, TransactionDescription.fromJson(value as Map))); + + if (!_transactionDescriptionBox.isOpen) { + final transactionDescriptionsBoxKey = await getEncryptionKey(secureStorage: secureStorageShared, forKey: TransactionDescription.boxKey); + final transactionDescriptionBox = await CakeHive.openBox( + TransactionDescription.boxName, + encryptionKey: transactionDescriptionsBoxKey, + ); + await transactionDescriptionBox.putAll(descriptionsMap); + return; + } + await _transactionDescriptionBox.putAll(descriptionsMap); + } + Future _importPreferencesDump() async { final appDir = await getAppDir(); final preferencesFile = File('${appDir.path}/~_preferences_dump'); diff --git a/lib/core/new_wallet_arguments.dart b/lib/core/new_wallet_arguments.dart new file mode 100644 index 000000000..2581c57bb --- /dev/null +++ b/lib/core/new_wallet_arguments.dart @@ -0,0 +1,15 @@ +import 'package:cw_core/wallet_type.dart'; + +class NewWalletArguments { + final WalletType type; + final String? mnemonic; + final String? parentAddress; + final bool isChildWallet; + + NewWalletArguments({ + required this.type, + this.parentAddress, + this.mnemonic, + this.isChildWallet = false, + }); +} diff --git a/lib/core/new_wallet_type_arguments.dart b/lib/core/new_wallet_type_arguments.dart new file mode 100644 index 000000000..b43617835 --- /dev/null +++ b/lib/core/new_wallet_type_arguments.dart @@ -0,0 +1,14 @@ +import 'package:cw_core/wallet_type.dart'; +import 'package:flutter/material.dart'; + +class NewWalletTypeArguments { + final void Function(BuildContext, WalletType)? onTypeSelected; + final bool isCreate; + final bool isHardwareWallet; + + NewWalletTypeArguments({ + required this.onTypeSelected, + required this.isCreate, + required this.isHardwareWallet, + }); +} diff --git a/lib/core/sync_status_title.dart b/lib/core/sync_status_title.dart index 465211f23..46dd62c3a 100644 --- a/lib/core/sync_status_title.dart +++ b/lib/core/sync_status_title.dart @@ -16,6 +16,13 @@ String syncStatusTitle(SyncStatus syncStatus) { return S.current.sync_status_syncronized; } + if (syncStatus is FailedSyncStatus) { + if (syncStatus.error != null) { + return syncStatus.error!; + } + return S.current.sync_status_failed_connect; + } + if (syncStatus is NotConnectedSyncStatus) { return S.current.sync_status_not_connected; } @@ -24,10 +31,6 @@ String syncStatusTitle(SyncStatus syncStatus) { return S.current.sync_status_attempting_sync; } - if (syncStatus is FailedSyncStatus) { - return S.current.sync_status_failed_connect; - } - if (syncStatus is ConnectingSyncStatus) { return S.current.sync_status_connecting; } @@ -53,7 +56,11 @@ String syncStatusTitle(SyncStatus syncStatus) { } if (syncStatus is StartingScanSyncStatus) { - return S.current.sync_status_starting_scan; + return S.current.sync_status_starting_scan(syncStatus.beginHeight.toString()); + } + + if (syncStatus is AttemptingScanSyncStatus) { + return S.current.sync_status_attempting_scan; } return ''; diff --git a/lib/core/wallet_creation_service.dart b/lib/core/wallet_creation_service.dart index 1e9299282..3ee630b33 100644 --- a/lib/core/wallet_creation_service.dart +++ b/lib/core/wallet_creation_service.dart @@ -75,8 +75,10 @@ class WalletCreationService { bool get _hasSeedPhraseLengthOption { switch (type) { - case WalletType.ethereum: + case WalletType.bitcoin: + case WalletType.litecoin: case WalletType.bitcoinCash: + case WalletType.ethereum: case WalletType.polygon: case WalletType.solana: case WalletType.tron: @@ -84,8 +86,6 @@ class WalletCreationService { case WalletType.monero: case WalletType.wownero: case WalletType.none: - case WalletType.bitcoin: - case WalletType.litecoin: case WalletType.haven: case WalletType.nano: case WalletType.banano: diff --git a/lib/core/wallet_loading_service.dart b/lib/core/wallet_loading_service.dart index 0087b1332..e58e14652 100644 --- a/lib/core/wallet_loading_service.dart +++ b/lib/core/wallet_loading_service.dart @@ -85,7 +85,8 @@ class WalletLoadingService { authenticatedErrorStreamController.add(corruptedWalletsSeeds); return wallet; - } catch (_) { + } catch (e) { + print(e); // save seeds and show corrupted wallets' seeds to the user try { final seeds = await _getCorruptedWalletSeeds(walletInfo.name, walletInfo.type); diff --git a/lib/di.dart b/lib/di.dart index 1967c9227..13ffd839e 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -12,10 +12,12 @@ import 'package:cake_wallet/buy/moonpay/moonpay_provider.dart'; import 'package:cake_wallet/buy/onramper/onramper_buy_provider.dart'; import 'package:cake_wallet/buy/order.dart'; import 'package:cake_wallet/buy/payfura/payfura_buy_provider.dart'; +import 'package:cake_wallet/core/new_wallet_arguments.dart'; import 'package:cake_wallet/buy/robinhood/robinhood_buy_provider.dart'; import 'package:cake_wallet/core/auth_service.dart'; import 'package:cake_wallet/core/backup_service.dart'; import 'package:cake_wallet/core/key_service.dart'; +import 'package:cake_wallet/core/new_wallet_type_arguments.dart'; import 'package:cake_wallet/core/secure_storage.dart'; import 'package:cake_wallet/core/totp_request_details.dart'; import 'package:cake_wallet/core/wallet_connect/wallet_connect_key_service.dart'; @@ -30,6 +32,8 @@ import 'package:cake_wallet/entities/contact.dart'; import 'package:cake_wallet/entities/contact_record.dart'; import 'package:cake_wallet/entities/exchange_api_mode.dart'; import 'package:cake_wallet/entities/parse_address_from_domain.dart'; +import 'package:cake_wallet/entities/wallet_edit_page_arguments.dart'; +import 'package:cake_wallet/entities/wallet_manager.dart'; import 'package:cake_wallet/src/screens/receive/address_list_page.dart'; import 'package:cake_wallet/view_model/link_view_model.dart'; import 'package:cake_wallet/tron/tron.dart'; @@ -102,6 +106,7 @@ import 'package:cake_wallet/src/screens/settings/desktop_settings/desktop_settin import 'package:cake_wallet/src/screens/settings/display_settings_page.dart'; import 'package:cake_wallet/src/screens/settings/domain_lookups_page.dart'; import 'package:cake_wallet/src/screens/settings/manage_nodes_page.dart'; +import 'package:cake_wallet/src/screens/settings/mweb_settings.dart'; import 'package:cake_wallet/src/screens/settings/other_settings_page.dart'; import 'package:cake_wallet/src/screens/settings/privacy_page.dart'; import 'package:cake_wallet/src/screens/settings/security_backup_page.dart'; @@ -145,11 +150,14 @@ import 'package:cake_wallet/view_model/cake_pay/cake_pay_cards_list_view_model.d import 'package:cake_wallet/view_model/cake_pay/cake_pay_purchase_view_model.dart'; import 'package:cake_wallet/view_model/nano_account_list/nano_account_edit_or_create_view_model.dart'; import 'package:cake_wallet/view_model/nano_account_list/nano_account_list_view_model.dart'; +import 'package:cake_wallet/view_model/new_wallet_type_view_model.dart'; import 'package:cake_wallet/view_model/node_list/pow_node_list_view_model.dart'; -import 'package:cake_wallet/view_model/seed_type_view_model.dart'; +import 'package:cake_wallet/view_model/wallet_groups_display_view_model.dart'; +import 'package:cake_wallet/view_model/seed_settings_view_model.dart'; import 'package:cake_wallet/view_model/set_up_2fa_viewmodel.dart'; import 'package:cake_wallet/view_model/restore/restore_from_qr_vm.dart'; import 'package:cake_wallet/view_model/settings/display_settings_view_model.dart'; +import 'package:cake_wallet/view_model/settings/mweb_settings_view_model.dart'; import 'package:cake_wallet/view_model/settings/other_settings_view_model.dart'; import 'package:cake_wallet/view_model/settings/privacy_settings_view_model.dart'; import 'package:cake_wallet/view_model/settings/security_settings_view_model.dart'; @@ -157,9 +165,9 @@ import 'package:cake_wallet/view_model/advanced_privacy_settings_view_model.dart import 'package:cake_wallet/view_model/settings/trocador_providers_view_model.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart'; import 'package:cake_wallet/view_model/wallet_list/wallet_edit_view_model.dart'; -import 'package:cake_wallet/view_model/wallet_list/wallet_list_item.dart'; import 'package:cake_wallet/view_model/wallet_restore_choose_derivation_view_model.dart'; import 'package:cw_core/nano_account.dart'; +import 'package:cw_core/unspent_coin_type.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/wallet_service.dart'; import 'package:cw_core/transaction_info.dart'; @@ -179,6 +187,7 @@ import 'package:cake_wallet/store/dashboard/trades_store.dart'; import 'package:cake_wallet/store/dashboard/transaction_filter_store.dart'; import 'package:cake_wallet/store/node_list_store.dart'; import 'package:cake_wallet/store/secret_store.dart'; +import 'package:cake_wallet/store/seed_settings_store.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/store/templates/exchange_template_store.dart'; import 'package:cake_wallet/store/templates/send_template_store.dart'; @@ -331,6 +340,7 @@ Future setup({ YatStore(appStore: getIt.get(), secureStorage: getIt.get())..init()); getIt.registerSingleton( AnonpayTransactionsStore(anonpayInvoiceInfoSource: _anonpayInvoiceInfoSource)); + getIt.registerSingleton(SeedSettingsStore()); getIt.registerLazySingleton(() => LedgerViewModel()); @@ -356,12 +366,35 @@ Future setup({ getIt.get(), (WalletType type) => getIt.get(param1: type))); - getIt.registerFactoryParam((type, _) => WalletNewVM( + getIt.registerFactoryParam( + (newWalletArgs, _) => WalletNewVM( getIt.get(), - getIt.get(param1: type), + getIt.get(param1:newWalletArgs.type), _walletInfoSource, - getIt.get(param1: type), - type: type)); + getIt.get(param1: newWalletArgs.type), + getIt.get(), + newWalletArguments: newWalletArgs,)); + + + getIt.registerFactory(() => NewWalletTypeViewModel(_walletInfoSource)); + + getIt.registerFactory( + () { + final instance = WalletManager(_walletInfoSource, getIt.get()); + instance.updateWalletGroups(); + return instance; + }, + ); + + getIt.registerFactoryParam( + (type, _) => WalletGroupsDisplayViewModel( + getIt.get(), + getIt.get(), + getIt.get(), + getIt.get(), + type: type, + ), + ); getIt.registerFactoryParam((args, closable) { return WalletUnlockPage( @@ -422,14 +455,21 @@ Future setup({ walletType: args.walletType ?? currentWalletType); }); - getIt.registerFactoryParam((WalletType type, _) { - return WalletRestorationFromQRVM(getIt.get(), - getIt.get(param1: type), _walletInfoSource, type); - }); + getIt.registerFactoryParam((WalletType type, _) => + WalletRestorationFromQRVM( + getIt.get(), + getIt.get(param1: type), + _walletInfoSource, + type, + getIt.get())); getIt.registerFactoryParam((type, _) => - WalletHardwareRestoreViewModel(getIt.get(), getIt.get(), - getIt.get(param1: type), _walletInfoSource, + WalletHardwareRestoreViewModel( + getIt.get(), + getIt.get(), + getIt.get(param1: type), + _walletInfoSource, + getIt.get(), type: type)); getIt.registerFactory(() => WalletAddressListViewModel( @@ -638,7 +678,8 @@ Future setup({ getIt.registerFactory( () => Modify2FAPage(setup2FAViewModel: getIt.get())); - getIt.registerFactory(() => DesktopSettingsPage(getIt.get())); + getIt.registerFactory( + () => DesktopSettingsPage(getIt.get())); getIt.registerFactoryParam( (pageOption, _) => ReceiveOptionViewModel(getIt.get().wallet!, pageOption)); @@ -685,8 +726,8 @@ Future setup({ getIt.get(), getIt.get())); - getIt.registerFactory( - () => SendViewModel( + getIt.registerFactoryParam( + (coinTypeToSpendFrom, _) => SendViewModel( getIt.get(), getIt.get(), getIt.get(), @@ -694,12 +735,13 @@ Future setup({ getIt.get(), _transactionDescriptionBox, getIt.get().wallet!.isHardwareWallet ? getIt.get() : null, + coinTypeToSpendFrom: coinTypeToSpendFrom ?? UnspentCoinType.any, ), ); - getIt.registerFactoryParam( - (PaymentRequest? initialPaymentRequest, _) => SendPage( - sendViewModel: getIt.get(), + getIt.registerFactoryParam( + (PaymentRequest? initialPaymentRequest, coinTypeToSpendFrom) => SendPage( + sendViewModel: getIt.get(param1: coinTypeToSpendFrom), authService: getIt.get(), initialPaymentRequest: initialPaymentRequest, )); @@ -713,6 +755,7 @@ Future setup({ _walletInfoSource, getIt.get(), getIt.get(), + getIt.get(), ), ); } else { @@ -723,6 +766,7 @@ Future setup({ _walletInfoSource, getIt.get(), getIt.get(), + getIt.get(), ), ); } @@ -733,17 +777,28 @@ Future setup({ )); getIt.registerFactoryParam( - (WalletListViewModel walletListViewModel, _) => - WalletEditViewModel(walletListViewModel, getIt.get())); + (WalletListViewModel walletListViewModel, _) => WalletEditViewModel( + walletListViewModel, + getIt.get(), + getIt.get(), + ), + ); + + getIt.registerFactoryParam((arguments, _) { - getIt.registerFactoryParam, void>((args, _) { - final walletListViewModel = args.first as WalletListViewModel; - final editingWallet = args.last as WalletListItem; return WalletEditPage( - walletEditViewModel: getIt.get(param1: walletListViewModel), + pageArguments: WalletEditPageArguments( + walletEditViewModel: getIt.get(param1: arguments.walletListViewModel), authService: getIt.get(), - walletNewVM: getIt.get(param1: editingWallet.type), - editingWallet: editingWallet); + walletNewVM: getIt.get( + param1: NewWalletArguments(type: arguments.editingWallet.type), + ), + editingWallet: arguments.editingWallet, + isWalletGroup: arguments.isWalletGroup, + groupName: arguments.groupName, + parentAddress: arguments.parentAddress, + ), + ); }); getIt.registerFactory(() { @@ -757,7 +812,9 @@ Future setup({ getIt.registerFactory(() { final wallet = getIt.get().wallet!; - if (wallet.type == WalletType.monero || wallet.type == WalletType.wownero || wallet.type == WalletType.haven) { + if (wallet.type == WalletType.monero || + wallet.type == WalletType.wownero || + wallet.type == WalletType.haven) { return MoneroAccountListViewModel(wallet); } throw Exception( @@ -817,6 +874,9 @@ Future setup({ getIt.registerFactory(() => SilentPaymentsSettingsViewModel(getIt.get(), getIt.get().wallet!)); + getIt.registerFactory( + () => MwebSettingsViewModel(getIt.get(), getIt.get().wallet!)); + getIt.registerFactory(() { return PrivacySettingsViewModel(getIt.get(), getIt.get().wallet!); }); @@ -833,7 +893,7 @@ Future setup({ getIt.registerFactory(() => WalletSeedViewModel(getIt.get().wallet!)); - getIt.registerFactory(() => SeedTypeViewModel(getIt.get())); + getIt.registerFactory(() => SeedSettingsViewModel(getIt.get(), getIt.get())); getIt.registerFactoryParam((bool isWalletCreated, _) => WalletSeedPage(getIt.get(), isNewWalletCreated: isWalletCreated)); @@ -883,6 +943,8 @@ Future setup({ getIt.registerFactory( () => SilentPaymentsSettingsPage(getIt.get())); + getIt.registerFactory(() => MwebSettingsPage(getIt.get())); + getIt.registerFactory(() => OtherSettingsPage(getIt.get())); getIt.registerFactory(() => NanoChangeRepPage( @@ -977,8 +1039,12 @@ Future setup({ SettingsStoreBase.walletPasswordDirectInput, ); case WalletType.litecoin: - return bitcoin!.createLitecoinWalletService(_walletInfoSource, _unspentCoinsInfoSource, - SettingsStoreBase.walletPasswordDirectInput); + return bitcoin!.createLitecoinWalletService( + _walletInfoSource, + _unspentCoinsInfoSource, + getIt.get().mwebAlwaysScan, + SettingsStoreBase.walletPasswordDirectInput, + ); case WalletType.ethereum: return ethereum!.createEthereumWalletService( _walletInfoSource, SettingsStoreBase.walletPasswordDirectInput); @@ -1018,12 +1084,12 @@ Future setup({ getIt.registerFactory(() => FaqPage(getIt.get())); getIt.registerFactoryParam((type, _) => - WalletRestoreViewModel( - getIt.get(), getIt.get(param1: type), _walletInfoSource, + WalletRestoreViewModel(getIt.get(), getIt.get(param1: type), + _walletInfoSource, getIt.get(), type: type)); getIt.registerFactoryParam((type, _) => WalletRestorePage( - getIt.get(param1: type), getIt.get())); + getIt.get(param1: type), getIt.get())); getIt.registerFactoryParam, void>( (derivations, _) => WalletRestoreChooseDerivationViewModel(derivationInfos: derivations)); @@ -1034,31 +1100,46 @@ Future setup({ param1: derivations, ))); - getIt.registerFactoryParam( - (TransactionInfo transactionInfo, _) { - final wallet = getIt.get().wallet!; - return TransactionDetailsViewModel( - transactionInfo: transactionInfo, - transactionDescriptionBox: _transactionDescriptionBox, - wallet: wallet, - settingsStore: getIt.get(), - sendViewModel: getIt.get()); - }); + getIt.registerFactoryParam, void>( + (params, _) { + final transactionInfo = params[0] as TransactionInfo; + final canReplaceByFee = params[1] as bool? ?? false; + final wallet = getIt.get().wallet!; + + return TransactionDetailsViewModel( + transactionInfo: transactionInfo, + transactionDescriptionBox: _transactionDescriptionBox, + wallet: wallet, + settingsStore: getIt.get(), + sendViewModel: getIt.get(), + canReplaceByFee: canReplaceByFee, + ); + } + ); getIt.registerFactoryParam( - (TransactionInfo transactionInfo, _) => TransactionDetailsPage( - transactionDetailsViewModel: - getIt.get(param1: transactionInfo))); + (TransactionInfo transactionInfo, _) => TransactionDetailsPage( + transactionDetailsViewModel: getIt.get( + param1: [transactionInfo, false]))); - getIt.registerFactoryParam?>((param1, additionalParams) { - final isCreate = additionalParams?[0] ?? true; - final isHardwareWallet = additionalParams?[1] ?? false; + getIt.registerFactoryParam, void>( + (params, _) { + final transactionInfo = params[0] as TransactionInfo; + final txHex = params[1] as String; + return RBFDetailsPage( + transactionDetailsViewModel: getIt.get( + param1: [transactionInfo, true], + ), + rawTransaction: txHex, + ); + } + ); + getIt.registerFactoryParam( + (newWalletTypeArguments, _) { return NewWalletTypePage( - onTypeSelected: param1, - isCreate: isCreate, - isHardwareWallet: isHardwareWallet, + newWalletTypeArguments: newWalletTypeArguments, + newWalletTypeViewModel: getIt.get(), ); }); @@ -1074,6 +1155,7 @@ Future setup({ getIt.registerFactory(() => CakeFeaturesViewModel(getIt.get())); getIt.registerFactory(() => BackupService(getIt.get(), _walletInfoSource, + _transactionDescriptionBox, getIt.get(), getIt.get())); getIt.registerFactory(() => BackupViewModel( @@ -1135,14 +1217,21 @@ Future setup({ getIt.registerFactory(() => SupportOtherLinksPage(getIt.get())); - getIt.registerFactory(() { + getIt.registerFactoryParam( + (coinTypeToSpendFrom, _) { final wallet = getIt.get().wallet; - return UnspentCoinsListViewModel(wallet: wallet!, unspentCoinsInfo: _unspentCoinsInfoSource); + return UnspentCoinsListViewModel( + wallet: wallet!, + unspentCoinsInfo: _unspentCoinsInfoSource, + coinTypeToSpendFrom: coinTypeToSpendFrom ?? UnspentCoinType.any, + ); }); - getIt.registerFactory(() => - UnspentCoinsListPage(unspentCoinsListViewModel: getIt.get())); + getIt.registerFactoryParam( + (coinTypeToSpendFrom, _) => UnspentCoinsListPage( + unspentCoinsListViewModel: + getIt.get(param1: coinTypeToSpendFrom))); getIt.registerFactoryParam( @@ -1175,7 +1264,8 @@ Future setup({ getIt.registerFactory( () => CakePayService(getIt.get(), getIt.get())); - getIt.registerFactory(() => CakePayCardsListViewModel(cakePayService: getIt.get())); + getIt.registerFactory( + () => CakePayCardsListViewModel(cakePayService: getIt.get())); getIt.registerFactory(() => CakePayAuthViewModel(cakePayService: getIt.get())); @@ -1207,12 +1297,12 @@ Future setup({ getIt.registerFactoryParam, void>((List args, _) { final vendor = args.first as CakePayVendor; - return CakePayBuyCardPage(getIt.get(param1: vendor), - getIt.get()); + return CakePayBuyCardPage( + getIt.get(param1: vendor), getIt.get()); }); - getIt.registerFactoryParam, void>( - (List args, _) { + getIt + .registerFactoryParam, void>((List args, _) { final paymentCredential = args.first as PaymentCredential; final card = args[1] as CakePayCard; return CakePayBuyCardDetailPage( @@ -1223,11 +1313,6 @@ Future setup({ getIt.registerFactory(() => CakePayAccountPage(getIt.get())); - getIt.registerFactoryParam( - (TransactionInfo transactionInfo, _) => RBFDetailsPage( - transactionDetailsViewModel: - getIt.get(param1: transactionInfo))); - getIt.registerFactory(() => AnonPayApi( useTorOnly: getIt.get().exchangeStatus == ExchangeApiMode.torOnly, wallet: getIt.get().wallet!)); @@ -1275,7 +1360,7 @@ Future setup({ getIt.registerFactory( () => WalletConnectConnectionsView(web3walletService: getIt.get())); - + getIt.registerFactory(() => NFTViewModel(appStore, getIt.get())); getIt.registerFactory(() => TorPage(getIt.get())); diff --git a/lib/entities/default_settings_migration.dart b/lib/entities/default_settings_migration.dart index 3aad38179..af6f1ce78 100644 --- a/lib/entities/default_settings_migration.dart +++ b/lib/entities/default_settings_migration.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'dart:io' show Directory, File, Platform; import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/core/secure_storage.dart'; @@ -40,6 +41,7 @@ const solanaDefaultNodeUri = 'rpc.ankr.com'; const tronDefaultNodeUri = 'trx.nownodes.io'; const newCakeWalletBitcoinUri = 'btc-electrum.cakewallet.com:50002'; const wowneroDefaultNodeUri = 'node3.monerodevs.org:34568'; +const moneroWorldNodeUri = '.moneroworld.com'; Future defaultSettingsMigration( {required int version, @@ -233,7 +235,8 @@ Future defaultSettingsMigration( break; case 36: await addWowneroNodeList(nodes: nodes); - await changeWowneroCurrentNodeToDefault(sharedPreferences: sharedPreferences, nodes: nodes); + await changeWowneroCurrentNodeToDefault( + sharedPreferences: sharedPreferences, nodes: nodes); break; case 37: await replaceTronDefaultNode(sharedPreferences: sharedPreferences, nodes: nodes); @@ -245,6 +248,17 @@ Future defaultSettingsMigration( _fixNodesUseSSLFlag(nodes); await changeDefaultNanoNode(nodes, sharedPreferences); break; + case 40: + await removeMoneroWorld(sharedPreferences: sharedPreferences, nodes: nodes); + break; + case 41: + _deselectQuantex(sharedPreferences); + await _addSethNode(nodes, sharedPreferences); + await updateTronNodesWithNowNodes(sharedPreferences: sharedPreferences, nodes: nodes); + break; + case 42: + updateBtcElectrumNodeToUseSSL(nodes, sharedPreferences); + break; default: break; } @@ -259,6 +273,28 @@ Future defaultSettingsMigration( await sharedPreferences.setInt(PreferencesKey.currentDefaultSettingsMigrationVersion, version); } +void updateBtcElectrumNodeToUseSSL(Box nodes, SharedPreferences sharedPreferences) { + final btcElectrumNode = nodes.values.firstWhereOrNull((element) => element.uriRaw == newCakeWalletBitcoinUri); + + if (btcElectrumNode != null) { + btcElectrumNode.useSSL = true; + btcElectrumNode.save(); + } +} + +void _deselectQuantex(SharedPreferences sharedPreferences) { + final Map exchangeProvidersSelection = + json.decode(sharedPreferences.getString(PreferencesKey.exchangeProvidersSelection) ?? "{}") + as Map; + + exchangeProvidersSelection['Quantex'] = false; + + sharedPreferences.setString( + PreferencesKey.exchangeProvidersSelection, + json.encode(exchangeProvidersSelection), + ); +} + void _fixNodesUseSSLFlag(Box nodes) { for (Node node in nodes.values) { switch (node.uriRaw) { @@ -488,15 +524,7 @@ Node? getBitcoinCashDefaultElectrumServer({required Box nodes}) { Node getMoneroDefaultNode({required Box nodes}) { final timeZone = DateTime.now().timeZoneOffset.inHours; - var nodeUri = ''; - - if (timeZone >= 1) { - // Eurasia - nodeUri = 'xmr-node-eu.cakewallet.com:18081'; - } else if (timeZone <= -4) { - // America - nodeUri = 'xmr-node-usa-east.cakewallet.com:18081'; - } + var nodeUri = newCakeWalletMoneroUri; try { return nodes.values.firstWhere((Node node) => node.uriRaw == nodeUri); @@ -883,7 +911,9 @@ Future changeDefaultBitcoinNode( final newCakeWalletBitcoinNode = Node(uri: newCakeWalletBitcoinUri, type: WalletType.bitcoin, useSSL: false); - await nodeSource.add(newCakeWalletBitcoinNode); + if (!nodeSource.values.any((element) => element.uriRaw == newCakeWalletBitcoinUri)) { + await nodeSource.add(newCakeWalletBitcoinNode); + } if (needToReplaceCurrentBitcoinNode) { await sharedPreferences.setInt( @@ -891,7 +921,34 @@ Future changeDefaultBitcoinNode( } } +Future _addSethNode(Box nodeSource, SharedPreferences sharedPreferences) async { + _addBitcoinNode( + nodeSource: nodeSource, + sharedPreferences: sharedPreferences, + nodeUri: "fulcrum.sethforprivacy.com:50002", + useSSL: false, + ); +} + Future _addElectRsNode(Box nodeSource, SharedPreferences sharedPreferences) async { + _addBitcoinNode( + nodeSource: nodeSource, + sharedPreferences: sharedPreferences, + nodeUri: cakeWalletSilentPaymentsElectrsUri, + ); +} + +Future _addBitcoinNode({ + required Box nodeSource, + required SharedPreferences sharedPreferences, + required String nodeUri, + bool replaceExisting = false, + bool useSSL = false, +}) async { + bool isNodeExists = nodeSource.values.any((element) => element.uriRaw == nodeUri); + if (isNodeExists) { + return; + } const cakeWalletBitcoinNodeUriPattern = '.cakewallet.com'; final currentBitcoinNodeId = sharedPreferences.getInt(PreferencesKey.currentBitcoinElectrumSererIdKey); @@ -900,12 +957,11 @@ Future _addElectRsNode(Box nodeSource, SharedPreferences sharedPrefe final needToReplaceCurrentBitcoinNode = currentBitcoinNode.uri.toString().contains(cakeWalletBitcoinNodeUriPattern); - final newElectRsBitcoinNode = - Node(uri: cakeWalletSilentPaymentsElectrsUri, type: WalletType.bitcoin, useSSL: false); + final newElectRsBitcoinNode = Node(uri: nodeUri, type: WalletType.bitcoin, useSSL: useSSL); await nodeSource.add(newElectRsBitcoinNode); - if (needToReplaceCurrentBitcoinNode) { + if (needToReplaceCurrentBitcoinNode && replaceExisting) { await sharedPreferences.setInt( PreferencesKey.currentBitcoinElectrumSererIdKey, newElectRsBitcoinNode.key as int); } @@ -1260,3 +1316,36 @@ Future replaceTronDefaultNode({ // If it's not, we switch user to the new default node: NowNodes await changeTronCurrentNodeToDefault(sharedPreferences: sharedPreferences, nodes: nodes); } + +Future removeMoneroWorld( + {required SharedPreferences sharedPreferences, required Box nodes}) async { + const cakeWalletMoneroNodeUriPattern = '.moneroworld.com'; + final currentMoneroNodeId = sharedPreferences.getInt(PreferencesKey.currentNodeIdKey); + final currentMoneroNode = nodes.values.firstWhere((node) => node.key == currentMoneroNodeId); + final needToReplaceCurrentMoneroNode = + currentMoneroNode.uri.toString().contains(cakeWalletMoneroNodeUriPattern); + + nodes.values.forEach((node) async { + if (node.type == WalletType.monero && + node.uri.toString().contains(cakeWalletMoneroNodeUriPattern)) { + await node.delete(); + } + }); + + if (needToReplaceCurrentMoneroNode) { + await changeMoneroCurrentNodeToDefault(sharedPreferences: sharedPreferences, nodes: nodes); + } +} + +Future updateTronNodesWithNowNodes({ + required SharedPreferences sharedPreferences, + required Box nodes, +}) async { + final tronNowNodesUri = 'trx.nownodes.io'; + + if (nodes.values.any((node) => node.uriRaw == tronNowNodesUri)) return; + + await nodes.add(Node(uri: tronNowNodesUri, type: WalletType.tron)); + + await replaceTronDefaultNode(sharedPreferences: sharedPreferences, nodes: nodes); +} diff --git a/lib/entities/erc20_token_info_explorers.dart b/lib/entities/erc20_token_info_explorers.dart new file mode 100644 index 000000000..386db5a50 --- /dev/null +++ b/lib/entities/erc20_token_info_explorers.dart @@ -0,0 +1,80 @@ +class Erc20TokenInfoExplorers { + String? contractAddress; + String? tokenName; + String? symbol; + String? divisor; + String? tokenType; + String? totalSupply; + String? blueCheckmark; + String? description; + String? website; + String? email; + String? blog; + String? reddit; + String? slack; + String? facebook; + String? twitter; + String? bitcointalk; + String? github; + String? telegram; + String? wechat; + String? linkedin; + String? discord; + String? whitepaper; + String? tokenPriceUSD; + String? image; + + Erc20TokenInfoExplorers({ + this.contractAddress, + this.tokenName, + this.symbol, + this.divisor, + this.tokenType, + this.totalSupply, + this.blueCheckmark, + this.description, + this.website, + this.email, + this.blog, + this.reddit, + this.slack, + this.facebook, + this.twitter, + this.bitcointalk, + this.github, + this.telegram, + this.wechat, + this.linkedin, + this.discord, + this.whitepaper, + this.tokenPriceUSD, + this.image, + }); + + Erc20TokenInfoExplorers.fromJson(Map json) { + contractAddress = json['contractAddress'] as String?; + tokenName = json['tokenName'] as String?; + symbol = json['symbol'] as String?; + divisor = json['divisor'] as String?; + tokenType = json['tokenType'] as String?; + totalSupply = json['totalSupply'] as String?; + blueCheckmark = json['blueCheckmark'] as String?; + description = json['description'] as String?; + website = json['website'] as String?; + email = json['email'] as String?; + blog = json['blog'] as String?; + reddit = json['reddit'] as String?; + slack = json['slack'] as String?; + facebook = json['facebook'] as String?; + twitter = json['twitter'] as String?; + bitcointalk = json['bitcointalk'] as String?; + github = json['github'] as String?; + telegram = json['telegram'] as String?; + wechat = json['wechat'] as String?; + linkedin = json['linkedin'] as String?; + discord = json['discord'] as String?; + whitepaper = json['whitepaper'] as String?; + tokenPriceUSD = json['tokenPriceUSD'] as String?; + image = json['image'] as String?; + } +} diff --git a/lib/entities/erc20_token_info_moralis.dart b/lib/entities/erc20_token_info_moralis.dart new file mode 100644 index 000000000..517ff9712 --- /dev/null +++ b/lib/entities/erc20_token_info_moralis.dart @@ -0,0 +1,85 @@ +class Erc20TokenInfoMoralis { + String? address; + String? addressLabel; + String? name; + String? symbol; + String? decimals; + String? logo; + String? logoHash; + String? thumbnail; + String? totalSupply; + String? totalSupplyFormatted; + String? fullyDilutedValuation; + String? blockNumber; + int? validated; + String? createdAt; + bool? possibleSpam; + bool? verifiedContract; + Links? links; + int? securityScore; + + Erc20TokenInfoMoralis({ + this.address, + this.addressLabel, + this.name, + this.symbol, + this.decimals, + this.logo, + this.logoHash, + this.thumbnail, + this.totalSupply, + this.totalSupplyFormatted, + this.fullyDilutedValuation, + this.blockNumber, + this.validated, + this.createdAt, + this.possibleSpam, + this.verifiedContract, + this.links, + this.securityScore, + }); + + Erc20TokenInfoMoralis.fromJson(Map json) { + address = json['address'] as String?; + addressLabel = json['address_label'] as String?; + name = json['name'] as String?; + symbol = json['symbol'] as String?; + decimals = json['decimals'] as String?; + logo = json['logo'] as String?; + logoHash = json['logo_hash'] as String?; + thumbnail = json['thumbnail'] as String?; + totalSupply = json['total_supply'] as String?; + totalSupplyFormatted = json['total_supply_formatted'] as String?; + fullyDilutedValuation = json['fully_diluted_valuation'] as String?; + blockNumber = json['block_number'] as String?; + validated = json['validated'] as int?; + createdAt = json['created_at'] as String?; + possibleSpam = json['possible_spam'] as bool?; + verifiedContract = json['verified_contract'] as bool; + links = + json['links'] != null ? new Links.fromJson(json['links'] as Map) : null; + securityScore = json['security_score'] as int?; + } +} + +class Links { + String? twitter; + String? website; + String? facebook; + String? reddit; + String? github; + String? linkedin; + String? telegram; + + Links({this.twitter, this.website, this.facebook, this.reddit}); + + Links.fromJson(Map json) { + twitter = json['twitter'] as String?; + website = json['website'] as String?; + facebook = json['facebook'] as String?; + reddit = json['reddit'] as String?; + github = json['github'] as String?; + linkedin = json['linkedin'] as String?; + telegram = json['telegram'] as String?; + } +} diff --git a/lib/entities/hardware_wallet/hardware_wallet_device.dart b/lib/entities/hardware_wallet/hardware_wallet_device.dart new file mode 100644 index 000000000..d3acc5d32 --- /dev/null +++ b/lib/entities/hardware_wallet/hardware_wallet_device.dart @@ -0,0 +1,65 @@ +import 'package:ledger_flutter_plus/ledger_flutter_plus.dart' as ledger; + +class HardwareWalletDevice { + final String name; + final HardwareWalletDeviceType type; + final HardwareWalletConnectionType connectionType; + + const HardwareWalletDevice({ + required this.name, + required this.type, + required this.connectionType, + }); + + factory HardwareWalletDevice.fromLedgerDevice(ledger.LedgerDevice device) => + HardwareWalletDevice( + name: device.name, + type: device.deviceInfo.toGeneric(), + connectionType: device.connectionType.toGeneric(), + ); +} + +enum HardwareWalletDeviceType { + ledgerBlue, + ledgerNanoS, + ledgerNanoX, + ledgerNanoSPlus, + ledgerStax, + ledgerFlex; +} + +enum HardwareWalletConnectionType { + usb, + ble, + nfc; +} + +extension ToGenericHardwareWalletDeviceType on ledger.LedgerDeviceType { + HardwareWalletDeviceType toGeneric() { + switch (this) { + case ledger.LedgerDeviceType.blue: + return HardwareWalletDeviceType.ledgerBlue; + case ledger.LedgerDeviceType.nanoS: + return HardwareWalletDeviceType.ledgerNanoS; + case ledger.LedgerDeviceType.nanoSP: + return HardwareWalletDeviceType.ledgerNanoSPlus; + case ledger.LedgerDeviceType.nanoX: + return HardwareWalletDeviceType.ledgerNanoX; + case ledger.LedgerDeviceType.stax: + return HardwareWalletDeviceType.ledgerStax; + case ledger.LedgerDeviceType.flex: + return HardwareWalletDeviceType.ledgerFlex; + } + } +} + +extension ToGenericHardwareWalletConnectionType on ledger.ConnectionType { + HardwareWalletConnectionType toGeneric() { + switch (this) { + case ledger.ConnectionType.usb: + return HardwareWalletConnectionType.usb; + case ledger.ConnectionType.ble: + return HardwareWalletConnectionType.ble; + } + } +} diff --git a/lib/entities/language_service.dart b/lib/entities/language_service.dart index 23d27dd38..78437fdce 100644 --- a/lib/entities/language_service.dart +++ b/lib/entities/language_service.dart @@ -29,7 +29,9 @@ class LanguageService { 'id': 'Bahasa Indonesia (Indonesian)', 'yo': 'Yorùbá (Yoruba)', 'ha': 'Hausa Najeriya (Nigeria)', - 'tl': 'Filipino (Tagalog)' + 'tl': 'Filipino (Tagalog)', + 'hy': 'Հայերեն (Armenian)', + 'vi': 'Tiếng Việt (Vietnamese)', }; static const Map localeCountryCode = { @@ -58,7 +60,9 @@ class LanguageService { 'id': 'idn', 'yo': 'nga', 'ha': 'hau', - 'tl': 'phl' + 'tl': 'phl', + 'hy': 'arm', + 'vi': 'vnm', }; static final list = {}; diff --git a/lib/entities/parse_address_from_domain.dart b/lib/entities/parse_address_from_domain.dart index c95ce9847..481db5620 100644 --- a/lib/entities/parse_address_from_domain.dart +++ b/lib/entities/parse_address_from_domain.dart @@ -51,7 +51,7 @@ class AddressResolver { throw Exception('Unexpected token: $type for getAddressFromStringPattern'); } - final match = RegExp(addressPattern).firstMatch(raw); + final match = RegExp(addressPattern, multiLine: true).firstMatch(raw); return match?.group(0)?.replaceAllMapped(RegExp('[^0-9a-zA-Z]|bitcoincash:|nano_|ban_'), (Match match) { String group = match.group(0)!; @@ -213,8 +213,7 @@ class AddressResolver { await NostrProfileHandler.processRelays(context, nostrProfile!, text); if (nostrUserData != null) { - String? addressFromBio = extractAddressByType( - raw: nostrUserData.about, type: currency); + String? addressFromBio = extractAddressByType(raw: nostrUserData.about, type: currency); if (addressFromBio != null) { return ParsedAddress.nostrAddress( address: addressFromBio, diff --git a/lib/entities/pin_code_required_duration.dart b/lib/entities/pin_code_required_duration.dart index fef5715b5..0c0d6d132 100644 --- a/lib/entities/pin_code_required_duration.dart +++ b/lib/entities/pin_code_required_duration.dart @@ -1,32 +1,39 @@ import 'package:cake_wallet/generated/i18n.dart'; -enum PinCodeRequiredDuration { - always(0), - tenminutes(10), - onehour(60); +enum PinCodeRequiredDuration { + always(0), + tenMinutes(10), + halfHour(30), + fortyFiveMinutes(45), + oneHour(60); const PinCodeRequiredDuration(this.value); + final int value; static PinCodeRequiredDuration deserialize({required int raw}) => PinCodeRequiredDuration.values.firstWhere((e) => e.value == raw); @override - String toString(){ + String toString() { String label = ''; switch (this) { case PinCodeRequiredDuration.always: label = S.current.always; break; - case PinCodeRequiredDuration.tenminutes: + case PinCodeRequiredDuration.tenMinutes: label = S.current.minutes_to_pin_code('10'); break; - case PinCodeRequiredDuration.onehour: + case PinCodeRequiredDuration.oneHour: label = S.current.minutes_to_pin_code('60'); break; + case PinCodeRequiredDuration.halfHour: + label = S.current.minutes_to_pin_code('30'); + break; + case PinCodeRequiredDuration.fortyFiveMinutes: + label = S.current.minutes_to_pin_code('45'); + break; } return label; - } - -} \ No newline at end of file +} diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index e1ee0ada3..4fbe358e5 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -48,6 +48,10 @@ class PreferencesKey { static const customBitcoinFeeRate = 'custom_electrum_fee_rate'; static const silentPaymentsCardDisplay = 'silentPaymentsCardDisplay'; static const silentPaymentsAlwaysScan = 'silentPaymentsAlwaysScan'; + static const mwebCardDisplay = 'mwebCardDisplay'; + static const mwebEnabled = 'mwebEnabled'; + static const hasEnabledMwebBefore = 'hasEnabledMwebBefore'; + static const mwebAlwaysScan = 'mwebAlwaysScan'; static const shouldShowReceiveWarning = 'should_show_receive_warning'; static const shouldShowYatPopup = 'should_show_yat_popup'; static const shouldShowRepWarning = 'should_show_rep_warning'; @@ -61,6 +65,7 @@ class PreferencesKey { static const useEtherscan = 'use_etherscan'; static const usePolygonScan = 'use_polygonscan'; static const useTronGrid = 'use_trongrid'; + static const useMempoolFeeAPI = 'use_mempool_fee_api'; static const defaultNanoRep = 'default_nano_representative'; static const defaultBananoRep = 'default_banano_representative'; static const lookupsTwitter = 'looks_up_twitter'; @@ -77,6 +82,8 @@ class PreferencesKey { static const exchangeProvidersSelection = 'exchange-providers-selection'; static const autoGenerateSubaddressStatusKey = 'auto_generate_subaddress_status'; static const moneroSeedType = 'monero_seed_type'; + static const bitcoinSeedType = 'bitcoin_seed_type'; + static const nanoSeedType = 'nano_seed_type'; static const clearnetDonationLink = 'clearnet_donation_link'; static const onionDonationLink = 'onion_donation_link'; static const donationLinkWalletName = 'donation_link_wallet_name'; diff --git a/lib/entities/seed_type.dart b/lib/entities/seed_type.dart index bc2f6cff7..20600e704 100644 --- a/lib/entities/seed_type.dart +++ b/lib/entities/seed_type.dart @@ -1,18 +1,19 @@ import 'package:cake_wallet/generated/i18n.dart'; import 'package:cw_core/enumerable_item.dart'; +import 'package:cw_core/wallet_info.dart'; -class SeedType extends EnumerableItem with Serializable { - const SeedType({required String title, required int raw}) : super(title: title, raw: raw); +class MoneroSeedType extends EnumerableItem with Serializable { + const MoneroSeedType({required String title, required int raw}) : super(title: title, raw: raw); - static const all = [SeedType.legacy, SeedType.polyseed]; + static const all = [MoneroSeedType.legacy, MoneroSeedType.polyseed]; static const defaultSeedType = polyseed; - static const legacy = SeedType(raw: 0, title: 'Legacy (25 words)'); - static const polyseed = SeedType(raw: 1, title: 'Polyseed (16 words)'); - static const wowneroSeed = SeedType(raw: 2, title: 'Wownero (14 words)'); + static const legacy = MoneroSeedType(raw: 0, title: 'Legacy (25 words)'); + static const polyseed = MoneroSeedType(raw: 1, title: 'Polyseed (16 words)'); + static const wowneroSeed = MoneroSeedType(raw: 2, title: 'Wownero (14 words)'); - static SeedType deserialize({required int raw}) { + static MoneroSeedType deserialize({required int raw}) { switch (raw) { case 0: return legacy; @@ -28,14 +29,64 @@ class SeedType extends EnumerableItem with Serializable { @override String toString() { switch (this) { - case SeedType.legacy: + case MoneroSeedType.legacy: return S.current.seedtype_legacy; - case SeedType.polyseed: + case MoneroSeedType.polyseed: return S.current.seedtype_polyseed; - case SeedType.wowneroSeed: + case MoneroSeedType.wowneroSeed: return S.current.seedtype_wownero; default: return ''; } } } + +class BitcoinSeedType extends EnumerableItem with Serializable { + const BitcoinSeedType(this.type, {required String title, required int raw}) + : super(title: title, raw: raw); + + final DerivationType type; + + static const all = [BitcoinSeedType.electrum, BitcoinSeedType.bip39]; + + static const defaultDerivationType = bip39; + + static const electrum = BitcoinSeedType(DerivationType.electrum, raw: 0, title: 'Electrum'); + static const bip39 = BitcoinSeedType(DerivationType.bip39, raw: 1, title: 'BIP39'); + + static BitcoinSeedType deserialize({required int raw}) { + switch (raw) { + case 0: + return electrum; + case 1: + return bip39; + default: + throw Exception('Unexpected token: $raw for SeedType deserialize'); + } + } +} + +class NanoSeedType extends EnumerableItem with Serializable { + const NanoSeedType(this.type, {required String title, required int raw}) + : super(title: title, raw: raw); + + final DerivationType type; + + static const all = [NanoSeedType.nanoStandard, NanoSeedType.bip39]; + + static const defaultDerivationType = bip39; + + static const nanoStandard = NanoSeedType(DerivationType.nano, raw: 0, title: 'Nano'); + static const bip39 = NanoSeedType(DerivationType.bip39, raw: 1, title: 'BIP39'); + + static NanoSeedType deserialize({required int raw}) { + switch (raw) { + case 0: + return nanoStandard; + case 1: + return bip39; + default: + throw Exception('Unexpected token: $raw for SeedType deserialize'); + } + } +} diff --git a/lib/entities/transaction_description.dart b/lib/entities/transaction_description.dart index 088f9c480..2ac573652 100644 --- a/lib/entities/transaction_description.dart +++ b/lib/entities/transaction_description.dart @@ -21,4 +21,18 @@ class TransactionDescription extends HiveObject { String? transactionNote; String get note => transactionNote ?? ''; + + Map toJson() => { + 'id': id, + 'recipientAddress': recipientAddress, + 'transactionNote': transactionNote, + }; + + factory TransactionDescription.fromJson(Map json) { + return TransactionDescription( + id: json['id'] as String, + recipientAddress: json['recipientAddress'] as String?, + transactionNote: json['transactionNote'] as String?, + ); + } } diff --git a/lib/entities/unstoppable_domain_address.dart b/lib/entities/unstoppable_domain_address.dart index 0f56517b8..6966fdd75 100644 --- a/lib/entities/unstoppable_domain_address.dart +++ b/lib/entities/unstoppable_domain_address.dart @@ -1,10 +1,7 @@ import 'dart:convert'; -import 'package:flutter/services.dart'; import 'package:http/http.dart' as http; -const channel = MethodChannel('com.cake_wallet/native_utils'); - Future fetchUnstoppableDomainAddress(String domain, String ticker) async { var address = ''; diff --git a/lib/entities/wallet_edit_page_arguments.dart b/lib/entities/wallet_edit_page_arguments.dart new file mode 100644 index 000000000..260471f7e --- /dev/null +++ b/lib/entities/wallet_edit_page_arguments.dart @@ -0,0 +1,28 @@ +import 'package:cake_wallet/core/auth_service.dart'; +import 'package:cake_wallet/view_model/wallet_list/wallet_edit_view_model.dart'; +import 'package:cake_wallet/view_model/wallet_list/wallet_list_item.dart'; +import 'package:cake_wallet/view_model/wallet_list/wallet_list_view_model.dart'; +import 'package:cake_wallet/view_model/wallet_new_vm.dart'; + +class WalletEditPageArguments { + WalletEditPageArguments({ + required this.editingWallet, + this.isWalletGroup = false, + this.walletListViewModel, + this.groupName = '', + this.parentAddress = '', + this.walletEditViewModel, + this.walletNewVM, + this.authService, + }); + + final WalletListItem editingWallet; + final bool isWalletGroup; + final String groupName; + final String parentAddress; + final WalletListViewModel? walletListViewModel; + + final WalletEditViewModel? walletEditViewModel; + final WalletNewVM? walletNewVM; + final AuthService? authService; +} diff --git a/lib/entities/wallet_group.dart b/lib/entities/wallet_group.dart new file mode 100644 index 000000000..9845aea65 --- /dev/null +++ b/lib/entities/wallet_group.dart @@ -0,0 +1,21 @@ +import 'package:cw_core/wallet_info.dart'; + +class WalletGroup { + WalletGroup(this.parentAddress) : wallets = []; + + /// Main identifier for each group, compulsory. + final String parentAddress; + + /// Child wallets that share the same parent address within this group + List wallets; + + /// Custom name for the group, editable for multi-child wallet groups + String? groupName; + + /// Allows editing of the group name (only for multi-child groups). + void setCustomName(String name) { + if (wallets.length > 1) { + groupName = name; + } + } +} diff --git a/lib/entities/wallet_manager.dart b/lib/entities/wallet_manager.dart new file mode 100644 index 000000000..29c873dae --- /dev/null +++ b/lib/entities/wallet_manager.dart @@ -0,0 +1,110 @@ +import 'package:cake_wallet/entities/wallet_group.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:hive/hive.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class WalletManager { + WalletManager( + this._walletInfoSource, + this._sharedPreferences, + ); + + final Box _walletInfoSource; + final SharedPreferences _sharedPreferences; + + final List walletGroups = []; + + /// Categorize wallets into groups based on their parentAddress. + /// + /// Update the lead wallet for each group and clean up empty groups + /// i.e remove group if there's no lead wallet (i.e, no wallets left) + void updateWalletGroups() { + walletGroups.clear(); + + for (var walletInfo in _walletInfoSource.values) { + final group = _getOrCreateGroup(_resolveParentAddress(walletInfo)); + group.wallets.add(walletInfo); + } + + walletGroups.removeWhere((group) => group.wallets.isEmpty); + + _loadCustomGroupNames(); + } + + /// Function to determine the correct parentAddress for a wallet. + /// + /// If it's a parent wallet (parentAddress is null), + /// use its own address as parentAddress. + String _resolveParentAddress(WalletInfo walletInfo) { + return walletInfo.parentAddress ?? walletInfo.address; + } + + /// Check if a group with the parentAddress already exists, + /// If no group exists, create a new one. + /// + WalletGroup _getOrCreateGroup(String parentAddress) { + return walletGroups.firstWhere( + (group) => group.parentAddress == parentAddress, + orElse: () { + final newGroup = WalletGroup(parentAddress); + walletGroups.add(newGroup); + return newGroup; + }, + ); + } + + /// Add a new wallet and update lead wallet after adding. + void addWallet(WalletInfo walletInfo) { + final group = _getOrCreateGroup(_resolveParentAddress(walletInfo)); + group.wallets.add(walletInfo); + } + + /// Removes a wallet from a group i.e when it's deleted. + /// + /// Update lead wallet after removing, + /// Remove the group if it's empty (i.e., no lead wallet). + void removeWallet(WalletInfo walletInfo) { + final group = _getOrCreateGroup(_resolveParentAddress(walletInfo)); + group.wallets.remove(walletInfo); + + if (group.wallets.isEmpty) { + walletGroups.remove(group); + } + } + + /// Returns all the child wallets within a group. + /// + /// If the group is not found, returns an empty group with no wallets. + List getWalletsInGroup(String parentAddress) { + return walletGroups + .firstWhere( + (group) => group.parentAddress == parentAddress, + orElse: () => WalletGroup(parentAddress), + ) + .wallets; + } + + /// Iterate through all groups and load their custom names from storage + void _loadCustomGroupNames() { + for (var group in walletGroups) { + final groupName = _sharedPreferences.getString('wallet_group_name_${group.parentAddress}'); + if (groupName != null && group.wallets.length > 1) { + group.groupName = groupName; // Restore custom name + } + } + } + + /// Save custom name for a group + void _saveCustomGroupName(String parentAddress, String name) { + _sharedPreferences.setString('wallet_group_name_$parentAddress', name); + } + + // Set custom group name and persist it + void setGroupName(String parentAddress, String name) { + if (parentAddress.isEmpty || name.isEmpty) return; + + final group = walletGroups.firstWhere((group) => group.parentAddress == parentAddress); + group.setCustomName(name); + _saveCustomGroupName(parentAddress, name); // Persist the custom name + } +} diff --git a/lib/ethereum/cw_ethereum.dart b/lib/ethereum/cw_ethereum.dart index 4e210b227..7a06a1679 100644 --- a/lib/ethereum/cw_ethereum.dart +++ b/lib/ethereum/cw_ethereum.dart @@ -10,18 +10,34 @@ class CWEthereum extends Ethereum { @override WalletCredentials createEthereumNewWalletCredentials({ required String name, + String? mnemonic, + String? parentAddress, WalletInfo? walletInfo, String? password, + String? passphrase, }) => - EVMChainNewWalletCredentials(name: name, walletInfo: walletInfo, password: password); + EVMChainNewWalletCredentials( + name: name, + walletInfo: walletInfo, + password: password, + parentAddress: parentAddress, + mnemonic: mnemonic, + passphrase: passphrase, + ); @override WalletCredentials createEthereumRestoreWalletFromSeedCredentials({ required String name, required String mnemonic, required String password, + String? passphrase, }) => - EVMChainRestoreWalletFromSeedCredentials(name: name, password: password, mnemonic: mnemonic); + EVMChainRestoreWalletFromSeedCredentials( + name: name, + password: password, + mnemonic: mnemonic, + passphrase: passphrase, + ); @override WalletCredentials createEthereumRestoreWalletFromPrivateKey({ @@ -175,21 +191,21 @@ class CWEthereum extends Ethereum { String getTokenAddress(CryptoCurrency asset) => (asset as Erc20Token).contractAddress; @override - void setLedger(WalletBase wallet, Ledger ledger, LedgerDevice device) { - ((wallet as EVMChainWallet).evmChainPrivateKey as EvmLedgerCredentials).setLedger( - ledger, - device.connectionType == ConnectionType.usb ? device : null, - wallet.walletInfo.derivationInfo?.derivationPath); + void setLedgerConnection( + WalletBase wallet, ledger.LedgerConnection connection) { + ((wallet as EVMChainWallet).evmChainPrivateKey as EvmLedgerCredentials) + .setLedgerConnection( + connection, wallet.walletInfo.derivationInfo?.derivationPath); } @override Future> getHardwareWalletAccounts(LedgerViewModel ledgerVM, {int index = 0, int limit = 5}) async { - final hardwareWalletService = EVMChainHardwareWalletService(ledgerVM.ledger, ledgerVM.device); + final hardwareWalletService = EVMChainHardwareWalletService(ledgerVM.connection); try { return await hardwareWalletService.getAvailableAccounts(index: index, limit: limit); - } on LedgerException catch (err) { - print(err.message); + } catch (err) { + print(err); throw err; } } diff --git a/lib/exchange/exchange_provider_description.dart b/lib/exchange/exchange_provider_description.dart index c28de5b72..9f3723356 100644 --- a/lib/exchange/exchange_provider_description.dart +++ b/lib/exchange/exchange_provider_description.dart @@ -27,6 +27,10 @@ class ExchangeProviderDescription extends EnumerableItem with Serializable< 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 letsExchange = + ExchangeProviderDescription(title: 'LetsExchange', raw: 10, image: 'assets/images/letsexchange_icon.svg'); + static const stealthEx = + ExchangeProviderDescription(title: 'StealthEx', raw: 11, image: 'assets/images/stealthex.png'); static ExchangeProviderDescription deserialize({required int raw}) { switch (raw) { @@ -50,6 +54,10 @@ class ExchangeProviderDescription extends EnumerableItem with Serializable< return thorChain; case 9: return quantex; + case 10: + return letsExchange; + case 11: + return stealthEx; default: throw Exception('Unexpected token: $raw for ExchangeProviderDescription deserialize'); } diff --git a/lib/exchange/provider/letsexchange_exchange_provider.dart b/lib/exchange/provider/letsexchange_exchange_provider.dart new file mode 100644 index 000000000..1d4da55cb --- /dev/null +++ b/lib/exchange/provider/letsexchange_exchange_provider.dart @@ -0,0 +1,290 @@ +import 'dart:convert'; +import 'dart:developer'; + +import 'package:cake_wallet/.secrets.g.dart' as secrets; +import 'package:cake_wallet/exchange/provider/exchange_provider.dart'; +import 'package:cake_wallet/exchange/exchange_provider_description.dart'; +import 'package:cake_wallet/exchange/limits.dart'; +import 'package:cake_wallet/exchange/trade.dart'; +import 'package:cake_wallet/exchange/trade_not_created_exception.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:http/http.dart' as http; + +class LetsExchangeExchangeProvider extends ExchangeProvider { + LetsExchangeExchangeProvider() : super(pairList: supportedPairs(_notSupported)); + + static const List _notSupported = []; + + static const apiKey = secrets.letsExchangeBearerToken; + static const _baseUrl = 'api.letsexchange.io'; + static const _infoPath = '/api/v1/info'; + static const _infoRevertPath = '/api/v1/info-revert'; + static const _createTransactionPath = '/api/v1/transaction'; + static const _createTransactionRevertPath = '/api/v1/transaction-revert'; + static const _getTransactionPath = '/api/v1/transaction'; + + static const _affiliateId = secrets.letsExchangeAffiliateId; + + @override + String get title => 'LetsExchange'; + + @override + bool get isAvailable => true; + + @override + bool get isEnabled => true; + + @override + bool get supportsFixedRate => true; + + @override + ExchangeProviderDescription get description => ExchangeProviderDescription.letsExchange; + + @override + Future checkIsAvailable() async => true; + + @override + Future fetchLimits( + {required CryptoCurrency from, + required CryptoCurrency to, + required bool isFixedRateMode}) async { + final networkFrom = _getNetworkType(from); + final networkTo = _getNetworkType(to); + + try { + final params = { + 'from': from.title, + 'to': to.title, + if (networkFrom != null) 'network_from': networkFrom, + if (networkTo != null) 'network_to': networkTo, + 'amount': '1', + 'affiliate_id': _affiliateId + }; + + final responseJSON = await _getInfo(params, isFixedRateMode); + final min = double.tryParse(responseJSON['min_amount'] as String); + final max = double.tryParse(responseJSON['max_amount'] as String); + return Limits(min: min, max: max); + } catch (e) { + log(e.toString()); + throw Exception('Failed to fetch limits'); + } + } + + @override + Future fetchRate( + {required CryptoCurrency from, + required CryptoCurrency to, + required double amount, + required bool isFixedRateMode, + required bool isReceiveAmount}) async { + final networkFrom = _getNetworkType(from); + final networkTo = _getNetworkType(to); + try { + final params = { + 'from': from.title, + 'to': to.title, + if (networkFrom != null) 'network_from': networkFrom, + if (networkTo != null) 'network_to': networkTo, + 'amount': amount.toString(), + 'affiliate_id': _affiliateId + }; + + final responseJSON = await _getInfo(params, isFixedRateMode); + + final amountToGet = double.tryParse(responseJSON['amount'] as String) ?? 0.0; + + return isFixedRateMode ? amount / amountToGet : amountToGet / amount; + } catch (e) { + log(e.toString()); + return 0.0; + } + } + + @override + Future createTrade( + {required TradeRequest request, + required bool isFixedRateMode, + required bool isSendAll}) async { + final networkFrom = _getNetworkType(request.fromCurrency); + final networkTo = _getNetworkType(request.toCurrency); + try { + final params = { + 'from': request.fromCurrency.title, + 'to': request.toCurrency.title, + if (networkFrom != null) 'network_from': networkFrom, + if (networkTo != null) 'network_to': networkTo, + 'amount': isFixedRateMode ? request.toAmount.toString() : request.fromAmount.toString(), + 'affiliate_id': _affiliateId + }; + + final responseInfoJSON = await _getInfo(params, isFixedRateMode); + final rateId = responseInfoJSON['rate_id'] as String; + + final withdrawalAddress = _normalizeBchAddress(request.toAddress); + final returnAddress = _normalizeBchAddress(request.refundAddress); + + final tradeParams = { + 'coin_from': request.fromCurrency.title, + 'coin_to': request.toCurrency.title, + if (!isFixedRateMode) 'deposit_amount': request.fromAmount.toString(), + 'withdrawal': withdrawalAddress, + if (isFixedRateMode) 'withdrawal_amount': request.toAmount.toString(), + 'withdrawal_extra_id': '', + 'return': returnAddress, + 'rate_id': rateId, + if (networkFrom != null) 'network_from': networkFrom, + if (networkTo != null) 'network_to': networkTo, + 'affiliate_id': _affiliateId + }; + + final headers = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Authorization': apiKey + }; + + final uri = Uri.https(_baseUrl, + isFixedRateMode ? _createTransactionRevertPath : _createTransactionPath, tradeParams); + final response = await http.post(uri, headers: headers); + + if (response.statusCode != 200) { + throw Exception('LetsExchange create trade failed: ${response.body}'); + } + final responseJSON = json.decode(response.body) as Map; + final id = responseJSON['transaction_id'] as String; + final from = responseJSON['coin_from'] as String; + final to = responseJSON['coin_to'] as String; + final payoutAddress = responseJSON['withdrawal'] as String; + final depositAddress = responseJSON['deposit'] as String; + final refundAddress = responseJSON['return'] as String; + final depositAmount = responseJSON['deposit_amount'] as String; + final receiveAmount = responseJSON['withdrawal_amount'] as String; + final status = responseJSON['status'] as String; + final createdAtString = responseJSON['created_at'] as String; + final expiredAtTimestamp = responseJSON['expired_at'] as int; + + final createdAt = DateTime.parse(createdAtString); + final expiredAt = DateTime.fromMillisecondsSinceEpoch(expiredAtTimestamp * 1000); + + CryptoCurrency fromCurrency; + if (request.fromCurrency.tag != null && request.fromCurrency.title == from) { + fromCurrency = request.fromCurrency; + } else { + fromCurrency = CryptoCurrency.fromString(from); + } + + CryptoCurrency toCurrency; + if (request.toCurrency.tag != null && request.toCurrency.title == to) { + toCurrency = request.toCurrency; + } else { + toCurrency = CryptoCurrency.fromString(to); + } + + return Trade( + id: id, + from: fromCurrency, + to: toCurrency, + provider: description, + inputAddress: depositAddress, + payoutAddress: payoutAddress, + refundAddress: refundAddress, + amount: depositAmount, + receiveAmount: receiveAmount, + state: TradeState.deserialize(raw: status), + createdAt: createdAt, + expiredAt: expiredAt, + ); + } catch (e) { + log(e.toString()); + throw TradeNotCreatedException(description); + } + } + + @override + Future findTradeById({required String id}) async { + final headers = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Authorization': apiKey + }; + + final url = Uri.https(_baseUrl, '$_getTransactionPath/$id'); + final response = await http.get(url, headers: headers); + + if (response.statusCode != 200) { + throw Exception('LetsExchange fetch trade failed: ${response.body}'); + } + final responseJSON = json.decode(response.body) as Map; + final from = responseJSON['coin_from'] as String; + final to = responseJSON['coin_to'] as String; + final payoutAddress = responseJSON['withdrawal'] as String; + final depositAddress = responseJSON['deposit'] as String; + final refundAddress = responseJSON['return'] as String; + final depositAmount = responseJSON['deposit_amount'] as String; + final receiveAmount = responseJSON['withdrawal_amount'] as String; + final status = responseJSON['status'] as String; + final createdAtString = responseJSON['created_at'] as String; + final expiredAtTimestamp = responseJSON['expired_at'] as int; + + final createdAt = DateTime.parse(createdAtString); + final expiredAt = DateTime.fromMillisecondsSinceEpoch(expiredAtTimestamp * 1000); + + return Trade( + id: id, + from: CryptoCurrency.fromString(from), + to: CryptoCurrency.fromString(to), + provider: description, + inputAddress: depositAddress, + payoutAddress: payoutAddress, + refundAddress: refundAddress, + amount: depositAmount, + receiveAmount: receiveAmount, + state: TradeState.deserialize(raw: status), + createdAt: createdAt, + expiredAt: expiredAt, + isRefund: status == 'refund', + ); + } + + Future> _getInfo(Map params, bool isFixedRateMode) async { + final headers = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Authorization': apiKey + }; + + try { + final uri = Uri.https(_baseUrl, isFixedRateMode ? _infoRevertPath : _infoPath, params); + final response = await http.post(uri, headers: headers); + if (response.statusCode != 200) { + throw Exception('LetsExchange fetch info failed: ${response.body}'); + } + return json.decode(response.body) as Map; + } catch (e) { + throw Exception('LetsExchange failed to fetch info ${e.toString()}'); + } + } + + String? _getNetworkType(CryptoCurrency currency) { + if (currency.tag != null && currency.tag!.isNotEmpty) { + switch (currency.tag!) { + case 'TRX': + return 'TRC20'; + case 'ETH': + return 'ERC20'; + case 'BSC': + return 'BEP20'; + default: + return currency.tag!; + } + } + return currency.title; + } + + String _normalizeBchAddress(String address) => + address.startsWith('bitcoincash:') ? address.substring(12) : address; +} diff --git a/lib/exchange/provider/simpleswap_exchange_provider.dart b/lib/exchange/provider/simpleswap_exchange_provider.dart index 885878684..be52b73fe 100644 --- a/lib/exchange/provider/simpleswap_exchange_provider.dart +++ b/lib/exchange/provider/simpleswap_exchange_provider.dart @@ -236,7 +236,7 @@ class SimpleSwapExchangeProvider extends ExchangeProvider { case CryptoCurrency.usdcsol: return 'usdcspl'; case CryptoCurrency.matic: - return 'maticerc20'; + return 'pol'; case CryptoCurrency.maticpoly: return 'matic'; default: diff --git a/lib/exchange/provider/stealth_ex_exchange_provider.dart b/lib/exchange/provider/stealth_ex_exchange_provider.dart new file mode 100644 index 000000000..601735595 --- /dev/null +++ b/lib/exchange/provider/stealth_ex_exchange_provider.dart @@ -0,0 +1,299 @@ +import 'dart:convert'; +import 'dart:developer'; + +import 'package:cake_wallet/.secrets.g.dart' as secrets; +import 'package:cake_wallet/exchange/provider/exchange_provider.dart'; +import 'package:cake_wallet/exchange/exchange_provider_description.dart'; +import 'package:cake_wallet/exchange/limits.dart'; +import 'package:cake_wallet/exchange/trade.dart'; +import 'package:cake_wallet/exchange/trade_not_created_exception.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:http/http.dart' as http; + +class StealthExExchangeProvider extends ExchangeProvider { + StealthExExchangeProvider() : super(pairList: supportedPairs(_notSupported)); + + static const List _notSupported = []; + + static final apiKey = secrets.stealthExBearerToken; + static final _additionalFeePercent = double.tryParse(secrets.stealthExAdditionalFeePercent); + static const _baseUrl = 'https://api.stealthex.io'; + static const _rangePath = '/v4/rates/range'; + static const _amountPath = '/v4/rates/estimated-amount'; + static const _exchangesPath = '/v4/exchanges'; + + @override + String get title => 'StealthEX'; + + @override + bool get isAvailable => true; + + @override + bool get isEnabled => true; + + @override + bool get supportsFixedRate => true; + + @override + ExchangeProviderDescription get description => ExchangeProviderDescription.stealthEx; + + @override + Future checkIsAvailable() async => true; + + @override + Future fetchLimits( + {required CryptoCurrency from, + required CryptoCurrency to, + required bool isFixedRateMode}) async { + final curFrom = isFixedRateMode ? to : from; + final curTo = isFixedRateMode ? from : to; + + final headers = {'Authorization': apiKey, 'Content-Type': 'application/json'}; + final body = { + 'route': { + 'from': {'symbol': _getName(curFrom), 'network': _getNetwork(curFrom)}, + 'to': {'symbol': _getName(curTo), 'network': _getNetwork(curTo)} + }, + 'estimation': isFixedRateMode ? 'reversed' : 'direct', + 'rate': isFixedRateMode ? 'fixed' : 'floating', + 'additional_fee_percent': _additionalFeePercent, + }; + + try { + final response = await http.post(Uri.parse(_baseUrl + _rangePath), + headers: headers, body: json.encode(body)); + if (response.statusCode != 200) { + throw Exception('StealthEx fetch limits failed: ${response.body}'); + } + final responseJSON = json.decode(response.body) as Map; + final min = toDouble(responseJSON['min_amount']); + final max = responseJSON['max_amount'] as double?; + return Limits(min: min, max: max); + } catch (e) { + log(e.toString()); + throw Exception('StealthEx failed to fetch limits'); + } + } + + @override + Future fetchRate( + {required CryptoCurrency from, + required CryptoCurrency to, + required double amount, + required bool isFixedRateMode, + required bool isReceiveAmount}) async { + final response = await getEstimatedExchangeAmount( + from: from, to: to, amount: amount, isFixedRateMode: isFixedRateMode); + final estimatedAmount = response['estimated_amount'] as double? ?? 0.0; + return estimatedAmount > 0.0 + ? isFixedRateMode + ? amount / estimatedAmount + : estimatedAmount / amount + : 0.0; + } + + @override + Future createTrade( + {required TradeRequest request, + required bool isFixedRateMode, + required bool isSendAll}) async { + String? rateId; + String? validUntil; + + try { + if (isFixedRateMode) { + final response = await getEstimatedExchangeAmount( + from: request.fromCurrency, + to: request.toCurrency, + amount: double.parse(request.toAmount), + isFixedRateMode: isFixedRateMode); + rateId = response['rate_id'] as String?; + validUntil = response['valid_until'] as String?; + if (rateId == null) throw TradeNotCreatedException(description); + } + + final headers = {'Authorization': apiKey, 'Content-Type': 'application/json'}; + final body = { + 'route': { + 'from': { + 'symbol': _getName(request.fromCurrency), + 'network': _getNetwork(request.fromCurrency) + }, + 'to': {'symbol': _getName(request.toCurrency), 'network': _getNetwork(request.toCurrency)} + }, + 'estimation': isFixedRateMode ? 'reversed' : 'direct', + 'rate': isFixedRateMode ? 'fixed' : 'floating', + if (isFixedRateMode) 'rate_id': rateId, + 'amount': + isFixedRateMode ? double.parse(request.toAmount) : double.parse(request.fromAmount), + 'address': request.toAddress, + 'refund_address': request.refundAddress, + 'additional_fee_percent': _additionalFeePercent, + }; + + final response = await http.post(Uri.parse(_baseUrl + _exchangesPath), + headers: headers, body: json.encode(body)); + + if (response.statusCode != 201) { + throw Exception('StealthEx create trade failed: ${response.body}'); + } + final responseJSON = json.decode(response.body) as Map; + final deposit = responseJSON['deposit'] as Map; + final withdrawal = responseJSON['withdrawal'] as Map; + + final id = responseJSON['id'] as String; + final from = deposit['symbol'] as String; + final to = withdrawal['symbol'] as String; + final payoutAddress = withdrawal['address'] as String; + final depositAddress = deposit['address'] as String; + final refundAddress = responseJSON['refund_address'] as String; + final depositAmount = toDouble(deposit['amount']); + final receiveAmount = toDouble(withdrawal['amount']); + final status = responseJSON['status'] as String; + final createdAtString = responseJSON['created_at'] as String; + + final createdAt = DateTime.parse(createdAtString); + final expiredAt = validUntil != null + ? DateTime.parse(validUntil) + : DateTime.now().add(Duration(minutes: 5)); + + + CryptoCurrency fromCurrency; + if (request.fromCurrency.tag != null && request.fromCurrency.title.toLowerCase() == from) { + fromCurrency = request.fromCurrency; + } else { + fromCurrency = CryptoCurrency.fromString(from); + } + + CryptoCurrency toCurrency; + if (request.toCurrency.tag != null && request.toCurrency.title.toLowerCase() == to) { + toCurrency = request.toCurrency; + } else { + toCurrency = CryptoCurrency.fromString(to); + } + + return Trade( + id: id, + from: fromCurrency, + to: toCurrency, + provider: description, + inputAddress: depositAddress, + payoutAddress: payoutAddress, + refundAddress: refundAddress, + amount: depositAmount.toString(), + receiveAmount: receiveAmount.toString(), + state: TradeState.deserialize(raw: status), + createdAt: createdAt, + expiredAt: expiredAt, + ); + } catch (e) { + log(e.toString()); + throw TradeNotCreatedException(description); + } + } + + @override + Future findTradeById({required String id}) async { + final headers = {'Authorization': apiKey, 'Content-Type': 'application/json'}; + + final uri = Uri.parse('$_baseUrl$_exchangesPath/$id'); + final response = await http.get(uri, headers: headers); + + if (response.statusCode != 200) { + throw Exception('StealthEx fetch trade failed: ${response.body}'); + } + final responseJSON = json.decode(response.body) as Map; + final deposit = responseJSON['deposit'] as Map; + final withdrawal = responseJSON['withdrawal'] as Map; + + final respId = responseJSON['id'] as String; + final from = deposit['symbol'] as String; + final to = withdrawal['symbol'] as String; + final payoutAddress = withdrawal['address'] as String; + final depositAddress = deposit['address'] as String; + final refundAddress = responseJSON['refund_address'] as String; + final depositAmount = toDouble(deposit['amount']); + final receiveAmount = toDouble(withdrawal['amount']); + final status = responseJSON['status'] as String; + final createdAtString = responseJSON['created_at'] as String; + final createdAt = DateTime.parse(createdAtString); + + return Trade( + id: respId, + from: CryptoCurrency.fromString(from), + to: CryptoCurrency.fromString(to), + provider: description, + inputAddress: depositAddress, + payoutAddress: payoutAddress, + refundAddress: refundAddress, + amount: depositAmount.toString(), + receiveAmount: receiveAmount.toString(), + state: TradeState.deserialize(raw: status), + createdAt: createdAt, + isRefund: status == 'refunded', + ); + } + + Future> getEstimatedExchangeAmount( + {required CryptoCurrency from, + required CryptoCurrency to, + required double amount, + required bool isFixedRateMode}) async { + final headers = {'Authorization': apiKey, 'Content-Type': 'application/json'}; + + final body = { + 'route': { + 'from': {'symbol': _getName(from), 'network': _getNetwork(from)}, + 'to': {'symbol': _getName(to), 'network': _getNetwork(to)} + }, + 'estimation': isFixedRateMode ? 'reversed' : 'direct', + 'rate': isFixedRateMode ? 'fixed' : 'floating', + 'amount': amount, + 'additional_fee_percent': _additionalFeePercent, + }; + + try { + final response = await http.post(Uri.parse(_baseUrl + _amountPath), + headers: headers, body: json.encode(body)); + if (response.statusCode != 200) return {}; + final responseJSON = json.decode(response.body) as Map; + final rate = responseJSON['rate'] as Map?; + return { + 'estimated_amount': responseJSON['estimated_amount'] as double?, + if (rate != null) 'valid_until': rate['valid_until'] as String?, + if (rate != null) 'rate_id': rate['id'] as String? + }; + } catch (e) { + log(e.toString()); + return {}; + } + } + + double toDouble(dynamic value) { + if (value is int) { + return value.toDouble(); + } else if (value is double) { + return value; + } else { + return 0.0; + } + } + + String _getName(CryptoCurrency currency) { + if (currency == CryptoCurrency.usdcEPoly) return 'usdce'; + return currency.title.toLowerCase(); + } + + String _getNetwork(CryptoCurrency currency) { + if (currency.tag == null) return 'mainnet'; + + if (currency == CryptoCurrency.maticpoly) return 'mainnet'; + + if (currency.tag == 'POLY') return 'matic'; + + return currency.tag!.toLowerCase(); + } +} diff --git a/lib/exchange/trade_state.dart b/lib/exchange/trade_state.dart index 0a196835e..6d2472a11 100644 --- a/lib/exchange/trade_state.dart +++ b/lib/exchange/trade_state.dart @@ -40,7 +40,6 @@ class TradeState extends EnumerableItem with Serializable { static const exchanging = TradeState(raw: 'exchanging', title: 'Exchanging'); static const sending = TradeState(raw: 'sending', title: 'Sending'); static const success = TradeState(raw: 'success', title: 'Success'); - static TradeState deserialize({required String raw}) { switch (raw) { @@ -107,6 +106,7 @@ class TradeState extends EnumerableItem with Serializable { case 'waitingAuthorization': return waitingAuthorization; case 'failed': + case 'error': return failed; case 'completed': return completed; @@ -119,12 +119,14 @@ class TradeState extends EnumerableItem with Serializable { case 'refunded': return refunded; case 'confirmation': + case 'verifying': return confirmation; case 'confirmed': return confirmed; case 'exchanging': return exchanging; case 'sending': + case 'sending_confirmation': return sending; case 'success': case 'done': diff --git a/lib/main.dart b/lib/main.dart index aeb76b3a8..29b216b22 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:io'; import 'package:cake_wallet/anonpay/anonpay_invoice_info.dart'; import 'package:cake_wallet/app_scroll_behavior.dart'; import 'package:cake_wallet/buy/order.dart'; @@ -30,6 +31,7 @@ import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cw_core/address_info.dart'; import 'package:cw_core/cake_hive.dart'; import 'package:cw_core/hive_type_ids.dart'; +import 'package:cw_core/mweb_utxo.dart'; import 'package:cw_core/node.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/wallet_info.dart'; @@ -42,17 +44,21 @@ import 'package:hive/hive.dart'; import 'package:cw_core/root_dir.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:cw_core/window_size.dart'; +import 'package:logging/logging.dart'; final navigatorKey = GlobalKey(); final rootKey = GlobalKey(); final RouteObserver> routeObserver = RouteObserver>(); -Future main() async { +Future main({Key? topLevelKey}) async { + await runAppWithZone(topLevelKey: topLevelKey); +} +Future runAppWithZone({Key? topLevelKey}) async { bool isAppRunning = false; + await runZonedGuarded(() async { WidgetsFlutterBinding.ensureInitialized(); - FlutterError.onError = ExceptionHandler.onError; /// A callback that is invoked when an unhandled error occurs in the root @@ -62,42 +68,25 @@ Future main() async { return true; }; + await initializeAppAtRoot(); - await setDefaultMinimumWindowSize(); + if (kDebugMode) { + final appDocDir = await getAppDir(); - await CakeHive.close(); - - await initializeAppConfigs(); - - runApp(App()); + final ledgerFile = File('${appDocDir.path}/ledger_log.txt'); + if (!ledgerFile.existsSync()) ledgerFile.createSync(); + Logger.root.onRecord.listen((event) async { + final content = ledgerFile.readAsStringSync(); + ledgerFile.writeAsStringSync("$content\n${event.message}"); + }); + } + runApp(App(key: topLevelKey)); isAppRunning = true; }, (error, stackTrace) async { if (!isAppRunning) { runApp( - MaterialApp( - debugShowCheckedModeBanner: false, - scrollBehavior: AppScrollBehavior(), - home: Scaffold( - body: SingleChildScrollView( - child: Container( - margin: EdgeInsets.only(top: 50, left: 20, right: 20, bottom: 20), - child: Column( - children: [ - Text( - 'Error:\n${error.toString()}', - style: TextStyle(fontSize: 22), - ), - Text( - 'Stack trace:\n${stackTrace.toString()}', - style: TextStyle(fontSize: 16), - ), - ], - ), - ), - ), - ), - ), + TopLevelErrorWidget(error: error, stackTrace: stackTrace), ); } @@ -105,6 +94,12 @@ Future main() async { }); } +Future initializeAppAtRoot({bool reInitializing = false}) async { + if (!reInitializing) await setDefaultMinimumWindowSize(); + await CakeHive.close(); + await initializeAppConfigs(); +} + Future initializeAppConfigs() async { setRootDirFromEnv(); final appDir = await getAppDir(); @@ -170,6 +165,10 @@ Future initializeAppConfigs() async { CakeHive.registerAdapter(AnonpayInvoiceInfoAdapter()); } + if (!CakeHive.isAdapterRegistered(MwebUtxo.typeId)) { + CakeHive.registerAdapter(MwebUtxoAdapter()); + } + final secureStorage = secureStorageShared; final transactionDescriptionsBoxKey = await getEncryptionKey(secureStorage: secureStorage, forKey: TransactionDescription.boxKey); @@ -205,7 +204,7 @@ Future initializeAppConfigs() async { transactionDescriptions: transactionDescriptions, secureStorage: secureStorage, anonpayInvoiceInfo: anonpayInvoiceInfo, - initialMigrationVersion: 39, + initialMigrationVersion: 42, ); } @@ -251,10 +250,12 @@ Future initialSetup( secureStorage: secureStorage, ); await bootstrap(navigatorKey); - monero?.onStartup(); } class App extends StatefulWidget { + App({this.key}); + + final Key? key; @override AppState createState() => AppState(); } @@ -283,7 +284,7 @@ class AppState extends State with SingleTickerProviderStateMixin { statusBarIconBrightness: statusBarIconBrightness)); return Root( - key: rootKey, + key: widget.key ?? rootKey, appStore: appStore, authenticationStore: authenticationStore, navigatorKey: navigatorKey, @@ -338,3 +339,41 @@ class _HomeState extends State<_Home> { return const SizedBox.shrink(); } } + +class TopLevelErrorWidget extends StatelessWidget { + const TopLevelErrorWidget({ + required this.error, + required this.stackTrace, + super.key, + }); + + final Object error; + final StackTrace stackTrace; + + @override + Widget build(BuildContext context) { + return MaterialApp( + debugShowCheckedModeBanner: false, + scrollBehavior: AppScrollBehavior(), + home: Scaffold( + body: SingleChildScrollView( + child: Container( + margin: EdgeInsets.only(top: 50, left: 20, right: 20, bottom: 20), + child: Column( + children: [ + Text( + 'Error:\n${error.toString()}', + style: TextStyle(fontSize: 22), + ), + Text( + 'Stack trace:\n${stackTrace.toString()}', + style: TextStyle(fontSize: 16), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/monero/cw_monero.dart b/lib/monero/cw_monero.dart index 1f1888b44..b1cf49482 100644 --- a/lib/monero/cw_monero.dart +++ b/lib/monero/cw_monero.dart @@ -61,7 +61,13 @@ class CWMoneroSubaddressList extends MoneroSubaddressList { ObservableList get subaddresses { final moneroWallet = _wallet as MoneroWallet; final subAddresses = moneroWallet.walletAddresses.subaddressList.subaddresses - .map((sub) => Subaddress(id: sub.id, address: sub.address, label: sub.label)) + .map((sub) => Subaddress( + id: sub.id, + address: sub.address, + label: sub.label, + received: sub.balance??"unknown", + txCount: sub.txCount??0, + )) .toList(); return ObservableList.of(subAddresses); } @@ -83,7 +89,12 @@ class CWMoneroSubaddressList extends MoneroSubaddressList { final moneroWallet = wallet as MoneroWallet; return moneroWallet.walletAddresses.subaddressList .getAll() - .map((sub) => Subaddress(id: sub.id, label: sub.label, address: sub.address)) + .map((sub) => Subaddress( + id: sub.id, + label: sub.label, + address: sub.address, + txCount: sub.txCount??0, + received: sub.balance??'unknown')) .toList(); } @@ -91,7 +102,7 @@ class CWMoneroSubaddressList extends MoneroSubaddressList { Future addSubaddress(Object wallet, {required int accountIndex, required String label}) async { final moneroWallet = wallet as MoneroWallet; - await moneroWallet.walletAddresses.subaddressList + return await moneroWallet.walletAddresses.subaddressList .addSubaddress(accountIndex: accountIndex, label: label); } diff --git a/lib/nano/cw_nano.dart b/lib/nano/cw_nano.dart index 8cf640d8b..9e47edc04 100644 --- a/lib/nano/cw_nano.dart +++ b/lib/nano/cw_nano.dart @@ -91,12 +91,19 @@ class CWNano extends Nano { @override WalletCredentials createNanoNewWalletCredentials({ required String name, + WalletInfo? walletInfo, String? password, + String? mnemonic, + String? parentAddress, + String? passphrase, }) => NanoNewWalletCredentials( name: name, password: password, - derivationType: DerivationType.nano, + mnemonic: mnemonic, + parentAddress: parentAddress, + walletInfo: walletInfo, + passphrase: passphrase, ); @override @@ -105,6 +112,7 @@ class CWNano extends Nano { required String password, required String mnemonic, required DerivationType derivationType, + String? passphrase, }) { if (mnemonic.split(" ").length == 12 && derivationType != DerivationType.bip39) { throw Exception("Invalid mnemonic for derivation type!"); @@ -115,6 +123,7 @@ class CWNano extends Nano { password: password, mnemonic: mnemonic, derivationType: derivationType, + passphrase: passphrase, ); } diff --git a/lib/polygon/cw_polygon.dart b/lib/polygon/cw_polygon.dart index 5bb87ff5b..74b4026eb 100644 --- a/lib/polygon/cw_polygon.dart +++ b/lib/polygon/cw_polygon.dart @@ -10,18 +10,34 @@ class CWPolygon extends Polygon { @override WalletCredentials createPolygonNewWalletCredentials({ required String name, + String? mnemonic, + String? parentAddress, WalletInfo? walletInfo, - String? password + String? password, + String? passphrase, }) => - EVMChainNewWalletCredentials(name: name, walletInfo: walletInfo, password: password); + EVMChainNewWalletCredentials( + name: name, + walletInfo: walletInfo, + password: password, + mnemonic: mnemonic, + parentAddress: parentAddress, + passphrase: passphrase, + ); @override WalletCredentials createPolygonRestoreWalletFromSeedCredentials({ required String name, required String mnemonic, required String password, + String? passphrase, }) => - EVMChainRestoreWalletFromSeedCredentials(name: name, password: password, mnemonic: mnemonic); + EVMChainRestoreWalletFromSeedCredentials( + name: name, + password: password, + mnemonic: mnemonic, + passphrase: passphrase, + ); @override WalletCredentials createPolygonRestoreWalletFromPrivateKey({ @@ -77,21 +93,21 @@ class CWPolygon extends Polygon { int? feeRate, }) => EVMChainTransactionCredentials( - 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 EVMChainTransactionPriority, - currency: currency, - feeRate: feeRate, - ); + 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 EVMChainTransactionPriority, + currency: currency, + feeRate: feeRate, + ); Object createPolygonTransactionCredentialsRaw( List outputs, { @@ -149,7 +165,8 @@ class CWPolygon extends Polygon { @override CryptoCurrency assetOfTransaction(WalletBase wallet, TransactionInfo transaction) { transaction as EVMChainTransactionInfo; - if (transaction.tokenSymbol == CryptoCurrency.maticpoly.title) { + if (transaction.tokenSymbol == CryptoCurrency.maticpoly.title || + transaction.tokenSymbol == "MATIC") { return CryptoCurrency.maticpoly; } @@ -173,20 +190,21 @@ class CWPolygon extends Polygon { String getTokenAddress(CryptoCurrency asset) => (asset as Erc20Token).contractAddress; @override - void setLedger(WalletBase wallet, Ledger ledger, LedgerDevice device) { - ((wallet as EVMChainWallet).evmChainPrivateKey as EvmLedgerCredentials).setLedger( - ledger, - device.connectionType == ConnectionType.usb ? device : null, - wallet.walletInfo.derivationInfo?.derivationPath); + void setLedgerConnection( + WalletBase wallet, ledger.LedgerConnection connection) { + ((wallet as EVMChainWallet).evmChainPrivateKey as EvmLedgerCredentials) + .setLedgerConnection( + connection, wallet.walletInfo.derivationInfo?.derivationPath); } @override Future> getHardwareWalletAccounts(LedgerViewModel ledgerVM, {int index = 0, int limit = 5}) async { - final hardwareWalletService = EVMChainHardwareWalletService(ledgerVM.ledger, ledgerVM.device); + final hardwareWalletService = EVMChainHardwareWalletService(ledgerVM.connection); try { return await hardwareWalletService.getAvailableAccounts(index: index, limit: limit); - } on LedgerException catch (err) { + } catch (err) { + print(err); throw err; } } diff --git a/lib/reactions/bip39_wallet_utils.dart b/lib/reactions/bip39_wallet_utils.dart new file mode 100644 index 000000000..a31fec91f --- /dev/null +++ b/lib/reactions/bip39_wallet_utils.dart @@ -0,0 +1,21 @@ +import 'package:cw_core/wallet_type.dart'; + +bool isBIP39Wallet(WalletType walletType) { + switch (walletType) { + case WalletType.ethereum: + case WalletType.polygon: + case WalletType.solana: + case WalletType.tron: + case WalletType.bitcoin: + case WalletType.litecoin: + case WalletType.bitcoinCash: + case WalletType.nano: + case WalletType.banano: + return true; + case WalletType.monero: + case WalletType.wownero: + case WalletType.haven: + case WalletType.none: + return false; + } +} diff --git a/lib/reactions/check_connection.dart b/lib/reactions/check_connection.dart index 3252797dd..570b96864 100644 --- a/lib/reactions/check_connection.dart +++ b/lib/reactions/check_connection.dart @@ -11,6 +11,8 @@ Timer? _checkConnectionTimer; void startCheckConnectionReaction(WalletBase wallet, SettingsStore settingsStore, {int timeInterval = 5}) { _checkConnectionTimer?.cancel(); + // TODO: check the validity of this code, and if it's working fine, then no need for + // having the connect function in electrum.dart when the syncstatus is lost or failed and add the not connected state _checkConnectionTimer = Timer.periodic(Duration(seconds: timeInterval), (_) async { if (wallet.type == WalletType.bitcoin && wallet.syncStatus is SyncingSyncStatus) { return; @@ -24,7 +26,9 @@ void startCheckConnectionReaction(WalletBase wallet, SettingsStore settingsStore return; } - if (wallet.syncStatus is LostConnectionSyncStatus || wallet.syncStatus is FailedSyncStatus) { + if (wallet.type != WalletType.bitcoin && + (wallet.syncStatus is LostConnectionSyncStatus || + wallet.syncStatus is FailedSyncStatus)) { final alive = await settingsStore.getCurrentNode(wallet.type).requestNode(); if (alive) { diff --git a/lib/reactions/on_current_wallet_change.dart b/lib/reactions/on_current_wallet_change.dart index 6f1ba1d8c..6630f1dfc 100644 --- a/lib/reactions/on_current_wallet_change.dart +++ b/lib/reactions/on_current_wallet_change.dart @@ -152,11 +152,6 @@ void _setAutoGenerateSubaddressStatus( WalletBase, TransactionInfo> wallet, SettingsStore settingsStore, ) async { - final walletHasAddresses = await wallet.walletAddresses.addressesMap.length > 1; - if (settingsStore.autoGenerateSubaddressStatus == AutoGenerateSubaddressStatus.initialized && - walletHasAddresses) { - settingsStore.autoGenerateSubaddressStatus = AutoGenerateSubaddressStatus.disabled; - } wallet.isEnabledAutoGenerateSubaddress = settingsStore.autoGenerateSubaddressStatus == AutoGenerateSubaddressStatus.enabled || settingsStore.autoGenerateSubaddressStatus == AutoGenerateSubaddressStatus.initialized; diff --git a/lib/router.dart b/lib/router.dart index 25af39043..3b4c38546 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -1,11 +1,14 @@ import 'package:cake_wallet/anonpay/anonpay_info_base.dart'; import 'package:cake_wallet/anonpay/anonpay_invoice_info.dart'; +import 'package:cake_wallet/core/new_wallet_arguments.dart'; import 'package:cake_wallet/buy/order.dart'; +import 'package:cake_wallet/core/new_wallet_type_arguments.dart'; import 'package:cake_wallet/core/totp_request_details.dart'; import 'package:cake_wallet/core/wallet_connect/web3wallet_service.dart'; import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/entities/contact_record.dart'; import 'package:cake_wallet/entities/qr_view_data.dart'; +import 'package:cake_wallet/entities/wallet_edit_page_arguments.dart'; import 'package:cake_wallet/entities/wallet_nft_response.dart'; import 'package:cake_wallet/exchange/trade.dart'; import 'package:cake_wallet/generated/i18n.dart'; @@ -17,6 +20,8 @@ import 'package:cake_wallet/src/screens/backup/edit_backup_password_page.dart'; import 'package:cake_wallet/src/screens/buy/buy_options_page.dart'; import 'package:cake_wallet/src/screens/buy/buy_webview_page.dart'; import 'package:cake_wallet/src/screens/buy/webview_page.dart'; +import 'package:cake_wallet/src/screens/cake_pay/auth/cake_pay_account_page.dart'; +import 'package:cake_wallet/src/screens/cake_pay/cake_pay.dart'; import 'package:cake_wallet/src/screens/connect_device/connect_device_page.dart'; import 'package:cake_wallet/src/screens/connect_device/select_hardware_wallet_account_page.dart'; import 'package:cake_wallet/src/screens/contact/contact_list_page.dart'; @@ -27,8 +32,8 @@ import 'package:cake_wallet/src/screens/dashboard/edit_token_page.dart'; import 'package:cake_wallet/src/screens/dashboard/home_settings_page.dart'; import 'package:cake_wallet/src/screens/dashboard/pages/address_page.dart'; import 'package:cake_wallet/src/screens/dashboard/pages/nft_details_page.dart'; -import 'package:cake_wallet/src/screens/dashboard/sign_page.dart'; import 'package:cake_wallet/src/screens/dashboard/pages/transactions_page.dart'; +import 'package:cake_wallet/src/screens/dashboard/sign_page.dart'; import 'package:cake_wallet/src/screens/disclaimer/disclaimer_page.dart'; import 'package:cake_wallet/src/screens/exchange/exchange_page.dart'; import 'package:cake_wallet/src/screens/exchange/exchange_template_page.dart'; @@ -38,15 +43,16 @@ import 'package:cake_wallet/src/screens/faq/faq_page.dart'; import 'package:cake_wallet/src/screens/monero_accounts/monero_account_edit_or_create_page.dart'; import 'package:cake_wallet/src/screens/nano/nano_change_rep_page.dart'; import 'package:cake_wallet/src/screens/nano_accounts/nano_account_edit_or_create_page.dart'; +import 'package:cake_wallet/src/screens/new_wallet/wallet_group_display_page.dart'; import 'package:cake_wallet/src/screens/new_wallet/advanced_privacy_settings_page.dart'; 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/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/receive/address_list_page.dart'; -import 'package:cake_wallet/src/screens/restore/sweeping_wallet_page.dart'; import 'package:cake_wallet/src/screens/order_details/order_details_page.dart'; import 'package:cake_wallet/src/screens/pin_code/pin_code_widget.dart'; +import 'package:cake_wallet/src/screens/receive/address_list_page.dart'; import 'package:cake_wallet/src/screens/receive/anonpay_invoice_page.dart'; import 'package:cake_wallet/src/screens/receive/anonpay_receive_page.dart'; import 'package:cake_wallet/src/screens/receive/fullscreen_qr_page.dart'; @@ -66,14 +72,13 @@ import 'package:cake_wallet/src/screens/settings/desktop_settings/desktop_settin import 'package:cake_wallet/src/screens/settings/display_settings_page.dart'; import 'package:cake_wallet/src/screens/settings/domain_lookups_page.dart'; import 'package:cake_wallet/src/screens/settings/manage_nodes_page.dart'; +import 'package:cake_wallet/src/screens/settings/mweb_settings.dart'; import 'package:cake_wallet/src/screens/settings/other_settings_page.dart'; import 'package:cake_wallet/src/screens/settings/privacy_page.dart'; import 'package:cake_wallet/src/screens/settings/security_backup_page.dart'; -import 'package:cake_wallet/src/screens/cake_pay/auth/cake_pay_account_page.dart'; import 'package:cake_wallet/src/screens/settings/silent_payments_settings.dart'; import 'package:cake_wallet/src/screens/settings/tor_page.dart'; import 'package:cake_wallet/src/screens/settings/trocador_providers_page.dart'; -import 'package:cake_wallet/src/screens/settings/tor_page.dart'; import 'package:cake_wallet/src/screens/setup_2fa/modify_2fa_page.dart'; import 'package:cake_wallet/src/screens/setup_2fa/setup_2fa.dart'; import 'package:cake_wallet/src/screens/setup_2fa/setup_2fa_enter_code_page.dart'; @@ -85,19 +90,18 @@ import 'package:cake_wallet/src/screens/support/support_page.dart'; import 'package:cake_wallet/src/screens/support_chat/support_chat_page.dart'; import 'package:cake_wallet/src/screens/support_other_links/support_other_links_page.dart'; import 'package:cake_wallet/src/screens/trade_details/trade_details_page.dart'; -import 'package:cake_wallet/src/screens/transaction_details/transaction_details_page.dart'; import 'package:cake_wallet/src/screens/transaction_details/rbf_details_page.dart'; +import 'package:cake_wallet/src/screens/transaction_details/transaction_details_page.dart'; import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_details_page.dart'; import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_list_page.dart'; import 'package:cake_wallet/src/screens/wallet/wallet_edit_page.dart'; import 'package:cake_wallet/src/screens/wallet_connect/wc_connections_listing_view.dart'; import 'package:cake_wallet/src/screens/wallet_keys/wallet_keys_page.dart'; import 'package:cake_wallet/src/screens/wallet_list/wallet_list_page.dart'; +import 'package:cake_wallet/src/screens/wallet_unlock/wallet_unlock_arguments.dart'; import 'package:cake_wallet/src/screens/wallet_unlock/wallet_unlock_page.dart'; import 'package:cake_wallet/src/screens/welcome/create_welcome_page.dart'; import 'package:cake_wallet/store/settings_store.dart'; -import 'package:cake_wallet/src/screens/wallet_unlock/wallet_unlock_arguments.dart'; -import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/utils/payment_request.dart'; import 'package:cake_wallet/view_model/advanced_privacy_settings_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; @@ -106,7 +110,8 @@ import 'package:cake_wallet/view_model/dashboard/sign_view_model.dart'; import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart'; import 'package:cake_wallet/view_model/monero_account_list/account_list_item.dart'; import 'package:cake_wallet/view_model/node_list/node_create_or_edit_view_model.dart'; -import 'package:cake_wallet/view_model/seed_type_view_model.dart'; +import 'package:cake_wallet/view_model/wallet_groups_display_view_model.dart'; +import 'package:cake_wallet/view_model/seed_settings_view_model.dart'; import 'package:cake_wallet/view_model/wallet_hardware_restore_view_model.dart'; import 'package:cake_wallet/view_model/wallet_new_vm.dart'; import 'package:cake_wallet/wallet_type_utils.dart'; @@ -115,12 +120,13 @@ import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/nano_account.dart'; import 'package:cw_core/node.dart'; import 'package:cw_core/transaction_info.dart'; +import 'package:cw_core/unspent_coin_type.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:cake_wallet/src/screens/cake_pay/cake_pay.dart'; + import 'src/screens/dashboard/pages/nft_import_page.dart'; late RouteSettings currentRouteSettings; @@ -135,7 +141,12 @@ Route createRoute(RouteSettings settings) { case Routes.newWalletFromWelcome: if (SettingsStoreBase.walletPasswordDirectInput) { if (availableWalletTypes.length == 1) { - return createRoute(RouteSettings(name: Routes.newWallet, arguments: availableWalletTypes.first)); + return createRoute( + RouteSettings( + name: Routes.newWallet, + arguments: NewWalletArguments(type: availableWalletTypes.first), + ), + ); } else { return createRoute(RouteSettings(name: Routes.newWalletType)); } @@ -145,8 +156,10 @@ Route createRoute(RouteSettings settings) { builder: (_) => getIt.get(param1: (PinCodeState context, dynamic _) { if (availableWalletTypes.length == 1) { - Navigator.of(context.context) - .pushNamed(Routes.newWallet, arguments: availableWalletTypes.first); + Navigator.of(context.context).pushNamed( + Routes.newWallet, + arguments: NewWalletArguments(type: availableWalletTypes.first), + ); } else { Navigator.of(context.context).pushNamed(Routes.newWalletType); } @@ -155,17 +168,39 @@ Route createRoute(RouteSettings settings) { case Routes.newWalletType: return CupertinoPageRoute( - builder: (_) => getIt.get( - param1: (BuildContext context, WalletType type) => - Navigator.of(context).pushNamed(Routes.newWallet, arguments: type))); + builder: (_) => getIt.get( + param1: NewWalletTypeArguments( + onTypeSelected: (BuildContext context, WalletType type) => + Navigator.of(context).pushNamed( + Routes.newWallet, + arguments: NewWalletArguments(type: type), + ), + isCreate: true, + isHardwareWallet: false, + ), + ), + ); - case Routes.newWallet: + case Routes.walletGroupsDisplayPage: final type = settings.arguments as WalletType; - final walletNewVM = getIt.get(param1: type); - final seedTypeViewModel = getIt.get(); + final walletGroupsDisplayVM = getIt.get(param1: type); return CupertinoPageRoute( - builder: (_) => NewWalletPage(walletNewVM, seedTypeViewModel)); + builder: (_) => WalletGroupsDisplayPage(walletGroupsDisplayVM)); + + case Routes.newWallet: + final args = settings.arguments as NewWalletArguments; + + final walletNewVM = getIt.get(param1: args); + final seedSettingsViewModel = getIt.get(); + + return CupertinoPageRoute( + builder: (_) => NewWalletPage( + walletNewVM, + seedSettingsViewModel, + isChildWallet: args.isChildWallet, + ), + ); case Routes.chooseHardwareWalletAccount: final arguments = settings.arguments as List; @@ -186,10 +221,15 @@ Route createRoute(RouteSettings settings) { case Routes.restoreWalletType: return CupertinoPageRoute( - builder: (_) => getIt.get( - param1: (BuildContext context, WalletType type) => - Navigator.of(context).pushNamed(Routes.restoreWallet, arguments: type), - param2: [false, false])); + builder: (_) => getIt.get( + param1: NewWalletTypeArguments( + onTypeSelected: (BuildContext context, WalletType type) => + Navigator.of(context).pushNamed(Routes.restoreWallet, arguments: type), + isCreate: false, + isHardwareWallet: false, + ), + ), + ); case Routes.restoreOptions: if (SettingsStoreBase.walletPasswordDirectInput) { @@ -221,10 +261,15 @@ Route createRoute(RouteSettings settings) { builder: (_) => getIt.get(param1: availableWalletTypes.first)); } else { return CupertinoPageRoute( - builder: (_) => getIt.get( - param1: (BuildContext context, WalletType type) => - Navigator.of(context).pushNamed(Routes.restoreWallet, arguments: type), - param2: [false, false])); + builder: (_) => getIt.get( + param1: NewWalletTypeArguments( + onTypeSelected: (BuildContext context, WalletType type) => + Navigator.of(context).pushNamed(Routes.restoreWallet, arguments: type), + isCreate: false, + isHardwareWallet: false, + ), + ), + ); } case Routes.restoreWalletFromHardwareWallet: @@ -253,23 +298,35 @@ Route createRoute(RouteSettings settings) { )); } else { return CupertinoPageRoute( - builder: (_) => getIt.get( - param1: (BuildContext context, WalletType type) { - final arguments = ConnectDevicePageParams( - walletType: type, - onConnectDevice: (BuildContext context, _) => Navigator.of(context) - .pushNamed(Routes.chooseHardwareWalletAccount, arguments: [type]), - ); + builder: (_) => getIt.get( + param1: NewWalletTypeArguments( + onTypeSelected: (BuildContext context, WalletType type) { + final arguments = ConnectDevicePageParams( + walletType: type, + onConnectDevice: (BuildContext context, _) => Navigator.of(context) + .pushNamed(Routes.chooseHardwareWalletAccount, arguments: [type]), + ); - Navigator.of(context).pushNamed(Routes.connectDevices, arguments: arguments); - }, - param2: [false, true])); + Navigator.of(context).pushNamed(Routes.connectDevices, arguments: arguments); + }, + isCreate: false, + isHardwareWallet: true, + ), + ), + ); } case Routes.restoreWalletTypeFromQR: return CupertinoPageRoute( - builder: (_) => getIt.get( - param1: (BuildContext context, WalletType type) => Navigator.of(context).pop(type))); + builder: (_) => getIt.get( + param1: NewWalletTypeArguments( + onTypeSelected: (BuildContext context, WalletType type) => + Navigator.of(context).pop(type), + isCreate: false, + isHardwareWallet: false, + ), + ), + ); case Routes.seed: return MaterialPageRoute( @@ -293,13 +350,17 @@ Route createRoute(RouteSettings settings) { settings: settings, builder: (_) => getIt.get()); case Routes.send: - final initialPaymentRequest = settings.arguments as PaymentRequest?; + final args = settings.arguments as Map?; + final initialPaymentRequest = args?['paymentRequest'] as PaymentRequest?; + final coinTypeToSpendFrom = args?['coinTypeToSpendFrom'] as UnspentCoinType?; return CupertinoPageRoute( - fullscreenDialog: true, - builder: (_) => getIt.get( - param1: initialPaymentRequest, - )); + fullscreenDialog: true, + builder: (_) => getIt.get( + param1: initialPaymentRequest, + param2: coinTypeToSpendFrom, + ), + ); case Routes.sendTemplate: return CupertinoPageRoute( @@ -321,7 +382,7 @@ Route createRoute(RouteSettings settings) { case Routes.bumpFeePage: return CupertinoPageRoute( fullscreenDialog: true, - builder: (_) => getIt.get(param1: settings.arguments as TransactionInfo)); + builder: (_) => getIt.get(param1: settings.arguments as List)); case Routes.newSubaddress: return CupertinoPageRoute( @@ -342,22 +403,22 @@ Route createRoute(RouteSettings settings) { case Routes.walletEdit: return MaterialPageRoute( - fullscreenDialog: true, - builder: (_) => getIt.get(param1: settings.arguments as List)); + fullscreenDialog: true, + builder: (_) => + getIt.get(param1: settings.arguments as WalletEditPageArguments), + ); case Routes.auth: return MaterialPageRoute( fullscreenDialog: true, - builder: (_) - => SettingsStoreBase.walletPasswordDirectInput - ? getIt.get( - param1: WalletUnlockArguments( + builder: (_) => SettingsStoreBase.walletPasswordDirectInput + ? getIt.get( + param1: WalletUnlockArguments( callback: settings.arguments as OnAuthenticationFinished), - instanceName: 'wallet_unlock_verifiable', - param2: true) - : getIt.get( - param1: settings.arguments as OnAuthenticationFinished, - param2: true)); + instanceName: 'wallet_unlock_verifiable', + param2: true) + : getIt.get( + param1: settings.arguments as OnAuthenticationFinished, param2: true)); case Routes.totpAuthCodePage: final args = settings.arguments as TotpAuthArgumentsModel; @@ -371,34 +432,35 @@ Route createRoute(RouteSettings settings) { case Routes.walletUnlockLoadable: return MaterialPageRoute( fullscreenDialog: true, - builder: (_) - => getIt.get( + builder: (_) => getIt.get( param1: settings.arguments as WalletUnlockArguments, - instanceName: 'wallet_unlock_loadable', + instanceName: 'wallet_unlock_loadable', param2: true)); case Routes.unlock: return MaterialPageRoute( fullscreenDialog: true, - builder: (_) - => SettingsStoreBase.walletPasswordDirectInput - ? WillPopScope( - child: getIt.get( + builder: (_) => SettingsStoreBase.walletPasswordDirectInput + ? WillPopScope( + child: getIt.get( param1: WalletUnlockArguments( - callback: settings.arguments as OnAuthenticationFinished), + callback: settings.arguments as OnAuthenticationFinished), param2: false, instanceName: 'wallet_unlock_verifiable'), - onWillPop: () async => false) - : WillPopScope( - child: getIt.get( - param1: settings.arguments as OnAuthenticationFinished, - param2: false), + onWillPop: () async => false) + : WillPopScope( + child: getIt.get( + param1: settings.arguments as OnAuthenticationFinished, param2: false), onWillPop: () async => false)); case Routes.silentPaymentsSettings: return CupertinoPageRoute( fullscreenDialog: true, builder: (_) => getIt.get()); + case Routes.mwebSettings: + return CupertinoPageRoute( + fullscreenDialog: true, builder: (_) => getIt.get()); + case Routes.connectionSync: return CupertinoPageRoute( fullscreenDialog: true, builder: (_) => getIt.get()); @@ -437,11 +499,12 @@ Route createRoute(RouteSettings settings) { return CupertinoPageRoute( builder: (context) => WillPopScope( child: SettingsStoreBase.walletPasswordDirectInput - ? getIt.get(instanceName: 'wallet_password_login') - : getIt.get(instanceName: 'login'), + ? getIt.get(instanceName: 'wallet_password_login') + : getIt.get(instanceName: 'login'), onWillPop: () async => - // FIX-ME: Additional check does it works correctly - (await SystemChannels.platform.invokeMethod('SystemNavigator.pop') ?? false)), + // FIX-ME: Additional check does it works correctly + (await SystemChannels.platform.invokeMethod('SystemNavigator.pop') ?? + false)), fullscreenDialog: true); case Routes.newPowNode: @@ -537,8 +600,7 @@ Route createRoute(RouteSettings settings) { case Routes.support: return CupertinoPageRoute( - fullscreenDialog: true, - builder: (_) => getIt.get()); + fullscreenDialog: true, builder: (_) => getIt.get()); case Routes.supportLiveChat: return CupertinoPageRoute(builder: (_) => getIt.get()); @@ -548,7 +610,9 @@ Route createRoute(RouteSettings settings) { fullscreenDialog: true, builder: (_) => getIt.get()); case Routes.unspentCoinsList: - return MaterialPageRoute(builder: (_) => getIt.get()); + final coinTypeToSpendFrom = settings.arguments as UnspentCoinType?; + return MaterialPageRoute( + builder: (_) => getIt.get(param1: coinTypeToSpendFrom)); case Routes.unspentCoinsDetails: final args = settings.arguments as List; @@ -567,8 +631,7 @@ Route createRoute(RouteSettings settings) { case Routes.cakePayBuyCardPage: final args = settings.arguments as List; - return CupertinoPageRoute( - builder: (_) => getIt.get(param1: args)); + return CupertinoPageRoute(builder: (_) => getIt.get(param1: args)); case Routes.cakePayBuyCardDetailPage: final args = settings.arguments as List; @@ -582,7 +645,8 @@ Route createRoute(RouteSettings settings) { case Routes.cakePayVerifyOtpPage: final args = settings.arguments as List; - return CupertinoPageRoute(builder: (_) => getIt.get(param1: args)); + return CupertinoPageRoute( + builder: (_) => getIt.get(param1: args)); case Routes.cakePayAccountPage: return CupertinoPageRoute(builder: (_) => getIt.get()); @@ -597,16 +661,21 @@ Route createRoute(RouteSettings settings) { case Routes.advancedPrivacySettings: final args = settings.arguments as Map; final type = args['type'] as WalletType; + final isFromRestore = args['isFromRestore'] as bool? ?? false; + final isChildWallet = args['isChildWallet'] as bool? ?? false; final useTestnet = args['useTestnet'] as bool; final toggleTestnet = args['toggleTestnet'] as Function(bool? val); return CupertinoPageRoute( builder: (_) => AdvancedPrivacySettingsPage( - useTestnet, - toggleTestnet, - getIt.get(param1: type), - getIt.get(param1: type, param2: false), - getIt.get(), + isFromRestore: isFromRestore, + isChildWallet: isChildWallet, + useTestnet: useTestnet, + toggleUseTestnet: toggleTestnet, + advancedPrivacySettingsViewModel: + getIt.get(param1: type), + nodeViewModel: getIt.get(param1: type, param2: false), + seedSettingsViewModel: getIt.get(), )); case Routes.anonPayInvoicePage: @@ -709,12 +778,21 @@ Route createRoute(RouteSettings settings) { getIt.get(), ), ); - + case Routes.connectDevices: final params = settings.arguments as ConnectDevicePageParams; return MaterialPageRoute( builder: (_) => ConnectDevicePage(params, getIt.get())); + case Routes.walletGroupDescription: + final walletType = settings.arguments as WalletType; + + return MaterialPageRoute( + builder: (_) => WalletGroupDescriptionPage( + selectedWalletType: walletType, + ), + ); + default: return MaterialPageRoute( builder: (_) => Scaffold( diff --git a/lib/routes.dart b/lib/routes.dart index 9c421cab5..0529d7c6f 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -8,8 +8,7 @@ class Routes { static const restoreWalletFromSeedKeys = '/restore_wallet_from_seeds_keys'; static const restoreWalletFromHardwareWallet = '/restore/hardware_wallet'; static const restoreWalletTypeFromQR = '/restore_wallet_from_qr_code'; - static const restoreWalletChooseDerivation = - '/restore_wallet_choose_derivation'; + static const restoreWalletChooseDerivation = '/restore_wallet_choose_derivation'; static const chooseHardwareWalletAccount = '/restore/hardware_wallet/accounts'; static const dashboard = '/dashboard'; static const send = '/send'; @@ -74,6 +73,7 @@ class Routes { static const cakePayAccountPage = '/cake_pay_account_page'; static const webViewPage = '/web_view_page'; static const silentPaymentsSettings = '/silent_payments_settings'; + static const mwebSettings = '/mweb_settings'; static const connectionSync = '/connection_sync_page'; static const securityBackupPage = '/security_and_backup_page'; static const privacyPage = '/privacy_page'; @@ -99,11 +99,13 @@ class Routes { static const editToken = '/edit_token'; static const manageNodes = '/manage_nodes'; static const managePowNodes = '/manage_pow_nodes'; - static const walletConnectConnectionsListing = - '/wallet-connect-connections-listing'; + static const walletConnectConnectionsListing = '/wallet-connect-connections-listing'; static const nftDetailsPage = '/nft_details_page'; static const importNFTPage = '/import_nft_page'; static const torPage = '/tor_page'; + static const signPage = '/sign_page'; static const connectDevices = '/device/connect'; + static const walletGroupsDisplayPage = '/wallet_groups_display_page'; + static const walletGroupDescription = '/wallet_group_description'; } diff --git a/lib/solana/cw_solana.dart b/lib/solana/cw_solana.dart index e70739db9..7894f77ed 100644 --- a/lib/solana/cw_solana.dart +++ b/lib/solana/cw_solana.dart @@ -10,18 +10,34 @@ class CWSolana extends Solana { @override WalletCredentials createSolanaNewWalletCredentials({ required String name, + String? mnemonic, + String? parentAddress, WalletInfo? walletInfo, String? password, + String? passphrase, }) => - SolanaNewWalletCredentials(name: name, walletInfo: walletInfo, password: password); + SolanaNewWalletCredentials( + name: name, + walletInfo: walletInfo, + password: password, + mnemonic: mnemonic, + parentAddress: parentAddress, + passphrase: passphrase, + ); @override WalletCredentials createSolanaRestoreWalletFromSeedCredentials({ required String name, required String mnemonic, required String password, + String? passphrase, }) => - SolanaRestoreWalletFromSeedCredentials(name: name, password: password, mnemonic: mnemonic); + SolanaRestoreWalletFromSeedCredentials( + name: name, + password: password, + mnemonic: mnemonic, + passphrase: passphrase, + ); @override WalletCredentials createSolanaRestoreWalletFromPrivateKey({ diff --git a/lib/src/screens/connect_device/connect_device_page.dart b/lib/src/screens/connect_device/connect_device_page.dart index a482b1c41..9e331e818 100644 --- a/lib/src/screens/connect_device/connect_device_page.dart +++ b/lib/src/screens/connect_device/connect_device_page.dart @@ -3,15 +3,14 @@ import 'dart:io'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; -import 'package:cake_wallet/src/screens/connect_device/debug_device_page.dart'; import 'package:cake_wallet/src/screens/connect_device/widgets/device_tile.dart'; import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; -import 'package:ledger_flutter/ledger_flutter.dart'; -import 'package:permission_handler/permission_handler.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:ledger_flutter_plus/ledger_flutter_plus.dart'; typedef OnConnectDevice = void Function(BuildContext, LedgerViewModel); @@ -19,7 +18,8 @@ class ConnectDevicePageParams { final WalletType walletType; final OnConnectDevice onConnectDevice; - ConnectDevicePageParams({required this.walletType, required this.onConnectDevice}); + ConnectDevicePageParams( + {required this.walletType, required this.onConnectDevice}); } class ConnectDevicePage extends BasePage { @@ -35,7 +35,8 @@ class ConnectDevicePage extends BasePage { String get title => S.current.restore_title_from_hardware_wallet; @override - Widget body(BuildContext context) => ConnectDevicePageBody(walletType, onConnectDevice, ledgerVM); + Widget body(BuildContext context) => + ConnectDevicePageBody(walletType, onConnectDevice, ledgerVM); } class ConnectDevicePageBody extends StatefulWidget { @@ -43,47 +44,35 @@ class ConnectDevicePageBody extends StatefulWidget { final OnConnectDevice onConnectDevice; final LedgerViewModel ledgerVM; - const ConnectDevicePageBody(this.walletType, this.onConnectDevice, this.ledgerVM); + const ConnectDevicePageBody( + this.walletType, this.onConnectDevice, this.ledgerVM); @override ConnectDevicePageBodyState createState() => ConnectDevicePageBodyState(); } class ConnectDevicePageBodyState extends State { - final imageLedger = 'assets/images/ledger_nano.png'; - - final ledger = Ledger( - options: LedgerOptions( - scanMode: ScanMode.balanced, - maxScanDuration: const Duration(minutes: 5), - ), - onPermissionRequest: (_) async { - Map statuses = await [ - Permission.bluetoothScan, - Permission.bluetoothConnect, - Permission.bluetoothAdvertise, - ].request(); - - return statuses.values.where((status) => status.isDenied).isEmpty; - }, - ); - - var bleIsEnabled = true; var bleDevices = []; var usbDevices = []; late Timer? _usbRefreshTimer = null; late Timer? _bleRefreshTimer = null; + late Timer? _bleStateTimer = null; late StreamSubscription? _bleRefresh = null; @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { - _bleRefreshTimer = Timer.periodic(Duration(seconds: 1), (_) => _refreshBleDevices()); + _bleStateTimer = Timer.periodic( + Duration(seconds: 1), (_) => widget.ledgerVM.updateBleState()); + + _bleRefreshTimer = + Timer.periodic(Duration(seconds: 1), (_) => _refreshBleDevices()); if (Platform.isAndroid) { - _usbRefreshTimer = Timer.periodic(Duration(seconds: 1), (_) => _refreshUsbDevices()); + _usbRefreshTimer = + Timer.periodic(Duration(seconds: 1), (_) => _refreshUsbDevices()); } }); } @@ -91,35 +80,59 @@ class ConnectDevicePageBodyState extends State { @override void dispose() { _bleRefreshTimer?.cancel(); + _bleStateTimer?.cancel(); _usbRefreshTimer?.cancel(); _bleRefresh?.cancel(); super.dispose(); } Future _refreshUsbDevices() async { - final dev = await ledger.listUsbDevices(); + final dev = await widget.ledgerVM.ledgerPlusUSB.devices; if (usbDevices.length != dev.length) setState(() => usbDevices = dev); + // _usbRefresh = widget.ledgerVM + // .scanForUsbDevices() + // .listen((device) => setState(() => usbDevices.add(device))) + // ..onError((e) { + // throw e.toString(); + // }); + // Keep polling until the lfp lib gets updated + // _usbRefreshTimer?.cancel(); + // _usbRefreshTimer = null; } Future _refreshBleDevices() async { try { - _bleRefresh = ledger.scan().listen((device) => setState(() => bleDevices.add(device))) + _bleRefresh = widget.ledgerVM + .scanForBleDevices() + .listen((device) => setState(() => bleDevices.add(device))) ..onError((e) { throw e.toString(); }); - setState(() => bleIsEnabled = true); _bleRefreshTimer?.cancel(); _bleRefreshTimer = null; } catch (e) { - setState(() => bleIsEnabled = false); + print(e); } } Future _connectToDevice(LedgerDevice device) async { - await widget.ledgerVM.connectLedger(device); + await widget.ledgerVM.connectLedger(device, widget.walletType); widget.onConnectDevice(context, widget.ledgerVM); } + String _getDeviceTileLeading(LedgerDeviceType deviceInfo) { + switch (deviceInfo) { + case LedgerDeviceType.nanoX: + return 'assets/images/hardware_wallet/ledger_nano_x.png'; + case LedgerDeviceType.stax: + return 'assets/images/hardware_wallet/ledger_stax.png'; + case LedgerDeviceType.flex: + return 'assets/images/hardware_wallet/ledger_flex.png'; + default: + return 'assets/images/hardware_wallet/ledger_nano_x.png'; + } + } + @override Widget build(BuildContext context) { return Center( @@ -139,7 +152,9 @@ class ConnectDevicePageBodyState extends State { style: TextStyle( fontSize: 16, fontWeight: FontWeight.w500, - color: Theme.of(context).extension()!.titleColor), + color: Theme.of(context) + .extension()! + .titleColor), textAlign: TextAlign.center, ), ), @@ -152,18 +167,25 @@ class ConnectDevicePageBodyState extends State { // title: "Debug Ledger", // leading: imageLedger, // ), - if (!bleIsEnabled) - Padding( - padding: EdgeInsets.only(left: 20, right: 20, bottom: 20), - child: Text( - S.of(context).ledger_please_enable_bluetooth, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - color: Theme.of(context).extension()!.titleColor), - textAlign: TextAlign.center, + Observer( + builder: (_) => Offstage( + offstage: widget.ledgerVM.bleIsEnabled, + child: Padding( + padding: EdgeInsets.only(left: 20, right: 20, bottom: 20), + child: Text( + S.of(context).ledger_please_enable_bluetooth, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Theme.of(context) + .extension()! + .titleColor), + textAlign: TextAlign.center, + ), ), ), + ), + if (bleDevices.length > 0) ...[ Padding( padding: EdgeInsets.only(left: 20, right: 20, bottom: 20), @@ -174,7 +196,9 @@ class ConnectDevicePageBodyState extends State { style: TextStyle( fontSize: 14, fontWeight: FontWeight.w400, - color: Theme.of(context).extension()!.titleColor, + color: Theme.of(context) + .extension()! + .titleColor, ), ), ), @@ -186,7 +210,7 @@ class ConnectDevicePageBodyState extends State { child: DeviceTile( onPressed: () => _connectToDevice(device), title: device.name, - leading: imageLedger, + leading: _getDeviceTileLeading(device.deviceInfo), connectionType: device.connectionType, ), ), @@ -203,7 +227,9 @@ class ConnectDevicePageBodyState extends State { style: TextStyle( fontSize: 14, fontWeight: FontWeight.w400, - color: Theme.of(context).extension()!.titleColor, + color: Theme.of(context) + .extension()! + .titleColor, ), ), ), @@ -215,7 +241,7 @@ class ConnectDevicePageBodyState extends State { child: DeviceTile( onPressed: () => _connectToDevice(device), title: device.name, - leading: imageLedger, + leading: _getDeviceTileLeading(device.deviceInfo), connectionType: device.connectionType, ), ), diff --git a/lib/src/screens/connect_device/debug_device_page.dart b/lib/src/screens/connect_device/debug_device_page.dart index f5a9ef2a4..bed9d59a7 100644 --- a/lib/src/screens/connect_device/debug_device_page.dart +++ b/lib/src/screens/connect_device/debug_device_page.dart @@ -1,15 +1,15 @@ -// import 'dart:convert'; +// import 'dart:typed_data'; // +// import 'package:basic_utils/basic_utils.dart'; +// import 'package:bitcoin_base/bitcoin_base.dart'; // import 'package:cake_wallet/src/screens/base_page.dart'; // import 'package:cake_wallet/src/screens/connect_device/widgets/device_tile.dart'; // import 'package:cake_wallet/src/widgets/primary_button.dart'; // import 'package:cake_wallet/utils/responsive_layout_util.dart'; -// import 'package:convert/convert.dart'; // import 'package:flutter/material.dart'; -// import 'package:ledger_bitcoin/ledger_bitcoin.dart'; -// import 'package:ledger_flutter/ledger_flutter.dart'; +// import 'package:ledger_flutter_plus/ledger_flutter_plus.dart'; +// import 'package:ledger_litecoin/ledger_litecoin.dart'; // import 'package:permission_handler/permission_handler.dart'; -// import 'package:polyseed/polyseed.dart'; // // class DebugDevicePage extends BasePage { // @override @@ -50,7 +50,9 @@ // }, // ); // -// late BitcoinLedgerApp btc; +// // late BitcoinLedgerApp btc; +// late LitecoinLedgerApp ltc; +// // var devices = []; // var status = ""; // var counter = 0; @@ -59,7 +61,8 @@ // @override // void initState() { // super.initState(); -// btc = BitcoinLedgerApp(ledger); +// // btc = BitcoinLedgerApp(ledger); +// ltc = LitecoinLedgerApp(ledger); // } // // @override @@ -81,7 +84,7 @@ // // @override // Widget build(BuildContext context) { -// final imageLedger = 'assets/images/ledger_nano.png'; +// final imageLedger = 'assets/images/hardware_wallet/ledger_nano_x.png'; // // return Center( // child: Container( @@ -99,40 +102,25 @@ // DebugButton( // title: "Get Version", // method: "Version", -// func: () async => await btc.getVersion(selectedDevice!), -// ), -// DebugButton( -// title: "Get Master Fingerprint", -// method: "Master Fingerprint", -// func: () async => hex.encode(await btc.getMasterFingerprint(selectedDevice!)), -// ), -// DebugButton( -// title: "Get XPub", -// method: "XPub", -// func: () async => await btc.getXPubKey(selectedDevice!, derivationPath: "m/84'/0'/$counter'"), +// // func: () async => await btc.getVersion(selectedDevice!), +// func: () async => await ltc.getVersion(selectedDevice!), // ), // DebugButton( // title: "Get Wallet Address", // method: "Wallet Address", // func: () async { // setState(() => counter++); -// final derivationPath = "m/84'/0'/$counter'/0/0"; -// return await btc.getAccounts(selectedDevice!, accountsDerivationPath: derivationPath); +// final derivationPath = "m/84'/2'/0'/0/0"; +// return await ltc.getAccounts(selectedDevice!, +// accountsDerivationPath: derivationPath); +// // return await btc.getAccounts(selectedDevice!, accountsDerivationPath: derivationPath); // // return await ethereum!.getHardwareWalletAccounts(selectedDevice!); -// }, +// }, // ), // DebugButton( // title: "Send Money", -// method: "Sig", -// func: () async { -// final psbt = PsbtV2(); -// final psbtBuf = base64.decode( -// "cHNidP8BAgQCAAAAAQQBAQEFAQIAAQ4gTW6k/cwKKu1u7m9oKr5ob7VcAC0IPkfaDitRi/FkD7sBDwQAAAAAARAE/////wEA/ekBAQAAAAABA9AYVQLI722H0osKMa/4dvMucrnKV1Myxtlp0l0BoOBDAQAAAAD/////ku6r2ABaHt9N26f/P4eMljX8t1f4lBcFfEwuNm/uXYoBAAAAAP////+YeAl8arEGKOcyrWJAYwSboyCstkhHN8zn7/vy7pkYTAEAAAAA/////wHlHgAAAAAAABYAFKdq0umSucBGVkl2MpT6Hgo/0a/xAkcwRAIgMkiJmNFbEi2I3CQYOwyV/JepCnFQRvj4xghkySpFcJMCIGAypkkWltfj+ucvqUIu27tusDAIAAB+rBhX/GV7hPlEASEDyLmWyTLjLfC9kn8pnW42jW5N6EJo5fObjWWEyfLDu9UCSDBFAiEAg9crVtwBPF+sWk+Th6pLwzDjJGItwsUCvoBPtmMTEb4CIDGuM7WOguV0TP21oidF3bSUZlEAjUHWfWzxLKw+3LofASEDfN16xKb70UZSeQyX5Tlh8iRq7np5Nlz9GYdcSU50sKwCSDBFAiEAvotOblaEiBptRWkvb6bj2MGyRjTphKLBLiHYmrRMTCgCIEKJH+z65uPSSz1NIb0d/u3bU9l0xcWk0idEsXjB+BIiASEDrAEiEtrSNKxbh6F/KPaCTafF2LVjCzb75WB+x4xSuoQAAAAAAQEf5R4AAAAAAAAWABSnatLpkrnARlZJdjKU+h4KP9Gv8SIGA3xMuxmPsBAm9aMEUBs3N46DB+Kdts3bZR/Wxt+uM0H4GKtN6bpUAACAAAAAgAAAAIAAAAAAAAAAAAABBBTk7bEOxYcdXDi1eeWraYDufm6eJgEDCOgDAAAAAAAAAAEEFDX3g/pnDXIfsRw8shK42NZn+SdpAQMIiBMAAAAAAAAiAgN8TLsZj7AQJvWjBFAbNzeOgwfinbbN22Uf1sbfrjNB+BirTem6VAAAgAAAAIAAAACAAAAAAAAAAAAA" -// ); -// psbt.deserialize(psbtBuf); -// final result = await btc.signPsbt(selectedDevice!, psbt: psbt); -// return result.toHexString(); -// }, +// method: "Raw Tx", +// func: sendMoney // ), // Padding( // padding: EdgeInsets.only(top: 20), @@ -147,18 +135,18 @@ // ...devices // .map( // (device) => Padding( -// padding: EdgeInsets.only(bottom: 20), -// child: DeviceTile( -// onPressed: () { -// setState(() => selectedDevice = device); -// ledger.connect(device); -// }, -// title: device.name, -// leading: imageLedger, -// connectionType: device.connectionType, -// ), -// ), -// ) +// padding: EdgeInsets.only(bottom: 20), +// child: DeviceTile( +// onPressed: () { +// setState(() => selectedDevice = device); +// ledger.connect(device); +// }, +// title: device.name, +// leading: imageLedger, +// connectionType: device.connectionType, +// ), +// ), +// ) // .toList(), // PrimaryButton( // text: "Refresh BLE", @@ -188,6 +176,42 @@ // ); // } // +// Future sendMoney() async { +// final readyInputs = [ +// LedgerTransaction( +// rawTx: "010000000001018c055c85c3724c98842d27712771dd0de139711f5940bba2df4615c5522184740000000017160014faf7f6dfb4e70798b92c93f33b4c51024491829df0ffffff022b05c70000000000160014f489f947fd13a1fb44ac168427081d3f30b6ce0cde9dd82e0000000017a914d5eca376cb49d65031220ff9093b7d407073ed0d8702483045022100f648c9f6a9b8f35b6ec29bbfae312c95ed3d56ce6a3f177d994efe90562ec4bd02205b82ce2c94bc0c9d152c3afc668b200bd82f48d6a14e83c66ba0f154cd5f69190121038f1dca119420d4aa7ad04af1c0d65304723789cccc56d335b18692390437f35900000000", +// outputIndex: 0, +// ownerPublicKey: +// HexUtils.decode("03b2e67958ed3356e329e05cf94c3bee6b20c17175ac3b2a1278e073bf44f5d6ec"), +// ownerDerivationPath: "m/84'/2'/0'/0/0", +// sequence: 0xffffffff, +// ) +// ]; +// +// final outputs = [ +// BitcoinOutput( +// address: P2wpkhAddress.fromAddress( +// address: "ltc1qn0g5e36xaj07lqj6w9xn52ng07hud42g3jf5ps", +// network: LitecoinNetwork.mainnet), +// value: BigInt.from(1000000)), +// BitcoinOutput( +// address: P2wpkhAddress.fromAddress( +// address: "ltc1qrx29qz4ghu4j0xk37ptgk7034cwpmjyxhrcnk9", +// network: LitecoinNetwork.mainnet), +// value: BigInt.from(12042705)), +// ]; +// return await ltc.createTransaction(selectedDevice!, +// inputs: readyInputs, +// outputs: outputs +// .map((e) => TransactionOutput.fromBigInt( +// e.value, Uint8List.fromList(e.address.toScriptPubKey().toBytes()))) +// .toList(), +// sigHashType: 0x01, +// additionals: ["bech32"], +// isSegWit: true, +// useTrustedInputForSegwit: true); +// } +// // Widget DebugButton( // {required String title, required String method, required Future Function() func}) { // return Padding( diff --git a/lib/src/screens/connect_device/widgets/device_tile.dart b/lib/src/screens/connect_device/widgets/device_tile.dart index 8367d1606..58f65c5de 100644 --- a/lib/src/screens/connect_device/widgets/device_tile.dart +++ b/lib/src/screens/connect_device/widgets/device_tile.dart @@ -1,6 +1,6 @@ import 'package:cake_wallet/themes/extensions/option_tile_theme.dart'; import 'package:flutter/material.dart'; -import 'package:ledger_flutter/ledger_flutter.dart'; +import 'package:ledger_flutter_plus/ledger_flutter_plus.dart'; class DeviceTile extends StatelessWidget { const DeviceTile({ diff --git a/lib/src/screens/dashboard/dashboard_page.dart b/lib/src/screens/dashboard/dashboard_page.dart index ad6e68cd8..953463269 100644 --- a/lib/src/screens/dashboard/dashboard_page.dart +++ b/lib/src/screens/dashboard/dashboard_page.dart @@ -147,6 +147,7 @@ class _DashboardPageView extends BasePage { return Observer( builder: (context) { return ServicesUpdatesWidget( + key: ValueKey('dashboard_page_services_update_button_key'), dashboardViewModel.getServicesStatus(), enabled: dashboardViewModel.isEnabledBulletinAction, ); @@ -157,6 +158,7 @@ class _DashboardPageView extends BasePage { @override Widget middle(BuildContext context) { return SyncIndicator( + key: ValueKey('dashboard_page_sync_indicator_button_key'), dashboardViewModel: dashboardViewModel, onTap: () => Navigator.of(context, rootNavigator: true).pushNamed(Routes.connectionSync), ); @@ -173,6 +175,7 @@ class _DashboardPageView extends BasePage { alignment: Alignment.centerRight, width: 40, child: TextButton( + key: ValueKey('dashboard_page_wallet_menu_button_key'), // FIX-ME: Style //highlightColor: Colors.transparent, //splashColor: Colors.transparent, @@ -226,6 +229,7 @@ class _DashboardPageView extends BasePage { child: Observer( builder: (context) { return PageView.builder( + key: ValueKey('dashboard_page_view_key'), controller: controller, itemCount: pages.length, itemBuilder: (context, index) => pages[index], @@ -291,6 +295,8 @@ class _DashboardPageView extends BasePage { 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, 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 0dd3458b6..f8cbe9120 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 @@ -1,4 +1,6 @@ import 'package:another_flushbar/flushbar.dart'; +import 'package:cake_wallet/core/new_wallet_arguments.dart'; +import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; import 'package:cake_wallet/core/auth_service.dart'; import 'package:cake_wallet/entities/desktop_dropdown_item.dart'; import 'package:cake_wallet/generated/i18n.dart'; @@ -219,7 +221,7 @@ class _DesktopWalletSelectionDropDownState extends State { if (widget.token != null) { address = widget.homeSettingsViewModel.getTokenAddressBasedOnWallet(widget.token!); - + _contractAddressController.text = address ?? ''; _tokenNameController.text = widget.token!.name; _tokenSymbolController.text = widget.token!.title; @@ -135,8 +138,8 @@ class _EditTokenPageBodyState extends State { S.of(context).warning, style: TextStyle( fontSize: 16, - fontWeight: FontWeight.w500, - color: Theme.of(context).dialogTheme.backgroundColor, + fontWeight: FontWeight.bold, + color: Theme.of(context).extension()!.titleColor, ), ), Padding( @@ -145,7 +148,7 @@ class _EditTokenPageBodyState extends State { S.of(context).add_token_warning, style: TextStyle( fontSize: 14, - fontWeight: FontWeight.normal, + fontWeight: FontWeight.bold, color: Theme.of(context) .extension()! .detailsTitlesColor, @@ -176,47 +179,84 @@ class _EditTokenPageBodyState extends State { ), SizedBox(height: 20), ], - Row( - children: [ - Expanded( - child: PrimaryButton( - onPressed: () async { - if (widget.token != null) { - await widget.homeSettingsViewModel.deleteToken(widget.token!); - } - Navigator.pop(context); - }, - text: widget.token != null ? S.of(context).delete : S.of(context).cancel, - color: Colors.red, - textColor: Colors.white, - ), - ), - SizedBox(width: 20), - Expanded( - child: PrimaryButton( - onPressed: () async { - if (_formKey.currentState!.validate() && - (!_showDisclaimer || _disclaimerChecked)) { - 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) { + Observer( + builder: (context) { + return Row( + children: [ + Expanded( + child: LoadingPrimaryButton( + isLoading: widget.homeSettingsViewModel.isDeletingToken, + onPressed: () async { + if (widget.token != null) { + await widget.homeSettingsViewModel.deleteToken(widget.token!); + } Navigator.pop(context); - } - } - }, - text: S.of(context).save, - color: Theme.of(context).primaryColor, - textColor: Colors.white, - ), - ), - ], + }, + text: widget.token != null ? S.of(context).delete : S.of(context).cancel, + color: Colors.red, + textColor: Colors.white, + ), + ), + SizedBox(width: 20), + Expanded( + child: LoadingPrimaryButton( + isLoading: widget.homeSettingsViewModel.isAddingToken || + widget.homeSettingsViewModel.isValidatingContractAddress, + onPressed: () async { + if (_formKey.currentState!.validate() && + (!_showDisclaimer || _disclaimerChecked)) { + final hasPotentialError = await widget.homeSettingsViewModel + .checkIfERC20TokenContractAddressIsAPotentialScamAddress( + _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, + ); + }; + + if (hasPotentialError) { + showPopUp( + context: context, + builder: (dialogContext) { + return AlertWithTwoActions( + alertTitle: S.current.warning, + alertContent: S.current.contract_warning, + rightButtonText: S.of(context).continue_text, + leftButtonText: S.of(context).cancel, + actionRightButton: () async { + Navigator.of(dialogContext).pop(); + await actionCall(); + if (mounted) { + Navigator.pop(context); + } + }, + actionLeftButton: () => Navigator.of(dialogContext).pop(), + ); + }, + ); + } else { + await actionCall(); + if (mounted) { + Navigator.pop(context); + } + } + } + }, + text: S.of(context).save, + color: Theme.of(context).primaryColor, + textColor: Colors.white, + ), + ), + ], + ); + }, ), ], ), diff --git a/lib/src/screens/dashboard/pages/address_page.dart b/lib/src/screens/dashboard/pages/address_page.dart index 3ac97740d..10f9aef43 100644 --- a/lib/src/screens/dashboard/pages/address_page.dart +++ b/lib/src/screens/dashboard/pages/address_page.dart @@ -155,13 +155,14 @@ class AddressPage extends BasePage { amountTextFieldFocusNode: _cryptoAmountFocus, amountController: _amountController, isLight: dashboardViewModel.settingsStore.currentTheme.type == - ThemeType.light))), + ThemeType.light, + ))), SizedBox(height: 16), Observer(builder: (_) { if (addressListViewModel.hasAddressList) { return SelectButton( text: addressListViewModel.buttonTitle, - onTap: () async => Navigator.of(context).pushNamed(Routes.receive), + onTap: () => Navigator.of(context).pushNamed(Routes.receive), textColor: Theme.of(context).extension()!.textColor, color: Theme.of(context).extension()!.syncedBackgroundColor, borderColor: Theme.of(context).extension()!.cardBorderColor, @@ -225,7 +226,8 @@ class AddressPage extends BasePage { } break; default: - if (addressListViewModel.type == WalletType.bitcoin) { + if (addressListViewModel.type == WalletType.bitcoin || + addressListViewModel.type == WalletType.litecoin) { addressListViewModel.setAddressType(bitcoin!.getBitcoinAddressType(option)); } } diff --git a/lib/src/screens/dashboard/pages/balance_page.dart b/lib/src/screens/dashboard/pages/balance_page.dart index 57f908986..028b3cfcf 100644 --- a/lib/src/screens/dashboard/pages/balance_page.dart +++ b/lib/src/screens/dashboard/pages/balance_page.dart @@ -7,6 +7,7 @@ import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/dashboard/pages/nft_listing_page.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/home_screen_account_widget.dart'; +import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; import 'package:cake_wallet/src/screens/exchange_trade/information_page.dart'; @@ -18,12 +19,13 @@ import 'package:cake_wallet/themes/extensions/balance_page_theme.dart'; import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart'; import 'package:cake_wallet/themes/extensions/sync_indicator_theme.dart'; import 'package:cake_wallet/utils/feature_flag.dart'; +import 'package:cake_wallet/utils/payment_request.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/nft_view_model.dart'; import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/unspent_coin_type.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -129,7 +131,7 @@ class CryptoBalanceWidget extends StatelessWidget { builder: (_) { if (dashboardViewModel.getMoneroError != null) { return Padding( - padding: const EdgeInsets.fromLTRB(16,0,16,16), + padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), child: DashBoardRoundedCardWidget( title: "Invalid monero bindings", subTitle: dashboardViewModel.getMoneroError.toString(), @@ -144,13 +146,12 @@ class CryptoBalanceWidget extends StatelessWidget { builder: (_) { if (dashboardViewModel.getWowneroError != null) { return Padding( - padding: const EdgeInsets.fromLTRB(16,0,16,16), - child: DashBoardRoundedCardWidget( - title: "Invalid wownero bindings", - subTitle: dashboardViewModel.getWowneroError.toString(), - onTap: () {}, - ) - ); + padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), + child: DashBoardRoundedCardWidget( + title: "Invalid wownero bindings", + subTitle: dashboardViewModel.getWowneroError.toString(), + onTap: () {}, + )); } return Container(); }, @@ -187,7 +188,7 @@ class CryptoBalanceWidget extends StatelessWidget { Padding( padding: const EdgeInsets.all(8.0), child: Image.asset( - 'assets/images/ledger_nano.png', + 'assets/images/hardware_wallet/ledger_nano_x.png', width: 24, color: Theme.of(context) .extension()! @@ -237,8 +238,8 @@ class CryptoBalanceWidget extends StatelessWidget { return Padding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), child: DashBoardRoundedCardWidget( - title: S.current.rep_warning, - subTitle: S.current.rep_warning_sub, + title: S.of(context).rep_warning, + subTitle: S.of(context).rep_warning_sub, onTap: () => Navigator.of(context).pushNamed(Routes.changeRep), onClose: () { dashboardViewModel.settingsStore.shouldShowRepWarning = false; @@ -258,6 +259,7 @@ class CryptoBalanceWidget extends StatelessWidget { dashboardViewModel.balanceViewModel.formattedBalances.elementAt(index); return Observer(builder: (_) { return BalanceRowWidget( + dashboardViewModel: dashboardViewModel, availableBalanceLabel: '${dashboardViewModel.balanceViewModel.availableBalanceLabel}', availableBalance: balance.availableBalance, @@ -271,6 +273,18 @@ class CryptoBalanceWidget extends StatelessWidget { currency: balance.asset, hasAdditionalBalance: dashboardViewModel.balanceViewModel.hasAdditionalBalance, + hasSecondAdditionalBalance: + dashboardViewModel.balanceViewModel.hasSecondAdditionalBalance, + hasSecondAvailableBalance: + dashboardViewModel.balanceViewModel.hasSecondAvailableBalance, + secondAdditionalBalance: balance.secondAdditionalBalance, + secondAdditionalFiatBalance: balance.fiatSecondAdditionalBalance, + secondAvailableBalance: balance.secondAvailableBalance, + secondAvailableFiatBalance: balance.fiatSecondAvailableBalance, + secondAdditionalBalanceLabel: + '${dashboardViewModel.balanceViewModel.secondAdditionalBalanceLabel}', + secondAvailableBalanceLabel: + '${dashboardViewModel.balanceViewModel.secondAvailableBalanceLabel}', isTestnet: dashboardViewModel.isTestnet, ); }); @@ -284,16 +298,15 @@ class CryptoBalanceWidget extends StatelessWidget { if (dashboardViewModel.isMoneroWalletBrokenReasons.isNotEmpty) ...[ SizedBox(height: 10), Padding( - padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), - child: DashBoardRoundedCardWidget( - customBorder: 30, - title: "This wallet has encountered an issue", - 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: () {}, - ) - ) + padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), + child: DashBoardRoundedCardWidget( + customBorder: 30, + title: "This wallet has encountered an issue", + 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), @@ -360,7 +373,87 @@ class CryptoBalanceWidget extends StatelessWidget { ), ), ), - ] + ], + if (dashboardViewModel.showMwebCard) ...[ + SizedBox(height: 10), + Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), + child: DashBoardRoundedCardWidget( + customBorder: 30, + title: S.of(context).litecoin_mweb, + subTitle: S.of(context).litecoin_mweb_description, + hint: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => launchUrl( + Uri.parse( + "https://guides.cakewallet.com/docs/cryptos/litecoin/#mweb"), + mode: LaunchMode.externalApplication, + ), + child: Text( + S.of(context).learn_more, + style: TextStyle( + fontSize: 12, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: Theme.of(context) + .extension()! + .labelTextColor, + height: 1, + ), + softWrap: true, + ), + ), + SizedBox(height: 8), + Row( + children: [ + Expanded( + child: ElevatedButton( + onPressed: () => _dismissMweb(context), + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).primaryColor, + ), + child: Text( + S.of(context).litecoin_mweb_dismiss, + style: TextStyle(color: Colors.white), + ), + ), + ), + const SizedBox(width: 8), + Expanded( + child: ElevatedButton( + onPressed: () => _enableMweb(context), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + foregroundColor: Colors.black, + ), + child: Text( + S.of(context).enable, + maxLines: 1, + ), + ), + ), + ], + ), + ], + ), + onTap: () => {}, + icon: Container( + decoration: BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + child: ImageIcon( + AssetImage('assets/images/mweb_logo.png'), + color: Color.fromARGB(255, 11, 70, 129), + size: 40, + ), + ), + ), + ), + ], ], ); }), @@ -400,6 +493,36 @@ class CryptoBalanceWidget extends StatelessWidget { return dashboardViewModel.setSilentPaymentsScanning(newValue); } + + Future _enableMweb(BuildContext context) async { + if (!dashboardViewModel.hasEnabledMwebBefore) { + await showPopUp( + context: context, + builder: (BuildContext context) => AlertWithOneAction( + alertTitle: S.of(context).alert_notice, + alertContent: S.of(context).litecoin_mweb_warning, + buttonText: S.of(context).understand, + buttonAction: () { + Navigator.of(context).pop(); + }, + )); + } + dashboardViewModel.setMwebEnabled(); + } + + Future _dismissMweb(BuildContext context) async { + await showPopUp( + context: context, + builder: (BuildContext context) => AlertWithOneAction( + alertTitle: S.of(context).alert_notice, + alertContent: S.of(context).litecoin_mweb_enable_later, + buttonText: S.of(context).understand, + buttonAction: () { + Navigator.of(context).pop(); + }, + )); + dashboardViewModel.dismissMweb(); + } } class BalanceRowWidget extends StatelessWidget { @@ -410,11 +533,20 @@ class BalanceRowWidget extends StatelessWidget { required this.additionalBalanceLabel, required this.additionalBalance, required this.additionalFiatBalance, + required this.secondAvailableBalanceLabel, + required this.secondAvailableBalance, + required this.secondAvailableFiatBalance, + required this.secondAdditionalBalanceLabel, + required this.secondAdditionalBalance, + required this.secondAdditionalFiatBalance, required this.frozenBalance, required this.frozenFiatBalance, required this.currency, required this.hasAdditionalBalance, + required this.hasSecondAvailableBalance, + required this.hasSecondAdditionalBalance, required this.isTestnet, + required this.dashboardViewModel, super.key, }); @@ -424,192 +556,251 @@ class BalanceRowWidget extends StatelessWidget { final String additionalBalanceLabel; final String additionalBalance; final String additionalFiatBalance; + final String secondAvailableBalanceLabel; + final String secondAvailableBalance; + final String secondAvailableFiatBalance; + final String secondAdditionalBalanceLabel; + final String secondAdditionalBalance; + final String secondAdditionalFiatBalance; final String frozenBalance; final String frozenFiatBalance; final CryptoCurrency currency; final bool hasAdditionalBalance; + final bool hasSecondAvailableBalance; + final bool hasSecondAdditionalBalance; final bool isTestnet; + final DashboardViewModel dashboardViewModel; // void _showBalanceDescription(BuildContext context) { // showPopUp( // context: context, // builder: (_) => - // InformationPage(information: S.current.available_balance_description), + // InformationPage(information: S.of(context).available_balance_description), // ); // } @override Widget build(BuildContext context) { - return Container( - margin: const EdgeInsets.only(left: 16, right: 16), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(30.0), - border: Border.all( - color: Theme.of(context).extension()!.cardBorderColor, - width: 1, + return Column(children: [ + Container( + margin: const EdgeInsets.only(left: 16, right: 16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(30.0), + border: Border.all( + color: Theme.of(context).extension()!.cardBorderColor, + width: 1, + ), + color: Theme.of(context).extension()!.syncedBackgroundColor, ), - color: Theme.of(context).extension()!.syncedBackgroundColor, - ), - child: Container( - margin: const EdgeInsets.only(top: 16, left: 24, right: 8, bottom: 16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ + child: Container( + margin: const EdgeInsets.only(top: 16, left: 24, right: 8, bottom: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: hasAdditionalBalance + ? () => _showBalanceDescription( + context, S.of(context).available_balance_description) + : null, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Semantics( + hint: 'Double tap to see more information', + container: true, + child: Text('${availableBalanceLabel}', + style: TextStyle( + fontSize: 12, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: Theme.of(context) + .extension()! + .labelTextColor, + height: 1)), + ), + if (hasAdditionalBalance) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: Icon(Icons.help_outline, + size: 16, + color: Theme.of(context) + .extension()! + .labelTextColor), + ), + ], + ), + SizedBox(height: 6), + AutoSizeText(availableBalance, + style: TextStyle( + fontSize: 24, + fontFamily: 'Lato', + fontWeight: FontWeight.w900, + color: Theme.of(context) + .extension()! + .balanceAmountColor, + height: 1), + maxLines: 1, + textAlign: TextAlign.start), + SizedBox(height: 6), + if (isTestnet) + Text(S.of(context).testnet_coins_no_value, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: Theme.of(context).extension()!.textColor, + height: 1)), + if (!isTestnet) + Text('${availableFiatBalance}', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 16, + fontFamily: 'Lato', + fontWeight: FontWeight.w500, + color: Theme.of(context).extension()!.textColor, + height: 1)), + ], + ), + ), + SizedBox( + width: min(MediaQuery.of(context).size.width * 0.2, 100), + child: Center( + child: Column( + children: [ + CakeImageWidget( + imageUrl: currency.iconPath, + height: 40, + width: 40, + displayOnError: Container( + height: 30.0, + width: 30.0, + child: Center( + child: Text( + currency.title.substring(0, min(currency.title.length, 2)), + style: TextStyle(fontSize: 11), + ), + ), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.grey.shade400, + ), + ), + ), + const SizedBox(height: 10), + Text( + currency.title, + style: TextStyle( + fontSize: 15, + fontFamily: 'Lato', + fontWeight: FontWeight.w800, + color: + Theme.of(context).extension()!.assetTitleColor, + height: 1, + ), + ), + ], + ), + ), + ), + ], + ), + if (frozenBalance.isNotEmpty) GestureDetector( behavior: HitTestBehavior.opaque, onTap: hasAdditionalBalance - ? () => - _showBalanceDescription(context, S.current.available_balance_description) + ? () => _showBalanceDescription( + context, S.of(context).unavailable_balance_description) : null, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + SizedBox(height: 26), Row( children: [ - Semantics( - hint: 'Double tap to see more information', - container: true, - child: Text('${availableBalanceLabel}', - style: TextStyle( - fontSize: 12, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: Theme.of(context) - .extension()! - .labelTextColor, - height: 1)), - ), - if (hasAdditionalBalance) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 4), - child: Icon(Icons.help_outline, - size: 16, - color: Theme.of(context) - .extension()! - .labelTextColor), + Text( + S.of(context).unavailable_balance, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: + Theme.of(context).extension()!.labelTextColor, + height: 1, ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: Icon(Icons.help_outline, + size: 16, + color: Theme.of(context) + .extension()! + .labelTextColor), + ), ], ), - SizedBox(height: 6), - AutoSizeText(availableBalance, - style: TextStyle( - fontSize: 24, - fontFamily: 'Lato', - fontWeight: FontWeight.w900, - color: Theme.of(context) - .extension()! - .balanceAmountColor, - height: 1), - maxLines: 1, - textAlign: TextAlign.start), - SizedBox(height: 6), - if (isTestnet) - Text(S.current.testnet_coins_no_value, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 14, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: Theme.of(context).extension()!.textColor, - height: 1)), + SizedBox(height: 8), + AutoSizeText( + frozenBalance, + style: TextStyle( + fontSize: 20, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: + Theme.of(context).extension()!.balanceAmountColor, + height: 1, + ), + maxLines: 1, + textAlign: TextAlign.center, + ), + SizedBox(height: 4), if (!isTestnet) - Text('${availableFiatBalance}', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 16, - fontFamily: 'Lato', - fontWeight: FontWeight.w500, - color: Theme.of(context).extension()!.textColor, - height: 1)), - ], - ), - ), - SizedBox( - width: min(MediaQuery.of(context).size.width * 0.2, 100), - child: Center( - child: Column( - children: [ - CakeImageWidget( - imageUrl: currency.iconPath, - height: 40, - width: 40, - displayOnError: Container( - height: 30.0, - width: 30.0, - child: Center( - child: Text( - currency.title.substring(0, min(currency.title.length, 2)), - style: TextStyle(fontSize: 11), - ), - ), - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Colors.grey.shade400, - ), - ), - ), - const SizedBox(height: 10), Text( - currency.title, - style: TextStyle( - fontSize: 15, - fontFamily: 'Lato', - fontWeight: FontWeight.w800, - color: Theme.of(context).extension()!.assetTitleColor, - height: 1, - ), - ), - ], - ), - ), - ), - ], - ), - if (frozenBalance.isNotEmpty) - GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: hasAdditionalBalance - ? () => - _showBalanceDescription(context, S.current.unavailable_balance_description) - : null, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox(height: 26), - Row( - children: [ - Text( - S.current.unavailable_balance, + frozenFiatBalance, textAlign: TextAlign.center, style: TextStyle( fontSize: 12, fontFamily: 'Lato', fontWeight: FontWeight.w400, - color: Theme.of(context).extension()!.labelTextColor, + color: Theme.of(context).extension()!.textColor, height: 1, ), ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 4), - child: Icon(Icons.help_outline, - size: 16, - color: - Theme.of(context).extension()!.labelTextColor), - ), - ], + ], + ), + ), + if (hasAdditionalBalance) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 24), + Text( + '${additionalBalanceLabel}', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: Theme.of(context).extension()!.labelTextColor, + height: 1, + ), ), SizedBox(height: 8), AutoSizeText( - frozenBalance, + additionalBalance, style: TextStyle( fontSize: 20, fontFamily: 'Lato', fontWeight: FontWeight.w400, - color: Theme.of(context).extension()!.balanceAmountColor, + color: Theme.of(context).extension()!.assetTitleColor, height: 1, ), maxLines: 1, @@ -618,7 +809,7 @@ class BalanceRowWidget extends StatelessWidget { SizedBox(height: 4), if (!isTestnet) Text( - frozenFiatBalance, + '${additionalFiatBalance}', textAlign: TextAlign.center, style: TextStyle( fontSize: 12, @@ -630,55 +821,316 @@ class BalanceRowWidget extends StatelessWidget { ), ], ), - ), - if (hasAdditionalBalance) - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox(height: 24), - Text( - '${additionalBalanceLabel}', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 12, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: Theme.of(context).extension()!.labelTextColor, - height: 1, - ), - ), - SizedBox(height: 8), - AutoSizeText( - additionalBalance, - style: TextStyle( - fontSize: 20, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: Theme.of(context).extension()!.assetTitleColor, - height: 1, - ), - maxLines: 1, - textAlign: TextAlign.center, - ), - SizedBox(height: 4), - if (!isTestnet) - Text( - '${additionalFiatBalance}', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 12, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: Theme.of(context).extension()!.textColor, - height: 1, - ), - ), - ], - ), - ], + ], + ), ), ), - ); + if (hasSecondAdditionalBalance || hasSecondAvailableBalance) ...[ + SizedBox(height: 16), + Container( + margin: const EdgeInsets.only(left: 16, right: 16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(30.0), + border: Border.all( + color: Theme.of(context).extension()!.cardBorderColor, + width: 1, + ), + color: Theme.of(context).extension()!.syncedBackgroundColor, + ), + child: Container( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + margin: const EdgeInsets.only(top: 16, left: 24, right: 8, bottom: 16), + child: Stack( + children: [ + if (currency == CryptoCurrency.ltc) + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Container( + padding: EdgeInsets.only(right: 16, top: 0), + child: Column( + children: [ + Container( + child: ImageIcon( + AssetImage('assets/images/mweb_logo.png'), + color: Theme.of(context) + .extension()! + .assetTitleColor, + size: 40, + ), + ), + const SizedBox(height: 10), + Text( + 'MWEB', + style: TextStyle( + fontSize: 15, + fontFamily: 'Lato', + fontWeight: FontWeight.w800, + color: Theme.of(context) + .extension()! + .assetTitleColor, + height: 1, + ), + ), + ], + ), + ), + ], + ), + if (hasSecondAvailableBalance) + Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '${secondAvailableBalanceLabel}', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: Theme.of(context) + .extension()! + .labelTextColor, + height: 1, + ), + ), + SizedBox(height: 8), + AutoSizeText( + secondAvailableBalance, + style: TextStyle( + fontSize: 24, + fontFamily: 'Lato', + fontWeight: FontWeight.w900, + color: Theme.of(context) + .extension()! + .assetTitleColor, + height: 1, + ), + maxLines: 1, + textAlign: TextAlign.center, + ), + SizedBox(height: 6), + if (!isTestnet) + Text( + '${secondAvailableFiatBalance}', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 16, + fontFamily: 'Lato', + fontWeight: FontWeight.w500, + color: Theme.of(context) + .extension()! + .textColor, + height: 1, + ), + ), + ], + ), + ], + ), + ], + ), + ), + Container( + margin: const EdgeInsets.only(top: 0, left: 24, right: 8, bottom: 16), + child: Stack( + children: [ + if (hasSecondAdditionalBalance) + Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 24), + Text( + '${secondAdditionalBalanceLabel}', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: Theme.of(context) + .extension()! + .labelTextColor, + height: 1, + ), + ), + SizedBox(height: 8), + AutoSizeText( + secondAdditionalBalance, + style: TextStyle( + fontSize: 20, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: Theme.of(context) + .extension()! + .assetTitleColor, + height: 1, + ), + maxLines: 1, + textAlign: TextAlign.center, + ), + SizedBox(height: 4), + if (!isTestnet) + Text( + '${secondAdditionalFiatBalance}', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: Theme.of(context) + .extension()! + .textColor, + height: 1, + ), + ), + ], + ), + ], + ), + ], + ), + ), + IntrinsicHeight( + child: Container( + padding: EdgeInsets.symmetric(horizontal: 24), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Semantics( + label: S.of(context).litecoin_mweb_pegin, + child: OutlinedButton( + onPressed: () { + final mwebAddress = + bitcoin!.getUnusedMwebAddress(dashboardViewModel.wallet); + PaymentRequest? paymentRequest = null; + if ((mwebAddress?.isNotEmpty ?? false)) { + paymentRequest = + PaymentRequest.fromUri(Uri.parse("litecoin:${mwebAddress}")); + } + Navigator.pushNamed( + context, + Routes.send, + arguments: { + 'paymentRequest': paymentRequest, + 'coinTypeToSpendFrom': UnspentCoinType.nonMweb, + }, + ); + }, + style: OutlinedButton.styleFrom( + backgroundColor: Colors.grey.shade400 + .withAlpha(50), + side: BorderSide(color: Colors.grey.shade400 + .withAlpha(50), width: 0), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + ), + child: Container( + padding: EdgeInsets.symmetric(vertical: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset( + height: 30, + width: 30, + 'assets/images/received.png', + color: Theme.of(context) + .extension()! + .balanceAmountColor, + ), + const SizedBox(width: 8), + Text( + S.of(context).litecoin_mweb_pegin, + style: TextStyle( + color: Theme.of(context) + .extension()! + .textColor, + ), + ), + ], + ), + ), + ), + ), + ), + SizedBox(width: 24), + Expanded( + child: Semantics( + label: S.of(context).litecoin_mweb_pegout, + child: OutlinedButton( + onPressed: () { + final litecoinAddress = + bitcoin!.getUnusedSegwitAddress(dashboardViewModel.wallet); + PaymentRequest? paymentRequest = null; + if ((litecoinAddress?.isNotEmpty ?? false)) { + paymentRequest = PaymentRequest.fromUri( + Uri.parse("litecoin:${litecoinAddress}")); + } + Navigator.pushNamed( + context, + Routes.send, + arguments: { + 'paymentRequest': paymentRequest, + 'coinTypeToSpendFrom': UnspentCoinType.mweb, + }, + ); + }, + style: OutlinedButton.styleFrom( + backgroundColor: Colors.grey.shade400 + .withAlpha(50), + side: BorderSide(color: Colors.grey.shade400 + .withAlpha(50), width: 0), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + ), + child: Container( + padding: EdgeInsets.symmetric(vertical: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset( + height: 30, + width: 30, + 'assets/images/upload.png', + color: Theme.of(context) + .extension()! + .balanceAmountColor, + ), + const SizedBox(width: 8), + Text( + S.of(context).litecoin_mweb_pegout, + style: TextStyle( + color: Theme.of(context) + .extension()! + .textColor, + ), + ), + ], + ), + ), + ), + ), + ), + ], + ), + ), + ), + SizedBox(height: 16), + ], + ), + ), + ), + ], + ]); } void _showBalanceDescription(BuildContext context, String content) { diff --git a/lib/src/screens/dashboard/pages/cake_features_page.dart b/lib/src/screens/dashboard/pages/cake_features_page.dart index 0c953c892..37bc3a55f 100644 --- a/lib/src/screens/dashboard/pages/cake_features_page.dart +++ b/lib/src/screens/dashboard/pages/cake_features_page.dart @@ -52,20 +52,26 @@ class CakeFeaturesPage extends StatelessWidget { onTap: () => _navigatorToGiftCardsPage(context), title: 'Cake Pay', subTitle: S.of(context).cake_pay_subtitle, - svgPicture: SvgPicture.asset( - 'assets/images/cards.svg', - height: 125, - width: 125, + image: Image.asset( + 'assets/images/cards.png', + height: 100, + width: 115, fit: BoxFit.cover, ), ), SizedBox(height: 10), DashBoardRoundedCardWidget( + onTap: () => _launchUrl("cake.nano-gpt.com"), title: "NanoGPT", subTitle: S.of(context).nanogpt_subtitle, - onTap: () => _launchUrl("cake.nano-gpt.com"), + image: Image.asset( + 'assets/images/nanogpt.png', + height: 80, + width: 80, + fit: BoxFit.cover, + ), ), - SizedBox(height: 20), + SizedBox(height: 10), Observer( builder: (context) { if (!dashboardViewModel.hasSignMessages) { @@ -75,6 +81,12 @@ class CakeFeaturesPage extends StatelessWidget { 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, + ), ); }, ), diff --git a/lib/src/screens/dashboard/pages/transactions_page.dart b/lib/src/screens/dashboard/pages/transactions_page.dart index 7c0e9cad4..b6d1c286b 100644 --- a/lib/src/screens/dashboard/pages/transactions_page.dart +++ b/lib/src/screens/dashboard/pages/transactions_page.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/anonpay_transaction_row.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/order_row.dart'; import 'package:cake_wallet/themes/extensions/placeholder_theme.dart'; @@ -52,7 +53,7 @@ class TransactionsPage extends StatelessWidget { try { final uri = Uri.parse( "https://guides.cakewallet.com/docs/FAQ/why_are_my_funds_not_appearing/"); - launchUrl(uri, mode: LaunchMode.externalApplication); + launchUrl(uri, mode: LaunchMode.externalApplication); } catch (_) {} }, title: S.of(context).syncing_wallet_alert_title, @@ -83,10 +84,19 @@ class TransactionsPage extends StatelessWidget { } final transaction = item.transaction; - final transactionType = dashboardViewModel.type == WalletType.ethereum && - transaction.evmSignatureName == 'approval' - ? ' (${transaction.evmSignatureName})' - : ''; + final transactionType = dashboardViewModel.getTransactionType(transaction); + + List tags = []; + if (dashboardViewModel.type == WalletType.bitcoin) { + if (bitcoin!.txIsReceivedSilentPayment(transaction)) { + tags.add(S.of(context).silent_payment); + } + } + if (dashboardViewModel.type == WalletType.litecoin) { + if (bitcoin!.txIsMweb(transaction)) { + tags.add("MWEB"); + } + } return Observer( builder: (_) => TransactionRow( @@ -100,8 +110,9 @@ class TransactionsPage extends StatelessWidget { ? '' : item.formattedFiatAmount, isPending: transaction.isPending, - title: item.formattedTitle + - item.formattedStatus + ' $transactionType', + title: + item.formattedTitle + item.formattedStatus + transactionType, + tags: tags, ), ); } diff --git a/lib/src/screens/dashboard/widgets/action_button.dart b/lib/src/screens/dashboard/widgets/action_button.dart index 23f5c2f93..49ebab3cd 100644 --- a/lib/src/screens/dashboard/widgets/action_button.dart +++ b/lib/src/screens/dashboard/widgets/action_button.dart @@ -2,13 +2,15 @@ import 'package:flutter/material.dart'; import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart'; class ActionButton extends StatelessWidget { - ActionButton( - {required this.image, - required this.title, - this.route, - this.onClick, - this.alignment = Alignment.center, - this.textColor}); + ActionButton({ + required this.image, + required this.title, + this.route, + this.onClick, + this.alignment = Alignment.center, + this.textColor, + super.key, + }); final Image image; final String title; diff --git a/lib/src/screens/dashboard/widgets/menu_widget.dart b/lib/src/screens/dashboard/widgets/menu_widget.dart index 78d8abc95..6d8379b29 100644 --- a/lib/src/screens/dashboard/widgets/menu_widget.dart +++ b/lib/src/screens/dashboard/widgets/menu_widget.dart @@ -97,7 +97,14 @@ class MenuWidgetState extends State { @override Widget build(BuildContext context) { - final itemCount = SettingActions.all.length; + List items = List.of(SettingActions.all); + if (!widget.dashboardViewModel.hasSilentPayments) { + items.removeWhere((element) => element.name(context) == S.of(context).silent_payments_settings); + } + if (!widget.dashboardViewModel.hasMweb) { + items.removeWhere((element) => element.name(context) == S.of(context).litecoin_mweb_settings); + } + int itemCount = items.length; moneroIcon = Image.asset('assets/images/monero_menu.png', color: Theme.of(context).extension()!.iconColor); @@ -181,12 +188,7 @@ class MenuWidgetState extends State { index--; - final item = SettingActions.all[index]; - - if (!widget.dashboardViewModel.hasSilentPayments && - item.name(context) == S.of(context).silent_payments_settings) { - return Container(); - } + final item = items[index]; final isLastTile = index == itemCount - 1; diff --git a/lib/src/screens/dashboard/widgets/sign_form.dart b/lib/src/screens/dashboard/widgets/sign_form.dart index c0f8ba328..deead87a4 100644 --- a/lib/src/screens/dashboard/widgets/sign_form.dart +++ b/lib/src/screens/dashboard/widgets/sign_form.dart @@ -20,7 +20,7 @@ class SignForm extends StatefulWidget { SignFormState createState() => SignFormState(); } -class SignFormState extends State { +class SignFormState extends State with AutomaticKeepAliveClientMixin { SignFormState() : formKey = GlobalKey(), messageController = TextEditingController(), @@ -42,8 +42,12 @@ class SignFormState extends State { super.dispose(); } + @override + bool get wantKeepAlive => true; + @override Widget build(BuildContext context) { + super.build(context); return Container( padding: EdgeInsets.only(left: 24, right: 24), child: Column( diff --git a/lib/src/screens/dashboard/widgets/sync_indicator.dart b/lib/src/screens/dashboard/widgets/sync_indicator.dart index 52e596a83..27b3d0109 100644 --- a/lib/src/screens/dashboard/widgets/sync_indicator.dart +++ b/lib/src/screens/dashboard/widgets/sync_indicator.dart @@ -7,7 +7,11 @@ import 'package:cw_core/sync_status.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/sync_indicator_icon.dart'; class SyncIndicator extends StatelessWidget { - SyncIndicator({required this.dashboardViewModel, required this.onTap}); + SyncIndicator({ + required this.dashboardViewModel, + required this.onTap, + super.key, + }); final DashboardViewModel dashboardViewModel; final Function() onTap; diff --git a/lib/src/screens/dashboard/widgets/trade_row.dart b/lib/src/screens/dashboard/widgets/trade_row.dart index caccb8047..7c809aa9d 100644 --- a/lib/src/screens/dashboard/widgets/trade_row.dart +++ b/lib/src/screens/dashboard/widgets/trade_row.dart @@ -1,4 +1,5 @@ import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; +import 'package:cake_wallet/utils/image_utill.dart'; import 'package:flutter/material.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cake_wallet/exchange/exchange_provider_description.dart'; @@ -36,7 +37,8 @@ class TradeRow extends StatelessWidget { children: [ ClipRRect( borderRadius: BorderRadius.circular(50), - child: Image.asset(provider.image, width: 36, height: 36)), + child: ImageUtil.getImageFromPath( + imagePath: provider.image, height: 36, width: 36)), SizedBox(width: 12), Expanded( child: Column( diff --git a/lib/src/screens/dashboard/widgets/transaction_raw.dart b/lib/src/screens/dashboard/widgets/transaction_raw.dart index 3a95b9f2e..b18131f3d 100644 --- a/lib/src/screens/dashboard/widgets/transaction_raw.dart +++ b/lib/src/screens/dashboard/widgets/transaction_raw.dart @@ -5,14 +5,16 @@ import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart'; import 'package:cake_wallet/themes/extensions/transaction_trade_theme.dart'; class TransactionRow extends StatelessWidget { - TransactionRow( - {required this.direction, - required this.formattedDate, - required this.formattedAmount, - required this.formattedFiatAmount, - required this.isPending, - required this.title, - required this.onTap}); + TransactionRow({ + required this.direction, + required this.formattedDate, + required this.formattedAmount, + required this.formattedFiatAmount, + required this.isPending, + required this.tags, + required this.title, + required this.onTap, + }); final VoidCallback onTap; final TransactionDirection direction; @@ -21,6 +23,7 @@ class TransactionRow extends StatelessWidget { final String formattedFiatAmount; final bool isPending; final String title; + final List tags; @override Widget build(BuildContext context) { @@ -38,50 +41,80 @@ class TransactionRow extends StatelessWidget { width: 36, decoration: BoxDecoration( shape: BoxShape.circle, - color: Theme.of(context).extension()!.rowsColor - ), - child: Image.asset( - direction == TransactionDirection.incoming - ? 'assets/images/down_arrow.png' - : 'assets/images/up_arrow.png'), + color: Theme.of(context).extension()!.rowsColor), + child: Image.asset(direction == TransactionDirection.incoming + ? 'assets/images/down_arrow.png' + : 'assets/images/up_arrow.png'), ), SizedBox(width: 12), Expanded( child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(title, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - color: Theme.of(context).extension()!.textColor)), - Text(formattedAmount, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - color: Theme.of(context).extension()!.textColor)) - ]), - SizedBox(height: 5), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(formattedDate, - style: TextStyle( - fontSize: 14, - color: Theme.of(context).extension()!.dateSectionRowColor)), - Text(formattedFiatAmount, - style: TextStyle( - fontSize: 14, - color: Theme.of(context).extension()!.dateSectionRowColor)) - ]) - ], - ) - ) + mainAxisSize: MainAxisSize.min, + children: [ + Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + Row( + children: [ + Text(title, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Theme.of(context).extension()!.textColor, + )), + ...tags.map((tag) => Row(children: [SizedBox(width: 8), TxTag(tag: tag)])), + ], + ), + Text(formattedAmount, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Theme.of(context).extension()!.textColor)) + ]), + SizedBox(height: 5), + Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + Text(formattedDate, + style: TextStyle( + fontSize: 14, + color: + Theme.of(context).extension()!.dateSectionRowColor)), + Text(formattedFiatAmount, + style: TextStyle( + fontSize: 14, + color: + Theme.of(context).extension()!.dateSectionRowColor)) + ]) + ], + )) ], ), )); } } + +// A tag to add context to a transaction +// example use: differ silent payments from regular txs +class TxTag extends StatelessWidget { + TxTag({required this.tag}); + + final String tag; + + @override + Widget build(BuildContext context) { + return Container( + height: 17, + padding: EdgeInsets.only(left: 6, right: 6), + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(8.5)), + color: Theme.of(context).extension()!.rowsColor, + ), + alignment: Alignment.center, + child: Text( + tag.toLowerCase(), + style: TextStyle( + color: Theme.of(context).extension()!.textColor, + fontSize: 9, + fontWeight: FontWeight.w600, + ), + ), + ); + } +} diff --git a/lib/src/screens/dashboard/widgets/verify_form.dart b/lib/src/screens/dashboard/widgets/verify_form.dart index d59261494..282ffa9da 100644 --- a/lib/src/screens/dashboard/widgets/verify_form.dart +++ b/lib/src/screens/dashboard/widgets/verify_form.dart @@ -1,23 +1,7 @@ -import 'package:cake_wallet/core/wallet_name_validator.dart'; -import 'package:cake_wallet/entities/generate_name.dart'; -import 'package:cake_wallet/entities/seed_type.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/widgets/address_text_field.dart'; -import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; -import 'package:cake_wallet/src/widgets/blockchain_height_widget.dart'; -import 'package:cake_wallet/src/widgets/picker.dart'; -import 'package:cake_wallet/src/widgets/seed_language_picker.dart'; -import 'package:cake_wallet/src/widgets/seed_widget.dart'; -import 'package:cake_wallet/themes/extensions/address_theme.dart'; -import 'package:cake_wallet/themes/extensions/send_page_theme.dart'; -import 'package:cake_wallet/utils/show_bar.dart'; -import 'package:cake_wallet/utils/show_pop_up.dart'; -import 'package:cake_wallet/view_model/seed_type_view_model.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:mobx/mobx.dart'; -import 'package:polyseed/polyseed.dart'; class VerifyForm extends StatefulWidget { VerifyForm({ @@ -31,7 +15,7 @@ class VerifyForm extends StatefulWidget { VerifyFormState createState() => VerifyFormState(); } -class VerifyFormState extends State { +class VerifyFormState extends State with AutomaticKeepAliveClientMixin { VerifyFormState() : formKey = GlobalKey(), messageController = TextEditingController(), @@ -52,9 +36,13 @@ class VerifyFormState extends State { void dispose() { super.dispose(); } + + @override + bool get wantKeepAlive => true; @override Widget build(BuildContext context) { + super.build(context); return Container( padding: EdgeInsets.only(left: 24, right: 24), child: Form( diff --git a/lib/src/screens/disclaimer/disclaimer_page.dart b/lib/src/screens/disclaimer/disclaimer_page.dart index f82a9efbe..c9d959b40 100644 --- a/lib/src/screens/disclaimer/disclaimer_page.dart +++ b/lib/src/screens/disclaimer/disclaimer_page.dart @@ -207,6 +207,7 @@ class DisclaimerBodyState extends State { padding: EdgeInsets.only( left: 24.0, top: 10.0, right: 24.0, bottom: 10.0), child: InkWell( + key: ValueKey('disclaimer_check_key'), onTap: () { setState(() { _checked = !_checked; @@ -230,6 +231,7 @@ class DisclaimerBodyState extends State { color: Theme.of(context).colorScheme.background), child: _checked ? Icon( + key: ValueKey('disclaimer_check_icon_key'), Icons.check, color: Colors.blue, size: 20.0, @@ -253,6 +255,7 @@ class DisclaimerBodyState extends State { padding: EdgeInsets.only(left: 24.0, right: 24.0, bottom: 24.0), child: PrimaryButton( + key: ValueKey('disclaimer_accept_button_key'), onPressed: _checked ? () => Navigator.of(context) .popAndPushNamed(Routes.welcome) diff --git a/lib/src/screens/exchange/exchange_page.dart b/lib/src/screens/exchange/exchange_page.dart index 2c717a3c8..2f8e3eb5c 100644 --- a/lib/src/screens/exchange/exchange_page.dart +++ b/lib/src/screens/exchange/exchange_page.dart @@ -228,6 +228,7 @@ class ExchangePage extends BasePage { ), Observer( builder: (_) => LoadingPrimaryButton( + key: ValueKey('exchange_page_exchange_button_key'), text: S.of(context).exchange, onPressed: () { if (_formKey.currentState != null && @@ -430,6 +431,8 @@ class ExchangePage extends BasePage { context: context, builder: (BuildContext context) { return AlertWithOneAction( + key: ValueKey('exchange_page_trade_creation_failure_dialog_key'), + buttonKey: ValueKey('exchange_page_trade_creation_failure_dialog_button_key'), alertTitle: S.of(context).provider_error(state.title), alertContent: state.error, buttonText: S.of(context).ok, @@ -506,7 +509,7 @@ class ExchangePage extends BasePage { } }); - reaction((_) => exchangeViewModel.wallet.walletAddresses.address, (String address) { + reaction((_) => exchangeViewModel.wallet.walletAddresses.addressForExchange, (String address) { if (exchangeViewModel.depositCurrency == CryptoCurrency.xmr) { depositKey.currentState!.changeAddress(address: address); } @@ -562,7 +565,7 @@ class ExchangePage extends BasePage { key.currentState!.changeWalletName(isCurrentTypeWallet ? exchangeViewModel.wallet.name : ''); key.currentState!.changeAddress( - address: isCurrentTypeWallet ? exchangeViewModel.wallet.walletAddresses.address : ''); + address: isCurrentTypeWallet ? exchangeViewModel.wallet.walletAddresses.addressForExchange : ''); key.currentState!.changeAmount(amount: ''); } @@ -573,9 +576,9 @@ class ExchangePage extends BasePage { if (isCurrentTypeWallet) { key.currentState!.changeWalletName(exchangeViewModel.wallet.name); - key.currentState!.addressController.text = exchangeViewModel.wallet.walletAddresses.address; + key.currentState!.addressController.text = exchangeViewModel.wallet.walletAddresses.addressForExchange; } else if (key.currentState!.addressController.text == - exchangeViewModel.wallet.walletAddresses.address) { + exchangeViewModel.wallet.walletAddresses.addressForExchange) { key.currentState!.changeWalletName(''); key.currentState!.addressController.text = ''; } @@ -612,6 +615,7 @@ class ExchangePage extends BasePage { Widget _exchangeCardsSection(BuildContext context) { final firstExchangeCard = Observer( builder: (_) => ExchangeCard( + cardInstanceName: 'deposit_exchange_card', onDispose: disposeBestRateSync, hasAllAmount: exchangeViewModel.hasAllAmount, allAmount: exchangeViewModel.hasAllAmount @@ -625,7 +629,7 @@ class ExchangePage extends BasePage { initialCurrency: exchangeViewModel.depositCurrency, initialWalletName: depositWalletName ?? '', initialAddress: exchangeViewModel.depositCurrency == exchangeViewModel.wallet.currency - ? exchangeViewModel.wallet.walletAddresses.address + ? exchangeViewModel.wallet.walletAddresses.addressForExchange : exchangeViewModel.depositAddress, initialIsAmountEditable: true, initialIsAddressEditable: exchangeViewModel.isDepositAddressEnabled, @@ -681,6 +685,7 @@ class ExchangePage extends BasePage { final secondExchangeCard = Observer( builder: (_) => ExchangeCard( + cardInstanceName: 'receive_exchange_card', onDispose: disposeBestRateSync, amountFocusNode: _receiveAmountFocus, addressFocusNode: _receiveAddressFocus, @@ -689,7 +694,7 @@ class ExchangePage extends BasePage { initialCurrency: exchangeViewModel.receiveCurrency, initialWalletName: receiveWalletName ?? '', initialAddress: exchangeViewModel.receiveCurrency == exchangeViewModel.wallet.currency - ? exchangeViewModel.wallet.walletAddresses.address + ? exchangeViewModel.wallet.walletAddresses.addressForExchange : exchangeViewModel.receiveAddress, initialIsAmountEditable: exchangeViewModel.isReceiveAmountEditable, isAmountEstimated: true, diff --git a/lib/src/screens/exchange/exchange_template_page.dart b/lib/src/screens/exchange/exchange_template_page.dart index d24c91dad..f82318ca2 100644 --- a/lib/src/screens/exchange/exchange_template_page.dart +++ b/lib/src/screens/exchange/exchange_template_page.dart @@ -121,6 +121,7 @@ class ExchangeTemplatePage extends BasePage { padding: EdgeInsets.fromLTRB(24, 100, 24, 32), child: Observer( builder: (_) => ExchangeCard( + cardInstanceName: 'deposit_exchange_template_card', amountFocusNode: _depositAmountFocus, key: depositKey, title: S.of(context).you_will_send, @@ -128,7 +129,7 @@ class ExchangeTemplatePage extends BasePage { initialWalletName: depositWalletName ?? '', initialAddress: exchangeViewModel.depositCurrency == exchangeViewModel.wallet.currency - ? exchangeViewModel.wallet.walletAddresses.address + ? exchangeViewModel.wallet.walletAddresses.addressForExchange : exchangeViewModel.depositAddress, initialIsAmountEditable: true, initialIsAddressEditable: exchangeViewModel.isDepositAddressEnabled, @@ -157,6 +158,7 @@ class ExchangeTemplatePage extends BasePage { padding: EdgeInsets.only(top: 29, left: 24, right: 24), child: Observer( builder: (_) => ExchangeCard( + cardInstanceName: 'receive_exchange_template_card', amountFocusNode: _receiveAmountFocus, key: receiveKey, title: S.of(context).you_will_get, @@ -164,7 +166,7 @@ class ExchangeTemplatePage extends BasePage { initialWalletName: receiveWalletName ?? '', initialAddress: exchangeViewModel.receiveCurrency == exchangeViewModel.wallet.currency - ? exchangeViewModel.wallet.walletAddresses.address + ? exchangeViewModel.wallet.walletAddresses.addressForExchange : exchangeViewModel.receiveAddress, initialIsAmountEditable: false, isAmountEstimated: true, diff --git a/lib/src/screens/exchange/widgets/currency_picker.dart b/lib/src/screens/exchange/widgets/currency_picker.dart index 0fe1d4e67..8c6f5e214 100644 --- a/lib/src/screens/exchange/widgets/currency_picker.dart +++ b/lib/src/screens/exchange/widgets/currency_picker.dart @@ -12,7 +12,8 @@ class CurrencyPicker extends StatefulWidget { this.title, this.hintText, this.isMoneroWallet = false, - this.isConvertFrom = false}); + this.isConvertFrom = false, + super.key}); final int selectedAtIndex; final List items; diff --git a/lib/src/screens/exchange/widgets/exchange_card.dart b/lib/src/screens/exchange/widgets/exchange_card.dart index 02218f848..75a2eadd7 100644 --- a/lib/src/screens/exchange/widgets/exchange_card.dart +++ b/lib/src/screens/exchange/widgets/exchange_card.dart @@ -19,34 +19,35 @@ import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart'; import 'package:cake_wallet/themes/extensions/send_page_theme.dart'; class ExchangeCard extends StatefulWidget { - ExchangeCard( - {Key? key, - required this.initialCurrency, - required this.initialAddress, - required this.initialWalletName, - required this.initialIsAmountEditable, - required this.isAmountEstimated, - required this.currencies, - required this.onCurrencySelected, - this.imageArrow, - this.currencyValueValidator, - this.addressTextFieldValidator, - this.title = '', - this.initialIsAddressEditable = true, - this.hasRefundAddress = false, - this.isMoneroWallet = false, - this.currencyButtonColor = Colors.transparent, - this.addressButtonsColor = Colors.transparent, - this.borderColor = Colors.transparent, - this.hasAllAmount = false, - this.isAllAmountEnabled = false, - this.amountFocusNode, - this.addressFocusNode, - this.allAmount, - this.onPushPasteButton, - this.onPushAddressBookButton, - this.onDispose}) - : super(key: key); + ExchangeCard({ + Key? key, + required this.initialCurrency, + required this.initialAddress, + required this.initialWalletName, + required this.initialIsAmountEditable, + required this.isAmountEstimated, + required this.currencies, + required this.onCurrencySelected, + this.imageArrow, + this.currencyValueValidator, + this.addressTextFieldValidator, + this.title = '', + this.initialIsAddressEditable = true, + this.hasRefundAddress = false, + this.isMoneroWallet = false, + this.currencyButtonColor = Colors.transparent, + this.addressButtonsColor = Colors.transparent, + this.borderColor = Colors.transparent, + this.hasAllAmount = false, + this.isAllAmountEnabled = false, + this.amountFocusNode, + this.addressFocusNode, + this.allAmount, + this.onPushPasteButton, + this.onPushAddressBookButton, + this.onDispose, + required this.cardInstanceName, + }) : super(key: key); final List currencies; final Function(CryptoCurrency) onCurrencySelected; @@ -74,6 +75,7 @@ class ExchangeCard extends StatefulWidget { final void Function(BuildContext context)? onPushPasteButton; final void Function(BuildContext context)? onPushAddressBookButton; final Function()? onDispose; + final String cardInstanceName; @override ExchangeCardState createState() => ExchangeCardState(); @@ -89,11 +91,13 @@ class ExchangeCardState extends State { _walletName = '', _selectedCurrency = CryptoCurrency.btc, _isAmountEstimated = false, - _isMoneroWallet = false; + _isMoneroWallet = false, + _cardInstanceName = ''; final addressController = TextEditingController(); final amountController = TextEditingController(); + String _cardInstanceName; String _title; String? _min; String? _max; @@ -106,6 +110,7 @@ class ExchangeCardState extends State { @override void initState() { + _cardInstanceName = widget.cardInstanceName; _title = widget.title; _isAmountEditable = widget.initialIsAmountEditable; _isAddressEditable = widget.initialIsAddressEditable; @@ -184,6 +189,7 @@ class ExchangeCardState extends State { mainAxisAlignment: MainAxisAlignment.start, children: [ Text( + key: ValueKey('${_cardInstanceName}_title_key'), _title, style: TextStyle( fontSize: 18, @@ -193,17 +199,26 @@ class ExchangeCardState extends State { ], ), CurrencyAmountTextField( - imageArrow: widget.imageArrow, - selectedCurrency: _selectedCurrency.toString(), - amountFocusNode: widget.amountFocusNode, - amountController: amountController, - onTapPicker: () => _presentPicker(context), - isAmountEditable: _isAmountEditable, - isPickerEnable: true, - allAmountButton: widget.hasAllAmount, - currencyValueValidator: widget.currencyValueValidator, - tag: _selectedCurrency.tag, - allAmountCallback: widget.allAmount), + currencyPickerButtonKey: ValueKey('${_cardInstanceName}_currency_picker_button_key'), + selectedCurrencyTextKey: ValueKey('${_cardInstanceName}_selected_currency_text_key'), + selectedCurrencyTagTextKey: + ValueKey('${_cardInstanceName}_selected_currency_tag_text_key'), + amountTextfieldKey: ValueKey('${_cardInstanceName}_amount_textfield_key'), + sendAllButtonKey: ValueKey('${_cardInstanceName}_send_all_button_key'), + currencyAmountTextFieldWidgetKey: + ValueKey('${_cardInstanceName}_currency_amount_textfield_widget_key'), + imageArrow: widget.imageArrow, + selectedCurrency: _selectedCurrency.toString(), + amountFocusNode: widget.amountFocusNode, + amountController: amountController, + onTapPicker: () => _presentPicker(context), + isAmountEditable: _isAmountEditable, + isPickerEnable: true, + allAmountButton: widget.hasAllAmount, + currencyValueValidator: widget.currencyValueValidator, + tag: _selectedCurrency.tag, + allAmountCallback: widget.allAmount, + ), Divider(height: 1, color: Theme.of(context).extension()!.textFieldHintColor), Padding( padding: EdgeInsets.only(top: 5), @@ -212,6 +227,7 @@ class ExchangeCardState extends State { child: Row(mainAxisAlignment: MainAxisAlignment.start, children: [ _min != null ? Text( + key: ValueKey('${_cardInstanceName}_min_limit_text_key'), S.of(context).min_value(_min ?? '', _selectedCurrency.toString()), style: TextStyle( fontSize: 10, @@ -221,11 +237,15 @@ class ExchangeCardState extends State { : Offstage(), _min != null ? SizedBox(width: 10) : Offstage(), _max != null - ? Text(S.of(context).max_value(_max ?? '', _selectedCurrency.toString()), + ? Text( + key: ValueKey('${_cardInstanceName}_max_limit_text_key'), + S.of(context).max_value(_max ?? '', _selectedCurrency.toString()), style: TextStyle( - fontSize: 10, - height: 1.2, - color: Theme.of(context).extension()!.hintTextColor)) + fontSize: 10, + height: 1.2, + color: Theme.of(context).extension()!.hintTextColor, + ), + ) : Offstage(), ])), ), @@ -246,6 +266,7 @@ class ExchangeCardState extends State { child: Padding( padding: EdgeInsets.only(top: 20), child: AddressTextField( + addressKey: ValueKey('${_cardInstanceName}_editable_address_textfield_key'), focusNode: widget.addressFocusNode, controller: addressController, onURIScanned: (uri) { @@ -286,6 +307,8 @@ class ExchangeCardState extends State { FocusTraversalOrder( order: NumericFocusOrder(3), child: BaseTextFormField( + key: ValueKey( + '${_cardInstanceName}_non_editable_address_textfield_key'), controller: addressController, borderColor: Colors.transparent, suffixIcon: SizedBox(width: _isMoneroWallet ? 80 : 36), @@ -309,6 +332,8 @@ class ExchangeCardState extends State { child: Semantics( label: S.of(context).address_book, child: InkWell( + key: ValueKey( + '${_cardInstanceName}_address_book_button_key'), onTap: () async { final contact = await Navigator.of(context).pushNamed( @@ -346,6 +371,8 @@ class ExchangeCardState extends State { child: Semantics( label: S.of(context).copy_address, child: InkWell( + key: ValueKey( + '${_cardInstanceName}_copy_refund_address_button_key'), onTap: () { Clipboard.setData( ClipboardData(text: addressController.text)); @@ -369,6 +396,7 @@ class ExchangeCardState extends State { showPopUp( context: context, builder: (_) => CurrencyPicker( + key: ValueKey('${_cardInstanceName}_currency_picker_dialog_button_key'), selectedAtIndex: widget.currencies.indexOf(_selectedCurrency), items: widget.currencies, hintText: S.of(context).search_currency, diff --git a/lib/src/screens/exchange_trade/exchange_confirm_page.dart b/lib/src/screens/exchange_trade/exchange_confirm_page.dart index 8070febdf..a179ee473 100644 --- a/lib/src/screens/exchange_trade/exchange_confirm_page.dart +++ b/lib/src/screens/exchange_trade/exchange_confirm_page.dart @@ -2,6 +2,7 @@ import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; import 'package:cake_wallet/exchange/exchange_provider_description.dart'; import 'package:cake_wallet/store/dashboard/trades_store.dart'; import 'package:cake_wallet/themes/extensions/exchange_page_theme.dart'; +import 'package:cake_wallet/utils/image_utill.dart'; import 'package:cake_wallet/utils/show_bar.dart'; import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; @@ -82,6 +83,7 @@ class ExchangeConfirmPage extends BasePage { padding: EdgeInsets.fromLTRB(10, 0, 10, 10), child: Builder( builder: (context) => PrimaryButton( + key: ValueKey('exchange_confirm_page_copy_to_clipboard_button_key'), onPressed: () { Clipboard.setData(ClipboardData(text: trade.id)); showBar( @@ -101,7 +103,8 @@ class ExchangeConfirmPage extends BasePage { mainAxisAlignment: MainAxisAlignment.center, children: [ (trade.provider.image?.isNotEmpty ?? false) - ? Image.asset(trade.provider.image, height: 50) + ? ImageUtil.getImageFromPath( + imagePath: trade.provider.image, width: 50) : const SizedBox(), if (!trade.provider.horizontalLogo) Padding( @@ -115,6 +118,7 @@ class ExchangeConfirmPage extends BasePage { ], )), PrimaryButton( + key: ValueKey('exchange_confirm_page_saved_id_button_key'), onPressed: () => Navigator.of(context) .pushReplacementNamed(Routes.exchangeTrade), text: S.of(context).saved_the_trade_id, diff --git a/lib/src/screens/exchange_trade/exchange_trade_page.dart b/lib/src/screens/exchange_trade/exchange_trade_page.dart index 0766a4562..0f3cc7bd9 100644 --- a/lib/src/screens/exchange_trade/exchange_trade_page.dart +++ b/lib/src/screens/exchange_trade/exchange_trade_page.dart @@ -39,7 +39,9 @@ void showInformation( showPopUp( context: context, - builder: (_) => InformationPage(information: information)); + builder: (_) => InformationPage( + key: ValueKey('information_page_dialog_key'), + information: information)); } class ExchangeTradePage extends BasePage { @@ -215,6 +217,7 @@ class ExchangeTradeState extends State { return widget.exchangeTradeViewModel.isSendable && !(sendingState is TransactionCommitted) ? LoadingPrimaryButton( + key: ValueKey('exchange_trade_page_confirm_sending_button_key'), isDisabled: trade.inputAddress == null || trade.inputAddress!.isEmpty, isLoading: sendingState is IsExecutingState, @@ -241,6 +244,8 @@ class ExchangeTradeState extends State { context: context, builder: (BuildContext popupContext) { return AlertWithOneAction( + key: ValueKey('exchange_trade_page_send_failure_dialog_key'), + buttonKey: ValueKey('exchange_trade_page_send_failure_dialog_button_key'), alertTitle: S.of(popupContext).error, alertContent: state.error, buttonText: S.of(popupContext).ok, @@ -255,6 +260,10 @@ class ExchangeTradeState extends State { context: context, builder: (BuildContext popupContext) { return ConfirmSendingAlert( + key: ValueKey('exchange_trade_page_confirm_sending_dialog_key'), + alertLeftActionButtonKey: ValueKey('exchange_trade_page_confirm_sending_dialog_cancel_button_key'), + alertRightActionButtonKey: + ValueKey('exchange_trade_page_confirm_sending_dialog_send_button_key'), alertTitle: S.of(popupContext).confirm_sending, amount: S.of(popupContext).send_amount, amountValue: widget.exchangeTradeViewModel.sendViewModel diff --git a/lib/src/screens/exchange_trade/information_page.dart b/lib/src/screens/exchange_trade/information_page.dart index eed124b97..114a4824d 100644 --- a/lib/src/screens/exchange_trade/information_page.dart +++ b/lib/src/screens/exchange_trade/information_page.dart @@ -10,7 +10,7 @@ import 'package:cake_wallet/src/widgets/alert_background.dart'; import 'package:cake_wallet/themes/extensions/menu_theme.dart'; class InformationPage extends StatelessWidget { - InformationPage({required this.information}); + InformationPage({required this.information, super.key}); final String information; @@ -47,6 +47,7 @@ class InformationPage extends StatelessWidget { Padding( padding: EdgeInsets.fromLTRB(10, 0, 10, 10), child: PrimaryButton( + key: ValueKey('information_page_got_it_button_key'), onPressed: () => Navigator.of(context).pop(), text: S.of(context).got_it, color: Theme.of(context).extension()!.buttonBackgroundColor, diff --git a/lib/src/screens/nano/nano_change_rep_page.dart b/lib/src/screens/nano/nano_change_rep_page.dart index 9f71bb59c..60207b507 100644 --- a/lib/src/screens/nano/nano_change_rep_page.dart +++ b/lib/src/screens/nano/nano_change_rep_page.dart @@ -40,13 +40,11 @@ class NanoChangeRepPage extends BasePage { (node) => node.account == currentRepAccount, orElse: () => N2Node( account: currentRepAccount, - alias: currentRepAccount, score: 0, uptime: "???", weight: 0, ), ); - return currentNode; } @@ -57,9 +55,7 @@ class NanoChangeRepPage extends BasePage { child: FutureBuilder( future: nano!.getN2Reps(_wallet), builder: (context, snapshot) { - if (snapshot.data == null) { - return SizedBox(); - } + final reps = snapshot.data ?? []; return Container( padding: EdgeInsets.only(left: 24, right: 24), @@ -101,29 +97,35 @@ class NanoChangeRepPage extends BasePage { ), _buildSingleRepresentative( context, - getCurrentRepNode(snapshot.data as List), + getCurrentRepNode(reps), isList: false, + divider: false, ), - Divider(height: 20), - Container( - margin: EdgeInsets.only(top: 12), - child: Text( - S.current.nano_pick_new_rep, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w700, + if (reps.isNotEmpty) ...[ + Divider(height: 20), + Container( + margin: EdgeInsets.only(top: 12), + child: Text( + S.current.nano_pick_new_rep, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w700, + ), ), ), - ), + Divider(height: 20), + ], ], ), ], ), contentPadding: EdgeInsets.only(bottom: 24), content: Container( - child: Column( - children: _getRepresentativeWidgets(context, snapshot.data as List), - ), + child: reps.isNotEmpty + ? Column( + children: _getRepresentativeWidgets(context, reps), + ) + : SizedBox(), ), bottomSectionPadding: EdgeInsets.only(bottom: 24), bottomSection: Observer( @@ -207,19 +209,22 @@ class NanoChangeRepPage extends BasePage { final List ret = []; for (final N2Node node in list) { if (node.alias != null && node.alias!.trim().isNotEmpty) { - ret.add(_buildSingleRepresentative(context, node)); + bool divider = node != list.first; + ret.add(_buildSingleRepresentative(context, node, divider: divider, isList: true)); } } return ret; } - Widget _buildSingleRepresentative(BuildContext context, N2Node rep, {bool isList = true}) { + Widget _buildSingleRepresentative( + BuildContext context, + N2Node rep, { + bool isList = true, + bool divider = false, + }) { return Column( children: [ - if (isList) - Divider( - height: 2, - ), + if (divider) Divider(height: 2), TextButton( style: TextButton.styleFrom( padding: EdgeInsets.zero, @@ -244,11 +249,11 @@ class NanoChangeRepPage extends BasePage { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - _sanitizeAlias(rep.alias), + rep.alias ?? rep.account!, style: TextStyle( color: Theme.of(context).extension()!.titleColor, fontWeight: FontWeight.w700, - fontSize: 18, + fontSize: rep.alias == null ? 14 : 18, ), ), Container( @@ -337,11 +342,4 @@ class NanoChangeRepPage extends BasePage { ], ); } - - String _sanitizeAlias(String? alias) { - if (alias != null) { - return alias.replaceAll(RegExp(r'[^a-zA-Z_.!?_;:-]'), ''); - } - return ''; - } } 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 881d9f95a..26c96fb74 100644 --- a/lib/src/screens/new_wallet/advanced_privacy_settings_page.dart +++ b/lib/src/screens/new_wallet/advanced_privacy_settings_page.dart @@ -3,52 +3,79 @@ import 'package:cake_wallet/entities/exchange_api_mode.dart'; import 'package:cake_wallet/entities/fiat_api_mode.dart'; import 'package:cake_wallet/entities/seed_phrase_length.dart'; import 'package:cake_wallet/entities/seed_type.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/screens/nodes/widgets/node_form.dart'; import 'package:cake_wallet/src/screens/settings/widgets/settings_choices_cell.dart'; import 'package:cake_wallet/src/screens/settings/widgets/settings_picker_cell.dart'; import 'package:cake_wallet/src/screens/settings/widgets/settings_switcher_cell.dart'; -import 'package:cake_wallet/themes/extensions/new_wallet_theme.dart'; -import 'package:cake_wallet/view_model/node_list/node_create_or_edit_view_model.dart'; -import 'package:cake_wallet/view_model/advanced_privacy_settings_view_model.dart'; -import 'package:cake_wallet/view_model/seed_type_view_model.dart'; -import 'package:cake_wallet/view_model/settings/choices_list_item.dart'; -import 'package:cw_core/wallet_type.dart'; -import 'package:flutter_mobx/flutter_mobx.dart'; -import 'package:flutter/material.dart'; -import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; +import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; +import 'package:cake_wallet/themes/extensions/new_wallet_theme.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; +import 'package:cake_wallet/view_model/advanced_privacy_settings_view_model.dart'; +import 'package:cake_wallet/view_model/node_list/node_create_or_edit_view_model.dart'; +import 'package:cake_wallet/view_model/seed_settings_view_model.dart'; +import 'package:cake_wallet/view_model/settings/choices_list_item.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; class AdvancedPrivacySettingsPage extends BasePage { - AdvancedPrivacySettingsPage(this.useTestnet, this.toggleUseTestnet, - this.advancedPrivacySettingsViewModel, this.nodeViewModel, this.seedTypeViewModel); + AdvancedPrivacySettingsPage({ + required this.isFromRestore, + required this.isChildWallet, + required this.useTestnet, + required this.toggleUseTestnet, + required this.advancedPrivacySettingsViewModel, + required this.nodeViewModel, + required this.seedSettingsViewModel, + }); final AdvancedPrivacySettingsViewModel advancedPrivacySettingsViewModel; final NodeCreateOrEditViewModel nodeViewModel; - final SeedTypeViewModel seedTypeViewModel; + final SeedSettingsViewModel seedSettingsViewModel; @override String get title => S.current.privacy_settings; + final bool isFromRestore; + final bool isChildWallet; final bool useTestnet; final Function(bool? val) toggleUseTestnet; @override - Widget body(BuildContext context) => AdvancedPrivacySettingsBody(useTestnet, toggleUseTestnet, - advancedPrivacySettingsViewModel, nodeViewModel, seedTypeViewModel); + Widget body(BuildContext context) => _AdvancedPrivacySettingsBody( + isFromRestore, + isChildWallet, + useTestnet, + toggleUseTestnet, + advancedPrivacySettingsViewModel, + nodeViewModel, + seedSettingsViewModel, + ); } -class AdvancedPrivacySettingsBody extends StatefulWidget { - const AdvancedPrivacySettingsBody(this.useTestnet, this.toggleUseTestnet, - this.privacySettingsViewModel, this.nodeViewModel, this.seedTypeViewModel, - {Key? key}) - : super(key: key); +class _AdvancedPrivacySettingsBody extends StatefulWidget { + const _AdvancedPrivacySettingsBody( + this.isFromRestore, + this.isChildWallet, + this.useTestnet, + this.toggleUseTestnet, + this.privacySettingsViewModel, + this.nodeViewModel, + this.seedTypeViewModel, { + Key? key, + }) : super(key: key); final AdvancedPrivacySettingsViewModel privacySettingsViewModel; final NodeCreateOrEditViewModel nodeViewModel; - final SeedTypeViewModel seedTypeViewModel; + final SeedSettingsViewModel seedTypeViewModel; + final bool isFromRestore; + final bool isChildWallet; final bool useTestnet; final Function(bool? val) toggleUseTestnet; @@ -56,15 +83,35 @@ class AdvancedPrivacySettingsBody extends StatefulWidget { _AdvancedPrivacySettingsBodyState createState() => _AdvancedPrivacySettingsBodyState(); } -class _AdvancedPrivacySettingsBodyState extends State { - _AdvancedPrivacySettingsBodyState(); - +class _AdvancedPrivacySettingsBodyState extends State<_AdvancedPrivacySettingsBody> { + final TextEditingController passphraseController = TextEditingController(); + final TextEditingController confirmPassphraseController = TextEditingController(); final _formKey = GlobalKey(); + final _passphraseFormKey = GlobalKey(); bool? testnetValue; + bool obscurePassphrase = true; + + @override + void initState() { + passphraseController.text = widget.seedTypeViewModel.passphrase ?? ''; + confirmPassphraseController.text = widget.seedTypeViewModel.passphrase ?? ''; + + if (widget.isChildWallet) { + if (widget.privacySettingsViewModel.type == WalletType.bitcoin) { + widget.seedTypeViewModel.setBitcoinSeedType(BitcoinSeedType.bip39); + } + + if (widget.privacySettingsViewModel.type == WalletType.nano) { + widget.seedTypeViewModel.setNanoSeedType(NanoSeedType.bip39); + } + } + super.initState(); + } + @override Widget build(BuildContext context) { - if (testnetValue == null && widget.useTestnet != null) { + if (testnetValue == null && widget.useTestnet) { testnetValue = widget.useTestnet; } @@ -97,6 +144,111 @@ class _AdvancedPrivacySettingsBodyState extends State( + title: S.current.seedtype, + items: MoneroSeedType.all, + selectedItem: widget.seedTypeViewModel.moneroSeedType, + onItemSelected: widget.seedTypeViewModel.setMoneroSeedType, + ), + ); + }), + if (widget.privacySettingsViewModel.isBitcoinSeedTypeOptionsEnabled) + Observer(builder: (_) { + return SettingsChoicesCell( + ChoicesListItem( + title: S.current.seedtype, + items: BitcoinSeedType.all, + selectedItem: widget.seedTypeViewModel.bitcoinSeedType, + onItemSelected: (type) { + if (widget.isChildWallet && type != BitcoinSeedType.bip39) { + showAlertForSelectingNonBIP39DerivationTypeForChildWallets(); + } else { + widget.seedTypeViewModel.setBitcoinSeedType(type); + } + }, + ), + ); + }), + if (widget.privacySettingsViewModel.isNanoSeedTypeOptionsEnabled) + Observer(builder: (_) { + return SettingsChoicesCell( + ChoicesListItem( + title: S.current.seedtype, + items: NanoSeedType.all, + selectedItem: widget.seedTypeViewModel.nanoSeedType, + onItemSelected: (type) { + if (widget.isChildWallet && type != NanoSeedType.bip39) { + showAlertForSelectingNonBIP39DerivationTypeForChildWallets(); + } else { + widget.seedTypeViewModel.setNanoSeedType(type); + } + }, + ), + ); + }), + if (!widget.isFromRestore) + Observer(builder: (_) { + if (widget.privacySettingsViewModel.hasSeedPhraseLengthOption) + return SettingsPickerCell( + title: S.current.seed_phrase_length, + items: SeedPhraseLength.values, + selectedItem: widget.privacySettingsViewModel.seedPhraseLength, + onItemSelected: (SeedPhraseLength length) { + widget.privacySettingsViewModel.setSeedPhraseLength(length); + }, + ); + return Container(); + }), + if (widget.privacySettingsViewModel.hasPassphraseOption) + Padding( + padding: EdgeInsets.all(24), + child: Form( + key: _passphraseFormKey, + child: Column( + children: [ + BaseTextFormField( + hintText: S.of(context).passphrase, + controller: passphraseController, + obscureText: obscurePassphrase, + suffixIcon: GestureDetector( + onTap: () => setState(() { + obscurePassphrase = !obscurePassphrase; + }), + child: Icon( + Icons.remove_red_eye, + color: obscurePassphrase ? Colors.black54 : Colors.black26, + ), + ), + ), + const SizedBox(height: 10), + BaseTextFormField( + hintText: S.of(context).confirm_passphrase, + controller: confirmPassphraseController, + obscureText: obscurePassphrase, + validator: (text) { + if (text == passphraseController.text) { + return null; + } + + return S.of(context).passphrases_doesnt_match; + }, + suffixIcon: GestureDetector( + onTap: () => setState(() { + obscurePassphrase = !obscurePassphrase; + }), + child: Icon( + Icons.remove_red_eye, + color: obscurePassphrase ? Colors.black54 : Colors.black26, + ), + ), + ), + ], + ), + ), + ), Observer(builder: (_) { return Column( children: [ @@ -122,31 +274,9 @@ class _AdvancedPrivacySettingsBodyState extends State( - title: S.current.seed_phrase_length, - items: SeedPhraseLength.values, - selectedItem: widget.privacySettingsViewModel.seedPhraseLength, - onItemSelected: (SeedPhraseLength length) { - widget.privacySettingsViewModel.setSeedPhraseLength(length); - }, - ); - }), - if (widget.privacySettingsViewModel.hasSeedTypeOption) - Observer(builder: (_) { - return SettingsChoicesCell( - ChoicesListItem( - title: S.current.seedtype, - items: SeedType.all, - selectedItem: widget.seedTypeViewModel.moneroSeedType, - onItemSelected: widget.seedTypeViewModel.setMoneroSeedType, - ), - ); - }), if (widget.privacySettingsViewModel.type == WalletType.bitcoin) Builder(builder: (_) { - final val = testnetValue!; + final val = testnetValue ?? false; return SettingsSwitcherCell( title: S.current.use_testnet, value: val, @@ -154,7 +284,7 @@ class _AdvancedPrivacySettingsBodyState extends State( + context: context, + builder: (BuildContext context) { + return AlertWithOneAction( + alertTitle: S.current.seedtype_alert_title, + alertContent: S.current.seedtype_alert_content, + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(context).pop(), + ); + }); + } } diff --git a/lib/src/screens/new_wallet/new_wallet_page.dart b/lib/src/screens/new_wallet/new_wallet_page.dart index 471240877..929e3027a 100644 --- a/lib/src/screens/new_wallet/new_wallet_page.dart +++ b/lib/src/screens/new_wallet/new_wallet_page.dart @@ -1,36 +1,40 @@ +import 'package:cake_wallet/core/execution_state.dart'; +import 'package:cake_wallet/core/wallet_name_validator.dart'; import 'package:cake_wallet/entities/generate_name.dart'; -import 'package:cake_wallet/src/screens/new_wallet/widgets/select_button.dart'; -import 'package:cake_wallet/src/widgets/picker.dart'; -import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; +import 'package:cake_wallet/entities/seed_type.dart'; +import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/main.dart'; import 'package:cake_wallet/routes.dart'; -import 'package:cake_wallet/themes/theme_base.dart'; -import 'package:cake_wallet/utils/responsive_layout_util.dart'; -import 'package:cake_wallet/utils/show_pop_up.dart'; -import 'package:cake_wallet/view_model/seed_type_view_model.dart'; -import 'package:mobx/mobx.dart'; -import 'package:flutter_mobx/flutter_mobx.dart'; -import 'package:flutter/material.dart'; -import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/core/wallet_name_validator.dart'; -import 'package:cake_wallet/src/widgets/seed_language_selector.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/src/screens/new_wallet/widgets/select_button.dart'; +import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; +import 'package:cake_wallet/src/widgets/picker.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; import 'package:cake_wallet/src/widgets/seed_language_picker.dart'; -import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; -import 'package:cake_wallet/core/execution_state.dart'; -import 'package:cake_wallet/view_model/wallet_new_vm.dart'; +import 'package:cake_wallet/src/widgets/seed_language_selector.dart'; +import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; import 'package:cake_wallet/themes/extensions/new_wallet_theme.dart'; import 'package:cake_wallet/themes/extensions/send_page_theme.dart'; -import 'package:cake_wallet/entities/seed_type.dart'; - +import 'package:cake_wallet/themes/theme_base.dart'; +import 'package:cake_wallet/utils/responsive_layout_util.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; +import 'package:cake_wallet/view_model/seed_settings_view_model.dart'; +import 'package:cake_wallet/view_model/wallet_new_vm.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:mobx/mobx.dart'; class NewWalletPage extends BasePage { - NewWalletPage(this._walletNewVM, this._seedTypeViewModel); + NewWalletPage( + this._walletNewVM, + this._seedSettingsViewModel, { + this.isChildWallet = false, + }); final WalletNewVM _walletNewVM; - final SeedTypeViewModel _seedTypeViewModel; + final SeedSettingsViewModel _seedSettingsViewModel; + final bool isChildWallet; final walletNameImage = Image.asset('assets/images/wallet_name.png'); @@ -49,17 +53,25 @@ class NewWalletPage extends BasePage { @override Widget body(BuildContext context) => WalletNameForm( - _walletNewVM, - currentTheme.type == ThemeType.dark ? walletNameImage : walletNameLightImage, - _seedTypeViewModel); + _walletNewVM, + currentTheme.type == ThemeType.dark ? walletNameImage : walletNameLightImage, + _seedSettingsViewModel, + isChildWallet, + ); } class WalletNameForm extends StatefulWidget { - WalletNameForm(this._walletNewVM, this.walletImage, this._seedTypeViewModel); + WalletNameForm( + this._walletNewVM, + this.walletImage, + this._seedSettingsViewModel, + this.isChildWallet, + ); final WalletNewVM _walletNewVM; + final bool isChildWallet; final Image walletImage; - final SeedTypeViewModel _seedTypeViewModel; + final SeedSettingsViewModel _seedSettingsViewModel; @override _WalletNameFormState createState() => _WalletNameFormState(_walletNewVM); @@ -110,7 +122,7 @@ class _WalletNameFormState extends State { } }); - _setSeedType(SeedType.defaultSeedType); + _setSeedType(MoneroSeedType.defaultSeedType); super.initState(); } @@ -285,12 +297,12 @@ class _WalletNameFormState extends State { builder: (BuildContext build) => Padding( padding: EdgeInsets.only(top: 24), child: SelectButton( - text: widget._seedTypeViewModel.moneroSeedType.title, + text: widget._seedSettingsViewModel.moneroSeedType.title, onTap: () async { await showPopUp( context: context, builder: (_) => Picker( - items: SeedType.all, + items: MoneroSeedType.all, selectedAtIndex: isPolyseed ? 1 : 0, onItemSelected: _setSeedType, isSeparated: false, @@ -308,8 +320,8 @@ class _WalletNameFormState extends State { key: _languageSelectorKey, initialSelected: defaultSeedLanguage, seedType: _walletNewVM.hasSeedType - ? widget._seedTypeViewModel.moneroSeedType - : SeedType.legacy, + ? widget._seedSettingsViewModel.moneroSeedType + : MoneroSeedType.legacy, ), ), ) @@ -339,7 +351,8 @@ class _WalletNameFormState extends State { Navigator.of(context).pushNamed(Routes.advancedPrivacySettings, arguments: { "type": _walletNewVM.type, "useTestnet": _walletNewVM.useTestnet, - "toggleTestnet": _walletNewVM.toggleUseTestnet + "toggleTestnet": _walletNewVM.toggleUseTestnet, + "isChildWallet": widget.isChildWallet, }); }, child: Text(S.of(context).advanced_settings), @@ -380,10 +393,10 @@ class _WalletNameFormState extends State { _formProcessing = false; } - bool get isPolyseed => widget._seedTypeViewModel.moneroSeedType == SeedType.polyseed; + bool get isPolyseed => widget._seedSettingsViewModel.moneroSeedType == MoneroSeedType.polyseed; - void _setSeedType(SeedType item) { - widget._seedTypeViewModel.setMoneroSeedType(item); + void _setSeedType(MoneroSeedType item) { + widget._seedSettingsViewModel.setMoneroSeedType(item); _languageSelectorKey.currentState?.selected = defaultSeedLanguage; // Reset Seed language } } diff --git a/lib/src/screens/new_wallet/new_wallet_type_page.dart b/lib/src/screens/new_wallet/new_wallet_type_page.dart index 65c7bd59b..6cf21ae58 100644 --- a/lib/src/screens/new_wallet/new_wallet_type_page.dart +++ b/lib/src/screens/new_wallet/new_wallet_type_page.dart @@ -1,6 +1,10 @@ +import 'package:cake_wallet/core/new_wallet_arguments.dart'; +import 'package:cake_wallet/core/new_wallet_type_arguments.dart'; import 'dart:io'; import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/reactions/bip39_wallet_utils.dart'; +import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/screens/new_wallet/widgets/select_button.dart'; import 'package:cake_wallet/src/screens/setup_2fa/widgets/popup_cancellable_alert.dart'; @@ -11,6 +15,7 @@ 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:cake_wallet/utils/show_pop_up.dart'; +import 'package:cake_wallet/view_model/new_wallet_type_view_model.dart'; import 'package:cake_wallet/wallet_types.g.dart'; import 'package:cw_core/hardware/device_connection_type.dart'; import 'package:cw_core/wallet_type.dart'; @@ -18,21 +23,20 @@ import 'package:flutter/material.dart'; class NewWalletTypePage extends BasePage { NewWalletTypePage({ - required this.onTypeSelected, - required this.isCreate, - required this.isHardwareWallet, + required this.newWalletTypeViewModel, + required this.newWalletTypeArguments, }); - final void Function(BuildContext, WalletType) onTypeSelected; - final bool isCreate; - final bool isHardwareWallet; + final NewWalletTypeViewModel newWalletTypeViewModel; + final NewWalletTypeArguments newWalletTypeArguments; final walletTypeImage = Image.asset('assets/images/wallet_type.png'); final walletTypeLightImage = Image.asset('assets/images/wallet_type_light.png'); @override - String get title => - isCreate ? S.current.wallet_list_create_new_wallet : S.current.wallet_list_restore_wallet; + String get title => newWalletTypeArguments.isCreate + ? S.current.wallet_list_create_new_wallet + : S.current.wallet_list_restore_wallet; @override Function(BuildContext)? get pushToNextWidget => (context) { @@ -44,24 +48,27 @@ class NewWalletTypePage extends BasePage { @override Widget body(BuildContext context) => WalletTypeForm( - onTypeSelected: onTypeSelected, walletImage: currentTheme.type == ThemeType.dark ? walletTypeImage : walletTypeLightImage, - isCreate: isCreate, - isHardwareWallet: isHardwareWallet, + isCreate: newWalletTypeArguments.isCreate, + newWalletTypeViewModel: newWalletTypeViewModel, + onTypeSelected: newWalletTypeArguments.onTypeSelected, + isHardwareWallet: newWalletTypeArguments.isHardwareWallet, ); } class WalletTypeForm extends StatefulWidget { WalletTypeForm({ - required this.onTypeSelected, required this.walletImage, required this.isCreate, + required this.newWalletTypeViewModel, + this.onTypeSelected, required this.isHardwareWallet, }); - final void Function(BuildContext, WalletType) onTypeSelected; - final Image walletImage; final bool isCreate; + final Image walletImage; + final NewWalletTypeViewModel newWalletTypeViewModel; + final void Function(BuildContext, WalletType)? onTypeSelected; final bool isHardwareWallet; @override @@ -124,6 +131,7 @@ class WalletTypeFormState extends State { Expanded( child: ScrollableWithBottomSection( contentPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24), + scrollableKey: ValueKey('new_wallet_type_scrollable_key'), content: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ @@ -131,6 +139,7 @@ class WalletTypeFormState extends State { (type) => Padding( padding: EdgeInsets.only(top: 12), child: SelectButton( + key: ValueKey('new_wallet_type_${type.name}_button_key'), image: Image.asset( walletTypeToCryptoCurrency(type).iconPath ?? '', height: 24, @@ -151,6 +160,7 @@ class WalletTypeFormState extends State { ), bottomSectionPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24), bottomSection: PrimaryButton( + key: ValueKey('new_wallet_type_next_button_key'), onPressed: () => onTypeSelected(), text: S.of(context).seed_language_next, color: Theme.of(context).primaryColor, @@ -179,6 +189,18 @@ class WalletTypeFormState extends State { ); } - widget.onTypeSelected(context, selected!); + // If it's a restore flow, trigger the external callback + // If it's not a BIP39 Wallet or if there are no other wallets, route to the newWallet page + // Any other scenario, route to pre-existing seed page + if (!widget.isCreate) { + widget.onTypeSelected!(context, selected!); + } else if (!isBIP39Wallet(selected!) || !widget.newWalletTypeViewModel.hasExisitingWallet) { + Navigator.of(context).pushNamed( + Routes.newWallet, + arguments: NewWalletArguments(type: selected!), + ); + } else { + Navigator.of(context).pushNamed(Routes.walletGroupDescription, arguments: selected!); + } } } diff --git a/lib/src/screens/new_wallet/wallet_group_description_page.dart b/lib/src/screens/new_wallet/wallet_group_description_page.dart new file mode 100644 index 000000000..d0936b640 --- /dev/null +++ b/lib/src/screens/new_wallet/wallet_group_description_page.dart @@ -0,0 +1,107 @@ +import 'package:cake_wallet/core/new_wallet_arguments.dart'; +import 'package:cake_wallet/src/widgets/primary_button.dart'; +import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:flutter/material.dart'; +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'; + +class WalletGroupDescriptionPage extends BasePage { + WalletGroupDescriptionPage({required this.selectedWalletType}); + + final WalletType selectedWalletType; + + @override + String get title => S.current.wallet_group; + + + @override + Widget body(BuildContext context) { + + return Container( + alignment: Alignment.center, + padding: EdgeInsets.all(24), + child: Column( + children: [ + Image.asset( + _getThemedWalletGroupImage(currentTheme.type), + height: 200, + ), + SizedBox(height: 32), + Expanded( + child: Text.rich( + TextSpan( + children: [ + TextSpan(text: '${S.of(context).wallet_group_description_one} '), + TextSpan( + text: '${S.of(context).wallet_group.toLowerCase()} ', + style: TextStyle(fontWeight: FontWeight.w700), + ), + TextSpan( + text: '${S.of(context).wallet_group_description_two} ', + ), + TextSpan( + text: '${S.of(context).choose_wallet_group} ', + style: TextStyle(fontWeight: FontWeight.w700), + ), + TextSpan( + text: '${S.of(context).wallet_group_description_three} ', + ), + TextSpan( + text: '${S.of(context).create_new_seed} ', + style: TextStyle(fontWeight: FontWeight.w700), + ), + TextSpan(text: S.of(context).wallet_group_description_four), + ], + ), + textAlign: TextAlign.center, + style: TextStyle( + height: 1.5, + fontSize: 16, + fontWeight: FontWeight.w400, + color: Theme.of(context).extension()!.secondaryTextColor, + ), + ), + ), + PrimaryButton( + onPressed: () => Navigator.of(context).pushNamed( + Routes.newWallet, + arguments: NewWalletArguments(type: selectedWalletType), + ), + text: S.of(context).create_new_seed, + color: Theme.of(context).cardColor, + textColor: Theme.of(context).extension()!.titleColor, + ), + SizedBox(height: 12), + PrimaryButton( + onPressed: () => Navigator.of(context).pushNamed( + Routes.walletGroupsDisplayPage, + arguments: selectedWalletType, + ), + text: S.of(context).choose_wallet_group, + color: Theme.of(context).primaryColor, + textColor: Colors.white, + ), + SizedBox(height: 32), + ], + ), + ); + } + + 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 new file mode 100644 index 000000000..a99d2bac7 --- /dev/null +++ b/lib/src/screens/new_wallet/wallet_group_display_page.dart @@ -0,0 +1,212 @@ +import 'package:cake_wallet/core/new_wallet_arguments.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/src/screens/new_wallet/widgets/grouped_wallet_expansion_tile.dart'; +import 'package:cake_wallet/src/widgets/primary_button.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); + + final WalletGroupsDisplayViewModel walletGroupsDisplayViewModel; + + @override + String get title => S.current.wallet_group; + + @override + Widget body(BuildContext context) => WalletGroupsDisplayBody( + walletGroupsDisplayViewModel: walletGroupsDisplayViewModel, + currentTheme: currentTheme, + ); +} + +class WalletGroupsDisplayBody extends StatelessWidget { + WalletGroupsDisplayBody({ + required this.walletGroupsDisplayViewModel, + required this.currentTheme, + }); + + final WalletGroupsDisplayViewModel walletGroupsDisplayViewModel; + final ThemeBase currentTheme; + + @override + Widget build(BuildContext context) { + return Center( + child: Padding( + padding: EdgeInsets.all(24), + child: Column( + children: [ + Expanded( + child: SingleChildScrollView( + child: Observer( + builder: (context) { + return Column( + children: [ + if (walletGroupsDisplayViewModel.hasNoFilteredWallet) ...{ + WalletGroupEmptyStateWidget( + currentTheme: currentTheme, + ), + }, + ...walletGroupsDisplayViewModel.multiWalletGroups.map( + (walletGroup) { + return Observer(builder: (context) { + final index = walletGroupsDisplayViewModel.multiWalletGroups + .indexOf(walletGroup); + final group = walletGroupsDisplayViewModel.multiWalletGroups[index]; + final groupName = + group.groupName ?? '${S.of(context).wallet_group} ${index + 1}'; + return GroupedWalletExpansionTile( + shouldShowCurrentWalletPointer: false, + leadingWidget: + Icon(Icons.account_balance_wallet_outlined, size: 28), + borderRadius: BorderRadius.all(Radius.circular(16)), + title: groupName, + childWallets: group.wallets.map((walletInfo) { + return walletGroupsDisplayViewModel + .convertWalletInfoToWalletListItem(walletInfo); + }).toList(), + isSelected: + walletGroupsDisplayViewModel.selectedWalletGroup == group, + onTitleTapped: () => + walletGroupsDisplayViewModel.selectWalletGroup(group), + onChildItemTapped: (_) => + walletGroupsDisplayViewModel.selectWalletGroup(group), + ); + }); + }, + ).toList(), + ...walletGroupsDisplayViewModel.singleWalletsList.map((singleWallet) { + return Observer( + builder: (context) { + final index = walletGroupsDisplayViewModel.singleWalletsList + .indexOf(singleWallet); + final wallet = walletGroupsDisplayViewModel.singleWalletsList[index]; + return GroupedWalletExpansionTile( + borderRadius: BorderRadius.all(Radius.circular(16)), + title: wallet.name, + isSelected: + walletGroupsDisplayViewModel.selectedSingleWallet == wallet, + leadingWidget: Image.asset( + walletTypeToCryptoCurrency(wallet.type).iconPath!, + width: 32, + height: 32, + ), + onTitleTapped: () => + walletGroupsDisplayViewModel.selectSingleWallet(wallet), + ); + }, + ); + }).toList(), + ], + ); + }, + ), + ), + ), + Observer( + builder: (context) { + return LoadingPrimaryButton( + isLoading: walletGroupsDisplayViewModel.isFetchingMnemonic, + onPressed: () { + if (walletGroupsDisplayViewModel.hasNoFilteredWallet) { + Navigator.of(context).pushNamed( + Routes.newWallet, + arguments: NewWalletArguments(type: walletGroupsDisplayViewModel.type), + ); + } else { + onTypeSelected(context); + } + }, + text: walletGroupsDisplayViewModel.hasNoFilteredWallet + ? S.of(context).create_new_seed + : S.of(context).seed_language_next, + color: Theme.of(context).primaryColor, + textColor: Colors.white, + isDisabled: !walletGroupsDisplayViewModel.hasNoFilteredWallet + ? (walletGroupsDisplayViewModel.selectedWalletGroup == null && + walletGroupsDisplayViewModel.selectedSingleWallet == null) + : false, + ); + }, + ), + SizedBox(height: 32), + ], + ), + ), + ); + } + + Future onTypeSelected(BuildContext context) async { + final mnemonic = await walletGroupsDisplayViewModel.getSelectedWalletMnemonic(); + Navigator.of(context).pushNamed( + Routes.newWallet, + arguments: NewWalletArguments( + type: walletGroupsDisplayViewModel.type, + mnemonic: mnemonic, + parentAddress: walletGroupsDisplayViewModel.parentAddress, + isChildWallet: true, + ), + ); + } +} + +class WalletGroupEmptyStateWidget extends StatelessWidget { + const WalletGroupEmptyStateWidget({required this.currentTheme, super.key}); + + final ThemeBase currentTheme; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Image.asset( + _getThemedWalletGroupImage(currentTheme.type), + scale: 1.8, + ), + SizedBox(height: 32), + Text.rich( + TextSpan( + children: [ + TextSpan( + text: '${S.of(context).wallet_group_empty_state_text_one} ', + ), + TextSpan( + text: '${S.of(context).create_new_seed} ', + style: TextStyle(fontWeight: FontWeight.w700), + ), + TextSpan(text: S.of(context).wallet_group_empty_state_text_two), + ], + ), + textAlign: TextAlign.center, + style: TextStyle( + height: 1.5, + fontSize: 16, + fontWeight: FontWeight.w400, + color: Theme.of(context).extension()!.secondaryTextColor, + ), + ), + ], + ); + } + + 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/widgets/grouped_wallet_expansion_tile.dart b/lib/src/screens/new_wallet/widgets/grouped_wallet_expansion_tile.dart new file mode 100644 index 000000000..74057cc87 --- /dev/null +++ b/lib/src/screens/new_wallet/widgets/grouped_wallet_expansion_tile.dart @@ -0,0 +1,157 @@ +import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; +import 'package:cake_wallet/themes/extensions/filter_theme.dart'; +import 'package:cake_wallet/themes/extensions/wallet_list_theme.dart'; +import 'package:cake_wallet/view_model/wallet_list/wallet_list_item.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:flutter/material.dart'; + +class GroupedWalletExpansionTile extends StatelessWidget { + GroupedWalletExpansionTile({ + required this.title, + required this.isSelected, + this.childWallets = const [], + this.onTitleTapped, + this.onChildItemTapped = _defaultVoidCallback, + this.onExpansionChanged, + this.leadingWidget, + this.trailingWidget, + this.childTrailingWidget, + this.decoration, + this.color, + this.textColor, + this.arrowColor, + this.borderRadius, + this.margin, + this.tileKey, + this.isCurrentlySelectedWallet = false, + this.shouldShowCurrentWalletPointer = false, + }) : super(key: tileKey); + + final Key? tileKey; + final bool isSelected; + final bool isCurrentlySelectedWallet; + final bool shouldShowCurrentWalletPointer; + + final VoidCallback? onTitleTapped; + final void Function(WalletListItem item) onChildItemTapped; + final void Function(bool)? onExpansionChanged; + + final String title; + final Widget? leadingWidget; + final Widget? trailingWidget; + final Widget Function(WalletListItem)? childTrailingWidget; + + final List childWallets; + + final Color? color; + final Color? textColor; + final Color? arrowColor; + final EdgeInsets? margin; + final Decoration? decoration; + final BorderRadius? borderRadius; + + static void _defaultVoidCallback(WalletListItem ITEM) {} + + @override + Widget build(BuildContext context) { + final backgroundColor = color ?? (isSelected ? Colors.green : Theme.of(context).cardColor); + final effectiveTextColor = textColor ?? + (isSelected + ? Theme.of(context).extension()!.restoreWalletButtonTextColor + : Theme.of(context).extension()!.buttonTextColor); + + final effectiveArrowColor = arrowColor ?? + (isSelected + ? Theme.of(context).extension()!.restoreWalletButtonTextColor + : Theme.of(context).extension()!.titlesColor); + return Container( + decoration: BoxDecoration( + borderRadius: borderRadius ?? BorderRadius.all(Radius.circular(30)), + color: backgroundColor, + ), + margin: margin ?? const EdgeInsets.only(bottom: 12.0), + child: Theme( + data: Theme.of(context).copyWith( + dividerColor: Colors.transparent, + splashFactory: NoSplash.splashFactory, + ), + child: ExpansionTile( + onExpansionChanged: onExpansionChanged, + initiallyExpanded: shouldShowCurrentWalletPointer + ? childWallets.any((element) => element.isCurrent) + : false, + key: tileKey, + tilePadding: + EdgeInsets.symmetric(vertical: 1, horizontal: !isCurrentlySelectedWallet ? 16 : 0), + iconColor: effectiveArrowColor, + collapsedIconColor: effectiveArrowColor, + leading: leadingWidget, + trailing: trailingWidget ?? (childWallets.isEmpty ? SizedBox.shrink() : null), + title: GestureDetector( + onTap: onTitleTapped, + child: Text( + title, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w500, + color: effectiveTextColor, + ), + textAlign: TextAlign.left, + ), + ), + children: childWallets.map( + (item) { + final currentColor = item.isCurrent + ? Theme.of(context) + .extension()! + .createNewWalletButtonBackgroundColor + : Theme.of(context).colorScheme.background; + final walletTypeToCrypto = walletTypeToCryptoCurrency(item.type); + return ListTile( + contentPadding: EdgeInsets.zero, + key: ValueKey(item.name), + trailing: childTrailingWidget?.call(item), + onTap: () => onChildItemTapped(item), + leading: SizedBox( + width: 60, + child: Row( + children: [ + item.isCurrent && shouldShowCurrentWalletPointer + ? Container( + height: 35, + width: 6, + decoration: BoxDecoration( + borderRadius: BorderRadius.only( + topRight: Radius.circular(16), + bottomRight: Radius.circular(16), + ), + color: currentColor, + ), + ) + : SizedBox(width: 6), + SizedBox(width: 16), + Image.asset( + walletTypeToCrypto.iconPath!, + width: 32, + height: 32, + ), + ], + ), + ), + title: Text( + item.name, + maxLines: 2, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w500, + color: effectiveTextColor, + ), + ), + ); + }, + ).toList(), + ), + ), + ); + } +} diff --git a/lib/src/screens/new_wallet/widgets/select_button.dart b/lib/src/screens/new_wallet/widgets/select_button.dart index 2eda77d01..87015b89e 100644 --- a/lib/src/screens/new_wallet/widgets/select_button.dart +++ b/lib/src/screens/new_wallet/widgets/select_button.dart @@ -18,9 +18,12 @@ class SelectButton extends StatelessWidget { this.arrowColor, this.borderColor, this.deviceConnectionTypes, + this.borderRadius, + this.padding, + super.key, }); - final Image? image; + final Widget? image; final String text; final double textSize; final bool isSelected; @@ -32,6 +35,8 @@ class SelectButton extends StatelessWidget { final Color? textColor; final Color? arrowColor; final Color? borderColor; + final BorderRadius? borderRadius; + final EdgeInsets? padding; @override Widget build(BuildContext context) { @@ -62,10 +67,10 @@ class SelectButton extends StatelessWidget { child: Container( width: double.infinity, height: height, - padding: EdgeInsets.only(left: 30, right: 30), + padding: padding ?? EdgeInsets.only(left: 30, right: 30), alignment: Alignment.center, decoration: BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(30)), + borderRadius: borderRadius ?? BorderRadius.all(Radius.circular(30)), color: backgroundColor, border: borderColor != null ? Border.all(color: borderColor!) : null, ), diff --git a/lib/src/screens/pin_code/pin_code_widget.dart b/lib/src/screens/pin_code/pin_code_widget.dart index 36328aee2..d39c88cc9 100644 --- a/lib/src/screens/pin_code/pin_code_widget.dart +++ b/lib/src/screens/pin_code/pin_code_widget.dart @@ -240,6 +240,7 @@ class PinCodeState extends State { return Container( margin: EdgeInsets.only(left: marginLeft, right: marginRight), child: TextButton( + key: ValueKey('pin_code_button_${index}_key'), onPressed: () => _push(index), style: TextButton.styleFrom( backgroundColor: Theme.of(context).colorScheme.background, diff --git a/lib/src/screens/receive/receive_page.dart b/lib/src/screens/receive/receive_page.dart index 724e5c3bd..7e3c2b555 100644 --- a/lib/src/screens/receive/receive_page.dart +++ b/lib/src/screens/receive/receive_page.dart @@ -121,7 +121,8 @@ class ReceivePage extends BasePage { heroTag: _heroTag, amountTextFieldFocusNode: _cryptoAmountFocus, amountController: _amountController, - isLight: currentTheme.type == ThemeType.light), + isLight: currentTheme.type == ThemeType.light, + ), ), AddressList(addressListViewModel: addressListViewModel), Padding( diff --git a/lib/src/screens/receive/widgets/address_cell.dart b/lib/src/screens/receive/widgets/address_cell.dart index 850c08209..beef7c762 100644 --- a/lib/src/screens/receive/widgets/address_cell.dart +++ b/lib/src/screens/receive/widgets/address_cell.dart @@ -2,6 +2,7 @@ import 'package:auto_size_text/auto_size_text.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; @@ -15,11 +16,14 @@ class AddressCell extends StatelessWidget { required this.textColor, this.onTap, this.onEdit, + this.onHide, + this.isHidden = false, this.onDelete, this.txCount, this.balance, this.isChange = false, - this.hasBalance = false}); + this.hasBalance = false, + this.hasReceived = false}); factory AddressCell.fromItem( WalletAddressListItem item, { @@ -28,7 +32,10 @@ class AddressCell extends StatelessWidget { required Color textColor, Function(String)? onTap, bool hasBalance = false, + bool hasReceived = false, Function()? onEdit, + Function()? onHide, + bool isHidden = false, Function()? onDelete, }) => AddressCell( @@ -40,11 +47,14 @@ class AddressCell extends StatelessWidget { textColor: textColor, onTap: onTap, onEdit: onEdit, + onHide: onHide, + isHidden: isHidden, onDelete: onDelete, txCount: item.txCount, balance: item.balance, isChange: item.isChange, - hasBalance: hasBalance); + hasBalance: hasBalance, + hasReceived: hasReceived,); final String address; final String name; @@ -54,11 +64,14 @@ class AddressCell extends StatelessWidget { final Color textColor; final Function(String)? onTap; final Function()? onEdit; + final Function()? onHide; + final bool isHidden; final Function()? onDelete; final int? txCount; final String? balance; final bool isChange; final bool hasBalance; + final bool hasReceived; static const int addressPreviewLength = 8; @@ -138,7 +151,7 @@ class AddressCell extends StatelessWidget { ), ], ), - if (hasBalance) + if (hasBalance || hasReceived) Padding( padding: const EdgeInsets.only(top: 8.0), child: Row( @@ -146,7 +159,7 @@ class AddressCell extends StatelessWidget { mainAxisSize: MainAxisSize.max, children: [ Text( - '${S.of(context).balance}: $balance', + '${hasReceived ? S.of(context).received : S.of(context).balance}: $balance', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, @@ -178,14 +191,28 @@ class AddressCell extends StatelessWidget { enabled: !isCurrent, child: Slidable( key: Key(address), - startActionPane: _actionPane(context), - endActionPane: _actionPane(context), + startActionPane: _actionPaneStart(context), + endActionPane: _actionPaneEnd(context), child: cell, ), ); } - ActionPane _actionPane(BuildContext context) => ActionPane( + ActionPane _actionPaneEnd(BuildContext context) => ActionPane( + motion: const ScrollMotion(), + extentRatio: onDelete != null ? 0.4 : 0.3, + children: [ + SlidableAction( + onPressed: (_) => onHide?.call(), + backgroundColor: isHidden ? Colors.green : Colors.red, + foregroundColor: Colors.white, + icon: isHidden ? CupertinoIcons.arrow_left : CupertinoIcons.arrow_right, + label: isHidden ? S.of(context).show : S.of(context).hide, + ), + ], + ); + + ActionPane _actionPaneStart(BuildContext context) => ActionPane( motion: const ScrollMotion(), extentRatio: onDelete != null ? 0.4 : 0.3, children: [ diff --git a/lib/src/screens/receive/widgets/address_list.dart b/lib/src/screens/receive/widgets/address_list.dart index 8dfbedec1..9f15018d0 100644 --- a/lib/src/screens/receive/widgets/address_list.dart +++ b/lib/src/screens/receive/widgets/address_list.dart @@ -1,3 +1,6 @@ + +import 'dart:math'; + import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/routes.dart'; @@ -7,16 +10,19 @@ import 'package:cake_wallet/src/screens/receive/widgets/address_cell.dart'; import 'package:cake_wallet/src/screens/receive/widgets/header_tile.dart'; import 'package:cake_wallet/src/widgets/section_divider.dart'; import 'package:cake_wallet/themes/extensions/receive_page_theme.dart'; +import 'package:cake_wallet/utils/list_item.dart'; import 'package:cake_wallet/utils/show_pop_up.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'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_header.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart'; import 'package:cw_core/wallet_type.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; -class AddressList extends StatelessWidget { +class AddressList extends StatefulWidget { const AddressList({ super.key, required this.addressListViewModel, @@ -26,59 +32,123 @@ class AddressList extends StatelessWidget { final WalletAddressListViewModel addressListViewModel; final Function(String)? onSelect; + @override + State createState() => _AddressListState(); +} + +class _AddressListState extends State { + + bool showHiddenAddresses = false; + + void _toggleHiddenAddresses() { + setState(() { + showHiddenAddresses = !showHiddenAddresses; + }); + updateItems(); + } + + List getItems(List list, bool showHidden) { + return list.where((element) { + if (element is WalletAddressListItem) { + if (showHidden && element.isHidden) return true; + if (!showHidden && !element.isHidden) return true; + return false; + } + return true; + }).toList(); + } + + List items = []; + + void updateItems() { + setState(() { + items = getItems(widget.addressListViewModel.items, showHiddenAddresses); + }); + } + + @override + void initState() { + super.initState(); + + items = getItems(widget.addressListViewModel.items, showHiddenAddresses); + } + @override Widget build(BuildContext context) { - bool editable = onSelect == null; - return Observer( - builder: (_) => ListView.separated( - padding: EdgeInsets.all(0), - separatorBuilder: (context, _) => const HorizontalSectionDivider(), - shrinkWrap: true, - physics: NeverScrollableScrollPhysics(), - itemCount: addressListViewModel.items.length, - itemBuilder: (context, index) { - final item = addressListViewModel.items[index]; - Widget cell = Container(); + bool editable = widget.onSelect == null; + return ListView.separated( + padding: EdgeInsets.all(0), + separatorBuilder: (context, _) => const HorizontalSectionDivider(), + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + itemCount: items.length, + itemBuilder: (context, index) { + final item = items[index]; + Widget cell = Container(); - if (item is WalletAccountListHeader) { - cell = HeaderTile( - showTrailingButton: true, - walletAddressListViewModel: addressListViewModel, - trailingButtonTap: () async { - if (addressListViewModel.type == WalletType.monero || - addressListViewModel.type == WalletType.haven) { - await showPopUp( - context: context, builder: (_) => getIt.get()); - } else { - await showPopUp( - context: context, builder: (_) => getIt.get()); - } - }, - title: S.of(context).accounts, - trailingIcon: Icon( - Icons.arrow_forward_ios, - size: 14, - color: Theme.of(context).extension()!.iconsColor, - )); - } + if (item is WalletAccountListHeader) { + cell = HeaderTile( + showTrailingButton: true, + walletAddressListViewModel: widget.addressListViewModel, + trailingButtonTap: () async { + if (widget.addressListViewModel.type == WalletType.monero || + widget.addressListViewModel.type == WalletType.haven) { + await showPopUp( + context: context, builder: (_) => getIt.get()); + updateItems(); + } else { + await showPopUp( + context: context, builder: (_) => getIt.get()); + updateItems(); + } + }, + title: S.of(context).accounts, + trailingIcon: Icon( + Icons.arrow_forward_ios, + size: 14, + color: Theme.of(context).extension()!.iconsColor, + )); + } - if (item is WalletAddressListHeader) { - cell = HeaderTile( - title: S.of(context).addresses, - walletAddressListViewModel: addressListViewModel, - showTrailingButton: !addressListViewModel.isAutoGenerateSubaddressEnabled, - showSearchButton: true, - trailingButtonTap: () => Navigator.of(context).pushNamed(Routes.newSubaddress), - trailingIcon: Icon( - Icons.add, - size: 20, - color: Theme.of(context).extension()!.iconsColor, - )); - } + if (item is WalletAddressHiddenListHeader) { + cell = HeaderTile( + title: S.of(context).hidden_addresses, + walletAddressListViewModel: widget.addressListViewModel, + showTrailingButton: true, + showSearchButton: false, + trailingButtonTap: _toggleHiddenAddresses, + trailingIcon: Icon( + showHiddenAddresses ? Icons.toggle_on : Icons.toggle_off, + size: 20, + color: Theme.of(context).extension()!.iconsColor, + )); + } - if (item is WalletAddressListItem) { + if (item is WalletAddressListHeader) { + cell = HeaderTile( + title: S.of(context).addresses, + walletAddressListViewModel: widget.addressListViewModel, + showTrailingButton: widget.addressListViewModel.showAddManualAddresses, + showSearchButton: true, + onSearchCallback: updateItems, + trailingButtonTap: () => Navigator.of(context).pushNamed(Routes.newSubaddress).then((value) { + updateItems(); // refresh the new address + }), + trailingIcon: Icon( + Icons.add, + size: 20, + color: Theme.of(context).extension()!.iconsColor, + )); + } + + if (item is WalletAddressListItem) { + if (item.isHidden && !showHiddenAddresses) { + cell = Container(); + } else if (!item.isHidden && showHiddenAddresses) { + cell = Container(); + } else { cell = Observer(builder: (_) { - final isCurrent = item.address == addressListViewModel.address.address && editable; + final isCurrent = item.address == widget.addressListViewModel.address.address && editable; final backgroundColor = isCurrent ? Theme.of(context).extension()!.currentTileBackgroundColor : Theme.of(context).extension()!.tilesBackgroundColor; @@ -86,35 +156,51 @@ class AddressList extends StatelessWidget { ? Theme.of(context).extension()!.currentTileTextColor : Theme.of(context).extension()!.tilesTextColor; + return AddressCell.fromItem( item, isCurrent: isCurrent, - hasBalance: addressListViewModel.isElectrumWallet, - backgroundColor: backgroundColor, + hasBalance: widget.addressListViewModel.isBalanceAvailable, + hasReceived: widget.addressListViewModel.isReceivedAvailable, + // hasReceived: + backgroundColor: (kDebugMode && item.isHidden) ? + Theme.of(context).colorScheme.error : + (kDebugMode && item.isManual) ? Theme.of(context).colorScheme.error.withBlue(255) : + backgroundColor, textColor: textColor, onTap: (_) { - if (onSelect != null) { - onSelect!(item.address); + if (widget.onSelect != null) { + widget.onSelect!(item.address); return; } - addressListViewModel.setAddress(item); + widget.addressListViewModel.setAddress(item); }, onEdit: editable - ? () => Navigator.of(context).pushNamed(Routes.newSubaddress, arguments: item) + ? () => Navigator.of(context).pushNamed(Routes.newSubaddress, arguments: item).then((value) { + updateItems(); // refresh the new address + }) : null, + isHidden: item.isHidden, + onHide: () => _hideAddress(item), ); }); } + } - return index != 0 - ? cell - : ClipRRect( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(30), topRight: Radius.circular(30)), - child: cell, - ); - }, - ), + return index != 0 + ? cell + : ClipRRect( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(30), topRight: Radius.circular(30)), + child: cell, + ); + }, ); } + + void _hideAddress(WalletAddressListItem item) async { + await widget.addressListViewModel.toggleHideAddress(item); + updateItems(); + } + } diff --git a/lib/src/screens/receive/widgets/currency_input_field.dart b/lib/src/screens/receive/widgets/currency_input_field.dart index ce3de9a6c..382c53f9d 100644 --- a/lib/src/screens/receive/widgets/currency_input_field.dart +++ b/lib/src/screens/receive/widgets/currency_input_field.dart @@ -24,8 +24,20 @@ class CurrencyAmountTextField extends StatelessWidget { this.tagBackgroundColor, this.currencyValueValidator, this.allAmountCallback, - }); + this.sendAllButtonKey, + this.amountTextfieldKey, + this.currencyPickerButtonKey, + this.selectedCurrencyTextKey, + this.selectedCurrencyTagTextKey, + this.currencyAmountTextFieldWidgetKey, + }) : super(key: currencyAmountTextFieldWidgetKey); + final Key? sendAllButtonKey; + final Key? amountTextfieldKey; + final Key? currencyPickerButtonKey; + final Key? selectedCurrencyTextKey; + final Key? selectedCurrencyTagTextKey; + final Key? currencyAmountTextFieldWidgetKey; final Widget? imageArrow; final String selectedCurrency; final String? tag; @@ -54,6 +66,7 @@ class CurrencyAmountTextField extends StatelessWidget { ? Container( height: 32, child: InkWell( + key: currencyPickerButtonKey, onTap: onTapPicker, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -65,6 +78,7 @@ class CurrencyAmountTextField extends StatelessWidget { Image.asset('assets/images/arrow_bottom_purple_icon.png', color: textColor, height: 8)), Text( + key: selectedCurrencyTextKey, selectedCurrency, style: TextStyle( fontWeight: FontWeight.w600, @@ -77,6 +91,7 @@ class CurrencyAmountTextField extends StatelessWidget { ), ) : Text( + key: selectedCurrencyTextKey, selectedCurrency, style: TextStyle( fontWeight: FontWeight.w600, @@ -98,6 +113,7 @@ class CurrencyAmountTextField extends StatelessWidget { child: Padding( padding: const EdgeInsets.all(6.0), child: Text( + key: selectedCurrencyTagTextKey, tag!, style: TextStyle( fontSize: 12, @@ -132,9 +148,9 @@ class CurrencyAmountTextField extends StatelessWidget { padding: EdgeInsets.symmetric(vertical: 4, horizontal: 8), margin: const EdgeInsets.only(right: 3), decoration: BoxDecoration( - border: Border.all( - color: textColor, - ), + border: Border.all( + color: textColor, + ), borderRadius: BorderRadius.circular(26), color: Theme.of(context).primaryColor)) : _prefixContent, @@ -146,6 +162,7 @@ class CurrencyAmountTextField extends StatelessWidget { child: FocusTraversalOrder( order: NumericFocusOrder(1), child: BaseTextFormField( + key: amountTextfieldKey, focusNode: amountFocusNode, controller: amountController, enabled: isAmountEditable, @@ -184,6 +201,7 @@ class CurrencyAmountTextField extends StatelessWidget { borderRadius: const BorderRadius.all(Radius.circular(6)), ), child: InkWell( + key: sendAllButtonKey, onTap: allAmountCallback, child: Center( child: Text( diff --git a/lib/src/screens/receive/widgets/header_tile.dart b/lib/src/screens/receive/widgets/header_tile.dart index faaa9ed07..dc48db89d 100644 --- a/lib/src/screens/receive/widgets/header_tile.dart +++ b/lib/src/screens/receive/widgets/header_tile.dart @@ -10,6 +10,7 @@ class HeaderTile extends StatefulWidget { this.showSearchButton = false, this.showTrailingButton = false, this.trailingButtonTap, + this.onSearchCallback, this.trailingIcon, }); @@ -18,6 +19,7 @@ class HeaderTile extends StatefulWidget { final bool showSearchButton; final bool showTrailingButton; final VoidCallback? trailingButtonTap; + final VoidCallback? onSearchCallback; final Icon? trailingIcon; @override @@ -41,7 +43,10 @@ class _HeaderTileState extends State { _isSearchActive ? Expanded( child: TextField( - onChanged: (value) => widget.walletAddressListViewModel.updateSearchText(value), + onChanged: (value) { + widget.walletAddressListViewModel.updateSearchText(value); + widget.onSearchCallback?.call(); + }, cursorColor: Theme.of(context).extension()!.tilesTextColor, cursorWidth: 0.5, decoration: InputDecoration( diff --git a/lib/src/screens/receive/widgets/qr_image.dart b/lib/src/screens/receive/widgets/qr_image.dart index f388fdd0b..043958ef4 100644 --- a/lib/src/screens/receive/widgets/qr_image.dart +++ b/lib/src/screens/receive/widgets/qr_image.dart @@ -23,7 +23,7 @@ class QrImage extends StatelessWidget { return qr.QrImageView( data: data, errorCorrectionLevel: errorCorrectionLevel, - version: version ?? 9, // Previous value: 7 something happened after flutter upgrade monero wallets addresses are longer than ver. 7 ??? + version: version ?? qr.QrVersions.auto, size: size, foregroundColor: foregroundColor, backgroundColor: backgroundColor, diff --git a/lib/src/screens/receive/widgets/qr_widget.dart b/lib/src/screens/receive/widgets/qr_widget.dart index 9f0db059a..9d09e57a1 100644 --- a/lib/src/screens/receive/widgets/qr_widget.dart +++ b/lib/src/screens/receive/widgets/qr_widget.dart @@ -37,6 +37,10 @@ class QRWidget extends StatelessWidget { final int? qrVersion; final String? heroTag; + PaymentURI get addressUri { + return addressListViewModel.uri; + } + @override Widget build(BuildContext context) { final copyImage = Image.asset('assets/images/copy_address.png', @@ -77,14 +81,14 @@ class QRWidget extends StatelessWidget { () async { await Navigator.pushNamed(context, Routes.fullscreenQR, arguments: QrViewData( - data: addressListViewModel.uri.toString(), + data: addressUri.toString(), heroTag: heroTag, )); }, ); }, child: Hero( - tag: Key(heroTag ?? addressListViewModel.uri.toString()), + tag: Key(heroTag ?? addressUri.toString()), child: Center( child: AspectRatio( aspectRatio: 1.0, @@ -105,7 +109,7 @@ class QRWidget extends StatelessWidget { color: Colors.white, ), ), - child: QrImage(data: addressListViewModel.uri.toString())), + child: QrImage(data: addressUri.toString())), ), ), ), @@ -148,7 +152,7 @@ class QRWidget extends StatelessWidget { builder: (context) => Observer( builder: (context) => GestureDetector( onTap: () { - Clipboard.setData(ClipboardData(text: addressListViewModel.address.address)); + Clipboard.setData(ClipboardData(text: addressUri.address)); showBar(context, S.of(context).copied_to_clipboard); }, child: Row( @@ -157,7 +161,7 @@ class QRWidget extends StatelessWidget { children: [ Expanded( child: Text( - addressListViewModel.address.address, + addressUri.address, textAlign: TextAlign.center, style: TextStyle( fontSize: 15, diff --git a/lib/src/screens/rescan/rescan_page.dart b/lib/src/screens/rescan/rescan_page.dart index b66c94878..2c1c213c1 100644 --- a/lib/src/screens/rescan/rescan_page.dart +++ b/lib/src/screens/rescan/rescan_page.dart @@ -33,10 +33,13 @@ class RescanPage extends BasePage { key: _blockchainHeightWidgetKey, onHeightOrDateEntered: (value) => _rescanViewModel.isButtonEnabled = value, isSilentPaymentsScan: _rescanViewModel.isSilentPaymentsScan, + isMwebScan: _rescanViewModel.isMwebScan, doSingleScan: _rescanViewModel.doSingleScan, + hasDatePicker: !_rescanViewModel.isMwebScan,// disable date picker for mweb for now toggleSingleScan: () => _rescanViewModel.doSingleScan = !_rescanViewModel.doSingleScan, walletType: _rescanViewModel.wallet.type, + bitcoinMempoolAPIEnabled: _rescanViewModel.isBitcoinMempoolAPIEnabled, )), Observer( builder: (_) => LoadingPrimaryButton( diff --git a/lib/src/screens/restore/restore_options_page.dart b/lib/src/screens/restore/restore_options_page.dart index a703c9f9e..57f5ec727 100644 --- a/lib/src/screens/restore/restore_options_page.dart +++ b/lib/src/screens/restore/restore_options_page.dart @@ -8,7 +8,6 @@ import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/screens/pin_code/pin_code_widget.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/src/widgets/option_tile.dart'; -import 'package:cake_wallet/themes/extensions/option_tile_theme.dart'; import 'package:cake_wallet/utils/device_info.dart'; import 'package:cake_wallet/utils/permission_handler.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; @@ -20,6 +19,9 @@ import 'package:cw_core/hardware/device_connection_type.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; import 'package:permission_handler/permission_handler.dart'; +import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart'; +import 'package:cake_wallet/themes/extensions/info_theme.dart'; +import 'package:cake_wallet/themes/theme_base.dart'; class RestoreOptionsPage extends BasePage { RestoreOptionsPage({required this.isNewInstall}); @@ -29,26 +31,45 @@ class RestoreOptionsPage extends BasePage { final bool isNewInstall; + @override + Widget body(BuildContext context) { + return _RestoreOptionsBody(isNewInstall: isNewInstall, themeType: currentTheme.type); + } +} + +class _RestoreOptionsBody extends StatefulWidget { + const _RestoreOptionsBody({required this.isNewInstall, required this.themeType}); + + final bool isNewInstall; + final ThemeType themeType; + + @override + _RestoreOptionsBodyState createState() => _RestoreOptionsBodyState(); +} + +class _RestoreOptionsBodyState extends State<_RestoreOptionsBody> { + bool isRestoring = false; + bool get _doesSupportHardwareWallets { if (!DeviceInfo.instance.isMobile) { return false; } if (isMoneroOnly) { - return DeviceConnectionType.supportedConnectionTypes(WalletType.monero, Platform.isIOS) - .isNotEmpty; + return DeviceConnectionType.supportedConnectionTypes(WalletType.monero, Platform.isIOS).isNotEmpty; } return true; } @override - Widget body(BuildContext context) { - final imageColor = Theme.of(context).extension()!.titleColor; - final imageLedger = Image.asset('assets/images/ledger_nano.png', width: 40, color: imageColor); + Widget build(BuildContext context) { + final mainImageColor = Theme.of(context).extension()!.pageTitleTextColor; + final brightImageColor = Theme.of(context).extension()!.textColor; + final imageColor = widget.themeType == ThemeType.bright ? brightImageColor : mainImageColor; + final imageLedger = Image.asset('assets/images/hardware_wallet/ledger_nano_x.png', width: 40, color: imageColor); final imageSeedKeys = Image.asset('assets/images/restore_wallet_image.png', color: imageColor); final imageBackup = Image.asset('assets/images/backup.png', color: imageColor); - final qrCode = Image.asset('assets/images/restore_qr.png', color: imageColor); return Center( child: Container( @@ -59,16 +80,22 @@ class RestoreOptionsPage extends BasePage { child: Column( children: [ OptionTile( - onPressed: () => Navigator.pushNamed(context, Routes.restoreWalletFromSeedKeys, - arguments: isNewInstall), + key: ValueKey('restore_options_from_seeds_button_key'), + onPressed: () => + Navigator.pushNamed( + context, + Routes.restoreWalletFromSeedKeys, + arguments: widget.isNewInstall, + ), image: imageSeedKeys, title: S.of(context).restore_title_from_seed_keys, description: S.of(context).restore_description_from_seed_keys, ), - if (isNewInstall) + if (widget.isNewInstall) Padding( padding: EdgeInsets.only(top: 24), child: OptionTile( + key: ValueKey('restore_options_from_backup_button_key'), onPressed: () => Navigator.pushNamed(context, Routes.restoreFromBackup), image: imageBackup, title: S.of(context).restore_title_from_backup, @@ -79,9 +106,9 @@ class RestoreOptionsPage extends BasePage { Padding( padding: EdgeInsets.only(top: 24), child: OptionTile( - onPressed: () => Navigator.pushNamed( - context, Routes.restoreWalletFromHardwareWallet, - arguments: isNewInstall), + key: ValueKey('restore_options_from_hardware_wallet_button_key'), + onPressed: () => Navigator.pushNamed(context, Routes.restoreWalletFromHardwareWallet, + arguments: widget.isNewInstall), image: imageLedger, title: S.of(context).restore_title_from_hardware_wallet, description: S.of(context).restore_description_from_hardware_wallet, @@ -90,8 +117,13 @@ class RestoreOptionsPage extends BasePage { Padding( padding: EdgeInsets.only(top: 24), child: OptionTile( + key: ValueKey('restore_options_from_qr_button_key'), onPressed: () => _onScanQRCode(context), - image: qrCode, + icon: Icon( + Icons.qr_code_rounded, + color: imageColor, + size: 50, + ), title: S.of(context).scan_qr_code, description: S.of(context).cold_or_recover_wallet), ) @@ -102,36 +134,47 @@ class RestoreOptionsPage extends BasePage { } void _onWalletCreateFailure(BuildContext context, String error) { - showPopUp( - context: context, - builder: (BuildContext context) { - return AlertWithOneAction( - alertTitle: S.current.error, - alertContent: error, - buttonText: S.of(context).ok, - buttonAction: () => Navigator.of(context).pop()); - }); + setState(() { + isRestoring = false; + }); + + WidgetsBinding.instance.addPostFrameCallback((_) { + showPopUp( + context: context, + builder: (BuildContext context) { + return AlertWithOneAction( + alertTitle: S.current.error, + alertContent: error, + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(context).pop()); + }); + }); + } Future _onScanQRCode(BuildContext context) async { - final isCameraPermissionGranted = - await PermissionHandler.checkPermission(Permission.camera, context); + final isCameraPermissionGranted = await PermissionHandler.checkPermission(Permission.camera, context); if (!isCameraPermissionGranted) return; bool isPinSet = false; - if (isNewInstall) { + if (widget.isNewInstall) { await Navigator.pushNamed(context, Routes.setupPin, arguments: (PinCodeState setupPinContext, String _) { - setupPinContext.close(); - isPinSet = true; - }); + setupPinContext.close(); + isPinSet = true; + }); } - if (!isNewInstall || isPinSet) { + if (!widget.isNewInstall || isPinSet) { try { + if (isRestoring) { + return; + } + setState(() { + isRestoring = true; + }); final restoreWallet = await WalletRestoreFromQRCode.scanQRCodeForRestoring(context); - final restoreFromQRViewModel = - getIt.get(param1: restoreWallet.type); + final restoreFromQRViewModel = getIt.get(param1: restoreWallet.type); await restoreFromQRViewModel.create(restoreWallet: restoreWallet); if (restoreFromQRViewModel.state is FailureState) { diff --git a/lib/src/screens/restore/wallet_restore_from_keys_form.dart b/lib/src/screens/restore/wallet_restore_from_keys_form.dart index 56e49b087..83772f866 100644 --- a/lib/src/screens/restore/wallet_restore_from_keys_form.dart +++ b/lib/src/screens/restore/wallet_restore_from_keys_form.dart @@ -112,10 +112,12 @@ class WalletRestoreFromKeysFromState extends State { alignment: Alignment.centerRight, children: [ BaseTextFormField( + key: ValueKey('wallet_restore_from_keys_wallet_name_textfield_key'), controller: nameTextEditingController, hintText: S.of(context).wallet_name, validator: WalletNameValidator(), suffixIcon: IconButton( + key: ValueKey('wallet_restore_from_keys_wallet_name_refresh_button_key'), onPressed: () async { final rName = await generateName(); FocusManager.instance.primaryFocus?.unfocus(); @@ -175,6 +177,7 @@ class WalletRestoreFromKeysFromState extends State { bool nanoBased = widget.walletRestoreViewModel.type == WalletType.nano || widget.walletRestoreViewModel.type == WalletType.banano; return AddressTextField( + addressKey: ValueKey('wallet_restore_from_key_private_key_textfield_key'), controller: privateKeyController, placeholder: nanoBased ? S.of(context).seed_hex_form : S.of(context).private_key, options: [AddressTextFieldOption.paste], 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 ec40eb1c1..c8a75cf40 100644 --- a/lib/src/screens/restore/wallet_restore_from_seed_form.dart +++ b/lib/src/screens/restore/wallet_restore_from_seed_form.dart @@ -9,35 +9,32 @@ import 'package:cake_wallet/src/widgets/seed_language_picker.dart'; import 'package:cake_wallet/src/widgets/seed_widget.dart'; import 'package:cake_wallet/themes/extensions/send_page_theme.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; -import 'package:cake_wallet/view_model/seed_type_view_model.dart'; +import 'package:cake_wallet/view_model/seed_settings_view_model.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; import 'package:mobx/mobx.dart'; import 'package:polyseed/polyseed.dart'; class WalletRestoreFromSeedForm extends StatefulWidget { - WalletRestoreFromSeedForm( - {Key? key, - required this.displayLanguageSelector, - required this.displayBlockHeightSelector, - required this.displayPassphrase, - required this.type, - required this.displayWalletPassword, - required this.seedTypeViewModel, - this.blockHeightFocusNode, - this.onHeightOrDateEntered, - this.onSeedChange, - this.onLanguageChange, - this.onPasswordChange, - this.onRepeatedPasswordChange}) - : super(key: key); + WalletRestoreFromSeedForm({Key? key, + required this.displayLanguageSelector, + required this.displayBlockHeightSelector, + required this.type, + required this.displayWalletPassword, + required this.seedSettingsViewModel, + this.blockHeightFocusNode, + this.onHeightOrDateEntered, + this.onSeedChange, + this.onLanguageChange, + this.onPasswordChange, + this.onRepeatedPasswordChange, + }) : super(key: key); final WalletType type; final bool displayLanguageSelector; final bool displayBlockHeightSelector; final bool displayWalletPassword; - final bool displayPassphrase; - final SeedTypeViewModel seedTypeViewModel; + final SeedSettingsViewModel seedSettingsViewModel; final FocusNode? blockHeightFocusNode; final Function(bool)? onHeightOrDateEntered; final void Function(String)? onSeedChange; @@ -58,8 +55,9 @@ class WalletRestoreFromSeedFormState extends State { languageController = TextEditingController(), nameTextEditingController = TextEditingController(), passwordTextEditingController = displayWalletPassword ? TextEditingController() : null, - repeatedPasswordTextEditingController = displayWalletPassword ? TextEditingController() : null, - passphraseController = TextEditingController(), + repeatedPasswordTextEditingController = displayWalletPassword + ? TextEditingController() + : null, seedTypeController = TextEditingController(); final GlobalKey seedWidgetStateKey; @@ -69,7 +67,6 @@ class WalletRestoreFromSeedFormState extends State { final TextEditingController? passwordTextEditingController; final TextEditingController? repeatedPasswordTextEditingController; final TextEditingController seedTypeController; - final TextEditingController passphraseController; final GlobalKey formKey; late ReactionDisposer moneroSeedTypeReaction; String language; @@ -78,7 +75,7 @@ class WalletRestoreFromSeedFormState extends State { @override void initState() { - _setSeedType(widget.seedTypeViewModel.moneroSeedType); + _setSeedType(widget.seedSettingsViewModel.moneroSeedType); _setLanguageLabel(language); if (passwordTextEditingController != null) { @@ -87,14 +84,16 @@ class WalletRestoreFromSeedFormState extends State { } if (repeatedPasswordTextEditingController != null) { - repeatedPasswordListener = () => widget.onRepeatedPasswordChange?.call(repeatedPasswordTextEditingController!.text); + repeatedPasswordListener = + () => widget.onRepeatedPasswordChange?.call(repeatedPasswordTextEditingController!.text); repeatedPasswordTextEditingController?.addListener(repeatedPasswordListener!); } + moneroSeedTypeReaction = - reaction((_) => widget.seedTypeViewModel.moneroSeedType, (SeedType item) { - _setSeedType(item); - _changeLanguage('English'); - }); + reaction((_) => widget.seedSettingsViewModel.moneroSeedType, (MoneroSeedType item) { + _setSeedType(item); + _changeLanguage('English'); + }); super.initState(); } @@ -110,6 +109,7 @@ class WalletRestoreFromSeedFormState extends State { if (repeatedPasswordListener != null) { repeatedPasswordTextEditingController?.removeListener(repeatedPasswordListener!); } + super.dispose(); } @@ -118,11 +118,13 @@ class WalletRestoreFromSeedFormState extends State { Polyseed.isValidSeed(seed)) { final lang = PolyseedLang.getByPhrase(seed); - _changeSeedType(SeedType.polyseed); + _changeSeedType(MoneroSeedType.polyseed); _changeLanguage(lang.nameEnglish); } - if (widget.type == WalletType.wownero && seed.split(" ").length == 14) { - _changeSeedType(SeedType.wowneroSeed); + if (widget.type == WalletType.wownero && seed + .split(" ") + .length == 14) { + _changeSeedType(MoneroSeedType.wowneroSeed); _changeLanguage("English"); } widget.onSeedChange?.call(seed); @@ -139,9 +141,13 @@ class WalletRestoreFromSeedFormState extends State { alignment: Alignment.centerRight, children: [ BaseTextFormField( + key: ValueKey('wallet_restore_from_seed_wallet_name_textfield_key'), controller: nameTextEditingController, - hintText: S.of(context).wallet_name, + hintText: S + .of(context) + .wallet_name, suffixIcon: IconButton( + key: ValueKey('wallet_restore_from_seed_wallet_name_refresh_button_key'), onPressed: () async { final rName = await generateName(); FocusManager.instance.primaryFocus?.unfocus(); @@ -156,7 +162,9 @@ class WalletRestoreFromSeedFormState extends State { padding: const EdgeInsets.all(8), decoration: BoxDecoration( borderRadius: BorderRadius.circular(6.0), - color: Theme.of(context).hintColor, + color: Theme + .of(context) + .hintColor, ), width: 34, height: 34, @@ -174,22 +182,26 @@ class WalletRestoreFromSeedFormState extends State { )), Container(height: 20), SeedWidget( - key: seedWidgetStateKey, - language: language, - type: widget.type, - onSeedChange: onSeedChange), + key: seedWidgetStateKey, + language: language, + type: widget.type, + onSeedChange: onSeedChange, + seedTextFieldKey: ValueKey('wallet_restore_from_seed_wallet_seeds_textfield_key'), + pasteButtonKey: ValueKey('wallet_restore_from_seed_wallet_seeds_paste_button_key'), + ), if (widget.type == WalletType.monero || widget.type == WalletType.wownero) GestureDetector( onTap: () async { await showPopUp( context: context, - builder: (_) => Picker( + builder: (_) => + Picker( items: _getItems(), selectedAtIndex: isPolyseed ? 1 : seedTypeController.value.text.contains("14") - ? 2 - : 0, + ? 2 + : 0, mainAxisAlignment: MainAxisAlignment.start, onItemSelected: _changeSeedType, isSeparated: false, @@ -211,37 +223,43 @@ class WalletRestoreFromSeedFormState extends State { if (widget.displayWalletPassword) ...[BaseTextFormField( controller: passwordTextEditingController, - hintText: S.of(context).password, + hintText: S + .of(context) + .password, obscureText: true), BaseTextFormField( controller: repeatedPasswordTextEditingController, - hintText: S.of(context).repeat_wallet_password, - obscureText: true)], + hintText: S + .of(context) + .repeat_wallet_password, + obscureText: true) + ], if (widget.displayLanguageSelector) - if (!seedTypeController.value.text.contains("14") && widget.displayLanguageSelector) - GestureDetector( - onTap: () async { - await showPopUp( - context: context, - builder: (_) => SeedLanguagePicker( - selected: language, - onItemSelected: _changeLanguage, - seedType: isPolyseed ? SeedType.polyseed : SeedType.legacy, - )); - }, - child: Container( - color: Colors.transparent, - padding: EdgeInsets.only(top: 20.0), - child: IgnorePointer( - child: BaseTextFormField( - controller: languageController, - enableInteractiveSelection: false, - readOnly: true, - suffixIcon: expandIcon, + if (!seedTypeController.value.text.contains("14") && widget.displayLanguageSelector) + GestureDetector( + onTap: () async { + await showPopUp( + context: context, + builder: (_) => + SeedLanguagePicker( + selected: language, + onItemSelected: _changeLanguage, + seedType: isPolyseed ? MoneroSeedType.polyseed : MoneroSeedType.legacy, + )); + }, + child: Container( + color: Colors.transparent, + padding: EdgeInsets.only(top: 20.0), + child: IgnorePointer( + child: BaseTextFormField( + controller: languageController, + enableInteractiveSelection: false, + readOnly: true, + suffixIcon: expandIcon, + ), ), ), ), - ), if ((!isPolyseed) && widget.displayBlockHeightSelector) BlockchainHeightWidget( focusNode: widget.blockHeightFocusNode, @@ -250,29 +268,24 @@ class WalletRestoreFromSeedFormState extends State { hasDatePicker: widget.type == WalletType.monero || widget.type == WalletType.wownero, walletType: widget.type, ), - if (widget.displayPassphrase) ...[ - const SizedBox(height: 10), - BaseTextFormField( - hintText: S.current.passphrase, - controller: passphraseController, - obscureText: true, - ), - ] ])); } bool get isPolyseed => - widget.seedTypeViewModel.moneroSeedType == SeedType.polyseed && - (widget.type == WalletType.monero || widget.type == WalletType.wownero); + widget.seedSettingsViewModel.moneroSeedType == MoneroSeedType.polyseed && + (widget.type == WalletType.monero || widget.type == WalletType.wownero); - Widget get expandIcon => Container( + Widget get expandIcon => + Container( padding: EdgeInsets.all(18), width: 24, height: 24, child: Image.asset( 'assets/images/arrow_bottom_purple_icon.png', height: 8, - color: Theme.of(context).hintColor, + color: Theme + .of(context) + .hintColor, ), ); @@ -280,8 +293,8 @@ class WalletRestoreFromSeedFormState extends State { final setLang = isPolyseed ? "POLYSEED_$language" : seedTypeController.value.text.contains("14") - ? "WOWSEED_" + language - : language; + ? "WOWSEED_" + language + : language; setState(() { this.language = setLang; seedWidgetStateKey.currentState!.changeSeedLanguage(setLang); @@ -293,24 +306,24 @@ class WalletRestoreFromSeedFormState extends State { void _setLanguageLabel(String language) => languageController.text = '${language.replaceAll("POLYSEED_", "")} (Seed language)'; - void _changeSeedType(SeedType item) { + void _changeSeedType(MoneroSeedType item) { _setSeedType(item); _changeLanguage('English'); - widget.seedTypeViewModel.setMoneroSeedType(item); + widget.seedSettingsViewModel.setMoneroSeedType(item); } - void _setSeedType(SeedType item) { + void _setSeedType(MoneroSeedType item) { seedTypeController.text = item.toString(); } - List _getItems() { + List _getItems() { switch (widget.type) { case WalletType.monero: - return [SeedType.legacy, SeedType.polyseed]; + return [MoneroSeedType.legacy, MoneroSeedType.polyseed]; case WalletType.wownero: - return [SeedType.legacy, SeedType.polyseed, SeedType.wowneroSeed]; + return [MoneroSeedType.legacy, MoneroSeedType.polyseed, MoneroSeedType.wowneroSeed]; default: - return [SeedType.legacy]; + return [MoneroSeedType.legacy]; } } } diff --git a/lib/src/screens/restore/wallet_restore_page.dart b/lib/src/screens/restore/wallet_restore_page.dart index c8fa3665e..6215e26c3 100644 --- a/lib/src/screens/restore/wallet_restore_page.dart +++ b/lib/src/screens/restore/wallet_restore_page.dart @@ -12,7 +12,7 @@ import 'package:cake_wallet/themes/extensions/wallet_list_theme.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/restore/restore_mode.dart'; -import 'package:cake_wallet/view_model/seed_type_view_model.dart'; +import 'package:cake_wallet/view_model/seed_settings_view_model.dart'; import 'package:cake_wallet/view_model/wallet_restore_view_model.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; @@ -23,7 +23,7 @@ import 'package:mobx/mobx.dart'; import 'package:smooth_page_indicator/smooth_page_indicator.dart'; class WalletRestorePage extends BasePage { - WalletRestorePage(this.walletRestoreViewModel, this.seedTypeViewModel) + WalletRestorePage(this.walletRestoreViewModel, this.seedSettingsViewModel) : walletRestoreFromSeedFormKey = GlobalKey(), walletRestoreFromKeysFormKey = GlobalKey(), _pages = [], @@ -33,11 +33,10 @@ class WalletRestorePage extends BasePage { switch (mode) { case WalletRestoreMode.seed: _pages.add(WalletRestoreFromSeedForm( - seedTypeViewModel: seedTypeViewModel, + seedSettingsViewModel: seedSettingsViewModel, displayBlockHeightSelector: walletRestoreViewModel.hasBlockchainHeightLanguageSelector, displayLanguageSelector: walletRestoreViewModel.hasSeedLanguageSelector, - displayPassphrase: walletRestoreViewModel.hasPassphrase, type: walletRestoreViewModel.type, key: walletRestoreFromSeedFormKey, blockHeightFocusNode: _blockHeightFocusNode, @@ -96,7 +95,7 @@ class WalletRestorePage extends BasePage { )); final WalletRestoreViewModel walletRestoreViewModel; - final SeedTypeViewModel seedTypeViewModel; + final SeedSettingsViewModel seedSettingsViewModel; final PageController _controller; final List _pages; final GlobalKey walletRestoreFromSeedFormKey; @@ -213,6 +212,7 @@ class WalletRestorePage extends BasePage { Observer( builder: (context) { return LoadingPrimaryButton( + key: ValueKey('wallet_restore_seed_or_key_restore_button_key'), onPressed: () async { await _confirmForm(context); }, @@ -230,9 +230,11 @@ class WalletRestorePage extends BasePage { ), const SizedBox(height: 25), GestureDetector( + key: ValueKey('wallet_restore_advanced_settings_button_key'), onTap: () { Navigator.of(context) .pushNamed(Routes.advancedPrivacySettings, arguments: { + 'isFromRestore': true, 'type': walletRestoreViewModel.type, 'useTestnet': walletRestoreViewModel.useTestnet, 'toggleTestnet': walletRestoreViewModel.toggleUseTestnet @@ -280,16 +282,12 @@ class WalletRestorePage extends BasePage { return false; } - if ((walletRestoreViewModel.type == WalletType.litecoin) && - (seedWords.length != WalletRestoreViewModelBase.electrumSeedMnemonicLength && - seedWords.length != WalletRestoreViewModelBase.electrumShortSeedMnemonicLength)) { - return false; - } - // bip39: - const validSeedLengths = [12, 18, 24]; - if (walletRestoreViewModel.type == WalletType.bitcoin && - !(validSeedLengths.contains(seedWords.length))) { + 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; } @@ -321,10 +319,7 @@ class WalletRestorePage extends BasePage { -1; } - if (walletRestoreViewModel.hasPassphrase) { - credentials['passphrase'] = - walletRestoreFromSeedFormKey.currentState!.passphraseController.text; - } + credentials['passphrase'] = seedSettingsViewModel.passphrase; credentials['name'] = walletRestoreFromSeedFormKey.currentState!.nameTextEditingController.text; @@ -406,26 +401,19 @@ class WalletRestorePage extends BasePage { ) as DerivationInfo?; } else if (derivationsWithHistory == 1) { dInfo = derivations[derivationWithHistoryIndex]; - } - - // get the default derivation for this wallet type: - if (dInfo == null) { + } else if (derivations.length == 1) { // we only return 1 derivation if we're pretty sure we know which one to use: - if (derivations.length == 1) { - dInfo = derivations.first; - } else { - // if we have multiple possible derivations, and none have histories - // we just default to the most common one: - dInfo = walletRestoreViewModel.getCommonRestoreDerivation(); - } + dInfo = derivations.first; + } else { + // if we have multiple possible derivations, and none (or multiple) have histories + // we just default to the most common one: + dInfo = walletRestoreViewModel.getCommonRestoreDerivation(); } this.derivationInfo = dInfo; - if (this.derivationInfo == null) { - this.derivationInfo = walletRestoreViewModel.getDefaultDerivation(); - } await walletRestoreViewModel.create(options: _credentials()); + seedSettingsViewModel.setPassphrase(null); } catch (e) { _formProcessing = false; rethrow; diff --git a/lib/src/screens/root/root.dart b/lib/src/screens/root/root.dart index 76c6c5cfd..6b62435d0 100644 --- a/lib/src/screens/root/root.dart +++ b/lib/src/screens/root/root.dart @@ -5,6 +5,7 @@ import 'package:cake_wallet/core/totp_request_details.dart'; import 'package:cake_wallet/utils/device_info.dart'; import 'package:cake_wallet/view_model/link_view_model.dart'; import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/auth/auth_page.dart'; diff --git a/lib/src/screens/send/send_page.dart b/lib/src/screens/send/send_page.dart index 97a7ad88d..bce82312d 100644 --- a/lib/src/screens/send/send_page.dart +++ b/lib/src/screens/send/send_page.dart @@ -28,6 +28,7 @@ import 'package:cake_wallet/utils/request_review_handler.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/send/output.dart'; +import 'package:cw_core/unspent_coin_type.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:cake_wallet/view_model/send/send_view_model.dart'; import 'package:cake_wallet/view_model/send/send_view_model_state.dart'; @@ -68,11 +69,11 @@ class SendPage extends BasePage { @override Function(BuildContext)? get pushToNextWidget => (context) { - FocusScopeNode currentFocus = FocusScope.of(context); - if (!currentFocus.hasPrimaryFocus) { - currentFocus.focusedChild?.unfocus(); - } - }; + FocusScopeNode currentFocus = FocusScope.of(context); + if (!currentFocus.hasPrimaryFocus) { + currentFocus.focusedChild?.unfocus(); + } + }; @override Widget? leading(BuildContext context) { @@ -212,26 +213,25 @@ class SendPage extends BasePage { final count = sendViewModel.outputs.length; return count > 1 - ? Semantics ( - label: 'Page Indicator', - hint: 'Swipe to change receiver', - excludeSemantics: true, - child: - SmoothPageIndicator( - controller: controller, - count: count, - effect: ScrollingDotsEffect( - spacing: 6.0, - radius: 6.0, - dotWidth: 6.0, - dotHeight: 6.0, - dotColor: Theme.of(context) - .extension()! - .indicatorDotColor, - activeDotColor: Theme.of(context) - .extension()! - .templateBackgroundColor), - )) + ? Semantics( + label: 'Page Indicator', + hint: 'Swipe to change receiver', + excludeSemantics: true, + child: SmoothPageIndicator( + controller: controller, + count: count, + effect: ScrollingDotsEffect( + spacing: 6.0, + radius: 6.0, + dotWidth: 6.0, + dotHeight: 6.0, + dotColor: Theme.of(context) + .extension()! + .indicatorDotColor, + activeDotColor: Theme.of(context) + .extension()! + .templateBackgroundColor), + )) : Offstage(); }, ), @@ -251,6 +251,7 @@ class SendPage extends BasePage { return Row( children: [ AddTemplateButton( + key: ValueKey('send_page_add_template_button_key'), onTap: () => Navigator.of(context).pushNamed(Routes.sendTemplate), currentTemplatesLength: templates.length, ), @@ -273,6 +274,7 @@ class SendPage extends BasePage { ? template.cryptoCurrency : template.fiatCurrency, onTap: () async { + sendViewModel.state = IsExecutingState(); if (template.additionalRecipients?.isNotEmpty ?? false) { sendViewModel.clearOutputs(); @@ -301,6 +303,7 @@ class SendPage extends BasePage { template: template, ); } + sendViewModel.state = InitialExecutionState(); }, onRemove: () { showPopUp( @@ -338,19 +341,22 @@ class SendPage extends BasePage { children: [ if (sendViewModel.hasCurrecyChanger) Observer( - builder: (_) => Padding( - padding: EdgeInsets.only(bottom: 12), - child: PrimaryButton( - onPressed: () => presentCurrencyPicker(context), - text: 'Change your asset (${sendViewModel.selectedCryptoCurrency})', - color: Colors.transparent, - textColor: - Theme.of(context).extension()!.hintTextColor, - ))), + builder: (_) => Padding( + padding: EdgeInsets.only(bottom: 12), + child: PrimaryButton( + key: ValueKey('send_page_change_asset_button_key'), + onPressed: () => presentCurrencyPicker(context), + text: 'Change your asset (${sendViewModel.selectedCryptoCurrency})', + color: Colors.transparent, + textColor: Theme.of(context).extension()!.hintTextColor, + ), + ), + ), if (sendViewModel.sendTemplateViewModel.hasMultiRecipient) Padding( padding: EdgeInsets.only(bottom: 12), child: PrimaryButton( + key: ValueKey('send_page_add_receiver_button_key'), onPressed: () { sendViewModel.addOutput(); Future.delayed(const Duration(milliseconds: 250), () { @@ -367,7 +373,9 @@ class SendPage extends BasePage { Observer( builder: (_) { return LoadingPrimaryButton( + key: ValueKey('send_page_send_button_key'), onPressed: () async { + if (sendViewModel.state is IsExecutingState) return; if (_formKey.currentState != null && !_formKey.currentState!.validate()) { if (sendViewModel.outputs.length > 1) { showErrorValidationAlert(context); @@ -387,16 +395,19 @@ class SendPage extends BasePage { if (sendViewModel.wallet.isHardwareWallet) { if (!sendViewModel.ledgerViewModel!.isConnected) { - await Navigator.of(context).pushNamed(Routes.connectDevices, + await Navigator.of(context).pushNamed( + Routes.connectDevices, arguments: ConnectDevicePageParams( walletType: sendViewModel.walletType, onConnectDevice: (BuildContext context, _) { - sendViewModel.ledgerViewModel!.setLedger(sendViewModel.wallet); + sendViewModel.ledgerViewModel! + .setLedger(sendViewModel.wallet); Navigator.of(context).pop(); }, )); } else { - sendViewModel.ledgerViewModel!.setLedger(sendViewModel.wallet); + sendViewModel.ledgerViewModel! + .setLedger(sendViewModel.wallet); } } @@ -449,6 +460,8 @@ class SendPage extends BasePage { context: context, builder: (BuildContext context) { return AlertWithOneAction( + key: ValueKey('send_page_send_failure_dialog_key'), + buttonKey: ValueKey('send_page_send_failure_dialog_button_key'), alertTitle: S.of(context).error, alertContent: state.error, buttonText: S.of(context).ok, @@ -464,6 +477,7 @@ class SendPage extends BasePage { context: context, builder: (BuildContext _dialogContext) { return ConfirmSendingAlert( + key: ValueKey('send_page_confirm_sending_dialog_key'), alertTitle: S.of(_dialogContext).confirm_sending, amount: S.of(_dialogContext).send_amount, amountValue: sendViewModel.pendingTransaction!.amountFormatted, @@ -475,8 +489,13 @@ class SendPage extends BasePage { feeValue: sendViewModel.pendingTransaction!.feeFormatted, feeFiatAmount: sendViewModel.pendingTransactionFeeFiatAmountFormatted, outputs: sendViewModel.outputs, + change: sendViewModel.pendingTransaction!.change, rightButtonText: S.of(_dialogContext).send, leftButtonText: S.of(_dialogContext).cancel, + alertRightActionButtonKey: + ValueKey('send_page_confirm_sending_dialog_send_button_key'), + alertLeftActionButtonKey: + ValueKey('send_page_confirm_sending_dialog_cancel_button_key'), actionRightButton: () async { Navigator.of(_dialogContext).pop(); sendViewModel.commitTransaction(); @@ -494,6 +513,10 @@ class SendPage extends BasePage { newContactAddress = newContactAddress ?? sendViewModel.newContactAddress(); + if (sendViewModel.coinTypeToSpendFrom != UnspentCoinType.any) { + newContactAddress = null; + } + final successMessage = S.of(_dialogContext).send_success( sendViewModel.selectedCryptoCurrency.toString()); @@ -510,10 +533,15 @@ class SendPage extends BasePage { if (newContactAddress != null) { return AlertWithTwoActions( + alertDialogKey: ValueKey('send_page_sent_dialog_key'), alertTitle: '', alertContent: alertContent, rightButtonText: S.of(_dialogContext).add_contact, leftButtonText: S.of(_dialogContext).ignor, + alertLeftActionButtonKey: + ValueKey('send_page_sent_dialog_ignore_button_key'), + alertRightActionButtonKey: ValueKey( + 'send_page_sent_dialog_add_contact_button_key'), actionRightButton: () { Navigator.of(_dialogContext).pop(); RequestReviewHandler.requestReview(); diff --git a/lib/src/screens/send/widgets/confirm_sending_alert.dart b/lib/src/screens/send/widgets/confirm_sending_alert.dart index 3af1c3f8c..83866a73c 100644 --- a/lib/src/screens/send/widgets/confirm_sending_alert.dart +++ b/lib/src/screens/send/widgets/confirm_sending_alert.dart @@ -1,6 +1,7 @@ import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; import 'package:cake_wallet/palette.dart'; import 'package:cake_wallet/view_model/send/output.dart'; +import 'package:cw_core/pending_transaction.dart'; import 'package:flutter/material.dart'; import 'package:cake_wallet/src/widgets/base_alert_dialog.dart'; import 'package:cake_wallet/generated/i18n.dart'; @@ -8,29 +9,34 @@ import 'package:cake_wallet/src/widgets/cake_scrollbar.dart'; import 'package:flutter/scheduler.dart'; class ConfirmSendingAlert extends BaseAlertDialog { - ConfirmSendingAlert( - {required this.alertTitle, - this.paymentId, - this.paymentIdValue, - this.expirationTime, - required this.amount, - required this.amountValue, - required this.fiatAmountValue, - required this.fee, - this.feeRate, - required this.feeValue, - required this.feeFiatAmount, - required this.outputs, - required this.leftButtonText, - required this.rightButtonText, - required this.actionLeftButton, - required this.actionRightButton, - this.alertBarrierDismissible = true, - this.alertLeftActionButtonTextColor, - this.alertRightActionButtonTextColor, - this.alertLeftActionButtonColor, - this.alertRightActionButtonColor, - this.onDispose}); + ConfirmSendingAlert({ + required this.alertTitle, + this.paymentId, + this.paymentIdValue, + this.expirationTime, + required this.amount, + required this.amountValue, + required this.fiatAmountValue, + required this.fee, + this.feeRate, + required this.feeValue, + required this.feeFiatAmount, + required this.outputs, + this.change, + required this.leftButtonText, + required this.rightButtonText, + required this.actionLeftButton, + required this.actionRightButton, + this.alertBarrierDismissible = true, + this.alertLeftActionButtonTextColor, + this.alertRightActionButtonTextColor, + this.alertLeftActionButtonColor, + this.alertRightActionButtonColor, + this.onDispose, + this.alertLeftActionButtonKey, + this.alertRightActionButtonKey, + Key? key, + }); final String alertTitle; final String? paymentId; @@ -44,6 +50,7 @@ class ConfirmSendingAlert extends BaseAlertDialog { final String feeValue; final String feeFiatAmount; final List outputs; + final PendingChange? change; final String leftButtonText; final String rightButtonText; final VoidCallback actionLeftButton; @@ -54,6 +61,8 @@ class ConfirmSendingAlert extends BaseAlertDialog { final Color? alertLeftActionButtonColor; final Color? alertRightActionButtonColor; final Function? onDispose; + final Key? alertRightActionButtonKey; + final Key? alertLeftActionButtonKey; @override String get titleText => alertTitle; @@ -88,6 +97,12 @@ class ConfirmSendingAlert extends BaseAlertDialog { @override Color? get rightActionButtonColor => alertRightActionButtonColor; + @override + Key? get leftActionButtonKey => alertLeftActionButtonKey; + + @override + Key? get rightActionButtonKey => alertLeftActionButtonKey; + @override Widget content(BuildContext context) => ConfirmSendingAlertContent( paymentId: paymentId, @@ -101,6 +116,7 @@ class ConfirmSendingAlert extends BaseAlertDialog { feeValue: feeValue, feeFiatAmount: feeFiatAmount, outputs: outputs, + change: change, onDispose: onDispose); } @@ -117,6 +133,7 @@ class ConfirmSendingAlertContent extends StatefulWidget { required this.feeValue, required this.feeFiatAmount, required this.outputs, + this.change, required this.onDispose}) {} final String? paymentId; @@ -130,6 +147,7 @@ class ConfirmSendingAlertContent extends StatefulWidget { final String feeValue; final String feeFiatAmount; final List outputs; + final PendingChange? change; final Function? onDispose; @override @@ -145,6 +163,7 @@ class ConfirmSendingAlertContent extends StatefulWidget { feeValue: feeValue, feeFiatAmount: feeFiatAmount, outputs: outputs, + change: change, onDispose: onDispose); } @@ -161,6 +180,7 @@ class ConfirmSendingAlertContentState extends State required this.feeValue, required this.feeFiatAmount, required this.outputs, + this.change, this.onDispose}) : recipientTitle = '' { recipientTitle = outputs.length > 1 @@ -179,6 +199,7 @@ class ConfirmSendingAlertContentState extends State final String feeValue; final String feeFiatAmount; final List outputs; + final PendingChange? change; final Function? onDispose; final double backgroundHeight = 160; @@ -279,6 +300,7 @@ class ConfirmSendingAlertContentState extends State crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( + key: ValueKey('confirm_sending_dialog_amount_text_value_key'), amountValue, style: TextStyle( fontSize: 18, @@ -391,100 +413,57 @@ class ConfirmSendingAlertContentState extends State decoration: TextDecoration.none, ), ), - outputs.length > 1 - ? ListView.builder( - padding: EdgeInsets.only(top: 0), - shrinkWrap: true, - physics: NeverScrollableScrollPhysics(), - itemCount: outputs.length, - itemBuilder: (context, index) { - final item = outputs[index]; - final _address = - item.isParsedAddress ? item.extractedAddress : item.address; - final _amount = item.cryptoAmount.replaceAll(',', '.'); + ListView.builder( + padding: EdgeInsets.only(top: 0), + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + itemCount: outputs.length, + itemBuilder: (context, index) { + final item = outputs[index]; + final _address = + item.isParsedAddress ? item.extractedAddress : item.address; + final _amount = item.cryptoAmount.replaceAll(',', '.'); - return Column( - children: [ - if (item.isParsedAddress) - Padding( - padding: EdgeInsets.only(top: 8), - child: Text( - item.parsedAddress.name, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, - fontFamily: 'Lato', - color: PaletteDark.pigeonBlue, - decoration: TextDecoration.none, - ), - )), - Padding( - padding: EdgeInsets.only(top: 8), - child: Text( - _address, - style: TextStyle( - fontSize: 10, - fontWeight: FontWeight.w600, - fontFamily: 'Lato', - color: PaletteDark.pigeonBlue, - decoration: TextDecoration.none, - ), - )), - Padding( - padding: EdgeInsets.only(top: 8), - child: Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Text( - _amount, - style: TextStyle( - fontSize: 10, - fontWeight: FontWeight.w600, - fontFamily: 'Lato', - color: PaletteDark.pigeonBlue, - decoration: TextDecoration.none, - ), - ) - ], - )) - ], - ); - }) - : Column(children: [ - if (outputs.first.isParsedAddress) - Padding( - padding: EdgeInsets.only(top: 8), - child: Text( - outputs.first.parsedAddress.name, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, - fontFamily: 'Lato', - color: PaletteDark.pigeonBlue, - decoration: TextDecoration.none, - ), - )), - Padding( - padding: EdgeInsets.only(top: 8), - child: Text( - outputs.first.isParsedAddress - ? outputs.first.extractedAddress - : outputs.first.address, - style: TextStyle( - fontSize: 10, - fontWeight: FontWeight.w600, - fontFamily: 'Lato', - color: PaletteDark.pigeonBlue, - decoration: TextDecoration.none, - ), - )), - ]) + return Column( + children: [ + if (item.isParsedAddress) + AddressText(text: item.parsedAddress.name), + AddressText(text: _address, fontSize: 10), + if (stealthAddressText(item.stealthAddress) != null) + AddressText( + text: stealthAddressText(item.stealthAddress)!, fontSize: 10), + AmountText(text: _amount), + ], + ); + }, + ) ], ), - ) + ), + if (change != null) + Padding( + padding: EdgeInsets.only(top: 16), + child: Column( + children: [ + Text( + S.of(context).send_change_to_you, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.normal, + fontFamily: 'Lato', + color: Theme.of(context).extension()!.titleColor, + decoration: TextDecoration.none, + ), + ), + Column( + children: [ + AddressText(text: change!.address, fontSize: 10), + AmountText(text: change!.amount), + ], + ) + ], + ), + ) ], ))), if (showScrollbar) @@ -539,3 +518,78 @@ class ExpirationTimeWidget extends StatelessWidget { ); } } + +class AddressText extends StatelessWidget { + final String text; + final double fontSize; + final FontWeight fontWeight; + final TextAlign? textAlign; + + const AddressText({ + required this.text, + this.fontSize = 14, + this.fontWeight = FontWeight.w600, + this.textAlign, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.only(top: 8), + child: Text( + text, + style: TextStyle( + fontSize: fontSize, + fontWeight: fontWeight, + fontFamily: 'Lato', + color: PaletteDark.pigeonBlue, + decoration: TextDecoration.none, + ), + ), + ); + } +} + +class AmountText extends StatelessWidget { + final String text; + final double fontSize; + final FontWeight fontWeight; + final TextAlign? textAlign; + + const AmountText({ + required this.text, + this.fontSize = 10, + this.fontWeight = FontWeight.w600, + this.textAlign, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.only(top: 8), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Text( + text, + style: TextStyle( + fontSize: fontSize, + fontWeight: fontWeight, + fontFamily: 'Lato', + color: PaletteDark.pigeonBlue, + decoration: TextDecoration.none, + ), + ) + ], + )); + } +} + +String? stealthAddressText(String? stealthAddress) { + if (stealthAddress == null) { + return null; + } + + return stealthAddress.isNotEmpty ? "-> $stealthAddress" : null; +} diff --git a/lib/src/screens/send/widgets/send_card.dart b/lib/src/screens/send/widgets/send_card.dart index 214d162ed..0713fb8c4 100644 --- a/lib/src/screens/send/widgets/send_card.dart +++ b/lib/src/screens/send/widgets/send_card.dart @@ -14,7 +14,6 @@ import 'package:cake_wallet/view_model/send/output.dart'; import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:mobx/mobx.dart'; import 'package:keyboard_actions/keyboard_actions.dart'; @@ -158,6 +157,7 @@ class SendCardState extends State with AutomaticKeepAliveClientMixin with AutomaticKeepAliveClientMixin with AutomaticKeepAliveClientMixin _presentPicker(context), isPickerEnable: sendViewModel.hasMultipleTokens, tag: sendViewModel.selectedCryptoCurrency.tag, - allAmountButton: !sendViewModel.isBatchSending && sendViewModel.shouldDisplaySendALL, + allAmountButton: + !sendViewModel.isBatchSending && sendViewModel.shouldDisplaySendALL, currencyValueValidator: output.sendAll ? sendViewModel.allAmountValidator : sendViewModel.amountValidator, @@ -257,6 +263,9 @@ class SendCardState extends State with AutomaticKeepAliveClientMixin with AutomaticKeepAliveClientMixin with AutomaticKeepAliveClientMixin GestureDetector( + key: ValueKey('send_page_select_fee_priority_button_key'), onTap: sendViewModel.hasFeesPriority ? () => pickTransactionPriority(context) : () {}, @@ -360,7 +371,11 @@ class SendCardState extends State with AutomaticKeepAliveClientMixin Navigator.of(context).pushNamed(Routes.unspentCoinsList), + key: ValueKey('send_page_unspent_coin_button_key'), + onTap: () => Navigator.of(context).pushNamed( + Routes.unspentCoinsList, + arguments: widget.sendViewModel.coinTypeToSpendFrom, + ), child: Container( color: Colors.transparent, child: Row( @@ -544,11 +559,13 @@ class SendCardState extends State with AutomaticKeepAliveClientMixin( context: context, builder: (_) => CurrencyPicker( - selectedAtIndex: sendViewModel.currencies.indexOf(sendViewModel.selectedCryptoCurrency), - items: sendViewModel.currencies, - hintText: S.of(context).search_currency, - onItemSelected: (Currency cur) => - sendViewModel.selectedCryptoCurrency = (cur as CryptoCurrency)), + key: ValueKey('send_page_currency_picker_dialog_button_key'), + selectedAtIndex: sendViewModel.currencies.indexOf(sendViewModel.selectedCryptoCurrency), + items: sendViewModel.currencies, + hintText: S.of(context).search_currency, + onItemSelected: (Currency cur) => + sendViewModel.selectedCryptoCurrency = (cur as CryptoCurrency), + ), ); } diff --git a/lib/src/screens/settings/desktop_settings/desktop_settings_page.dart b/lib/src/screens/settings/desktop_settings/desktop_settings_page.dart index 611b2acb7..79f74065a 100644 --- a/lib/src/screens/settings/desktop_settings/desktop_settings_page.dart +++ b/lib/src/screens/settings/desktop_settings/desktop_settings_page.dart @@ -60,6 +60,11 @@ class _DesktopSettingsPageState extends State { return Container(); } + if (!widget.dashboardViewModel.hasMweb && + item.name(context) == S.of(context).litecoin_mweb_settings) { + return Container(); + } + final isLastTile = index == itemCount - 1; return SettingActionButton( isLastTile: isLastTile, diff --git a/lib/src/screens/settings/mweb_settings.dart b/lib/src/screens/settings/mweb_settings.dart new file mode 100644 index 000000000..722ffa9aa --- /dev/null +++ b/lib/src/screens/settings/mweb_settings.dart @@ -0,0 +1,50 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/src/screens/settings/widgets/settings_cell_with_arrow.dart'; +import 'package:cake_wallet/src/screens/settings/widgets/settings_switcher_cell.dart'; +import 'package:cake_wallet/view_model/settings/mweb_settings_view_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; + +class MwebSettingsPage extends BasePage { + MwebSettingsPage(this._mwebSettingsViewModel); + + @override + String get title => S.current.litecoin_mweb_settings; + + final MwebSettingsViewModel _mwebSettingsViewModel; + + @override + Widget body(BuildContext context) { + return SingleChildScrollView( + child: Observer(builder: (_) { + return Container( + padding: EdgeInsets.only(top: 10), + child: Column( + children: [ + SettingsSwitcherCell( + title: S.current.litecoin_mweb_display_card, + value: _mwebSettingsViewModel.mwebCardDisplay, + onValueChange: (_, bool value) { + _mwebSettingsViewModel.setMwebCardDisplay(value); + }, + ), + SettingsSwitcherCell( + title: S.current.litecoin_mweb_always_scan, + value: _mwebSettingsViewModel.mwebEnabled, + onValueChange: (_, bool value) { + _mwebSettingsViewModel.setMwebEnabled(value); + }, + ), + SettingsCellWithArrow( + title: S.current.litecoin_mweb_scanning, + handler: (BuildContext context) => Navigator.of(context).pushNamed(Routes.rescan), + ), + ], + ), + ); + }), + ); + } +} diff --git a/lib/src/screens/settings/privacy_page.dart b/lib/src/screens/settings/privacy_page.dart index 0eaf3ffbd..53e7686e8 100644 --- a/lib/src/screens/settings/privacy_page.dart +++ b/lib/src/screens/settings/privacy_page.dart @@ -6,7 +6,9 @@ import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/screens/settings/widgets/settings_cell_with_arrow.dart'; import 'package:cake_wallet/src/screens/settings/widgets/settings_choices_cell.dart'; import 'package:cake_wallet/src/screens/settings/widgets/settings_switcher_cell.dart'; +import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; import 'package:cake_wallet/utils/device_info.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/settings/choices_list_item.dart'; import 'package:cake_wallet/view_model/settings/privacy_settings_view_model.dart'; import 'package:flutter/material.dart'; @@ -56,8 +58,8 @@ class PrivacyPage extends BasePage { if (_privacySettingsViewModel.isAutoGenerateSubaddressesVisible) SettingsSwitcherCell( title: _privacySettingsViewModel.isMoneroWallet - ? S.current.auto_generate_subaddresses - : S.current.auto_generate_addresses, + ? S.current.auto_generate_subaddresses + : S.current.auto_generate_addresses, value: _privacySettingsViewModel.isAutoGenerateSubaddressesEnabled, onValueChange: (BuildContext _, bool value) { _privacySettingsViewModel.setAutoGenerateSubaddresses(value); @@ -111,6 +113,33 @@ class PrivacyPage extends BasePage { _privacySettingsViewModel.setUseTronGrid(value); }, ), + if (_privacySettingsViewModel.canUseMempoolFeeAPI) + SettingsSwitcherCell( + title: S.current.enable_mempool_api, + value: _privacySettingsViewModel.useMempoolFeeAPI, + onValueChange: (BuildContext _, bool isEnabled) async { + if (!isEnabled) { + final bool confirmation = await showPopUp( + context: context, + builder: (BuildContext context) { + return AlertWithTwoActions( + alertTitle: S.of(context).warning, + alertContent: S.of(context).disable_fee_api_warning, + rightButtonText: S.of(context).confirm, + leftButtonText: S.of(context).cancel, + actionRightButton: () => Navigator.of(context).pop(true), + actionLeftButton: () => Navigator.of(context).pop(false)); + }) ?? + false; + if (confirmation) { + _privacySettingsViewModel.setUseMempoolFeeAPI(isEnabled); + } + return; + } + + _privacySettingsViewModel.setUseMempoolFeeAPI(isEnabled); + }, + ), SettingsCellWithArrow( title: S.current.domain_looks_up, handler: (context) => Navigator.of(context).pushNamed(Routes.domainLookupsPage), diff --git a/lib/src/screens/setup_pin_code/setup_pin_code.dart b/lib/src/screens/setup_pin_code/setup_pin_code.dart index 833fd9b60..d78a2df63 100644 --- a/lib/src/screens/setup_pin_code/setup_pin_code.dart +++ b/lib/src/screens/setup_pin_code/setup_pin_code.dart @@ -52,6 +52,7 @@ class SetupPinCodePage extends BasePage { context: context, builder: (BuildContext context) { return AlertWithOneAction( + buttonKey: ValueKey('setup_pin_code_success_button_key'), alertTitle: S.current.setup_pin, alertContent: S.of(context).setup_successful, buttonText: S.of(context).ok, diff --git a/lib/src/screens/support/support_page.dart b/lib/src/screens/support/support_page.dart index 471ff15b0..049ab2fb7 100644 --- a/lib/src/screens/support/support_page.dart +++ b/lib/src/screens/support/support_page.dart @@ -2,17 +2,20 @@ 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/option_tile.dart'; +import 'package:cake_wallet/themes/extensions/info_theme.dart'; import 'package:cake_wallet/utils/device_info.dart'; import 'package:cake_wallet/view_model/support_view_model.dart'; import 'package:flutter/material.dart'; import 'package:url_launcher/url_launcher.dart'; +import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart'; +import 'package:cake_wallet/themes/theme_base.dart'; class SupportPage extends BasePage { SupportPage(this.supportViewModel); final SupportViewModel supportViewModel; - final imageLiveSupport = Image.asset('assets/images/live_support.png'); + final imageLiveSupport = Image.asset('assets/images/cake_icon.png'); final imageWalletGuides = Image.asset('assets/images/wallet_guides.png'); final imageMoreLinks = Image.asset('assets/images/more_links.png'); @@ -22,8 +25,16 @@ class SupportPage extends BasePage { @override AppBarStyle get appBarStyle => AppBarStyle.regular; + + @override Widget body(BuildContext context) { + + final mainColor = Theme.of(context).extension()!.pageTitleTextColor; + final brightColor = Theme.of(context).extension()!.textColor; + + final iconColor = currentTheme.type == ThemeType.bright ? brightColor : mainColor; + return Container( child: Center( child: ConstrainedBox( @@ -33,7 +44,11 @@ class SupportPage extends BasePage { Padding( padding: EdgeInsets.only(top: 24), child: OptionTile( - image: imageLiveSupport, + icon: Icon( + Icons.support_agent, + color: iconColor, + size: 50, + ), title: S.of(context).support_title_live_chat, description: S.of(context).support_description_live_chat, onPressed: () { @@ -48,7 +63,11 @@ class SupportPage extends BasePage { Padding( padding: EdgeInsets.only(top: 24), child: OptionTile( - image: imageWalletGuides, + icon: Icon( + Icons.find_in_page, + color: iconColor, + size: 50, + ), title: S.of(context).support_title_guides, description: S.of(context).support_description_guides, onPressed: () => _launchUrl(supportViewModel.guidesUrl), @@ -57,7 +76,11 @@ class SupportPage extends BasePage { Padding( padding: EdgeInsets.only(top: 24), child: OptionTile( - image: imageMoreLinks, + icon: Icon( + Icons.contact_support, + color: iconColor, + size: 50, + ), title: S.of(context).support_title_other_links, description: S.of(context).support_description_other_links, onPressed: () => Navigator.pushNamed(context, Routes.supportOtherLinks), diff --git a/lib/src/screens/support_other_links/support_other_links_page.dart b/lib/src/screens/support_other_links/support_other_links_page.dart index 7a1a945ca..b115594b4 100644 --- a/lib/src/screens/support_other_links/support_other_links_page.dart +++ b/lib/src/screens/support_other_links/support_other_links_page.dart @@ -37,7 +37,6 @@ class SupportOtherLinksPage extends BasePage { itemCounter: (int _) => supportViewModel.items.length, itemBuilder: (_, index) { final item = supportViewModel.items[index]; - if (item is RegularListItem) { return SettingsCellWithArrow(title: item.title, handler: item.handler); } diff --git a/lib/src/screens/transaction_details/rbf_details_page.dart b/lib/src/screens/transaction_details/rbf_details_page.dart index 3faec48a8..b117a0b68 100644 --- a/lib/src/screens/transaction_details/rbf_details_page.dart +++ b/lib/src/screens/transaction_details/rbf_details_page.dart @@ -24,19 +24,22 @@ import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:mobx/mobx.dart'; class RBFDetailsPage extends BasePage { - RBFDetailsPage({required this.transactionDetailsViewModel}); + RBFDetailsPage({required this.transactionDetailsViewModel, required this.rawTransaction}) { + transactionDetailsViewModel.addBumpFeesListItems( + transactionDetailsViewModel.transactionInfo, rawTransaction); + } @override String get title => S.current.bump_fee; final TransactionDetailsViewModel transactionDetailsViewModel; + final String rawTransaction; bool _effectsInstalled = false; @override Widget body(BuildContext context) { _setEffects(context); - return Column( children: [ Expanded( @@ -166,7 +169,9 @@ class RBFDetailsPage extends BasePage { actionRightButton: () async { Navigator.of(popupContext).pop(); await transactionDetailsViewModel.sendViewModel.commitTransaction(); - // transactionStatePopup(); + try { + Navigator.of(popupContext).pop(); + } catch (_) {} }, actionLeftButton: () => Navigator.of(popupContext).pop(), feeFiatAmount: diff --git a/lib/src/screens/transaction_details/transaction_details_page.dart b/lib/src/screens/transaction_details/transaction_details_page.dart index d06b935dd..1b088fc31 100644 --- a/lib/src/screens/transaction_details/transaction_details_page.dart +++ b/lib/src/screens/transaction_details/transaction_details_page.dart @@ -75,7 +75,7 @@ class TransactionDetailsPage extends BasePage { text: S.of(context).bump_fee, onTap: () async { Navigator.of(context).pushNamed(Routes.bumpFeePage, - arguments: transactionDetailsViewModel.transactionInfo); + arguments: [transactionDetailsViewModel.transactionInfo, transactionDetailsViewModel.rawTransaction]); }, ), ); diff --git a/lib/src/screens/transaction_details/widgets/textfield_list_row.dart b/lib/src/screens/transaction_details/widgets/textfield_list_row.dart index ff5513502..a86645ecb 100644 --- a/lib/src/screens/transaction_details/widgets/textfield_list_row.dart +++ b/lib/src/screens/transaction_details/widgets/textfield_list_row.dart @@ -1,27 +1,49 @@ -import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; -import 'package:flutter/material.dart'; import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; import 'package:cake_wallet/themes/extensions/transaction_trade_theme.dart'; +import 'package:flutter/material.dart'; -class TextFieldListRow extends StatelessWidget { +class TextFieldListRow extends StatefulWidget { TextFieldListRow( {required this.title, required this.value, this.titleFontSize = 14, this.valueFontSize = 16, - this.onSubmitted, - this.onTapOutside}) - : _textController = TextEditingController() { - _textController.text = value; - } + this.onSubmitted}); final String title; final String value; final double titleFontSize; final double valueFontSize; final Function(String value)? onSubmitted; - final Function(String value)? onTapOutside; - final TextEditingController _textController; + + @override + _TextFieldListRowState createState() => _TextFieldListRowState(); +} + +class _TextFieldListRowState extends State { + late TextEditingController _textController; + late FocusNode _focusNode; + + @override + void initState() { + super.initState(); + _textController = TextEditingController(text: widget.value); + _focusNode = FocusNode(); + + _focusNode.addListener(() { + if (!_focusNode.hasFocus) { + widget.onSubmitted?.call(_textController.text); + } + }); + } + + @override + void dispose() { + _textController.dispose(); + _focusNode.dispose(); + super.dispose(); + } @override Widget build(BuildContext context) { @@ -29,41 +51,48 @@ class TextFieldListRow extends StatelessWidget { width: double.infinity, color: Theme.of(context).colorScheme.background, child: Padding( - padding: - const EdgeInsets.only(left: 24, top: 16, bottom: 16, right: 24), + padding: const EdgeInsets.only(left: 24, top: 16, bottom: 16, right: 24), child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(title, - style: TextStyle( - fontSize: titleFontSize, - fontWeight: FontWeight.w500, - color: Theme.of(context).extension()!.detailsTitlesColor), - textAlign: TextAlign.left), - TextField( - controller: _textController, - keyboardType: TextInputType.multiline, - textInputAction: TextInputAction.done, - maxLines: null, - textAlign: TextAlign.start, - style: TextStyle( - fontSize: valueFontSize, - fontWeight: FontWeight.w500, - color: - Theme.of(context).extension()!.titleColor), - decoration: InputDecoration( - isDense: true, - contentPadding: EdgeInsets.only(top: 12, bottom: 0), - hintText: S.of(context).enter_your_note, - hintStyle: TextStyle( - fontSize: valueFontSize, - fontWeight: FontWeight.w500, - color: Theme.of(context).extension()!.detailsTitlesColor), - border: InputBorder.none), - onTapOutside: (_) => onTapOutside?.call(_textController.text), - onSubmitted: (value) => onSubmitted?.call(value), - ) - ]), + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.title, + style: TextStyle( + fontSize: widget.titleFontSize, + fontWeight: FontWeight.w500, + color: Theme.of(context).extension()!.detailsTitlesColor, + ), + textAlign: TextAlign.left, + ), + TextField( + controller: _textController, + focusNode: _focusNode, + keyboardType: TextInputType.multiline, + textInputAction: TextInputAction.done, + maxLines: null, + textAlign: TextAlign.start, + style: TextStyle( + fontSize: widget.valueFontSize, + fontWeight: FontWeight.w500, + color: Theme.of(context).extension()!.titleColor, + ), + decoration: InputDecoration( + isDense: true, + contentPadding: EdgeInsets.only(top: 12, bottom: 0), + hintText: S.of(context).enter_your_note, + hintStyle: TextStyle( + fontSize: widget.valueFontSize, + fontWeight: FontWeight.w500, + color: Theme.of(context).extension()!.detailsTitlesColor, + ), + border: InputBorder.none, + ), + onSubmitted: (value) { + widget.onSubmitted?.call(value); + }, + ), + ], + ), ), ); } diff --git a/lib/src/screens/unspent_coins/unspent_coins_details_page.dart b/lib/src/screens/unspent_coins/unspent_coins_details_page.dart index 61689b52a..165995ba0 100644 --- a/lib/src/screens/unspent_coins/unspent_coins_details_page.dart +++ b/lib/src/screens/unspent_coins/unspent_coins_details_page.dart @@ -44,7 +44,6 @@ class UnspentCoinsDetailsPage extends BasePage { return TextFieldListRow( title: item.title, value: item.value, - onTapOutside: item.onSubmitted, onSubmitted: item.onSubmitted, ); } diff --git a/lib/src/screens/unspent_coins/widgets/unspent_coins_list_item.dart b/lib/src/screens/unspent_coins/widgets/unspent_coins_list_item.dart index 60a23c99b..236d06f4e 100644 --- a/lib/src/screens/unspent_coins/widgets/unspent_coins_list_item.dart +++ b/lib/src/screens/unspent_coins/widgets/unspent_coins_list_item.dart @@ -103,40 +103,63 @@ class UnspentCoinsListItem extends StatelessWidget { ), maxLines: 1, ), - if (isChange) - Container( - height: 17, - padding: EdgeInsets.only(left: 6, right: 6), - decoration: BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(8.5)), - color: Colors.white), - alignment: Alignment.center, - child: Text( - S.of(context).unspent_change, - style: TextStyle( - color: itemColor, - fontSize: 7, - fontWeight: FontWeight.w600, + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + if (isChange) + Container( + height: 17, + padding: EdgeInsets.only(left: 6, right: 6), + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(8.5)), + color: Colors.white), + alignment: Alignment.center, + child: Text( + S.of(context).unspent_change, + style: TextStyle( + color: itemColor, + fontSize: 7, + fontWeight: FontWeight.w600, + ), + ), ), - ), - ), - if (isSilentPayment) - Container( - height: 17, - padding: EdgeInsets.only(left: 6, right: 6), - decoration: BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(8.5)), - color: Colors.white), - alignment: Alignment.center, - child: Text( - S.of(context).silent_payments, - style: TextStyle( - color: itemColor, - fontSize: 7, - fontWeight: FontWeight.w600, + if (address.toLowerCase().contains("mweb")) + Container( + height: 17, + padding: EdgeInsets.only(left: 6, right: 6), + margin: EdgeInsets.only(left: 6), + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(8.5)), + color: Colors.white), + alignment: Alignment.center, + child: Text( + "MWEB", + style: TextStyle( + color: itemColor, + fontSize: 7, + fontWeight: FontWeight.w600, + ), + ), ), - ), - ), + if (isSilentPayment) + Container( + height: 17, + padding: EdgeInsets.only(left: 6, right: 6), + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(8.5)), + color: Colors.white), + alignment: Alignment.center, + child: Text( + S.of(context).silent_payments, + style: TextStyle( + color: itemColor, + fontSize: 7, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), ], ), ), diff --git a/lib/src/screens/wallet/wallet_edit_page.dart b/lib/src/screens/wallet/wallet_edit_page.dart index 2d1bb9e47..340091a1e 100644 --- a/lib/src/screens/wallet/wallet_edit_page.dart +++ b/lib/src/screens/wallet/wallet_edit_page.dart @@ -1,20 +1,16 @@ import 'package:another_flushbar/flushbar.dart'; -import 'package:cake_wallet/core/auth_service.dart'; import 'package:cake_wallet/core/wallet_name_validator.dart'; +import 'package:cake_wallet/entities/wallet_edit_page_arguments.dart'; import 'package:cake_wallet/palette.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/auth/auth_page.dart'; import 'package:cake_wallet/src/screens/wallet_unlock/wallet_unlock_arguments.dart'; -import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; -import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/utils/show_bar.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/wallet_list/wallet_edit_view_model.dart'; -import 'package:cake_wallet/view_model/wallet_list/wallet_list_item.dart'; -import 'package:cake_wallet/view_model/wallet_new_vm.dart'; import 'package:flutter/material.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; @@ -23,28 +19,27 @@ import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; class WalletEditPage extends BasePage { - WalletEditPage( - {required this.walletEditViewModel, - required this.editingWallet, - required this.walletNewVM, - required this.authService}) - : _formKey = GlobalKey(), + WalletEditPage({ + required this.pageArguments, + }) : _formKey = GlobalKey(), _labelController = TextEditingController(), + walletEditViewModel = pageArguments.walletEditViewModel!, super() { - _labelController.text = editingWallet.name; + _labelController.text = + pageArguments.isWalletGroup ? pageArguments.groupName : pageArguments.editingWallet.name; _labelController.addListener(() => walletEditViewModel.newName = _labelController.text); } final GlobalKey _formKey; final TextEditingController _labelController; + final WalletEditPageArguments pageArguments; final WalletEditViewModel walletEditViewModel; - final WalletNewVM walletNewVM; - final WalletListItem editingWallet; - final AuthService authService; @override - String get title => S.current.wallet_list_edit_wallet; + String get title => pageArguments.isWalletGroup + ? S.current.wallet_list_edit_group_name + : S.current.wallet_list_edit_wallet; Flushbar? _progressBar; @@ -57,11 +52,14 @@ class WalletEditPage extends BasePage { child: Column( children: [ Expanded( - child: Center( - child: BaseTextFormField( - controller: _labelController, - hintText: S.of(context).wallet_list_wallet_name, - validator: WalletNameValidator()))), + child: Center( + child: BaseTextFormField( + controller: _labelController, + hintText: S.of(context).wallet_list_wallet_name, + validator: WalletNameValidator(), + ), + ), + ), Observer( builder: (_) { final isLoading = walletEditViewModel.state is WalletEditRenamePending || @@ -69,24 +67,27 @@ class WalletEditPage extends BasePage { return Row( children: [ - Flexible( - child: Container( - padding: EdgeInsets.only(right: 8.0), - child: LoadingPrimaryButton( - isDisabled: isLoading, - onPressed: () => _removeWallet(context), - text: S.of(context).delete, - color: Palette.red, - textColor: Colors.white), + if (!pageArguments.isWalletGroup) + Flexible( + child: Container( + padding: EdgeInsets.only(right: 8.0), + child: LoadingPrimaryButton( + isDisabled: isLoading, + onPressed: () => _removeWallet(context), + text: S.of(context).delete, + color: Palette.red, + textColor: Colors.white), + ), ), - ), Flexible( child: Container( padding: EdgeInsets.only(left: 8.0), child: LoadingPrimaryButton( onPressed: () async { if (_formKey.currentState?.validate() ?? false) { - if (walletNewVM.nameExists(walletEditViewModel.newName)) { + if (!pageArguments.isWalletGroup && + pageArguments.walletNewVM! + .nameExists(walletEditViewModel.newName)) { showPopUp( context: context, builder: (_) { @@ -102,29 +103,33 @@ class WalletEditPage extends BasePage { try { bool confirmed = false; - if (SettingsStoreBase - .walletPasswordDirectInput) { + if (SettingsStoreBase.walletPasswordDirectInput) { await Navigator.of(context).pushNamed( Routes.walletUnlockLoadable, arguments: WalletUnlockArguments( - authPasswordHandler: - (String password) async { - await walletEditViewModel - .changeName(editingWallet, - password: password); + authPasswordHandler: (String password) async { + await walletEditViewModel.changeName( + pageArguments.editingWallet, + password: password, + isWalletGroup: pageArguments.isWalletGroup, + groupParentAddress: pageArguments.parentAddress, + ); }, - callback: (bool - isAuthenticatedSuccessfully, + callback: (bool isAuthenticatedSuccessfully, AuthPageState auth) async { if (isAuthenticatedSuccessfully) { auth.close(); confirmed = true; } }, - walletName: editingWallet.name, - walletType: editingWallet.type)); + walletName: pageArguments.editingWallet.name, + walletType: pageArguments.editingWallet.type)); } else { - await walletEditViewModel.changeName(editingWallet); + await walletEditViewModel.changeName( + pageArguments.editingWallet, + isWalletGroup: pageArguments.isWalletGroup, + groupParentAddress: pageArguments.parentAddress, + ); confirmed = true; } @@ -154,7 +159,9 @@ class WalletEditPage extends BasePage { } Future _removeWallet(BuildContext context) async { - authService.authenticateAction(context, onAuthSuccess: (isAuthenticatedSuccessfully) async { + pageArguments.authService!.authenticateAction( + context, + onAuthSuccess: (isAuthenticatedSuccessfully) async { if (!isAuthenticatedSuccessfully) { return; } @@ -173,7 +180,8 @@ class WalletEditPage extends BasePage { builder: (BuildContext dialogContext) { return AlertWithTwoActions( alertTitle: S.of(context).delete_wallet, - alertContent: S.of(context).delete_wallet_confirm_message(editingWallet.name), + alertContent: + S.of(context).delete_wallet_confirm_message(pageArguments.editingWallet.name), leftButtonText: S.of(context).cancel, rightButtonText: S.of(context).delete, actionLeftButton: () => Navigator.of(dialogContext).pop(), @@ -187,13 +195,16 @@ class WalletEditPage extends BasePage { Navigator.of(context).pop(); try { - changeProcessText(context, S.of(context).wallet_list_removing_wallet(editingWallet.name)); - await walletEditViewModel.remove(editingWallet); + changeProcessText( + context, S.of(context).wallet_list_removing_wallet(pageArguments.editingWallet.name)); + await walletEditViewModel.remove(pageArguments.editingWallet); hideProgressText(); } catch (e) { changeProcessText( context, - S.of(context).wallet_list_failed_to_remove(editingWallet.name, e.toString()), + S + .of(context) + .wallet_list_failed_to_remove(pageArguments.editingWallet.name, e.toString()), ); } } diff --git a/lib/src/screens/wallet_list/edit_wallet_button_widget.dart b/lib/src/screens/wallet_list/edit_wallet_button_widget.dart new file mode 100644 index 000000000..fd6f1a0ee --- /dev/null +++ b/lib/src/screens/wallet_list/edit_wallet_button_widget.dart @@ -0,0 +1,56 @@ +import 'package:cake_wallet/themes/extensions/filter_theme.dart'; +import 'package:cake_wallet/themes/extensions/receive_page_theme.dart'; +import 'package:flutter/material.dart'; + +class EditWalletButtonWidget extends StatelessWidget { + const EditWalletButtonWidget({ + required this.width, + required this.onTap, + this.isGroup = false, + this.isExpanded = false, + super.key, + }); + + final bool isGroup; + final double width; + final bool isExpanded; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return Container( + width: width, + child: Row( + children: [ + GestureDetector( + onTap: onTap, + child: Center( + child: Container( + height: 40, + width: 44, + padding: EdgeInsets.all(10), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Theme.of(context).extension()!.iconsBackgroundColor, + ), + child: Icon( + Icons.edit, + size: 14, + color: Theme.of(context).extension()!.iconsColor, + ), + ), + ), + ), + if (isGroup) ...{ + SizedBox(width: 6), + Icon( + isExpanded ? Icons.keyboard_arrow_up : Icons.keyboard_arrow_down, + size: 24, + color: Theme.of(context).extension()!.titlesColor, + ), + }, + ], + ), + ); + } +} diff --git a/lib/src/screens/wallet_list/filtered_list.dart b/lib/src/screens/wallet_list/filtered_list.dart index 7149833a8..63a1ae392 100644 --- a/lib/src/screens/wallet_list/filtered_list.dart +++ b/lib/src/screens/wallet_list/filtered_list.dart @@ -7,11 +7,13 @@ class FilteredList extends StatefulWidget { required this.list, required this.itemBuilder, required this.updateFunction, + this.shrinkWrap = false, }); final ObservableList list; final Widget Function(BuildContext, int) itemBuilder; final Function updateFunction; + final bool shrinkWrap; @override FilteredListState createState() => FilteredListState(); @@ -22,6 +24,7 @@ class FilteredListState extends State { Widget build(BuildContext context) { return Observer( builder: (_) => ReorderableListView.builder( + shrinkWrap: widget.shrinkWrap, physics: const BouncingScrollPhysics(), itemBuilder: widget.itemBuilder, itemCount: widget.list.length, diff --git a/lib/src/screens/wallet_list/wallet_list_page.dart b/lib/src/screens/wallet_list/wallet_list_page.dart index 0d6d9e912..d17534f6b 100644 --- a/lib/src/screens/wallet_list/wallet_list_page.dart +++ b/lib/src/screens/wallet_list/wallet_list_page.dart @@ -1,5 +1,9 @@ +import 'package:cake_wallet/core/new_wallet_arguments.dart'; +import 'package:cake_wallet/entities/wallet_edit_page_arguments.dart'; import 'package:cake_wallet/entities/wallet_list_order_types.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/filter_list_widget.dart'; +import 'package:cake_wallet/src/screens/new_wallet/widgets/grouped_wallet_expansion_tile.dart'; +import 'package:cake_wallet/src/screens/wallet_list/edit_wallet_button_widget.dart'; import 'package:cake_wallet/src/screens/wallet_list/filtered_list.dart'; import 'package:cake_wallet/src/screens/wallet_unlock/wallet_unlock_arguments.dart'; import 'package:cake_wallet/store/settings_store.dart'; @@ -7,8 +11,7 @@ import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; import 'package:cake_wallet/src/screens/auth/auth_page.dart'; import 'package:cake_wallet/core/auth_service.dart'; import 'package:cake_wallet/themes/extensions/filter_theme.dart'; -import 'package:cake_wallet/themes/extensions/receive_page_theme.dart'; -import 'package:cake_wallet/utils/device_info.dart'; +import 'package:cake_wallet/themes/extensions/wallet_list_theme.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cake_wallet/utils/show_bar.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; @@ -23,7 +26,6 @@ import 'package:cake_wallet/view_model/wallet_list/wallet_list_view_model.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/wallet_type_utils.dart'; -import 'package:cake_wallet/themes/extensions/wallet_list_theme.dart'; class WalletListPage extends BasePage { WalletListPage({required this.walletListViewModel, required this.authService}); @@ -128,112 +130,186 @@ class WalletListBodyState extends State { child: Column( children: [ Expanded( - child: Container( - child: Observer( - builder: (_) => FilteredList( - list: widget.walletListViewModel.wallets, - updateFunction: widget.walletListViewModel.reorderAccordingToWalletList, - itemBuilder: (__, index) { - final wallet = widget.walletListViewModel.wallets[index]; - final currentColor = wallet.isCurrent - ? Theme.of(context) - .extension()! - .createNewWalletButtonBackgroundColor - : Theme.of(context).colorScheme.background; - final row = GestureDetector( - key: ValueKey(wallet.name), - onTap: () => wallet.isCurrent ? null : _loadWallet(wallet), - child: Container( - height: tileHeight, - width: double.infinity, - child: Row( - children: [ - Container( - height: tileHeight, - width: 4, - decoration: BoxDecoration( - borderRadius: BorderRadius.only( - topRight: Radius.circular(4), - bottomRight: Radius.circular(4)), - color: currentColor), - ), - Expanded( - child: Container( - height: tileHeight, - padding: EdgeInsets.only(left: 20, right: 20), - color: Theme.of(context).colorScheme.background, - alignment: Alignment.centerLeft, - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - wallet.isEnabled - ? _imageFor( - type: wallet.type, - isTestnet: wallet.isTestnet, - ) - : nonWalletTypeIcon, - SizedBox(width: 10), - Flexible( - child: Text( - wallet.name, - maxLines: null, - softWrap: true, - style: TextStyle( - fontSize: DeviceInfo.instance.isDesktop ? 18 : 20, - fontWeight: FontWeight.w500, - color: Theme.of(context) - .extension()! - .titleColor, + 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, + ), + ), + ), + 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, + ); + + 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, + ), + ); + }, + ), + 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( + Routes.walletEdit, + arguments: WalletEditPageArguments( + walletListViewModel: widget.walletListViewModel, + editingWallet: wallet, + ), + ); + }, + ), + ); + }, ), ), - ); - - return wallet.isCurrent - ? row - : Row( - key: ValueKey(wallet.name), - children: [ - Expanded(child: row), - GestureDetector( - onTap: () => Navigator.of(context).pushNamed(Routes.walletEdit, - arguments: [widget.walletListViewModel, wallet]), - child: Container( - padding: EdgeInsets.only( - right: DeviceInfo.instance.isMobile ? 20 : 40), - child: Center( - child: Container( - height: 40, - width: 44, - padding: EdgeInsets.all(10), - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Theme.of(context) - .extension()! - .iconsBackgroundColor, - ), - child: Icon( - Icons.edit, - size: 14, - color: Theme.of(context) - .extension()! - .iconsColor, - ), - ), - ), - ), - ), - ], - ); + ), }, - ), + ], ), ), ), @@ -249,14 +325,18 @@ class WalletListBodyState extends State { widget.authService.authenticateAction( context, route: Routes.newWallet, - arguments: widget.walletListViewModel.currentWalletType, + arguments: NewWalletArguments( + type: widget.walletListViewModel.currentWalletType, + ), conditionToDetermineIfToUse2FA: widget.walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets, ); } else { Navigator.of(context).pushNamed( Routes.newWallet, - arguments: widget.walletListViewModel.currentWalletType, + arguments: NewWalletArguments( + type: widget.walletListViewModel.currentWalletType, + ), ); } } else { @@ -340,15 +420,15 @@ class WalletListBodyState extends State { Future _loadWallet(WalletListItem wallet) async { if (SettingsStoreBase.walletPasswordDirectInput) { - Navigator.of(context).pushNamed( - Routes.walletUnlockLoadable, + Navigator.of(context).pushNamed(Routes.walletUnlockLoadable, arguments: WalletUnlockArguments( callback: (bool isAuthenticatedSuccessfully, AuthPageState auth) async { if (isAuthenticatedSuccessfully) { auth.close(); setState(() {}); } - }, walletName: wallet.name, + }, + walletName: wallet.name, walletType: wallet.type)); return; } diff --git a/lib/src/screens/welcome/welcome_page.dart b/lib/src/screens/welcome/welcome_page.dart index defc8e2c8..5b7b7f06d 100644 --- a/lib/src/screens/welcome/welcome_page.dart +++ b/lib/src/screens/welcome/welcome_page.dart @@ -133,6 +133,7 @@ class WelcomePage extends BasePage { Padding( padding: EdgeInsets.only(top: 24), child: PrimaryImageButton( + key: ValueKey('welcome_page_create_new_wallet_button_key'), onPressed: () => Navigator.pushNamed(context, Routes.newWalletFromWelcome), image: newWalletImage, text: S.of(context).create_new, @@ -146,6 +147,7 @@ class WelcomePage extends BasePage { Padding( padding: EdgeInsets.only(top: 10), child: PrimaryImageButton( + key: ValueKey('welcome_page_restore_wallet_button_key'), onPressed: () { Navigator.pushNamed(context, Routes.restoreOptions, arguments: true); }, diff --git a/lib/src/widgets/address_text_field.dart b/lib/src/widgets/address_text_field.dart index f229ea8ef..0b1ef4796 100644 --- a/lib/src/widgets/address_text_field.dart +++ b/lib/src/widgets/address_text_field.dart @@ -15,28 +15,27 @@ import 'package:permission_handler/permission_handler.dart'; enum AddressTextFieldOption { paste, qrCode, addressBook, walletAddresses } class AddressTextField extends StatelessWidget { - AddressTextField( - {required this.controller, - this.isActive = true, - this.placeholder, - this.options = const [ - AddressTextFieldOption.qrCode, - AddressTextFieldOption.addressBook - ], - this.onURIScanned, - this.focusNode, - this.isBorderExist = true, - this.buttonColor, - this.borderColor, - this.iconColor, - this.textStyle, - this.hintStyle, - this.validator, - this.onPushPasteButton, - this.onPushAddressBookButton, - this.onPushAddressPickerButton, - this.onSelectedContact, - this.selectedCurrency}); + AddressTextField({ + required this.controller, + this.isActive = true, + this.placeholder, + this.options = const [AddressTextFieldOption.qrCode, AddressTextFieldOption.addressBook], + this.onURIScanned, + this.focusNode, + this.isBorderExist = true, + this.buttonColor, + this.borderColor, + this.iconColor, + this.textStyle, + this.hintStyle, + this.validator, + this.onPushPasteButton, + this.onPushAddressBookButton, + this.onPushAddressPickerButton, + this.onSelectedContact, + this.selectedCurrency, + this.addressKey, + }); static const prefixIconWidth = 34.0; static const prefixIconHeight = 34.0; @@ -60,12 +59,14 @@ class AddressTextField extends StatelessWidget { final Function(BuildContext context)? onPushAddressPickerButton; final Function(ContactBase contact)? onSelectedContact; final CryptoCurrency? selectedCurrency; + final Key? addressKey; @override Widget build(BuildContext context) { return Stack( children: [ TextFormField( + key: addressKey, enableIMEPersonalizedLearning: false, keyboardType: TextInputType.visiblePassword, onFieldSubmitted: (_) => FocusScope.of(context).unfocus(), diff --git a/lib/src/widgets/alert_close_button.dart b/lib/src/widgets/alert_close_button.dart index e3ff037a9..6ef0bdaa5 100644 --- a/lib/src/widgets/alert_close_button.dart +++ b/lib/src/widgets/alert_close_button.dart @@ -3,7 +3,12 @@ import 'package:cake_wallet/palette.dart'; import 'package:flutter/material.dart'; class AlertCloseButton extends StatelessWidget { - AlertCloseButton({this.image, this.bottom, this.onTap}); + AlertCloseButton({ + this.image, + this.bottom, + this.onTap, + super.key, + }); final VoidCallback? onTap; diff --git a/lib/src/widgets/alert_with_no_action.dart.dart b/lib/src/widgets/alert_with_no_action.dart.dart new file mode 100644 index 000000000..623656397 --- /dev/null +++ b/lib/src/widgets/alert_with_no_action.dart.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; +import 'package:cake_wallet/src/widgets/base_alert_dialog.dart'; + +class AlertWithNoAction extends BaseAlertDialog { + AlertWithNoAction({ + required this.alertTitle, + required this.alertContent, + this.alertBarrierDismissible = true, + Key? key, + }); + + final String alertTitle; + final String alertContent; + final bool alertBarrierDismissible; + + @override + String get titleText => alertTitle; + + @override + String get contentText => alertContent; + + @override + bool get barrierDismissible => alertBarrierDismissible; + + @override + bool get isBottomDividerExists => false; + + @override + Widget actionButtons(BuildContext context) => Container(height: 60); +} diff --git a/lib/src/widgets/alert_with_one_action.dart b/lib/src/widgets/alert_with_one_action.dart index 7ad0ac1af..6f0ba5e8b 100644 --- a/lib/src/widgets/alert_with_one_action.dart +++ b/lib/src/widgets/alert_with_one_action.dart @@ -9,7 +9,9 @@ class AlertWithOneAction extends BaseAlertDialog { required this.buttonAction, this.alertBarrierDismissible = true, this.headerTitleText, - this.headerImageProfileUrl + this.headerImageProfileUrl, + this.buttonKey, + Key? key, }); final String alertTitle; @@ -19,6 +21,7 @@ class AlertWithOneAction extends BaseAlertDialog { final bool alertBarrierDismissible; final String? headerTitleText; final String? headerImageProfileUrl; + final Key? buttonKey; @override String get titleText => alertTitle; @@ -45,6 +48,7 @@ class AlertWithOneAction extends BaseAlertDialog { child: ButtonTheme( minWidth: double.infinity, child: TextButton( + key: buttonKey, onPressed: buttonAction, // FIX-ME: Style //highlightColor: Colors.transparent, @@ -62,4 +66,4 @@ class AlertWithOneAction extends BaseAlertDialog { ), ); } -} \ No newline at end of file +} diff --git a/lib/src/widgets/alert_with_two_actions.dart b/lib/src/widgets/alert_with_two_actions.dart index ddb11c3ee..e3d4408a6 100644 --- a/lib/src/widgets/alert_with_two_actions.dart +++ b/lib/src/widgets/alert_with_two_actions.dart @@ -14,6 +14,9 @@ class AlertWithTwoActions extends BaseAlertDialog { this.isDividerExist = false, // this.leftActionColor, // this.rightActionColor, + this.alertRightActionButtonKey, + this.alertLeftActionButtonKey, + this.alertDialogKey, }); final String alertTitle; @@ -26,6 +29,9 @@ class AlertWithTwoActions extends BaseAlertDialog { // final Color leftActionColor; // final Color rightActionColor; final bool isDividerExist; + final Key? alertRightActionButtonKey; + final Key? alertLeftActionButtonKey; + final Key? alertDialogKey; @override String get titleText => alertTitle; @@ -47,4 +53,13 @@ class AlertWithTwoActions extends BaseAlertDialog { // Color get rightButtonColor => rightActionColor; @override bool get isDividerExists => isDividerExist; + + @override + Key? get dialogKey => alertDialogKey; + + @override + Key? get leftActionButtonKey => alertLeftActionButtonKey; + + @override + Key? get rightActionButtonKey => alertRightActionButtonKey; } diff --git a/lib/src/widgets/base_alert_dialog.dart b/lib/src/widgets/base_alert_dialog.dart index 5c1111740..1b521a427 100644 --- a/lib/src/widgets/base_alert_dialog.dart +++ b/lib/src/widgets/base_alert_dialog.dart @@ -17,6 +17,8 @@ class BaseAlertDialog extends StatelessWidget { bool get isDividerExists => false; + bool get isBottomDividerExists => true; + VoidCallback get actionLeft => () {}; VoidCallback get actionRight => () {}; @@ -33,6 +35,12 @@ class BaseAlertDialog extends StatelessWidget { String? get headerImageUrl => null; + Key? leftActionButtonKey; + + Key? rightActionButtonKey; + + Key? dialogKey; + Widget title(BuildContext context) { return Text( titleText, @@ -87,6 +95,7 @@ class BaseAlertDialog extends StatelessWidget { children: [ Expanded( child: TextButton( + key: leftActionButtonKey, onPressed: actionLeft, style: TextButton.styleFrom( backgroundColor: @@ -109,6 +118,7 @@ class BaseAlertDialog extends StatelessWidget { const VerticalSectionDivider(), Expanded( child: TextButton( + key: rightActionButtonKey, onPressed: actionRight, style: TextButton.styleFrom( backgroundColor: @@ -152,6 +162,7 @@ class BaseAlertDialog extends StatelessWidget { @override Widget build(BuildContext context) { return GestureDetector( + key: key, onTap: () => barrierDismissible ? Navigator.of(context).pop() : null, child: Container( color: Colors.transparent, @@ -196,7 +207,7 @@ class BaseAlertDialog extends StatelessWidget { ) ], ), - const HorizontalSectionDivider(), + if (isBottomDividerExists) const HorizontalSectionDivider(), ClipRRect( borderRadius: BorderRadius.all(Radius.circular(30)), child: actionButtons(context)) diff --git a/lib/src/widgets/base_text_form_field.dart b/lib/src/widgets/base_text_form_field.dart index 534e6dae2..4648b88cc 100644 --- a/lib/src/widgets/base_text_form_field.dart +++ b/lib/src/widgets/base_text_form_field.dart @@ -30,7 +30,8 @@ class BaseTextFormField extends StatelessWidget { this.focusNode, this.initialValue, this.onSubmit, - this.borderWidth = 1.0}); + this.borderWidth = 1.0, + super.key}); final TextEditingController? controller; final TextInputType? keyboardType; diff --git a/lib/src/widgets/blockchain_height_widget.dart b/lib/src/widgets/blockchain_height_widget.dart index e0f83a4f4..9d66c1789 100644 --- a/lib/src/widgets/blockchain_height_widget.dart +++ b/lib/src/widgets/blockchain_height_widget.dart @@ -18,8 +18,10 @@ class BlockchainHeightWidget extends StatefulWidget { this.onHeightOrDateEntered, this.hasDatePicker = true, this.isSilentPaymentsScan = false, + this.isMwebScan = false, this.toggleSingleScan, this.doSingleScan = false, + this.bitcoinMempoolAPIEnabled, required this.walletType, }) : super(key: key); @@ -28,7 +30,9 @@ class BlockchainHeightWidget extends StatefulWidget { final FocusNode? focusNode; final bool hasDatePicker; final bool isSilentPaymentsScan; + final bool isMwebScan; final bool doSingleScan; + final Future? bitcoinMempoolAPIEnabled; final Function()? toggleSingleScan; final WalletType walletType; @@ -79,7 +83,8 @@ class BlockchainHeightState extends State { child: BaseTextFormField( focusNode: widget.focusNode, controller: restoreHeightController, - keyboardType: TextInputType.numberWithOptions(signed: false, decimal: false), + keyboardType: + TextInputType.numberWithOptions(signed: false, decimal: false), hintText: widget.isSilentPaymentsScan ? S.of(context).silent_payments_scan_from_height : S.of(context).widgets_restore_from_blockheight, @@ -146,7 +151,9 @@ class BlockchainHeightState extends State { : S.of(context).restore_from_date_or_blockheight, textAlign: TextAlign.center, style: TextStyle( - fontSize: 12, fontWeight: FontWeight.normal, color: Theme.of(context).hintColor), + fontSize: 12, + fontWeight: FontWeight.normal, + color: Theme.of(context).hintColor), ), ) ] @@ -165,8 +172,13 @@ class BlockchainHeightState extends State { if (date != null) { int height; - if (widget.isSilentPaymentsScan) { - height = bitcoin!.getHeightByDate(date: date); + if (widget.isMwebScan) { + height = bitcoin!.getLitecoinHeightByDate(date: date); + } else if (widget.isSilentPaymentsScan) { + height = await bitcoin!.getHeightByDate( + date: date, + bitcoinMempoolAPIEnabled: await widget.bitcoinMempoolAPIEnabled, + ); } else { if (widget.walletType == WalletType.monero) { height = monero!.getHeightByDate(date: date); diff --git a/lib/src/widgets/check_box_picker.dart b/lib/src/widgets/check_box_picker.dart index b4db82628..847b5b1c4 100644 --- a/lib/src/widgets/check_box_picker.dart +++ b/lib/src/widgets/check_box_picker.dart @@ -113,10 +113,6 @@ class CheckBoxPickerState extends State { return GestureDetector( onTap: () { - if (item.isDisabled) { - return; - } - bool newValue = !item.value; item.value = newValue; widget.onChanged(index, newValue); @@ -134,7 +130,7 @@ class CheckBoxPickerState extends State { borderColor: Theme.of(context).dividerColor, iconColor: Colors.white, onChanged: (bool? value) { - if (value == null || item.isDisabled) { + if (value == null) { return; } diff --git a/lib/src/widgets/dashboard_card_widget.dart b/lib/src/widgets/dashboard_card_widget.dart index 5a8ca14a4..d9b545040 100644 --- a/lib/src/widgets/dashboard_card_widget.dart +++ b/lib/src/widgets/dashboard_card_widget.dart @@ -11,6 +11,7 @@ class DashBoardRoundedCardWidget extends StatelessWidget { required this.subTitle, this.hint, this.svgPicture, + this.image, this.icon, this.onClose, this.customBorder, @@ -22,7 +23,8 @@ class DashBoardRoundedCardWidget extends StatelessWidget { final String subTitle; final Widget? hint; final SvgPicture? svgPicture; - final Icon? icon; + final Widget? icon; + final Image? image; final double? customBorder; @override @@ -77,7 +79,8 @@ class DashBoardRoundedCardWidget extends StatelessWidget { ], ), ), - if (svgPicture != null) svgPicture!, + if (image != null) image! + else if (svgPicture != null) svgPicture!, if (icon != null) icon! ], ), diff --git a/lib/src/widgets/option_tile.dart b/lib/src/widgets/option_tile.dart index 8b46641fb..31f958f54 100644 --- a/lib/src/widgets/option_tile.dart +++ b/lib/src/widgets/option_tile.dart @@ -4,12 +4,16 @@ import 'package:flutter/material.dart'; class OptionTile extends StatelessWidget { const OptionTile( {required this.onPressed, - required this.image, + this.image, + this.icon, required this.title, - required this.description}); + required this.description, + super.key}) + : assert(image!=null || icon!=null); final VoidCallback onPressed; - final Image image; + final Image? image; + final Icon? icon; final String title; final String description; @@ -30,7 +34,7 @@ class OptionTile extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ - image, + icon ?? image!, Expanded( child: Padding( padding: EdgeInsets.only(left: 16), diff --git a/lib/src/widgets/picker.dart b/lib/src/widgets/picker.dart index a7cb03a4e..801a79595 100644 --- a/lib/src/widgets/picker.dart +++ b/lib/src/widgets/picker.dart @@ -4,6 +4,7 @@ import 'dart:math'; import 'package:cake_wallet/src/widgets/search_bar_widget.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; +import 'package:cw_core/transaction_priority.dart'; import 'package:flutter/material.dart'; import 'package:cw_core/currency.dart'; import 'package:cake_wallet/src/widgets/picker_wrapper_widget.dart'; @@ -11,6 +12,7 @@ import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; import 'package:cake_wallet/themes/extensions/cake_scrollbar_theme.dart'; import 'package:cake_wallet/themes/extensions/picker_theme.dart'; +//TODO(David): PickerWidget is intertwined and confusing as is, find a way to optimize? class Picker extends StatefulWidget { Picker({ required this.selectedAtIndex, @@ -153,6 +155,7 @@ class _PickerState extends State> { Container( padding: EdgeInsets.symmetric(horizontal: padding), child: Text( + key: ValueKey('picker_title_text_key'), widget.title!, textAlign: TextAlign.center, style: TextStyle( @@ -189,7 +192,10 @@ class _PickerState extends State> { Padding( padding: const EdgeInsets.all(16), child: SearchBarWidget( - searchController: searchController, hintText: widget.hintText), + key: ValueKey('picker_search_bar_key'), + searchController: searchController, + hintText: widget.hintText, + ), ), Divider( color: Theme.of(context).extension()!.dividerColor, @@ -203,6 +209,7 @@ class _PickerState extends State> { children: [ filteredItems.length > 3 ? Scrollbar( + key: ValueKey('picker_scrollbar_key'), controller: controller, child: itemsList(), ) @@ -213,6 +220,7 @@ class _PickerState extends State> { left: padding, right: padding, child: Text( + key: ValueKey('picker_descriptinon_text_key'), widget.description!, textAlign: TextAlign.center, style: TextStyle( @@ -242,6 +250,7 @@ class _PickerState extends State> { if (widget.isWrapped) { return PickerWrapperWidget( + key: ValueKey('picker_wrapper_widget_key'), hasTitle: widget.title?.isNotEmpty ?? false, children: [content], ); @@ -260,6 +269,7 @@ class _PickerState extends State> { color: Theme.of(context).extension()!.dividerColor, child: widget.isGridView ? GridView.builder( + key: ValueKey('picker_items_grid_view_key'), padding: EdgeInsets.zero, controller: controller, shrinkWrap: true, @@ -275,6 +285,7 @@ class _PickerState extends State> { : buildItem(index), ) : ListView.separated( + key: ValueKey('picker_items_list_view_key'), padding: EdgeInsets.zero, controller: controller, shrinkWrap: true, @@ -293,10 +304,25 @@ class _PickerState extends State> { ); } + String _getItemName(Item item) { + String itemName; + if (item is Currency) { + itemName = item.name; + } else if (item is TransactionPriority) { + itemName = item.title; + } else { + itemName = ''; + } + + return itemName; + } + Widget buildItem(int index) { final item = widget.headerEnabled ? filteredItems[index] : items[index]; final tag = item is Currency ? item.tag : null; + final itemName = _getItemName(item); + final icon = _getItemIcon(item); final image = images.isNotEmpty ? filteredImages[index] : icon; @@ -316,6 +342,7 @@ class _PickerState extends State> { children: [ Flexible( child: Text( + key: ValueKey('picker_items_index_${itemName}_text_key'), widget.displayItem?.call(item) ?? item.toString(), softWrap: true, style: TextStyle( @@ -335,6 +362,7 @@ class _PickerState extends State> { height: 18.0, child: Center( child: Text( + key: ValueKey('picker_items_index_${index}_tag_key'), tag, style: TextStyle( fontSize: 7.0, @@ -358,6 +386,7 @@ class _PickerState extends State> { ); return GestureDetector( + key: ValueKey('picker_items_index_${itemName}_button_key'), onTap: () { if (widget.closeOnItemSelected) Navigator.of(context).pop(); onItemSelected(item!); @@ -383,6 +412,7 @@ class _PickerState extends State> { final item = items[index]; final tag = item is Currency ? item.tag : null; + final itemName = _getItemName(item); final icon = _getItemIcon(item); final image = images.isNotEmpty ? images[index] : icon; @@ -390,6 +420,7 @@ class _PickerState extends State> { final isCustomItem = widget.customItemIndex != null && index == widget.customItemIndex; final itemContent = Row( + key: ValueKey('picker_selected_item_row_key'), mainAxisSize: MainAxisSize.max, mainAxisAlignment: widget.mainAxisAlignment, crossAxisAlignment: CrossAxisAlignment.center, @@ -402,6 +433,7 @@ class _PickerState extends State> { children: [ Flexible( child: Text( + key: ValueKey('picker_items_index_${itemName}_selected_item_text_key'), widget.displayItem?.call(item) ?? item.toString(), softWrap: true, style: TextStyle( @@ -445,6 +477,7 @@ class _PickerState extends State> { ); return GestureDetector( + key: ValueKey('picker_items_index_${itemName}_selected_item_button_key'), onTap: () { if (widget.closeOnItemSelected) Navigator.of(context).pop(); }, diff --git a/lib/src/widgets/picker_wrapper_widget.dart b/lib/src/widgets/picker_wrapper_widget.dart index f4e52c5cd..ac863ac5d 100644 --- a/lib/src/widgets/picker_wrapper_widget.dart +++ b/lib/src/widgets/picker_wrapper_widget.dart @@ -4,7 +4,12 @@ import 'package:cake_wallet/src/widgets/alert_background.dart'; import 'package:cake_wallet/src/widgets/alert_close_button.dart'; class PickerWrapperWidget extends StatelessWidget { - PickerWrapperWidget({required this.children, this.hasTitle = false, this.onClose}); + PickerWrapperWidget({ + required this.children, + this.hasTitle = false, + this.onClose, + super.key, + }); final List children; final bool hasTitle; @@ -29,8 +34,8 @@ class PickerWrapperWidget extends StatelessWidget { final containerBottom = screenCenter - containerCenter; // position the close button right below the search container - closeButtonBottom = closeButtonBottom - - containerBottom + (!hasTitle ? padding : padding / 1.5); + closeButtonBottom = + closeButtonBottom - containerBottom + (!hasTitle ? padding : padding / 1.5); } return AlertBackground( @@ -46,7 +51,11 @@ class PickerWrapperWidget extends StatelessWidget { children: children, ), SizedBox(height: ResponsiveLayoutUtilBase.kPopupSpaceHeight), - AlertCloseButton(bottom: closeButtonBottom, onTap: onClose), + AlertCloseButton( + key: ValueKey('picker_wrapper_close_button_key'), + bottom: closeButtonBottom, + onTap: onClose, + ), ], ), ), diff --git a/lib/src/widgets/primary_button.dart b/lib/src/widgets/primary_button.dart index 5f6b50f8b..06bfda157 100644 --- a/lib/src/widgets/primary_button.dart +++ b/lib/src/widgets/primary_button.dart @@ -4,15 +4,17 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; class PrimaryButton extends StatelessWidget { - const PrimaryButton( - {required this.text, - required this.color, - required this.textColor, - this.onPressed, - this.isDisabled = false, - this.isDottedBorder = false, - this.borderColor = Colors.black, - this.onDisabledPressed}); + const PrimaryButton({ + required this.text, + required this.color, + required this.textColor, + this.onPressed, + this.isDisabled = false, + this.isDottedBorder = false, + this.borderColor = Colors.black, + this.onDisabledPressed, + super.key, + }); final VoidCallback? onPressed; final VoidCallback? onDisabledPressed; @@ -31,23 +33,23 @@ class PrimaryButton extends StatelessWidget { width: double.infinity, height: 52.0, child: TextButton( - onPressed: isDisabled - ? (onDisabledPressed != null ? onDisabledPressed : null) : onPressed, - style: ButtonStyle(backgroundColor: MaterialStateProperty.all(isDisabled ? color.withOpacity(0.5) : color), + onPressed: + isDisabled ? (onDisabledPressed != null ? onDisabledPressed : null) : onPressed, + style: ButtonStyle( + backgroundColor: + MaterialStateProperty.all(isDisabled ? color.withOpacity(0.5) : color), shape: MaterialStateProperty.all( RoundedRectangleBorder( borderRadius: BorderRadius.circular(26.0), ), ), - overlayColor: MaterialStateProperty.all(Colors.transparent)), + overlayColor: MaterialStateProperty.all(Colors.transparent)), child: Text(text, textAlign: TextAlign.center, style: TextStyle( fontSize: 15.0, fontWeight: FontWeight.w600, - color: isDisabled - ? textColor.withOpacity(0.5) - : textColor)), + color: isDisabled ? textColor.withOpacity(0.5) : textColor)), )), ); @@ -64,13 +66,15 @@ class PrimaryButton extends StatelessWidget { } class LoadingPrimaryButton extends StatelessWidget { - const LoadingPrimaryButton( - {required this.onPressed, - required this.text, - required this.color, - required this.textColor, - this.isDisabled = false, - this.isLoading = false}); + const LoadingPrimaryButton({ + required this.onPressed, + required this.text, + required this.color, + required this.textColor, + this.isDisabled = false, + this.isLoading = false, + super.key, + }); final VoidCallback onPressed; final Color color; @@ -88,41 +92,38 @@ class LoadingPrimaryButton extends StatelessWidget { height: 52.0, child: TextButton( onPressed: (isLoading || isDisabled) ? null : onPressed, - style: ButtonStyle(backgroundColor: MaterialStateProperty.all(isDisabled ? color.withOpacity(0.5) : color), - shape: MaterialStateProperty.all( - RoundedRectangleBorder( - borderRadius: BorderRadius.circular(26.0), - ), - )), - + style: ButtonStyle( + backgroundColor: + MaterialStateProperty.all(isDisabled ? color.withOpacity(0.5) : color), + shape: MaterialStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(26.0), + ), + )), child: isLoading ? CupertinoActivityIndicator(animating: true) : Text(text, - style: TextStyle( - fontSize: 15.0, - fontWeight: FontWeight.w600, - color: isDisabled - ? textColor.withOpacity(0.5) - : textColor - )), + style: TextStyle( + fontSize: 15.0, + fontWeight: FontWeight.w600, + color: isDisabled ? textColor.withOpacity(0.5) : textColor)), )), ); } } class PrimaryIconButton extends StatelessWidget { - const PrimaryIconButton({ - required this.onPressed, - required this.iconData, - required this.text, - required this.color, - required this.borderColor, - required this.iconColor, - required this.iconBackgroundColor, - required this.textColor, - this.mainAxisAlignment = MainAxisAlignment.start, - this.radius = 26 - }); + const PrimaryIconButton( + {required this.onPressed, + required this.iconData, + required this.text, + required this.color, + required this.borderColor, + required this.iconColor, + required this.iconBackgroundColor, + required this.textColor, + this.mainAxisAlignment = MainAxisAlignment.start, + this.radius = 26, super.key}); final VoidCallback onPressed; final IconData iconData; @@ -144,7 +145,8 @@ class PrimaryIconButton extends StatelessWidget { height: 52.0, child: TextButton( onPressed: onPressed, - style: ButtonStyle(backgroundColor: MaterialStateProperty.all(color), + style: ButtonStyle( + backgroundColor: MaterialStateProperty.all(color), shape: MaterialStateProperty.all( RoundedRectangleBorder( borderRadius: BorderRadius.circular(radius), @@ -158,21 +160,15 @@ class PrimaryIconButton extends StatelessWidget { Container( width: 26.0, height: 52.0, - decoration: BoxDecoration( - shape: BoxShape.circle, color: iconBackgroundColor), - child: Center( - child: Icon(iconData, color: iconColor, size: 22.0) - ), + decoration: BoxDecoration(shape: BoxShape.circle, color: iconBackgroundColor), + child: Center(child: Icon(iconData, color: iconColor, size: 22.0)), ), ], ), Container( height: 52.0, child: Center( - child: Text(text, - style: TextStyle( - fontSize: 16.0, - color: textColor)), + child: Text(text, style: TextStyle(fontSize: 16.0, color: textColor)), ), ) ], @@ -189,7 +185,7 @@ class PrimaryImageButton extends StatelessWidget { required this.text, required this.color, required this.textColor, - this.borderColor = Colors.transparent}); + this.borderColor = Colors.transparent, super.key}); final VoidCallback onPressed; final Image image; @@ -206,31 +202,27 @@ class PrimaryImageButton extends StatelessWidget { width: double.infinity, height: 52.0, child: TextButton( - onPressed: onPressed, - style: ButtonStyle(backgroundColor: MaterialStateProperty.all(color), - shape: MaterialStateProperty.all( - RoundedRectangleBorder( - borderRadius: BorderRadius.circular(26.0), - ), - )), - child:Center( - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - image, - SizedBox(width: 15), - Text( - text, - style: TextStyle( - fontSize: 15, - fontWeight: FontWeight.w600, - color: textColor + onPressed: onPressed, + style: ButtonStyle( + backgroundColor: MaterialStateProperty.all(color), + shape: MaterialStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(26.0), ), - ) - ], - ), - ) - )), + )), + child: Center( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + image, + SizedBox(width: 15), + Text( + text, + style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: textColor), + ) + ], + ), + ))), ); } } diff --git a/lib/src/widgets/scollable_with_bottom_section.dart b/lib/src/widgets/scollable_with_bottom_section.dart index e15be610e..07a070204 100644 --- a/lib/src/widgets/scollable_with_bottom_section.dart +++ b/lib/src/widgets/scollable_with_bottom_section.dart @@ -9,6 +9,7 @@ class ScrollableWithBottomSection extends StatefulWidget { this.contentPadding, this.bottomSectionPadding, this.topSectionPadding, + this.scrollableKey, }); final Widget content; @@ -17,6 +18,7 @@ class ScrollableWithBottomSection extends StatefulWidget { final EdgeInsets? contentPadding; final EdgeInsets? bottomSectionPadding; final EdgeInsets? topSectionPadding; + final Key? scrollableKey; @override ScrollableWithBottomSectionState createState() => ScrollableWithBottomSectionState(); @@ -35,6 +37,7 @@ class ScrollableWithBottomSectionState extends State supportedSeedTypes; + final List supportedSeedTypes; } final List seedLanguages = [ SeedLanguagePickerOption('English', S.current.seed_language_english, - Image.asset('assets/images/flags/usa.png'), [SeedType.legacy, SeedType.polyseed]), + Image.asset('assets/images/flags/usa.png'), [MoneroSeedType.legacy, MoneroSeedType.polyseed]), SeedLanguagePickerOption('Chinese (Simplified)', S.current.seed_language_chinese, - Image.asset('assets/images/flags/chn.png'), [SeedType.legacy, SeedType.polyseed]), + Image.asset('assets/images/flags/chn.png'), [MoneroSeedType.legacy, MoneroSeedType.polyseed]), SeedLanguagePickerOption('Chinese (Traditional)', S.current.seed_language_chinese_traditional, - Image.asset('assets/images/flags/chn.png'), [SeedType.polyseed]), + Image.asset('assets/images/flags/chn.png'), [MoneroSeedType.polyseed]), SeedLanguagePickerOption('Dutch', S.current.seed_language_dutch, - Image.asset('assets/images/flags/nld.png'), [SeedType.legacy]), + Image.asset('assets/images/flags/nld.png'), [MoneroSeedType.legacy]), SeedLanguagePickerOption('German', S.current.seed_language_german, - Image.asset('assets/images/flags/deu.png'), [SeedType.legacy]), + Image.asset('assets/images/flags/deu.png'), [MoneroSeedType.legacy]), SeedLanguagePickerOption('Japanese', S.current.seed_language_japanese, - Image.asset('assets/images/flags/jpn.png'), [SeedType.legacy, SeedType.polyseed]), + Image.asset('assets/images/flags/jpn.png'), [MoneroSeedType.legacy, MoneroSeedType.polyseed]), SeedLanguagePickerOption('Korean', S.current.seed_language_korean, - Image.asset('assets/images/flags/kor.png'), [SeedType.polyseed]), + Image.asset('assets/images/flags/kor.png'), [MoneroSeedType.polyseed]), SeedLanguagePickerOption('Portuguese', S.current.seed_language_portuguese, - Image.asset('assets/images/flags/prt.png'), [SeedType.legacy, SeedType.polyseed]), + Image.asset('assets/images/flags/prt.png'), [MoneroSeedType.legacy, MoneroSeedType.polyseed]), SeedLanguagePickerOption('Russian', S.current.seed_language_russian, - Image.asset('assets/images/flags/rus.png'), [SeedType.legacy]), + Image.asset('assets/images/flags/rus.png'), [MoneroSeedType.legacy]), SeedLanguagePickerOption('Czech', S.current.seed_language_czech, - Image.asset('assets/images/flags/czk.png'), [SeedType.polyseed]), + Image.asset('assets/images/flags/czk.png'), [MoneroSeedType.polyseed]), SeedLanguagePickerOption('Spanish', S.current.seed_language_spanish, - Image.asset('assets/images/flags/esp.png'), [SeedType.legacy, SeedType.polyseed]), + Image.asset('assets/images/flags/esp.png'), [MoneroSeedType.legacy, MoneroSeedType.polyseed]), SeedLanguagePickerOption('French', S.current.seed_language_french, - Image.asset('assets/images/flags/fra.png'), [SeedType.legacy, SeedType.polyseed]), + Image.asset('assets/images/flags/fra.png'), [MoneroSeedType.legacy, MoneroSeedType.polyseed]), SeedLanguagePickerOption('Italian', S.current.seed_language_italian, - Image.asset('assets/images/flags/ita.png'), [SeedType.legacy, SeedType.polyseed]), + Image.asset('assets/images/flags/ita.png'), [MoneroSeedType.legacy, MoneroSeedType.polyseed]), ]; const defaultSeedLanguage = 'English'; @@ -51,11 +51,11 @@ class SeedLanguagePicker extends StatefulWidget { SeedLanguagePicker( {Key? key, this.selected = defaultSeedLanguage, - this.seedType = SeedType.defaultSeedType, + this.seedType = MoneroSeedType.defaultSeedType, required this.onItemSelected}) : super(key: key); - final SeedType seedType; + final MoneroSeedType seedType; final String selected; final Function(String) onItemSelected; @@ -68,7 +68,7 @@ class SeedLanguagePickerState extends State { SeedLanguagePickerState( {required this.selected, required this.onItemSelected, required this.seedType}); - final SeedType seedType; + final MoneroSeedType seedType; final String selected; final Function(String) onItemSelected; diff --git a/lib/src/widgets/seed_language_selector.dart b/lib/src/widgets/seed_language_selector.dart index dde78c58c..87f3aa573 100644 --- a/lib/src/widgets/seed_language_selector.dart +++ b/lib/src/widgets/seed_language_selector.dart @@ -7,11 +7,11 @@ import 'package:flutter/material.dart'; class SeedLanguageSelector extends StatefulWidget { SeedLanguageSelector( - {Key? key, required this.initialSelected, this.seedType = SeedType.defaultSeedType}) + {Key? key, required this.initialSelected, this.seedType = MoneroSeedType.defaultSeedType}) : super(key: key); final String initialSelected; - final SeedType seedType; + final MoneroSeedType seedType; @override SeedLanguageSelectorState createState() => SeedLanguageSelectorState(selected: initialSelected); diff --git a/lib/src/widgets/seed_widget.dart b/lib/src/widgets/seed_widget.dart index d71208bb2..1c94718f1 100644 --- a/lib/src/widgets/seed_widget.dart +++ b/lib/src/widgets/seed_widget.dart @@ -9,11 +9,15 @@ import 'package:flutter/services.dart'; class SeedWidget extends StatefulWidget { SeedWidget({ - Key? key, required this.language, required this.type, - this.onSeedChange}) : super(key: key); - + this.onSeedChange, + this.pasteButtonKey, + this.seedTextFieldKey, + super.key, + }); + final Key? seedTextFieldKey; + final Key? pasteButtonKey; final String language; final WalletType type; final void Function(String)? onSeedChange; @@ -78,11 +82,11 @@ class SeedWidgetState extends State { top: 10, left: 0, child: Text(S.of(context).enter_seed_phrase, - style: TextStyle( - fontSize: 16.0, color: Theme.of(context).hintColor))), + style: TextStyle(fontSize: 16.0, color: Theme.of(context).hintColor))), Padding( padding: EdgeInsets.only(right: 40, top: 10), child: ValidatableAnnotatedEditableText( + key: widget.seedTextFieldKey, cursorColor: Colors.blue, backgroundCursorColor: Colors.blue, validStyle: TextStyle( @@ -112,15 +116,17 @@ class SeedWidgetState extends State { width: 32, height: 32, child: InkWell( + key: widget.pasteButtonKey, onTap: () async => _pasteText(), child: Container( padding: EdgeInsets.all(8), decoration: BoxDecoration( color: Theme.of(context).hintColor, - borderRadius: - BorderRadius.all(Radius.circular(6))), + borderRadius: BorderRadius.all(Radius.circular(6))), child: Image.asset('assets/images/paste_ios.png', - color: Theme.of(context).extension()!.textFieldButtonIconColor)), + color: Theme.of(context) + .extension()! + .textFieldButtonIconColor)), ))) ]), Container( diff --git a/lib/src/widgets/setting_actions.dart b/lib/src/widgets/setting_actions.dart index 272ed57c2..b9af97f32 100644 --- a/lib/src/widgets/setting_actions.dart +++ b/lib/src/widgets/setting_actions.dart @@ -18,6 +18,7 @@ class SettingActions { walletSettingAction, addressBookSettingAction, silentPaymentsSettingAction, + litecoinMwebSettingAction, securityBackupSettingAction, privacySettingAction, displaySettingAction, @@ -46,6 +47,15 @@ class SettingActions { }, ); + static SettingActions litecoinMwebSettingAction = 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); + }, + ); + static SettingActions connectionSettingAction = SettingActions._( name: (context) => S.of(context).connection_sync, image: 'assets/images/nodes_menu.png', diff --git a/lib/store/app_store.dart b/lib/store/app_store.dart index 7d61abfc5..cd8881633 100644 --- a/lib/store/app_store.dart +++ b/lib/store/app_store.dart @@ -37,7 +37,8 @@ abstract class AppStoreBase with Store { @action Future changeCurrentWallet( WalletBase, TransactionInfo> wallet) async { - this.wallet?.close(); + bool changingToSameWalletType = this.wallet?.type == wallet.type; + this.wallet?.close(shouldCleanup: !changingToSameWalletType); this.wallet = wallet; this.wallet!.setExceptionHandler(ExceptionHandler.onError); diff --git a/lib/store/dashboard/trade_filter_store.dart b/lib/store/dashboard/trade_filter_store.dart index c05839578..c1e462cd6 100644 --- a/lib/store/dashboard/trade_filter_store.dart +++ b/lib/store/dashboard/trade_filter_store.dart @@ -16,7 +16,9 @@ abstract class TradeFilterStoreBase with Store { displaySimpleSwap = true, displayTrocador = true, displayExolix = true, - displayThorChain = true; + displayThorChain = true, + displayLetsExchange = true, + displayStealthEx = true; @observable bool displayXMRTO; @@ -42,6 +44,12 @@ abstract class TradeFilterStoreBase with Store { @observable bool displayThorChain; + @observable + bool displayLetsExchange; + + @observable + bool displayStealthEx; + @computed bool get displayAllTrades => displayChangeNow && @@ -49,7 +57,9 @@ abstract class TradeFilterStoreBase with Store { displaySimpleSwap && displayTrocador && displayExolix && - displayThorChain; + displayThorChain && + displayLetsExchange && + displayStealthEx; @action void toggleDisplayExchange(ExchangeProviderDescription provider) { @@ -78,6 +88,11 @@ abstract class TradeFilterStoreBase with Store { case ExchangeProviderDescription.thorChain: displayThorChain = !displayThorChain; break; + case ExchangeProviderDescription.letsExchange: + displayLetsExchange = !displayLetsExchange; + case ExchangeProviderDescription.stealthEx: + displayStealthEx = !displayStealthEx; + break; case ExchangeProviderDescription.all: if (displayAllTrades) { displayChangeNow = false; @@ -88,6 +103,8 @@ abstract class TradeFilterStoreBase with Store { displayTrocador = false; displayExolix = false; displayThorChain = false; + displayLetsExchange = false; + displayStealthEx = false; } else { displayChangeNow = true; displaySideShift = true; @@ -97,6 +114,8 @@ abstract class TradeFilterStoreBase with Store { displayTrocador = true; displayExolix = true; displayThorChain = true; + displayLetsExchange = true; + displayStealthEx = true; } break; } @@ -112,13 +131,21 @@ abstract class TradeFilterStoreBase with Store { ? _trades .where((item) => (displayXMRTO && item.trade.provider == ExchangeProviderDescription.xmrto) || - (displaySideShift && item.trade.provider == ExchangeProviderDescription.sideShift) || - (displayChangeNow && item.trade.provider == ExchangeProviderDescription.changeNow) || - (displayMorphToken && item.trade.provider == ExchangeProviderDescription.morphToken) || - (displaySimpleSwap && item.trade.provider == ExchangeProviderDescription.simpleSwap) || + (displaySideShift && + item.trade.provider == ExchangeProviderDescription.sideShift) || + (displayChangeNow && + item.trade.provider == ExchangeProviderDescription.changeNow) || + (displayMorphToken && + item.trade.provider == ExchangeProviderDescription.morphToken) || + (displaySimpleSwap && + item.trade.provider == ExchangeProviderDescription.simpleSwap) || (displayTrocador && item.trade.provider == ExchangeProviderDescription.trocador) || (displayExolix && item.trade.provider == ExchangeProviderDescription.exolix) || - (displayThorChain && item.trade.provider == ExchangeProviderDescription.thorChain)) + (displayThorChain && + item.trade.provider == ExchangeProviderDescription.thorChain) || + (displayLetsExchange && + item.trade.provider == ExchangeProviderDescription.letsExchange) || + (displayStealthEx && item.trade.provider == ExchangeProviderDescription.stealthEx)) .toList() : _trades; } diff --git a/lib/store/dashboard/transaction_filter_store.dart b/lib/store/dashboard/transaction_filter_store.dart index af0dcce87..fb9ee14cd 100644 --- a/lib/store/dashboard/transaction_filter_store.dart +++ b/lib/store/dashboard/transaction_filter_store.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/view_model/dashboard/action_list_item.dart'; import 'package:cake_wallet/view_model/dashboard/anonpay_transaction_list_item.dart'; import 'package:mobx/mobx.dart'; @@ -6,12 +7,13 @@ import 'package:cake_wallet/view_model/dashboard/transaction_list_item.dart'; part 'transaction_filter_store.g.dart'; -class TransactionFilterStore = TransactionFilterStoreBase - with _$TransactionFilterStore; +class TransactionFilterStore = TransactionFilterStoreBase with _$TransactionFilterStore; abstract class TransactionFilterStoreBase with Store { - TransactionFilterStoreBase() : displayIncoming = true, - displayOutgoing = true; + TransactionFilterStoreBase() + : displayIncoming = true, + displayOutgoing = true, + displaySilentPayments = true; @observable bool displayIncoming; @@ -19,6 +21,9 @@ abstract class TransactionFilterStoreBase with Store { @observable bool displayOutgoing; + @observable + bool displaySilentPayments; + @observable DateTime? startDate; @@ -26,31 +31,36 @@ abstract class TransactionFilterStoreBase with Store { DateTime? endDate; @computed - bool get displayAll => displayIncoming && displayOutgoing; + bool get displayAll => displayIncoming && displayOutgoing && displaySilentPayments; @action void toggleAll() { if (displayAll) { displayOutgoing = false; displayIncoming = false; + displaySilentPayments = false; } else { displayOutgoing = true; displayIncoming = true; + displaySilentPayments = true; } } - @action void toggleIncoming() { displayIncoming = !displayIncoming; } - @action void toggleOutgoing() { displayOutgoing = !displayOutgoing; } + @action + void toggleSilentPayments() { + displaySilentPayments = !displaySilentPayments; + } + @action void changeStartDate(DateTime date) => startDate = date; @@ -59,34 +69,33 @@ abstract class TransactionFilterStoreBase with Store { List filtered({required List transactions}) { var _transactions = []; - final needToFilter = !displayAll || - (startDate != null && endDate != null); + final needToFilter = !displayAll || (startDate != null && endDate != null); if (needToFilter) { _transactions = transactions.where((item) { var allowed = true; if (allowed && startDate != null && endDate != null) { - if(item is TransactionListItem){ - allowed = (startDate?.isBefore(item.transaction.date) ?? false) - && (endDate?.isAfter(item.transaction.date) ?? false); - }else if(item is AnonpayTransactionListItem){ - allowed = (startDate?.isBefore(item.transaction.createdAt) ?? false) - && (endDate?.isAfter(item.transaction.createdAt) ?? false); - } + if (item is TransactionListItem) { + allowed = (startDate?.isBefore(item.transaction.date) ?? false) && + (endDate?.isAfter(item.transaction.date) ?? false); + } else if (item is AnonpayTransactionListItem) { + allowed = (startDate?.isBefore(item.transaction.createdAt) ?? false) && + (endDate?.isAfter(item.transaction.createdAt) ?? false); + } } if (allowed && (!displayAll)) { - if(item is TransactionListItem){ - allowed = (displayOutgoing && - item.transaction.direction == - TransactionDirection.outgoing) || - (displayIncoming && - item.transaction.direction == TransactionDirection.incoming); - } else if(item is AnonpayTransactionListItem){ + if (item is TransactionListItem) { + allowed = + (displayOutgoing && item.transaction.direction == TransactionDirection.outgoing) || + (displayIncoming && + item.transaction.direction == TransactionDirection.incoming && + !bitcoin!.txIsReceivedSilentPayment(item.transaction)) || + (displaySilentPayments && bitcoin!.txIsReceivedSilentPayment(item.transaction)); + } else if (item is AnonpayTransactionListItem) { allowed = displayIncoming; } - } return allowed; @@ -97,4 +106,4 @@ abstract class TransactionFilterStoreBase with Store { return _transactions; } -} \ No newline at end of file +} diff --git a/lib/store/seed_settings_store.dart b/lib/store/seed_settings_store.dart new file mode 100644 index 000000000..90c02ba97 --- /dev/null +++ b/lib/store/seed_settings_store.dart @@ -0,0 +1,11 @@ +import 'package:mobx/mobx.dart'; + +part 'seed_settings_store.g.dart'; + +class SeedSettingsStore = SeedSettingsStoreBase with _$SeedSettingsStore; + +abstract class SeedSettingsStoreBase with Store { + + @observable + String? passphrase; +} diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index ee145195c..9f03c95c3 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -1,45 +1,46 @@ import 'dart:io'; + import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart'; import 'package:cake_wallet/core/secure_storage.dart'; +import 'package:cake_wallet/di.dart'; +import 'package:cake_wallet/entities/action_list_display_mode.dart'; import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart'; -import 'package:cake_wallet/entities/provider_types.dart'; -import 'package:cake_wallet/entities/cake_2fa_preset_options.dart'; import 'package:cake_wallet/entities/background_tasks.dart'; +import 'package:cake_wallet/entities/balance_display_mode.dart'; +import 'package:cake_wallet/entities/cake_2fa_preset_options.dart'; import 'package:cake_wallet/entities/exchange_api_mode.dart'; +import 'package:cake_wallet/entities/fiat_api_mode.dart'; +import 'package:cake_wallet/entities/fiat_currency.dart'; +import 'package:cake_wallet/entities/language_service.dart'; import 'package:cake_wallet/entities/pin_code_required_duration.dart'; import 'package:cake_wallet/entities/preferences_key.dart'; +import 'package:cake_wallet/entities/provider_types.dart'; import 'package:cake_wallet/entities/secret_store_key.dart'; import 'package:cake_wallet/entities/seed_phrase_length.dart'; 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/polygon/polygon.dart'; -import 'package:cake_wallet/exchange/provider/trocador_exchange_provider.dart'; -import 'package:cake_wallet/view_model/settings/sync_mode.dart'; -import 'package:cake_wallet/utils/device_info.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: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'; import 'package:cake_wallet/themes/theme_base.dart'; import 'package:cake_wallet/themes/theme_list.dart'; +import 'package:cake_wallet/utils/device_info.dart'; +import 'package:cake_wallet/utils/package_info.dart'; +import 'package:cake_wallet/view_model/settings/sync_mode.dart'; +import 'package:cake_wallet/wallet_type_utils.dart'; +import 'package:cake_wallet/wownero/wownero.dart'; +import 'package:cw_core/node.dart'; +import 'package:cw_core/set_app_secure_native.dart'; +import 'package:cw_core/transaction_priority.dart'; +import 'package:cw_core/wallet_type.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/material.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; -import 'package:cake_wallet/utils/package_info.dart'; -import 'package:cake_wallet/di.dart'; -import 'package:cw_core/wallet_type.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:cake_wallet/entities/language_service.dart'; -import 'package:cake_wallet/entities/balance_display_mode.dart'; -import 'package:cake_wallet/entities/fiat_currency.dart'; -import 'package:cw_core/node.dart'; -import 'package:cake_wallet/monero/monero.dart'; -import 'package:cake_wallet/entities/action_list_display_mode.dart'; -import 'package:cake_wallet/entities/fiat_api_mode.dart'; -import 'package:cw_core/set_app_secure_native.dart'; part 'settings_store.g.dart'; @@ -55,7 +56,9 @@ abstract class SettingsStoreBase with Store { required BalanceDisplayMode initialBalanceDisplayMode, required bool initialSaveRecipientAddress, required AutoGenerateSubaddressStatus initialAutoGenerateSubaddressStatus, - required SeedType initialMoneroSeedType, + required MoneroSeedType initialMoneroSeedType, + required BitcoinSeedType initialBitcoinSeedType, + required NanoSeedType initialNanoSeedType, required bool initialAppSecure, required bool initialDisableBuy, required bool initialDisableSell, @@ -99,6 +102,7 @@ abstract class SettingsStoreBase with Store { required this.useEtherscan, required this.usePolygonScan, required this.useTronGrid, + required this.useMempoolFeeAPI, required this.defaultNanoRep, required this.defaultBananoRep, required this.lookupsTwitter, @@ -110,6 +114,10 @@ abstract class SettingsStoreBase with Store { required this.customBitcoinFeeRate, required this.silentPaymentsCardDisplay, required this.silentPaymentsAlwaysScan, + required this.mwebAlwaysScan, + required this.mwebCardDisplay, + required this.mwebEnabled, + required this.hasEnabledMwebBefore, TransactionPriority? initialBitcoinTransactionPriority, TransactionPriority? initialMoneroTransactionPriority, TransactionPriority? initialWowneroTransactionPriority, @@ -128,6 +136,8 @@ abstract class SettingsStoreBase with Store { shouldSaveRecipientAddress = initialSaveRecipientAddress, autoGenerateSubaddressStatus = initialAutoGenerateSubaddressStatus, moneroSeedType = initialMoneroSeedType, + bitcoinSeedType = initialBitcoinSeedType, + nanoSeedType = initialNanoSeedType, fiatApiMode = initialFiatMode, allowBiometricalAuthentication = initialAllowBiometricalAuthentication, selectedCake2FAPreset = initialCake2FAPresetOptions, @@ -329,9 +339,19 @@ abstract class SettingsStoreBase with Store { reaction( (_) => moneroSeedType, - (SeedType moneroSeedType) => + (MoneroSeedType moneroSeedType) => sharedPreferences.setInt(PreferencesKey.moneroSeedType, moneroSeedType.raw)); + reaction( + (_) => bitcoinSeedType, + (BitcoinSeedType bitcoinSeedType) => sharedPreferences.setInt( + PreferencesKey.bitcoinSeedType, bitcoinSeedType.raw)); + + reaction( + (_) => nanoSeedType, + (NanoSeedType nanoSeedType) => + sharedPreferences.setInt(PreferencesKey.nanoSeedType, nanoSeedType.raw)); + reaction( (_) => fiatApiMode, (FiatApiMode mode) => @@ -408,6 +428,9 @@ abstract class SettingsStoreBase with Store { reaction((_) => useTronGrid, (bool useTronGrid) => _sharedPreferences.setBool(PreferencesKey.useTronGrid, useTronGrid)); + reaction((_) => useMempoolFeeAPI, + (bool useMempoolFeeAPI) => _sharedPreferences.setBool(PreferencesKey.useMempoolFeeAPI, useMempoolFeeAPI)); + reaction((_) => defaultNanoRep, (String nanoRep) => _sharedPreferences.setString(PreferencesKey.defaultNanoRep, nanoRep)); @@ -536,6 +559,24 @@ abstract class SettingsStoreBase with Store { (bool silentPaymentsAlwaysScan) => _sharedPreferences.setBool( PreferencesKey.silentPaymentsAlwaysScan, silentPaymentsAlwaysScan)); + reaction( + (_) => mwebAlwaysScan, + (bool mwebAlwaysScan) => + _sharedPreferences.setBool(PreferencesKey.mwebAlwaysScan, mwebAlwaysScan)); + + reaction( + (_) => mwebCardDisplay, + (bool mwebCardDisplay) => + _sharedPreferences.setBool(PreferencesKey.mwebCardDisplay, mwebCardDisplay)); + + reaction((_) => mwebEnabled, + (bool mwebEnabled) => _sharedPreferences.setBool(PreferencesKey.mwebEnabled, mwebEnabled)); + + reaction( + (_) => hasEnabledMwebBefore, + (bool hasEnabledMwebBefore) => + _sharedPreferences.setBool(PreferencesKey.hasEnabledMwebBefore, hasEnabledMwebBefore)); + this.nodes.observe((change) { if (change.newValue != null && change.key != null) { _saveCurrentNode(change.newValue!, change.key!); @@ -551,11 +592,13 @@ abstract class SettingsStoreBase with Store { static const defaultPinLength = 4; static const defaultActionsMode = 11; - static const defaultPinCodeTimeOutDuration = PinCodeRequiredDuration.tenminutes; + static const defaultPinCodeTimeOutDuration = PinCodeRequiredDuration.tenMinutes; static const defaultAutoGenerateSubaddressStatus = AutoGenerateSubaddressStatus.initialized; static final walletPasswordDirectInput = Platform.isLinux; static const defaultSeedPhraseLength = SeedPhraseLength.twelveWords; - static const defaultMoneroSeedType = SeedType.defaultSeedType; + static const defaultMoneroSeedType = MoneroSeedType.defaultSeedType; + static const defaultBitcoinSeedType = BitcoinSeedType.defaultDerivationType; + static const defaultNanoSeedType = NanoSeedType.defaultDerivationType; @observable FiatCurrency fiatCurrency; @@ -585,7 +628,13 @@ abstract class SettingsStoreBase with Store { AutoGenerateSubaddressStatus autoGenerateSubaddressStatus; @observable - SeedType moneroSeedType; + MoneroSeedType moneroSeedType; + + @observable + BitcoinSeedType bitcoinSeedType; + + @observable + NanoSeedType nanoSeedType; @observable bool isAppSecure; @@ -695,6 +744,9 @@ abstract class SettingsStoreBase with Store { @observable bool useTronGrid; + @observable + bool useMempoolFeeAPI; + @observable String defaultNanoRep; @@ -738,6 +790,18 @@ abstract class SettingsStoreBase with Store { @observable bool silentPaymentsAlwaysScan; + @observable + bool mwebAlwaysScan; + + @observable + bool mwebCardDisplay; + + @observable + bool mwebEnabled; + + @observable + bool hasEnabledMwebBefore; + final SecureStorage _secureStorage; final SharedPreferences _sharedPreferences; final BackgroundTasks _backgroundTasks; @@ -880,6 +944,7 @@ abstract class SettingsStoreBase with Store { final useEtherscan = sharedPreferences.getBool(PreferencesKey.useEtherscan) ?? true; final usePolygonScan = sharedPreferences.getBool(PreferencesKey.usePolygonScan) ?? true; final useTronGrid = sharedPreferences.getBool(PreferencesKey.useTronGrid) ?? true; + final useMempoolFeeAPI = sharedPreferences.getBool(PreferencesKey.useMempoolFeeAPI) ?? true; final defaultNanoRep = sharedPreferences.getString(PreferencesKey.defaultNanoRep) ?? ""; final defaultBananoRep = sharedPreferences.getString(PreferencesKey.defaultBananoRep) ?? ""; final lookupsTwitter = sharedPreferences.getBool(PreferencesKey.lookupsTwitter) ?? true; @@ -894,6 +959,11 @@ abstract class SettingsStoreBase with Store { sharedPreferences.getBool(PreferencesKey.silentPaymentsCardDisplay) ?? true; final silentPaymentsAlwaysScan = sharedPreferences.getBool(PreferencesKey.silentPaymentsAlwaysScan) ?? false; + final mwebAlwaysScan = sharedPreferences.getBool(PreferencesKey.mwebAlwaysScan) ?? false; + final mwebCardDisplay = sharedPreferences.getBool(PreferencesKey.mwebCardDisplay) ?? true; + final mwebEnabled = sharedPreferences.getBool(PreferencesKey.mwebEnabled) ?? false; + final hasEnabledMwebBefore = + sharedPreferences.getBool(PreferencesKey.hasEnabledMwebBefore) ?? false; // If no value if (pinLength == null || pinLength == 0) { @@ -945,9 +1015,20 @@ abstract class SettingsStoreBase with Store { final _moneroSeedType = sharedPreferences.getInt(PreferencesKey.moneroSeedType); final moneroSeedType = _moneroSeedType != null - ? SeedType.deserialize(raw: _moneroSeedType) + ? MoneroSeedType.deserialize(raw: _moneroSeedType) : defaultMoneroSeedType; + final _bitcoinSeedType = sharedPreferences.getInt(PreferencesKey.bitcoinSeedType); + + final bitcoinSeedType = _bitcoinSeedType != null + ? BitcoinSeedType.deserialize(raw: _bitcoinSeedType) + : defaultBitcoinSeedType; + + final _nanoSeedType = sharedPreferences.getInt(PreferencesKey.nanoSeedType); + + final nanoSeedType = + _nanoSeedType != null ? NanoSeedType.deserialize(raw: _nanoSeedType) : defaultNanoSeedType; + final nodes = {}; final powNodes = {}; @@ -1111,6 +1192,8 @@ abstract class SettingsStoreBase with Store { initialSaveRecipientAddress: shouldSaveRecipientAddress, initialAutoGenerateSubaddressStatus: autoGenerateSubaddressStatus, initialMoneroSeedType: moneroSeedType, + initialBitcoinSeedType: bitcoinSeedType, + initialNanoSeedType: nanoSeedType, initialAppSecure: isAppSecure, initialDisableBuy: disableBuy, initialDisableSell: disableSell, @@ -1135,6 +1218,7 @@ abstract class SettingsStoreBase with Store { useEtherscan: useEtherscan, usePolygonScan: usePolygonScan, useTronGrid: useTronGrid, + useMempoolFeeAPI: useMempoolFeeAPI, defaultNanoRep: defaultNanoRep, defaultBananoRep: defaultBananoRep, lookupsTwitter: lookupsTwitter, @@ -1146,6 +1230,10 @@ abstract class SettingsStoreBase with Store { customBitcoinFeeRate: customBitcoinFeeRate, silentPaymentsCardDisplay: silentPaymentsCardDisplay, silentPaymentsAlwaysScan: silentPaymentsAlwaysScan, + mwebAlwaysScan: mwebAlwaysScan, + mwebCardDisplay: mwebCardDisplay, + mwebEnabled: mwebEnabled, + hasEnabledMwebBefore: hasEnabledMwebBefore, initialMoneroTransactionPriority: moneroTransactionPriority, initialWowneroTransactionPriority: wowneroTransactionPriority, initialBitcoinTransactionPriority: bitcoinTransactionPriority, @@ -1233,9 +1321,20 @@ abstract class SettingsStoreBase with Store { final _moneroSeedType = sharedPreferences.getInt(PreferencesKey.moneroSeedType); moneroSeedType = _moneroSeedType != null - ? SeedType.deserialize(raw: _moneroSeedType) + ? MoneroSeedType.deserialize(raw: _moneroSeedType) : defaultMoneroSeedType; + final _bitcoinSeedType = sharedPreferences.getInt(PreferencesKey.bitcoinSeedType); + + bitcoinSeedType = _bitcoinSeedType != null + ? BitcoinSeedType.deserialize(raw: _bitcoinSeedType) + : defaultBitcoinSeedType; + + final _nanoSeedType = sharedPreferences.getInt(PreferencesKey.nanoSeedType); + + nanoSeedType = + _nanoSeedType != null ? NanoSeedType.deserialize(raw: _nanoSeedType) : defaultNanoSeedType; + balanceDisplayMode = BalanceDisplayMode.deserialize( raw: sharedPreferences.getInt(PreferencesKey.currentBalanceDisplayModeKey)!); shouldSaveRecipientAddress = @@ -1282,6 +1381,7 @@ abstract class SettingsStoreBase with Store { useEtherscan = sharedPreferences.getBool(PreferencesKey.useEtherscan) ?? true; usePolygonScan = sharedPreferences.getBool(PreferencesKey.usePolygonScan) ?? true; useTronGrid = sharedPreferences.getBool(PreferencesKey.useTronGrid) ?? true; + useMempoolFeeAPI = sharedPreferences.getBool(PreferencesKey.useMempoolFeeAPI) ?? true; defaultNanoRep = sharedPreferences.getString(PreferencesKey.defaultNanoRep) ?? ""; defaultBananoRep = sharedPreferences.getString(PreferencesKey.defaultBananoRep) ?? ""; lookupsTwitter = sharedPreferences.getBool(PreferencesKey.lookupsTwitter) ?? true; @@ -1296,6 +1396,10 @@ abstract class SettingsStoreBase with Store { sharedPreferences.getBool(PreferencesKey.silentPaymentsCardDisplay) ?? true; silentPaymentsAlwaysScan = sharedPreferences.getBool(PreferencesKey.silentPaymentsAlwaysScan) ?? false; + mwebAlwaysScan = sharedPreferences.getBool(PreferencesKey.mwebAlwaysScan) ?? false; + mwebCardDisplay = sharedPreferences.getBool(PreferencesKey.mwebCardDisplay) ?? true; + mwebEnabled = sharedPreferences.getBool(PreferencesKey.mwebEnabled) ?? false; + hasEnabledMwebBefore = sharedPreferences.getBool(PreferencesKey.hasEnabledMwebBefore) ?? false; final nodeId = sharedPreferences.getInt(PreferencesKey.currentNodeIdKey); final bitcoinElectrumServerId = sharedPreferences.getInt(PreferencesKey.currentBitcoinElectrumSererIdKey); diff --git a/lib/tron/cw_tron.dart b/lib/tron/cw_tron.dart index c6ac89342..8bceafe01 100644 --- a/lib/tron/cw_tron.dart +++ b/lib/tron/cw_tron.dart @@ -13,16 +13,31 @@ class CWTron extends Tron { required String name, WalletInfo? walletInfo, String? password, + String? mnemonic, + String? parentAddress, + String? passphrase, }) => - TronNewWalletCredentials(name: name, walletInfo: walletInfo, password: password); + TronNewWalletCredentials( + name: name, + walletInfo: walletInfo, + password: password, + mnemonic: mnemonic, + passphrase: passphrase, + parentAddress: parentAddress); @override WalletCredentials createTronRestoreWalletFromSeedCredentials({ required String name, required String mnemonic, required String password, + String? passphrase, }) => - TronRestoreWalletFromSeedCredentials(name: name, password: password, mnemonic: mnemonic); + TronRestoreWalletFromSeedCredentials( + name: name, + password: password, + mnemonic: mnemonic, + passphrase: passphrase, + ); @override WalletCredentials createTronRestoreWalletFromPrivateKey({ @@ -34,7 +49,7 @@ class CWTron extends Tron { @override String getAddress(WalletBase wallet) => (wallet as TronWallet).walletAddresses.address; - + Object createTronTransactionCredentials( List outputs, { required CryptoCurrency currency, @@ -63,10 +78,10 @@ class CWTron extends Tron { @override Future addTronToken(WalletBase wallet, CryptoCurrency token, String contractAddress) async { - final tronToken = TronToken( + final tronToken = TronToken( name: token.name, symbol: token.title, - contractAddress: contractAddress, + contractAddress: contractAddress, decimal: token.decimals, enabled: token.enabled, iconPath: token.iconPath, diff --git a/lib/utils/brightness_util.dart b/lib/utils/brightness_util.dart index 5afe065e5..1b34bceac 100644 --- a/lib/utils/brightness_util.dart +++ b/lib/utils/brightness_util.dart @@ -9,15 +9,12 @@ class BrightnessUtil { return; } - // Get the current brightness: - final brightness = await DeviceDisplayBrightness.getBrightness(); - // ignore: unawaited_futures DeviceDisplayBrightness.setBrightness(1.0); await func(); // ignore: unawaited_futures - DeviceDisplayBrightness.setBrightness(brightness); + DeviceDisplayBrightness.resetBrightness(); } } \ No newline at end of file diff --git a/lib/utils/exception_handler.dart b/lib/utils/exception_handler.dart index 5e0c83f88..91797c45e 100644 --- a/lib/utils/exception_handler.dart +++ b/lib/utils/exception_handler.dart @@ -21,9 +21,9 @@ class ExceptionHandler { static File? _file; static void _saveException(String? error, StackTrace? stackTrace, {String? library}) async { - if (_file == null) { - final appDocDir = await getAppDir(); + final appDocDir = await getAppDir(); + if (_file == null) { _file = File('${appDocDir.path}/error.txt'); } @@ -83,7 +83,7 @@ class ExceptionHandler { } static void onError(FlutterErrorDetails errorDetails) async { - if (kDebugMode) { + if (kDebugMode || kProfileMode) { FlutterError.presentError(errorDetails); debugPrint(errorDetails.toString()); return; @@ -172,8 +172,14 @@ class ExceptionHandler { "Error while launching http", "OS Error: Network is unreachable", "ClientException: Write failed, uri=http", - "Connection terminated during handshake", "Corrupted wallets seeds", + "bad_alloc", + "does not correspond", + "basic_string", + "input_stream", + "input stream error", + "invalid signature", + "invalid password", ]; static Future _addDeviceInfo(File file) async { diff --git a/lib/utils/image_utill.dart b/lib/utils/image_utill.dart new file mode 100644 index 000000000..a138df23a --- /dev/null +++ b/lib/utils/image_utill.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; + +class ImageUtil { + static Widget getImageFromPath({required String imagePath, double? height, double? width}) { + final bool isNetworkImage = imagePath.startsWith('http') || imagePath.startsWith('https'); + final bool isSvg = imagePath.endsWith('.svg'); + final double _height = height ?? 35; + final double _width = width ?? 35; + + if (isNetworkImage) { + return isSvg + ? SvgPicture.network( + imagePath, + height: _height, + width: _width, + placeholderBuilder: (BuildContext context) => Container( + height: _height, + width: _width, + child: Center( + child: CircularProgressIndicator(), + ), + ), + ) + : Image.network( + imagePath, + height: _height, + width: _width, + loadingBuilder: + (BuildContext context, Widget child, ImageChunkEvent? loadingProgress) { + if (loadingProgress == null) { + return child; + } + return Container( + height: _height, + width: _width, + child: Center( + child: CircularProgressIndicator( + value: loadingProgress.expectedTotalBytes != null + ? loadingProgress.cumulativeBytesLoaded / + loadingProgress.expectedTotalBytes! + : null, + ), + ), + ); + }, + errorBuilder: (BuildContext context, Object exception, StackTrace? stackTrace) { + return Container( + height: _height, + width: _width, + ); + }, + ); + } else { + return isSvg + ? SvgPicture.asset(imagePath, height: _height, width: _width) + : Image.asset(imagePath, height: _height, width: _width); + } + } +} diff --git a/lib/view_model/advanced_privacy_settings_view_model.dart b/lib/view_model/advanced_privacy_settings_view_model.dart index 73308f15a..85b9dbead 100644 --- a/lib/view_model/advanced_privacy_settings_view_model.dart +++ b/lib/view_model/advanced_privacy_settings_view_model.dart @@ -30,6 +30,7 @@ abstract class AdvancedPrivacySettingsViewModelBase with Store { final SettingsStore _settingsStore; + @computed bool get hasSeedPhraseLengthOption { // convert to switch case so that it give a syntax error when adding a new wallet type // thus we don't forget about it @@ -40,19 +41,44 @@ abstract class AdvancedPrivacySettingsViewModelBase with Store { case WalletType.solana: case WalletType.tron: return true; + + case WalletType.bitcoin: + case WalletType.litecoin: + return _settingsStore.bitcoinSeedType == BitcoinSeedType.bip39; + + case WalletType.nano: + case WalletType.banano: + return _settingsStore.nanoSeedType == NanoSeedType.bip39; + case WalletType.monero: case WalletType.wownero: case WalletType.none: - case WalletType.bitcoin: - case WalletType.litecoin: case WalletType.haven: - case WalletType.nano: - case WalletType.banano: return false; } } - bool get hasSeedTypeOption => type == WalletType.monero || type == WalletType.wownero; + + bool get isMoneroSeedTypeOptionsEnabled => [ + WalletType.monero, + WalletType.wownero, + ].contains(type); + + bool get isBitcoinSeedTypeOptionsEnabled => [ + WalletType.bitcoin, + WalletType.litecoin, + ].contains(type); + + bool get isNanoSeedTypeOptionsEnabled => [WalletType.nano].contains(type); + + bool get hasPassphraseOption => [ + WalletType.bitcoin, + WalletType.litecoin, + WalletType.bitcoinCash, + WalletType.ethereum, + WalletType.polygon, + WalletType.tron, + ].contains(type); @computed bool get addCustomNode => _addCustomNode; @@ -61,7 +87,7 @@ abstract class AdvancedPrivacySettingsViewModelBase with Store { SeedPhraseLength get seedPhraseLength => _settingsStore.seedPhraseLength; @computed - bool get isPolySeed => _settingsStore.moneroSeedType == SeedType.polyseed; + bool get isPolySeed => _settingsStore.moneroSeedType == MoneroSeedType.polyseed; @action void setFiatApiMode(FiatApiMode fiatApiMode) => _settingsStore.fiatApiMode = fiatApiMode; diff --git a/lib/view_model/contact_list/contact_list_view_model.dart b/lib/view_model/contact_list/contact_list_view_model.dart index d63f78224..4089d988b 100644 --- a/lib/view_model/contact_list/contact_list_view_model.dart +++ b/lib/view_model/contact_list/contact_list_view_model.dart @@ -26,37 +26,47 @@ abstract class ContactListViewModelBase with Store { isAutoGenerateEnabled = settingsStore.autoGenerateSubaddressStatus == AutoGenerateSubaddressStatus.enabled { walletInfoSource.values.forEach((info) { - if (isAutoGenerateEnabled && info.type == WalletType.monero && info.addressInfos != null) { - info.addressInfos!.forEach((key, value) { - final nextUnusedAddress = value.firstWhereOrNull( - (addressInfo) => !(info.usedAddresses?.contains(addressInfo.address) ?? false)); - if (nextUnusedAddress != null) { - final name = _createName(info.name, nextUnusedAddress.label); + if ([WalletType.monero, WalletType.wownero, WalletType.haven].contains(info.type) && info.addressInfos != null) { + for (var key in info.addressInfos!.keys) { + final value = info.addressInfos![key]; + final address = value?.first; + if (address != null) { + final name = _createName(info.name, address.label, key: key); walletContacts.add(WalletContact( - nextUnusedAddress.address, + address.address, name, walletTypeToCryptoCurrency(info.type), )); } - }); + } } else if (info.addresses?.isNotEmpty == true && info.addresses!.length > 1) { - info.addresses!.forEach((address, label) { - if (label.isEmpty) { - return; - } - final name = _createName(info.name, label); + if ([WalletType.monero, WalletType.wownero, WalletType.haven].contains(info.type)) { + final address = info.address; + final name = _createName(info.name, "", key: 0); walletContacts.add(WalletContact( address, name, - walletTypeToCryptoCurrency(info.type, - isTestnet: - info.network == null ? false : info.network!.toLowerCase().contains("testnet")), + walletTypeToCryptoCurrency(info.type), )); - }); + } else { + info.addresses!.forEach((address, label) { + if (label.isEmpty) { + return; + } + final name = _createName(info.name, label, key: null); + walletContacts.add(WalletContact( + address, + name, + walletTypeToCryptoCurrency(info.type, + isTestnet: + info.network == null ? false : info.network!.toLowerCase().contains("testnet")), + )); + }); + } } else { walletContacts.add(WalletContact( info.address, - info.name, + _createName(info.name, "", key: [WalletType.monero, WalletType.wownero, WalletType.haven].contains(info.type) ? 0 : null), walletTypeToCryptoCurrency(info.type), )); } @@ -67,10 +77,9 @@ abstract class ContactListViewModelBase with Store { initialFire: true); } - String _createName(String walletName, String label) { - return label.isNotEmpty - ? '$walletName (${label.replaceAll(RegExp(r'active', caseSensitive: false), S.current.active).replaceAll(RegExp(r'silent payments', caseSensitive: false), S.current.silent_payments)})' - : walletName; + String _createName(String walletName, String label, {int? key = null}) { + final actualLabel = label.replaceAll(RegExp(r'active', caseSensitive: false), S.current.active).replaceAll(RegExp(r'silent payments', caseSensitive: false), S.current.silent_payments); + return '$walletName${key == null ? "" : " [#${key}]"} ${actualLabel.isNotEmpty ? "($actualLabel)" : ""}'.trim(); } final bool isAutoGenerateEnabled; diff --git a/lib/view_model/dashboard/balance_view_model.dart b/lib/view_model/dashboard/balance_view_model.dart index 045b55261..56a5935c9 100644 --- a/lib/view_model/dashboard/balance_view_model.dart +++ b/lib/view_model/dashboard/balance_view_model.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/entities/fiat_api_mode.dart'; import 'package:cake_wallet/entities/sort_balance_types.dart'; import 'package:cake_wallet/reactions/wallet_connect.dart'; @@ -21,18 +22,27 @@ class BalanceRecord { const BalanceRecord( {required this.availableBalance, required this.additionalBalance, + required this.secondAvailableBalance, + required this.secondAdditionalBalance, required this.frozenBalance, required this.fiatAvailableBalance, required this.fiatAdditionalBalance, required this.fiatFrozenBalance, + required this.fiatSecondAvailableBalance, + required this.fiatSecondAdditionalBalance, required this.asset, required this.formattedAssetTitle}); + final String fiatAdditionalBalance; final String fiatAvailableBalance; final String fiatFrozenBalance; final String additionalBalance; final String availableBalance; final String frozenBalance; + final String secondAvailableBalance; + final String secondAdditionalBalance; + final String fiatSecondAdditionalBalance; + final String fiatSecondAvailableBalance; final CryptoCurrency asset; final String formattedAssetTitle; } @@ -45,7 +55,22 @@ abstract class BalanceViewModelBase with Store { : isReversing = false, isShowCard = appStore.wallet!.walletInfo.isShowIntroCakePayCard, wallet = appStore.wallet! { - reaction((_) => appStore.wallet, _onWalletChange); + reaction((_) => appStore.wallet, (wallet) { + _onWalletChange(wallet); + _checkMweb(); + }); + + _checkMweb(); + + reaction((_) => settingsStore.mwebAlwaysScan, (bool value) { + _checkMweb(); + }); + } + + void _checkMweb() { + if (wallet.type == WalletType.litecoin) { + mwebEnabled = bitcoin!.getMwebEnabled(wallet); + } } final AppStore appStore; @@ -158,6 +183,26 @@ abstract class BalanceViewModelBase with Store { } } + @computed + String get secondAvailableBalanceLabel { + switch (wallet.type) { + case WalletType.litecoin: + return S.current.mweb_confirmed; + default: + return S.current.confirmed; + } + } + + @computed + String get secondAdditionalBalanceLabel { + switch (wallet.type) { + case WalletType.litecoin: + return S.current.mweb_unconfirmed; + default: + return S.current.unconfirmed; + } + } + @computed bool get hasMultiBalance => appStore.wallet!.type == WalletType.haven; @@ -243,9 +288,13 @@ abstract class BalanceViewModelBase with Store { availableBalance: '---', additionalBalance: '---', frozenBalance: '---', + secondAvailableBalance: '---', + secondAdditionalBalance: '---', fiatAdditionalBalance: isFiatDisabled ? '' : '---', fiatAvailableBalance: isFiatDisabled ? '' : '---', fiatFrozenBalance: isFiatDisabled ? '' : '---', + fiatSecondAvailableBalance: isFiatDisabled ? '' : '---', + fiatSecondAdditionalBalance: isFiatDisabled ? '' : '---', asset: key, formattedAssetTitle: _formatterAsset(key))); } @@ -274,24 +323,51 @@ abstract class BalanceViewModelBase with Store { ' ' + _getFiatBalance(price: price, cryptoAmount: getFormattedFrozenBalance(value))); + final secondAdditionalFiatBalance = isFiatDisabled + ? '' + : (fiatCurrency.toString() + + ' ' + + _getFiatBalance(price: price, cryptoAmount: value.formattedSecondAdditionalBalance)); + + final secondAvailableFiatBalance = isFiatDisabled + ? '' + : (fiatCurrency.toString() + + ' ' + + _getFiatBalance(price: price, cryptoAmount: value.formattedSecondAvailableBalance)); + return MapEntry( key, BalanceRecord( availableBalance: value.formattedAvailableBalance, additionalBalance: value.formattedAdditionalBalance, frozenBalance: getFormattedFrozenBalance(value), + secondAvailableBalance: value.formattedSecondAvailableBalance, + secondAdditionalBalance: value.formattedSecondAdditionalBalance, fiatAdditionalBalance: additionalFiatBalance, fiatAvailableBalance: availableFiatBalance, fiatFrozenBalance: frozenFiatBalance, + fiatSecondAvailableBalance: secondAvailableFiatBalance, + fiatSecondAdditionalBalance: secondAdditionalFiatBalance, asset: key, formattedAssetTitle: _formatterAsset(key))); }); } - @computed - bool get hasAdditionalBalance => _hasAdditionBalanceForWalletType(wallet.type); + @observable + bool mwebEnabled = false; - bool _hasAdditionBalanceForWalletType(WalletType type) { + @computed + bool get hasAdditionalBalance => _hasAdditionalBalanceForWalletType(wallet.type); + + @computed + bool get hasSecondAdditionalBalance => + mwebEnabled && _hasSecondAdditionalBalanceForWalletType(wallet.type); + + @computed + bool get hasSecondAvailableBalance => + mwebEnabled && _hasSecondAvailableBalanceForWalletType(wallet.type); + + bool _hasAdditionalBalanceForWalletType(WalletType type) { switch (type) { case WalletType.ethereum: case WalletType.polygon: @@ -303,6 +379,22 @@ abstract class BalanceViewModelBase with Store { } } + bool _hasSecondAdditionalBalanceForWalletType(WalletType type) { + if (wallet.type == WalletType.litecoin) { + if ((wallet.balance[CryptoCurrency.ltc]?.secondAdditional ?? 0) > 0) { + return true; + } + } + return false; + } + + bool _hasSecondAvailableBalanceForWalletType(WalletType type) { + if (wallet.type == WalletType.litecoin) { + return true; + } + return false; + } + @computed List get formattedBalances { final balance = balances.values.toList(); diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index d58d7535c..53a8277ab 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -1,12 +1,16 @@ +import 'dart:async'; import 'dart:convert'; +import 'dart:io' show Platform; +import 'package:cake_wallet/.secrets.g.dart' as secrets; +import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/buy/buy_provider.dart'; import 'package:cake_wallet/core/key_service.dart'; import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart'; import 'package:cake_wallet/entities/balance_display_mode.dart'; +import 'package:cake_wallet/entities/exchange_api_mode.dart'; import 'package:cake_wallet/entities/preferences_key.dart'; import 'package:cake_wallet/entities/provider_types.dart'; -import 'package:cake_wallet/entities/exchange_api_mode.dart'; import 'package:cake_wallet/entities/service_status.dart'; import 'package:cake_wallet/exchange/exchange_provider_description.dart'; import 'package:cake_wallet/generated/i18n.dart'; @@ -45,11 +49,9 @@ import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:eth_sig_util/util/utils.dart'; import 'package:flutter/services.dart'; -import 'package:mobx/mobx.dart'; -import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:http/http.dart' as http; +import 'package:mobx/mobx.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:cake_wallet/.secrets.g.dart' as secrets; part 'dashboard_view_model.g.dart'; @@ -88,6 +90,11 @@ abstract class DashboardViewModelBase with Store { value: () => transactionFilterStore.displayOutgoing, caption: S.current.outgoing, onChanged: transactionFilterStore.toggleOutgoing), + FilterItem( + value: () => transactionFilterStore.displaySilentPayments, + caption: S.current.silent_payments, + onChanged: transactionFilterStore.toggleSilentPayments, + ), // FilterItem( // value: () => false, // caption: S.current.transactions_by_date, @@ -129,6 +136,16 @@ abstract class DashboardViewModelBase with Store { caption: ExchangeProviderDescription.thorChain.title, onChanged: () => tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.thorChain)), + FilterItem( + value: () => tradeFilterStore.displayLetsExchange, + caption: ExchangeProviderDescription.letsExchange.title, + onChanged: () => tradeFilterStore + .toggleDisplayExchange(ExchangeProviderDescription.letsExchange)), + FilterItem( + value: () => tradeFilterStore.displayStealthEx, + caption: ExchangeProviderDescription.stealthEx.title, + onChanged: () => + tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.stealthEx)), ] }, subname = '', @@ -207,7 +224,10 @@ abstract class DashboardViewModelBase with Store { // subname = nano!.getCurrentAccount(_wallet).label; // } - reaction((_) => appStore.wallet, _onWalletChange); + reaction((_) => appStore.wallet, (wallet) { + _onWalletChange(wallet); + _checkMweb(); + }); connectMapToListWithTransform( appStore.wallet!.transactionHistory.transactions, @@ -240,6 +260,18 @@ abstract class DashboardViewModelBase with Store { silentPaymentsScanningActive = bitcoin!.getScanningActive(wallet); }); } + + _checkMweb(); + reaction((_) => settingsStore.mwebAlwaysScan, (bool value) { + _checkMweb(); + }); + } + + void _checkMweb() { + if (hasMweb) { + mwebEnabled = bitcoin!.getMwebEnabled(wallet); + balanceViewModel.mwebEnabled = mwebEnabled; + } } @observable @@ -333,6 +365,7 @@ abstract class DashboardViewModelBase with Store { bool get hasRescan => wallet.type == WalletType.bitcoin || wallet.type == WalletType.monero || + wallet.type == WalletType.litecoin || wallet.type == WalletType.wownero || wallet.type == WalletType.haven; @@ -366,12 +399,15 @@ abstract class DashboardViewModelBase with Store { // to not cause work duplication, this will do the job as well, it will be slightly less precise // about what happened - but still enough. // if (keys['privateSpendKey'] == List.generate(64, (index) => "0").join("")) "Private spend key is 0", - if (keys['privateViewKey'] == List.generate(64, (index) => "0").join("")) "private view key is 0", + if (keys['privateViewKey'] == List.generate(64, (index) => "0").join("")) + "private view key is 0", // if (keys['publicSpendKey'] == List.generate(64, (index) => "0").join("")) "public spend key is 0", - if (keys['publicViewKey'] == List.generate(64, (index) => "0").join("")) "public view key is 0", + if (keys['publicViewKey'] == List.generate(64, (index) => "0").join("")) + "public view key is 0", // if (wallet.seed == null) "wallet seed is null", // if (wallet.seed == "") "wallet seed is empty", - if (monero!.getSubaddressList(wallet).getAll(wallet)[0].address == "41d7FXjswpK1111111111111111111111111111111111111111111111111111111111111111111111111111112KhNi4") + if (monero!.getSubaddressList(wallet).getAll(wallet)[0].address == + "41d7FXjswpK1111111111111111111111111111111111111111111111111111111111111111111111111111112KhNi4") "primary address is invalid, you won't be able to receive / spend funds", ]; return errors; @@ -398,6 +434,40 @@ abstract class DashboardViewModelBase with Store { } } + @computed + bool get hasMweb => wallet.type == WalletType.litecoin && (Platform.isIOS || Platform.isAndroid) && !wallet.isHardwareWallet; + + @computed + bool get showMwebCard => hasMweb && settingsStore.mwebCardDisplay && !mwebEnabled; + + @observable + bool mwebEnabled = false; + + @computed + bool get hasEnabledMwebBefore => settingsStore.hasEnabledMwebBefore; + + @action + void setMwebEnabled() { + if (!hasMweb) { + return; + } + + settingsStore.hasEnabledMwebBefore = true; + mwebEnabled = true; + bitcoin!.setMwebEnabled(wallet, true); + balanceViewModel.mwebEnabled = true; + settingsStore.mwebAlwaysScan = true; + } + + @action + void dismissMweb() { + settingsStore.mwebCardDisplay = false; + balanceViewModel.mwebEnabled = false; + settingsStore.mwebAlwaysScan = false; + mwebEnabled = false; + bitcoin!.setMwebEnabled(wallet, false); + } + BalanceViewModel balanceViewModel; AppStore appStore; @@ -746,6 +816,16 @@ abstract class DashboardViewModelBase with Store { } } + String getTransactionType(TransactionInfo tx) { + if (wallet.type == WalletType.bitcoin) { + if (tx.isReplaced == true) return ' (replaced)'; + } + + if (wallet.type == WalletType.ethereum && tx.evmSignatureName == 'approval') + return ' (${tx.evmSignatureName})'; + return ''; + } + Future refreshDashboard() async { reconnect(); } diff --git a/lib/view_model/dashboard/home_settings_view_model.dart b/lib/view_model/dashboard/home_settings_view_model.dart index 5778f1e19..1aeacd28f 100644 --- a/lib/view_model/dashboard/home_settings_view_model.dart +++ b/lib/view_model/dashboard/home_settings_view_model.dart @@ -1,8 +1,14 @@ +import 'dart:convert'; +import 'dart:developer'; + import 'package:cake_wallet/core/fiat_conversion_service.dart'; +import 'package:cake_wallet/entities/erc20_token_info_explorers.dart'; import 'package:cake_wallet/entities/fiat_api_mode.dart'; +import 'package:cake_wallet/entities/erc20_token_info_moralis.dart'; import 'package:cake_wallet/entities/sort_balance_types.dart'; import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/polygon/polygon.dart'; +import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cake_wallet/solana/solana.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/tron/tron.dart'; @@ -11,6 +17,8 @@ import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/erc20_token.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:mobx/mobx.dart'; +import 'package:http/http.dart' as http; +import 'package:cake_wallet/.secrets.g.dart' as secrets; part 'home_settings_view_model.g.dart'; @@ -18,7 +26,10 @@ class HomeSettingsViewModel = HomeSettingsViewModelBase with _$HomeSettingsViewM abstract class HomeSettingsViewModelBase with Store { HomeSettingsViewModelBase(this._settingsStore, this._balanceViewModel) - : tokens = ObservableSet() { + : tokens = ObservableSet(), + isAddingToken = false, + isDeletingToken = false, + isValidatingContractAddress = false { _updateTokensList(); } @@ -27,6 +38,15 @@ abstract class HomeSettingsViewModelBase with Store { final ObservableSet tokens; + @observable + bool isAddingToken; + + @observable + bool isDeletingToken; + + @observable + bool isValidatingContractAddress; + @observable String searchText = ''; @@ -45,66 +65,261 @@ abstract class HomeSettingsViewModelBase with Store { @action void setPinNativeToken(bool value) => _settingsStore.pinNativeTokenAtTop = value; + @action Future addToken({ required String contractAddress, required CryptoCurrency token, }) async { - if (_balanceViewModel.wallet.type == WalletType.ethereum) { - final erc20token = Erc20Token( - name: token.name, - symbol: token.title, - decimal: token.decimals, - contractAddress: contractAddress, - iconPath: token.iconPath, - ); + try { + isAddingToken = true; + if (_balanceViewModel.wallet.type == WalletType.ethereum) { + final erc20token = Erc20Token( + name: token.name, + symbol: token.title, + decimal: token.decimals, + contractAddress: contractAddress, + iconPath: token.iconPath, + ); - await ethereum!.addErc20Token(_balanceViewModel.wallet, erc20token); + await ethereum!.addErc20Token(_balanceViewModel.wallet, erc20token); + } + + if (_balanceViewModel.wallet.type == WalletType.polygon) { + final polygonToken = Erc20Token( + name: token.name, + symbol: token.title, + decimal: token.decimals, + contractAddress: contractAddress, + iconPath: token.iconPath, + ); + await polygon!.addErc20Token(_balanceViewModel.wallet, polygonToken); + } + + if (_balanceViewModel.wallet.type == WalletType.solana) { + await solana!.addSPLToken( + _balanceViewModel.wallet, + token, + contractAddress, + ); + } + + if (_balanceViewModel.wallet.type == WalletType.tron) { + await tron!.addTronToken(_balanceViewModel.wallet, token, contractAddress); + } + + _updateTokensList(); + _updateFiatPrices(token); + } finally { + isAddingToken = false; } - - if (_balanceViewModel.wallet.type == WalletType.polygon) { - final polygonToken = Erc20Token( - name: token.name, - symbol: token.title, - decimal: token.decimals, - contractAddress: contractAddress, - iconPath: token.iconPath, - ); - await polygon!.addErc20Token(_balanceViewModel.wallet, polygonToken); - } - - if (_balanceViewModel.wallet.type == WalletType.solana) { - await solana!.addSPLToken( - _balanceViewModel.wallet, - token, - contractAddress, - ); - } - - if (_balanceViewModel.wallet.type == WalletType.tron) { - await tron!.addTronToken(_balanceViewModel.wallet, token, contractAddress); - } - - _updateTokensList(); - _updateFiatPrices(token); } + @action Future deleteToken(CryptoCurrency token) async { - if (_balanceViewModel.wallet.type == WalletType.ethereum) { - await ethereum!.deleteErc20Token(_balanceViewModel.wallet, token as Erc20Token); - } + try { + isDeletingToken = true; + if (_balanceViewModel.wallet.type == WalletType.ethereum) { + await ethereum!.deleteErc20Token(_balanceViewModel.wallet, token as Erc20Token); + } - if (_balanceViewModel.wallet.type == WalletType.polygon) { - await polygon!.deleteErc20Token(_balanceViewModel.wallet, token as Erc20Token); - } + if (_balanceViewModel.wallet.type == WalletType.polygon) { + await polygon!.deleteErc20Token(_balanceViewModel.wallet, token as Erc20Token); + } - if (_balanceViewModel.wallet.type == WalletType.solana) { - await solana!.deleteSPLToken(_balanceViewModel.wallet, token); - } + if (_balanceViewModel.wallet.type == WalletType.solana) { + await solana!.deleteSPLToken(_balanceViewModel.wallet, token); + } - if (_balanceViewModel.wallet.type == WalletType.tron) { - await tron!.deleteTronToken(_balanceViewModel.wallet, token); + if (_balanceViewModel.wallet.type == WalletType.tron) { + await tron!.deleteTronToken(_balanceViewModel.wallet, token); + } + _updateTokensList(); + } finally { + isDeletingToken = false; + } + } + + Future checkIfERC20TokenContractAddressIsAPotentialScamAddress( + String contractAddress, + ) async { + try { + isValidatingContractAddress = true; + + if (!isEVMCompatibleChain(_balanceViewModel.wallet.type)) { + return false; + } + + bool isEthereum = _balanceViewModel.wallet.type == WalletType.ethereum; + + bool isPotentialScamViaMoralis = await _isPotentialScamTokenViaMoralis( + contractAddress, + isEthereum ? 'eth' : 'polygon', + ); + + bool isPotentialScamViaExplorers = await _isPotentialScamTokenViaExplorers( + contractAddress, + isEthereum: isEthereum, + ); + + bool isUnverifiedContract = await _isContractUnverified( + contractAddress, + isEthereum: isEthereum, + ); + + final showWarningForContractAddress = + isPotentialScamViaMoralis || isUnverifiedContract || isPotentialScamViaExplorers; + + return showWarningForContractAddress; + } finally { + isValidatingContractAddress = false; + } + } + + Future _isPotentialScamTokenViaMoralis( + String contractAddress, + String chainName, + ) async { + final uri = Uri.https( + 'deep-index.moralis.io', + '/api/v2.2/erc20/metadata', + { + "chain": chainName, + "addresses": contractAddress, + }, + ); + + try { + final response = await http.get( + uri, + headers: { + "Accept": "application/json", + "X-API-Key": secrets.moralisApiKey, + }, + ); + + final decodedResponse = jsonDecode(response.body); + + final tokenInfo = Erc20TokenInfoMoralis.fromJson(decodedResponse[0] as Map); + + // Based on analysis using Moralis internal metrics + if (tokenInfo.possibleSpam == true) { + return true; + } + + // Tokens whose contract have not been verified are potentially risky tokens. + if (tokenInfo.verifiedContract == false) { + return true; + } + + // Tokens with a security score less than 40 are potentially risky, requiring caution when dealing with them. + if (tokenInfo.securityScore == null || tokenInfo.securityScore! < 40) { + return true; + } + + // Absence of a website URL for an ERC-20 token can be a potential red flag. A legitimate ERC-20 projects should have a well-maintained website that provides information about the token, its purpose, team, and roadmap. + if (tokenInfo.links?.website == null || tokenInfo.links!.website!.isEmpty) { + return true; + } + + // Having a Fully Diluted Valiuation of 0 is a significant red flag that could signify: + // - An abandoned/unlaunched project + // - Incorrect/missing token data + // - Suspicious manipulation of token data + if (tokenInfo.fullyDilutedValuation == '0') { + return true; + } + + // I mean, a logo is the most basic of all the potential causes, but why does your fully functional project not have a logo? + if (tokenInfo.logo == null) { + return true; + } + + return false; + } catch (e) { + print('Error while checking scam via moralis: ${e.toString()}'); + return true; + } + } + + Future _isPotentialScamTokenViaExplorers( + String contractAddress, { + required bool isEthereum, + }) async { + final uri = Uri.https( + isEthereum ? "api.etherscan.io" : "api.polygonscan.com", + "/api", + { + "module": "token", + "action": "tokeninfo", + "contractaddress": contractAddress, + "apikey": isEthereum ? secrets.etherScanApiKey : secrets.polygonScanApiKey, + }, + ); + + try { + final response = await http.get(uri); + + final decodedResponse = jsonDecode(response.body) as Map; + + if (decodedResponse['status'] != '1') { + log('${response.body}\n'); + log('${decodedResponse['result']}\n'); + return true; + } + + final tokenInfo = + Erc20TokenInfoExplorers.fromJson(decodedResponse['result'][0] as Map); + + // A token without a website is a potential red flag + if (tokenInfo.website?.isEmpty == true) { + return true; + } + + return false; + } catch (e) { + print('Error while checking scam via explorers: ${e.toString()}'); + return true; + } + } + + Future _isContractUnverified( + String contractAddress, { + required bool isEthereum, + }) async { + final uri = Uri.https( + isEthereum ? "api.etherscan.io" : "api.polygonscan.com", + "/api", + { + "module": "contract", + "action": "getsourcecode", + "address": contractAddress, + "apikey": isEthereum ? secrets.etherScanApiKey : secrets.polygonScanApiKey, + }, + ); + + try { + final response = await http.get(uri); + + final decodedResponse = jsonDecode(response.body) as Map; + + if (decodedResponse['status'] == '0') { + print('${response.body}\n'); + print('${decodedResponse['result']}\n'); + return true; + } + + if (decodedResponse['status'] == '1' && + decodedResponse['result'][0]['ABI'] == 'Contract source code not verified') { + print('Call is valid but contract is not verified'); + return true; // Contract is not verified + } else { + print('Call is valid and contract is verified'); + return false; // Contract is verified + } + } catch (e) { + print('Error while checking contract verification: ${e.toString()}'); + return true; } - _updateTokensList(); } Future getToken(String contractAddress) async { diff --git a/lib/view_model/dashboard/receive_option_view_model.dart b/lib/view_model/dashboard/receive_option_view_model.dart index 1e4726eee..744e4c58d 100644 --- a/lib/view_model/dashboard/receive_option_view_model.dart +++ b/lib/view_model/dashboard/receive_option_view_model.dart @@ -11,19 +11,31 @@ class ReceiveOptionViewModel = ReceiveOptionViewModelBase with _$ReceiveOptionVi abstract class ReceiveOptionViewModelBase with Store { ReceiveOptionViewModelBase(this._wallet, this.initialPageOption) : selectedReceiveOption = initialPageOption ?? - (_wallet.type == WalletType.bitcoin + (_wallet.type == WalletType.bitcoin || + _wallet.type == WalletType.litecoin ? bitcoin!.getSelectedAddressType(_wallet) : ReceivePageOption.mainnet), _options = [] { final walletType = _wallet.type; - _options = walletType == WalletType.haven - ? [ReceivePageOption.mainnet] - : walletType == WalletType.bitcoin - ? [ - ...bitcoin!.getBitcoinReceivePageOptions(), - ...ReceivePageOptions.where((element) => element != ReceivePageOption.mainnet) - ] - : ReceivePageOptions; + switch (walletType) { + case WalletType.bitcoin: + _options = [ + ...bitcoin!.getBitcoinReceivePageOptions(), + ...ReceivePageOptions.where((element) => element != ReceivePageOption.mainnet) + ]; + break; + case WalletType.litecoin: + _options = [ + ...bitcoin!.getLitecoinReceivePageOptions(), + ...ReceivePageOptions.where((element) => element != ReceivePageOption.mainnet) + ]; + break; + case WalletType.haven: + _options = [ReceivePageOption.mainnet]; + break; + default: + _options = ReceivePageOptions; + } } final WalletBase _wallet; diff --git a/lib/view_model/dashboard/transaction_list_item.dart b/lib/view_model/dashboard/transaction_list_item.dart index 176b4e58d..47fc32ab6 100644 --- a/lib/view_model/dashboard/transaction_list_item.dart +++ b/lib/view_model/dashboard/transaction_list_item.dart @@ -56,7 +56,8 @@ class TransactionListItem extends ActionListItem with Keyable { } String get formattedPendingStatus { - if (balanceViewModel.wallet.type == WalletType.monero || balanceViewModel.wallet.type == WalletType.haven) { + if (balanceViewModel.wallet.type == WalletType.monero || + balanceViewModel.wallet.type == WalletType.haven) { if (transaction.confirmations >= 0 && transaction.confirmations < 10) { return ' (${transaction.confirmations}/10)'; } @@ -69,16 +70,21 @@ class TransactionListItem extends ActionListItem with Keyable { } String get formattedStatus { - if (transaction.direction == TransactionDirection.incoming) { - if (balanceViewModel.wallet.type == WalletType.monero || - balanceViewModel.wallet.type == WalletType.wownero || - balanceViewModel.wallet.type == WalletType.haven) { - return formattedPendingStatus; - } + if (balanceViewModel.wallet.type == WalletType.monero || + balanceViewModel.wallet.type == WalletType.wownero || + balanceViewModel.wallet.type == WalletType.haven) { + return formattedPendingStatus; } return transaction.isPending ? S.current.pending : ''; } + String get formattedType { + if (transaction.evmSignatureName == 'approval') { + return ' (${transaction.evmSignatureName})'; + } + return ''; + } + CryptoCurrency? get assetOfTransaction { try { if (balanceViewModel.wallet.type == WalletType.ethereum) { diff --git a/lib/view_model/exchange/exchange_trade_view_model.dart b/lib/view_model/exchange/exchange_trade_view_model.dart index 5d99ff8a5..4cb7e4cad 100644 --- a/lib/view_model/exchange/exchange_trade_view_model.dart +++ b/lib/view_model/exchange/exchange_trade_view_model.dart @@ -7,6 +7,7 @@ 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/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'; import 'package:cake_wallet/exchange/provider/thorchain_exchange.provider.dart'; import 'package:cake_wallet/exchange/provider/trocador_exchange_provider.dart'; import 'package:cake_wallet/exchange/trade.dart'; @@ -52,6 +53,8 @@ abstract class ExchangeTradeViewModelBase with Store { case ExchangeProviderDescription.quantex: _provider = QuantexExchangeProvider(); break; + case ExchangeProviderDescription.stealthEx: + _provider = StealthExExchangeProvider(); case ExchangeProviderDescription.thorChain: _provider = ThorChainExchangeProvider(tradesStore: trades); break; diff --git a/lib/view_model/exchange/exchange_view_model.dart b/lib/view_model/exchange/exchange_view_model.dart index f2ea8eeb4..d29b7df6b 100644 --- a/lib/view_model/exchange/exchange_view_model.dart +++ b/lib/view_model/exchange/exchange_view_model.dart @@ -4,6 +4,8 @@ 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/letsexchange_exchange_provider.dart'; +import 'package:cake_wallet/exchange/provider/stealth_ex_exchange_provider.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/sync_status.dart'; import 'package:cw_core/transaction_priority.dart'; @@ -119,7 +121,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with depositAmount = ''; receiveAmount = ''; receiveAddress = ''; - depositAddress = depositCurrency == wallet.currency ? wallet.walletAddresses.address : ''; + depositAddress = depositCurrency == wallet.currency ? wallet.walletAddresses.addressForExchange : ''; provider = providersForCurrentPair().first; final initialProvider = provider; provider!.checkIsAvailable().then((bool isAvailable) { @@ -153,6 +155,10 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with wallet.type == WalletType.litecoin || wallet.type == WalletType.bitcoinCash; + bool get hideAddressAfterExchange => + wallet.type == WalletType.monero || + wallet.type == WalletType.wownero; + bool _useTorOnly; final Box trades; final ExchangeTemplateStore _exchangeTemplateStore; @@ -160,15 +166,17 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with final SharedPreferences sharedPreferences; List get _allProviders => [ - ChangeNowExchangeProvider(settingsStore: _settingsStore), - SideShiftExchangeProvider(), - SimpleSwapExchangeProvider(), - ThorChainExchangeProvider(tradesStore: trades), - if (FeatureFlag.isExolixEnabled) ExolixExchangeProvider(), - QuantexExchangeProvider(), - TrocadorExchangeProvider( - useTorOnly: _useTorOnly, providerStates: _settingsStore.trocadorProviderStates), - ]; + ChangeNowExchangeProvider(settingsStore: _settingsStore), + SideShiftExchangeProvider(), + SimpleSwapExchangeProvider(), + ThorChainExchangeProvider(tradesStore: trades), + if (FeatureFlag.isExolixEnabled) ExolixExchangeProvider(), + QuantexExchangeProvider(), + LetsExchangeExchangeProvider(), + StealthExExchangeProvider(), + TrocadorExchangeProvider( + useTorOnly: _useTorOnly, providerStates: _settingsStore.trocadorProviderStates), + ]; @observable ExchangeProvider? provider; @@ -536,6 +544,11 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with isFixedRate: isFixedRateMode, ); + if (hideAddressAfterExchange) { + wallet.walletAddresses.hiddenAddresses.add(depositAddress); + await wallet.walletAddresses.saveAddressesInBox(); + } + var amount = isFixedRateMode ? receiveAmount : depositAmount; amount = amount.replaceAll(',', '.'); @@ -599,8 +612,8 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with isReceiveAmountEntered = false; depositAmount = ''; receiveAmount = ''; - depositAddress = depositCurrency == wallet.currency ? wallet.walletAddresses.address : ''; - receiveAddress = receiveCurrency == wallet.currency ? wallet.walletAddresses.address : ''; + depositAddress = depositCurrency == wallet.currency ? wallet.walletAddresses.addressForExchange : ''; + receiveAddress = receiveCurrency == wallet.currency ? wallet.walletAddresses.addressForExchange : ''; isDepositAddressEnabled = !(depositCurrency == wallet.currency); isFixedRateMode = false; _onPairChange(); @@ -845,6 +858,13 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with ); } + if ((trade.memo == null || trade.memo!.isEmpty)) { + return CreateTradeResult( + result: false, + errorMessage: 'Memo is required for Thorchain trade', + ); + } + final currenciesToCheckPattern = RegExp('0x[0-9a-zA-Z]'); // Perform checks for payOutAddress diff --git a/lib/view_model/hardware_wallet/ledger_view_model.dart b/lib/view_model/hardware_wallet/ledger_view_model.dart index f05b1c805..19b190fe3 100644 --- a/lib/view_model/hardware_wallet/ledger_view_model.dart +++ b/lib/view_model/hardware_wallet/ledger_view_model.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:io'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; @@ -9,11 +10,19 @@ import 'package:cake_wallet/wallet_type_utils.dart'; import 'package:cw_core/hardware/device_connection_type.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_type.dart'; -import 'package:ledger_flutter/ledger_flutter.dart'; + +import 'package:ledger_flutter_plus/ledger_flutter_plus.dart' as sdk; +import 'package:mobx/mobx.dart'; import 'package:permission_handler/permission_handler.dart'; -class LedgerViewModel { - late final Ledger ledger; +part 'ledger_view_model.g.dart'; + +class LedgerViewModel = LedgerViewModelBase with _$LedgerViewModel; + +abstract class LedgerViewModelBase with Store { + // late final Ledger ledger; + late final sdk.LedgerInterface ledgerPlusBLE; + late final sdk.LedgerInterface ledgerPlusUSB; bool get _doesSupportHardwareWallets { if (!DeviceInfo.instance.isMobile) { @@ -21,53 +30,97 @@ class LedgerViewModel { } if (isMoneroOnly) { - return DeviceConnectionType.supportedConnectionTypes(WalletType.monero, Platform.isIOS) + return DeviceConnectionType.supportedConnectionTypes( + WalletType.monero, Platform.isIOS) .isNotEmpty; } return true; } - LedgerViewModel() { + LedgerViewModelBase() { if (_doesSupportHardwareWallets) { - ledger = Ledger( - options: LedgerOptions( - scanMode: ScanMode.balanced, - maxScanDuration: const Duration(minutes: 5), - ), - onPermissionRequest: (_) async { - Map statuses = await [ - Permission.bluetoothScan, - Permission.bluetoothConnect, - Permission.bluetoothAdvertise, - ].request(); + reaction((_) => bleIsEnabled, (_) { + if (bleIsEnabled) _initBLE(); + }); + updateBleState(); - return statuses.values.where((status) => status.isDenied).isEmpty; - }, - ); + if (!Platform.isIOS) { + ledgerPlusUSB = sdk.LedgerInterface.usb(); + } } } - Future connectLedger(LedgerDevice device) async { - await ledger.connect(device); + @observable + bool bleIsEnabled = false; - if (device.connectionType == ConnectionType.usb) _device = device; + 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(); + + return statuses.values.where((status) => status.isDenied).isEmpty; + }); + _bleIsInitialized = true; + } } - LedgerDevice? _device; + Future updateBleState() async { + final bleState = await sdk.UniversalBle.getBluetoothAvailabilityState(); - bool get isConnected => ledger.devices.isNotEmpty || _device != null; + final newState = bleState == sdk.AvailabilityState.poweredOn; - LedgerDevice get device => _device ?? ledger.devices.first; + if (newState != bleIsEnabled) bleIsEnabled = newState; + } + + Stream scanForBleDevices() => ledgerPlusBLE.scan(); + + Stream scanForUsbDevices() => ledgerPlusUSB.scan(); + + Future connectLedger(sdk.LedgerDevice device, WalletType type) async { + if (isConnected) { + try { + await _connection!.disconnect(); + } catch (_) {} + } + final ledger = device.connectionType == sdk.ConnectionType.ble + ? ledgerPlusBLE + : ledgerPlusUSB; + + if (_connectionChangeListener == null) { + _connectionChangeListener = ledger.deviceStateChanges.listen((event) { + print('Ledger Device State Changed: $event'); + if (event == sdk.BleConnectionState.disconnected) { + _connection = null; + _connectionChangeListener?.cancel(); + } + }); + } + + _connection = await ledger.connect(device); + } + + StreamSubscription? _connectionChangeListener; + sdk.LedgerConnection? _connection; + + bool get isConnected => _connection != null && !(_connection!.isDisconnected); + + sdk.LedgerConnection get connection => _connection!; void setLedger(WalletBase wallet) { switch (wallet.type) { case WalletType.bitcoin: - return bitcoin!.setLedger(wallet, ledger, device); + case WalletType.litecoin: + return bitcoin!.setLedgerConnection(wallet, connection); case WalletType.ethereum: - return ethereum!.setLedger(wallet, ledger, device); + return ethereum!.setLedgerConnection(wallet, connection); case WalletType.polygon: - return polygon!.setLedger(wallet, ledger, device); + return polygon!.setLedgerConnection(wallet, connection); default: throw Exception('Unexpected wallet type: ${wallet.type}'); } diff --git a/lib/view_model/link_view_model.dart b/lib/view_model/link_view_model.dart index 99aed486e..27f0c0560 100644 --- a/lib/view_model/link_view_model.dart +++ b/lib/view_model/link_view_model.dart @@ -65,15 +65,16 @@ class LinkViewModel { if (isNanoGptLink) { switch (currentLink?.authority ?? '') { case "exchange": - case "send": return PaymentRequest.fromUri(currentLink); + case "send": + return {"paymentRequest": PaymentRequest.fromUri(currentLink)}; case "buy": return true; } } if (_isValidPaymentUri) { - return PaymentRequest.fromUri(currentLink); + return {"paymentRequest": PaymentRequest.fromUri(currentLink)}; } return null; diff --git a/lib/view_model/new_wallet_type_view_model.dart b/lib/view_model/new_wallet_type_view_model.dart new file mode 100644 index 000000000..9237bc692 --- /dev/null +++ b/lib/view_model/new_wallet_type_view_model.dart @@ -0,0 +1,16 @@ +import 'package:cw_core/wallet_info.dart'; +import 'package:hive/hive.dart'; +import 'package:mobx/mobx.dart'; + +part 'new_wallet_type_view_model.g.dart'; + +class NewWalletTypeViewModel = NewWalletTypeViewModelBase with _$NewWalletTypeViewModel; + +abstract class NewWalletTypeViewModelBase with Store { + NewWalletTypeViewModelBase(this._walletInfoSource); + + @computed + bool get hasExisitingWallet => _walletInfoSource.isNotEmpty; + + final Box _walletInfoSource; +} diff --git a/lib/view_model/rescan_view_model.dart b/lib/view_model/rescan_view_model.dart index dcc81c0a0..7ef1f6ba5 100644 --- a/lib/view_model/rescan_view_model.dart +++ b/lib/view_model/rescan_view_model.dart @@ -29,6 +29,12 @@ abstract class RescanViewModelBase with Store { @computed bool get isSilentPaymentsScan => wallet.type == WalletType.bitcoin; + @computed + bool get isMwebScan => wallet.type == WalletType.litecoin; + + Future get isBitcoinMempoolAPIEnabled async => + wallet.type == WalletType.bitcoin && await bitcoin!.checkIfMempoolAPIIsEnabled(wallet); + @action Future rescanCurrentWallet({required int restoreHeight}) async { state = RescanWalletState.rescaning; diff --git a/lib/view_model/restore/restore_from_qr_vm.dart b/lib/view_model/restore/restore_from_qr_vm.dart index f5938911b..cbdad85b8 100644 --- a/lib/view_model/restore/restore_from_qr_vm.dart +++ b/lib/view_model/restore/restore_from_qr_vm.dart @@ -7,12 +7,13 @@ import 'package:cake_wallet/solana/solana.dart'; import 'package:cake_wallet/tron/tron.dart'; 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:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/store/app_store.dart'; -import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_base.dart'; import 'package:cake_wallet/core/generate_wallet_password.dart'; import 'package:cake_wallet/core/wallet_creation_service.dart'; import 'package:cw_core/wallet_credentials.dart'; @@ -25,14 +26,19 @@ part 'restore_from_qr_vm.g.dart'; class WalletRestorationFromQRVM = WalletRestorationFromQRVMBase with _$WalletRestorationFromQRVM; abstract class WalletRestorationFromQRVMBase extends WalletCreationVM with Store { - WalletRestorationFromQRVMBase(AppStore appStore, WalletCreationService walletCreationService, - Box walletInfoSource, WalletType type) + WalletRestorationFromQRVMBase( + AppStore appStore, + WalletCreationService walletCreationService, + Box walletInfoSource, + WalletType type, + SeedSettingsViewModel seedSettingsViewModel) : height = 0, viewKey = '', spendKey = '', wif = '', address = '', - super(appStore, walletInfoSource, walletCreationService, type: type, isRecovery: true); + super(appStore, walletInfoSource, walletCreationService, seedSettingsViewModel, + type: type, isRecovery: true); @observable int height; @@ -52,16 +58,9 @@ abstract class WalletRestorationFromQRVMBase extends WalletCreationVM with Store bool get hasRestorationHeight => type == WalletType.monero || type == WalletType.wownero; @override - WalletCredentials getCredentialsFromRestoredWallet( - dynamic options, RestoredWallet restoreWallet) { + Future getWalletCredentialsFromQRCredentials( + RestoredWallet restoreWallet) async { final password = generateWalletPassword(); - String? passphrase; - DerivationInfo? derivationInfo; - if (options != null) { - derivationInfo = options["derivationInfo"] as DerivationInfo?; - passphrase = options["passphrase"] as String?; - } - derivationInfo ??= getDefaultDerivation(); switch (restoreWallet.restoreMode) { case WalletRestoreMode.keys: @@ -114,12 +113,20 @@ abstract class WalletRestorationFromQRVMBase extends WalletCreationVM with Store ); case WalletType.bitcoin: case WalletType.litecoin: + + final derivationInfoList = await getDerivationInfoFromQRCredentials(restoreWallet); + DerivationInfo derivationInfo; + if (derivationInfoList.isEmpty) { + derivationInfo = getDefaultCreateDerivation()!; + } else { + derivationInfo = derivationInfoList.first; + } return bitcoin!.createBitcoinRestoreWalletFromSeedCredentials( name: name, mnemonic: restoreWallet.mnemonicSeed ?? '', password: password, - passphrase: passphrase, - derivationType: derivationInfo!.derivationType!, + passphrase: restoreWallet.passphrase, + derivationType: derivationInfo.derivationType!, derivationPath: derivationInfo.derivationPath!, ); case WalletType.bitcoinCash: @@ -127,26 +134,46 @@ abstract class WalletRestorationFromQRVMBase extends WalletCreationVM with Store name: name, mnemonic: restoreWallet.mnemonicSeed ?? '', password: password, + passphrase: restoreWallet.passphrase, ); case WalletType.ethereum: return ethereum!.createEthereumRestoreWalletFromSeedCredentials( - name: name, mnemonic: restoreWallet.mnemonicSeed ?? '', password: password); + name: name, + mnemonic: restoreWallet.mnemonicSeed ?? '', + password: password, + passphrase: restoreWallet.passphrase, + ); case WalletType.nano: + final derivationInfo = + (await getDerivationInfoFromQRCredentials(restoreWallet)).first; return nano!.createNanoRestoreWalletFromSeedCredentials( name: name, mnemonic: restoreWallet.mnemonicSeed ?? '', password: password, - derivationType: derivationInfo!.derivationType!, + derivationType: derivationInfo.derivationType!, + passphrase: restoreWallet.passphrase, ); case WalletType.polygon: return polygon!.createPolygonRestoreWalletFromSeedCredentials( - name: name, mnemonic: restoreWallet.mnemonicSeed ?? '', password: password); + name: name, + mnemonic: restoreWallet.mnemonicSeed ?? '', + password: password, + passphrase: restoreWallet.passphrase, + ); case WalletType.solana: return solana!.createSolanaRestoreWalletFromSeedCredentials( - name: name, mnemonic: restoreWallet.mnemonicSeed ?? '', password: password); + name: name, + mnemonic: restoreWallet.mnemonicSeed ?? '', + password: password, + passphrase: restoreWallet.passphrase, + ); case WalletType.tron: return tron!.createTronRestoreWalletFromSeedCredentials( - name: name, mnemonic: restoreWallet.mnemonicSeed ?? '', password: password); + name: name, + mnemonic: restoreWallet.mnemonicSeed ?? '', + password: password, + passphrase: restoreWallet.passphrase, + ); case WalletType.wownero: return wownero!.createWowneroRestoreWalletFromSeedCredentials( name: name, @@ -163,8 +190,8 @@ abstract class WalletRestorationFromQRVMBase extends WalletCreationVM with Store } @override - Future processFromRestoredWallet( - WalletCredentials credentials, RestoredWallet restoreWallet) async { + Future processFromRestoredWallet(WalletCredentials credentials, + RestoredWallet restoreWallet) async { try { switch (restoreWallet.restoreMode) { case WalletRestoreMode.keys: diff --git a/lib/view_model/restore/restore_wallet.dart b/lib/view_model/restore/restore_wallet.dart index d46c48092..2c2a25005 100644 --- a/lib/view_model/restore/restore_wallet.dart +++ b/lib/view_model/restore/restore_wallet.dart @@ -10,6 +10,7 @@ class RestoredWallet { this.spendKey, this.viewKey, this.mnemonicSeed, + this.passphrase, this.txAmount, this.txDescription, this.recipientName, @@ -23,6 +24,7 @@ class RestoredWallet { final String? spendKey; final String? viewKey; final String? mnemonicSeed; + final String? passphrase; final String? txAmount; final String? txDescription; final String? recipientName; @@ -46,11 +48,13 @@ class RestoredWallet { final height = json['height'] as String?; final mnemonic_seed = json['mnemonic_seed'] as String?; final seed = json['seed'] as String? ?? json['hexSeed'] as String?; + final passphrase = json['passphrase'] as String?; return RestoredWallet( restoreMode: json['mode'] as WalletRestoreMode, type: json['type'] as WalletType, address: json['address'] as String?, mnemonicSeed: mnemonic_seed ?? seed, + passphrase: passphrase, height: height != null ? int.parse(height) : 0, ); } 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 335b1a006..23850befa 100644 --- a/lib/view_model/restore/wallet_restore_from_qr_code.dart +++ b/lib/view_model/restore/wallet_restore_from_qr_code.dart @@ -142,6 +142,10 @@ class WalletRestoreFromQRCode { return WalletRestoreMode.seed; } + if ((type == WalletType.monero || type == WalletType.wownero)) { + return WalletRestoreMode.seed; + } + seedValue.split(' ').forEach((element) { if (!words.contains(element)) { throw Exception( diff --git a/lib/view_model/restore_from_backup_view_model.dart b/lib/view_model/restore_from_backup_view_model.dart index 365afb1fa..432cac67e 100644 --- a/lib/view_model/restore_from_backup_view_model.dart +++ b/lib/view_model/restore_from_backup_view_model.dart @@ -46,7 +46,7 @@ abstract class RestoreFromBackupViewModelBase with Store { final data = await file.readAsBytes(); await backupService.importBackup(data, password); - await main(); + await initializeAppAtRoot(reInitializing: true); final store = getIt.get(); ReactionDisposer? reaction; diff --git a/lib/view_model/seed_settings_view_model.dart b/lib/view_model/seed_settings_view_model.dart new file mode 100644 index 000000000..221899676 --- /dev/null +++ b/lib/view_model/seed_settings_view_model.dart @@ -0,0 +1,41 @@ +import 'package:cake_wallet/entities/seed_type.dart'; +import 'package:cake_wallet/store/app_store.dart'; +import 'package:cake_wallet/store/seed_settings_store.dart'; +import 'package:mobx/mobx.dart'; + +part 'seed_settings_view_model.g.dart'; + +class SeedSettingsViewModel = SeedSettingsViewModelBase with _$SeedSettingsViewModel; + +abstract class SeedSettingsViewModelBase with Store { + SeedSettingsViewModelBase(this._appStore, this._seedSettingsStore); + + @computed + MoneroSeedType get moneroSeedType => _appStore.settingsStore.moneroSeedType; + + @action + void setMoneroSeedType(MoneroSeedType seedType) => _appStore.settingsStore.moneroSeedType = seedType; + + @computed + BitcoinSeedType get bitcoinSeedType => _appStore.settingsStore.bitcoinSeedType; + + @action + void setBitcoinSeedType(BitcoinSeedType derivationType) => + _appStore.settingsStore.bitcoinSeedType = derivationType; + + @computed + NanoSeedType get nanoSeedType => _appStore.settingsStore.nanoSeedType; + + @action + void setNanoSeedType(NanoSeedType derivationType) => + _appStore.settingsStore.nanoSeedType = derivationType; + + @computed + String? get passphrase => this._seedSettingsStore.passphrase; + + @action + void setPassphrase(String? passphrase) => this._seedSettingsStore.passphrase = passphrase; + + final AppStore _appStore; + final SeedSettingsStore _seedSettingsStore; +} diff --git a/lib/view_model/seed_type_view_model.dart b/lib/view_model/seed_type_view_model.dart deleted file mode 100644 index e3597be1c..000000000 --- a/lib/view_model/seed_type_view_model.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:cake_wallet/entities/seed_type.dart'; -import 'package:cake_wallet/store/app_store.dart'; -import 'package:mobx/mobx.dart'; - -part 'seed_type_view_model.g.dart'; - -class SeedTypeViewModel = SeedTypeViewModelBase with _$SeedTypeViewModel; - -abstract class SeedTypeViewModelBase with Store { - SeedTypeViewModelBase(this._appStore); - - @computed - SeedType get moneroSeedType => _appStore.settingsStore.moneroSeedType; - - @action - void setMoneroSeedType(SeedType seedType) => _appStore.settingsStore.moneroSeedType = seedType; - - final AppStore _appStore; -} diff --git a/lib/view_model/send/output.dart b/lib/view_model/send/output.dart index 94854df31..e53127e0c 100644 --- a/lib/view_model/send/output.dart +++ b/lib/view_model/send/output.dart @@ -79,6 +79,9 @@ abstract class OutputBase with Store { bool get isParsedAddress => parsedAddress.parseFrom != ParseFrom.notParsed && parsedAddress.name.isNotEmpty; + @observable + String? stealthAddress; + @computed int get formattedCryptoAmount { int amount = 0; @@ -134,9 +137,8 @@ abstract class OutputBase with Store { final trc20EstimatedFee = tron!.getTronTRC20EstimatedFee(_wallet) ?? 0; return double.parse(trc20EstimatedFee.toString()); } - } - + if (_wallet.type == WalletType.solana) { return solana!.getEstimateFees(_wallet) ?? 0.0; } @@ -145,16 +147,16 @@ abstract class OutputBase with Store { _settingsStore.priority[_wallet.type]!, formattedCryptoAmount); if (_wallet.type == WalletType.bitcoin) { - if (_settingsStore.priority[_wallet.type] == bitcoin!.getBitcoinTransactionPriorityCustom()) { - fee = bitcoin!.getEstimatedFeeWithFeeRate(_wallet, - _settingsStore.customBitcoinFeeRate,formattedCryptoAmount); + if (_settingsStore.priority[_wallet.type] == + bitcoin!.getBitcoinTransactionPriorityCustom()) { + fee = bitcoin!.getEstimatedFeeWithFeeRate( + _wallet, _settingsStore.customBitcoinFeeRate, formattedCryptoAmount); } return bitcoin!.formatterBitcoinAmountToDouble(amount: fee); } - if (_wallet.type == WalletType.litecoin || - _wallet.type == WalletType.bitcoinCash) { + if (_wallet.type == WalletType.litecoin || _wallet.type == WalletType.bitcoinCash) { return bitcoin!.formatterBitcoinAmountToDouble(amount: fee); } @@ -249,7 +251,8 @@ abstract class OutputBase with Store { try { final fiat = calculateFiatAmount( price: _fiatConversationStore.prices[cryptoCurrencyHandler()]!, - cryptoAmount: sendAll ? cryptoFullBalance.replaceAll(",", ".") : cryptoAmount.replaceAll(',', '.')); + cryptoAmount: + sendAll ? cryptoFullBalance.replaceAll(",", ".") : cryptoAmount.replaceAll(',', '.')); if (fiatAmount != fiat) { fiatAmount = fiat; } diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index 863c83957..24e83f364 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -18,11 +18,12 @@ 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/wownero/wownero.dart'; import 'package:cw_core/exceptions.dart'; +import 'package:cw_core/transaction_info.dart'; import 'package:cw_core/transaction_priority.dart'; +import 'package:cw_core/unspent_coin_type.dart'; import 'package:cake_wallet/view_model/send/output.dart'; import 'package:cake_wallet/view_model/send/send_template_view_model.dart'; import 'package:hive/hive.dart'; -import 'package:ledger_flutter/ledger_flutter.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/entities/template.dart'; import 'package:cake_wallet/core/address_validator.dart'; @@ -66,8 +67,9 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor this.balanceViewModel, this.contactListViewModel, this.transactionDescriptionBox, - this.ledgerViewModel, - ) : state = InitialExecutionState(), + this.ledgerViewModel, { + this.coinTypeToSpendFrom = UnspentCoinType.any, + }) : state = InitialExecutionState(), currencies = appStore.wallet!.balance.keys.toList(), selectedCryptoCurrency = appStore.wallet!.currency, hasMultipleTokens = isEVMCompatibleChain(appStore.wallet!.type) || @@ -96,6 +98,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor ObservableList outputs; + final UnspentCoinType coinTypeToSpendFrom; + @action void addOutput() { outputs @@ -118,7 +122,17 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor @computed bool get isBatchSending => outputs.length > 1; - bool get shouldDisplaySendALL => walletType != WalletType.solana; + bool get shouldDisplaySendALL { + if (walletType == WalletType.solana) return false; + + if (walletType == WalletType.ethereum && selectedCryptoCurrency == CryptoCurrency.eth) + return false; + + if (walletType == WalletType.polygon && selectedCryptoCurrency == CryptoCurrency.matic) + return false; + + return true; + } @computed String get pendingTransactionFiatAmount { @@ -216,7 +230,14 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor PendingTransaction? pendingTransaction; @computed - String get balance => wallet.balance[selectedCryptoCurrency]!.formattedAvailableBalance; + String get balance { + if (coinTypeToSpendFrom == UnspentCoinType.mweb) { + return balanceViewModel.balances.values.first.secondAvailableBalance; + } else if (coinTypeToSpendFrom == UnspentCoinType.nonMweb) { + return balanceViewModel.balances.values.first.availableBalance; + } + return wallet.balance[selectedCryptoCurrency]!.formattedFullAvailableBalance; + } @computed bool get isFiatDisabled => balanceViewModel.isFiatDisabled; @@ -374,43 +395,63 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor throw Exception("THORChain does not support Taproot addresses"); } } + + if (wallet.type == WalletType.bitcoin) { + final updatedOutputs = bitcoin!.updateOutputs(pendingTransaction!, outputs); + + if (outputs.length == updatedOutputs.length) { + outputs = ObservableList.of(updatedOutputs); + } + } + state = ExecutedSuccessfullyState(); return pendingTransaction; } catch (e) { - if (e is LedgerException) { - final errorCode = e.errorCode.toRadixString(16); - final fallbackMsg = - e.message.isNotEmpty ? e.message : "Unexpected Ledger Error Code: $errorCode"; - final errorMsg = ledgerViewModel!.interpretErrorCode(errorCode) ?? fallbackMsg; - - state = FailureState(errorMsg); - } else { + // if (e is LedgerException) { + // final errorCode = e.errorCode.toRadixString(16); + // final fallbackMsg = + // e.message.isNotEmpty ? e.message : "Unexpected Ledger Error Code: $errorCode"; + // final errorMsg = ledgerViewModel!.interpretErrorCode(errorCode) ?? fallbackMsg; + // + // state = FailureState(errorMsg); + // } else { state = FailureState(translateErrorMessage(e, wallet.type, wallet.currency)); - } + // } } return null; } @action - Future replaceByFee(String txId, String newFee) async { + Future replaceByFee(TransactionInfo tx, String newFee) async { state = IsExecutingState(); - final isSufficient = await bitcoin!.isChangeSufficientForFee(wallet, txId, newFee); + try { + final isSufficient = await bitcoin!.isChangeSufficientForFee(wallet, tx.id, newFee); - if (!isSufficient) { - state = AwaitingConfirmationState( - title: S.current.confirm_fee_deduction, - message: S.current.confirm_fee_deduction_content, - onConfirm: () async { - pendingTransaction = await bitcoin!.replaceByFee(wallet, txId, newFee); - state = ExecutedSuccessfullyState(); - }, - onCancel: () { - state = FailureState('Insufficient change for fee'); - }); - } else { - pendingTransaction = await bitcoin!.replaceByFee(wallet, txId, newFee); + if (!isSufficient) { + state = AwaitingConfirmationState( + title: S.current.confirm_fee_deduction, + message: S.current.confirm_fee_deduction_content, + onConfirm: () async => await _executeReplaceByFee(tx, newFee), + onCancel: () => state = FailureState('Insufficient change for fee')); + } else { + await _executeReplaceByFee(tx, newFee); + } + } catch (e) { + state = FailureState(e.toString()); + } + } + + Future _executeReplaceByFee(TransactionInfo tx, String newFee) async { + clearOutputs(); + final output = outputs.first; + output.address = tx.outputAddresses?.first ?? ''; + + try { + pendingTransaction = await bitcoin!.replaceByFee(wallet, tx.id, newFee); state = ExecutedSuccessfullyState(); + } catch (e) { + state = FailureState(e.toString()); } } @@ -440,12 +481,18 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor nano!.updateTransactions(wallet); } + if (pendingTransaction!.id.isNotEmpty) { + + final descriptionKey = '${pendingTransaction!.id}_${wallet.walletAddresses.primaryAddress}'; _settingsStore.shouldSaveRecipientAddress ? await transactionDescriptionBox.add(TransactionDescription( - id: pendingTransaction!.id, recipientAddress: address, transactionNote: note)) - : await transactionDescriptionBox - .add(TransactionDescription(id: pendingTransaction!.id, transactionNote: note)); + id: descriptionKey, + recipientAddress: address, + transactionNote: note)) + : await transactionDescriptionBox.add(TransactionDescription( + id: descriptionKey, + transactionNote: note)); } state = TransactionCommitted(); @@ -473,8 +520,12 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor case WalletType.bitcoin: case WalletType.litecoin: case WalletType.bitcoinCash: - return bitcoin!.createBitcoinTransactionCredentials(outputs, - priority: priority!, feeRate: customBitcoinFeeRate); + return bitcoin!.createBitcoinTransactionCredentials( + outputs, + priority: priority!, + feeRate: customBitcoinFeeRate, + coinTypeToSpendFrom: coinTypeToSpendFrom, + ); case WalletType.monero: return monero! @@ -654,6 +705,9 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor return S.current.tx_no_dust_exception; } if (error is TransactionCommitFailed) { + if (error.errorMessage != null && error.errorMessage!.contains("no peers replied")) { + return S.current.tx_commit_failed_no_peers; + } return "${S.current.tx_commit_failed}${error.errorMessage != null ? "\n\n${error.errorMessage}" : ""}"; } if (error is TransactionCommitFailedDustChange) { @@ -671,6 +725,9 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor if (error is TransactionCommitFailedBIP68Final) { return S.current.tx_rejected_bip68_final; } + if (error is TransactionCommitFailedLessThanMin) { + return S.current.fee_less_than_min; + } if (error is TransactionNoDustOnChangeException) { return S.current.tx_commit_exception_no_dust_on_change(error.min, error.max); } diff --git a/lib/view_model/settings/mweb_settings_view_model.dart b/lib/view_model/settings/mweb_settings_view_model.dart new file mode 100644 index 000000000..c6370e23f --- /dev/null +++ b/lib/view_model/settings/mweb_settings_view_model.dart @@ -0,0 +1,36 @@ +import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:mobx/mobx.dart'; + +part 'mweb_settings_view_model.g.dart'; + +class MwebSettingsViewModel = MwebSettingsViewModelBase with _$MwebSettingsViewModel; + +abstract class MwebSettingsViewModelBase with Store { + MwebSettingsViewModelBase(this._settingsStore, this._wallet) { + mwebEnabled = bitcoin!.getMwebEnabled(_wallet); + _settingsStore.mwebAlwaysScan = mwebEnabled; + } + + final SettingsStore _settingsStore; + final WalletBase _wallet; + + @computed + bool get mwebCardDisplay => _settingsStore.mwebCardDisplay; + + @observable + late bool mwebEnabled; + + @action + void setMwebCardDisplay(bool value) { + _settingsStore.mwebCardDisplay = value; + } + + @action + void setMwebEnabled(bool value) { + mwebEnabled = value; + bitcoin!.setMwebEnabled(_wallet, value); + _settingsStore.mwebAlwaysScan = value; + } +} diff --git a/lib/view_model/settings/privacy_settings_view_model.dart b/lib/view_model/settings/privacy_settings_view_model.dart index 90511af8e..c1e0fb1ce 100644 --- a/lib/view_model/settings/privacy_settings_view_model.dart +++ b/lib/view_model/settings/privacy_settings_view_model.dart @@ -1,3 +1,4 @@ +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'; @@ -75,6 +76,9 @@ abstract class PrivacySettingsViewModelBase with Store { @computed bool get useTronGrid => _settingsStore.useTronGrid; + @computed + bool get useMempoolFeeAPI => _settingsStore.useMempoolFeeAPI; + @computed bool get lookupTwitter => _settingsStore.lookupsTwitter; @@ -99,6 +103,8 @@ abstract class PrivacySettingsViewModelBase with Store { bool get canUseTronGrid => _wallet.type == WalletType.tron; + bool get canUseMempoolFeeAPI => _wallet.type == WalletType.bitcoin; + @action void setShouldSaveRecipientAddress(bool value) => _settingsStore.shouldSaveRecipientAddress = value; @@ -156,4 +162,9 @@ abstract class PrivacySettingsViewModelBase with Store { _settingsStore.useTronGrid = value; tron!.updateTronGridUsageState(_wallet, value); } + + @action + void setUseMempoolFeeAPI(bool value) { + _settingsStore.useMempoolFeeAPI = value; + } } diff --git a/lib/view_model/support_view_model.dart b/lib/view_model/support_view_model.dart index 2bb749b42..45b9cb9bf 100644 --- a/lib/view_model/support_view_model.dart +++ b/lib/view_model/support_view_model.dart @@ -14,14 +14,16 @@ abstract class SupportViewModelBase with Store { : items = [ LinkListItem( title: 'Email', + icon: 'assets/images/support_icon.png', linkTitle: 'support@cakewallet.com', link: 'mailto:support@cakewallet.com'), if (!isMoneroOnly) LinkListItem( title: 'Website', + icon: 'assets/images/global.png', linkTitle: 'cakewallet.com', link: 'https://cakewallet.com'), - if (!isMoneroOnly) + if (!isMoneroOnly) LinkListItem( title: 'GitHub', icon: 'assets/images/github.png', @@ -29,7 +31,12 @@ abstract class SupportViewModelBase with Store { linkTitle: S.current.apk_update, link: 'https://github.com/cake-tech/cake_wallet/releases'), LinkListItem( - title: 'Telegram', + title: 'Telegram Community', + icon: 'assets/images/Telegram.png', + linkTitle: 't.me/cakewallet', + link: 'https://t.me/cakewallet'), + LinkListItem( + title: 'Telegram Support Bot', icon: 'assets/images/Telegram.png', linkTitle: '@cakewallet_bot', link: 'https://t.me/cakewallet_bot'), diff --git a/lib/view_model/trade_details_view_model.dart b/lib/view_model/trade_details_view_model.dart index eed1b6c75..19315f40d 100644 --- a/lib/view_model/trade_details_view_model.dart +++ b/lib/view_model/trade_details_view_model.dart @@ -4,9 +4,11 @@ import 'package:cake_wallet/exchange/exchange_provider_description.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/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'; import 'package:cake_wallet/exchange/provider/thorchain_exchange.provider.dart'; import 'package:cake_wallet/exchange/provider/trocador_exchange_provider.dart'; import 'package:cake_wallet/exchange/trade.dart'; @@ -59,6 +61,11 @@ abstract class TradeDetailsViewModelBase with Store { break; case ExchangeProviderDescription.quantex: _provider = QuantexExchangeProvider(); + case ExchangeProviderDescription.letsExchange: + _provider = LetsExchangeExchangeProvider(); + break; + case ExchangeProviderDescription.stealthEx: + _provider = StealthExExchangeProvider(); break; } @@ -86,6 +93,10 @@ abstract class TradeDetailsViewModelBase with Store { return 'https://track.ninerealms.com/${trade.id}'; case ExchangeProviderDescription.quantex: return 'https://myquantex.com/send/${trade.id}'; + case ExchangeProviderDescription.letsExchange: + return 'https://letsexchange.io/?transactionId=${trade.id}'; + case ExchangeProviderDescription.stealthEx: + return 'https://stealthex.io/exchange/?id=${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 18715e508..1833a0f51 100644 --- a/lib/view_model/transaction_details_view_model.dart +++ b/lib/view_model/transaction_details_view_model.dart @@ -36,7 +36,8 @@ abstract class TransactionDetailsViewModelBase with Store { required this.transactionDescriptionBox, required this.wallet, required this.settingsStore, - required this.sendViewModel}) + required this.sendViewModel, + this.canReplaceByFee = false}) : items = [], RBFListItems = [], newFee = 0, @@ -51,8 +52,7 @@ abstract class TransactionDetailsViewModelBase with Store { break; case WalletType.bitcoin: _addElectrumListItems(tx, dateFormat); - _addBumpFeesListItems(tx); - _checkForRBF(); + if(!canReplaceByFee)_checkForRBF(tx); break; case WalletType.litecoin: case WalletType.bitcoinCash: @@ -110,9 +110,11 @@ abstract class TransactionDetailsViewModelBase with Store { } catch (e) {} })); + final descriptionKey = '${transactionInfo.txHash}_${wallet.walletAddresses.primaryAddress}'; + final description = transactionDescriptionBox.values.firstWhere( - (val) => val.id == transactionInfo.txHash, - orElse: () => TransactionDescription(id: transactionInfo.txHash)); + (val) => val.id == descriptionKey || val.id == transactionInfo.txHash, + orElse: () => TransactionDescription(id: descriptionKey)); items.add(TextFieldListItem( title: S.current.note_tap_to_change, @@ -139,13 +141,11 @@ abstract class TransactionDetailsViewModelBase with Store { bool showRecipientAddress; bool isRecipientAddressShown; int newFee; + String? rawTransaction; TransactionPriority? transactionPriority; @observable - bool _canReplaceByFee = false; - - @computed - bool get canReplaceByFee => _canReplaceByFee /*&& transactionInfo.confirmations <= 0*/; + bool canReplaceByFee; String _explorerUrl(WalletType type, String txId) { switch (type) { @@ -162,9 +162,9 @@ abstract class TransactionDetailsViewModelBase with Store { case WalletType.ethereum: return 'https://etherscan.io/tx/${txId}'; case WalletType.nano: - return 'https://nanolooker.com/block/${txId}'; + return 'https://nanexplorer.com/nano/block/${txId}'; case WalletType.banano: - return 'https://bananolooker.com/block/${txId}'; + return 'https://nanexplorer.com/banano/block/${txId}'; case WalletType.polygon: return 'https://polygonscan.com/tx/${txId}'; case WalletType.solana: @@ -192,9 +192,9 @@ abstract class TransactionDetailsViewModelBase with Store { case WalletType.ethereum: return S.current.view_transaction_on + 'etherscan.io'; case WalletType.nano: - return S.current.view_transaction_on + 'nanolooker.com'; + return S.current.view_transaction_on + 'nanexplorer.com'; case WalletType.banano: - return S.current.view_transaction_on + 'bananolooker.com'; + return S.current.view_transaction_on + 'nanexplorer.com'; case WalletType.polygon: return S.current.view_transaction_on + 'polygonscan.com'; case WalletType.solana: @@ -347,17 +347,28 @@ abstract class TransactionDetailsViewModelBase with Store { items.addAll(_items); } - void _addBumpFeesListItems(TransactionInfo tx) { + void addBumpFeesListItems(TransactionInfo tx, String rawTransaction) { transactionPriority = bitcoin!.getBitcoinTransactionPriorityMedium(); + final inputsCount = (transactionInfo.inputAddresses?.isEmpty ?? true) + ? 1 + : transactionInfo.inputAddresses!.length; + final outputsCount = (transactionInfo.outputAddresses?.isEmpty ?? true) + ? 1 + : transactionInfo.outputAddresses!.length; newFee = bitcoin!.getFeeAmountForPriority( - wallet, - bitcoin!.getBitcoinTransactionPriorityMedium(), - transactionInfo.inputAddresses?.length ?? 1, - transactionInfo.outputAddresses?.length ?? 1); + wallet, bitcoin!.getBitcoinTransactionPriorityMedium(), inputsCount, outputsCount); RBFListItems.add(StandartListItem(title: S.current.old_fee, value: tx.feeFormatted() ?? '0.0')); + if (transactionInfo.fee != null && rawTransaction.isNotEmpty) { + final size = bitcoin!.getTransactionVSize(wallet, rawTransaction); + final recommendedRate = (transactionInfo.fee! / size).round() + 1; + + RBFListItems.add( + StandartListItem(title: 'New recommended fee rate', value: '$recommendedRate sat/byte')); + } + final priorities = priorityForWalletType(wallet.type); final selectedItem = priorities.indexOf(sendViewModel.transactionPriority); final customItem = priorities @@ -383,14 +394,21 @@ abstract class TransactionDetailsViewModelBase with Store { return setNewFee(value: sliderValue, priority: transactionPriority!); })); - if (transactionInfo.inputAddresses != null) { + if (transactionInfo.inputAddresses != null && transactionInfo.inputAddresses!.isNotEmpty) { RBFListItems.add(StandardExpandableListItem( title: S.current.inputs, expandableItems: transactionInfo.inputAddresses!)); } - if (transactionInfo.outputAddresses != null) { - RBFListItems.add(StandardExpandableListItem( - title: S.current.outputs, expandableItems: transactionInfo.outputAddresses!)); + if (transactionInfo.outputAddresses != null && transactionInfo.outputAddresses!.isNotEmpty) { + final outputAddresses = transactionInfo.outputAddresses!.map((element) { + if (element.contains('OP_RETURN:') && element.length > 40) { + return element.substring(0, 40) + '...'; + } + return element; + }).toList(); + + RBFListItems.add( + StandardExpandableListItem(title: S.current.outputs, expandableItems: outputAddresses)); } } @@ -416,11 +434,12 @@ abstract class TransactionDetailsViewModelBase with Store { } @action - Future _checkForRBF() async { + Future _checkForRBF(TransactionInfo tx) async { if (wallet.type == WalletType.bitcoin && transactionInfo.direction == TransactionDirection.outgoing) { - if (await bitcoin!.canReplaceByFee(wallet, transactionInfo.id)) { - _canReplaceByFee = true; + rawTransaction = await bitcoin!.canReplaceByFee(wallet, tx); + if (rawTransaction != null) { + canReplaceByFee = true; } } } @@ -441,7 +460,7 @@ abstract class TransactionDetailsViewModelBase with Store { return bitcoin!.formatterBitcoinAmountToString(amount: newFee); } - void replaceByFee(String newFee) => sendViewModel.replaceByFee(transactionInfo.id, newFee); + void replaceByFee(String newFee) => sendViewModel.replaceByFee(transactionInfo, newFee,); @computed String get pendingTransactionFiatAmountValueFormatted => sendViewModel.isFiatDisabled diff --git a/lib/view_model/unspent_coins/unspent_coins_list_view_model.dart b/lib/view_model/unspent_coins/unspent_coins_list_view_model.dart index e2d8469f1..f16b8390f 100644 --- a/lib/view_model/unspent_coins/unspent_coins_list_view_model.dart +++ b/lib/view_model/unspent_coins/unspent_coins_list_view_model.dart @@ -3,6 +3,7 @@ import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/utils/exception_handler.dart'; import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_item.dart'; import 'package:cake_wallet/wownero/wownero.dart'; +import 'package:cw_core/unspent_coin_type.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/unspent_transaction_output.dart'; import 'package:cw_core/wallet_base.dart'; @@ -16,9 +17,11 @@ part 'unspent_coins_list_view_model.g.dart'; class UnspentCoinsListViewModel = UnspentCoinsListViewModelBase with _$UnspentCoinsListViewModel; abstract class UnspentCoinsListViewModelBase with Store { - UnspentCoinsListViewModelBase( - {required this.wallet, required Box unspentCoinsInfo}) - : _unspentCoinsInfo = unspentCoinsInfo, + UnspentCoinsListViewModelBase({ + required this.wallet, + required Box unspentCoinsInfo, + this.coinTypeToSpendFrom = UnspentCoinType.any, + }) : _unspentCoinsInfo = unspentCoinsInfo, _items = ObservableList() { _updateUnspentCoinsInfo(); _updateUnspents(); @@ -26,6 +29,7 @@ abstract class UnspentCoinsListViewModelBase with Store { WalletBase wallet; final Box _unspentCoinsInfo; + final UnspentCoinType coinTypeToSpendFrom; @observable ObservableList _items; @@ -38,6 +42,10 @@ abstract class UnspentCoinsListViewModelBase with Store { final info = getUnspentCoinInfo(item.hash, item.address, item.amountRaw, item.vout, item.keyImage); + if (info == null) { + return; + } + info.isFrozen = item.isFrozen; info.isSending = item.isSending; info.note = item.note; @@ -50,15 +58,21 @@ abstract class UnspentCoinsListViewModelBase with Store { } } - UnspentCoinsInfo getUnspentCoinInfo( - String hash, String address, int value, int vout, String? keyImage) => - _unspentCoinsInfo.values.firstWhere((element) => + UnspentCoinsInfo? getUnspentCoinInfo( + String hash, String address, int value, int vout, String? keyImage) { + try { + return _unspentCoinsInfo.values.firstWhere((element) => element.walletId == wallet.id && element.hash == hash && element.address == address && element.value == value && element.vout == vout && element.keyImage == keyImage); + } catch (e) { + print("UnspentCoinsInfo not found for coin: $e"); + return null; + } + } String formatAmountToString(int fullBalance) { if (wallet.type == WalletType.monero) @@ -85,11 +99,18 @@ abstract class UnspentCoinsListViewModelBase with Store { } List _getUnspents() { - if (wallet.type == WalletType.monero) return monero!.getUnspents(wallet); - if (wallet.type == WalletType.wownero) return wownero!.getUnspents(wallet); - if ([WalletType.bitcoin, WalletType.litecoin, WalletType.bitcoinCash].contains(wallet.type)) - return bitcoin!.getUnspents(wallet); - return List.empty(); + switch (wallet.type) { + case WalletType.monero: + return monero!.getUnspents(wallet); + case WalletType.wownero: + return wownero!.getUnspents(wallet); + case WalletType.bitcoin: + case WalletType.litecoin: + case WalletType.bitcoinCash: + return bitcoin!.getUnspents(wallet, coinTypeToSpendFrom: coinTypeToSpendFrom); + default: + return List.empty(); + } } @action @@ -97,10 +118,13 @@ abstract class UnspentCoinsListViewModelBase with Store { _items.clear(); List unspents = []; - _getUnspents().forEach((elem) { + _getUnspents().forEach((Unspent elem) { try { final info = getUnspentCoinInfo(elem.hash, elem.address, elem.value, elem.vout, elem.keyImage); + if (info == null) { + return; + } unspents.add(UnspentCoinsItem( address: elem.address, diff --git a/lib/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart b/lib/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart index 2edda3d29..d365c8e00 100644 --- a/lib/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart +++ b/lib/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart @@ -78,6 +78,8 @@ abstract class WalletAddressEditOrCreateViewModelBase with Store { wallet, accountIndex: monero!.getCurrentAccount(wallet).id, label: label); + final addr = await monero!.getSubaddressList(wallet).subaddresses.first.address; // first because the order is reversed + wallet.walletAddresses.manualAddresses.add(addr); await wallet.save(); } @@ -88,6 +90,8 @@ abstract class WalletAddressEditOrCreateViewModelBase with Store { wallet, accountIndex: wownero!.getCurrentAccount(wallet).id, label: label); + final addr = await wownero!.getSubaddressList(wallet).subaddresses.first.address; // first because the order is reversed + wallet.walletAddresses.manualAddresses.add(addr); await wallet.save(); } diff --git a/lib/view_model/wallet_address_list/wallet_address_hidden_list_header.dart b/lib/view_model/wallet_address_list/wallet_address_hidden_list_header.dart new file mode 100644 index 000000000..74c7605e9 --- /dev/null +++ b/lib/view_model/wallet_address_list/wallet_address_hidden_list_header.dart @@ -0,0 +1,3 @@ +import 'package:cake_wallet/utils/list_item.dart'; + +class WalletAddressHiddenListHeader extends ListItem {} \ No newline at end of file diff --git a/lib/view_model/wallet_address_list/wallet_address_list_item.dart b/lib/view_model/wallet_address_list/wallet_address_list_item.dart index 6a6e34113..725b1ddbf 100644 --- a/lib/view_model/wallet_address_list/wallet_address_list_item.dart +++ b/lib/view_model/wallet_address_list/wallet_address_list_item.dart @@ -1,7 +1,7 @@ import 'package:cake_wallet/utils/list_item.dart'; class WalletAddressListItem extends ListItem { - const WalletAddressListItem({ + WalletAddressListItem({ required this.address, required this.isPrimary, this.id, @@ -11,6 +11,8 @@ class WalletAddressListItem extends ListItem { this.isChange = false, // Address that is only ever used once, shouldn't be used to receive funds, copy and paste, share etc this.isOneTimeReceiveAddress = false, + this.isHidden = false, + this.isManual = false, }) : super(); final int? id; @@ -20,6 +22,8 @@ class WalletAddressListItem extends ListItem { final int? txCount; final String? balance; final bool isChange; + bool isHidden; + bool isManual; final bool? isOneTimeReceiveAddress; @override 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 0bd936720..3e399266a 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 @@ -1,12 +1,15 @@ +import 'dart:developer' as dev; + import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/core/fiat_conversion_service.dart'; import 'package:cake_wallet/core/wallet_change_listener_view_model.dart'; import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart'; +import 'package:cake_wallet/entities/fiat_api_mode.dart'; import 'package:cake_wallet/entities/fiat_currency.dart'; import 'package:cake_wallet/ethereum/ethereum.dart'; 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/nano/nano.dart'; import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/solana/solana.dart'; import 'package:cake_wallet/store/app_store.dart'; @@ -16,6 +19,7 @@ import 'package:cake_wallet/store/yat/yat_store.dart'; import 'package:cake_wallet/tron/tron.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'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_header.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart'; import 'package:cake_wallet/wownero/wownero.dart'; @@ -27,7 +31,8 @@ import 'package:mobx/mobx.dart'; part 'wallet_address_list_view_model.g.dart'; -class WalletAddressListViewModel = WalletAddressListViewModelBase with _$WalletAddressListViewModel; +class WalletAddressListViewModel = WalletAddressListViewModelBase + with _$WalletAddressListViewModel; abstract class PaymentURI { PaymentURI({required this.amount, required this.address}); @@ -37,12 +42,11 @@ abstract class PaymentURI { } class MoneroURI extends PaymentURI { - MoneroURI({required String amount, required String address}) - : super(amount: amount, address: address); + MoneroURI({required super.amount, required super.address}); @override String toString() { - var base = 'monero:' + address; + var base = 'monero:$address'; if (amount.isNotEmpty) { base += '?tx_amount=${amount.replaceAll(',', '.')}'; @@ -53,12 +57,11 @@ class MoneroURI extends PaymentURI { } class HavenURI extends PaymentURI { - HavenURI({required String amount, required String address}) - : super(amount: amount, address: address); + HavenURI({required super.amount, required super.address}); @override String toString() { - var base = 'haven:' + address; + var base = 'haven:$address'; if (amount.isNotEmpty) { base += '?tx_amount=${amount.replaceAll(',', '.')}'; @@ -69,12 +72,11 @@ class HavenURI extends PaymentURI { } class BitcoinURI extends PaymentURI { - BitcoinURI({required String amount, required String address}) - : super(amount: amount, address: address); + BitcoinURI({required super.amount, required super.address}); @override String toString() { - var base = 'bitcoin:' + address; + var base = 'bitcoin:$address'; if (amount.isNotEmpty) { base += '?amount=${amount.replaceAll(',', '.')}'; @@ -85,12 +87,11 @@ class BitcoinURI extends PaymentURI { } class LitecoinURI extends PaymentURI { - LitecoinURI({required String amount, required String address}) - : super(amount: amount, address: address); + LitecoinURI({required super.amount, required super.address}); @override String toString() { - var base = 'litecoin:' + address; + var base = 'litecoin:$address'; if (amount.isNotEmpty) { base += '?amount=${amount.replaceAll(',', '.')}'; @@ -101,12 +102,11 @@ class LitecoinURI extends PaymentURI { } class EthereumURI extends PaymentURI { - EthereumURI({required String amount, required String address}) - : super(amount: amount, address: address); + EthereumURI({required super.amount, required super.address}); @override String toString() { - var base = 'ethereum:' + address; + var base = 'ethereum:$address'; if (amount.isNotEmpty) { base += '?amount=${amount.replaceAll(',', '.')}'; @@ -117,8 +117,7 @@ class EthereumURI extends PaymentURI { } class BitcoinCashURI extends PaymentURI { - BitcoinCashURI({required String amount, required String address}) - : super(amount: amount, address: address); + BitcoinCashURI({required super.amount, required super.address}); @override String toString() { @@ -133,12 +132,11 @@ class BitcoinCashURI extends PaymentURI { } class NanoURI extends PaymentURI { - NanoURI({required String amount, required String address}) - : super(amount: amount, address: address); + NanoURI({required super.amount, required super.address}); @override String toString() { - var base = 'nano:' + address; + var base = 'nano:$address'; if (amount.isNotEmpty) { base += '?amount=${amount.replaceAll(',', '.')}'; } @@ -148,12 +146,11 @@ class NanoURI extends PaymentURI { } class PolygonURI extends PaymentURI { - PolygonURI({required String amount, required String address}) - : super(amount: amount, address: address); + PolygonURI({required super.amount, required super.address}); @override String toString() { - var base = 'polygon:' + address; + var base = 'polygon:$address'; if (amount.isNotEmpty) { base += '?amount=${amount.replaceAll(',', '.')}'; @@ -164,12 +161,12 @@ class PolygonURI extends PaymentURI { } class SolanaURI extends PaymentURI { - SolanaURI({required String amount, required String address}) - : super(amount: amount, address: address); + SolanaURI({required super.amount, required super.address}); @override String toString() { - var base = 'solana:' + address; + var base = 'solana:$address'; + if (amount.isNotEmpty) { base += '?amount=${amount.replaceAll(',', '.')}'; } @@ -179,12 +176,12 @@ class SolanaURI extends PaymentURI { } class TronURI extends PaymentURI { - TronURI({required String amount, required String address}) - : super(amount: amount, address: address); + TronURI({required super.amount, required super.address}); @override String toString() { - var base = 'tron:' + address; + var base = 'tron:$address'; + if (amount.isNotEmpty) { base += '?amount=${amount.replaceAll(',', '.')}'; } @@ -194,12 +191,11 @@ class TronURI extends PaymentURI { } class WowneroURI extends PaymentURI { - WowneroURI({required String amount, required String address}) - : super(amount: amount, address: address); + WowneroURI({required super.amount, required super.address}); @override String toString() { - var base = 'wownero:' + address; + var base = 'wownero:$address'; if (amount.isNotEmpty) { base += '?tx_amount=${amount.replaceAll(',', '.')}'; @@ -209,7 +205,8 @@ class WowneroURI extends PaymentURI { } } -abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewModel with Store { +abstract class WalletAddressListViewModelBase + extends WalletChangeListenerViewModel with Store { WalletAddressListViewModelBase({ required AppStore appStore, required this.yatStore, @@ -217,8 +214,8 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo }) : _baseItems = [], selectedCurrency = walletTypeToCryptoCurrency(appStore.wallet!.type), _cryptoNumberFormat = NumberFormat(_cryptoNumberPattern), - hasAccounts = - appStore.wallet!.type == WalletType.monero || appStore.wallet!.type == WalletType.wownero || appStore.wallet!.type == WalletType.haven, + hasAccounts = [WalletType.monero, WalletType.wownero, WalletType.haven] + .contains(appStore.wallet!.type), amount = '', _settingsStore = appStore.settingsStore, super(appStore: appStore) { @@ -230,7 +227,8 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo _init(); selectedCurrency = walletTypeToCryptoCurrency(wallet.type); - hasAccounts = wallet.type == WalletType.monero || wallet.type == WalletType.wownero || wallet.type == WalletType.haven; + hasAccounts = [WalletType.monero, WalletType.wownero, WalletType.haven] + .contains(wallet.type); } static const String _cryptoNumberPattern = '0.00000000'; @@ -240,7 +238,11 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo final FiatConversionStore fiatConversionStore; final SettingsStore _settingsStore; - List get currencies => [walletTypeToCryptoCurrency(wallet.type), ...FiatCurrency.all]; + double? _fiatRate; + String _rawAmount = ''; + + List get currencies => + [walletTypeToCryptoCurrency(wallet.type), ...FiatCurrency.all]; String get buttonTitle { if (isElectrumWallet) { @@ -266,56 +268,39 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo WalletType get type => wallet.type; @computed - WalletAddressListItem get address => - WalletAddressListItem(address: wallet.walletAddresses.address, isPrimary: false); + WalletAddressListItem get address => WalletAddressListItem( + address: wallet.walletAddresses.address, isPrimary: false); @computed PaymentURI get uri { - if (wallet.type == WalletType.monero) { - return MoneroURI(amount: amount, address: address.address); + switch (wallet.type) { + case WalletType.monero: + return MoneroURI(amount: amount, address: address.address); + case WalletType.haven: + return HavenURI(amount: amount, address: address.address); + case WalletType.bitcoin: + return BitcoinURI(amount: amount, address: address.address); + case WalletType.litecoin: + return LitecoinURI(amount: amount, address: address.address); + case WalletType.ethereum: + return EthereumURI(amount: amount, address: address.address); + case WalletType.bitcoinCash: + return BitcoinCashURI(amount: amount, address: address.address); + case WalletType.banano: + return NanoURI(amount: amount, address: address.address); + case WalletType.nano: + return NanoURI(amount: amount, address: address.address); + case WalletType.polygon: + return PolygonURI(amount: amount, address: address.address); + case WalletType.solana: + return SolanaURI(amount: amount, address: address.address); + case WalletType.tron: + return TronURI(amount: amount, address: address.address); + case WalletType.wownero: + return WowneroURI(amount: amount, address: address.address); + case WalletType.none: + throw Exception('Unexpected type: ${type.toString()}'); } - - if (wallet.type == WalletType.haven) { - return HavenURI(amount: amount, address: address.address); - } - - if (wallet.type == WalletType.bitcoin) { - return BitcoinURI(amount: amount, address: address.address); - } - - if (wallet.type == WalletType.litecoin) { - return LitecoinURI(amount: amount, address: address.address); - } - - if (wallet.type == WalletType.ethereum) { - return EthereumURI(amount: amount, address: address.address); - } - - if (wallet.type == WalletType.bitcoinCash) { - return BitcoinCashURI(amount: amount, address: address.address); - } - - if (wallet.type == WalletType.nano) { - return NanoURI(amount: amount, address: address.address); - } - - if (wallet.type == WalletType.polygon) { - return PolygonURI(amount: amount, address: address.address); - } - - if (wallet.type == WalletType.solana) { - return SolanaURI(amount: amount, address: address.address); - } - - if (wallet.type == WalletType.tron) { - return TronURI(amount: amount, address: address.address); - } - - if (wallet.type == WalletType.wownero) { - return WowneroURI(amount: amount, address: address.address); - } - - throw Exception('Unexpected type: ${type.toString()}'); } @computed @@ -328,22 +313,29 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo final addressList = ObservableList(); if (wallet.type == WalletType.monero) { - final primaryAddress = monero!.getSubaddressList(wallet).subaddresses.first; - final addressItems = monero!.getSubaddressList(wallet).subaddresses.map((subaddress) { + final primaryAddress = + monero!.getSubaddressList(wallet).subaddresses.first; + final addressItems = + monero!.getSubaddressList(wallet).subaddresses.map((subaddress) { final isPrimary = subaddress == primaryAddress; return WalletAddressListItem( - id: subaddress.id, - isPrimary: isPrimary, - name: subaddress.label, - address: subaddress.address); + id: subaddress.id, + isPrimary: isPrimary, + name: subaddress.label, + address: subaddress.address, + balance: subaddress.received, + txCount: subaddress.txCount, + ); }); addressList.addAll(addressItems); } if (wallet.type == WalletType.wownero) { - final primaryAddress = wownero!.getSubaddressList(wallet).subaddresses.first; - final addressItems = wownero!.getSubaddressList(wallet).subaddresses.map((subaddress) { + final primaryAddress = + wownero!.getSubaddressList(wallet).subaddresses.first; + final addressItems = + wownero!.getSubaddressList(wallet).subaddresses.map((subaddress) { final isPrimary = subaddress == primaryAddress; return WalletAddressListItem( @@ -356,8 +348,10 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo } if (wallet.type == WalletType.haven) { - final primaryAddress = haven!.getSubaddressList(wallet).subaddresses.first; - final addressItems = haven!.getSubaddressList(wallet).subaddresses.map((subaddress) { + final primaryAddress = + haven!.getSubaddressList(wallet).subaddresses.first; + final addressItems = + haven!.getSubaddressList(wallet).subaddresses.map((subaddress) { final isPrimary = subaddress == primaryAddress; return WalletAddressListItem( @@ -371,7 +365,8 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo if (isElectrumWallet) { if (bitcoin!.hasSelectedSilentPayments(wallet)) { - final addressItems = bitcoin!.getSilentPaymentAddresses(wallet).map((address) { + final addressItems = + bitcoin!.getSilentPaymentAddresses(wallet).map((address) { final isPrimary = address.id == 0; return WalletAddressListItem( @@ -404,7 +399,7 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo }); addressList.addAll(receivedAddressItems); } else { - final addressItems = bitcoin!.getSubAddresses(wallet).map((subaddress) { + var addressItems = bitcoin!.getSubAddresses(wallet).map((subaddress) { final isPrimary = subaddress.id == 0; return WalletAddressListItem( @@ -417,6 +412,19 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo walletTypeToCryptoCurrency(type), subaddress.balance), isChange: subaddress.isChange); }); + + // don't show all 1000+ mweb addresses: + if (wallet.type == WalletType.litecoin && addressItems.length >= 1000) { + // find the index of the last item with a txCount > 0 + final addressItemsList = addressItems.toList(); + int index = addressItemsList + .lastIndexWhere((item) => (item.txCount ?? 0) > 0); + if (index == -1) { + index = 0; + } + // show only up to that index + 20: + addressItems = addressItemsList.sublist(0, index + 20); + } addressList.addAll(addressItems); } } @@ -424,19 +432,22 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo if (wallet.type == WalletType.ethereum) { final primaryAddress = ethereum!.getAddress(wallet); - addressList.add(WalletAddressListItem(isPrimary: true, name: null, address: primaryAddress)); + addressList.add(WalletAddressListItem( + isPrimary: true, name: null, address: primaryAddress)); } if (wallet.type == WalletType.polygon) { final primaryAddress = polygon!.getAddress(wallet); - addressList.add(WalletAddressListItem(isPrimary: true, name: null, address: primaryAddress)); + addressList.add(WalletAddressListItem( + isPrimary: true, name: null, address: primaryAddress)); } if (wallet.type == WalletType.solana) { final primaryAddress = solana!.getAddress(wallet); - addressList.add(WalletAddressListItem(isPrimary: true, name: null, address: primaryAddress)); + addressList.add(WalletAddressListItem( + isPrimary: true, name: null, address: primaryAddress)); } if (wallet.type == WalletType.nano) { @@ -446,11 +457,26 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo address: wallet.walletAddresses.address, )); } - + if (wallet.type == WalletType.tron) { final primaryAddress = tron!.getAddress(wallet); - addressList.add(WalletAddressListItem(isPrimary: true, name: null, address: primaryAddress)); + addressList.add(WalletAddressListItem( + isPrimary: true, name: null, address: primaryAddress)); + } + + for (var i = 0; i < addressList.length; i++) { + if (!(addressList[i] is WalletAddressListItem)) continue; + (addressList[i] as WalletAddressListItem).isHidden = wallet + .walletAddresses.hiddenAddresses + .contains((addressList[i] as WalletAddressListItem).address); + } + + for (var i = 0; i < addressList.length; i++) { + if (!(addressList[i] is WalletAddressListItem)) continue; + (addressList[i] as WalletAddressListItem).isManual = wallet + .walletAddresses.manualAddresses + .contains((addressList[i] as WalletAddressListItem).address); } if (searchText.isNotEmpty) { @@ -465,50 +491,87 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo return addressList; } + Future toggleHideAddress(WalletAddressListItem item) async { + if (item.isHidden) { + wallet.walletAddresses.hiddenAddresses + .removeWhere((element) => element == item.address); + } else { + wallet.walletAddresses.hiddenAddresses.add(item.address); + } + await wallet.walletAddresses.saveAddressesInBox(); + if (wallet.type == WalletType.monero) { + monero! + .getSubaddressList(wallet) + .update(wallet, accountIndex: monero!.getCurrentAccount(wallet).id); + } else if (wallet.type == WalletType.wownero) { + wownero! + .getSubaddressList(wallet) + .update(wallet, accountIndex: wownero!.getCurrentAccount(wallet).id); + } else if (wallet.type == WalletType.haven) { + haven! + .getSubaddressList(wallet) + .update(wallet, accountIndex: haven!.getCurrentAccount(wallet).id); + } + } + @observable bool hasAccounts; @computed String get accountLabel { - if (wallet.type == WalletType.monero) { - return monero!.getCurrentAccount(wallet).label; + switch (wallet.type) { + case WalletType.monero: + return monero!.getCurrentAccount(wallet).label; + case WalletType.wownero: + wownero!.getCurrentAccount(wallet).label; + case WalletType.haven: + return haven!.getCurrentAccount(wallet).label; + default: + return ''; } - - if (wallet.type == WalletType.wownero) { - return wownero!.getCurrentAccount(wallet).label; - } - - if (wallet.type == WalletType.haven) { - return haven!.getCurrentAccount(wallet).label; - } - return ''; } @computed - bool get hasAddressList => - wallet.type == WalletType.monero || - wallet.type == WalletType.wownero || - wallet.type == WalletType.haven || - wallet.type == WalletType.bitcoinCash || - wallet.type == WalletType.bitcoin || - wallet.type == WalletType.litecoin; + bool get hasAddressList => [ + WalletType.monero, + WalletType.wownero, + WalletType.haven, + WalletType.bitcoinCash, + WalletType.bitcoin, + WalletType.litecoin + ].contains(wallet.type); @computed - bool get isElectrumWallet => - wallet.type == WalletType.bitcoin || - wallet.type == WalletType.litecoin || - wallet.type == WalletType.bitcoinCash; + bool get isElectrumWallet => [ + WalletType.bitcoin, + WalletType.litecoin, + WalletType.bitcoinCash + ].contains(wallet.type); + + @computed + bool get isBalanceAvailable => isElectrumWallet; + + @computed + bool get isReceivedAvailable => + [WalletType.monero, WalletType.wownero].contains(wallet.type); @computed bool get isSilentPayments => - wallet.type == WalletType.bitcoin && bitcoin!.hasSelectedSilentPayments(wallet); + wallet.type == WalletType.bitcoin && + bitcoin!.hasSelectedSilentPayments(wallet); @computed bool get isAutoGenerateSubaddressEnabled => - _settingsStore.autoGenerateSubaddressStatus != AutoGenerateSubaddressStatus.disabled && + _settingsStore.autoGenerateSubaddressStatus != + AutoGenerateSubaddressStatus.disabled && !isSilentPayments; + @computed + bool get showAddManualAddresses => + !isAutoGenerateSubaddressEnabled || + [WalletType.monero, WalletType.wownero].contains(wallet.type); + List _baseItems; final YatStore yatStore; @@ -519,7 +582,7 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo @action Future setAddressType(dynamic option) async { - if (wallet.type == WalletType.bitcoin) { + if ([WalletType.bitcoin, WalletType.litecoin].contains(wallet.type)) { await bitcoin!.setAddressType(wallet, option); } } @@ -527,23 +590,50 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo void _init() { _baseItems = []; - if (wallet.type == WalletType.monero || wallet.type == WalletType.wownero || wallet.type == WalletType.haven) { + if (wallet.walletAddresses.hiddenAddresses.isNotEmpty) { + _baseItems.add(WalletAddressHiddenListHeader()); + } + + if ([ + WalletType.monero, + WalletType.wownero, + WalletType.haven, + ].contains(wallet.type)) { _baseItems.add(WalletAccountListHeader()); } - if (wallet.type != WalletType.nano && wallet.type != WalletType.banano) { + if (![WalletType.nano, WalletType.banano].contains(wallet.type)) { _baseItems.add(WalletAddressListHeader()); } + if (wallet.isEnabledAutoGenerateSubaddress) { + wallet.walletAddresses.address = wallet.walletAddresses.latestAddress; + } } @action void selectCurrency(Currency currency) { selectedCurrency = currency; + + if (currency is FiatCurrency && _settingsStore.fiatCurrency != currency) { + final cryptoCurrency = walletTypeToCryptoCurrency(wallet.type); + + dev.log("Requesting Fiat rate for $cryptoCurrency-$currency"); + FiatConversionService.fetchPrice( + crypto: cryptoCurrency, + fiat: currency, + torOnly: _settingsStore.fiatApiMode == FiatApiMode.torOnly, + ).then((value) { + dev.log("Received Fiat rate 1 $cryptoCurrency = $value $currency"); + _fiatRate = value; + _convertAmountToCrypto(); + }); + } } @action void changeAmount(String amount) { this.amount = amount; + this._rawAmount = amount; if (selectedCurrency is FiatCurrency) { _convertAmountToCrypto(); } @@ -554,11 +644,20 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo searchText = text; } + @action void _convertAmountToCrypto() { final cryptoCurrency = walletTypeToCryptoCurrency(wallet.type); + final fiatRate = + _fiatRate ?? (fiatConversionStore.prices[cryptoCurrency] ?? 0.0); + + if (fiatRate <= 0.0) { + dev.log("Invalid Fiat Rate $fiatRate"); + amount = ''; + return; + } + try { - final crypto = - double.parse(amount.replaceAll(',', '.')) / fiatConversionStore.prices[cryptoCurrency]!; + final crypto = double.parse(_rawAmount.replaceAll(',', '.')) / fiatRate; final cryptoAmountTmp = _cryptoNumberFormat.format(crypto); if (amount != cryptoAmountTmp) { amount = cryptoAmountTmp; diff --git a/lib/view_model/wallet_creation_vm.dart b/lib/view_model/wallet_creation_vm.dart index e14934986..17a8d6d28 100644 --- a/lib/view_model/wallet_creation_vm.dart +++ b/lib/view_model/wallet_creation_vm.dart @@ -1,20 +1,23 @@ import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/core/execution_state.dart'; import 'package:cake_wallet/core/wallet_creation_service.dart'; -import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/entities/background_tasks.dart'; +import 'package:cake_wallet/entities/generate_name.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/nano/nano.dart'; +import 'package:cake_wallet/store/app_store.dart'; +import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cake_wallet/view_model/restore/restore_mode.dart'; import 'package:cake_wallet/view_model/restore/restore_wallet.dart'; -import 'package:hive/hive.dart'; -import 'package:mobx/mobx.dart'; -import 'package:cake_wallet/core/execution_state.dart'; +import 'package:cake_wallet/view_model/seed_settings_view_model.dart'; +import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_credentials.dart'; -import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; -import 'package:cake_wallet/store/app_store.dart'; -import 'package:cake_wallet/entities/generate_name.dart'; +import 'package:hive/hive.dart'; +import 'package:mobx/mobx.dart'; import 'package:polyseed/polyseed.dart'; part 'wallet_creation_vm.g.dart'; @@ -23,6 +26,7 @@ class WalletCreationVM = WalletCreationVMBase with _$WalletCreationVM; abstract class WalletCreationVMBase with Store { WalletCreationVMBase(this._appStore, this._walletInfoSource, this.walletCreationService, + this.seedSettingsViewModel, {required this.type, required this.isRecovery}) : state = InitialExecutionState(), name = ''; @@ -44,7 +48,6 @@ abstract class WalletCreationVMBase with Store { @observable String? repeatedWalletPassword; - bool get hasWalletPassword => SettingsStoreBase.walletPasswordDirectInput; WalletType type; @@ -52,6 +55,7 @@ abstract class WalletCreationVMBase with Store { final WalletCreationService walletCreationService; final Box _walletInfoSource; final AppStore _appStore; + final SeedSettingsViewModel seedSettingsViewModel; bool isPolyseed(String seed) => (type == WalletType.monero || type == WalletType.wownero) && @@ -80,8 +84,9 @@ abstract class WalletCreationVMBase with Store { walletCreationService.checkIfExists(name); final dirPath = await pathForWalletDir(name: name, type: type); final path = await pathForWallet(name: name, type: type); + final credentials = restoreWallet != null - ? getCredentialsFromRestoredWallet(options, restoreWallet) + ? await getWalletCredentialsFromQRCredentials(restoreWallet) : getCredentials(options); final walletInfo = WalletInfo.external( @@ -95,8 +100,9 @@ abstract class WalletCreationVMBase with Store { dirPath: dirPath, address: '', showIntroCakePayCard: (!walletCreationService.typeExists(type)) && type != WalletType.haven, - derivationInfo: credentials.derivationInfo ?? getDefaultDerivation(), + derivationInfo: credentials.derivationInfo ?? getDefaultCreateDerivation(), hardwareWalletType: credentials.hardwareWalletType, + parentAddress: credentials.parentAddress, ); credentials.walletInfo = walletInfo; @@ -110,16 +116,40 @@ abstract class WalletCreationVMBase with Store { _appStore.authenticationStore.allowed(); state = ExecutedSuccessfullyState(); } catch (e, s) { + print("error: $e"); + print("stack: $s"); state = FailureState(e.toString()); } } - DerivationInfo? getDefaultDerivation() { - switch (this.type) { + DerivationInfo? getDefaultCreateDerivation() { + final useBip39ForBitcoin = seedSettingsViewModel.bitcoinSeedType.type == DerivationType.bip39; + final useBip39ForNano = seedSettingsViewModel.nanoSeedType.type == DerivationType.bip39; + switch (type) { case WalletType.nano: + if (useBip39ForNano) { + return DerivationInfo(derivationType: DerivationType.bip39); + } return DerivationInfo(derivationType: DerivationType.nano); case WalletType.bitcoin: + if (useBip39ForBitcoin) { + return DerivationInfo( + derivationType: DerivationType.bip39, + derivationPath: "m/84'/0'/0'", + description: "Standard BIP84 native segwit", + scriptType: "p2wpkh", + ); + } + return bitcoin!.getElectrumDerivations()[DerivationType.electrum]!.first; case WalletType.litecoin: + if (useBip39ForBitcoin) { + return DerivationInfo( + derivationType: DerivationType.bip39, + derivationPath: "m/84'/2'/0'", + description: "Default Litecoin", + scriptType: "p2wpkh", + ); + } return bitcoin!.getElectrumDerivations()[DerivationType.electrum]!.first; default: return null; @@ -127,10 +157,18 @@ abstract class WalletCreationVMBase with Store { } DerivationInfo? getCommonRestoreDerivation() { + final useElectrum = seedSettingsViewModel.bitcoinSeedType.type == DerivationType.electrum; + final useNanoStandard = seedSettingsViewModel.nanoSeedType.type == DerivationType.nano; switch (this.type) { case WalletType.nano: - return DerivationInfo(derivationType: DerivationType.nano); + if (useNanoStandard) { + return DerivationInfo(derivationType: DerivationType.nano); + } + return DerivationInfo(derivationType: DerivationType.bip39); case WalletType.bitcoin: + if (useElectrum) { + return bitcoin!.getElectrumDerivations()[DerivationType.electrum]!.first; + } return DerivationInfo( derivationType: DerivationType.bip39, derivationPath: "m/84'/0'/0'/0", @@ -138,6 +176,9 @@ abstract class WalletCreationVMBase with Store { scriptType: "p2wpkh", ); case WalletType.litecoin: + if (useElectrum) { + return bitcoin!.getElectrumDerivations()[DerivationType.electrum]!.first; + } return DerivationInfo( derivationType: DerivationType.bip39, derivationPath: "m/84'/2'/0'/0", @@ -149,12 +190,43 @@ abstract class WalletCreationVMBase with Store { } } + Future> getDerivationInfoFromQRCredentials( + RestoredWallet restoreWallet) async { + var list = []; + final walletType = restoreWallet.type; + var appStore = getIt.get(); + var node = appStore.settingsStore.getCurrentNode(walletType); + + switch (walletType) { + case WalletType.bitcoin: + case WalletType.litecoin: + final derivationList = await bitcoin!.getDerivationsFromMnemonic( + mnemonic: restoreWallet.mnemonicSeed!, + node: node, + passphrase: restoreWallet.passphrase, + ); + + if (derivationList.firstOrNull?.transactionsCount == 0 && derivationList.length > 1) + return []; + return derivationList; + + case WalletType.nano: + return nanoUtil!.getDerivationsFromMnemonic( + mnemonic: restoreWallet.mnemonicSeed!, + node: node, + ); + default: + break; + } + return list; + } + WalletCredentials getCredentials(dynamic options) => throw UnimplementedError(); Future process(WalletCredentials credentials) => throw UnimplementedError(); - WalletCredentials getCredentialsFromRestoredWallet( - dynamic options, RestoredWallet restoreWallet) => + Future getWalletCredentialsFromQRCredentials( + RestoredWallet restoreWallet) async => throw UnimplementedError(); Future processFromRestoredWallet( diff --git a/lib/view_model/wallet_groups_display_view_model.dart b/lib/view_model/wallet_groups_display_view_model.dart new file mode 100644 index 000000000..08515febf --- /dev/null +++ b/lib/view_model/wallet_groups_display_view_model.dart @@ -0,0 +1,165 @@ +import 'package:cake_wallet/core/wallet_loading_service.dart'; +import 'package:cake_wallet/entities/wallet_group.dart'; +import 'package:cake_wallet/entities/wallet_manager.dart'; +import 'package:cake_wallet/reactions/bip39_wallet_utils.dart'; +import 'package:cake_wallet/store/app_store.dart'; +import 'package:cake_wallet/view_model/wallet_list/wallet_list_item.dart'; +import 'package:cake_wallet/view_model/wallet_list/wallet_list_view_model.dart'; +import 'package:cake_wallet/wallet_types.g.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:mobx/mobx.dart'; + +part 'wallet_groups_display_view_model.g.dart'; + +class WalletGroupsDisplayViewModel = WalletGroupsDisplayViewModelBase + with _$WalletGroupsDisplayViewModel; + +abstract class WalletGroupsDisplayViewModelBase with Store { + WalletGroupsDisplayViewModelBase( + this._appStore, + this._walletLoadingService, + this._walletManager, + this.walletListViewModel, { + required this.type, + }) : isFetchingMnemonic = false, + multiWalletGroups = ObservableList(), + singleWalletsList = ObservableList() { + reaction((_) => _appStore.wallet, (_) => updateWalletInfoSourceList()); + updateWalletInfoSourceList(); + } + + final WalletType type; + final AppStore _appStore; + final WalletManager _walletManager; + final WalletLoadingService _walletLoadingService; + final WalletListViewModel walletListViewModel; + + @observable + ObservableList multiWalletGroups; + + @observable + ObservableList singleWalletsList; + + @observable + WalletGroup? selectedWalletGroup; + + @observable + WalletInfo? selectedSingleWallet; + + @observable + String? parentAddress; + + @observable + bool isFetchingMnemonic; + + @computed + bool get hasNoFilteredWallet { + return singleWalletsList.isEmpty && multiWalletGroups.isEmpty; + } + + @action + Future getSelectedWalletMnemonic() async { + WalletListItem walletToUse; + + bool isGroupSelected = selectedWalletGroup != null; + + if (isGroupSelected) { + walletToUse = convertWalletInfoToWalletListItem(selectedWalletGroup!.wallets.first); + } else { + walletToUse = convertWalletInfoToWalletListItem(selectedSingleWallet!); + } + + try { + isFetchingMnemonic = true; + final wallet = await _walletLoadingService.load( + walletToUse.type, + walletToUse.name, + ); + + parentAddress = + isGroupSelected ? selectedWalletGroup!.parentAddress : selectedSingleWallet!.address; + + return wallet.seed; + } catch (e) { + return null; + } finally { + isFetchingMnemonic = false; + } + } + + @action + void selectWalletGroup(WalletGroup walletGroup) { + selectedWalletGroup = walletGroup; + selectedSingleWallet = null; + } + + @action + void selectSingleWallet(WalletInfo singleWallet) { + selectedSingleWallet = singleWallet; + selectedWalletGroup = null; + } + + @action + void updateWalletInfoSourceList() { + List wallets = []; + + multiWalletGroups.clear(); + singleWalletsList.clear(); + + _walletManager.updateWalletGroups(); + + final walletGroups = _walletManager.walletGroups; + + // Iterate through the wallet groups to filter and categorize wallets + for (var group in walletGroups) { + // Handle group wallet filtering + bool shouldExcludeGroup = group.wallets.any((wallet) { + // Check for non-BIP39 wallet types + bool isNonBIP39Wallet = !isBIP39Wallet(wallet.type); + + // Check for nano derivation type + bool isNanoDerivationType = wallet.type == WalletType.nano && + wallet.derivationInfo?.derivationType == DerivationType.nano; + + // Check for electrum derivation type + bool isElectrumDerivationType = + (wallet.type == WalletType.bitcoin || wallet.type == WalletType.litecoin) && + wallet.derivationInfo?.derivationType == DerivationType.electrum; + + // Check that selected wallet type is not present already in group + bool isSameTypeAsSelectedWallet = wallet.type == type; + + // Exclude if any of these conditions are true + return isNonBIP39Wallet || + isNanoDerivationType || + isElectrumDerivationType || + isSameTypeAsSelectedWallet; + }); + + if (shouldExcludeGroup) continue; + + // If the group passes the filters, add it to the wallets list + wallets.add(group); + } + + for (var group in wallets) { + if (group.wallets.length == 1) { + singleWalletsList.add(group.wallets.first); + } else { + multiWalletGroups.add(group); + } + } + } + + WalletListItem convertWalletInfoToWalletListItem(WalletInfo info) { + return WalletListItem( + name: info.name, + type: info.type, + key: info.key, + isCurrent: info.name == _appStore.wallet?.name && info.type == _appStore.wallet?.type, + isEnabled: availableWalletTypes.contains(info.type), + isTestnet: info.network?.toLowerCase().contains('testnet') ?? false, + ); + } +} diff --git a/lib/view_model/wallet_hardware_restore_view_model.dart b/lib/view_model/wallet_hardware_restore_view_model.dart index 804ef7e3c..91e0de685 100644 --- a/lib/view_model/wallet_hardware_restore_view_model.dart +++ b/lib/view_model/wallet_hardware_restore_view_model.dart @@ -5,6 +5,7 @@ import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart'; +import 'package:cake_wallet/view_model/seed_settings_view_model.dart'; import 'package:cake_wallet/view_model/wallet_creation_vm.dart'; import 'package:cw_core/hardware/hardware_account_data.dart'; import 'package:cw_core/wallet_base.dart'; @@ -12,7 +13,6 @@ import 'package:cw_core/wallet_credentials.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:hive/hive.dart'; -import 'package:ledger_flutter/ledger_flutter.dart'; import 'package:mobx/mobx.dart'; part 'wallet_hardware_restore_view_model.g.dart'; @@ -25,10 +25,15 @@ abstract class WalletHardwareRestoreViewModelBase extends WalletCreationVM with int _nextIndex = 0; - WalletHardwareRestoreViewModelBase(this.ledgerViewModel, AppStore appStore, - WalletCreationService walletCreationService, Box walletInfoSource, + WalletHardwareRestoreViewModelBase( + this.ledgerViewModel, + AppStore appStore, + WalletCreationService walletCreationService, + Box walletInfoSource, + SeedSettingsViewModel seedSettingsViewModel, {required WalletType type}) - : super(appStore, walletInfoSource, walletCreationService, type: type, isRecovery: true); + : super(appStore, walletInfoSource, walletCreationService, seedSettingsViewModel, + type: type, isRecovery: true); @observable String name = ""; @@ -52,7 +57,11 @@ abstract class WalletHardwareRestoreViewModelBase extends WalletCreationVM with switch (type) { case WalletType.bitcoin: accounts = await bitcoin! - .getHardwareWalletAccounts(ledgerViewModel, index: _nextIndex, limit: limit); + .getHardwareWalletBitcoinAccounts(ledgerViewModel, index: _nextIndex, limit: limit); + break; + case WalletType.litecoin: + accounts = await bitcoin! + .getHardwareWalletLitecoinAccounts(ledgerViewModel, index: _nextIndex, limit: limit); break; case WalletType.ethereum: accounts = await ethereum! @@ -68,9 +77,10 @@ abstract class WalletHardwareRestoreViewModelBase extends WalletCreationVM with availableAccounts.addAll(accounts); _nextIndex += limit; - } on LedgerException catch (e) { - error = ledgerViewModel.interpretErrorCode(e.errorCode.toRadixString(16)); + // } on LedgerException catch (e) { + // error = ledgerViewModel.interpretErrorCode(e.errorCode.toRadixString(16)); } catch (e) { + print(e); error = S.current.ledger_connection_error; } @@ -83,6 +93,7 @@ abstract class WalletHardwareRestoreViewModelBase extends WalletCreationVM with WalletCredentials credentials; switch (type) { case WalletType.bitcoin: + case WalletType.litecoin: credentials = bitcoin!.createBitcoinHardwareWalletCredentials(name: name, accountData: selectedAccount!); break; diff --git a/lib/view_model/wallet_keys_view_model.dart b/lib/view_model/wallet_keys_view_model.dart index 1d5c27fed..9921ae30a 100644 --- a/lib/view_model/wallet_keys_view_model.dart +++ b/lib/view_model/wallet_keys_view_model.dart @@ -276,7 +276,8 @@ abstract class WalletKeysViewModelBase with Store { if (_appStore.wallet!.seed == null && _appStore.wallet!.privateKey != null) 'private_key': _appStore.wallet!.privateKey!, - if (restoreHeightResult != null) ...{'height': restoreHeightResult} + if (restoreHeightResult != null) ...{'height': restoreHeightResult}, + if (_appStore.wallet!.passphrase != null) 'passphrase': _appStore.wallet!.passphrase! }; } diff --git a/lib/view_model/wallet_list/wallet_edit_view_model.dart b/lib/view_model/wallet_list/wallet_edit_view_model.dart index e5bfcd4e3..343f160db 100644 --- a/lib/view_model/wallet_list/wallet_edit_view_model.dart +++ b/lib/view_model/wallet_list/wallet_edit_view_model.dart @@ -1,4 +1,5 @@ import 'package:cake_wallet/core/wallet_loading_service.dart'; +import 'package:cake_wallet/entities/wallet_manager.dart'; import 'package:cake_wallet/view_model/wallet_list/wallet_list_view_model.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/di.dart'; @@ -18,8 +19,11 @@ class WalletEditRenamePending extends WalletEditViewModelState {} class WalletEditDeletePending extends WalletEditViewModelState {} abstract class WalletEditViewModelBase with Store { - WalletEditViewModelBase(this._walletListViewModel, this._walletLoadingService) - : state = WalletEditViewModelInitialState(), + WalletEditViewModelBase( + this._walletListViewModel, + this._walletLoadingService, + this._walletManager, + ) : state = WalletEditViewModelInitialState(), newName = ''; @observable @@ -30,13 +34,30 @@ abstract class WalletEditViewModelBase with Store { final WalletListViewModel _walletListViewModel; final WalletLoadingService _walletLoadingService; + final WalletManager _walletManager; @action - Future changeName(WalletListItem walletItem, {String? password}) async { + Future changeName( + WalletListItem walletItem, { + String? password, + String? groupParentAddress, + bool isWalletGroup = false, + }) async { state = WalletEditRenamePending(); - await _walletLoadingService.renameWallet( - walletItem.type, walletItem.name, newName, - password: password); + + if (isWalletGroup) { + _walletManager.updateWalletGroups(); + + _walletManager.setGroupName(groupParentAddress!, newName); + } else { + await _walletLoadingService.renameWallet( + walletItem.type, + walletItem.name, + newName, + password: password, + ); + } + _walletListViewModel.updateList(); } diff --git a/lib/view_model/wallet_list/wallet_list_view_model.dart b/lib/view_model/wallet_list/wallet_list_view_model.dart index 2ed6358f4..0ae6b75a5 100644 --- a/lib/view_model/wallet_list/wallet_list_view_model.dart +++ b/lib/view_model/wallet_list/wallet_list_view_model.dart @@ -1,5 +1,7 @@ import 'package:cake_wallet/core/wallet_loading_service.dart'; +import 'package:cake_wallet/entities/wallet_group.dart'; import 'package:cake_wallet/entities/wallet_list_order_types.dart'; +import 'package:cake_wallet/entities/wallet_manager.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/store/app_store.dart'; @@ -17,7 +19,11 @@ abstract class WalletListViewModelBase with Store { this._walletInfoSource, this._appStore, this._walletLoadingService, - ) : wallets = ObservableList() { + this._walletManager, + ) : wallets = ObservableList(), + multiWalletGroups = ObservableList(), + singleWalletsList = ObservableList(), + expansionTileStateTrack = ObservableMap() { setOrderType(_appStore.settingsStore.walletListOrder); reaction((_) => _appStore.wallet, (_) => updateList()); updateList(); @@ -26,6 +32,27 @@ abstract class WalletListViewModelBase with Store { @observable ObservableList wallets; + // @observable + // ObservableList walletGroups; + + @observable + ObservableList multiWalletGroups; + + @observable + ObservableList singleWalletsList; + + @observable + ObservableMap expansionTileStateTrack; + + @action + void updateTileState(int index, bool isExpanded) { + if (expansionTileStateTrack.containsKey(index)) { + expansionTileStateTrack.update(index, (value) => isExpanded); + } else { + expansionTileStateTrack.addEntries({index: isExpanded}.entries); + } + } + @computed bool get shouldRequireTOTP2FAForAccessingWallet => _appStore.settingsStore.shouldRequireTOTP2FAForAccessingWallet; @@ -35,6 +62,7 @@ abstract class WalletListViewModelBase with Store { _appStore.settingsStore.shouldRequireTOTP2FAForCreatingNewWallets; final AppStore _appStore; + final WalletManager _walletManager; final Box _walletInfoSource; final WalletLoadingService _walletLoadingService; @@ -42,6 +70,8 @@ abstract class WalletListViewModelBase with Store { @action Future loadWallet(WalletListItem walletItem) async { + // bool switchingToSameWalletType = walletItem.type == _appStore.wallet?.type; + // await _appStore.wallet?.close(shouldCleanup: !switchingToSameWalletType); final wallet = await _walletLoadingService.load(walletItem.type, walletItem.name); await _appStore.changeCurrentWallet(wallet); } @@ -53,18 +83,23 @@ abstract class WalletListViewModelBase with Store { @action void updateList() { wallets.clear(); + multiWalletGroups.clear(); + singleWalletsList.clear(); + wallets.addAll( - _walletInfoSource.values.map( - (info) => WalletListItem( - name: info.name, - type: info.type, - key: info.key, - isCurrent: info.name == _appStore.wallet?.name && info.type == _appStore.wallet?.type, - isEnabled: availableWalletTypes.contains(info.type), - isTestnet: info.network?.toLowerCase().contains('testnet') ?? false, - ), - ), + _walletInfoSource.values.map((info) => convertWalletInfoToWalletListItem(info)), ); + + //========== Split into shared seed groups and single wallets list + _walletManager.updateWalletGroups(); + + for (var group in _walletManager.walletGroups) { + if (group.wallets.length == 1) { + singleWalletsList.add(convertWalletInfoToWalletListItem(group.wallets.first)); + } else { + multiWalletGroups.add(group); + } + } } Future reorderAccordingToWalletList() async { @@ -80,8 +115,8 @@ abstract class WalletListViewModelBase with Store { // delete all wallets from walletInfoSource: await _walletInfoSource.clear(); - // add wallets from wallets list in order of wallets list, by name: - for (WalletListItem wallet in wallets) { + // Reorder single wallets using the singleWalletsList + for (WalletListItem wallet in singleWalletsList) { for (int i = 0; i < walletInfoSourceCopy.length; i++) { if (walletInfoSourceCopy[i].name == wallet.name) { await _walletInfoSource.add(walletInfoSourceCopy[i]); @@ -91,6 +126,20 @@ abstract class WalletListViewModelBase with Store { } } + // Reorder wallets within multi-wallet groups + for (WalletGroup group in multiWalletGroups) { + for (WalletInfo walletInfo in group.wallets) { + for (int i = 0; i < walletInfoSourceCopy.length; i++) { + if (walletInfoSourceCopy[i].name == walletInfo.name) { + await _walletInfoSource.add(walletInfoSourceCopy[i]); + walletInfoSourceCopy.removeAt(i); + break; + } + } + } + } + + // Rebuild the list of wallets and groups updateList(); } @@ -158,4 +207,15 @@ abstract class WalletListViewModelBase with Store { break; } } + + WalletListItem convertWalletInfoToWalletListItem(WalletInfo info) { + return WalletListItem( + name: info.name, + type: info.type, + key: info.key, + isCurrent: info.name == _appStore.wallet?.name && info.type == _appStore.wallet?.type, + isEnabled: availableWalletTypes.contains(info.type), + isTestnet: info.network?.toLowerCase().contains('testnet') ?? false, + ); + } } diff --git a/lib/view_model/wallet_new_vm.dart b/lib/view_model/wallet_new_vm.dart index a618695b1..be30811d9 100644 --- a/lib/view_model/wallet_new_vm.dart +++ b/lib/view_model/wallet_new_vm.dart @@ -1,95 +1,161 @@ +import 'package:cake_wallet/core/new_wallet_arguments.dart'; import 'package:cake_wallet/ethereum/ethereum.dart'; +import 'package:cake_wallet/bitcoin/bitcoin.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:hive/hive.dart'; -import 'package:mobx/mobx.dart'; +import 'package:cake_wallet/core/wallet_creation_service.dart'; +import 'package:cake_wallet/entities/seed_type.dart'; +import 'package:cake_wallet/haven/haven.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/nano/nano.dart'; +import 'package:cake_wallet/solana/solana.dart'; import 'package:cake_wallet/store/app_store.dart'; +import 'package:cake_wallet/tron/tron.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:cw_core/wallet_base.dart'; -import 'package:cake_wallet/core/wallet_creation_service.dart'; import 'package:cw_core/wallet_credentials.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; -import 'package:cake_wallet/view_model/wallet_creation_vm.dart'; -import 'package:cake_wallet/bitcoin/bitcoin.dart'; -import 'package:cake_wallet/haven/haven.dart'; -import 'advanced_privacy_settings_view_model.dart'; +import 'package:hive/hive.dart'; +import 'package:mobx/mobx.dart'; import '../polygon/polygon.dart'; +import 'advanced_privacy_settings_view_model.dart'; part 'wallet_new_vm.g.dart'; class WalletNewVM = WalletNewVMBase with _$WalletNewVM; abstract class WalletNewVMBase extends WalletCreationVM with Store { - WalletNewVMBase(AppStore appStore, WalletCreationService walletCreationService, - Box walletInfoSource, this.advancedPrivacySettingsViewModel, - {required WalletType type}) - : selectedMnemonicLanguage = '', - super(appStore, walletInfoSource, walletCreationService, type: type, isRecovery: false); + WalletNewVMBase( + AppStore appStore, + WalletCreationService walletCreationService, + Box walletInfoSource, + this.advancedPrivacySettingsViewModel, + SeedSettingsViewModel seedSettingsViewModel, { + required this.newWalletArguments, + }) : selectedMnemonicLanguage = '', + super(appStore, walletInfoSource, walletCreationService, seedSettingsViewModel, + type: newWalletArguments!.type, isRecovery: false); + final NewWalletArguments? newWalletArguments; final AdvancedPrivacySettingsViewModel advancedPrivacySettingsViewModel; @observable String selectedMnemonicLanguage; bool get hasLanguageSelector => - type == WalletType.monero || type == WalletType.haven || type == WalletType.wownero; + [WalletType.monero, WalletType.haven, WalletType.wownero].contains(type); int get seedPhraseWordsLength { switch (type) { case WalletType.monero: case WalletType.wownero: - if (advancedPrivacySettingsViewModel.isPolySeed) { - return 16; - } - return 25; + return advancedPrivacySettingsViewModel.isPolySeed ? 16 : 25; case WalletType.tron: case WalletType.solana: case WalletType.polygon: case WalletType.ethereum: case WalletType.bitcoinCash: return advancedPrivacySettingsViewModel.seedPhraseLength.value; + case WalletType.bitcoin: + case WalletType.litecoin: + return seedSettingsViewModel.bitcoinSeedType == BitcoinSeedType.bip39 + ? advancedPrivacySettingsViewModel.seedPhraseLength.value + : 24; + case WalletType.nano: + return seedSettingsViewModel.nanoSeedType == NanoSeedType.bip39 + ? advancedPrivacySettingsViewModel.seedPhraseLength.value + : 24; default: return 24; } } - bool get hasSeedType => type == WalletType.monero || type == WalletType.wownero; + bool get hasSeedType => [WalletType.monero, WalletType.wownero].contains(type); @override WalletCredentials getCredentials(dynamic _options) { final options = _options as List?; + final passphrase = seedSettingsViewModel.passphrase; + seedSettingsViewModel.setPassphrase(null); + switch (type) { case WalletType.monero: return monero!.createMoneroNewWalletCredentials( - name: name, language: options!.first as String, password: walletPassword, isPolyseed: options.last as bool); + name: name, + language: options!.first as String, + password: walletPassword, + isPolyseed: options.last as bool); case WalletType.bitcoin: - return bitcoin!.createBitcoinNewWalletCredentials(name: name, password: walletPassword); case WalletType.litecoin: - return bitcoin!.createBitcoinNewWalletCredentials(name: name, password: walletPassword); + return bitcoin!.createBitcoinNewWalletCredentials( + name: name, + password: walletPassword, + passphrase: passphrase, + mnemonic: newWalletArguments!.mnemonic, + parentAddress: newWalletArguments!.parentAddress, + ); case WalletType.haven: return haven!.createHavenNewWalletCredentials( name: name, language: options!.first as String, password: walletPassword); case WalletType.ethereum: - return ethereum!.createEthereumNewWalletCredentials(name: name, password: walletPassword); + return ethereum!.createEthereumNewWalletCredentials( + name: name, + password: walletPassword, + mnemonic: newWalletArguments!.mnemonic, + parentAddress: newWalletArguments!.parentAddress, + passphrase: passphrase, + ); case WalletType.bitcoinCash: - return bitcoinCash!.createBitcoinCashNewWalletCredentials(name: name, password: walletPassword); + return bitcoinCash!.createBitcoinCashNewWalletCredentials( + name: name, + password: walletPassword, + passphrase: passphrase, + mnemonic: newWalletArguments!.mnemonic, + parentAddress: newWalletArguments!.parentAddress, + ); case WalletType.nano: case WalletType.banano: - return nano!.createNanoNewWalletCredentials(name: name); + return nano!.createNanoNewWalletCredentials( + name: name, + password: walletPassword, + mnemonic: newWalletArguments!.mnemonic, + parentAddress: newWalletArguments!.parentAddress, + passphrase: passphrase, + ); case WalletType.polygon: - return polygon!.createPolygonNewWalletCredentials(name: name, password: walletPassword); + return polygon!.createPolygonNewWalletCredentials( + name: name, + password: walletPassword, + mnemonic: newWalletArguments!.mnemonic, + parentAddress: newWalletArguments!.parentAddress, + passphrase: passphrase, + ); case WalletType.solana: - return solana!.createSolanaNewWalletCredentials(name: name, password: walletPassword); + return solana!.createSolanaNewWalletCredentials( + name: name, + password: walletPassword, + mnemonic: newWalletArguments!.mnemonic, + parentAddress: newWalletArguments!.parentAddress, + passphrase: passphrase, + ); case WalletType.tron: - return tron!.createTronNewWalletCredentials(name: name); + return tron!.createTronNewWalletCredentials( + name: name, + password: walletPassword, + mnemonic: newWalletArguments!.mnemonic, + parentAddress: newWalletArguments!.parentAddress, + passphrase: passphrase, + ); case WalletType.wownero: return wownero!.createWowneroNewWalletCredentials( - name: name, language: options!.first as String, isPolyseed: options.last as bool); + name: name, + password: walletPassword, + language: options!.first as String, + isPolyseed: options.last as bool, + ); 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 a38baabd8..d37b69f74 100644 --- a/lib/view_model/wallet_restore_view_model.dart +++ b/lib/view_model/wallet_restore_view_model.dart @@ -1,25 +1,26 @@ import 'package:cake_wallet/bitcoin/bitcoin.dart'; -import 'package:cake_wallet/di.dart'; -import 'package:cake_wallet/nano/nano.dart'; -import 'package:cake_wallet/ethereum/ethereum.dart'; -import 'package:cake_wallet/wownero/wownero.dart'; import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart'; -import 'package:cake_wallet/polygon/polygon.dart'; -import 'package:cake_wallet/solana/solana.dart'; -import 'package:cake_wallet/tron/tron.dart'; -import 'package:hive/hive.dart'; -import 'package:mobx/mobx.dart'; -import 'package:cake_wallet/store/app_store.dart'; -import 'package:cw_core/wallet_base.dart'; import 'package:cake_wallet/core/generate_wallet_password.dart'; import 'package:cake_wallet/core/wallet_creation_service.dart'; -import 'package:cw_core/wallet_credentials.dart'; -import 'package:cw_core/wallet_type.dart'; -import 'package:cw_core/wallet_info.dart'; -import 'package:cake_wallet/view_model/wallet_creation_vm.dart'; -import 'package:cake_wallet/monero/monero.dart'; +import 'package:cake_wallet/di.dart'; +import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/haven/haven.dart'; +import 'package:cake_wallet/monero/monero.dart'; +import 'package:cake_wallet/nano/nano.dart'; +import 'package:cake_wallet/polygon/polygon.dart'; +import 'package:cake_wallet/solana/solana.dart'; +import 'package:cake_wallet/store/app_store.dart'; +import 'package:cake_wallet/tron/tron.dart'; 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:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_credentials.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:hive/hive.dart'; +import 'package:mobx/mobx.dart'; part 'wallet_restore_view_model.g.dart'; @@ -27,7 +28,7 @@ class WalletRestoreViewModel = WalletRestoreViewModelBase with _$WalletRestoreVi abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { WalletRestoreViewModelBase(AppStore appStore, WalletCreationService walletCreationService, - Box walletInfoSource, + Box walletInfoSource, SeedSettingsViewModel seedSettingsViewModel, {required WalletType type}) : hasSeedLanguageSelector = type == WalletType.monero || type == WalletType.haven || type == WalletType.wownero, @@ -41,7 +42,8 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { type == WalletType.tron, isButtonEnabled = false, mode = WalletRestoreMode.seed, - super(appStore, walletInfoSource, walletCreationService, type: type, isRecovery: true) { + super(appStore, walletInfoSource, walletCreationService, seedSettingsViewModel, + type: type, isRecovery: true) { switch (type) { case WalletType.monero: availableModes = WalletRestoreMode.values; @@ -76,8 +78,6 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { final bool hasBlockchainHeightLanguageSelector; final bool hasRestoreFromPrivateKey; - bool get hasPassphrase => [WalletType.bitcoin, WalletType.litecoin].contains(type); - @observable WalletRestoreMode mode; @@ -113,10 +113,18 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { name: name, height: height, mnemonic: seed, password: password); case WalletType.ethereum: return ethereum!.createEthereumRestoreWalletFromSeedCredentials( - name: name, mnemonic: seed, password: password); + name: name, + mnemonic: seed, + password: password, + passphrase: passphrase, + ); case WalletType.bitcoinCash: return bitcoinCash!.createBitcoinCashRestoreWalletFromSeedCredentials( - name: name, mnemonic: seed, password: password); + name: name, + mnemonic: seed, + password: password, + passphrase: passphrase, + ); case WalletType.nano: case WalletType.banano: return nano!.createNanoRestoreWalletFromSeedCredentials( @@ -124,24 +132,28 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { mnemonic: seed, password: password, derivationType: derivationInfo!.derivationType!, + passphrase: passphrase, ); case WalletType.polygon: return polygon!.createPolygonRestoreWalletFromSeedCredentials( name: name, mnemonic: seed, password: password, + passphrase: passphrase, ); case WalletType.solana: return solana!.createSolanaRestoreWalletFromSeedCredentials( name: name, mnemonic: seed, password: password, + passphrase: passphrase, ); case WalletType.tron: return tron!.createTronRestoreWalletFromSeedCredentials( name: name, mnemonic: seed, password: password, + passphrase: passphrase, ); case WalletType.wownero: return wownero!.createWowneroRestoreWalletFromSeedCredentials( @@ -192,10 +204,11 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { case WalletType.nano: return nano!.createNanoRestoreWalletFromKeysCredentials( - name: name, - password: password, - seedKey: options['private_key'] as String, - derivationType: options["derivationType"] as DerivationType); + name: name, + password: password, + seedKey: options['private_key'] as String, + derivationType: derivationInfo!.derivationType!, + ); case WalletType.polygon: return polygon!.createPolygonRestoreWalletFromPrivateKey( name: name, diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 0b4ee9415..9895e6305 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,6 +6,7 @@ import FlutterMacOS import Foundation import connectivity_plus +import cw_mweb import device_info_plus import devicelocale import flutter_inappwebview_macos @@ -16,11 +17,13 @@ import package_info_plus import path_provider_foundation import share_plus import shared_preferences_foundation +import universal_ble import url_launcher_macos import wakelock_plus func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { ConnectivityPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlugin")) + CwMwebPlugin.register(with: registry.registrar(forPlugin: "CwMwebPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) DevicelocalePlugin.register(with: registry.registrar(forPlugin: "DevicelocalePlugin")) InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin")) @@ -31,6 +34,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) + UniversalBlePlugin.register(with: registry.registrar(forPlugin: "UniversalBlePlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin")) } diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 8951a2dd1..d6199d028 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -2,6 +2,8 @@ PODS: - connectivity_plus (0.0.1): - FlutterMacOS - ReachabilitySwift + - cw_mweb (0.0.1): + - FlutterMacOS - device_info_plus (0.0.1): - FlutterMacOS - devicelocale (0.0.1): @@ -37,6 +39,7 @@ PODS: DEPENDENCIES: - connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos`) + - cw_mweb (from `Flutter/ephemeral/.symlinks/plugins/cw_mweb/macos`) - device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`) - devicelocale (from `Flutter/ephemeral/.symlinks/plugins/devicelocale/macos`) - flutter_inappwebview_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_inappwebview_macos/macos`) @@ -60,6 +63,8 @@ SPEC REPOS: EXTERNAL SOURCES: connectivity_plus: :path: Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos + cw_mweb: + :path: Flutter/ephemeral/.symlinks/plugins/cw_mweb/macos device_info_plus: :path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos devicelocale: @@ -91,6 +96,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: connectivity_plus: 18d3c32514c886e046de60e9c13895109866c747 + cw_mweb: 7440b12ead811dda972a9918442ea2a458e8742c device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f devicelocale: 9f0f36ac651cabae2c33f32dcff4f32b61c38225 flutter_inappwebview_macos: 9600c9df9fdb346aaa8933812009f8d94304203d @@ -105,7 +111,7 @@ SPEC CHECKSUMS: share_plus: 36537c04ce0c3e3f5bd297ce4318b6d5ee5fd6cf shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 sp_scanner: 269d96e0ec3173e69156be7239b95182be3b8303 - url_launcher_macos: 5f437abeda8c85500ceb03f5c1938a8c5a705399 + url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404 wakelock_plus: 4783562c9a43d209c458cb9b30692134af456269 PODFILE CHECKSUM: 65ec1541137fb5b35d00490dec1bb48d4d9586bb diff --git a/model_generator.sh b/model_generator.sh index 8a1173f7d..293923d1e 100755 --- a/model_generator.sh +++ b/model_generator.sh @@ -11,4 +11,5 @@ cd cw_tron; flutter pub get; flutter packages pub run build_runner build --delet cd cw_wownero; flutter pub get; flutter packages pub run build_runner build --delete-conflicting-outputs; cd .. cd cw_polygon; flutter pub get; cd .. cd cw_ethereum; flutter pub get; cd .. +cd cw_mweb && flutter pub get && cd .. flutter packages pub run build_runner build --delete-conflicting-outputs diff --git a/pubspec_base.yaml b/pubspec_base.yaml index 0d99c1c8c..d5fce76e9 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -12,7 +12,7 @@ dependencies: version: 4.0.2 shared_preferences: ^2.0.15 # provider: ^6.0.3 - rxdart: ^0.27.4 + rxdart: ^0.28.0 yaml: ^3.1.1 #barcode_scan: any barcode_scan2: ^4.2.1 @@ -97,21 +97,20 @@ dependencies: polyseed: ^0.0.6 nostr_tools: ^1.0.9 solana: ^0.30.1 - bitcoin_base: - git: - url: https://github.com/cake-tech/bitcoin_base - ref: cake-update-v5 - ledger_flutter: ^1.0.1 + ledger_flutter_plus: ^1.4.1 hashlib: ^1.19.2 dev_dependencies: flutter_test: sdk: flutter + integration_test: + sdk: flutter + mocktail: ^1.0.4 build_runner: ^2.3.3 logging: ^1.2.0 mobx_codegen: ^2.1.1 build_resolvers: ^2.0.9 - hive_generator: ^1.1.3 + hive_generator: ^2.0.1 # flutter_launcher_icons: ^0.11.0 # check flutter_launcher_icons for usage pedantic: ^1.8.0 @@ -126,19 +125,17 @@ dependency_overrides: bech32: git: url: https://github.com/cake-tech/bech32.git - ledger_flutter: - git: - url: https://github.com/cake-tech/ledger-flutter.git - ref: cake-v3 web3dart: git: url: https://github.com/cake-tech/web3dart.git ref: cake flutter_secure_storage_platform_interface: 1.0.2 + protobuf: ^3.1.0 bitcoin_base: git: url: https://github.com/cake-tech/bitcoin_base - ref: cake-update-v5 + ref: cake-update-v8 + ffi: 2.1.0 flutter_icons: image_path: "assets/images/app_logo.png" @@ -154,6 +151,7 @@ flutter: assets: - assets/images/ - assets/images/flags/ + - assets/images/hardware_wallet/ - assets/node_list.yml - assets/haven_node_list.yml - assets/bitcoin_electrum_server_list.yml diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index f84f6102b..81fe3cc2c 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -36,6 +36,7 @@ "agree": "موافق", "agree_and_continue": "الموافقة ومتابعة", "agree_to": "من خلال إنشاء حساب فإنك توافق على", + "alert_notice": "يلاحظ", "all": "الكل", "all_trades": "جميع عمليات التداول", "all_transactions": "كل التحركات المالية", @@ -113,7 +114,7 @@ "change_currency": "تغيير العملة", "change_current_node": "هل أنت متأكد من تغيير العقدة الحالية إلى ${node}؟", "change_current_node_title": "تغيير العقدة الحالية", - "change_exchange_provider": "تغيير مزود الصرف", + "change_exchange_provider": "تغيير مزود المبادلة", "change_language": "تغيير اللغة", "change_language_to": "هل تريد تغيير اللغة إلى ${language}؟", "change_password": "تغيير كلمة المرور", @@ -130,6 +131,7 @@ "choose_one": "اختر واحدة", "choose_relay": "ﻡﺍﺪﺨﺘﺳﻼﻟ ﻊﺑﺎﺘﺘﻟﺍ ﺭﺎﻴﺘﺧﺍ ءﺎﺟﺮﻟﺍ", "choose_wallet_currency": "الرجاء اختيار عملة المحفظة:", + "choose_wallet_group": "اختر مجموعة المحفظة", "clear": "مسح", "clearnet_link": "رابط Clearnet", "close": "يغلق", @@ -142,6 +144,7 @@ "confirm_delete_wallet": "سيؤدي هذا الإجراء إلى حذف هذه المحفظة. هل ترغب في الاستمرار؟", "confirm_fee_deduction": "تأكيد خصم الرسوم", "confirm_fee_deduction_content": "هل توافق على خصم الرسوم من الإخراج؟", + "confirm_passphrase": "تأكيد عبارة المرور", "confirm_sending": "تأكيد الإرسال", "confirm_silent_payments_switch_node": "العقدة الحالية لا تدعم المدفوعات الصامتة \\ ncake wallet سوف تتحول إلى عقدة متوافقة ، فقط للمسح الضوئي", "confirmations": "التأكيدات", @@ -160,6 +163,7 @@ "contact_name": "اسم جهة الاتصال", "contact_support": "اتصل بالدعم", "continue_text": "التالي", + "contract_warning": "تم وضع علامة على عنوان العقد هذا على أنه احتيالي محتمل. يرجى المعالجة بحذر.", "contractName": "ﺪﻘﻌﻟﺍ ﻢﺳﺍ", "contractSymbol": "ﺪﻘﻌﻟﺍ ﺰﻣﺭ", "copied_key_to_clipboard": "تم نسخ ${key} إلى الحافظة", @@ -175,6 +179,7 @@ "create_invoice": "إنشاء فاتورة", "create_new": "إنشاء محفظة جديدة", "create_new_account": "انشاء حساب جديد", + "create_new_seed": "إنشاء بذرة جديدة", "creating_new_wallet": "يتم إنشاء محفظة جديدة", "creating_new_wallet_error": "خطأ: ${description}", "creation_date": "تاريخ الإنشاء", @@ -195,6 +200,7 @@ "delete_wallet_confirm_message": "هل أنت متأكد أنك تريد حذف محفظة ${wallet_name}؟", "deleteConnectionConfirmationPrompt": "ـﺑ ﻝﺎﺼﺗﻻﺍ ﻑﺬﺣ ﺪﻳﺮﺗ ﻚﻧﺃ ﺪﻛﺄﺘﻣ ﺖﻧﺃ ﻞﻫ", "denominations": "الطوائف", + "derivationpath": "مسار الاشتقاق", "descending": "النزول", "description": "ﻒﺻﻭ", "destination_tag": "علامة الوجهة:", @@ -207,6 +213,7 @@ "disable_buy": "تعطيل إجراء الشراء", "disable_cake_2fa": "تعطيل 2 عامل المصادقة", "disable_exchange": "تعطيل التبادل", + "disable_fee_api_warning": "من خلال إيقاف تشغيل هذا ، قد تكون معدلات الرسوم غير دقيقة في بعض الحالات ، لذلك قد ينتهي بك الأمر إلى دفع مبالغ زائدة أو دفع رسوم المعاملات الخاصة بك", "disable_fiat": "تعطيل fiat", "disable_sell": "قم بتعطيل إجراء البيع", "disableBatteryOptimization": "تعطيل تحسين البطارية", @@ -228,8 +235,10 @@ "edit_token": "تحرير الرمز المميز", "electrum_address_disclaimer": "نقوم بإنشاء عناوين جديدة في كل مرة تستخدم فيها عنوانًا ، لكن العناوين السابقة تستمر في العمل", "email_address": "عنوان البريد الالكترونى", + "enable": "يُمكَِن", + "enable_mempool_api": "MEMPOOL API للحصول على رسوم وتواريخ دقيقة", "enable_replace_by_fee": "تمكين الاستبدال", - "enable_silent_payments_scanning": "تمكين المسح الضوئي للمدفوعات الصامتة", + "enable_silent_payments_scanning": "ابدأ في مسح المدفوعات الصامتة ، حتى يتم الوصول إلى الطرف", "enabled": "ممكنة", "enter_amount": "أدخل المبلغ", "enter_backup_password": "أدخل كلمة المرور الاحتياطية هنا", @@ -272,8 +281,8 @@ "etherscan_history": "Etherscan تاريخ", "event": "ﺙﺪﺣ", "events": "ﺙﺍﺪﺣﻷﺍ", - "exchange": "تبادل", - "exchange_incorrect_current_wallet_for_xmr": "إذا كنت ترغب في استبدال XMR من رصيد Cake Wallet Monero ، فيرجى التبديل إلى محفظة Monero أولاً.", + "exchange": "تبديل", + "exchange_incorrect_current_wallet_for_xmr": "إذا كنت ترغب في تبديل XMR من رصيد محفظة الكعكة ، فيرجى التبديل إلى محفظة Monero أولاً.", "exchange_new_template": "قالب جديد", "exchange_provider_unsupported": "${providerName} لم يعد مدعومًا!", "exchange_result_confirm": "بالضغط على تأكيد ، سترسل ${fetchingLabel} ${from} من محفظتك المسماة ${walletName} إلى العنوان الموضح أدناه. أو يمكنك الإرسال من محفظتك الخارجية إلى العنوان أدناه / QR.\n\nيرجى الضغط على تأكيد للمتابعة أو الرجوع لتغيير المبالغ.", @@ -291,6 +300,8 @@ "failed_authentication": "${state_error} فشل المصادقة.", "faq": "الأسئلة الشائعة", "features": "سمات", + "fee_less_than_min": "الرسوم المحددة أقل من الحد الأدنى ، يرجى زيادة الرسوم لتكون قادرة على إرسال المعاملة", + "fee_rate": "معدل الرسوم", "fetching": "جار الجلب", "fiat_api": "Fiat API", "fiat_balance": "الرصيد فيات", @@ -298,7 +309,7 @@ "fill_code": "يرجى ملء رمز التحقق المرسل إلى بريدك الإلكتروني", "filter_by": "تصفية حسب", "first_wallet_text": "محفظة رائعة ل Monero, Bitcoin, Ethereum, Litecoin و Haven", - "fixed_pair_not_supported": "هذا الزوج الثابت غير مدعوم في التبادلات المحددة", + "fixed_pair_not_supported": "لا يتم دعم هذا الزوج الثابت مع خدمات المبادلة المحددة", "fixed_rate": "السعر الثابت", "fixed_rate_alert": "ستتمكن من إدخال مبلغ الاستلام عند تشغيل وضع السعر الثابت. هل تريد التبديل إلى وضع السعر الثابت؟", "forgot_password": "هل نسيت كلمة السر", @@ -354,6 +365,20 @@ "ledger_error_wrong_app": "يرجى التأكد", "ledger_please_enable_bluetooth": "يرجى تمكين البلوتوث للكشف عن دفتر الأستاذ الخاص بك", "light_theme": "فاتح", + "litecoin_enable_mweb_sync": "تمكين MWEB المسح الضوئي", + "litecoin_mweb": "mweb", + "litecoin_mweb_always_scan": "اضبط MWEB دائمًا على المسح الضوئي", + "litecoin_mweb_description": "MWEB هو بروتوكول جديد يجلب معاملات أسرع وأرخص وأكثر خصوصية إلى Litecoin", + "litecoin_mweb_dismiss": "رفض", + "litecoin_mweb_display_card": "عرض بطاقة mweb", + "litecoin_mweb_enable_later": "يمكنك اختيار تمكين MWEB مرة أخرى ضمن إعدادات العرض.", + "litecoin_mweb_pegin": "ربط في", + "litecoin_mweb_pegout": "ربط", + "litecoin_mweb_scanning": "MWEB المسح الضوئي", + "litecoin_mweb_settings": "إعدادات MWEB", + "litecoin_mweb_warning": "سيقوم استخدام MWEB في البداية بتنزيل ~ 600 ميجابايت من البيانات ، وقد يستغرق ما يصل إلى 30 دقيقة حسب سرعة الشبكة. سيتم تنزيل هذه البيانات الأولية مرة واحدة فقط وستكون متاحة لجميع محافظ Litecoin", + "litecoin_what_is_mweb": "ما هو MWEB؟", + "live_fee_rates": "أسعار الرسوم المباشرة عبر API", "load_more": "تحميل المزيد", "loading_your_wallet": "يتم تحميل محفظتك", "login": "تسجيل الدخول", @@ -383,6 +408,8 @@ "monero_light_theme": " ضوء مونيرو", "moonpay_alert_text": "يجب أن تكون قيمة المبلغ أكبر من أو تساوي ${minAmount} ${fiatCurrency}", "more_options": "المزيد من الخيارات", + "mweb_confirmed": "أكد MWEB", + "mweb_unconfirmed": "غير مؤكد MWEB", "name": "ﻢﺳﺍ", "nano_current_rep": "الممثل الحالي", "nano_gpt_thanks_message": "شكرا لاستخدام nanogpt! تذكر أن تعود إلى المتصفح بعد اكتمال معاملتك!", @@ -412,7 +439,7 @@ "node_test": "تجربة", "nodes": "العقد", "nodes_list_reset_to_default_message": "هل أنت متأكد أنك تريد إعادة تعيين الإعدادات إلى الافتراضي؟", - "none_of_selected_providers_can_exchange": "لا يمكن لأي من مقدمي الخدمة المختارين إجراء هذا التبادل", + "none_of_selected_providers_can_exchange": "لا يمكن لأي من مقدمي الخدمات المختارين إجراء هذا المبادلة", "noNFTYet": "ﻥﻵﺍ ﻰﺘﺣ NFTs ﺪﺟﻮﻳ ﻻ", "normal": "طبيعي", "note_optional": "ملاحظة (اختياري)", @@ -442,6 +469,7 @@ "overwrite_amount": "تغير المبلغ", "pairingInvalidEvent": "ﺢﻟﺎﺻ ﺮﻴﻏ ﺙﺪﺣ ﻥﺍﺮﻗﺇ", "passphrase": "عبارة الممر (اختياري)", + "passphrases_doesnt_match": "لا تتطابق عبارات المرور ، يرجى المحاولة مرة أخرى", "password": "كلمة المرور", "paste": "لصق", "pause_wallet_creation": ".ﺎﻴًﻟﺎﺣ ﺎﺘًﻗﺆﻣ ﺔﻔﻗﻮﺘﻣ Haven Wallet ءﺎﺸﻧﺇ ﻰﻠﻋ ﺓﺭﺪﻘﻟﺍ", @@ -593,8 +621,11 @@ "seed_share": "شارك السييد", "seed_title": "سييد", "seedtype": "البذور", + "seedtype_alert_content": "مشاركة البذور مع محافظ أخرى ممكن فقط مع BIP39 Seedtype.", + "seedtype_alert_title": "تنبيه البذور", "seedtype_legacy": "إرث (25 كلمة)", "seedtype_polyseed": "بوليسيد (16 كلمة)", + "seedtype_wownero": "Wownero (14 كلمة)", "select_backup_file": "حدد ملف النسخ الاحتياطي", "select_buy_provider_notice": "حدد مزود شراء أعلاه. يمكنك تخطي هذه الشاشة عن طريق تعيين مزود شراء الافتراضي في إعدادات التطبيق.", "select_destination": ".ﻲﻃﺎﻴﺘﺣﻻﺍ ﺦﺴﻨﻟﺍ ﻒﻠﻣ ﺔﻬﺟﻭ ﺪﻳﺪﺤﺗ ءﺎﺟﺮﻟﺍ", @@ -605,6 +636,7 @@ "send": "إرسال", "send_address": "عنوان ${cryptoCurrency}", "send_amount": "مقدار:", + "send_change_to_you": "تغيير لك:", "send_creating_transaction": " يتم إنشاء المعاملة", "send_error_currency": "العملة يجب أن تحتوي على أرقام فقط", "send_error_minimum_value": "الحد الأدنى لقيمة المبلغ هو 0.01", @@ -658,19 +690,21 @@ "setup_your_debit_card": "قم بإعداد بطاقة ائتمان الخاصة بك", "share": "يشارك", "share_address": "شارك العنوان", + "shared_seed_wallet_groups": "مجموعات محفظة البذور المشتركة", "show_details": "اظهر التفاصيل", "show_keys": "اظهار السييد / المفاتيح", "show_market_place": "إظهار السوق", "show_seed": "عرض السييد", "sign_message": "تسجيل رسالة", "sign_up": "اشتراك", - "sign_verify_message": "توقيع أو التحقق من الرسالة", + "sign_verify_message": "تسجيل / تحقق", "sign_verify_message_sub": "قم بتوقيع أو التحقق من رسالة باستخدام المفتاح الخاص بك", "sign_verify_title": "تسجيل / تحقق", "signature": "إمضاء", "signature_invalid_error": "التوقيع غير صالح للرسالة المقدمة", "signTransaction": " ﺔﻠﻣﺎﻌﻤﻟﺍ ﻊﻴﻗﻮﺗ", "signup_for_card_accept_terms": "قم بالتسجيل للحصول على البطاقة وقبول الشروط.", + "silent_payment": "الدفع الصامت", "silent_payments": "مدفوعات صامتة", "silent_payments_always_scan": "حدد المدفوعات الصامتة دائمًا المسح الضوئي", "silent_payments_disclaimer": "العناوين الجديدة ليست هويات جديدة. إنها إعادة استخدام هوية موجودة مع ملصق مختلف.", @@ -681,6 +715,7 @@ "silent_payments_scanned_tip": "ممسوح ليفحص! (${tip})", "silent_payments_scanning": "المدفوعات الصامتة المسح الضوئي", "silent_payments_settings": "إعدادات المدفوعات الصامتة", + "single_seed_wallets_group": "محافظ بذرة واحدة", "slidable": "قابل للانزلاق", "sort_by": "ترتيب حسب", "spend_key_private": "مفتاح الإنفاق (خاص)", @@ -703,12 +738,13 @@ "switchToEVMCompatibleWallet": " (Ethereum، Polygon) ﻯﺮﺧﺃ ﺓﺮﻣ ﺔﻟﻭﺎﺤﻤﻟﺍﻭ EVM ﻊﻣ ﺔﻘﻓﺍﻮﺘﻣ ﺔﻈﻔﺤﻣ ﻰﻟﺇ ﻞﻳﺪﺒﺘﻟﺍ ﻰﺟﺮﻳ", "symbol": "ﺰﻣﺭ", "sync_all_wallets": "مزامنة جميع المحافظ", + "sync_status_attempting_scan": "محاولة المسح", "sync_status_attempting_sync": "جاري محاولة المزامنة", "sync_status_connected": "متصل", "sync_status_connecting": "يتم التوصيل", "sync_status_failed_connect": "انقطع الاتصال", "sync_status_not_connected": "غير متصل", - "sync_status_starting_scan": "بدء المسح", + "sync_status_starting_scan": "بدء المسح الضوئي (من ${height})", "sync_status_starting_sync": "بدء المزامنة", "sync_status_syncronized": "متزامن", "sync_status_syncronizing": "يتم المزامنة", @@ -795,6 +831,7 @@ "trusted": "موثوق به", "tx_commit_exception_no_dust_on_change": "يتم رفض المعاملة مع هذا المبلغ. باستخدام هذه العملات المعدنية ، يمكنك إرسال ${min} دون تغيير أو ${max} الذي يعيد التغيير.", "tx_commit_failed": "فشل ارتكاب المعاملة. يرجى الاتصال بالدعم.", + "tx_commit_failed_no_peers": "فشل المعاملة في البث ، يرجى المحاولة مرة أخرى في ثانية أو نحو ذلك", "tx_invalid_input": "أنت تستخدم نوع الإدخال الخاطئ لهذا النوع من الدفع", "tx_no_dust_exception": "يتم رفض المعاملة عن طريق إرسال مبلغ صغير جدًا. يرجى محاولة زيادة المبلغ.", "tx_not_enough_inputs_exception": "لا يكفي المدخلات المتاحة. الرجاء تحديد المزيد تحت التحكم في العملة", @@ -839,8 +876,16 @@ "view_transaction_on": "عرض العملية على", "voting_weight": "وزن التصويت", "waitFewSecondForTxUpdate": "ﺕﻼﻣﺎﻌﻤﻟﺍ ﻞﺠﺳ ﻲﻓ ﺔﻠﻣﺎﻌﻤﻟﺍ ﺲﻜﻌﻨﺗ ﻰﺘﺣ ﻥﺍﻮﺛ ﻊﻀﺒﻟ ﺭﺎﻈﺘﻧﻻﺍ ﻰﺟﺮﻳ", + "wallet_group": "مجموعة محفظة", + "wallet_group_description_four": "لإنشاء محفظة مع بذرة جديدة تماما.", + "wallet_group_description_one": "في محفظة الكيك ، يمكنك إنشاء ملف", + "wallet_group_description_three": "لرؤية المحافظ المتاحة و/أو شاشة مجموعات المحفظة. أو اختر", + "wallet_group_description_two": "عن طريق اختيار محفظة موجودة لتبادل البذور مع. يمكن أن تحتوي كل مجموعة محفظة على محفظة واحدة من كل نوع من العملة. \n\n يمكنك تحديدها", + "wallet_group_empty_state_text_one": "يبدو أنه ليس لديك أي مجموعات محفظة متوافقة !\n\n انقر", + "wallet_group_empty_state_text_two": "أدناه لجعل واحدة جديدة.", "wallet_keys": "سييد المحفظة / المفاتيح", "wallet_list_create_new_wallet": "إنشاء محفظة جديدة", + "wallet_list_edit_group_name": "تحرير اسم المجموعة", "wallet_list_edit_wallet": "تحرير المحفظة", "wallet_list_failed_to_load": "فشل تحميل محفظة ${wallet_name}. ${error}", "wallet_list_failed_to_remove": "فشلت إزالة محفظة ${wallet_name}. ${error}", diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index 0b6b688bb..50db1610a 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -36,6 +36,7 @@ "agree": "Съгласен/а съм", "agree_and_continue": "Съгласяване и продължаване", "agree_to": "Чрез създаването на акаунт вие се съгласявате с ", + "alert_notice": "Забележете", "all": "ALL", "all_trades": "Всички сделкки", "all_transactions": "Всички транзакции", @@ -113,7 +114,7 @@ "change_currency": "Смени валута", "change_current_node": "Сигурни ли сте, че искате да промените сегашния node на ${node}?", "change_current_node_title": "Промени сегашния node", - "change_exchange_provider": "Промяна на Exchange Provider", + "change_exchange_provider": "Промяна на доставчика на суап", "change_language": "Смяна на езика", "change_language_to": "Смяна на езика на ${language}?", "change_password": "Смяна на парола", @@ -130,6 +131,7 @@ "choose_one": "Изберете едно", "choose_relay": "Моля, изберете реле, което да използвате", "choose_wallet_currency": "Изберете валута за портфейла:", + "choose_wallet_group": "Изберете Group Wallet", "clear": "Изчисти", "clearnet_link": "Clearnet връзка", "close": "затвори", @@ -142,6 +144,7 @@ "confirm_delete_wallet": "Този портфейл ще бъде изтрит. Искате ли да продължите?", "confirm_fee_deduction": "Потвърдете приспадането на таксите", "confirm_fee_deduction_content": "Съгласни ли сте да приспадате таксата от продукцията?", + "confirm_passphrase": "Потвърдете парола", "confirm_sending": "Потвърждаване на изпращането", "confirm_silent_payments_switch_node": "Текущият ви възел не поддържа Silent Payments \\ Ncake Wallet ще премине към съвместим възел, само за сканиране", "confirmations": "потвърждения", @@ -160,6 +163,7 @@ "contact_name": "Име на контакт", "contact_support": "Свържи се с отдел поддръжка", "continue_text": "Напред", + "contract_warning": "Този адрес на договора е маркиран като потенциално измамник. Моля, обработете с повишено внимание.", "contractName": "Име на договора", "contractSymbol": "Договор Символ", "copied_key_to_clipboard": "Копиран ключ: ${key}", @@ -175,6 +179,7 @@ "create_invoice": "Създайте фактура", "create_new": "Създаване на нов портфейл", "create_new_account": "Създаване на нов профил", + "create_new_seed": "Създайте нови семена", "creating_new_wallet": "Създаване на нов портфейл", "creating_new_wallet_error": "Грешка: ${description}", "creation_date": "Дата на създаване", @@ -195,6 +200,7 @@ "delete_wallet_confirm_message": "Сигурни ли сте, че искате да изтриете протфейла ${wallet_name}?", "deleteConnectionConfirmationPrompt": "Сигурни ли сте, че искате да изтриете връзката към", "denominations": "Деноминации", + "derivationpath": "Пътят на производно", "descending": "Низходящ", "description": "Описание", "destination_tag": "Destination tag:", @@ -207,6 +213,7 @@ "disable_buy": "Деактивирайте действието за покупка", "disable_cake_2fa": "Деактивирайте Cake 2FA", "disable_exchange": "Деактивиране на борса", + "disable_fee_api_warning": "Като изключите това, таксите могат да бъдат неточни в някои случаи, така че може да се препланите или да не плащате таксите за вашите транзакции", "disable_fiat": "Деактивиране на fiat", "disable_sell": "Деактивирайте действието за продажба", "disableBatteryOptimization": "Деактивирайте оптимизацията на батерията", @@ -228,8 +235,10 @@ "edit_token": "Редактиране на токена", "electrum_address_disclaimer": "Нови адреси се генерират всеки път, когато използвате този, но и предишните продължават да работят", "email_address": "Имейл адрес", + "enable": "Активиране", + "enable_mempool_api": "Mempool API за точни такси и дати", "enable_replace_by_fee": "Активиране на замяна по забрана", - "enable_silent_payments_scanning": "Активирайте безшумните плащания за сканиране", + "enable_silent_payments_scanning": "Започнете да сканирате безшумните плащания, докато се достигне съветът", "enabled": "Активирано", "enter_amount": "Въведете сума", "enter_backup_password": "Въведете парола за възстановяване", @@ -272,8 +281,8 @@ "etherscan_history": "История на Etherscan", "event": "Събитие", "events": "събития", - "exchange": "Exchange", - "exchange_incorrect_current_wallet_for_xmr": "Ако искате да обмените XMR от своя Cake Wallet Monero баланс, първо изберете своя Monero портфейл.", + "exchange": "Разметка", + "exchange_incorrect_current_wallet_for_xmr": "Ако искате да смените XMR от вашия баланс на портфейла на тортата Monero, моля, преминете първо към вашия портфейл Monero.", "exchange_new_template": "Нов шаблон", "exchange_provider_unsupported": "${providerName} вече не се поддържа!", "exchange_result_confirm": "By pressing confirm, you will be sending ${fetchingLabel} ${from} from your wallet called ${walletName} to the address shown below. Or you can send from your external wallet to the below address/QR code.\n\nPlease press confirm to continue or go back to change the amounts.", @@ -291,6 +300,8 @@ "failed_authentication": "Неуспешно удостоверяване. ${state_error}", "faq": "FAQ", "features": "Характеристика", + "fee_less_than_min": "Избраната такса е по -малка от минимума, моля, увеличете таксите, за да можете да изпратите транзакцията", + "fee_rate": "Такса ставка", "fetching": "Обработване", "fiat_api": "Fiat API", "fiat_balance": "Фиат Баланс", @@ -298,7 +309,7 @@ "fill_code": "Моля, въведето кода за потвърждаване, изпратен на Вашия имейл", "filter_by": "Филтрирай по", "first_wallet_text": "Невероятен портфейл за Monero, Bitcoin, Ethereum, Litecoin и Haven", - "fixed_pair_not_supported": "Този fixed pair не се поддържа от избраната борса", + "fixed_pair_not_supported": "Тази фиксирана двойка не се поддържа с избраните услуги за суап", "fixed_rate": "Постоянен обменен курс", "fixed_rate_alert": "Ще можете да въведете сумата за получаване, когато е избранен постоянен обменен курс. Искате ли да изберете постоянен обменен курс?", "forgot_password": "Забравена парола", @@ -354,6 +365,20 @@ "ledger_error_wrong_app": "Моля, уверете се, че сте отворили правилното приложение на вашата книга", "ledger_please_enable_bluetooth": "Моля, активирайте Bluetooth да открие вашата книга", "light_theme": "Светло", + "litecoin_enable_mweb_sync": "Активирайте сканирането на MWeb", + "litecoin_mweb": "Mweb", + "litecoin_mweb_always_scan": "Задайте MWeb винаги сканиране", + "litecoin_mweb_description": "MWeb е нов протокол, който носи по -бърз, по -евтин и повече частни транзакции на Litecoin", + "litecoin_mweb_dismiss": "Уволнение", + "litecoin_mweb_display_card": "Показване на MWEB карта", + "litecoin_mweb_enable_later": "Можете да изберете да активирате MWEB отново под настройките на дисплея.", + "litecoin_mweb_pegin": "PEG в", + "litecoin_mweb_pegout": "PEG OUT", + "litecoin_mweb_scanning": "Сканиране на MWEB", + "litecoin_mweb_settings": "Настройки на MWEB", + "litecoin_mweb_warning": "Използването на MWEB първоначално ще изтегли ~ 600MB данни и може да отнеме до 30 минути в зависимост от скоростта на мрежата. Тези първоначални данни ще изтеглят само веднъж и ще бъдат достъпни за всички портфейли Litecoin", + "litecoin_what_is_mweb": "Какво е MWEB?", + "live_fee_rates": "Цени на таксите на живо чрез API", "load_more": "Зареди още", "loading_your_wallet": "Зареждане на портфейл", "login": "Влизане", @@ -383,6 +408,8 @@ "monero_light_theme": "Лека тема Monero", "moonpay_alert_text": "Сумата трябва да бъде най-малко ${minAmount} ${fiatCurrency}", "more_options": "Още настройки", + "mweb_confirmed": "Потвърден MWeb", + "mweb_unconfirmed": "Непотвърден mweb", "name": "Име", "nano_current_rep": "Настоящ представител", "nano_gpt_thanks_message": "Благодаря, че използвахте Nanogpt! Не забравяйте да се върнете обратно към браузъра, след като транзакцията ви приключи!", @@ -412,7 +439,7 @@ "node_test": "Тест", "nodes": "Nodes", "nodes_list_reset_to_default_message": "Сигурни ли сте, че искате да възстановите фабричните настройки?", - "none_of_selected_providers_can_exchange": "Нито един от избраните provider-ъри не може да направи този превод", + "none_of_selected_providers_can_exchange": "Нито един от избраните доставчици не може да направи този размяна", "noNFTYet": "Все още няма NFT", "normal": "нормално", "note_optional": "Бележка (не е задължително)", @@ -442,6 +469,7 @@ "overwrite_amount": "Промени сума", "pairingInvalidEvent": "Невалидно събитие при сдвояване", "passphrase": "Passphrase (по избор)", + "passphrases_doesnt_match": "Пасифрази не съвпадат, моля, опитайте отново", "password": "Парола", "paste": "Поставяне", "pause_wallet_creation": "Възможността за създаване на Haven Wallet в момента е на пауза.", @@ -593,8 +621,11 @@ "seed_share": "Споделяне на seed", "seed_title": "Seed", "seedtype": "Семенна тип", + "seedtype_alert_content": "Споделянето на семена с други портфейли е възможно само с BIP39 Seedtype.", + "seedtype_alert_title": "Сигнал за семена", "seedtype_legacy": "Наследство (25 думи)", "seedtype_polyseed": "Поли семе (16 думи)", + "seedtype_wownero": "Wownero (14 думи)", "select_backup_file": "Избор на резервно копие", "select_buy_provider_notice": "Изберете доставчик на покупка по -горе. Можете да пропуснете този екран, като зададете вашия доставчик по подразбиране по подразбиране в настройките на приложението.", "select_destination": "Моля, изберете дестинация за архивния файл.", @@ -605,6 +636,7 @@ "send": "Изпрати", "send_address": "${cryptoCurrency} адрес", "send_amount": "Сума:", + "send_change_to_you": "Променете, на вас:", "send_creating_transaction": "Създаване на транзакция", "send_error_currency": "Валутата може да съдържа само числа", "send_error_minimum_value": "Минималната сума е 0.01", @@ -658,19 +690,21 @@ "setup_your_debit_card": "Настройте своята дебитна карта", "share": "Дял", "share_address": "Сподели адрес", + "shared_seed_wallet_groups": "Споделени групи за портфейли за семена", "show_details": "Показване на подробностите", "show_keys": "Покажи seed/keys", "show_market_place": "Покажи пазар", "show_seed": "Покажи seed", "sign_message": "Съобщение за подписване", "sign_up": "Регистрация", - "sign_verify_message": "Подпишете или проверете съобщението", + "sign_verify_message": "Подпишете / проверете", "sign_verify_message_sub": "Подпишете или проверете съобщение с помощта на вашия личен ключ", "sign_verify_title": "Подпишете / проверете", "signature": "Подпис", "signature_invalid_error": "Подписът не е валиден за даденото съобщение", "signTransaction": "Подпишете транзакция", "signup_for_card_accept_terms": "Регистрайте се за картата и приемете условията.", + "silent_payment": "Безшумно плащане", "silent_payments": "Мълчаливи плащания", "silent_payments_always_scan": "Задайте мълчаливи плащания винаги сканиране", "silent_payments_disclaimer": "Новите адреси не са нови идентичности. Това е повторна употреба на съществуваща идентичност с различен етикет.", @@ -681,6 +715,7 @@ "silent_payments_scanned_tip": "Сканиран за съвет! (${tip})", "silent_payments_scanning": "Безшумни плащания за сканиране", "silent_payments_settings": "Настройки за безшумни плащания", + "single_seed_wallets_group": "Портфейли с единични семена", "slidable": "Плъзгащ се", "sort_by": "Сортирай по", "spend_key_private": "Spend key (таен)", @@ -703,12 +738,13 @@ "switchToEVMCompatibleWallet": "Моля, превключете към портфейл, съвместим с EVM, и опитайте отново (Ethereum, Polygon)", "symbol": "Символ", "sync_all_wallets": "Синхронизирайте всички портфейли", + "sync_status_attempting_scan": "Опит за сканиране", "sync_status_attempting_sync": "ОПИТ ЗА СИНХРОНИЗАЦИЯ", "sync_status_connected": "СВЪРЗВАНЕ", "sync_status_connecting": "СВЪРЗВАНЕ", "sync_status_failed_connect": "НЕУСПЕШНО СВЪРЗВАНЕ", "sync_status_not_connected": "НЯМА ВРЪЗКА", - "sync_status_starting_scan": "Стартово сканиране", + "sync_status_starting_scan": "Стартиране на сканиране (от ${height})", "sync_status_starting_sync": "ЗАПОЧВАНЕ НА СИНХРОНИЗАЦИЯ", "sync_status_syncronized": "СИНХРОНИЗИРАНО", "sync_status_syncronizing": "СИНХРОНИЗИРАНЕ", @@ -795,6 +831,7 @@ "trusted": "Надежден", "tx_commit_exception_no_dust_on_change": "Сделката се отхвърля с тази сума. С тези монети можете да изпратите ${min} без промяна или ${max}, която връща промяна.", "tx_commit_failed": "Компетацията на транзакцията не успя. Моля, свържете се с поддръжката.", + "tx_commit_failed_no_peers": "Сделката не успя да излъчи, моля, опитайте отново след секунда или така", "tx_invalid_input": "Използвате грешен тип вход за този тип плащане", "tx_no_dust_exception": "Сделката се отхвърля чрез изпращане на сума твърде малка. Моля, опитайте да увеличите сумата.", "tx_not_enough_inputs_exception": "Няма достатъчно налични входове. Моля, изберете повече под контрол на монети", @@ -839,8 +876,16 @@ "view_transaction_on": "Вижте транзакция на ", "voting_weight": "Тегло на гласуване", "waitFewSecondForTxUpdate": "Моля, изчакайте няколко секунди, докато транзакцията се отрази в историята на транзакциите", + "wallet_group": "Група на портфейла", + "wallet_group_description_four": "За да създадете портфейл с изцяло ново семе.", + "wallet_group_description_one": "В портфейла за торта можете да създадете a", + "wallet_group_description_three": "За да видите наличния екран за портфейли и/или групи за портфейли. Или изберете", + "wallet_group_description_two": "Чрез избора на съществуващ портфейл, с който да споделите семе. Всяка група за портфейл може да съдържа по един портфейл от всеки тип валута. \n\n Можете да изберете", + "wallet_group_empty_state_text_one": "Изглежда, че нямате съвместими групи портфейли !\n\n tap", + "wallet_group_empty_state_text_two": "по -долу, за да се направи нов.", "wallet_keys": "Seed/keys на портфейла", "wallet_list_create_new_wallet": "Създаване на нов портфейл", + "wallet_list_edit_group_name": "Редактиране на име на групата", "wallet_list_edit_wallet": "Редактиране на портфейла", "wallet_list_failed_to_load": "Грешка при зареждането на портфейл ${wallet_name}. ${error}", "wallet_list_failed_to_remove": "Грешка при премахването на портфейл${wallet_name}. ${error}", diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index ef225041d..ddc91340b 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -36,6 +36,7 @@ "agree": "Souhlasím", "agree_and_continue": "Souhlasím & pokračovat", "agree_to": "Vytvořením účtu souhlasíte s ", + "alert_notice": "Oznámení", "all": "VŠE", "all_trades": "Všechny obchody", "all_transactions": "Všechny transakce", @@ -113,7 +114,7 @@ "change_currency": "Změnit měnu", "change_current_node": "Opravdu chcete změnit současný uzel na ${node}?", "change_current_node_title": "Změnit současný uzel", - "change_exchange_provider": "Změnit směnárnu", + "change_exchange_provider": "Změnit poskytovatele swapu", "change_language": "Změnit jazyk", "change_language_to": "Změnit jazyk na ${language}?", "change_password": "Změnit heslo", @@ -130,6 +131,7 @@ "choose_one": "Zvolte si", "choose_relay": "Vyberte relé, které chcete použít", "choose_wallet_currency": "Prosím zvolte si měnu pro peněženku:", + "choose_wallet_group": "Vyberte skupinu peněženky", "clear": "Smazat", "clearnet_link": "Odkaz na Clearnet", "close": "zavřít", @@ -142,6 +144,7 @@ "confirm_delete_wallet": "Tato akce smaže tuto peněženku. Přejete si pokračovat?", "confirm_fee_deduction": "Potvrďte odpočet poplatků", "confirm_fee_deduction_content": "Souhlasíte s odečtením poplatku z výstupu?", + "confirm_passphrase": "Potvrďte přístupovou frázi", "confirm_sending": "Potvrdit odeslání", "confirm_silent_payments_switch_node": "Váš aktuální uzel nepodporuje tiché platby \\ Ncake peněženka se přepne na kompatibilní uzel, pouze pro skenování", "confirmations": "Potvrzení", @@ -160,6 +163,7 @@ "contact_name": "Jméno kontaktu", "contact_support": "Kontaktovat podporu", "continue_text": "Pokračovat", + "contract_warning": "Tato adresa smlouvy byla označena jako potenciálně podvodná. Zpracovejte prosím opatrně.", "contractName": "Název smlouvy", "contractSymbol": "Symbol smlouvy", "copied_key_to_clipboard": "Zkopírován ${key} do schránky", @@ -175,6 +179,7 @@ "create_invoice": "Vytvořit fakturu", "create_new": "Vytvořit novou peněženku", "create_new_account": "Vytvořit nový účet", + "create_new_seed": "Vytvořte nové semeno", "creating_new_wallet": "Vytvářím novou peněženku", "creating_new_wallet_error": "Chyba: ${description}", "creation_date": "Datum vzniku", @@ -195,6 +200,7 @@ "delete_wallet_confirm_message": "Opravdu chcete smazat ${wallet_name} peněženku?", "deleteConnectionConfirmationPrompt": "Jste si jisti, že chcete smazat připojení k?", "denominations": "Označení", + "derivationpath": "Derivační cesta", "descending": "Klesající", "description": "Popis", "destination_tag": "Destination Tag:", @@ -207,6 +213,7 @@ "disable_buy": "Zakázat akci nákupu", "disable_cake_2fa": "Zakázat Cake 2FA", "disable_exchange": "Zakázat směnárny", + "disable_fee_api_warning": "Tímto vypnutím by sazby poplatků mohly být v některých případech nepřesné, takže byste mohli skončit přepláváním nebo nedoplatkem poplatků za vaše transakce", "disable_fiat": "Zakázat fiat", "disable_sell": "Zakázat akci prodeje", "disableBatteryOptimization": "Zakázat optimalizaci baterie", @@ -228,8 +235,10 @@ "edit_token": "Upravit token", "electrum_address_disclaimer": "Po každém použití je generována nová adresa, ale předchozí adresy také stále fungují", "email_address": "E-mailová adresa", + "enable": "Umožnit", + "enable_mempool_api": "Mempool API pro přesné poplatky a data", "enable_replace_by_fee": "Povolit výměnu podle poplatku", - "enable_silent_payments_scanning": "Povolte skenování tichých plateb", + "enable_silent_payments_scanning": "Začněte skenovat tiché platby, dokud není dosaženo špičky", "enabled": "Povoleno", "enter_amount": "Zadejte částku", "enter_backup_password": "Zde zadejte své heslo pro zálohy", @@ -272,8 +281,8 @@ "etherscan_history": "Historie Etherscanu", "event": "událost", "events": "Události", - "exchange": "Směnit", - "exchange_incorrect_current_wallet_for_xmr": "Pokud chcete směnit XMR z Monero částky v Cake Wallet, prosím přepněte se nejprve do své Monero peněženky.", + "exchange": "Swap", + "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!", "exchange_result_confirm": "Po stisknutí Potvrdit odešlete ${fetchingLabel} ${from} ze své peněženky s názvem ${walletName} na adresu uvedenou níže. Nebo můžete prostředky poslat ze své externí peněženky na níže uvedenou adresu/QR kód.\n\nProsím stiskněte Potvrdit pro pokračování, nebo se vraťte zpět pro změnu částky.", @@ -291,6 +300,8 @@ "failed_authentication": "Ověřování selhalo. ${state_error}", "faq": "FAQ", "features": "Funkce", + "fee_less_than_min": "Vybraný poplatek je menší než minimum, zvýšit poplatky, abyste mohli transakci odeslat", + "fee_rate": "Sazba poplatků", "fetching": "Načítá se", "fiat_api": "Fiat API", "fiat_balance": "Fiat Balance", @@ -298,7 +309,7 @@ "fill_code": "Prosím vyplňte ověřovací kód zaslaný na Váš e-mail", "filter_by": "Filtrovat podle", "first_wallet_text": "Úžasná peněženka pro Monero, Bitcoin, Ethereum, Litecoin a Haven", - "fixed_pair_not_supported": "Tento pár s pevným kurzem není ve zvolené směnárně podporován", + "fixed_pair_not_supported": "Tento pevný pár není podporován vybranými službami swapu", "fixed_rate": "Pevný kurz", "fixed_rate_alert": "Když je zvolený pevný kurz, můžete zadat konkrétní částku, kterou chcete dostat. Chcete se přepnout do režimu s pevným kurzem?", "forgot_password": "Zapomenuté heslo", @@ -354,6 +365,20 @@ "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", "light_theme": "Světlý", + "litecoin_enable_mweb_sync": "Povolit skenování MWeb", + "litecoin_mweb": "MWeb", + "litecoin_mweb_always_scan": "Nastavit MWeb vždy skenování", + "litecoin_mweb_description": "MWEB je nový protokol, který do Litecoin přináší rychlejší, levnější a více soukromých transakcí", + "litecoin_mweb_dismiss": "Propustit", + "litecoin_mweb_display_card": "Zobrazit kartu MWeb", + "litecoin_mweb_enable_later": "V nastavení zobrazení můžete vybrat znovu povolit MWeb.", + "litecoin_mweb_pegin": "Peg in", + "litecoin_mweb_pegout": "Zkrachovat", + "litecoin_mweb_scanning": "Skenování mWeb", + "litecoin_mweb_settings": "Nastavení mWeb", + "litecoin_mweb_warning": "Pomocí MWeb zpočátku stahuje ~ 600 MB dat a může trvat až 30 minut v závislosti na rychlosti sítě. Tato počáteční data si stáhnou pouze jednou a budou k dispozici pro všechny litecoinové peněženky", + "litecoin_what_is_mweb": "Co je Mweb?", + "live_fee_rates": "Živé sazby poplatků prostřednictvím API", "load_more": "Načíst další", "loading_your_wallet": "Načítám peněženku", "login": "Login", @@ -383,6 +408,8 @@ "monero_light_theme": "Světlé téma Monero", "moonpay_alert_text": "Částka musí být větší nebo rovna ${minAmount} ${fiatCurrency}", "more_options": "Více možností", + "mweb_confirmed": "Potvrzený mweb", + "mweb_unconfirmed": "Nepotvrzené mWeb", "name": "název", "nano_current_rep": "Současný zástupce", "nano_gpt_thanks_message": "Děkujeme za používání Nanogpt! Nezapomeňte se po dokončení transakce vydat zpět do prohlížeče!", @@ -412,7 +439,7 @@ "node_test": "Otestovat", "nodes": "Uzly", "nodes_list_reset_to_default_message": "Opravdu chcete zrušit nastavení a vrátit výchozí hodnotu?", - "none_of_selected_providers_can_exchange": "Žádný ze zvolených poskytovatelů nemůže provést tuto směnu", + "none_of_selected_providers_can_exchange": "Žádný z vybraných poskytovatelů nemůže tuto swap provést", "noNFTYet": "Zatím žádné NFT", "normal": "Normální", "note_optional": "Poznámka (nepovinné)", @@ -442,6 +469,7 @@ "overwrite_amount": "Přepsat částku", "pairingInvalidEvent": "Neplatná událost párování", "passphrase": "Passphrase (volitelné)", + "passphrases_doesnt_match": "Passfrázy se neshodují, zkuste to znovu", "password": "Heslo", "paste": "Vložit", "pause_wallet_creation": "Možnost vytvářet Haven Wallet je momentálně pozastavena.", @@ -593,8 +621,11 @@ "seed_share": "Sdílet seed", "seed_title": "Seed", "seedtype": "SeedType", + "seedtype_alert_content": "Sdílení semen s jinými peněženkami je možné pouze u BIP39 SeedType.", + "seedtype_alert_title": "Upozornění seedtype", "seedtype_legacy": "Legacy (25 slov)", "seedtype_polyseed": "Polyseed (16 slov)", + "seedtype_wownero": "Wownero (14 slov)", "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.", @@ -605,6 +636,7 @@ "send": "Poslat", "send_address": "${cryptoCurrency} adresa", "send_amount": "Částka:", + "send_change_to_you": "Změňte, vám:", "send_creating_transaction": "Vytváření transakce", "send_error_currency": "Měna může obsahovat pouze čísla", "send_error_minimum_value": "Minimální částka je 0,01", @@ -658,19 +690,21 @@ "setup_your_debit_card": "Nastavit debetní kartu", "share": "Podíl", "share_address": "Sdílet adresu", + "shared_seed_wallet_groups": "Skupiny sdílených semen", "show_details": "Zobrazit detaily", "show_keys": "Zobrazit seed/klíče", "show_market_place": "Zobrazit trh", "show_seed": "Zobrazit seed", "sign_message": "Podepsat zprávu", "sign_up": "Registrovat se", - "sign_verify_message": "Podepište nebo ověřte zprávu", + "sign_verify_message": "Podepsat / ověřit", "sign_verify_message_sub": "Podepište nebo ověřte zprávu pomocí soukromého klíče", "sign_verify_title": "Podepsat / ověřit", "signature": "Podpis", "signature_invalid_error": "Podpis není platný pro danou zprávu", "signTransaction": "Podepsat transakci", "signup_for_card_accept_terms": "Zaregistrujte se pro kartu a souhlaste s podmínkami.", + "silent_payment": "Tichá platba", "silent_payments": "Tiché platby", "silent_payments_always_scan": "Nastavit tiché platby vždy skenování", "silent_payments_disclaimer": "Nové adresy nejsou nové identity. Je to opětovné použití existující identity s jiným štítkem.", @@ -681,6 +715,7 @@ "silent_payments_scanned_tip": "Naskenované na tip! (${tip})", "silent_payments_scanning": "Skenování tichých plateb", "silent_payments_settings": "Nastavení tichých plateb", + "single_seed_wallets_group": "Jednorázové peněženky", "slidable": "Posuvné", "sort_by": "Seřazeno podle", "spend_key_private": "Klíč pro platby (soukromý)", @@ -703,12 +738,13 @@ "switchToEVMCompatibleWallet": "Přepněte na peněženku kompatibilní s EVM a zkuste to znovu (Ethereum, Polygon)", "symbol": "Symbol", "sync_all_wallets": "Synchronizovat všechny peněženky", + "sync_status_attempting_scan": "Pokus o skenování", "sync_status_attempting_sync": "ZAHAJUJI SYNCHR.", "sync_status_connected": "PŘIPOJENO", "sync_status_connecting": "PŘIPOJOVÁNÍ", "sync_status_failed_connect": "ODPOJENO", "sync_status_not_connected": "NEPŘIPOJENO", - "sync_status_starting_scan": "Počáteční skenování", + "sync_status_starting_scan": "Počáteční skenování (z ${height})", "sync_status_starting_sync": "SPOUŠTĚNÍ SYNCHRONIZACE", "sync_status_syncronized": "SYNCHRONIZOVÁNO", "sync_status_syncronizing": "SYNCHRONIZUJI", @@ -795,6 +831,7 @@ "trusted": "Důvěřovat", "tx_commit_exception_no_dust_on_change": "Transakce je zamítnuta s touto částkou. S těmito mincemi můžete odeslat ${min} bez změny nebo ${max}, které se vrátí změna.", "tx_commit_failed": "Transakce COMPORT selhala. Kontaktujte prosím podporu.", + "tx_commit_failed_no_peers": "Transakce se nepodařilo vysílat, zkuste to prosím znovu za vteřinu", "tx_invalid_input": "Pro tento typ platby používáte nesprávný typ vstupu", "tx_no_dust_exception": "Transakce je zamítnuta odesláním příliš malé. Zkuste prosím zvýšit částku.", "tx_not_enough_inputs_exception": "Není k dispozici dostatek vstupů. Vyberte prosím více pod kontrolou mincí", @@ -839,8 +876,16 @@ "view_transaction_on": "Zobrazit transakci na ", "voting_weight": "Hlasová váha", "waitFewSecondForTxUpdate": "Počkejte několik sekund, než se transakce projeví v historii transakcí", + "wallet_group": "Skupina peněženky", + "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_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_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", "wallet_list_create_new_wallet": "Vytvořit novou peněženku", + "wallet_list_edit_group_name": "Upravit název skupiny", "wallet_list_edit_wallet": "Upravit peněženku", "wallet_list_failed_to_load": "Chyba při načítání ${wallet_name} peněženky. ${error}", "wallet_list_failed_to_remove": "Chyba při odstraňování ${wallet_name} peněženky. ${error}", diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 393c3d928..2ec59f349 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -36,6 +36,7 @@ "agree": "stimme zu", "agree_and_continue": "Zustimmen & fortfahren", "agree_to": "Indem Sie ein Konto erstellen, stimmen Sie den ", + "alert_notice": "Beachten", "all": "ALLES", "all_trades": "Alle Trades", "all_transactions": "Alle Transaktionen", @@ -96,7 +97,7 @@ "cake_pay_account_note": "Melden Sie sich nur mit einer E-Mail-Adresse an, um Karten anzuzeigen und zu kaufen. Einige sind sogar mit Rabatt erhältlich!", "cake_pay_learn_more": "Kaufen und lösen Sie Geschenkkarten sofort in der App ein!\nWischen Sie von links nach rechts, um mehr zu erfahren.", "cake_pay_save_order": "Die Karte sollte innerhalb von 1 Werktag an Ihre E-Mail gesendet werden, \n Ihre Bestell-ID zu speichern:", - "cake_pay_subtitle": "Kaufen Sie weltweite Prepaid -Karten und Geschenkkarten", + "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_wallet": "Cake Wallet", @@ -113,7 +114,7 @@ "change_currency": "Währung ändern", "change_current_node": "Möchten Sie den aktuellen Knoten wirklich zu ${node}? ändern?", "change_current_node_title": "Aktuellen Knoten ändern", - "change_exchange_provider": "Exchange-Anbieter ändern", + "change_exchange_provider": "Swap-Anbieter ändern", "change_language": "Sprache ändern", "change_language_to": "Sprache zu ${language} ändern?", "change_password": "Passwort ändern", @@ -130,6 +131,7 @@ "choose_one": "Wähle ein", "choose_relay": "Bitte wählen Sie ein zu verwendendes Relais aus", "choose_wallet_currency": "Bitte wählen Sie die Währung der Wallet:", + "choose_wallet_group": "Wählen Sie Walletgruppe", "clear": "Zurücksetzen", "clearnet_link": "Clearnet-Link", "close": "Schließen", @@ -142,6 +144,7 @@ "confirm_delete_wallet": "Diese Aktion löscht diese Wallet. Möchten Sie fortfahren?", "confirm_fee_deduction": "Gebührenabzug bestätigen", "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", "confirmations": "Bestätigungen", @@ -160,6 +163,7 @@ "contact_name": "Name des Kontakts", "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", "copied_key_to_clipboard": "${key} in Zwischenablage kopiert", @@ -175,6 +179,7 @@ "create_invoice": "Rechnung erstellen", "create_new": "Neue Wallet erstellen", "create_new_account": "Neues Konto erstellen", + "create_new_seed": "Neue Seed erstellen", "creating_new_wallet": "Neue Wallet erstellen", "creating_new_wallet_error": "Fehler: ${description}", "creation_date": "Erstellungsdatum", @@ -184,9 +189,9 @@ "custom_value": "Benutzerdefinierten Wert", "dark_theme": "Dunkel", "debit_card": "Debitkarte", - "debit_card_terms": "Die Speicherung und Nutzung Ihrer Zahlungskartennummer (und Ihrer Zahlungskartennummer entsprechenden Anmeldeinformationen) in dieser digitalen Geldbörse unterliegt den Allgemeinen Geschäftsbedingungen des geltenden Karteninhabervertrags mit dem Zahlungskartenaussteller, gültig ab von Zeit zu Zeit.", + "debit_card_terms": "Die Speicherung und Nutzung Ihrer Zahlungskartennummer (und Ihrer Zahlungskartennummer entsprechenden Anmeldeinformationen) in dieser digitalen Wallet unterliegt den Allgemeinen Geschäftsbedingungen des geltenden Karteninhabervertrags mit dem Zahlungskartenaussteller, gültig ab von Zeit zu Zeit.", "decimal_places_error": "Zu viele Nachkommastellen", - "decimals_cannot_be_zero": "Token -Dezimalzahl kann nicht Null sein.", + "decimals_cannot_be_zero": "Token-Dezimalzahl kann nicht Null sein.", "default_buy_provider": "Standard-Kaufanbieter", "default_sell_provider": "Standard-Verkaufsanbieter", "delete": "Löschen", @@ -195,6 +200,7 @@ "delete_wallet_confirm_message": "Sind Sie sicher, dass Sie das ${wallet_name} Wallet löschen möchten?", "deleteConnectionConfirmationPrompt": "Sind Sie sicher, dass Sie die Verbindung zu löschen möchten?", "denominations": "Konfessionen", + "derivationpath": "Ableitungspfad", "descending": "Absteigend", "description": "Beschreibung", "destination_tag": "Ziel-Tag:", @@ -207,6 +213,7 @@ "disable_buy": "Kaufaktion deaktivieren", "disable_cake_2fa": "Cake 2FA deaktivieren", "disable_exchange": "Exchange deaktivieren", + "disable_fee_api_warning": "Wenn dies ausgeschaltet wird, sind die Gebührenquoten in einigen Fällen möglicherweise ungenau, sodass Sie die Gebühren für Ihre Transaktionen möglicherweise überbezahlt oder unterzahlt", "disable_fiat": "Fiat deaktivieren", "disable_sell": "Verkaufsaktion deaktivieren", "disableBatteryOptimization": "Batterieoptimierung deaktivieren", @@ -228,15 +235,17 @@ "edit_token": "Token bearbeiten", "electrum_address_disclaimer": "Wir generieren jedes Mal neue Adressen, wenn Sie eine verwenden, aber vorherige Adressen funktionieren weiterhin", "email_address": "E-Mail-Adresse", + "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": "Aktivieren Sie stille Zahlungen Scannen", + "enable_silent_payments_scanning": "Scannen Sie stille Zahlungen, bis die Spitze erreicht ist", "enabled": "Ermöglicht", "enter_amount": "Betrag eingeben", "enter_backup_password": "Sicherungskennwort hier eingeben", "enter_code": "Code eingeben", "enter_seed_phrase": "Geben Sie Ihre Seed-Phrase ein", "enter_totp_code": "Bitte geben Sie den TOTP-Code ein.", - "enter_wallet_password": "Geben Sie das Brieftaschenkennwort ein", + "enter_wallet_password": "Geben Sie das Walletkennwort ein", "enter_your_note": "Geben Sie Ihre Bemerkung ein…", "enter_your_pin": "PIN eingeben", "enter_your_pin_again": "Geben Sie Ihre PIN erneut ein", @@ -272,8 +281,8 @@ "etherscan_history": "Etherscan-Geschichte", "event": "Ereignis", "events": "Veranstaltungen", - "exchange": "Umwechseln", - "exchange_incorrect_current_wallet_for_xmr": "Wenn Sie XMR von Ihrem Cake Wallet Monero-Guthaben umtauschen möchten, wechseln Sie bitte zuerst zu Ihrer Monero-Wallet.", + "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_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.", @@ -291,6 +300,8 @@ "failed_authentication": "Authentifizierung fehlgeschlagen. ${state_error}", "faq": "Häufig gestellte Fragen", "features": "Merkmale", + "fee_less_than_min": "Die ausgewählte Gebühr ist geringer als das Minimum. Bitte erhöhen Sie die Gebühren, um die Transaktion senden zu können", + "fee_rate": "Gebührenpreis", "fetching": "Frage ab", "fiat_api": "Fiat API", "fiat_balance": "Fiat Balance", @@ -298,7 +309,7 @@ "fill_code": "Geben Sie den Bestätigungscode ein, den Sie per E-Mail erhalten haben", "filter_by": "Filtern nach", "first_wallet_text": "Eine großartige Wallet für Monero, Bitcoin, Ethereum, Litecoin, und Haven", - "fixed_pair_not_supported": "Dieses feste Paar wird von den ausgewählten Vermittlungsstellen nicht unterstützt", + "fixed_pair_not_supported": "Dieses feste Paar wird nicht von den ausgewählten Swap-Diensten unterstützt", "fixed_rate": "Feste Rate", "fixed_rate_alert": "Sie können den Empfangsbetrag eingeben, wenn der Festratenmodus aktiviert ist. Möchten Sie in den Festratenmodus wechseln?", "forgot_password": "Passwort vergessen", @@ -338,9 +349,9 @@ "incoming": "Eingehend", "incorrect_seed": "Der eingegebene Text ist ungültig.", "inputs": "Eingänge", - "insufficient_lamport_for_tx": "Sie haben nicht genug SOL, um die Transaktion und ihre Transaktionsgebühr abzudecken. Bitte fügen Sie Ihrer Brieftasche mehr Sol hinzu oder reduzieren Sie die SO -Menge, die Sie senden.", - "insufficient_lamports": "Sie haben nicht genug SOL, um die Transaktion und ihre Transaktionsgebühr abzudecken. Sie brauchen mindestens ${solValueNeeded} Sol. Bitte fügen Sie mehr Sol zu Ihrer Wallet hinzu oder reduzieren Sie den von Ihnen gesendeten Sol -Betrag", - "insufficientFundsForRentError": "Sie haben nicht genug SOL, um die Transaktionsgebühr und die Miete für das Konto zu decken. Bitte fügen Sie mehr Sol zu Ihrer Brieftasche hinzu oder reduzieren Sie den von Ihnen gesendeten Sol -Betrag", + "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.", + "insufficient_lamports": "Sie haben nicht genug SOL, um die Transaktion und ihre Transaktionsgebühr abzudecken. Sie brauchen mindestens ${solValueNeeded} Sol. Bitte fügen Sie mehr Sol zu Ihrer Wallet hinzu oder reduzieren Sie den von Ihnen gesendeten Sol-Betrag", + "insufficientFundsForRentError": "Sie haben nicht genug SOL, um die Transaktionsgebühr und die Miete für das Konto zu decken. Bitte fügen Sie mehr Sol zu Ihrer Wallet hinzu oder reduzieren Sie den von Ihnen gesendeten Sol-Betrag", "introducing_cake_pay": "Einführung von Cake Pay!", "invalid_input": "Ungültige Eingabe", "invalid_password": "Ungültiges Passwort", @@ -354,6 +365,20 @@ "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.", "light_theme": "Hell", + "litecoin_enable_mweb_sync": "Aktivieren Sie das MWEB-Scannen", + "litecoin_mweb": "MWeb", + "litecoin_mweb_always_scan": "Setzen Sie MWeb immer scannen", + "litecoin_mweb_description": "MWWB ist ein neues Protokoll, das schnellere, billigere und privatere Transaktionen zu Litecoin bringt", + "litecoin_mweb_dismiss": "Zurückweisen", + "litecoin_mweb_display_card": "MWEB-Karte anzeigen", + "litecoin_mweb_enable_later": "Sie können MWEB unter Anzeigeeinstellungen erneut aktivieren.", + "litecoin_mweb_pegin": "Peg in", + "litecoin_mweb_pegout": "Abstecken", + "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", + "litecoin_what_is_mweb": "Was ist MWeb?", + "live_fee_rates": "Live-Gebührenpreise über API", "load_more": "Mehr laden", "loading_your_wallet": "Wallet wird geladen", "login": "Einloggen", @@ -383,6 +408,8 @@ "monero_light_theme": "Monero Light-Thema", "moonpay_alert_text": "Der Wert des Betrags muss größer oder gleich ${minAmount} ${fiatCurrency} sein", "more_options": "Weitere Optionen", + "mweb_confirmed": "Bestätigt MWeb", + "mweb_unconfirmed": "Unbestätigter MWeb", "name": "Name", "nano_current_rep": "Aktueller Vertreter", "nano_gpt_thanks_message": "Danke, dass du Nanogpt benutzt hast! Denken Sie daran, nach Abschluss Ihrer Transaktion zurück zum Browser zu gehen!", @@ -412,7 +439,7 @@ "node_test": "Test", "nodes": "Knoten", "nodes_list_reset_to_default_message": "Möchten Sie wirklich die Standardeinstellungen wiederherstellen?", - "none_of_selected_providers_can_exchange": "Keiner der ausgewählten Anbieter kann diesen Austausch vornehmen", + "none_of_selected_providers_can_exchange": "Keiner der ausgewählten Anbieter kann diesen Tausch machen", "noNFTYet": "Noch keine NFTs", "normal": "Normal", "note_optional": "Bemerkung (optional)", @@ -442,6 +469,7 @@ "overwrite_amount": "Overwrite amount", "pairingInvalidEvent": "Paarung ungültiges Ereignis", "passphrase": "Passphrase (optional)", + "passphrases_doesnt_match": "Passphrasen stimmen nicht überein, bitte versuchen Sie es erneut", "password": "Passwort", "paste": "Einfügen", "pause_wallet_creation": "Die Möglichkeit, Haven Wallet zu erstellen, ist derzeit pausiert.", @@ -456,8 +484,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", @@ -505,8 +533,8 @@ "rename": "Umbenennen", "rep_warning": "Repräsentative Warnung", "rep_warning_sub": "Ihr Vertreter scheint nicht gut zu sein. Tippen Sie hier, um eine neue auszuwählen", - "repeat_wallet_password": "Wiederholen Sie das Brieftaschenkennwort", - "repeated_password_is_incorrect": "Wiederholtes Passwort ist falsch. Bitte wiederholen Sie das Brieftaschenkennwort erneut.", + "repeat_wallet_password": "Wiederholen Sie das Walletkennwort", + "repeated_password_is_incorrect": "Wiederholtes Passwort ist falsch. Bitte wiederholen Sie das Walletkennwort erneut.", "require_for_adding_contacts": "Erforderlich zum Hinzufügen von Kontakten", "require_for_all_security_and_backup_settings": "Für alle Sicherheits- und Sicherungseinstellungen erforderlich", "require_for_assessing_wallet": "Für den Zugriff auf die Wallet erforderlich", @@ -548,7 +576,7 @@ "restore_wallet": "Wallet wiederherstellen", "restore_wallet_name": "Walletname", "restore_wallet_restore_description": "Beschreibung zur Wallet-Wiederherstellung", - "robinhood_option_description": "Kaufen und übertragen Sie sofort mit Ihrem Debitkarten-, Bankkonto- oder Robinhood -Guthaben. Nur USA.", + "robinhood_option_description": "Kaufen und übertragen Sie sofort mit Ihrem Debitkarten-, Bankkonto- oder Robinhood-Guthaben. Nur USA.", "router_no_route": "Keine Route definiert für ${name}", "save": "Speichern", "save_backup_password": "Bitte stellen Sie sicher, dass Sie Ihr Sicherungskennwort gespeichert haben. Ohne dieses können Sie Ihre Sicherungsdateien nicht importieren.", @@ -594,8 +622,11 @@ "seed_share": "Seed teilen", "seed_title": "Seed", "seedtype": "Seedtyp", + "seedtype_alert_content": "Das Teilen von Seeds mit anderen Wallet ist nur mit bip39 Seedype möglich.", + "seedtype_alert_title": "Seedype-Alarm", "seedtype_legacy": "Veraltet (25 Wörter)", "seedtype_polyseed": "Polyseed (16 Wörter)", + "seedtype_wownero": "WOWNO (14 Wörter)", "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.", @@ -606,6 +637,7 @@ "send": "Senden", "send_address": "${cryptoCurrency}-Adresse", "send_amount": "Betrag:", + "send_change_to_you": "Verändere dich zu dir:", "send_creating_transaction": "Erstelle Transaktion", "send_error_currency": "Die Währung darf nur Zahlen enthalten", "send_error_minimum_value": "Der Mindestbetrag ist 0,01", @@ -659,29 +691,32 @@ "setup_your_debit_card": "Richten Sie Ihre Debitkarte ein", "share": "Teilen", "share_address": "Adresse teilen ", + "shared_seed_wallet_groups": "Gemeinsame Walletsseed Gruppen", "show_details": "Details anzeigen", "show_keys": "Seed/Schlüssel anzeigen", "show_market_place": "Marktplatz anzeigen", "show_seed": "Seed zeigen", "sign_message": "Nachricht unterschreiben", "sign_up": "Anmelden", - "sign_verify_message": "Nachricht unterschreiben oder überprüfen", + "sign_verify_message": "Zeichen / überprüfen", "sign_verify_message_sub": "Unterschreiben oder überprüfen Sie eine Nachricht mit Ihrem privaten Schlüssel", "sign_verify_title": "Zeichen / überprüfen", "signature": "Signatur", "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_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_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 Brieftasche 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 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_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", + "single_seed_wallets_group": "Einzelne Wallets", "slidable": "Verschiebbar", "sort_by": "Sortiere nach", "spend_key_private": "Spend Key (geheim)", @@ -704,12 +739,13 @@ "switchToEVMCompatibleWallet": "Bitte wechseln Sie zu einem EVM-kompatiblen Wallet und versuchen Sie es erneut (Ethereum, Polygon)", "symbol": "Symbol", "sync_all_wallets": "Alle Wallets synchronisieren", + "sync_status_attempting_scan": "Versuch Scan", "sync_status_attempting_sync": "SYNC VERSUCHEN", "sync_status_connected": "VERBUNDEN", "sync_status_connecting": "VERBINDEN", "sync_status_failed_connect": "GETRENNT", "sync_status_not_connected": "NICHT VERBUNDEN", - "sync_status_starting_scan": "Scan beginnen", + "sync_status_starting_scan": "SCAN starten (von ${height})", "sync_status_starting_sync": "STARTE SYNCHRONISIERUNG", "sync_status_syncronized": "SYNCHRONISIERT", "sync_status_syncronizing": "SYNCHRONISIERE", @@ -719,11 +755,11 @@ "syncing_wallet_alert_title": "Ihr Wallet wird synchronisiert", "template": "Vorlage", "template_name": "Vorlagenname", - "testnet_coins_no_value": "Testnet -Münzen haben keinen Wert", + "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", - "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.", + "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", @@ -796,6 +832,7 @@ "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.", + "tx_commit_failed_no_peers": "Transaktion konnte nicht übertragen werden. Bitte versuchen Sie es in einer Sekunde oder so erneut", "tx_invalid_input": "Sie verwenden den falschen Eingangstyp für diese Art von Zahlung", "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", @@ -842,8 +879,16 @@ "voting_weight": "Stimmgewicht", "waitFewSecondForTxUpdate": "Bitte warten Sie einige Sekunden, bis die Transaktion im Transaktionsverlauf angezeigt wird", "waiting_payment_confirmation": "Warte auf Zahlungsbestätigung", + "wallet_group": "Walletgruppe", + "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_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_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", "wallet_list_create_new_wallet": "Neue Wallet erstellen", + "wallet_list_edit_group_name": "Gruppenname bearbeiten", "wallet_list_edit_wallet": "Wallet bearbeiten", "wallet_list_failed_to_load": "Laden der Wallet ${wallet_name} fehlgeschlagen. ${error}", "wallet_list_failed_to_remove": "Fehler beim Entfernen der Wallet ${wallet_name}. ${error}", @@ -856,7 +901,7 @@ "wallet_menu": "Wallet-Menü", "wallet_name": "Walletname", "wallet_name_exists": "Wallet mit diesem Namen existiert bereits", - "wallet_password_is_empty": "Brieftaschenkennwort ist leer. Brieftaschenkennwort sollte nicht leer sein", + "wallet_password_is_empty": "Walletkennwort ist leer. Walletkennwort sollte nicht leer sein", "wallet_recovery_height": "Erstellungshöhe", "wallet_restoration_store_incorrect_seed_length": "Falsche Seed-Länge", "wallet_seed": "Wallet-Seed", @@ -896,4 +941,4 @@ "you_will_get": "Konvertieren zu", "you_will_send": "Konvertieren von", "yy": "YY" -} \ No newline at end of file +} diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 4ac7dd8fd..d6a0ee9af 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -36,6 +36,7 @@ "agree": "Agree", "agree_and_continue": "Agree & Continue", "agree_to": "By creating account you agree to the ", + "alert_notice": "Notice", "all": "ALL", "all_trades": "All trades", "all_transactions": "All transactions", @@ -113,7 +114,7 @@ "change_currency": "Change Currency", "change_current_node": "Are you sure to change current node to ${node}?", "change_current_node_title": "Change current node", - "change_exchange_provider": "Change Exchange Provider", + "change_exchange_provider": "Change Swap Provider", "change_language": "Change language", "change_language_to": "Change language to ${language}?", "change_password": "Change password", @@ -130,6 +131,7 @@ "choose_one": "Choose one", "choose_relay": "Please choose a relay to use", "choose_wallet_currency": "Please choose wallet currency:", + "choose_wallet_group": "Choose Wallet Group", "clear": "Clear", "clearnet_link": "Clearnet link", "close": "Close", @@ -142,8 +144,9 @@ "confirm_delete_wallet": "This action will delete this wallet. Do you wish to continue?", "confirm_fee_deduction": "Confirm Fee Deduction", "confirm_fee_deduction_content": "Do you agree to deduct the fee from the output?", + "confirm_passphrase": "Confirm passphrase", "confirm_sending": "Confirm sending", - "confirm_silent_payments_switch_node": "Your current node does not support silent payments\\nCake Wallet will switch to a compatible node, just for scanning", + "confirm_silent_payments_switch_node": "Your current node does not support Silent Payments.\\n\\nCake Wallet will switch to a compatible node while scanning.", "confirmations": "Confirmations", "confirmed": "Confirmed Balance", "confirmed_tx": "Confirmed", @@ -160,6 +163,7 @@ "contact_name": "Contact Name", "contact_support": "Contact Support", "continue_text": "Continue", + "contract_warning": "This contract address has been flagged as potentially fraudulent. Please process with caution.", "contractName": "Contract Name", "contractSymbol": "Contract Symbol", "copied_key_to_clipboard": "Copied ${key} to Clipboard", @@ -175,6 +179,7 @@ "create_invoice": "Create invoice", "create_new": "Create New Wallet", "create_new_account": "Create new account", + "create_new_seed": "Create New Seed", "creating_new_wallet": "Creating new wallet", "creating_new_wallet_error": "Error: ${description}", "creation_date": "Creation Date", @@ -195,6 +200,7 @@ "delete_wallet_confirm_message": "Are you sure that you want to delete ${wallet_name} wallet?", "deleteConnectionConfirmationPrompt": "Are you sure that you want to delete the connection to", "denominations": "Denominations", + "derivationpath": "Derivation Path", "descending": "Descending", "description": "Description", "destination_tag": "Destination tag:", @@ -207,6 +213,7 @@ "disable_buy": "Disable buy action", "disable_cake_2fa": "Disable Cake 2FA", "disable_exchange": "Disable exchange", + "disable_fee_api_warning": "By turning this off, the fee rates might be inaccurate in some cases, so you might end up overpaying or underpaying the fees for your transactions", "disable_fiat": "Disable fiat", "disable_sell": "Disable sell action", "disableBatteryOptimization": "Disable Battery Optimization", @@ -228,8 +235,10 @@ "edit_token": "Edit token", "electrum_address_disclaimer": "We generate new addresses each time you use one, but previous addresses continue to work", "email_address": "Email Address", + "enable": "Enable", + "enable_mempool_api": "Mempool API for accurate fees and dates", "enable_replace_by_fee": "Enable Replace-By-Fee", - "enable_silent_payments_scanning": "Enable silent payments scanning", + "enable_silent_payments_scanning": "Start scanning for transactions sent to your Silent Payment address.", "enabled": "Enabled", "enter_amount": "Enter Amount", "enter_backup_password": "Enter backup password here", @@ -272,8 +281,8 @@ "etherscan_history": "Etherscan history", "event": "Event", "events": "Events", - "exchange": "Exchange", - "exchange_incorrect_current_wallet_for_xmr": "If you want to exchange XMR from your Cake Wallet Monero balance, please switch to your Monero wallet first.", + "exchange": "Swap", + "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!", "exchange_result_confirm": "By pressing confirm, you will be sending ${fetchingLabel} ${from} from your wallet called ${walletName} to the address shown below. Or you can send from your external wallet to the below address/QR code.\n\nPlease press confirm to continue or go back to change the amounts.", @@ -291,6 +300,8 @@ "failed_authentication": "Failed authentication. ${state_error}", "faq": "FAQ", "features": "Features", + "fee_less_than_min": "Selected Fee is less than the minimum, please increase the fees to be able to send the transaction", + "fee_rate": "Fee rate", "fetching": "Fetching", "fiat_api": "Fiat API", "fiat_balance": "Fiat Balance", @@ -298,7 +309,7 @@ "fill_code": "Please fill in the verification code provided to your email", "filter_by": "Filter by", "first_wallet_text": "Awesome wallet for Monero, Bitcoin, Ethereum, Litecoin, and Haven", - "fixed_pair_not_supported": "This fixed pair is not supported with the selected exchanges", + "fixed_pair_not_supported": "This fixed pair is not supported with the selected swap services", "fixed_rate": "Fixed rate", "fixed_rate_alert": "You will be able to enter receive amount when fixed rate mode is checked. Do you want to switch to fixed rate mode?", "forgot_password": "Forgot Password", @@ -324,7 +335,9 @@ "haven_app": "Haven by Cake Wallet", "haven_app_wallet_text": "Awesome wallet for Haven", "help": "help", + "hidden_addresses": "Hidden Addresses", "hidden_balance": "Hidden Balance", + "hide": "Hide", "hide_details": "Hide Details", "high_contrast_theme": "High Contrast Theme", "home_screen_settings": "Home screen settings", @@ -354,6 +367,20 @@ "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", "light_theme": "Light", + "litecoin_enable_mweb_sync": "Enable MWEB scanning", + "litecoin_mweb": "MWEB", + "litecoin_mweb_always_scan": "Set MWEB always scanning", + "litecoin_mweb_description": "MWEB is a new protocol that brings faster, cheaper, and more private transactions to Litecoin", + "litecoin_mweb_dismiss": "Dismiss", + "litecoin_mweb_display_card": "Show MWEB card", + "litecoin_mweb_enable_later": "You can choose to enable MWEB again under Display Settings.", + "litecoin_mweb_pegin": "Peg In", + "litecoin_mweb_pegout": "Peg Out", + "litecoin_mweb_scanning": "MWEB Scanning", + "litecoin_mweb_settings": "MWEB settings", + "litecoin_mweb_warning": "Using MWEB will initially download ~600MB of data, and may take up to 30 minutes depending on network speed. This initial data will only download once and be available for all Litecoin wallets", + "litecoin_what_is_mweb": "What is MWEB?", + "live_fee_rates": "Live fee rates via API", "load_more": "Load more", "loading_your_wallet": "Loading your wallet", "login": "Login", @@ -383,6 +410,8 @@ "monero_light_theme": "Monero Light Theme", "moonpay_alert_text": "Value of the amount must be more or equal to ${minAmount} ${fiatCurrency}", "more_options": "More Options", + "mweb_confirmed": "Confirmed MWEB", + "mweb_unconfirmed": "Unconfirmed MWEB", "name": "Name", "nano_current_rep": "Current Representative", "nano_gpt_thanks_message": "Thanks for using NanoGPT! Remember to head back to the browser after your transaction completes!", @@ -412,7 +441,7 @@ "node_test": "Test", "nodes": "Nodes", "nodes_list_reset_to_default_message": "Are you sure that you want to reset settings to default?", - "none_of_selected_providers_can_exchange": "None of the selected providers can make this exchange", + "none_of_selected_providers_can_exchange": "None of the selected providers can make this swap", "noNFTYet": "No NFTs yet", "normal": "Normal", "note_optional": "Note (optional)", @@ -442,6 +471,7 @@ "overwrite_amount": "Overwrite amount", "pairingInvalidEvent": "Pairing Invalid Event", "passphrase": "Passphrase (Optional)", + "passphrases_doesnt_match": "Passphrases do not match, please try again", "password": "Password", "paste": "Paste", "pause_wallet_creation": "Ability to create Haven Wallet is currently paused.", @@ -593,6 +623,8 @@ "seed_share": "Share seed", "seed_title": "Seed", "seedtype": "Seedtype", + "seedtype_alert_content": "Sharing seeds with other wallets is only possible with BIP39 SeedType.", + "seedtype_alert_title": "SeedType Alert", "seedtype_legacy": "Legacy (25 words)", "seedtype_polyseed": "Polyseed (16 words)", "seedtype_wownero": "Wownero (14 words)", @@ -606,6 +638,7 @@ "send": "Send", "send_address": "${cryptoCurrency} address", "send_amount": "Amount:", + "send_change_to_you": "Change, to you:", "send_creating_transaction": "Creating transaction", "send_error_currency": "Currency can only contain numbers", "send_error_minimum_value": "Minimum value of amount is 0.01", @@ -659,29 +692,33 @@ "setup_your_debit_card": "Set up your debit card", "share": "Share", "share_address": "Share address", + "shared_seed_wallet_groups": "Shared Seed Wallet Groups", + "show": "Show", "show_details": "Show Details", "show_keys": "Show seed/keys", "show_market_place": "Show Marketplace", "show_seed": "Show seed", "sign_message": "Sign Message", "sign_up": "Sign Up", - "sign_verify_message": "Sign or verify message", + "sign_verify_message": "Sign / Verify", "sign_verify_message_sub": "Sign or verify a message using your private key", "sign_verify_title": "Sign / Verify", "signature": "Signature", "signature_invalid_error": "The signature is not valid for the message given", "signTransaction": "Sign Transaction", "signup_for_card_accept_terms": "Sign up for the card and accept the terms.", + "silent_payment": "Silent Payment", "silent_payments": "Silent Payments", "silent_payments_always_scan": "Set Silent Payments always scanning", "silent_payments_disclaimer": "New addresses are not new identities. It is a re-use of an existing identity with a different label.", "silent_payments_display_card": "Show Silent Payments card", "silent_payments_scan_from_date": "Scan from date", - "silent_payments_scan_from_date_or_blockheight": "Please enter the block height you want to start scanning for incoming silent payments, or, use the date instead. You can choose if the wallet continues scanning every block, or checks only the specified height.", + "silent_payments_scan_from_date_or_blockheight": "Please enter the block height you want to start scanning for incoming Silent Payments or use the date instead. You can choose if the wallet continues scanning every block, or checks only the specified height.", "silent_payments_scan_from_height": "Scan from block height", "silent_payments_scanned_tip": "SCANNED TO TIP! (${tip})", "silent_payments_scanning": "Silent Payments Scanning", "silent_payments_settings": "Silent Payments settings", + "single_seed_wallets_group": "Single Seed Wallets", "slidable": "Slidable", "sort_by": "Sort by", "spend_key_private": "Spend key (private)", @@ -704,12 +741,13 @@ "switchToEVMCompatibleWallet": "Please switch to an EVM compatible wallet and try again (Ethereum, Polygon)", "symbol": "Symbol", "sync_all_wallets": "Sync all wallets", + "sync_status_attempting_scan": "ATTEMPTING SCAN", "sync_status_attempting_sync": "ATTEMPTING SYNC", "sync_status_connected": "CONNECTED", "sync_status_connecting": "CONNECTING", "sync_status_failed_connect": "DISCONNECTED", "sync_status_not_connected": "NOT CONNECTED", - "sync_status_starting_scan": "STARTING SCAN", + "sync_status_starting_scan": "STARTING SCAN (from ${height})", "sync_status_starting_sync": "STARTING SYNC", "sync_status_syncronized": "SYNCHRONIZED", "sync_status_syncronizing": "SYNCHRONIZING", @@ -796,6 +834,7 @@ "trusted": "Trusted", "tx_commit_exception_no_dust_on_change": "The transaction is rejected with this amount. With these coins you can send ${min} without change or ${max} that returns change.", "tx_commit_failed": "Transaction commit failed. Please contact support.", + "tx_commit_failed_no_peers": "Transaction failed to broadcast, please try again in a second or so", "tx_invalid_input": "You are using the wrong input type for this type of payment", "tx_no_dust_exception": "The transaction is rejected by sending an amount too small. Please try increasing the amount.", "tx_not_enough_inputs_exception": "Not enough inputs available. Please select more under Coin Control", @@ -840,8 +879,16 @@ "view_transaction_on": "View Transaction on ", "voting_weight": "Voting Weight", "waitFewSecondForTxUpdate": "Kindly wait for a few seconds for transaction to reflect in transactions history", + "wallet_group": "Wallet Group", + "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_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_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", "wallet_list_create_new_wallet": "Create New Wallet", + "wallet_list_edit_group_name": "Edit Group Name", "wallet_list_edit_wallet": "Edit wallet", "wallet_list_failed_to_load": "Failed to load ${wallet_name} wallet. ${error}", "wallet_list_failed_to_remove": "Failed to remove ${wallet_name} wallet. ${error}", @@ -865,7 +912,7 @@ "warning": "Warning", "welcome": "Welcome to", "welcome_to_cakepay": "Welcome to Cake Pay!", - "what_is_silent_payments": "What is silent payments?", + "what_is_silent_payments": "What are Silent Payments?", "widgets_address": "Address", "widgets_or": "or", "widgets_restore_from_blockheight": "Restore from blockheight", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index 02d17ac96..449557671 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -36,6 +36,7 @@ "agree": "De acuerdo", "agree_and_continue": "Aceptar y continuar", "agree_to": "Al crear una cuenta, aceptas ", + "alert_notice": "Aviso", "all": "TODOS", "all_trades": "Todos los oficios", "all_transactions": "Todas las transacciones", @@ -77,7 +78,7 @@ "billing_address_info": "Si se le solicita una dirección de facturación, proporcione su dirección de envío", "biometric_auth_reason": "Escanee su huella digital para autenticar", "bitcoin_dark_theme": "Tema oscuro de Bitcoin", - "bitcoin_light_theme": "Tema de la luz 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 su paciencia! Se le enviará un correo electrónico cuando se confirme el pago.", "block_remaining": "1 bloqueo restante", "Blocks_remaining": "${status} Bloques restantes", @@ -92,7 +93,7 @@ "buy_with": "Compra con", "by_cake_pay": "por Cake Pay", "cake_2fa_preset": "Pastel 2FA preestablecido", - "cake_dark_theme": "Tema oscuro del pastel", + "cake_dark_theme": "Tema oscuro", "cake_pay_account_note": "Regístrese con solo una dirección de correo electrónico para ver y comprar tarjetas. ¡Algunas incluso están disponibles con descuento!", "cake_pay_learn_more": "¡Compre y canjee tarjetas de regalo al instante en la aplicación!\nDeslice el dedo de izquierda a derecha para obtener más información.", "cake_pay_save_order": "La tarjeta debe enviarse a su correo electrónico dentro de 1 día hábil \n Guardar su ID de pedido:", @@ -130,6 +131,7 @@ "choose_one": "Elige uno", "choose_relay": "Por favor elija un relé para usar", "choose_wallet_currency": "Por favor, elija la moneda de la billetera:", + "choose_wallet_group": "Elija el grupo de billetera", "clear": "Claro", "clearnet_link": "enlace Clearnet", "close": "Cerca", @@ -142,8 +144,9 @@ "confirm_delete_wallet": "Esta acción eliminará esta billetera. ¿Desea continuar?", "confirm_fee_deduction": "Confirmar la deducción de la tarifa", "confirm_fee_deduction_content": "¿Acepta deducir la tarifa de la producción?", + "confirm_passphrase": "Confirmar la contraseña", "confirm_sending": "Confirmar envío", - "confirm_silent_payments_switch_node": "Su nodo actual no admite pagos silenciosos \\ ncake billet cambiará a un nodo compatible, solo para escanear", + "confirm_silent_payments_switch_node": "Su nodo actual no admite pagos silenciosos \\ nCake cambiará a un nodo compatible, solo para escanear", "confirmations": "Confirmaciones", "confirmed": "Saldo confirmado", "confirmed_tx": "Confirmado", @@ -160,6 +163,7 @@ "contact_name": "Nombre de contacto", "contact_support": "Contactar con Soporte", "continue_text": "Continuar", + "contract_warning": "Esta dirección de contrato ha sido marcada como potencialmente fraudulenta. Por favor, procese con precaución.", "contractName": "Nombre del contrato", "contractSymbol": "Símbolo de contrato", "copied_key_to_clipboard": "Copiado ${key} al portapapeles", @@ -175,6 +179,7 @@ "create_invoice": "Crear factura", "create_new": "Crear nueva billetera", "create_new_account": "Crear una nueva cuenta", + "create_new_seed": "Crear nueva semilla", "creating_new_wallet": "Creando nueva billetera", "creating_new_wallet_error": "Error: ${description}", "creation_date": "Fecha de creación", @@ -195,6 +200,7 @@ "delete_wallet_confirm_message": "¿Está seguro de que desea eliminar la billetera ${wallet_name}?", "deleteConnectionConfirmationPrompt": "¿Está seguro de que desea eliminar la conexión a", "denominations": "Denominaciones", + "derivationpath": "Ruta de derivación", "descending": "Descendente", "description": "Descripción", "destination_tag": "Etiqueta de destino:", @@ -207,6 +213,7 @@ "disable_buy": "Desactivar acción de compra", "disable_cake_2fa": "Desactivar pastel 2FA", "disable_exchange": "Deshabilitar intercambio", + "disable_fee_api_warning": "Al apagar esto, las tasas de tarifas pueden ser inexactas en algunos casos, por lo que puede terminar pagando en exceso o pagando menos las tarifas por sus transacciones", "disable_fiat": "Deshabilitar fiat", "disable_sell": "Desactivar acción de venta", "disableBatteryOptimization": "Deshabilitar la optimización de la batería", @@ -228,8 +235,10 @@ "edit_token": "Editar token", "electrum_address_disclaimer": "Generamos nuevas direcciones cada vez que usa una, pero las direcciones anteriores siguen funcionando", "email_address": "Dirección de correo electrónico", + "enable": "Permitir", + "enable_mempool_api": "API de Mempool para tarifas y fechas precisas", "enable_replace_by_fee": "Habilitar reemplazar por tarea", - "enable_silent_payments_scanning": "Habilitar escaneo de pagos silenciosos", + "enable_silent_payments_scanning": "Comience a escanear pagos silenciosos, hasta que se alcance la punta", "enabled": "Activado", "enter_amount": "Ingrese la cantidad", "enter_backup_password": "Ingrese la contraseña de respaldo aquí", @@ -273,7 +282,7 @@ "event": "Evento", "events": "Eventos", "exchange": "Intercambiar", - "exchange_incorrect_current_wallet_for_xmr": "Si desea intercambiar XMR de su saldo de Cake Wallet Monero, primero cambie a su billetera Monero.", + "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!", "exchange_result_confirm": "Al presionar confirmar, enviará ${fetchingLabel} ${from} desde su billetera llamada ${walletName} a la dirección que se muestra a continuación. O puede enviar desde su billetera externa a la siguiente dirección / código QR anterior.\n\nPresione confirmar para continuar o regrese para cambiar los montos.", @@ -291,6 +300,8 @@ "failed_authentication": "Autenticación fallida. ${state_error}", "faq": "FAQ", "features": "Características", + "fee_less_than_min": "La tarifa seleccionada es menor que la mínima, aumente las tarifas para poder enviar la transacción", + "fee_rate": "Tarifa", "fetching": "Cargando", "fiat_api": "Fiat API", "fiat_balance": "Equilibrio Fiat", @@ -298,7 +309,7 @@ "fill_code": "Por favor complete el código de verificación proporcionado a su correo electrónico", "filter_by": "Filtrado por", "first_wallet_text": "Impresionante billetera para Monero, Bitcoin, Ethereum, Litecoin, y Haven", - "fixed_pair_not_supported": "Este par fijo no es compatible con los intercambios seleccionados", + "fixed_pair_not_supported": "Este par fijo no es compatible con los servicios de intercambio seleccionados", "fixed_rate": "Tipo de interés fijo", "fixed_rate_alert": "Podrá ingresar la cantidad recibida cuando el modo de tarifa fija esté marcado. ¿Quieres cambiar al modo de tarifa fija?", "forgot_password": "Olvidé mi contraseña", @@ -354,6 +365,20 @@ "ledger_error_wrong_app": "Por favor, asegúrese de abrir la aplicación correcta en su libro mayor.", "ledger_please_enable_bluetooth": "Habilite Bluetooth para detectar su libro mayor", "light_theme": "Ligera", + "litecoin_enable_mweb_sync": "Habilitar el escaneo mweb", + "litecoin_mweb": "Mweb", + "litecoin_mweb_always_scan": "Establecer mweb siempre escaneo", + "litecoin_mweb_description": "Mweb es un nuevo protocolo que trae transacciones más rápidas, más baratas y más privadas a Litecoin", + "litecoin_mweb_dismiss": "Despedir", + "litecoin_mweb_display_card": "Mostrar tarjeta MWEB", + "litecoin_mweb_enable_later": "Puede elegir habilitar MWEB nuevamente en la configuración de visualización.", + "litecoin_mweb_pegin": "Convertir", + "litecoin_mweb_pegout": "Recuperar", + "litecoin_mweb_scanning": "Escaneo mweb", + "litecoin_mweb_settings": "Configuración de MWEB", + "litecoin_mweb_warning": "El uso de MWEB inicialmente descargará ~ 600 MB de datos, y puede tomar hasta 30 minutos según la velocidad de la red. Estos datos iniciales solo se descargarán una vez y estarán disponibles para todas las billeteras de Litecoin", + "litecoin_what_is_mweb": "¿Qué es mweb?", + "live_fee_rates": "Tasas de tarifas en vivo a través de API", "load_more": "Carga más", "loading_your_wallet": "Cargando tu billetera", "login": "Iniciar sesión", @@ -383,11 +408,13 @@ "monero_light_theme": "Tema ligero de Monero", "moonpay_alert_text": "El valor de la cantidad debe ser mayor o igual a ${minAmount} ${fiatCurrency}", "more_options": "Más Opciones", + "mweb_confirmed": "Confirmado mweb", + "mweb_unconfirmed": "Mweb no confirmado", "name": "Nombre", "nano_current_rep": "Representante actual", "nano_gpt_thanks_message": "¡Gracias por usar nanogpt! ¡Recuerde regresar al navegador después de que se complete su transacción!", "nano_pick_new_rep": "Elija un nuevo representante", - "nanogpt_subtitle": "Todos los modelos más nuevos (GPT-4, Claude). \\ Nno suscripción, pague con cripto.", + "nanogpt_subtitle": "Todos los modelos más nuevos (GPT-4, Claude). \\nSin suscripción, pague con cripto.", "narrow": "Angosto", "new_first_wallet_text": "Mantenga fácilmente su criptomoneda segura", "new_node_testing": "Prueba de nuevos nodos", @@ -412,7 +439,7 @@ "node_test": "Prueba", "nodes": "Nodos", "nodes_list_reset_to_default_message": "¿Está seguro de que desea restablecer la configuración predeterminada?", - "none_of_selected_providers_can_exchange": "Ninguno de los proveedores seleccionados puede realizar este intercambio", + "none_of_selected_providers_can_exchange": "Ninguno de los proveedores seleccionados puede hacer este intercambio", "noNFTYet": "Aún no hay NFT", "normal": "Normal", "note_optional": "Nota (opcional)", @@ -442,6 +469,7 @@ "overwrite_amount": "Overwrite amount", "pairingInvalidEvent": "Evento de emparejamiento no válido", "passphrase": "Passfrase (opcional)", + "passphrases_doesnt_match": "Las frases de contrato no coinciden, intente nuevamente", "password": "Contraseña", "paste": "Pegar", "pause_wallet_creation": "La capacidad para crear Haven Wallet está actualmente pausada.", @@ -594,8 +622,11 @@ "seed_share": "Compartir semillas", "seed_title": "Semilla", "seedtype": "Type de semillas", + "seedtype_alert_content": "Compartir semillas con otras billeteras solo es posible con Bip39 Seed Type.", + "seedtype_alert_title": "Alerta de type de semillas", "seedtype_legacy": "Legado (25 palabras)", "seedtype_polyseed": "Polieta (16 palabras)", + "seedtype_wownero": "Wownero (14 palabras)", "select_backup_file": "Seleccionar archivo de respaldo", "select_buy_provider_notice": "Seleccione 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": "Seleccione el destino del archivo de copia de seguridad.", @@ -606,6 +637,7 @@ "send": "Enviar", "send_address": "Dirección de ${cryptoCurrency}", "send_amount": "Cantidad:", + "send_change_to_you": "Cambiar, a ti:", "send_creating_transaction": "Creando transacción", "send_error_currency": "La moneda solo puede contener números", "send_error_minimum_value": "El valor mínimo de la cantidad es 0.01", @@ -659,19 +691,21 @@ "setup_your_debit_card": "Configura tu tarjeta de débito", "share": "Compartir", "share_address": "Compartir dirección", + "shared_seed_wallet_groups": "Grupos de billetera de semillas compartidas", "show_details": "Mostrar detalles", "show_keys": "Mostrar semilla/claves", "show_market_place": "Mostrar mercado", "show_seed": "Mostrar semilla", "sign_message": "Mensaje de firma", "sign_up": "Registrarse", - "sign_verify_message": "Firmar o verificar el mensaje", + "sign_verify_message": "Firmar / verificar", "sign_verify_message_sub": "Firmar o verificar un mensaje usando su clave privada", "sign_verify_title": "Firmar / verificar", "signature": "Firma", "signature_invalid_error": "La firma no es válida para el mensaje dado", "signTransaction": "Firmar transacción", "signup_for_card_accept_terms": "Regístrese para obtener la tarjeta y acepte los términos.", + "silent_payment": "Pago silencioso", "silent_payments": "Pagos silenciosos", "silent_payments_always_scan": "Establecer pagos silenciosos siempre escaneando", "silent_payments_disclaimer": "Las nuevas direcciones no son nuevas identidades. Es una reutilización de una identidad existente con una etiqueta diferente.", @@ -682,6 +716,7 @@ "silent_payments_scanned_tip": "Escaneado hasta la punta! (${tip})", "silent_payments_scanning": "Escaneo de pagos silenciosos", "silent_payments_settings": "Configuración de pagos silenciosos", + "single_seed_wallets_group": "Billeteras de semillas individuales", "slidable": "deslizable", "sort_by": "Ordenar por", "spend_key_private": "Spend clave (privado)", @@ -704,12 +739,13 @@ "switchToEVMCompatibleWallet": "Cambie a una billetera compatible con EVM e inténtelo nuevamente (Ethereum, Polygon)", "symbol": "Símbolo", "sync_all_wallets": "Sincronizar todas las billeteras", + "sync_status_attempting_scan": "Intento de escaneo", "sync_status_attempting_sync": "INTENTAR SINCRONIZAR", "sync_status_connected": "CONECTADO", "sync_status_connecting": "CONECTANDO", "sync_status_failed_connect": "DESCONECTADO", "sync_status_not_connected": "NO CONECTADO", - "sync_status_starting_scan": "Escaneo inicial", + "sync_status_starting_scan": "Iniciar escaneo (de ${height})", "sync_status_starting_sync": "EMPEZANDO A SINCRONIZAR", "sync_status_syncronized": "SINCRONIZADO", "sync_status_syncronizing": "SINCRONIZANDO", @@ -790,12 +826,13 @@ "transaction_priority_slow": "Lento", "transaction_sent": "Transacción enviada!", "transaction_sent_notice": "Si la pantalla no continúa después de 1 minuto, revisa un explorador de bloques y tu correo electrónico.", - "transactions": "Actas", + "transactions": "Transacciones", "transactions_by_date": "Transacciones por fecha", "trongrid_history": "Historia trongrid", "trusted": "de confianza", "tx_commit_exception_no_dust_on_change": "La transacción se rechaza con esta cantidad. Con estas monedas puede enviar ${min} sin cambios o ${max} que devuelve el cambio.", "tx_commit_failed": "La confirmación de transacción falló. Póngase en contacto con el soporte.", + "tx_commit_failed_no_peers": "La transacción no se transmitió, intente nuevamente en un segundo más o menos", "tx_invalid_input": "Está utilizando el tipo de entrada incorrecto para este tipo de pago", "tx_no_dust_exception": "La transacción se rechaza enviando una cantidad demasiado pequeña. Intente aumentar la cantidad.", "tx_not_enough_inputs_exception": "No hay suficientes entradas disponibles. Seleccione más bajo control de monedas", @@ -840,8 +877,16 @@ "view_transaction_on": "View Transaction on ", "voting_weight": "Peso de votación", "waitFewSecondForTxUpdate": "Espere unos segundos para que la transacción se refleje en el historial de transacciones.", + "wallet_group": "Grupo de billetera", + "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_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 puede seleccionar", + "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", "wallet_list_create_new_wallet": "Crear nueva billetera", + "wallet_list_edit_group_name": "Editar nombre de grupo", "wallet_list_edit_wallet": "Editar billetera", "wallet_list_failed_to_load": "No se pudo cargar ${wallet_name} la billetera. ${error}", "wallet_list_failed_to_remove": "Error al elimina ${wallet_name} billetera. ${error}", @@ -894,4 +939,4 @@ "you_will_get": "Convertir a", "you_will_send": "Convertir de", "yy": "YY" -} \ No newline at end of file +} diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index 4bc5c9809..be5b48dd8 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -36,6 +36,7 @@ "agree": "d'accord", "agree_and_continue": "Accepter et continuer", "agree_to": "En créant un compte, vous acceptez les ", + "alert_notice": "Avis", "all": "TOUT", "all_trades": "Tous échanges", "all_transactions": "Toutes transactions", @@ -113,7 +114,7 @@ "change_currency": "Changer de Devise", "change_current_node": "Êtes vous certain de vouloir changer le nœud actuel pour ${node} ?", "change_current_node_title": "Changer le nœud actuel", - "change_exchange_provider": "Changer de Plateforme d'Échange", + "change_exchange_provider": "Changer le fournisseur d'échange", "change_language": "Changer de langue", "change_language_to": "Changer la langue vers ${language} ?", "change_password": "Changer le mot de passe", @@ -130,6 +131,7 @@ "choose_one": "Choisissez-en un", "choose_relay": "Veuillez choisir un relais à utiliser", "choose_wallet_currency": "Merci de choisir la devise du portefeuille (wallet) :", + "choose_wallet_group": "Choisissez un groupe de portefeuille", "clear": "Effacer", "clearnet_link": "Lien Clearnet", "close": "Fermer", @@ -142,6 +144,7 @@ "confirm_delete_wallet": "Cette action va supprimer ce portefeuille (wallet). Souhaitez-vous contnuer ?", "confirm_fee_deduction": "Confirmer la déduction des frais", "confirm_fee_deduction_content": "Acceptez-vous de déduire les frais de la production?", + "confirm_passphrase": "Confirmer la phrase passante", "confirm_sending": "Confirmer l'envoi", "confirm_silent_payments_switch_node": "Votre nœud actuel ne prend pas en charge les paiements silencieux \\ ncake qui passera à un nœud compatible, juste pour la numérisation", "confirmations": "Confirmations", @@ -160,6 +163,7 @@ "contact_name": "Nom de Contact", "contact_support": "Contacter l'assistance", "continue_text": "Continuer", + "contract_warning": "Cette adresse contractuelle a été signalée comme potentiellement frauduleuse. Veuillez traiter avec prudence.", "contractName": "Nom du contrat", "contractSymbol": "Symbole du contrat", "copied_key_to_clipboard": "${key} copiée vers le presse-papier", @@ -175,6 +179,7 @@ "create_invoice": "Créer une facture", "create_new": "Créer un Nouveau Portefeuille (Wallet)", "create_new_account": "Créer un nouveau compte", + "create_new_seed": "Créer de nouvelles graines", "creating_new_wallet": "Création d'un nouveau portefeuille (wallet)", "creating_new_wallet_error": "Erreur : ${description}", "creation_date": "Date de création", @@ -195,6 +200,7 @@ "delete_wallet_confirm_message": "Êtes-vous sûr de vouloir supprimer le portefeuille (wallet) ${wallet_name}?", "deleteConnectionConfirmationPrompt": "Êtes-vous sûr de vouloir supprimer la connexion à", "denominations": "Dénominations", + "derivationpath": "Chemin de dérivation", "descending": "Descendant", "description": "Description", "destination_tag": "Tag de destination :", @@ -207,6 +213,7 @@ "disable_buy": "Désactiver l'action d'achat", "disable_cake_2fa": "Désactiver Cake 2FA", "disable_exchange": "Désactiver l'échange", + "disable_fee_api_warning": "En désactivant cela, les taux de frais peuvent être inexacts dans certains cas, vous pourriez donc finir par payer trop ou sous-paiement les frais pour vos transactions", "disable_fiat": "Désactiver les montants en fiat", "disable_sell": "Désactiver l'action de vente", "disableBatteryOptimization": "Désactiver l'optimisation de la batterie", @@ -228,8 +235,10 @@ "edit_token": "Modifier le token", "electrum_address_disclaimer": "Nous générons de nouvelles adresses à chaque fois que vous en utilisez une, mais les adresses précédentes continuent à fonctionner", "email_address": "Adresse e-mail", + "enable": "Activer", + "enable_mempool_api": "API Mempool pour les frais et dates précis", "enable_replace_by_fee": "Activer Remplace-by-Fee", - "enable_silent_payments_scanning": "Activer la numérisation des paiements silencieux", + "enable_silent_payments_scanning": "Commencez à scanner les paiements silencieux, jusqu'à ce que la pointe soit atteinte", "enabled": "Activé", "enter_amount": "Entrez le montant", "enter_backup_password": "Entrez le mot de passe de sauvegarde ici", @@ -273,7 +282,7 @@ "event": "Événement", "events": "Événements", "exchange": "Échanger", - "exchange_incorrect_current_wallet_for_xmr": "Si vous souhaitez échanger des XMR du solde Monero de votre Cake Wallet, merci de sélectionner votre portefeuille (wallet) Monero au préalable.", + "exchange_incorrect_current_wallet_for_xmr": "Si vous souhaitez échanger XMR à partir de votre balance monero portefeuille de gâteau, 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 !", "exchange_result_confirm": "En pressant confirmer, vous enverrez ${fetchingLabel} ${from} depuis votre portefeuille (wallet) nommé ${walletName} vers l'adresse ci-dessous. Vous pouvez aussi envoyer depuis votre portefeuille externe vers l'adresse/QR code ci-dessous.\n\nMerci d'appuyer sur confirmer pour continuer ou retournez en arrière pour modifier les montants.", @@ -291,6 +300,8 @@ "failed_authentication": "Échec d'authentification. ${state_error}", "faq": "FAQ", "features": "Caractéristiques", + "fee_less_than_min": "Les frais sélectionnés sont inférieurs au minimum, veuillez augmenter les frais pour pouvoir envoyer la transaction", + "fee_rate": "Taux de frais", "fetching": "Récupération", "fiat_api": "Fiat API", "fiat_balance": "Solde fiat", @@ -298,7 +309,7 @@ "fill_code": "Veuillez remplir le code de vérification fourni sur votre e-mail", "filter_by": "Filtrer par", "first_wallet_text": "Super portefeuille (wallet) pour Monero, Bitcoin, Ethereum, Litecoin et Haven", - "fixed_pair_not_supported": "Cette paire fixe n'est pas prise en charge avec les échanges sélectionnés", + "fixed_pair_not_supported": "Cette paire fixe n'est pas prise en charge avec les services d'échange sélectionnés", "fixed_rate": "Taux fixe", "fixed_rate_alert": "Vous aurez la possibilité de rentrer le montant reçu lorsque le mode taux fixe est sélectionné. Souhaitez vous basculer en mode taux fixe ?", "forgot_password": "Mot de passe oublié", @@ -354,6 +365,20 @@ "ledger_error_wrong_app": "Veuillez vous assurer d'ouvrir la bonne application sur votre grand livre", "ledger_please_enable_bluetooth": "Veuillez activer Bluetooth pour détecter votre grand livre", "light_theme": "Clair", + "litecoin_enable_mweb_sync": "Activer la numérisation MWEB", + "litecoin_mweb": "Mweb", + "litecoin_mweb_always_scan": "Définir MWEB Score Scanning", + "litecoin_mweb_description": "MWEB est un nouveau protocole qui apporte des transactions plus rapides, moins chères et plus privées à Litecoin", + "litecoin_mweb_dismiss": "Rejeter", + "litecoin_mweb_display_card": "Afficher la carte MWeb", + "litecoin_mweb_enable_later": "Vous pouvez choisir d'activer à nouveau MWEB sous Paramètres d'affichage.", + "litecoin_mweb_pegin": "Entraver", + "litecoin_mweb_pegout": "Crever", + "litecoin_mweb_scanning": "Scann mweb", + "litecoin_mweb_settings": "Paramètres MWEB", + "litecoin_mweb_warning": "L'utilisation de MWEB téléchargera initialement ~ 600 Mo de données et peut prendre jusqu'à 30 minutes en fonction de la vitesse du réseau. Ces données initiales ne téléchargeront qu'une seule fois et seront disponibles pour tous les portefeuilles litecoin", + "litecoin_what_is_mweb": "Qu'est-ce que MWEB?", + "live_fee_rates": "Taux de frais en direct via l'API", "load_more": "Charger plus", "loading_your_wallet": "Chargement de votre portefeuille (wallet)", "login": "Utilisateur", @@ -383,6 +408,8 @@ "monero_light_theme": "Thème de lumière Monero", "moonpay_alert_text": "Le montant doit être au moins égal à ${minAmount} ${fiatCurrency}", "more_options": "Plus d'options", + "mweb_confirmed": "Confirmé MWEB", + "mweb_unconfirmed": "Mweb non confirmé", "name": "Nom", "nano_current_rep": "Représentant actuel", "nano_gpt_thanks_message": "Merci d'avoir utilisé Nanogpt! N'oubliez pas de retourner au navigateur une fois votre transaction terminée!", @@ -412,7 +439,7 @@ "node_test": "Tester", "nodes": "Nœuds", "nodes_list_reset_to_default_message": "Êtes vous certain de vouloir revenir aux réglages par défaut ?", - "none_of_selected_providers_can_exchange": "Aucun des fournisseurs sélectionnés ne peut effectuer cet échange", + "none_of_selected_providers_can_exchange": "Aucun des fournisseurs sélectionnés ne peut faire cet échange", "noNFTYet": "Pas encore de NFT", "normal": "Normal", "note_optional": "Note (optionnelle)", @@ -442,6 +469,7 @@ "overwrite_amount": "Remplacer le montant", "pairingInvalidEvent": "Événement de couplage non valide", "passphrase": "Phrase de passe (facultative)", + "passphrases_doesnt_match": "Les phrases de passe ne correspondent pas, veuillez réessayer", "password": "Mot de passe", "paste": "Coller", "pause_wallet_creation": "La possibilité de créer Haven Wallet est actuellement suspendue.", @@ -593,8 +621,11 @@ "seed_share": "Partager la phrase secrète (seed)", "seed_title": "Phrase secrète (seed)", "seedtype": "Type de type graine", + "seedtype_alert_content": "Le partage de graines avec d'autres portefeuilles n'est possible qu'avec Bip39 SeedType.", + "seedtype_alert_title": "Alerte de type SeedType", "seedtype_legacy": "Héritage (25 mots)", "seedtype_polyseed": "Polyseed (16 mots)", + "seedtype_wownero": "WOWNERO (14 mots)", "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.", @@ -605,6 +636,7 @@ "send": "Envoyer", "send_address": "adresse ${cryptoCurrency}", "send_amount": "Montant :", + "send_change_to_you": "Changer, pour vous:", "send_creating_transaction": "Création de la transaction", "send_error_currency": "La monnaie ne peut contenir que des nombres", "send_error_minimum_value": "La valeur minimale du montant est 0.01", @@ -658,19 +690,21 @@ "setup_your_debit_card": "Configurer votre carte de débit", "share": "Partager", "share_address": "Partager l'adresse", + "shared_seed_wallet_groups": "Groupes de portefeuilles partagés", "show_details": "Afficher les détails", "show_keys": "Visualiser la phrase secrète (seed) et les clefs", "show_market_place": "Afficher la place de marché", "show_seed": "Visualiser la phrase secrète (seed)", "sign_message": "Signer le message", "sign_up": "S'inscrire", - "sign_verify_message": "Signer ou vérifier le message", + "sign_verify_message": "Signe / vérifier", "sign_verify_message_sub": "Signez ou vérifiez un message en utilisant votre clé privée", "sign_verify_title": "Signe / vérifier", "signature": "Signature", "signature_invalid_error": "La signature n'est pas valable pour le message donné", "signTransaction": "Signer une transaction", "signup_for_card_accept_terms": "Inscrivez-vous pour la carte et acceptez les conditions.", + "silent_payment": "Paiement silencieux", "silent_payments": "Paiements silencieux", "silent_payments_always_scan": "Définir les paiements silencieux toujours à la scanne", "silent_payments_disclaimer": "Les nouvelles adresses ne sont pas de nouvelles identités. Il s'agit d'une réutilisation d'une identité existante avec une étiquette différente.", @@ -681,6 +715,7 @@ "silent_payments_scanned_tip": "Scanné à la pointe! (${tip})", "silent_payments_scanning": "Payments silencieux SCANNING", "silent_payments_settings": "Paramètres de paiement silencieux", + "single_seed_wallets_group": "Portefeuilles de semences simples", "slidable": "Glissable", "sort_by": "Trier par", "spend_key_private": "Clef de dépense (spend key) (privée)", @@ -703,12 +738,13 @@ "switchToEVMCompatibleWallet": "Veuillez passer à un portefeuille compatible EVM et réessayer (Ethereum, Polygon)", "symbol": "Symbole", "sync_all_wallets": "Synchroniser tous les portefeuilles", + "sync_status_attempting_scan": "Tentative de numérisation", "sync_status_attempting_sync": "TENTATIVE DE SYNCHRONISATION", "sync_status_connected": "CONNECTÉ", "sync_status_connecting": "CONNEXION EN COURS", "sync_status_failed_connect": "DÉCONNECTÉ", "sync_status_not_connected": "NON CONNECTÉ", - "sync_status_starting_scan": "Démarrage", + "sync_status_starting_scan": "Démarrer la numérisation (à partir de ${height})", "sync_status_starting_sync": "DÉBUT DE SYNCHRO", "sync_status_syncronized": "SYNCHRONISÉ", "sync_status_syncronizing": "SYNCHRONISATION EN COURS", @@ -795,6 +831,7 @@ "trusted": "de confiance", "tx_commit_exception_no_dust_on_change": "La transaction est rejetée avec ce montant. Avec ces pièces, vous pouvez envoyer ${min} sans changement ou ${max} qui renvoie le changement.", "tx_commit_failed": "La validation de la transaction a échoué. Veuillez contacter l'assistance.", + "tx_commit_failed_no_peers": "La transaction n'a pas été diffusée, veuillez réessayer dans une seconde environ", "tx_invalid_input": "Vous utilisez le mauvais type d'entrée pour ce type de paiement", "tx_no_dust_exception": "La transaction est rejetée en envoyant un montant trop faible. Veuillez essayer d'augmenter le montant.", "tx_not_enough_inputs_exception": "Pas assez d'entrées disponibles. Veuillez sélectionner plus sous Control Control", @@ -839,8 +876,16 @@ "view_transaction_on": "Voir la Transaction sur ", "voting_weight": "Poids de vote", "waitFewSecondForTxUpdate": "Veuillez attendre quelques secondes pour que la transaction soit reflétée dans l'historique des transactions.", + "wallet_group": "Groupe de portefeuille", + "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_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_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)", "wallet_list_create_new_wallet": "Créer un Nouveau Portefeuille (Wallet)", + "wallet_list_edit_group_name": "Modifier le nom du groupe", "wallet_list_edit_wallet": "Modifier le portefeuille", "wallet_list_failed_to_load": "Échec de chargement du portefeuille (wallet) ${wallet_name}. ${error}", "wallet_list_failed_to_remove": "Échec de la suppression du portefeuille (wallet) ${wallet_name}. ${error}", diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index 025a33f6b..4deb0df1d 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -36,6 +36,7 @@ "agree": "Yarda", "agree_and_continue": "Amincewa & Ci gaba", "agree_to": "Ta hanyar ƙirƙirar asusu kun yarda da", + "alert_notice": "Sanarwa", "all": "DUK", "all_trades": "Duk ciniki", "all_transactions": "Dukan Ma'amaloli", @@ -113,7 +114,7 @@ "change_currency": "Canja Kuɗi", "change_current_node": "Kuna tabbatar kuna so ku canja node yanzu zuwa ${node}?", "change_current_node_title": "Canja node yanzu", - "change_exchange_provider": "Canza Mai Bayar da Musanya", + "change_exchange_provider": "Canza mai canzawa", "change_language": "canja harshen", "change_language_to": "canja harshen zuwa ${language}?", "change_password": "Canza kalmar shiga", @@ -130,6 +131,7 @@ "choose_one": "Zaɓi ɗaya", "choose_relay": "Da fatan za a zaɓi gudun ba da sanda don amfani", "choose_wallet_currency": "Da fatan za a zaɓi kuɗin walat:", + "choose_wallet_group": "Zabi kungiyar walat", "clear": "Share", "clearnet_link": "Lambar makomar kwayoyi", "close": "Rufa", @@ -142,6 +144,7 @@ "confirm_delete_wallet": "Wannan aikin zai share wannan walat. Kuna so ku ci gaba?", "confirm_fee_deduction": "Tabbatar da cire kudade", "confirm_fee_deduction_content": "Shin kun yarda ku cire kuɗin daga fitarwa?", + "confirm_passphrase": "Tabbatar da kalmar wucewa", "confirm_sending": "Tabbatar da aikawa", "confirm_silent_payments_switch_node": "Kumburinku na yanzu ba ya goyan bayan biyan shiru da shiru \\ NCADA Wallet zai canza zuwa kumburi mai dacewa, don bincika", "confirmations": "Tabbatar", @@ -160,6 +163,7 @@ "contact_name": "Sunan Tuntuɓi", "contact_support": "Tuntuɓi Support", "continue_text": "Ci gaba", + "contract_warning": "An kafa wannan adireshin kwantaragin kwangilar yayin da yuwuwar zamba. Da fatan za a aiwatar da taka tsantsan.", "contractName": "Sunan Kwangila", "contractSymbol": "Alamar Kwangila", "copied_key_to_clipboard": "An kwafa ${key} a cikin kwafin", @@ -175,6 +179,7 @@ "create_invoice": "Sanya bayanin wadannan", "create_new": "Ƙirƙira Sabon Kwalinku", "create_new_account": "Ƙirƙiri sabon asusu", + "create_new_seed": "Irƙiri sabon iri", "creating_new_wallet": "Haliccin walat sabuwa", "creating_new_wallet_error": "Kuskure: ${description}", "creation_date": "Ranar halitta", @@ -195,6 +200,7 @@ "delete_wallet_confirm_message": "Shin kun tabbata cewa kuna son share jakar ${wallet_name}?", "deleteConnectionConfirmationPrompt": "Shin kun tabbata cewa kuna son share haɗin zuwa", "denominations": "Denominations", + "derivationpath": "Hanyar Nasara", "descending": "Saukowa", "description": "Bayani", "destination_tag": "Tambarin makoma:", @@ -207,6 +213,7 @@ "disable_buy": "Kashe alama", "disable_cake_2fa": "Musaki Cake 2FA", "disable_exchange": "Kashe musanya", + "disable_fee_api_warning": "Ta hanyar juya wannan kashe, kudaden da zai iya zama ba daidai ba a wasu halaye, saboda haka zaku iya ƙare da overpaying ko a ƙarƙashin kudaden don ma'amaloli", "disable_fiat": "Dakatar da fiat", "disable_sell": "Kashe karbuwa", "disableBatteryOptimization": "Kashe ingantawa baturi", @@ -228,8 +235,10 @@ "edit_token": "Gyara alamar", "electrum_address_disclaimer": "Muna samar da sababbin adireshi duk lokacin da kuka yi amfani da ɗaya, amma adiresoshin da suka gabata suna ci gaba da aiki", "email_address": "Adireshin i-mel", + "enable": "Ba dama", + "enable_mempool_api": "Mampool API don ingantattun kudade da kwanakin", "enable_replace_by_fee": "Ba da damar maye gurbin-by-kudin", - "enable_silent_payments_scanning": "Kunna biya biya", + "enable_silent_payments_scanning": "Fara bincika biya na shiru, har sai tip ɗin ya kai", "enabled": "An kunna", "enter_amount": "Shigar da Adadi", "enter_backup_password": "Shigar da kalmar wucewa ta madadin nan", @@ -272,8 +281,8 @@ "etherscan_history": "Etherscan tarihin kowane zamani", "event": "Lamarin", "events": "Abubuwan da suka faru", - "exchange": "Exchange", - "exchange_incorrect_current_wallet_for_xmr": "Idan kana son musanya XMR daga ma'aunin Cake Wallet Monero, da fatan za a fara canza wallet ɗin Monero ɗin ku.", + "exchange": "Musya", + "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}", "exchange_result_confirm": "Ta danna tabbatarwa, zaku aika ${fetchingLabel} ${from} daga walat ɗin ku mai suna ${walletName} zuwa address dake kasa. Ko zaka iya aika daga kwalinku na external zuwa address/QR code dake kasa.\n\nDon Allah shigar da confirm don ci gaba ko dawo ka canja adadinku.", @@ -291,6 +300,8 @@ "failed_authentication": "Binne wajen shiga. ${state_error}", "faq": "FAQ", "features": "Fasas", + "fee_less_than_min": "Kudin da aka zaba kasa da mafi karancin, don Allah ƙara kudaden don samun damar aika ma'amala", + "fee_rate": "Kudi", "fetching": "Daukewa", "fiat_api": "API ɗin Fiat", "fiat_balance": "Fiat Balance", @@ -298,7 +309,7 @@ "fill_code": "Da fatan za a cika lambar tabbatarwa da aka bayar zuwa imel ɗin ku", "filter_by": "Tace ta", "first_wallet_text": "Aikace-aikacen e-wallet ga Monero, Bitcoin, Ethereum, Litecoin, da kuma Haven", - "fixed_pair_not_supported": "Wannan kafaffen guda biyu ba shi da tallafi tare da zaɓaɓɓun musayar", + "fixed_pair_not_supported": "Ba a tallafa wa wannan adireshin da aka zaɓi tare da zaɓin siye ba", "fixed_rate": "Kafaffen ƙima", "fixed_rate_alert": "Za ku iya shigar da adadin karɓa lokacin da aka duba ƙayyadadden zaɓin ƙimar kuɗi. Kuna so ku canza zuwa ƙayyadadden yanayin ƙimar kuɗi?", "forgot_password": "Manta Kalmar wucewa", @@ -354,6 +365,20 @@ "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", "light_theme": "Haske", + "litecoin_enable_mweb_sync": "Kunna binciken Mweb", + "litecoin_mweb": "Mweb", + "litecoin_mweb_always_scan": "Saita Mweb koyaushe", + "litecoin_mweb_description": "Mweb shine sabon tsarin yarjejeniya da ya kawo da sauri, mai rahusa, da kuma ma'amaloli masu zaman kansu zuwa Litecoin", + "litecoin_mweb_dismiss": "Tuɓe \\ sallama", + "litecoin_mweb_display_card": "Nuna katin Mweb", + "litecoin_mweb_enable_later": "Kuna iya zaɓar kunna Mweb kuma a ƙarƙashin saitunan nuni.", + "litecoin_mweb_pegin": "Peg in", + "litecoin_mweb_pegout": "Peg fita", + "litecoin_mweb_scanning": "Mweb scanning", + "litecoin_mweb_settings": "Saitunan Mweb", + "litecoin_mweb_warning": "Amfani da Mweb zai fara saukewa ~ 600MB na bayanai, kuma yana iya ɗaukar minti 30 dangane da saurin cibiyar sadarwa. Wannan bayanan farko zai saika saukarwa sau ɗaya kawai kuma a samu don duk wuraren shakatawa", + "litecoin_what_is_mweb": "Menene Mweb?", + "live_fee_rates": "Kudin Kiɗa ta API", "load_more": "Like more", "loading_your_wallet": "Ana loda walat ɗin ku", "login": "Shiga", @@ -383,6 +408,8 @@ "monero_light_theme": "Jigon Hasken Monero", "moonpay_alert_text": "Darajar adadin dole ne ya zama fiye ko daidai da ${minAmount} ${fiatCurrency}", "more_options": "Ƙarin Zaɓuɓɓuka", + "mweb_confirmed": "Tabbatar da Mweb", + "mweb_unconfirmed": "Myconfired", "name": "Suna", "nano_current_rep": "Wakilin Yanzu", "nano_gpt_thanks_message": "Na gode da amfani da Nanogpt! Ka tuna da komawa zuwa mai bincike bayan ma'amalar ka ta cika!", @@ -412,7 +439,7 @@ "node_test": "Gwaji", "nodes": "Nodes", "nodes_list_reset_to_default_message": "Kuna tabbatar kuna so ku sake saitunan zuwa default?", - "none_of_selected_providers_can_exchange": "Babu ɗaya daga cikin zaɓaɓɓun masu samarwa da zai iya yin wannan musayar", + "none_of_selected_providers_can_exchange": "Babu wani daga cikin masu siye da aka zaɓa na iya yin wannan musan", "noNFTYet": "Babu NFTs tukuna", "normal": "Na al'ada", "note_optional": "Bayani (optional)", @@ -444,6 +471,7 @@ "overwrite_amount": "Rubuta adadin", "pairingInvalidEvent": "Haɗa Lamarin mara inganci", "passphrase": "Passphrase (Zabi)", + "passphrases_doesnt_match": "Passphrases bai dace ba, don Allah sake gwadawa", "password": "Kalmar wucewa", "paste": "Manna", "pause_wallet_creation": "A halin yanzu an dakatar da ikon ƙirƙirar Haven Wallet.", @@ -595,8 +623,11 @@ "seed_share": "Raba iri", "seed_title": "iri", "seedtype": "Seedtype", + "seedtype_alert_content": "Raba tsaba tare da sauran wallets yana yiwuwa ne kawai tare da Bip39 seedtype.", + "seedtype_alert_title": "Seedtype farke", "seedtype_legacy": "Legacy (25 kalmomi)", "seedtype_polyseed": "Polyseed (16 kalmomi)", + "seedtype_wownero": "WowRero (kalmomi 14)", "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.", @@ -607,6 +638,7 @@ "send": "Aika", "send_address": "${cryptoCurrency} address", "send_amount": "Adadi:", + "send_change_to_you": "Canza, a gare ku:", "send_creating_transaction": "Ƙirƙirar ciniki", "send_error_currency": "Kudi zai iya ƙunsar lambobi kawai", "send_error_minimum_value": "Mafi ƙarancin ƙimar adadin shine 0.01", @@ -660,19 +692,21 @@ "setup_your_debit_card": "Saita katin zare kudi", "share": "Raba", "share_address": "Raba adireshin", + "shared_seed_wallet_groups": "Raba ƙungiya walat", "show_details": "Nuna Cikakkun bayanai", "show_keys": "Nuna iri/maɓallai", "show_market_place": "Nuna dan kasuwa", "show_seed": "Nuna iri", "sign_message": "Sa hannu", "sign_up": "Shiga", - "sign_verify_message": "Shiga ko Tabbatar da Saƙo", + "sign_verify_message": "Sa hannu / Tabbatar", "sign_verify_message_sub": "Shiga ko tabbatar da saƙo ta amfani da Maɓallinku na sirri", "sign_verify_title": "Sa hannu / Tabbatar", "signature": "Sa hannu", "signature_invalid_error": "Sa hannu ba shi da inganci ga sakon da aka bayar", "signTransaction": "Sa hannu Ma'amala", "signup_for_card_accept_terms": "Yi rajista don katin kuma karɓi sharuɗɗan.", + "silent_payment": "Biya silent", "silent_payments": "Biya silent", "silent_payments_always_scan": "Saita biya na shiru koyaushe", "silent_payments_disclaimer": "Sabbin adiresoshin ba sabon tsari bane. Wannan shine sake amfani da asalin asalin tare da wata alama daban.", @@ -683,6 +717,7 @@ "silent_payments_scanned_tip": "Bincika don tip! (${tip})", "silent_payments_scanning": "Silent biya scanning", "silent_payments_settings": "Saitunan Silent", + "single_seed_wallets_group": "Guaro", "slidable": "Mai iya zamewa", "sort_by": "Kasa", "spend_key_private": "makullin biya (maɓallin kalmar sirri)", @@ -705,12 +740,13 @@ "switchToEVMCompatibleWallet": "Da fatan za a canza zuwa walat ɗin EVM mai jituwa kuma a sake gwadawa (Ethereum, Polygon)", "symbol": "Alama", "sync_all_wallets": "Sync Duk Wallet", + "sync_status_attempting_scan": "Yunƙurin scan", "sync_status_attempting_sync": "KWAFI", "sync_status_connected": "HANNU", "sync_status_connecting": "HADA", "sync_status_failed_connect": "BABU INTERNET", "sync_status_not_connected": "BABU INTERNET", - "sync_status_starting_scan": "Fara scan", + "sync_status_starting_scan": "Farawa Scan (daga ${height})", "sync_status_starting_sync": "KWAFI", "sync_status_syncronized": "KYAU", "sync_status_syncronizing": "KWAFI", @@ -797,6 +833,7 @@ "trusted": "Amintacce", "tx_commit_exception_no_dust_on_change": "An ƙi ma'amala da wannan adadin. Tare da waɗannan tsabar kudi Zaka iya aika ${min}, ba tare da canji ba ko ${max} wanda ya dawo canzawa.", "tx_commit_failed": "Ma'amala ya kasa. Da fatan za a tuntuɓi goyan baya.", + "tx_commit_failed_no_peers": "Kasuwanci ya kasa watsa, don Allah sake gwadawa a cikin na biyu ko", "tx_invalid_input": "Kuna amfani da nau'in shigar da ba daidai ba don wannan nau'in biyan kuɗi", "tx_no_dust_exception": "An ƙi ma'amala ta hanyar aika adadin ƙarami. Da fatan za a gwada ƙara adadin.", "tx_not_enough_inputs_exception": "Bai isa ba hanyoyin da ake samu. Da fatan za selectiari a karkashin Kwarewar Coin", @@ -841,8 +878,16 @@ "view_transaction_on": "Dubo aikace-aikacen akan", "voting_weight": "Nauyi mai nauyi", "waitFewSecondForTxUpdate": "Da fatan za a jira ƴan daƙiƙa don ciniki don yin tunani a tarihin ma'amala", + "wallet_group": "Wallet kungiyar", + "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_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_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", "wallet_list_create_new_wallet": "Ƙirƙiri Sabon Wallet", + "wallet_list_edit_group_name": "Shirya sunan rukuni", "wallet_list_edit_wallet": "Gyara walat", "wallet_list_failed_to_load": "An kasa loda ${wallet_name} walat. ${error}", "wallet_list_failed_to_remove": "Ba a iya cirewa ${wallet_name} walat. ${error}", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index 346c420a9..5161250fc 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -36,6 +36,7 @@ "agree": "सहमत", "agree_and_continue": "सहमत और जारी रखें", "agree_to": "खाता बनाकर आप इससे सहमत होते हैं ", + "alert_notice": "सूचना", "all": "सब", "all_trades": "सभी व्यापार", "all_transactions": "सभी लेन - देन", @@ -113,7 +114,7 @@ "change_currency": "मुद्रा परिवर्तन करें", "change_current_node": "क्या आप वर्तमान नोड को बदलना सुनिश्चित करते हैं ${node}?", "change_current_node_title": "वर्तमान नोड बदलें", - "change_exchange_provider": "एक्सचेंज प्रदाता बदलें", + "change_exchange_provider": "स्वैप प्रदाता बदलें", "change_language": "भाषा बदलो", "change_language_to": "को भाषा बदलें ${language}?", "change_password": "पासवर्ड बदलें", @@ -130,6 +131,7 @@ "choose_one": "एक का चयन", "choose_relay": "कृपया उपयोग करने के लिए एक रिले चुनें", "choose_wallet_currency": "कृपया बटुआ मुद्रा चुनें:", + "choose_wallet_group": "वॉलेट ग्रुप चुनें", "clear": "स्पष्ट", "clearnet_link": "क्लियरनेट लिंक", "close": "बंद करना", @@ -142,6 +144,7 @@ "confirm_delete_wallet": "यह क्रिया इस वॉलेट को हटा देगी। क्या आप जारी रखना चाहते हैं?", "confirm_fee_deduction": "शुल्क कटौती की पुष्टि करें", "confirm_fee_deduction_content": "क्या आप आउटपुट से शुल्क में कटौती करने के लिए सहमत हैं?", + "confirm_passphrase": "पासफ़्रेज़ की पुष्टि करें", "confirm_sending": "भेजने की पुष्टि करें", "confirm_silent_payments_switch_node": "आपका वर्तमान नोड मूक भुगतान का समर्थन नहीं करता है \\ ncake वॉलेट एक संगत नोड पर स्विच करेगा, बस स्कैनिंग के लिए", "confirmations": "पुष्टिकरण", @@ -160,6 +163,7 @@ "contact_name": "संपर्क नाम", "contact_support": "सहायता से संपर्क करें", "continue_text": "जारी रहना", + "contract_warning": "इस अनुबंध के पते को संभावित रूप से धोखाधड़ी के रूप में चिह्नित किया गया है। कृपया सावधानी के साथ प्रक्रिया करें।", "contractName": "अनुबंध का नाम", "contractSymbol": "अनुबंध चिह्न", "copied_key_to_clipboard": "की नकल की ${key} क्लिपबोर्ड पर", @@ -175,6 +179,7 @@ "create_invoice": "इनवॉयस बनाएँ", "create_new": "नया बटुआ बनाएँ", "create_new_account": "नया खाता बनाएँ", + "create_new_seed": "नया बीज बनाएं", "creating_new_wallet": "नया बटुआ बनाना", "creating_new_wallet_error": "त्रुटि: ${description}", "creation_date": "निर्माण तिथि", @@ -195,6 +200,7 @@ "delete_wallet_confirm_message": "क्या आप वाकई ${wallet_name} वॉलेट हटाना चाहते हैं?", "deleteConnectionConfirmationPrompt": "क्या आप वाकई कनेक्शन हटाना चाहते हैं?", "denominations": "मूल्यवर्ग", + "derivationpath": "व्युत्पत्ति पथ", "descending": "अवरोही", "description": "विवरण", "destination_tag": "गंतव्य टैग:", @@ -207,6 +213,7 @@ "disable_buy": "खरीद कार्रवाई अक्षम करें", "disable_cake_2fa": "केक 2FA अक्षम करें", "disable_exchange": "एक्सचेंज अक्षम करें", + "disable_fee_api_warning": "इसे बंद करने से, कुछ मामलों में शुल्क दरें गलत हो सकती हैं, इसलिए आप अपने लेनदेन के लिए फीस को कम कर सकते हैं या कम कर सकते हैं", "disable_fiat": "िएट को अक्षम करें", "disable_sell": "बेचने की कार्रवाई अक्षम करें", "disableBatteryOptimization": "बैटरी अनुकूलन अक्षम करें", @@ -228,6 +235,8 @@ "edit_token": "टोकन संपादित करें", "electrum_address_disclaimer": "हर बार जब आप एक का उपयोग करते हैं तो हम नए पते उत्पन्न करते हैं, लेकिन पिछले पते काम करना जारी रखते हैं", "email_address": "ईमेल पता", + "enable": "सक्षम", + "enable_mempool_api": "सटीक शुल्क और तिथियों के लिए मेमपूल एपीआई", "enable_replace_by_fee": "प्रतिस्थापित-दर-शुल्क सक्षम करें", "enable_silent_payments_scanning": "मूक भुगतान स्कैनिंग सक्षम करें", "enabled": "सक्रिय", @@ -272,8 +281,8 @@ "etherscan_history": "इथरस्कैन इतिहास", "event": "आयोजन", "events": "आयोजन", - "exchange": "अदला बदली", - "exchange_incorrect_current_wallet_for_xmr": "यदि आप अपने केक वॉलेट मोनेरो बैलेंस से एक्सएमआर का आदान-प्रदान करना चाहते हैं, तो कृपया अपने मोनेरो वॉलेट में जाएं।", + "exchange": "बदलना", + "exchange_incorrect_current_wallet_for_xmr": "यदि आप अपने केक वॉलेट मोनेरो बैलेंस से XMR को स्वैप करना चाहते हैं, तो कृपया पहले अपने मोनेरो वॉलेट पर स्विच करें।", "exchange_new_template": "नया टेम्पलेट", "exchange_provider_unsupported": "${providerName} अब समर्थित नहीं है!", "exchange_result_confirm": "पुष्टि दबाकर, आप भेज रहे होंगे ${fetchingLabel} ${from} अपने बटुए से ${walletName} नीचे दिखाए गए पते पर। या आप अपने बाहरी वॉलेट से नीचे के पते पर भेज सकते हैं / क्यूआर कोड पर भेज सकते हैं।\n\nकृपया जारी रखने या राशि बदलने के लिए वापस जाने के लिए पुष्टि करें दबाएं.", @@ -291,6 +300,8 @@ "failed_authentication": "प्रमाणीकरण विफल. ${state_error}", "faq": "FAQ", "features": "विशेषताएँ", + "fee_less_than_min": "चयनित शुल्क न्यूनतम से कम है, कृपया लेन -देन भेजने में सक्षम होने के लिए शुल्क बढ़ाएं", + "fee_rate": "शुल्क दर", "fetching": "ला रहा है", "fiat_api": "फिएट पैसे API", "fiat_balance": "फिएट बैलेंस", @@ -298,7 +309,7 @@ "fill_code": "कृपया अपने ईमेल पर प्रदान किया गया सत्यापन कोड भरें", "filter_by": "के द्वारा छनित", "first_wallet_text": "Monero, Bitcoin, Ethereum, Litecoin, और Haven के लिए बहुत बढ़िया बटुआ", - "fixed_pair_not_supported": "यह निश्चित जोड़ी चयनित एक्सचेंजों के साथ समर्थित नहीं है", + "fixed_pair_not_supported": "यह निश्चित जोड़ी चयनित स्वैप सेवाओं के साथ समर्थित नहीं है", "fixed_rate": "निर्धारित दर", "fixed_rate_alert": "फिक्स्ड रेट मोड की जांच करने पर आप प्राप्त राशि दर्ज कर पाएंगे। क्या आप निश्चित दर मोड पर स्विच करना चाहते हैं?", "forgot_password": "पासवर्ड भूल गए", @@ -354,6 +365,20 @@ "ledger_error_wrong_app": "कृपया सुनिश्चित करें कि आप अपने लेजर पर सही ऐप को खोलते हैं", "ledger_please_enable_bluetooth": "कृपया अपने बहीखाने का पता लगाने के लिए ब्लूटूथ को सक्षम करें", "light_theme": "रोशनी", + "litecoin_enable_mweb_sync": "MWEB स्कैनिंग सक्षम करें", + "litecoin_mweb": "मावली", + "litecoin_mweb_always_scan": "MWEB हमेशा स्कैनिंग सेट करें", + "litecoin_mweb_description": "MWEB एक नया प्रोटोकॉल है जो लिटकोइन के लिए तेजी से, सस्ता और अधिक निजी लेनदेन लाता है", + "litecoin_mweb_dismiss": "नकार देना", + "litecoin_mweb_display_card": "MWEB कार्ड दिखाएं", + "litecoin_mweb_enable_later": "आप प्रदर्शन सेटिंग्स के तहत फिर से MWEB को सक्षम करने के लिए चुन सकते हैं।", + "litecoin_mweb_pegin": "खूंटी", + "litecoin_mweb_pegout": "मरना", + "litecoin_mweb_scanning": "MWEB स्कैनिंग", + "litecoin_mweb_settings": "MWEB सेटिंग्स", + "litecoin_mweb_warning": "MWEB का उपयोग शुरू में ~ 600MB डेटा डाउनलोड करेगा, और नेटवर्क की गति के आधार पर 30 मिनट तक का समय लग सकता है। यह प्रारंभिक डेटा केवल एक बार डाउनलोड करेगा और सभी लिटकोइन वॉलेट के लिए उपलब्ध होगा", + "litecoin_what_is_mweb": "MWEB क्या है?", + "live_fee_rates": "एपीआई के माध्यम से लाइव शुल्क दरें", "load_more": "और लोड करें", "loading_your_wallet": "अपना बटुआ लोड कर रहा है", "login": "लॉग इन करें", @@ -383,6 +408,8 @@ "monero_light_theme": "मोनेरो लाइट थीम", "moonpay_alert_text": "राशि का मूल्य अधिक है या करने के लिए बराबर होना चाहिए ${minAmount} ${fiatCurrency}", "more_options": "और विकल्प", + "mweb_confirmed": "MWEB की पुष्टि की", + "mweb_unconfirmed": "अपुष्ट MWEB", "name": "नाम", "nano_current_rep": "वर्तमान प्रतिनिधि", "nano_gpt_thanks_message": "Nanogpt का उपयोग करने के लिए धन्यवाद! अपने लेन -देन के पूरा होने के बाद ब्राउज़र पर वापस जाना याद रखें!", @@ -412,7 +439,7 @@ "node_test": "परीक्षा", "nodes": "नोड्स", "nodes_list_reset_to_default_message": "क्या आप वाकई सेटिंग को डिफ़ॉल्ट पर रीसेट करना चाहते हैं?", - "none_of_selected_providers_can_exchange": "चयनित प्रदाताओं में से कोई भी इस एक्सचेंज को नहीं बना सकता", + "none_of_selected_providers_can_exchange": "चयनित प्रदाता में से कोई भी यह स्वैप नहीं कर सकता है", "noNFTYet": "अभी तक कोई एनएफटी नहीं", "normal": "सामान्य", "note_optional": "नोट (वैकल्पिक)", @@ -442,6 +469,7 @@ "overwrite_amount": "Overwrite amount", "pairingInvalidEvent": "अमान्य ईवेंट युग्मित करना", "passphrase": "पासफ्रेज़ (वैकल्पिक)", + "passphrases_doesnt_match": "PassPhrases मेल नहीं खाता, कृपया पुनः प्रयास करें", "password": "पारण शब्द", "paste": "पेस्ट करें", "pause_wallet_creation": "हेवन वॉलेट बनाने की क्षमता फिलहाल रुकी हुई है।", @@ -595,8 +623,11 @@ "seed_share": "बीज साझा करें", "seed_title": "बीज", "seedtype": "बीज", + "seedtype_alert_content": "अन्य पर्स के साथ बीज साझा करना केवल BIP39 सीडटाइप के साथ संभव है।", + "seedtype_alert_title": "बीजगणित अलर्ट", "seedtype_legacy": "विरासत (25 शब्द)", "seedtype_polyseed": "पॉलीसीड (16 शब्द)", + "seedtype_wownero": "Wownero (14 शब्द)", "select_backup_file": "बैकअप फ़ाइल का चयन करें", "select_buy_provider_notice": "ऊपर एक खरीद प्रदाता का चयन करें। आप इस स्क्रीन को ऐप सेटिंग्स में अपना डिफ़ॉल्ट बाय प्रदाता सेट करके छोड़ सकते हैं।", "select_destination": "कृपया बैकअप फ़ाइल के लिए गंतव्य का चयन करें।", @@ -607,6 +638,7 @@ "send": "संदेश", "send_address": "${cryptoCurrency} पता", "send_amount": "रकम:", + "send_change_to_you": "बदलो, आप को:", "send_creating_transaction": "लेन-देन बनाना", "send_error_currency": "मुद्रा में केवल संख्याएँ हो सकती हैं", "send_error_minimum_value": "राशि का न्यूनतम मूल्य 0.01 है", @@ -660,19 +692,21 @@ "setup_your_debit_card": "अपना डेबिट कार्ड सेट करें", "share": "शेयर करना", "share_address": "पता साझा करें", + "shared_seed_wallet_groups": "साझा बीज बटुए समूह", "show_details": "विवरण दिखाएं", "show_keys": "बीज / कुंजियाँ दिखाएँ", "show_market_place": "बाज़ार दिखाएँ", "show_seed": "बीज दिखाओ", "sign_message": "हस्ताक्षर संदेश", "sign_up": "साइन अप करें", - "sign_verify_message": "संदेश पर हस्ताक्षर या सत्यापित करें", + "sign_verify_message": "हस्ताक्षर / सत्यापित करें", "sign_verify_message_sub": "अपनी निजी कुंजी का उपयोग करके किसी संदेश पर हस्ताक्षर या सत्यापित करें", "sign_verify_title": "हस्ताक्षर / सत्यापित करें", "signature": "हस्ताक्षर", "signature_invalid_error": "हस्ताक्षर दिए गए संदेश के लिए मान्य नहीं है", "signTransaction": "लेन-देन पर हस्ताक्षर करें", "signup_for_card_accept_terms": "कार्ड के लिए साइन अप करें और शर्तें स्वीकार करें।", + "silent_payment": "मूक भुगतान", "silent_payments": "मूक भुगतान", "silent_payments_always_scan": "मूक भुगतान हमेशा स्कैनिंग सेट करें", "silent_payments_disclaimer": "नए पते नई पहचान नहीं हैं। यह एक अलग लेबल के साथ एक मौजूदा पहचान का पुन: उपयोग है।", @@ -683,6 +717,7 @@ "silent_payments_scanned_tip": "टिप करने के लिए स्कैन किया! (${tip})", "silent_payments_scanning": "मूक भुगतान स्कैनिंग", "silent_payments_settings": "मूक भुगतान सेटिंग्स", + "single_seed_wallets_group": "एकल बीज बटुए", "slidable": "फिसलने लायक", "sort_by": "इसके अनुसार क्रमबद्ध करें", "spend_key_private": "खर्च करना (निजी)", @@ -705,12 +740,13 @@ "switchToEVMCompatibleWallet": "कृपया ईवीएम संगत वॉलेट पर स्विच करें और पुनः प्रयास करें (एथेरियम, पॉलीगॉन)", "symbol": "प्रतीक", "sync_all_wallets": "सभी वॉलेट सिंक करें", + "sync_status_attempting_scan": "स्कैन का प्रयास", "sync_status_attempting_sync": "सिंक करने का प्रयास", "sync_status_connected": "जुड़े हुए", "sync_status_connecting": "कनेक्ट", "sync_status_failed_connect": "डिस्कनेक्ट किया गया", "sync_status_not_connected": "जुड़े नहीं हैं", - "sync_status_starting_scan": "स्कैन शुरू करना", + "sync_status_starting_scan": "स्कैन शुरू करना (${height} से)", "sync_status_starting_sync": "सिताज़ा करना", "sync_status_syncronized": "सिंक्रनाइज़", "sync_status_syncronizing": "सिंक्रनाइज़ करने", @@ -797,6 +833,7 @@ "trusted": "भरोसा", "tx_commit_exception_no_dust_on_change": "लेनदेन को इस राशि से खारिज कर दिया जाता है। इन सिक्कों के साथ आप चेंज या ${min} के बिना ${max} को भेज सकते हैं जो परिवर्तन लौटाता है।", "tx_commit_failed": "लेन -देन प्रतिबद्ध विफल। कृपया संपर्क समर्थन करें।", + "tx_commit_failed_no_peers": "लेन -देन प्रसारित करने में विफल रहा, कृपया एक या दो सेकंड में पुनः प्रयास करें", "tx_invalid_input": "आप इस प्रकार के भुगतान के लिए गलत इनपुट प्रकार का उपयोग कर रहे हैं", "tx_no_dust_exception": "लेनदेन को बहुत छोटी राशि भेजकर अस्वीकार कर दिया जाता है। कृपया राशि बढ़ाने का प्रयास करें।", "tx_not_enough_inputs_exception": "पर्याप्त इनपुट उपलब्ध नहीं है। कृपया सिक्का नियंत्रण के तहत अधिक चुनें", @@ -841,8 +878,16 @@ "view_transaction_on": "View Transaction on ", "voting_weight": "वोटिंग वेट", "waitFewSecondForTxUpdate": "लेन-देन इतिहास में लेन-देन प्रतिबिंबित होने के लिए कृपया कुछ सेकंड प्रतीक्षा करें", + "wallet_group": "बटुए समूह", + "wallet_group_description_four": "एक पूरी तरह से नए बीज के साथ एक बटुआ बनाने के लिए।", + "wallet_group_description_one": "केक बटुए में, आप एक बना सकते हैं", + "wallet_group_description_three": "उपलब्ध वॉलेट और/या वॉलेट समूह स्क्रीन देखने के लिए। या चुनें", + "wallet_group_description_two": "एक बीज साझा करने के लिए एक मौजूदा बटुए का चयन करके। प्रत्येक वॉलेट समूह में प्रत्येक मुद्रा प्रकार का एक एकल वॉलेट हो सकता है।\n\nआप चयन कर सकते हैं", + "wallet_group_empty_state_text_one": "लगता है कि आपके पास कोई संगत बटुआ समूह नहीं है!\n\nनल", + "wallet_group_empty_state_text_two": "नीचे एक नया बनाने के लिए।", "wallet_keys": "बटुआ बीज / चाबियाँ", "wallet_list_create_new_wallet": "नया बटुआ बनाएँ", + "wallet_list_edit_group_name": "समूह का नाम संपादित करें", "wallet_list_edit_wallet": "बटुआ संपादित करें", "wallet_list_failed_to_load": "लोड करने में विफल ${wallet_name} बटुआ. ${error}", "wallet_list_failed_to_remove": "निकालने में विफल ${wallet_name} बटुआ. ${error}", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index 6f5bc88a6..8ef92aaf0 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -36,6 +36,7 @@ "agree": "Slažem se", "agree_and_continue": "Slažem se i nastavi", "agree_to": "Stvaranjem računa pristajete na ", + "alert_notice": "Obavijest", "all": "SVE", "all_trades": "Svi obrti", "all_transactions": "Sve transakcije", @@ -113,7 +114,7 @@ "change_currency": "Promijenite valutu", "change_current_node": "Jeste li sigurni da želite promijeniti trenutni node na ${node}?", "change_current_node_title": "Promijeni trenutni node", - "change_exchange_provider": "Promjena davatelja usluge razmjene", + "change_exchange_provider": "Promijenite davatelja zamjene", "change_language": "Promijeni jezik", "change_language_to": "Promijeni jezik u ${language}?", "change_password": "Promijeni lozinku", @@ -130,6 +131,7 @@ "choose_one": "Izaberi jedan", "choose_relay": "Odaberite relej za korištenje", "choose_wallet_currency": "Molimo odaberite valutu novčanika:", + "choose_wallet_group": "Odaberite grupu novčanika", "clear": "Izbriši", "clearnet_link": "Clearnet veza", "close": "Zatvoriti", @@ -142,6 +144,7 @@ "confirm_delete_wallet": "Ovom ćete radnjom izbrisati ovaj novčanik. Želite li nastaviti?", "confirm_fee_deduction": "Potvrdite odbitak naknade", "confirm_fee_deduction_content": "Slažete li se da ćete odbiti naknadu od izlaza?", + "confirm_passphrase": "Potvrdite prolaznu frazu", "confirm_sending": "Potvrdi slanje", "confirm_silent_payments_switch_node": "Vaš trenutni čvor ne podržava tiha plaćanja \\ ncake novčanik prebacit će se na kompatibilni čvor, samo za skeniranje", "confirmations": "Potvrde", @@ -160,6 +163,7 @@ "contact_name": "Ime kontakta", "contact_support": "Kontaktirajte podršku", "continue_text": "Nastavak", + "contract_warning": "Ova adresa ugovora označena je kao potencijalno lažna. Molimo obradite s oprezom.", "contractName": "Naziv ugovora", "contractSymbol": "Simbol ugovora", "copied_key_to_clipboard": "${key} kopiran u međuspremnik", @@ -175,6 +179,7 @@ "create_invoice": "Izradite fakturu", "create_new": "Izradi novi novčanik", "create_new_account": "Izradi novi račun", + "create_new_seed": "Stvorite novo sjeme", "creating_new_wallet": "Stvaranje novog novčanika", "creating_new_wallet_error": "Greška: ${description}", "creation_date": "Datum stvaranja", @@ -195,6 +200,7 @@ "delete_wallet_confirm_message": "Jeste li sigurni da želite izbrisati ${wallet_name} novčanik?", "deleteConnectionConfirmationPrompt": "Jeste li sigurni da želite izbrisati vezu s", "denominations": "Denominacije", + "derivationpath": "Put derivacije", "descending": "Silazni", "description": "Opis", "destination_tag": "Odredišna oznaka:", @@ -207,6 +213,7 @@ "disable_buy": "Onemogući kupnju", "disable_cake_2fa": "Onemogući Cake 2FA", "disable_exchange": "Onemogući exchange", + "disable_fee_api_warning": "Isključivanjem ovoga, stope naknade u nekim bi slučajevima mogle biti netočne, tako da biste mogli preplatiti ili predati naknadu za vaše transakcije", "disable_fiat": "Isključi, fiat", "disable_sell": "Onemogući akciju prodaje", "disableBatteryOptimization": "Onemogući optimizaciju baterije", @@ -228,8 +235,10 @@ "edit_token": "Uredi token", "electrum_address_disclaimer": "Minden egyes alkalommal új címeket generálunk, de a korábbi címek továbbra is működnek", "email_address": "Adresa e-pošte", + "enable": "Omogućiti", + "enable_mempool_api": "Mempool API za točne naknade i datume", "enable_replace_by_fee": "Omogući zamjenu", - "enable_silent_payments_scanning": "Omogući skeniranje tihih plaćanja", + "enable_silent_payments_scanning": "Započnite skeniranje tihih plaćanja, dok se ne postigne savjet", "enabled": "Omogućeno", "enter_amount": "Unesite iznos", "enter_backup_password": "Unesite svoju lozinku za sigurnosnu kopiju ovdje", @@ -272,8 +281,8 @@ "etherscan_history": "Etherscan povijest", "event": "Događaj", "events": "Događaji", - "exchange": "Razmijeni", - "exchange_incorrect_current_wallet_for_xmr": "Ako želite razmijeniti XMR s vlastitog Monero računa na Cake Wallet novčaniku, molimo prvo se prebacite na svoj Monero novčanik.", + "exchange": "Zamjena", + "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!", "exchange_result_confirm": "Pritiskom na potvrdi, poslat ćete ${fetchingLabel} ${from} sa svog novčanika pod nazivom ${walletName} na adresu prikazanu ispod ili iznos možete poslati s vanjskog novčanika na niže navedenu adresu. /QR code.\n\nMolimo potvrdite za nastavak ili se vratite natrag za promjenu iznosa.", @@ -291,6 +300,8 @@ "failed_authentication": "Autentifikacija neuspješna. ${state_error}", "faq": "FAQ", "features": "Značajke", + "fee_less_than_min": "Odabrana naknada je manja od minimalnog, povećajte naknade kako biste mogli poslati transakciju", + "fee_rate": "Stopa naknade", "fetching": "Dohvaćanje", "fiat_api": "Fiat API", "fiat_balance": "Fiat Bilans", @@ -298,7 +309,7 @@ "fill_code": "Molimo vas da ispunite kontrolni kod koji ste dobili na svojoj e-pošti", "filter_by": "Filtrirati po", "first_wallet_text": "Odličan novčanik za Monero, Bitcoin, Ethereum, Litecoin, i Haven", - "fixed_pair_not_supported": "Ovaj fiksni par nije podržan s odabranim burzama", + "fixed_pair_not_supported": "Ovaj fiksni par nije podržan s odabranim SWAP uslugama", "fixed_rate": "Fiksna stopa", "fixed_rate_alert": "Moći ćete unijeti iznos koji želite primiti nakon što označite način rada fiksne stope. Želite li se prebaciti na način rada fiksne stope?", "forgot_password": "Zaboravljena lozinka", @@ -354,6 +365,20 @@ "ledger_error_wrong_app": "Obavezno obavezno otvorite pravu aplikaciju na knjizi", "ledger_please_enable_bluetooth": "Omogućite Bluetooth da otkrije svoju knjigu", "light_theme": "Svijetla", + "litecoin_enable_mweb_sync": "Omogućite MWEB skeniranje", + "litecoin_mweb": "MWeb", + "litecoin_mweb_always_scan": "Postavite MWeb uvijek skeniranje", + "litecoin_mweb_description": "MWEB je novi protokol koji u Litecoin donosi brže, jeftinije i privatnije transakcije", + "litecoin_mweb_dismiss": "Odbaciti", + "litecoin_mweb_display_card": "Prikaži MWeb karticu", + "litecoin_mweb_enable_later": "Možete odabrati da MWEB ponovo omogućite pod postavkama zaslona.", + "litecoin_mweb_pegin": "Uvući se", + "litecoin_mweb_pegout": "Odapeti", + "litecoin_mweb_scanning": "MWEB skeniranje", + "litecoin_mweb_settings": "Postavke MWEB -a", + "litecoin_mweb_warning": "Korištenje MWEB -a u početku će preuzeti ~ 600MB podataka, a može potrajati do 30 minuta, ovisno o brzini mreže. Ovi početni podaci preuzet će samo jednom i biti dostupni za sve Litecoin novčanike", + "litecoin_what_is_mweb": "Što je MWEB?", + "live_fee_rates": "Stope naknada uživo putem API -ja", "load_more": "Učitaj više", "loading_your_wallet": "Novčanik se učitava", "login": "Prijava", @@ -383,6 +408,8 @@ "monero_light_theme": "Monero lagana tema", "moonpay_alert_text": "Vrijednost iznosa mora biti veća ili jednaka ${minAmount} ${fiatCurrency}", "more_options": "Više opcija", + "mweb_confirmed": "Potvrđen MWeb", + "mweb_unconfirmed": "Nepotvrđeni mWeb", "name": "Ime", "nano_current_rep": "Trenutni predstavnik", "nano_gpt_thanks_message": "Hvala što ste koristili nanogpt! Ne zaboravite da se vratite u preglednik nakon što vam se transakcija završi!", @@ -412,7 +439,7 @@ "node_test": "Provjeri", "nodes": "Nodes", "nodes_list_reset_to_default_message": "Jeste li sigurni da se želite vratiti na početne postavke?", - "none_of_selected_providers_can_exchange": "Niti jedan od odabranih pružatelja usluga ne može izvršiti ovu razmjenu", + "none_of_selected_providers_can_exchange": "Nijedan od odabranih pružatelja usluga ne može napraviti ovu zamjenu", "noNFTYet": "Još nema NFT-ova", "normal": "Normalno", "note_optional": "Poruka (nije obvezno)", @@ -442,6 +469,7 @@ "overwrite_amount": "Overwrite amount", "pairingInvalidEvent": "Nevažeći događaj uparivanja", "passphrase": "Prolaznica (neobavezno)", + "passphrases_doesnt_match": "Prolazne fraze se ne podudaraju, pokušajte ponovo", "password": "Lozinka", "paste": "Zalijepi", "pause_wallet_creation": "Mogućnost stvaranja novčanika Haven trenutno je pauzirana.", @@ -593,8 +621,11 @@ "seed_share": "Podijeli pristupni izraz", "seed_title": "Prisupni izraz", "seedtype": "Sjemenska vrsta", + "seedtype_alert_content": "Dijeljenje sjemena s drugim novčanicima moguće je samo s BIP39 sjemenom.", + "seedtype_alert_title": "Upozorenje o sjemenu", "seedtype_legacy": "Nasljeđe (25 riječi)", "seedtype_polyseed": "Poliseed (16 riječi)", + "seedtype_wownero": "WANERO (14 riječi)", "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.", @@ -605,6 +636,7 @@ "send": "Pošalji", "send_address": "${cryptoCurrency} adresa", "send_amount": "Iznos:", + "send_change_to_you": "Promijenite, u vas:", "send_creating_transaction": "Izrada transakcije", "send_error_currency": "Iznos smije sadržavati samo brojeve", "send_error_minimum_value": "Minimalna vrijednost iznosa je 0.01", @@ -658,19 +690,21 @@ "setup_your_debit_card": "Postavite svoju debitnu karticu", "share": "Udio", "share_address": "Podijeli adresu", + "shared_seed_wallet_groups": "Zajedničke grupe za sjeme novčanika", "show_details": "Prikaži pojedinosti", "show_keys": "Prikaži pristupni izraz/ključ", "show_market_place": "Prikaži tržište", "show_seed": "Prikaži pristupni izraz", "sign_message": "Poruka", "sign_up": "Prijavite se", - "sign_verify_message": "Potpisati ili provjeriti poruku", + "sign_verify_message": "Potpisati / provjeriti", "sign_verify_message_sub": "Potpišite ili provjerite poruku pomoću privatnog ključa", "sign_verify_title": "Potpisati / provjeriti", "signature": "Potpis", "signature_invalid_error": "Potpis ne vrijedi za danu poruku", "signTransaction": "Potpišite transakciju", "signup_for_card_accept_terms": "Prijavite se za karticu i prihvatite uvjete.", + "silent_payment": "Tiho plaćanje", "silent_payments": "Tiha plaćanja", "silent_payments_always_scan": "Postavite tiho plaćanje uvijek skeniranje", "silent_payments_disclaimer": "Nove adrese nisu novi identiteti. To je ponovna upotreba postojećeg identiteta s drugom oznakom.", @@ -681,6 +715,7 @@ "silent_payments_scanned_tip": "Skenirano na savjet! (${tip})", "silent_payments_scanning": "Skeniranje tihih plaćanja", "silent_payments_settings": "Postavke tihih plaćanja", + "single_seed_wallets_group": "Jednostruki novčanici", "slidable": "Klizna", "sort_by": "Poredaj po", "spend_key_private": "Spend key (privatni)", @@ -703,12 +738,13 @@ "switchToEVMCompatibleWallet": "Prijeđite na novčanik kompatibilan s EVM-om i pokušajte ponovno (Ethereum, Polygon)", "symbol": "Simbol", "sync_all_wallets": "Sinkronizirajte sve novčanike", + "sync_status_attempting_scan": "Pokušaj skeniranja", "sync_status_attempting_sync": "POKUŠAJ SINKRONIZACIJE", "sync_status_connected": "SPOJENO", "sync_status_connecting": "SPAJANJE", "sync_status_failed_connect": "ISKLJUČENO", "sync_status_not_connected": "NIJE POVEZANO", - "sync_status_starting_scan": "Početno skeniranje", + "sync_status_starting_scan": "Početno skeniranje (od ${height})", "sync_status_starting_sync": "ZAPOČINJEMO SINKRONIZIRANJE", "sync_status_syncronized": "SINKRONIZIRANO", "sync_status_syncronizing": "SINKRONIZIRANJE", @@ -795,6 +831,7 @@ "trusted": "vjerovao", "tx_commit_exception_no_dust_on_change": "Transakcija se odbija s tim iznosom. Pomoću ovih kovanica možete poslati ${min} bez promjene ili ${max} koja vraća promjenu.", "tx_commit_failed": "Obveza transakcije nije uspjela. Molimo kontaktirajte podršku.", + "tx_commit_failed_no_peers": "Transakcija nije uspjela emitirati, pokušajte ponovo u sekundi ili tako", "tx_invalid_input": "Koristite pogrešnu vrstu ulaza za ovu vrstu plaćanja", "tx_no_dust_exception": "Transakcija se odbija slanjem iznosa premalo. Pokušajte povećati iznos.", "tx_not_enough_inputs_exception": "Nema dovoljno unosa. Molimo odaberite više pod kontrolom novčića", @@ -839,8 +876,16 @@ "view_transaction_on": "View Transaction on ", "voting_weight": "Težina glasanja", "waitFewSecondForTxUpdate": "Pričekajte nekoliko sekundi da se transakcija prikaže u povijesti transakcija", + "wallet_group": "Skupina novčanika", + "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_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_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", "wallet_list_create_new_wallet": "Izradi novi novčanik", + "wallet_list_edit_group_name": "Uredi naziv grupe", "wallet_list_edit_wallet": "Uredi novčanik", "wallet_list_failed_to_load": "Neuspješno učitavanje novčanika ${wallet_name}. ${error}", "wallet_list_failed_to_remove": "Neuspješno uklanjanje novčanika ${wallet_name}. ${error}", diff --git a/res/values/strings_hy.arb b/res/values/strings_hy.arb new file mode 100644 index 000000000..40ed1e116 --- /dev/null +++ b/res/values/strings_hy.arb @@ -0,0 +1,933 @@ +{ + "about_cake_pay": "Cake Pay-ը Ձեզ թույլ է տալիս հեշտությամբ գնել նվեր քարտեր վիրտուալ ակտիվներով, որոնք անմիջապես ծախսվում են ԱՄՆ-ի 150,000-ից ավելի առևտրականների մոտ:", + "account": "Հաշիվ", + "accounts": "Հաշիվներ", + "accounts_subaddresses": "Հաշիվներ և ենթահասցեներ", + "activate": "Ակտիվացնել", + "active": "Ակտիվ", + "active_cards": "Ակտիվ քարտեր", + "activeConnectionsPrompt": "Ակտիվ կապերը կհայտնվեն այստեղ", + "add": "Ավելացնել", + "add_contact": "Ավելացնել կոնտակտ", + "add_contact_to_address_book": "Ուզում եք ավելացնել այս կոնտակտը Ձեր հասցեների գրքում?", + "add_custom_node": "Ավելացնել նոր պատվերով հանգույց", + "add_custom_redemption": "Ավելացնել պատվերով փրկագնում", + "add_fund_to_card": "Ավելացնել նախավճար քարտերի վրա (մինչև ${value})", + "add_new_node": "Ավելացնել նոր հանգույց", + "add_new_word": "Ավելացնել նոր բառ", + "add_receiver": "Ավելացնել ևս մեկ ստացող (ընտրովի)", + "add_secret_code": "Կամ ավելացրեք այս գաղտնի կոդը վավերացնող հավելվածում", + "add_tip": "Ավելացնել Թեյավճար", + "add_token_disclaimer_check": "Ես հաստատել եմ token-ի պայմանագրի հասցեն և տեղեկատվությունը վստահելի աղբյուրի օգտագործմամբ: Վնասակար կամ սխալ տեղեկատվության ավելացումը կարող է հանգեցնել միջոցների կորստի:", + "add_token_warning": "Մի խմբագրեք կամ ավելացրեք token-ներ, ինչպես կոչ են անում խարդախները:\nՄիշտ հաստատեք Token-ների հասցեները վստահելի աղբյուրներով:", + "add_value": "Ավելացնել արժեք", + "address": "Հասցե", + "address_book": "Հասցեագիրք", + "address_book_menu": "Հասցեագիրք", + "address_detected": "Հասցեն հայտնաբերված է", + "address_from_domain": "Այս հասցեն է ${domain} Unstoppable Domains-ից", + "address_from_yat": "Այս հասցեն է ${emoji} Yat-ից", + "address_label": "Հասցեի պիտակ", + "address_remove_contact": "Հեռացնել կոնտակտը", + "address_remove_content": "Վստահ եք, որ ուզում եք հեռացնել ընտրված կոնտակտը?", + "addresses": "Հասցեներ", + "advanced_settings": "Առաջադեմ կարգավորումներ", + "aggressive": "Ագրեսիվ", + "agree": "Համաձայն եմ", + "agree_and_continue": "Համաձայն եմ և շարունակեմ", + "agree_to": "Ստեղծելով հաշիվ դուք համաձայնում եք ", + "alert_notice": "Ծանուցում", + "all": "Բոլորը", + "all_trades": "Բոլոր գործարքները", + "all_transactions": "Բոլոր գործառնությունները", + "alphabetical": "Այբբենական", + "already_have_account": "Արդեն ունե՞ք հաշիվ?", + "always": "Միշտ", + "amount": "Քանակ՝ ", + "amount_is_below_minimum_limit": "Ձեր հաշվի մնացորդը կազմում է ավելի քիչ, քան փոխանակման համար անհրաժեշտ նվազագույն վճարը (${min})", + "amount_is_estimate": "Ստացվող գումարը մոտավոր է", + "amount_is_guaranteed": "Ստացվող գումարը երաշխավորված է", + "and": "և", + "anonpay_description": "${type} ստեղծել: Ստացողը կարող է ${method} ցանկացած աջակցվող կրիպտոարժույթով, և դուք կստանաք միջոցներ այս դրամապանակում։", + "apk_update": "APK թարմացում", + "approve": "Հաստատել", + "arrive_in_this_address": "${currency} ${tag}կժամանի այս հասցեում", + "ascending": "Աճող", + "ask_each_time": "Հարցնել ամեն անգամ", + "auth_store_ban_timeout": "արգելափակման ժամանակ", + "auth_store_banned_for": "Արգելափակված է ", + "auth_store_banned_minutes": " րոպե", + "auth_store_incorrect_password": "Սխալ PIN", + "authenticated": "Վավերացված", + "authentication": "Վավերացում", + "auto_generate_addresses": "Ավտոմատ հասցեներ ստեղծել", + "auto_generate_subaddresses": "Ավտոմատ ենթահասցեներ ստեղծել", + "automatic": "Ավտոմատ", + "available_balance": "Հասանելի մնացորդ", + "available_balance_description": "«Հասանելի մնացորդ» կամ «Հաստատված մնացորդ»-ը միջոցներ են, որոնք կարող են անմիջապես ծախսվել։ Եթե միջոցները հայտնվում են ստորին մնացորդում, բայց ոչ վերևի մնացորդում, ապա դուք պետք է մի քանի րոպե սպասեք, մինչև ներգաղթող միջոցները ավելի ցանցի հաստատումներ ստանան։ Հաստատումներ ստանալուց հետո դրանք կարող են ծախսվել։", + "avg_savings": "Միջին խնայողություն", + "awaitDAppProcessing": "Խնդրեմ սպասեք, մինչև դիմումը կավարտի մշակումը։", + "awaiting_payment_confirmation": "Վճարման հաստատման սպասում", + "background_sync_mode": "Հետին պլանի համաժամացման ռեժիմ", + "backup": "Կրկնօրինակ", + "backup_file": "Կրկնօրինակի ֆայլ", + "backup_password": "Կրկնօրինակի գաղտնաբառ", + "balance": "Հաշվեկշիռ", + "balance_page": "Հաշվեկշռի էջ", + "bill_amount": "Հաշիվը", + "billing_address_info": "Եթե խնդրեն հաշվեվճարի հասցե, ապա տրամադրեք ձեր առաքման հասցեն", + "biometric_auth_reason": "Սկանավեք ձեր մատնահետքը նույնականացման համար", + "bitcoin_dark_theme": "Bitcoin մութ տեսք", + "bitcoin_light_theme": "Bitcoin պայծառ տեսք", + "bitcoin_payments_require_1_confirmation": "Bitcoin վճարումները պահանջում են 1 հաստատում, որը կարող է տևել 20 րոպե կամ ավելի: Շնորհակալություն ձեր համբերության համար: Դուք էլ. նամակ կստանաք, երբ վճարումը հաստատվի։", + "block_remaining": "1 Բլոկ է մնացել", + "Blocks_remaining": "${status} Բլոկ է մնացել", + "bluetooth": "Bluetooth", + "bright_theme": "Պայծառ", + "bump_fee": "Բարձրացնել միջնորդավճարը", + "buy": "Գնել", + "buy_alert_content": "Այժմ մենք աջակցում ենք միայն Bitcoin, Ethereum, Litecoin և Monero արժույթների գնմանը: Խնդրում ենք ստեղծել կամ վերածանցել Bitcoin, Ethereum, Litecoin կամ Monero արժույթների հաշիվը։", + "buy_bitcoin": "Գնել Bitcoin", + "buy_now": "Գնել հիմա", + "buy_provider_unavailable": "Տվյալ պահին մատակարարը անհասանելի է։", + "buy_with": "Գնել", + "by_cake_pay": "Cake Pay-ով", + "cake_2fa_preset": "Cake 2FA նախապես կանխորոշված", + "cake_dark_theme": "Cake մութ տեսք", + "cake_pay_account_note": "Գրանցվեք միայն էլ. փոստի միջոցով, որպեսզի տեսնեք և գնեք քարտեր: Որոշ քարտեր հասանելի են նույնիսկ զեղչով:", + "cake_pay_learn_more": "Վայրկյանապես գնեք և փոխանակեք նվեր քարտերը հավելվածում:\nՍահեցրեք ձախից աջ՝ ավելին իմանալու համար:", + "cake_pay_save_order": "Քարտը պետք է ուղարկված լինի ձեր էլ. փոստին 1 աշխատանքային օրվա ընթացքում \n Պահպանեք Ձեր պատվերի համարը՝", + "cake_pay_subtitle": "Գնեք համաշխարհային նախավճարային քարտեր և նվեր քարտեր", + "cake_pay_web_cards_subtitle": "Գնեք համաշխարհային նախավճարային քարտեր և նվեր քարտեր", + "cake_pay_web_cards_title": "Cake Pay Վեբ Քարտեր", + "cake_wallet": "Cake Գաղտնապահոց", + "cakepay_prepaid_card": "CakePay Նախավճարային Դեբետային Քարտ", + "camera_consent": "Ձեր տեսախցիկը կօգտագործվի ${provider}-ի կողմից ինքնությունը հաստատելու նպատակով: Խնդրում ենք ծանոթանալ նրանց Գաղտնիության Քաղաքականության հետ:", + "camera_permission_is_required": "Տեսախցիկի թույլտվություն է պահանջվում: \nԽնդրում ենք այն ակտիվացնել հավելվածի կարգավորումներից:", + "cancel": "Չեղարկել", + "card_address": "Հասցե։", + "cardholder_agreement": "Քարտապանի Պայմանագիր", + "cards": "Քարտեր", + "chains": "Շղթաներ", + "change": "Փոփոխել", + "change_backup_password_alert": "Ձեր նախորդ կրկնօրինակ ֆայլերը չեն հասանելի լինի ներմուծել նոր կրկնօրինակի գաղտնաբառով: Նոր կրկնօրինակի գաղտնաբառը կօգտագործվի միայն նոր կրկնօրինակ ֆայլերի համար: Վստահ եք, որ ցանկանում եք փոխել կրկնօրինակի գաղտնաբառը?", + "change_currency": "Փոխել արժույթը", + "change_current_node": "Վստահ եք, որ ցանկանում եք փոխել ընթացիկ հանգույցը ${node}?", + "change_current_node_title": "Փոխել ընթացիկ հանգույցը", + "change_exchange_provider": "Փոխեք փոխանակման մատակարարը", + "change_language": "Փոխել լեզուն", + "change_language_to": "Փոխել լեզուն ${language}?", + "change_password": "Փոխել գաղտնաբառը", + "change_rep": "Փոխել ներկայացուցչին", + "change_rep_message": "Վստահ եք, որ ցանկանում եք փոխել ներկայացուցիչներին?", + "change_rep_successful": "Ներկայացուցչի փոփոխությունը հաջողությամբ կատարվեց", + "change_wallet_alert_content": "Ցանկանում եք փոխել ընթացիկ դրամապանակը ${wallet_name}?", + "change_wallet_alert_title": "Փոխել ընթացիկ դրամապանակը", + "choose_account": "Ընտրեք հաշիվը", + "choose_address": "\n\nԽնդրում ենք ընտրեք հասցեն", + "choose_card_value": "Ընտրեք քարտի արժեք", + "choose_derivation": "Ընտրեք դրամապանակի ածանցում", + "choose_from_available_options": "Ընտրեք առկա տարբերակներից", + "choose_one": "Ընտրեք մեկը", + "choose_relay": "Խնդրում ենք ընտրեք փոխանցման կետ", + "choose_wallet_currency": "Խնդրում ենք ընտրեք դրամապանակի արժույթը", + "choose_wallet_group": "Ընտրեք դրամապանակների խումբ", + "clear": "Մաքրել", + "clearnet_link": "Բաց ցանցի հղում", + "close": "Փակել", + "coin_control": "Մետաղադրամի վերահսկում (ըստ ցանկության)", + "cold_or_recover_wallet": "Ավելացնել սառը դրամապանակ կամ վերականգնել թղթային դրամապանակ", + "color_theme": "Գույների տեսք", + "commit_transaction_amount_fee": "Հաստատել գործարքը\nՍկզբնական գումար. ${amount}\nՄիջնորդավճար. ${fee}", + "confirm": "Հաստատել", + "confirm_delete_template": "Այս գործողությունը կջնջի այս ձևանմուշը: Ցանկանու՞մ եք շարունակել։", + "confirm_delete_wallet": "Այս գործողությունը կջնջի այս դրամապանակը: Ցանկանու՞մ եք շարունակել։", + "confirm_fee_deduction": "Հաստատեք միջնորդավճար հանումը", + "confirm_fee_deduction_content": "Դուք համաձայն եք միջնորդավճար հանել արդյունքից?", + "confirm_passphrase": "Հաստատեք գաղտնաբառը", + "confirm_sending": "Հաստատեք ուղարկումը", + "confirm_silent_payments_switch_node": "Ձեր ընթացիկ հանգույցը չի աջակցում Լուռ վճարումներին\nCake Wallet-ը կանցնի համատեղելի հանգույց, միայն սկանավորման համար", + "confirmations": "Հաստատումներ", + "confirmed": "Հաստատված մնացորդ", + "confirmed_tx": "Հաստատված", + "congratulations": "Շնորհավորանք!", + "connect_an_existing_yat": "Միացրեք գոյություն ունեցող Yat-ը", + "connect_yats": "Միացրեք Yat-ները", + "connect_your_hardware_wallet": "Միացրեք ձեր ապարատային դրամապանակը Bluetooth-ի կամ USB-ի միջոցով", + "connect_your_hardware_wallet_ios": "Միացրեք ձեր ապարատային դրամապանակը Bluetooth-ի միջոցով", + "connection_sync": "Կապ և սինխրոնիզացիա", + "connectWalletPrompt": "Միացրեք ձեր դրամապանակը WalletConnect-ի միջոցով գործարքներ կատարելու համար", + "contact": "Կոնտակտ", + "contact_list_contacts": "Կոնտակտներ", + "contact_list_wallets": "Իմ դրամապանակներ", + "contact_name": "Կոնտակտի անուն", + "contact_support": "Հետադարձ կապ", + "continue_text": "Շարունակել", + "contract_warning": "Պայմանագրի այս հասցեն դրոշմել է որպես հնարավոր կեղծ: Խնդրում ենք զգուշությամբ մշակել:", + "contractName": "Գործարքի անուն", + "contractSymbol": "Գործարքի Նշան", + "copied_key_to_clipboard": "${key} պատճենված է տեքստի բուֆերում", + "copied_to_clipboard": "Պատճենված է տեքստի բուֆերում", + "copy": "Պատճենել", + "copy_address": "Պատճենել հասցեն", + "copy_id": "Պատճենել ID", + "copyWalletConnectLink": "Պատճենել WalletConnect հղումը dApp-ից և տեղադրել այստեղ", + "countries": "Երկրներ", + "create_account": "Ստեղծել հաշիվ", + "create_backup": "Ստեղծել կրկնօրինակ", + "create_donation_link": "Ստեղծել նվիրատվության հղում", + "create_invoice": "Ստեղծել հաշիվ-ապրանքագիր", + "create_new": "Ստեղծել նոր դրամապանակ", + "create_new_account": "Ստեղծել նոր հաշիվ", + "create_new_seed": "Ստեղծեք նոր սերունդ", + "creating_new_wallet": "Նոր դրամապանակ ստեղծվում է", + "creating_new_wallet_error": "Սխալ: ${description}", + "creation_date": "Ստեղծման ամսաթիվ", + "custom": "Պատվերով", + "custom_drag": "Պատվերով (Պահել և Գցել)", + "custom_redeem_amount": "Պատվերով Փրկագնման Գումար", + "custom_value": "Պատվերով Արժեք", + "dark_theme": "Մութ", + "debit_card": "Դեբետային քարտ", + "debit_card_terms": "Ձեր վճարային քարտի համարի (և ձեր վճարային քարտի համարի համապատասխան վկայականներ) պահպանումն ու օգտագործումը այս թվային դրամապանակում ենթակա են վճարային քարտ թողարկող կողմի գործող պայմանների և պայմանագրի", + "decimal_places_error": "Խմբակային տեղերի սխալ", + "decimals_cannot_be_zero": "Խմբակային տեղերը չեն կարող լինել զրո", + "default_buy_provider": "Լռելյայն գնման մատակարար", + "default_sell_provider": "Լռելյայն վաճառքի մատակարար", + "delete": "Ջնջել", + "delete_account": "Ջնջել հաշիվը", + "delete_wallet": "Ջնջել դրամապանակը", + "delete_wallet_confirm_message": "Դուք վստահ եք, որ ուզում եք ջնջել ${wallet_name} դրամապանակը?", + "deleteConnectionConfirmationPrompt": "Դուք վստահ եք, որ ուզում եք ջնջել կապը", + "denominations": "Անվանակարգեր", + "derivationpath": "Ածանցման ուղին", + "descending": "Նվազող", + "description": "Նկարագրություն", + "destination_tag": "Նպատակակետի պիտակ:", + "dfx_option_description": "Գնեք կրիպտոարժույթ EUR և CHF: Կորպորատիվ և մանրածախ հաճախորդների համար Եվրոպայում", + "didnt_get_code": "Չեք ստացել կոդը?", + "digit_pin": "-նիշ ՊԻՆ", + "digital_and_physical_card": " թվային և ֆիզիկական նախավճարային դեբետային քարտ", + "disable": "Անջատել", + "disable_bulletin": "Անջատել ծառայության վիճակի տեղեկագիրը", + "disable_buy": "Անջատել գնում գործողությունը", + "disable_cake_2fa": "Անջատել Cake 2FA", + "disable_exchange": "Անջատել փոխանակումը", + "disable_fee_api_warning": "Դրանից անջատելով, վճարների տեմպերը որոշ դեպքերում կարող են անճիշտ լինել, այնպես որ դուք կարող եք վերջ տալ ձեր գործարքների համար վճարների գերավճարների կամ գերավճարների վրա", + "disable_fiat": "Անջատել ֆիատ", + "disable_sell": "Անջատել վաճառք գործողությունը", + "disableBatteryOptimization": "Անջատել մարտկոցի օպտիմիզացիան", + "disableBatteryOptimizationDescription": "Դուք ցանկանում եք անջատել մարտկոցի օպտիմիզացիան ֆոնային համաժամացման ավելի ազատ և հարթ ընթացքի համար?", + "disabled": "Անջատված", + "discount": "Խնայեք ${value}%", + "display_settings": "Ցուցադրման կարգավորումներ", + "displayable": "Ցուցադրվող", + "do_not_have_enough_gas_asset": "Դուք չունեք բավարար ${currency} տրանզակցիան կատարելու համար ընթացիկ բլոկչեյն ցանցի պայմաններում: Դուք պետք է ունենաք ավելի շատ ${currency} blockchain ցանցի միջնորդավճարները վճարելու համար, նույնիսկ եթե դուք այլ ակտիվ եք ուղարկում:", + "do_not_send": "Մի ուղարկեք", + "do_not_share_warning_text": "Մի կիսեք այս տեղեկատվությունը որևէ մեկի հետ, այդ թվում նաև աջակցության հետ: \n\nՁեր միջոցները կարող են գողանալ կորցնել!", + "do_not_show_me": "Մի ցուցադրեք ինձ նորից", + "domain_looks_up": "Դոմեյնի որոնում", + "donation_link_details": "Նվիրատվության հղումի մանրամասներ", + "e_sign_consent": "Էլեկտրոնային ստորագրության համաձայնություն", + "edit": "Խմբագրել", + "edit_backup_password": "Փոփոխել Կրկնօրինակի Գաղտնաբառը", + "edit_node": "Փոփոխել հանգույց", + "edit_token": "Փոփոխել տոկեն", + "electrum_address_disclaimer": "Մենք ստեղծում ենք նոր հասցե ամեն անգամ, երբ դուք օգտագործում եք այն, բայց նախորդ հասցեները շարունակում են աշխատել", + "email_address": "Էլ. փոստի հասցե", + "enable": "Միացնել", + "enable_mempool_api": "Mempool API ճշգրիտ վճարների եւ ամսաթվերի համար", + "enable_replace_by_fee": "Միացնել փոխարինումը միջնորդավճարով", + "enable_silent_payments_scanning": "Միացնել Լուռ Վճարումների սկանավորումը", + "enabled": "Միացված", + "enter_amount": "Մուտքագրեք գումար", + "enter_backup_password": "Մուտքագրեք կրկնօրինակի գաղտնաբառը", + "enter_code": "Մուտքագրեք կոդը", + "enter_seed_phrase": "Մուտքագրեք սերմի արտահայտությունը", + "enter_totp_code": "Մուտքագրեք TOTP կոդը", + "enter_wallet_password": "Մուտքագրեք դրամապանակի գաղտնաբառը", + "enter_your_note": "Մուտքագրեք ձեր նշումը…", + "enter_your_pin": "Մուտքագրեք ձեր PIN-ը", + "enter_your_pin_again": "Մուտքագրեք ձեր PIN-ը կրկին", + "enterTokenID": "Մուտքագրեք Token ID֊ն", + "enterWalletConnectURI": "Մուտքագրեք WalletConnect URI", + "error": "Սխալ", + "error_dialog_content": "Օպս, մենք սխալ ենք ստացել: \n\nԽնդրում ենք ուղարկել սխալի հաշվետվությունը մեր աջակցության թիմին ծրագիրը բարելավելու համար:", + "error_text_account_name": "Հաշվի անունը կարող է պարունակել միայն տառեր և թվեր և պետք է լինի 1-15 նիշ", + "error_text_address": "Դրամապանակի հասցեն պետք է համապատասխանի կրիպտոարժույթի տեսակին", + "error_text_amount": "Գումարը կարող է պարունակել միայն թվեր", + "error_text_contact_name": "Կոնտակտի անունը չի կարող պարունակել ` , '' \" սիմվոլներ և պետք է լինի 1-32 նիշ", + "error_text_crypto_currency": "Կոտորակային մասը պետք է լինի 12 նիշից ցածր կամ հավասար", + "error_text_fiat": "Գումարը չի կարող գերազանցել մնացորդը: Կոտորակային մասը պետք է լինի 2 նիշից ցածր կամ հավասար", + "error_text_input_above_maximum_limit": "Գումարը ավելի է, քան առավելագույնը", + "error_text_input_below_minimum_limit": "Գումարը ցածր է, քան նվազագույնը", + "error_text_keys": "Դրամապանակի բանալիները կարող են պարունակել միայն 64 նիշ հեքսային կոդով", + "error_text_limits_loading_failed": "Առևտրի համար ${provider} չի ստեղծվել: Սահմանների բեռնում ձախողվել է", + "error_text_maximum_limit": "Առևտրի համար ${provider} չի ստեղծվել: Գումարը ավելի է, քան առավելագույն ${max} ${currency}", + "error_text_minimal_limit": "Առևտրի համար ${provider} չի ստեղծվել: Գումարը ցածր է, քան նվազագույն ${min} ${currency}", + "error_text_node_address": "Խնդրում ենք մուտքագրել iPv4 հասցե", + "error_text_node_port": "Հանգույցի պորտը կարող է պարունակել միայն թվեր 0-65535 միջակայքում", + "error_text_node_proxy_address": "Խնդրում ենք մուտքագրել :<պորտ>, օրինակ 127.0.0.1:9050", + "error_text_payment_id": "Վճարման ID-ն կարող է պարունակել 16-64 նիշ հեքսային կոդով", + "error_text_subaddress_name": "Ենթահասցեի անունը չի կարող պարունակել , '' \" նշանները\nև պետք է լինի 1-ից 20 նիշ երկարությամբ", + "error_text_template": "Տեսակի անունը և հասցեն չեն կարող պարունակել , '' \" նշանները\nև պետք է լինի 1-ից 106 նիշ երկարությամբ", + "error_text_wallet_name": "Գաղտնաբառը կարող է պարունակել միայն տառեր, թվեր, _ - նշաններ\nև պետք է լինի 1-ից 33 նիշ երկարությամբ", + "error_text_xmr": "XMR արժեքը չի կարող գերազանցել հասանելի մնացորդը:\nԿոտորակային թվերի քանակը պետք է լինի 12-ից պակաս կամ հավասար", + "errorGettingCredentials": "Սխալ. ծանրաբեռնված վստահագրեր ստանալիս", + "errorSigningTransaction": "Սխալ է տեղի ունեցել գործարքը ստորագրելիս", + "estimated": "Գնահատված", + "estimated_new_fee": "Գնահատված նոր միջնորդավճար", + "estimated_receive_amount": "Գնահատված ստացված գումար", + "etherscan_history": "Etherscan պատմություն", + "event": "Իրադարձություն", + "events": "Իրադարձություններ", + "exchange": "Փոխանակել", + "exchange_incorrect_current_wallet_for_xmr": "Եթե ​​ցանկանում եք փոխանակել XMR ձեր տորթի դրամապանակից Monero Relandal- ից, խնդրում ենք նախ անցնել ձեր Monero դրամապանակին:", + "exchange_new_template": "Նոր տեսակ", + "exchange_provider_unsupported": "${providerName} այլևս չի ապահովվում", + "exchange_result_confirm": "Սեղմելով հաստատել, դուք կուղարկեք ${fetchingLabel} ${from} ձեր հաշվեհամարից ${walletName} հետևյալ հասցեին: Կամ կարող եք ուղարկել ձեր արտաքին հաշվեհամարից հետևյալ հասցեին/QR կոդին:\n\nԽնդրում ենք սեղմել հաստատել կամ վերադառնալ գումարը փոխելու համար", + "exchange_result_description": "Դուք պետք է ուղարկեք նվազագույնը ${fetchingLabel} ${from} հետևյալ էջում ցուցադրված հասցեին: Եթե դուք ուղարկում եք ավելի ցածր գումար, քան ${fetchingLabel} ${from} այն կարող է չփոխանակվել և չվերադարձվել", + "exchange_result_write_down_ID": "*Խնդրում ենք պատճենել կամ գրել ձեր ID-ն վերևում", + "exchange_result_write_down_trade_id": "Խնդրում ենք պատճենել կամ գրել առևտրի ID-ն շարունակելու համար", + "exchange_sync_alert_content": "Խնդրում ենք սպասել մինչև ձեր հաշվեհամարը համաժամացվի", + "expired": "Վավերականությունը լրացել է", + "expires": "Վավերականությունը լրանում է", + "expiresOn": "Վավերականությունը լրանում է", + "expiry_and_validity": "Վավերականություն և լրացում", + "export_backup": "Արտահանել կրկնօրինակը", + "extra_id": "Լրացուցիչ ID", + "extracted_address_content": "Դուք կուղարկեք գումար ${recipient_name}", + "failed_authentication": "Վավերացումը ձախողվեց. ${state_error}", + "faq": "Հաճախ տրվող հարցեր", + "features": "Հատկանիշներ", + "fee_less_than_min": "Ընտրված վճարը նվազագույնից պակաս է, խնդրում ենք ավելացնել վճարները, որպեսզի կարողանաք գործարքը ուղարկել", + "fee_rate": "Վճարման տոկոսադրույքը", + "fetching": "Ստացվում է", + "fiat_api": "Fiat API", + "fiat_balance": "Fiat մնացորդ", + "field_required": "Այս դաշտը պարտադիր է", + "fill_code": "Խնդրում ենք լրացնել հաստատման կոդը ձեր էլեկտրոնային փոստում", + "filter_by": "Ֆիլտրել ըստ", + "first_wallet_text": "Հիանալի հաշվեհամար Monero, Bitcoin, Ethereum, Litecoin և Haven արժույթների համար", + "fixed_pair_not_supported": "Այս ֆիքսված զույգը չի ապահովվում ընտրված փոխանակման ծառայություններ", + "fixed_rate": "Ֆիքսված փոխարժեք", + "fixed_rate_alert": "Դուք կկարողանաք մուտքագրել ստացվող գումարը, երբ ֆիքսված փոխարժեքի ռեժիմը միացված է: Դուք ցանկանում եք անցնել ֆիքսված փոխարժեքի ռեժիմին?", + "forgot_password": "Մոռացել եմ գաղտնաբառը", + "freeze": "Կասեցնել", + "frequently_asked_questions": "Հաճախ տրվող հարցեր", + "frozen": "Կասեցված", + "full_balance": "Լրիվ մնացորդ", + "generate_name": "Գեներացնել անուն", + "generating_gift_card": "Գեներացնում է նվեր քարտ", + "get_a": "Ստանալ ", + "get_card_note": " որը կարող եք լրացնել թվային արժույթներով: Հավելյալ տեղեկատվություն պետք չէ!", + "get_your_yat": "Ստանալ ձեր Yat", + "gift_card_amount": "Նվեր քարտի չափ", + "gift_card_balance_note": "Նվեր քարտերը մնացորդով կհայտնվեն այստեղ", + "gift_card_is_generated": "Նվեր քարտ գեներացված է", + "gift_card_number": "Նվեր քարտի համար", + "gift_card_redeemed_note": "Նվեր քարտերը, որ դուք գնել եք, կհայտնվեն այստեղ", + "gift_cards": "Նվեր քարտեր", + "gift_cards_unavailable": "Նվեր քարտերը հասանելի են միայն Monero, Bitcoin և Litecoin արժույթների համար", + "got_it": "Հասկացա", + "gross_balance": "Ընդհանուր մնացորդ", + "group_by_type": "Խմբավորել ըստ տեսակի", + "haven_app": "Haven ծրագիր", + "haven_app_wallet_text": "Հիանալի հաշվեհամար Haven համար", + "help": "Օգնություն", + "hidden_balance": "Թաքնված մնացորդ", + "hide_details": "Թաքցնել մանրամասները", + "high_contrast_theme": "Բարձր հակադրության տեսք", + "home_screen_settings": "Գլխավոր էկրանի կարգավորումներ", + "how_to_use": "Ինչպես օգտագործել", + "how_to_use_card": "Ինչպես օգտագործել այս քարտը", + "id": "ID: ", + "ignor": "Անտեսել", + "import": "Ներմուծել", + "importNFTs": "Ներմուծել NFT-ներ", + "in_store": "Տեղում", + "incoming": "Մուտքային", + "incorrect_seed": "Տեքստը սխալ է", + "inputs": "Մուտքեր", + "insufficient_lamport_for_tx": "Դուք չունեք բավարար SOL՝ գործարքն և գործարքի վարձը ծածկելու համար։ Խնդրում ենք ավելացնել ավելի շատ SOL ձեր դրամապանակում կամ նվազեցնել ուղարկվող SOL-ի քանակը։", + "insufficient_lamports": "Դուք չունեք բավարար SOL՝ գործարքն և գործարքի վարձը ծածկելու համար։ Ձեզ անհրաժեշտ է առնվազն ${solValueNeeded} SOL։ Խնդրում ենք ավելացնել ավելի շատ SOL ձեր դրամապանակում կամ նվազեցնել ուղարկվող SOL-ի քանակը։", + "insufficientFundsForRentError": "Ձեր մնացորդը բավարար չէ վարձակալության համար: Խնդրում ենք ավելացնել մնացորդը կամ նվազեցնել ուղարկվող գումարը", + "introducing_cake_pay": "Ներկայացնում ենք Cake Pay!", + "invalid_input": "Սխալ մուտք", + "invalid_password": "Սխալ գաղտնաբառ", + "invoice_details": "Հաշիվ-ապրանքագրի մանրամասներ", + "is_percentage": "կազմում է", + "last_30_days": "Վերջին 30 օրը", + "learn_more": "Տեղեկանալ ավելին", + "ledger_connection_error": "Չկարողացանք կապ հաստատել Ledger-ի հետ: Խնդրում ենք փորձել նորից", + "ledger_error_device_locked": "Ledger-ը արգելափակված է", + "ledger_error_tx_rejected_by_user": "Գործարքը մերժված է օգտատերի կողմից", + "ledger_error_wrong_app": "Խնդրում ենք համոզվել, որ դուք բացել եք ճիշտ ծրագիրը ձեր Ledger-ում", + "ledger_please_enable_bluetooth": "Խնդրում ենք միացնել Bluetooth-ը ձեր Ledger-ը հայտնաբերելու համար", + "light_theme": "Լուսավոր", + "litecoin_mweb_description": "Mweb- ը նոր արձանագրություն է, որը բերում է ավելի արագ, ավելի էժան եւ ավելի մասնավոր գործարքներ դեպի LITECOIN", + "litecoin_mweb_dismiss": "Հեռացնել", + "litecoin_mweb_enable_later": "Կարող եք ընտրել Mweb- ը կրկին միացնել ցուցադրման պարամետրերը:", + "litecoin_mweb_pegin": "Peg in", + "litecoin_mweb_pegout": "Հափշտակել", + "live_fee_rates": "Ապակի վարձավճարներ API- ի միջոցով", + "load_more": "Բեռնել ավելին", + "loading_your_wallet": "Ձեր հաշվեհամարը բեռնում է", + "login": "Մուտք", + "logout": "Ելք", + "low_fee": "Ցածր միջնորդավճար", + "low_fee_alert": "Դուք ներկայումս օգտագործում եք ցածր ցանցային միջնորդավճար: Սա կարող է հանգեցնել երկար սպասելու ժամանակի, տարբեր փոխարժեքների կամ չեղարկված առևտրի: Մենք առաջարկում ենք սահմանել ավելի բարձր միջնորդավճար ավելի լավ փորձ ունենալու համար", + "manage_nodes": "Կարգավորել հանգույցները", + "manage_pow_nodes": "Կարգավորել PoW հանգույցները", + "manage_yats": "Կառավարել Yats", + "mark_as_redeemed": "Նշել որպես մարված", + "market_place": "Շուկա", + "matrix_green_dark_theme": "Matrix Կանաչ Մութ տեսք", + "max_amount": "Առավելագույն գումար: ${value}", + "max_value": "Առավելագույն արժեք: ${value} ${currency}", + "memo": "Մեմո:", + "message": "Հաղորդագրություն", + "message_verified": "Հաղորդագրությունը հաջողությամբ հաստատվեց", + "methods": "Մեթոդներ", + "min_amount": "Նվազը: ${value}", + "min_value": "Նվազը: ${value} ${currency}", + "minutes_to_pin_code": "${minute} րոպե", + "mm": "ԱԱ", + "modify_2fa": "Փոփոխել Cake 2FA", + "monero_com": "Monero.com by Cake Wallet", + "monero_com_wallet_text": "Անհավանական դրամապանակ Monero-ի համար", + "monero_dark_theme": "Monero մութ տեսք", + "monero_light_theme": "Monero պայծառ տեսք", + "moonpay_alert_text": "Գումարի արժեքը պետք է լինի հավասար կամ ավելի քան ${minAmount} ${fiatCurrency}", + "more_options": "Այլ տարբերակներ", + "mweb_confirmed": "Հաստատված MWEB", + "mweb_unconfirmed": "Չկարգավորված Mweb", + "name": "Անուն", + "nano_current_rep": "Ընթացիկ ներկայացուցիչ", + "nano_gpt_thanks_message": "Շնորհակալություն NanoGPT-ն օգտագործելու համար: Հիշեք վերադառնալ դիտարկիչ ձեր փոխանցումն ավարտելուց հետո", + "nano_pick_new_rep": "Ընտրեք նոր ներկայացուցիչ", + "nanogpt_subtitle": "Բոլոր ամենանոր մոդելներ (GPT-4, Claude).\\nԱռանց բաժանորդագրության, վճարեք կրիպտոարժույթով", + "narrow": "Նեղ", + "new_first_wallet_text": "Ինչպես պահել ձեր կրիպտոգրաֆիան անվտանգ, կարկանդակ", + "new_node_testing": "Նոր հանգույցի փորձարկում", + "new_subaddress_create": "Ստեղծել", + "new_subaddress_label_name": "Պիտակի անուն", + "new_subaddress_title": "Նոր հասցե", + "new_template": "Նոր նմուշ", + "new_wallet": "Նոր դրամապանակ", + "newConnection": "Նոր կապ", + "no_cards_found": "Ոչ մի քարտ չի գտնվել", + "no_id_needed": "Ոչ մի փաստաթուղթ չի պահանջվում!", + "no_id_required": "Ոչ մի փաստաթուղթ չի պահանջվում։ Լրացրեք և ծախսեք ամենուր", + "no_relay_on_domain": "Տիրույթի համար ընդունող չկա կամ անհասանելի է։ Խնդրում ենք ընտրել ընդունող", + "no_relays": "Ընդունողներ չկան", + "no_relays_message": "Մենք գտել ենք Nostr NIP-05 գրառում այս օգտատիրոջ համար, բայց այն չի պարունակում ոչ մի ընդունող։ Խնդրում ենք հրահանգել ստացողին ավելացնել ընդունողներ իր Nostr գրառման մեջ", + "node_address": "Հանգույցի հասցե", + "node_connection_failed": "Հանգույցի միացումը ձախողվեց", + "node_connection_successful": "Հանգույցի միացումը հաջող է", + "node_new": "Նոր հանգույց", + "node_port": "Հանգույցի պորտ", + "node_reset_settings_title": "Հանգույցի կարգավորումների վերակայում", + "node_test": "Փորձարկում", + "nodes": "Հանգույցներ", + "nodes_list_reset_to_default_message": "Վերակայվում եք կարգավորումները լռությամբ?", + "none_of_selected_providers_can_exchange": "Ընտրված մատակարարներից ոչ մեկը չի կարող կատարել այս փոխանակումը", + "noNFTYet": "Դեռ ոչ մի NFT", + "normal": "Նորմալ", + "note_optional": "Նշում (ոչ պարտադիր)", + "note_tap_to_change": "Նշում (սեղմեք փոխելու համար)", + "nullURIError": "URI-ն դատարկ է", + "offer_expires_in": "Առաջարկը վաղեմության է հասնում ", + "offline": "Անցանց", + "ok": "Լավ", + "old_fee": "Հին վճար", + "onion_link": "Onion հղում", + "online": "Առցանց", + "onramper_option_description": "Արագ գնեք կրիպտոցուլեր շատ վճարման մեթոդներով։ Հասանելի է մեծ մասամբ երկրներում։ Տարածված և վճարները փոփոխվում են", + "open_gift_card": "Բացեք նվեր քարտ", + "optional_description": "Ոչ պարտադիր նկարագրություն", + "optional_email_hint": "Ոչ պարտադիր վճարողի ծանուցման էլեկտրոնային փոստ", + "optional_name": "Ոչ պարտադիր ստացողի անուն", + "optionally_order_card": "Ոչ պարտադիր պատվեր ֆիզիկական քարտ", + "orbot_running_alert": "Խնդրում ենք համոզվել, որ Orbot-ը աշխատում է այս հանգույցին միանալուց առաջ", + "order_by": "Պատվեր ըստ", + "order_id": "Պատվերի հերթական համար", + "order_physical_card": "Պատվիրել ֆիզիկական քարտ", + "other_settings": "Այլ կարգավորումներ", + "outdated_electrum_wallet_description": "Cake-ում ստեղծված նոր Bitcoin դրամապանակները ներկայում 24 բառանոց սերմ ունեն։ Դուք պետք է ստեղծեք նոր Bitcoin դրամապանակ և տեղափոխեք բոլոր միջոցները նոր 24 բառանոց դրամապանակ, և դադարեցրեք օգտագործել 12 բառանոց դրամապանակներ", + "outdated_electrum_wallet_receive_warning": "Եթե այս դրամապանակը 12 բառանոց սերմ ունի և ստեղծվել է Cake-ում, Bitcoin մուծել այս դրամապանակ մի տեղափոխեք։ Որեւէ Bitcoin այս դրամապանակ տեղափոխվել կարող է կորած լինել։ Ստեղծեք նոր 24-բառանոց Bitcoin դրամապանակ (սեղմեք վերևի աջ մենյուն, ընտրեք Դրամապանակներ, ընտրեք Նոր Դրամապանակ, ապա ընտրեք Bitcoin) և տեղափոխեք ձեր ԲԻՏԿ-ն այնտեղ", + "outgoing": "Ելքային", + "outputs": "Ելքեր", + "overwrite_amount": "Գրեք գումարը", + "pairingInvalidEvent": "Սխալ միացում", + "passphrase": "Պարող արտահայտություն (Ոչ պարտադիր)", + "passphrases_doesnt_match": "Անհատները չեն համընկնում, խնդրում ենք կրկին փորձել", + "password": "Գաղտնաբառ", + "paste": "Տեղադրել", + "pause_wallet_creation": "Հնարավորություն ստեղծել Haven Դրամապանակ ընթացիկ դադարեցված է", + "payment_id": "Վճարման հերթական համար", + "payment_was_received": "Վճարումը ստացված է", + "pending": " (մշակվում է)", + "percentageOf": "${amount}-ի տոկոս", + "pin_at_top": "${token} պին վերևում", + "pin_is_incorrect": "Պին-կոդը սխալ է", + "pin_number": "Պին-կոդ", + "placeholder_contacts": "Ձեր կոնտակտները այստեղ կցուցադրվեն", + "placeholder_transactions": "Ձեր գործարքները այստեղ կցուցադրվեն", + "please_fill_totp": "Խնդրում ենք լրացնել 8-նիշյա կոդը, որը կա ձեր այլ սարքի վրա", + "please_make_selection": "Խնդրում ենք ընտրություն կատարել ներքևում ձեր դրամապանակը ստեղծելու կամ վերականգնելու համար", + "please_reference_document": "Խնդրում ենք դիտել ներքևի փաստաթղթերը ավելի շատ տեղեկատվության համար", + "please_select": "Խնդրում ենք ընտրել", + "please_select_backup_file": "Խնդրում ենք ընտրել կրկնօրինակի ֆայլը և մուտքագրել կրկնօրինակի գաղտնաբառը", + "please_try_to_connect_to_another_node": "Խնդրում ենք փորձել միանալ այլ հանգույցի", + "please_wait": "Խնդրում ենք սպասել", + "polygonscan_history": "PolygonScan պատմություն", + "powered_by": "${title} կողմից ապահովված", + "pre_seed_button_text": "Ես հասկանում եմ։ Ցույց տվեք իմ սերմը", + "pre_seed_description": "Հաջորդ էջում դուք կտեսնեք ${words} բառերի շարք։ Սա ձեր յուրահատուկ և գաղտնի սերմն է, որը ձեր դրամապանակը վերականգնելու միակ միջոցն է կորուստի կամ սխալ գործարքի դեպքում։ Դուք պատասխանատու եք այն գրառել և ապահով վայրում պահել Cake Wallet հավելվածից դուրս", + "pre_seed_title": "ԿԱՐԵՎՈՐ", + "prepaid_cards": "Նախավճարային քարտեր", + "prevent_screenshots": "Կանխել էկրանի պատկերները և տեսագրությունը", + "privacy": "Գաղտնիություն", + "privacy_policy": "Գաղտնիության քաղաքականություն", + "privacy_settings": "Գաղտնիության կարգավորումներ", + "private_key": "Գաղտնի բանալի", + "proceed_after_one_minute": "Եթե էկրանը 1 րոպեից ավել չի անցնում, ստուգեք ձեր էլեկտրոնային փոստը", + "proceed_on_device": "Շարունակեք ձեր սարքի վրա", + "proceed_on_device_description": "Խնդրում ենք հետևել ձեր սարքի վրա ցուցադրվող հրահանգներին", + "profile": "Պրոֆիլ", + "provider_error": "${provider} սխալ", + "public_key": "Հանրային բանալի", + "purchase_gift_card": "Նվեր քարտ գնել", + "purple_dark_theme": "Մանուշակագույն մութ տեսք", + "qr_fullscreen": "Սեղմեք այստեղ բացելու ամբողջ էկրանը QR կոդ", + "qr_payment_amount": "Այս QR կոդը պարունակում է վճարման գումար։ Ուզում եք թարմացնել ընթացիկ արժեքը?", + "quantity": "Քանակ", + "question_to_disable_2fa": "Դուք վստահ եք, որ ուզում եք անջատել Cake 2FA-ն? 2FA կոդը այլևս չի պահանջվի դրամապանակ մուտք գործելու և որոշ ֆունկցիաներ օգտագործելու համար", + "receivable_balance": "Ստացվող հաշվեկշիռ", + "receive": "Ստանալ", + "receive_amount": "Գումար", + "received": "Ստացված", + "recipient_address": "Ստացողի հասցե", + "reconnect": "Վերակապվել", + "reconnect_alert_text": "Դուք վստահ եք, որ ուզում եք վերակապվել?", + "reconnection": "Վերակապում", + "red_dark_theme": "Կարմիր մութ տեսք", + "red_light_theme": "Կարմիր պայծառ տեսք", + "redeemed": "Փոխանակված", + "refund_address": "Վերադարձի հասցե", + "reject": "Մերժել", + "remaining": "Մնացած", + "remove": "Հեռացնել", + "remove_node": "Հեռացնել հանգույց", + "remove_node_message": "Դուք վստահ եք, որ ուզում եք հեռացնել ընտրված հանգույցը?", + "rename": "Վերանվանել", + "rep_warning": "Ներկայացուցիչ նախազգուշացում", + "rep_warning_sub": "Ձեր ներկայացուցիչը չի հայտնվում լավ վիճակում։ Սեղմեք այստեղ նոր ներկայացուցիչ ընտրելու համար", + "repeat_wallet_password": "Վերականգնել դրամապանակի գաղտնաբառ", + "repeated_password_is_incorrect": "Վերականգնված գաղտնաբառը սխալ է։ Խնդրում ենք վերականգնել դրամապանակի գաղտնաբառը", + "require_for_adding_contacts": "Պահանջվում է կոնտակտներ ավելացնելու համար", + "require_for_all_security_and_backup_settings": "Պահանջվում է բոլոր անվտանգության և կրկնօրինակման կարգավորումների համար", + "require_for_assessing_wallet": "Պահանջվում է դրամապանակ մուտք գործելու համար", + "require_for_creating_new_wallets": "Պահանջվում է նոր դրամապանակներ ստեղծելու համար", + "require_for_exchanges_to_external_wallets": "Պահանջվում է արտաքին դրամապանակների հետ փոխանակության համար", + "require_for_exchanges_to_internal_wallets": "Պահանջվում է ներքին դրամապանակների հետ փոխանակության համար", + "require_for_sends_to_contacts": "Պահանջվում է կոնտակտներին ուղարկելու համար", + "require_for_sends_to_internal_wallets": "Պահանջվում է ներքին դրամապանակներ ուղարկելու համար", + "require_for_sends_to_non_contacts": "Պահանջվում է ոչ կոնտակտ անձանց ուղարկելու համար", + "require_pin_after": "Պահանջվում է PIN-ը հետո", + "rescan": "Վերասկանավորել", + "resend_code": "Խնդրում ենք կրկին ուղարկել", + "reset": "Վերասահմանել", + "reset_password": "Վերասահմանել գաղտնաբառը", + "restore_active_seed": "Ակտիվ սերմ", + "restore_address": "Հասցե", + "restore_bitcoin_description_from_keys": "Վերականգնեք ձեր դրամապանակը ձեր գախտնի բանալիներից ստացված WIF տողից", + "restore_bitcoin_description_from_seed": "Վերականգնեք ձեր դրամապանակը 24 բառերի համադրությամբ", + "restore_bitcoin_title_from_keys": "Վերականգնել WIF-ից", + "restore_description_from_backup": "Դուք կարող եք վերականգնել ամբողջ Cake Wallet հավելվածը ձեր կրկնօրինակ ֆայլից", + "restore_description_from_hardware_wallet": "Վերականգնեք Ledger հարդվերային դրամապանակից", + "restore_description_from_keys": "Վերականգնեք ձեր դրամապանակը ձեր գախտնի բանալիների հիման վրա ստեղծված մուտքագրումներից", + "restore_description_from_seed": "Վերականգնեք ձեր դրամապպանակը 25 բառերի կամ 13 բառերի համադրությամբ", + "restore_description_from_seed_keys": "Վերականգնեք ձեր դրամապանակը սերմից/բանալիներից, որը դուք պահպանել եք ապահով վայրում", + "restore_from_date_or_blockheight": "Խնդրում ենք մուտքագրել այն ամսաթիվը, երբ դուք ստեղծել եք այս դրամապանակը։ Կամ, եթե դուք գիտեք բլոկի բարձրությունը, խնդրում ենք մուտքագրել այն", + "restore_from_seed_placeholder": "Խնդրում ենք մուտքագրել կամ տեղադրել ձեր սերմը այստեղ", + "restore_new_seed": "Նոր սերմ", + "restore_next": "Հաջորդ", + "restore_recover": "Վերականգնել", + "restore_restore_wallet": "Վերականգնել դրամապանակ", + "restore_seed_keys_restore": "Սերմ/բանալիներ վերականգնել", + "restore_spend_key_private": "Վճարման բանալի (գախտնի)", + "restore_title_from_backup": "Վերականգնել կրկնօրինակ ֆայլից", + "restore_title_from_hardware_wallet": "Վերականգնել հարդվերային դրամապանակից", + "restore_title_from_keys": "Վերականգնել բանալիներից", + "restore_title_from_seed": "Վերականգնել սերմից", + "restore_title_from_seed_keys": "Վերականգնել սերմ/բանալիներից", + "restore_view_key_private": "Տեսողական բանալի (գախտնի)", + "restore_wallet": "Վերականգնել դրամապանակ", + "restore_wallet_name": "Դրամապանակի անվանում", + "restore_wallet_restore_description": "Դրամապանակի վերականգնման նկարագրություն", + "robinhood_option_description": "Գնեք և տեղափոխեք ակնթարթորեն ձեր դեբետային քարտով, բանկային հաշվով կամ Robinhood հաշվեկշռով։ Միայն ԱՄՆ-ում։", + "router_no_route": "Ոչ մի երթուղի չի սահմանված ${name}", + "save": "Պահպանել", + "save_backup_password": "Խնդրում ենք հաստատել, որ դուք պահպանել եք ձեր կրկնօրինակի գաղտնաբառը։ Դուք չեք կարող ներմուծել ձեր կրկնօրինակ ֆայլերը առանց դրա։", + "save_backup_password_alert": "Պահպանել կրկնօրինակի գաղտնաբառը", + "save_to_downloads": "Պահպանել ներբեռնումներում", + "saved_the_trade_id": "Ես պահպանել եմ առևտրի ID-ն", + "scan_one_block": "Սկանավորել մեկ բլոկ", + "scan_qr_code": "Սկանավորել QR կոդ", + "scan_qr_code_to_get_address": "Սկանավորել QR կոդը հասցեն ստանալու համար", + "scan_qr_on_device": "Սկանավորել այս QR կոդը այլ սարքի վրա", + "search": "Որոնել", + "search_add_token": "Որոնել / Ավելացնել token", + "search_category": "Որոնել կատեգորիա", + "search_currency": "Որոնել արժույթ", + "search_language": "Որոնել լեզու", + "second_intro_content": "Ձեր Yat-ը միակ եզակի էմոջի հասցե է, որը փոխարինում է ձեր բոլոր արժույթների համար ձեր բոլոր երկար հեքսադեցիմալ հասցեները", + "second_intro_title": "Մի էմոջի հասցե, որը կառավարում է դրանք բոլորը", + "security_and_backup": "Անվտանգություն և կրկնօրինակ", + "seed_alert_back": "Վերադառնալ", + "seed_alert_content": "Սերմը վերականգնելու ձեր դրամապանակի միակ միջոցն է։ Դուք արդեն գրի եք առել այն?", + "seed_alert_title": "Ուշադրություն", + "seed_alert_yes": "Այո, ես արդեն գրի եմ առել այն", + "seed_choose": "Ընտրել սերմի լեզուն", + "seed_hex_form": "Դրամապանակի սերմ (hex ֆորմատ)", + "seed_key": "Սերմի բանալի", + "seed_language": "Սերմի լեզու", + "seed_language_chinese": "Չինարեն", + "seed_language_chinese_traditional": "Չինարեն (ավանդական)", + "seed_language_czech": "Չեխերեն", + "seed_language_dutch": "Հոլանդերեն", + "seed_language_english": "Անգլերեն", + "seed_language_french": "Ֆրանսերեն", + "seed_language_german": "Գերմաներեն", + "seed_language_italian": "Իտալերեն", + "seed_language_japanese": "Ճապոներեն", + "seed_language_korean": "Կորեերեն", + "seed_language_next": "Հաջորդ", + "seed_language_portuguese": "Պորտուգալերեն", + "seed_language_russian": "Ռուսերեն", + "seed_language_spanish": "Իսպաներեն", + "seed_phrase_length": "Սերմի արտահայտության երկարություն", + "seed_reminder": "Խնդրում ենք գրի առնել այս տեղեկությունը, եթե դուք կորցնեք կամ ջնջեք ձեր հեռախոսը", + "seed_share": "Կիսվել սերմով", + "seed_title": "Սերմ", + "seedtype": "Սերմի տեսակ", + "seedtype_alert_content": "Այլ դրամապանակներով սերմերի փոխանակումը հնարավոր է միայն BIP39 SEEDTYPE- ով:", + "seedtype_alert_title": "SEEDTYPE ALERT", + "seedtype_legacy": "Legacy (25 բառ)", + "seedtype_polyseed": "Polyseed (16 բառ)", + "seedtype_wownero": "Wownero (14 բառ)", + "select_backup_file": "Ընտրել կրկնօրինակ ֆայլ", + "select_buy_provider_notice": "Ընտրեք գնման մատակարարը վերևում։ Դուք կարող եք բաց թողնել այս էկրանը ձեր լռելայն գնման մատակարարը հավելվածի կարգավորումներում սահմանելով", + "select_destination": "Խնդրում ենք ընտրել կրկնօրինակ ֆայլի նպատակակետը", + "select_sell_provider_notice": "Ընտրեք վաճառքի մատակարարը վերևում։ Դուք կարող եք բաց թողնել այս էկրանը ձեր լռելայն վաճառքի մատակարարը հավելվածի կարգավորումներում սահմանելով", + "sell": "Ծախել", + "sell_alert_content": "Մենք ներկայումս պաշտպանում ենք միայն Bitcoin, Ethereum և Litecoin վաճառքը։ Խնդրում ենք ստեղծել կամ միացնել ձեր Bitcoin, Ethereum կամ Litecoin դրամապանակը", + "sell_monero_com_alert_content": "Monero-ի վաճառքը դեռ չի պաշտպանվում", + "send": "Ուղարկել", + "send_address": "${cryptoCurrency} հասցե", + "send_amount": "Քանակ՝", + "send_change_to_you": "Փոփոխություն, ձեզ համար.", + "send_creating_transaction": "Ստեղծել գործարք", + "send_error_currency": "Արժույթը կարող է պարունակել միայն թվեր", + "send_error_minimum_value": "Քանակի նվազագույն արժեքը 0.01 է", + "send_estimated_fee": "Գնահատված վարձը՝", + "send_fee": "Վարձը՝", + "send_name": "Անվանում", + "send_new": "Նոր", + "send_payment_id": "Վճարման ID (կամավոր)", + "send_priority": "Ներկայումս վարձը սահմանված է ${transactionPriority} առաջնահերթությամբ։ Գործարքի առաջնահերթությունը կարող է կարգավորվել կարգավորումներում", + "send_sending": "Ուղարկվում է...", + "send_success": "Ձեր ${crypto}-ն հաջողությամբ ուղարկվել է", + "send_templates": "Տեղակալներ", + "send_title": "Ուղարկել", + "send_to_this_address": "Ուղարկել ${currency} ${tag} այս հասցեով", + "send_xmr": "Ուղարկել XMR", + "send_your_wallet": "Ձեր դրամապանակ", + "sending": "Ուղարկում", + "sent": "Ուղարկված", + "service_health_disabled": "Ծառայության առողջությունը անջատված է", + "service_health_disabled_message": "Սա ծառայության առողջության էջն է, դուք կարող եք այս էջը միացնել Կարգավորումներ -> Գաղտնիություն", + "settings": "Կարգավորումներ", + "settings_all": "Բոլորը", + "settings_allow_biometrical_authentication": "Թույլատրել կենսաչափական վավերացում", + "settings_can_be_changed_later": "Այս կարգավորումները կարող են փոխվել ավելի ուշ հավելվածի կարգավորումներում", + "settings_change_language": "Փոխել լեզուն", + "settings_change_pin": "Փոխել PIN-ը", + "settings_currency": "Արժույթ", + "settings_current_node": "Ներկայիս հանգույց", + "settings_dark_mode": "Մութ ռեժիմ", + "settings_display_balance": "Ցուցադրել հաշիվը", + "settings_display_on_dashboard_list": "Ցուցադրել վահանակի ցանկում", + "settings_fee_priority": "Վարձի առաջնահերթություն", + "settings_nodes": "Հանգույցներ", + "settings_none": "Ոչինչ", + "settings_only_trades": "Միայն առևտուր", + "settings_only_transactions": "Միայն փոխանցումներ", + "settings_personal": "Անձնական", + "settings_save_recipient_address": "Պահպանել ստացողի հասցե", + "settings_support": "Աջակցություն", + "settings_terms_and_conditions": "Պայմաններ և պայմանավորվածություններ", + "settings_title": "Կարգավորումներ", + "settings_trades": "Առևտուր", + "settings_transactions": "Փոխանցումներ", + "settings_wallets": "Դրամապանակներ", + "setup_2fa": "Հավատարմագրել 2FA", + "setup_2fa_text": "Cake 2FA-ն աշխատում է TOTP-ի հետ որպես երկրորդ հավատարմագրման գործոն։\n\nCake 2FA-ի TOTP-ը պահանջում է SHA-512 և 8 թվանշանների աջակցություն; դա ավելի մեծ անվտանգություն է ապահովում։ Ավելի շատ տեղեկություն և աջակցվող հավելվածներ կարելի է գտնել ուղեցույցում։", + "setup_pin": "Հավատարմագրել PIN", + "setup_successful": "Ձեր PIN-ը հաջողությամբ հավատարմագրվել է!", + "setup_totp_recommended": "Հավատարմագրել TOTP", + "setup_warning_2fa_text": "Cake 2FA-ն երկրորդ վավերացում է որոշակի գործողությունների համար դրամապանակում։ Այն նույն անվտանգ չէ, ինչ ցուրտ պահեստավորումը.\n\nԵթե դուք կորցնեք ձեր 2FA հավելվածի կամատեղությունը կամ TOTP բանալիները, դուք կկորցնեք այս դրամապանակի հասանելիությունը։ Դուք կստիպվեք վերականգնել ձեր դրամապանակը մնեմոնիկ սերմերի միջոցով։\n\nCake աջակցությունը չի կարող օգնել ձեզ, եթե դուք կորցնեք ձեր 2FA կամ մնեմոնիկ սերմերը։ Խնդրում ենք կարդալ ուղեցույցը, նախքան Cake 2FA-ն օգտագործելը", + "setup_your_debit_card": "Հավատարմագրել ձեր դեբետային քարտ", + "share": "Կիսվել", + "share_address": "Կիսվել հասցեով", + "shared_seed_wallet_groups": "Համօգտագործված սերմերի դրամապանակների խմբեր", + "show_details": "Ցուցադրել մանրամասներ", + "show_keys": "Ցուցադրել բանալիներ", + "show_market_place": "Ցուցադրել շուկան", + "show_seed": "Ցուցադրել սերմ", + "sign_message": "Կնքել հաղորդագրություն", + "sign_up": "Գրանցվել", + "sign_verify_message": "Նշեք / ստուգեք", + "sign_verify_message_sub": "Կնքել կամ ստուգել հաղորդագրությունը ձեր գախտնի բանալիով", + "sign_verify_title": "Կնքել / Ստուգել", + "signature": "Կնիք", + "signature_invalid_error": "Կնիքը անվավեր է տրված հաղորդագրության համար", + "signTransaction": "Կնքել Գործարք", + "signup_for_card_accept_terms": "Գրանցվել քարտի համար և ընդունել պայմանները", + "silent_payment": "Լուռ վճարում", + "silent_payments": "Լուռ Վճարումներ", + "silent_payments_always_scan": "Միացնել Լուռ Վճարումներ մշտական սկանավորումը", + "silent_payments_disclaimer": "Նոր հասցեները նոր ինքնություն չեն։ Դա այլ պիտակով գոյություն ունեցող ինքնության վերագործածում է", + "silent_payments_display_card": "Ցուցադրել Լուռ Վճարումներ քարտը", + "silent_payments_scan_from_date": "Սկանավորել ամսաթվից", + "silent_payments_scan_from_date_or_blockheight": "Խնդրում ենք մուտքագրել բլոկի բարձրությունը, որտեղից դուք ցանկանում եք սկսել սկանավորել մուտքային Լուռ Վճարումները կամ տեղափոխել ամսաթվի փոխարեն։ Դուք կարող եք ընտրել, արդյոք դրամապանակը շարունակելու է սկանավորել ամեն բլոկ կամ ստուգել միայն սահմանված բարձրությունը", + "silent_payments_scan_from_height": "Բլոկի բարձրությունից սկանավորել", + "silent_payments_scanned_tip": "ՍԿԱՆԱՎՈՐՎԵՑ ԹԵՅԱՎՃԱՐ! (${tip})", + "silent_payments_scanning": "Լուռ Վճարումներ Սկանավորում", + "silent_payments_settings": "Լուռ Վճարումներ Կարգավորումներ", + "single_seed_wallets_group": "Մեկ սերմերի դրամապանակներ", + "slidable": "Սահելի", + "sort_by": "Դասավորել ըստ", + "spend_key_private": "Վճարման բանալի (գախտնի)", + "spend_key_public": "Վճարման բանալի (հանրային)", + "status": "Կարգավիճակ՝ ", + "string_default": "Լռելայն", + "subaddress_title": "Ենթահասցեների ցանկ", + "subaddresses": "Ենթահասցեներ", + "submit_request": "Ուղարկել հարցում", + "successful": "Հաջողված", + "support_description_guides": "Տարածված խնդիրների փաստաթղթավորում և աջակցություն", + "support_description_live_chat": "Անվճար և արագ աջակցություն! Պատրաստված մասնագետները պատրաստ են օգնել", + "support_description_other_links": "Միացեք մեր համայնքին կամ հասեք մեզ այլ միջոցներով", + "support_title_guides": "Cake Wallet ուղեցույց", + "support_title_live_chat": "Անմիջական աջակցություն", + "support_title_other_links": "Այլ աջակցության հղումներ", + "sweeping_wallet": "Դրամապանակը մաքրվում է", + "sweeping_wallet_alert": "Սա չի տևի երկար։ Խնդրում ենք չլքել այս էկրանը կամ մաքրված միջոցները կկորչեն։", + "switchToETHWallet": "Խնդրում ենք անցնել Ethereum դրամապանակ և փորձել կրկին", + "switchToEVMCompatibleWallet": "Խնդրում ենք անցնել EVM համատեղելի դրամապանակ և փորձել կրկին (Ethereum, Polygon)", + "symbol": "Նշան", + "sync_all_wallets": "Համաժամեցնել բոլոր դրամապանակները", + "sync_status_attempting_scan": "Փորձի սկան", + "sync_status_attempting_sync": "ՀԱՄԱԺԱՄԵՑՄԱՆ ՓՈՐՁ", + "sync_status_connected": "ՄԻԱՑՎԱԾԷ", + "sync_status_connecting": "ՄԻԱՑՎՈՒՄ Է", + "sync_status_failed_connect": "ՉՄԻԱՑԱՎ", + "sync_status_not_connected": "ՄԻԱՑՎԱԾ ՉԷ", + "sync_status_starting_scan": "Սկսած սկան (${height})", + "sync_status_starting_sync": "ՀԱՄԱԺԱՄԵՑՈՒՄԸ ՍԿՍՎՈՒՄ Է", + "sync_status_syncronized": "ՀԱՄԱԺԱՄԵՑՎԱԾԷ", + "sync_status_syncronizing": "ՀԱՄԱԺԱՄԵՑՎՈՒՄ Է", + "sync_status_timed_out": "Ժամանակը սպառվեց", + "sync_status_unsupported": "ՉԱՋԱԿՑՎՈՂ ՀԱՆԳՈՒՅՑ,", + "syncing_wallet_alert_content": "Ձեր հաշիվը և գործարքների ցանկը կարող են լինել անավարտ մինչև վերին մասում գրված լինի “ՀԱՂՈՒՄ”։ Սեղմեք/դիպեք ուսումնասիրելու համար։", + "syncing_wallet_alert_title": "Ձեր դրամապանակը համաժամացվում է", + "template": "Տարբերակ", + "template_name": "Տարբերակի անուն", + "testnet_coins_no_value": "Testnet արժույթները չունեն արժեք", + "third_intro_content": "Yats-ը ապրում է Cake Wallet-ի դրսում ևս: Երկրի ցանկացած դրամապանակի հասցե կարող է փոխարինվել Yat-ով!", + "third_intro_title": "Yat-ը լավ է համագործակցում ուրիշների հետ", + "thorchain_contract_address_not_supported": "THORChain-ը չի աջակցում պայմանագրի հասցե ուղարկելուն", + "thorchain_taproot_address_not_supported": "ThorChain մատակարարը չի աջակցում Taproot հասցեները: Խնդրում ենք փոխել հասցեն կամ ընտրել այլ մատակարար:", + "time": "${minutes}ր ${seconds}վ", + "tip": "Թեյավճար", + "today": "Այսօր", + "token_contract_address": "Token-ի պայմանագրի հասցե", + "token_decimal": "Token-ի տասանիշ", + "token_name": "Token-ի անուն, օրինակ՝ Tether", + "token_symbol": "Token-ի նշան, օրինակ՝ USDT", + "tokenID": "ID", + "tor_connection": "Tor կապ", + "tor_only": "Միայն Tor", + "total": "Ընդհանուր", + "total_saving": "Ընդհանուր խնայողություն", + "totp_2fa_failure": "Սխալ կոդ: Խնդրում ենք փորձել այլ կոդ կամ ստեղծել նոր գաղտնի բանալի: Օգտագործեք համատեղելի 2FA ծրագիր, որը աջակցում է 8-նիշերի կոդեր և SHA512:", + "totp_2fa_success": "Հաջողություն: Cake 2FA-ն ակտիվացված է այս դրամապանակի համար: Հիշեք Ձեր մնեմոնիկ սերմը, եթե կորցնեք դրամապանակի հասանելիությունը:", + "totp_auth_url": "TOTP հեղինակավորման URL", + "totp_code": "TOTP կոդ", + "totp_secret_code": "TOTP գաղտնի կոդ", + "totp_verification_success": "Ստուգումը հաջողվել է", + "track": "Հետևել", + "trade_details_copied": "${title} պատճենված է գրանցամատյան", + "trade_details_created_at": "Ստեղծվել է", + "trade_details_fetching": "Բեռնում", + "trade_details_id": "ID", + "trade_details_pair": "Զույգ", + "trade_details_provider": "Մատակարար", + "trade_details_state": "Կարգավիճակ", + "trade_details_title": "Գործարքի մանրամասներ", + "trade_for_not_created": "${title} գործարք չի ստեղծվել", + "trade_history_title": "Գործարքների պատմություն", + "trade_id": "Գործարքի ID", + "trade_id_not_found": "${tradeId} ${title} գործարք չի գտնվել", + "trade_is_powered_by": "Այս գործարքը աջակցվում է ${provider} կողմից", + "trade_not_created": "Գործարք չի ստեղծվել", + "trade_not_found": "Գործարք չի գտնվել", + "trade_state_btc_sent": "BTC ուղարկված է", + "trade_state_complete": "Ավարտված է", + "trade_state_confirming": "Հաստատվում է", + "trade_state_created": "Ստեղծված է", + "trade_state_finished": "Ավարտված է", + "trade_state_paid": "Վճարված է", + "trade_state_paid_unconfirmed": "Վճարված է, բայց չի հաստատվել", + "trade_state_pending": "Սպասման մեջ է", + "trade_state_timeout": "Ժամանակը սպառվել է", + "trade_state_to_be_created": "Ստեղծվելու է", + "trade_state_traded": "Գործարք է կատարվել", + "trade_state_trading": "Գործարք է կատարվում", + "trade_state_underpaid": "Վճարված է, բայց ոչ ամբողջությամբ", + "trade_state_unpaid": "Վճարված չէ", + "trades": "Գործարքներ", + "transaction_details_amount": "Գումար", + "transaction_details_copied": "${title} պատճենված է գրանցամատյան", + "transaction_details_date": "Ամսաթիվ", + "transaction_details_fee": "Տուրք", + "transaction_details_height": "Բլոկի համար", + "transaction_details_recipient_address": "Ստացողի հասցե", + "transaction_details_source_address": "Ուղարկողի հասցե", + "transaction_details_title": "Գործարքի մանրամասներ", + "transaction_details_transaction_id": "Գործարքի ID", + "transaction_key": "Գործարքի բանալի", + "transaction_priority_fast": "Արագ", + "transaction_priority_fastest": "Ամենաարագ", + "transaction_priority_medium": "Միջին", + "transaction_priority_regular": "Սովորական", + "transaction_priority_slow": "Դանդաղ", + "transaction_sent": "Փոխանցումն ուղարկված է", + "transaction_sent_notice": "Եթե էկրանը 1 րոպեի ընթացքում չի թարմանում, խնդրում ենք ստուգել բլոկի բացահայտիչը և Ձեր էլ. փոստը", + "transactions": "Փոխանցումներ", + "transactions_by_date": "Փոխանցումներ ամսաթվով", + "trongrid_history": "TronGrid պատմություն", + "trusted": "Վստահելի", + "tx_commit_exception_no_dust_on_change": "Փոխանցումը մերժվել է այս գումարով: Այս արժույթներով կարող եք ուղարկել ${min} առանց փոփոխության կամ ${max} որը վերադարձնում է փոփոխությունը", + "tx_commit_failed": "Փոխանցումը ձախողվել է: Խնդրում ենք դիմել աջակցությանը", + "tx_commit_failed_no_peers": "Գործարքը չի հաջողվել հեռարձակել, խնդրում ենք կրկին փորձել մեկ վայրկյանում", + "tx_invalid_input": "Դուք օգտագործում եք սխալ մուտքային տիպ այս տեսակի վճարման համար", + "tx_no_dust_exception": "Փոխանցումը մերժվել է շատ փոքր գումարով: Խնդրում ենք փորձել ավելացնել գումարը", + "tx_not_enough_inputs_exception": "Չկան բավարար մուտքեր: Խնդրում ենք ընտրել ավելին Coin Control֊ում", + "tx_rejected_bip68_final": "Փոխանցումը մերժվել է չհաստատված մուտքերով և ձախողվել է փոխարինել վճարման համար", + "tx_rejected_dust_change": "Փոխանցումը մերժվել է ցածր փոփոխության գումարով (փոշի): Փորձեք ուղարկել ԲՈԼՈՐ կամ նվազեցնել գումարը", + "tx_rejected_dust_output": "Փոխանցումը մերժվել է ցածր ելքային գումարով (փոշի): Խնդրում ենք ավելացնել գումարը", + "tx_rejected_dust_output_send_all": "Փոխանցումը մերժվել է ցածր ելքային գումարով (փոշի): Խնդրում ենք ստուգել արժույթների հաշիվը մուտքային վերահսկողության տակ", + "tx_rejected_vout_negative": "Բավարար մնացորդ չկա այս փոխանցման վճարների համար։ Խնդրում ենք ստուգել արժույթների մնացորդը Coin Control-ում։", + "tx_wrong_balance_exception": "Դուք չունեք բավարար ${currency} այս գումարը ուղարկելու համար։", + "tx_wrong_balance_with_amount_exception": "Դուք չունեք բավարար ${currency} ${amount} գումարը ուղարկելու համար։", + "tx_zero_fee_exception": "Չի կարող ուղարկվել Փոխանցումը առանց վճարի։ Փորձեք բարձրացնել գինը կամ ստուգել ձեր կապը վերջին գնահատականների համար։", + "unavailable_balance": "Անհասանելի մնացորդ", + "unavailable_balance_description": "Անհասանելի մնացորդ: Այս ընդհանուր գումարը ներառում է այն միջոցները, որոնք արգելափակված են ընթացիկ գործարքներում և նրանք, որոնք դուք ակտիվորեն սառեցրել եք արժույթների վերահսկման կարգավորումների մեջ։ Արգելափակված մնացորդները կդառնան հասանելի, երբ համապատասխան գործարքները կավարտվեն, մինչդեռ սառեցված մնացորդները կմնան անհասանելի մինչև դուք որոշեք դրանք ապասառեցնել։", + "unconfirmed": "Չհաստատված մնացորդ", + "understand": "Ես հասկանում եմ", + "unlock": "Բացել", + "unmatched_currencies": "Ձեր ընթացիկ դրամապանակի արժույթը չի համապատասխանում սկանավորված QR կոդի արժույթին", + "unspent_change": "Մնացորդ", + "unspent_coins_details_title": "Չծախսված արժույթների մանրամասները", + "unspent_coins_title": "Չծախսված արժույթներ", + "unsupported_asset": "Մենք չենք աջակցում այս գործողությունը այս ակտիվի համար։ Խնդրում ենք ստեղծել կամ անցնել աջակցվող ակտիվի տեսակին համապատասխան դրամապանակի։", + "uptime": "Աշխատանքային ժամանակ", + "upto": "մինչև ${value}", + "usb": "USB", + "use": "Փոխեք ", + "use_card_info_three": "Օգտագործեք թվային քարտը առցանց կամ անշփման վճարման մեթոդներով։", + "use_card_info_two": "Միջոցները փոխարկվում են ԱՄՆ դոլար երբ դրանք պահվում են կանխավճարային հաշվեկշռում, ոչ թե թվային արժույթներում։", + "use_ssl": "Օգտագործել SSL", + "use_suggested": "Օգտագործել առաջարկվածը", + "use_testnet": "Օգտագործել Testnet", + "value": "Արժեք", + "value_type": "Արժեքի տեսակ", + "variable_pair_not_supported": "Այս փոփոխականի զույգը չի աջակցվում ընտրված բորսաների հետ", + "verification": "Ստուգում", + "verify_message": "Ստուգել հաղորդագրությունը", + "verify_with_2fa": "Ստուգեք Cake 2FA-ով", + "version": "Տարբերակ ${currentVersion}", + "view_all": "Դիտել բոլորը", + "view_in_block_explorer": "Դիտել Block Explorer-ում", + "view_key_private": "Դիտել բանալին (գախտնի)", + "view_key_public": "Դիտել բանալին (հանրային)", + "view_transaction_on": "Դիտել Գործարքը ", + "voting_weight": "Քվեարկության Քաշ", + "waitFewSecondForTxUpdate": "Խնդրում ենք սպասել մի քանի վայրկյան, որպեսզի գործարքը արտացոլվի գործարքների պատմության մեջ", + "wallet_group": "Դրամապանակների խումբ", + "wallet_group_description_four": "Ամբողջովին նոր սերմով դրամապանակ ստեղծելու համար:", + "wallet_group_description_one": "Տորթի դրամապանակում կարող եք ստեղծել ա", + "wallet_group_description_three": "Տեսնել առկա դրամապանակներն ու (կամ) դրամապանակների խմբերի էկրանը: Կամ ընտրել", + "wallet_group_description_two": "ընտրելով գոյություն ունեցող դրամապանակ `սերմը կիսելու համար: Դրամապանակների յուրաքանչյուր խումբ կարող է պարունակել յուրաքանչյուր արժույթի տեսակի մեկ դրամապանակ:\n\nԿարող եք ընտրել", + "wallet_group_empty_state_text_one": "Կարծես թե որեւէ համատեղելի դրամապանակի խմբեր չունեք:\n\nԹակել", + "wallet_group_empty_state_text_two": "ներքեւում `նորը կազմելու համար:", + "wallet_keys": "Դրամապանակի սերմ/բանալիներ", + "wallet_list_create_new_wallet": "Ստեղծել Նոր Դրամապանակ", + "wallet_list_edit_group_name": "Խմբագրել խմբի անվանումը", + "wallet_list_edit_wallet": "Խմբագրել դրամապանակը", + "wallet_list_failed_to_load": "Չհաջողվեց բեռնել ${wallet_name} դրամապանակը։ ${error}", + "wallet_list_failed_to_remove": "Չհաջողվեց հեռացնել ${wallet_name} դրամապանակը։ ${error}", + "wallet_list_load_wallet": "Բեռնել դրամապանակը", + "wallet_list_loading_wallet": "Բեռնվում է ${wallet_name} դրամապանակը", + "wallet_list_removing_wallet": "Հեռացվում է ${wallet_name} դրամապանակը", + "wallet_list_restore_wallet": "Վերականգնել դրամապանակը", + "wallet_list_title": "Monero Դրամապանակ", + "wallet_list_wallet_name": "Դրամապանակի անունը", + "wallet_menu": "Մենյու", + "wallet_name": "Դրամապանակի անունը", + "wallet_name_exists": "Այդ անունով դրամապանակ արդեն կա։ Խնդրում ենք ընտրել այլ անուն կամ վերանվանել մյուս դրամապանակը։", + "wallet_password_is_empty": "Դրամապանակի գաղտնաբառը դատարկ է։ Դրամապանակի գաղտնաբառը չպետք է դատարկ լինի", + "wallet_recovery_height": "Վերականգնման բարձրություն", + "wallet_restoration_store_incorrect_seed_length": "Սերմի երկարությունը սխալ է", + "wallet_seed": "Դրամապանակի սերմ", + "wallet_seed_legacy": "Legacy դրամապանակի սերմ", + "wallet_store_monero_wallet": "Monero Դրամապանակ", + "walletConnect": "WalletConnect", + "wallets": "Դրամապանակներ", + "warning": "Զգուշացում", + "welcome": "Բարի գալուստ", + "welcome_to_cakepay": "Բարի գալուստ Cake Pay!", + "what_is_silent_payments": "Ի՞նչ է Լուռ Վճարումները:", + "widgets_address": "Հասցե", + "widgets_or": "կամ", + "widgets_restore_from_blockheight": "Վերականգնել բլոկի բարձրությունից", + "widgets_restore_from_date": "Վերականգնել ամսաթվից", + "widgets_seed": "Սերմ", + "wouoldLikeToConnect": "ցանկանում է միանալ", + "write_down_backup_password": "Խնդրում ենք գրանցել ձեր կրկնօրինակի գաղտնաբառը, որը օգտագործվում է կրկնօրինակ ֆայլերի ներմուծման համար։", + "xlm_extra_info": "Խնդրում ենք չմոռանալ նշել Memo ID-ն երբ ուղարկում եք XLM գործարք փոխանակման համար", + "xmr_available_balance": "Հասանելի մնացորդ", + "xmr_full_balance": "Լրիվ մնացորդ", + "xmr_hidden": "Թաքնված", + "xmr_to_error": "XMR.TO սխալ", + "xmr_to_error_description": "Սխալ գումար։ Առավելագույն սահմանը 8 թվանշան է կետից հետո", + "xrp_extra_info": "Խնդրում ենք չմոռանալ նշել Destination Tag-ը երբ ուղարկում եք XRP գործարք փոխանակման համար", + "yat": "Yat", + "yat_address": "Yat Հասցե", + "yat_alert_content": "Cake Wallet-ի օգտատերերը այժմ կարող են ուղարկել և ստանալ իրենց սիրած արժույթները emoji-ների հիման վրա օգտագործվող անունով։", + "yat_alert_title": "Ուղարկել և ստանալ կրիպտո ավելի հեշտությամբ Yat-ի միջոցով", + "yat_error": "Yat սխալ", + "yat_error_content": "Այս Yat-ի հետ կապված հասցեներ չկան։ Փորձեք այլ Yat", + "yat_popup_content": "Այժմ դուք կարող եք ուղարկել և ստանալ կրիպտո Cake Wallet-ում ձեր Yat-ով՝ կարճ, emoji-ների վրա հիմնված օգտագործողի անունով։ Կառավարեք Yat-երը ցանկացած ժամանակ կարգավորումների էկրանին", + "yat_popup_title": "Ձեր դրամապանակի հասցեն կարող է emoji-ացվել։", + "yesterday": "Երեկ", + "you_now_have_debit_card": "Դուք այժմ ունեք դեբետային քարտ", + "you_pay": "Դուք վճարում եք", + "you_will_get": "Ստացեք", + "you_will_send": "Փոխանակեք", + "yy": "ՏՏ" +} \ No newline at end of file diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index 2bc298aa7..5f93082ec 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -36,6 +36,7 @@ "agree": "Setuju", "agree_and_continue": "Setuju & Lanjutkan", "agree_to": "Dengan membuat akun Anda setuju dengan ", + "alert_notice": "Melihat", "all": "SEMUA", "all_trades": "Semua perdagangan", "all_transactions": "Semua transaksi", @@ -113,7 +114,7 @@ "change_currency": "Ganti Mata Uang", "change_current_node": "Apakah Anda yakin ingin mengubah node saat ini menjadi ${node}?", "change_current_node_title": "Ubah node saat ini", - "change_exchange_provider": "Ganti Penyedia Tukar", + "change_exchange_provider": "Ubah penyedia swap", "change_language": "Ganti bahasa", "change_language_to": "Ganti bahasa ke ${language}?", "change_password": "Ubah kata sandi", @@ -130,6 +131,7 @@ "choose_one": "Pilih satu", "choose_relay": "Silakan pilih relai yang akan digunakan", "choose_wallet_currency": "Silahkan pilih mata uang dompet:", + "choose_wallet_group": "Pilih Grup Dompet", "clear": "Hapus", "clearnet_link": "Tautan clearnet", "close": "Menutup", @@ -142,6 +144,7 @@ "confirm_delete_wallet": "Tindakan ini akan menghapus dompet ini. Apakah Anda ingin melanjutkan?", "confirm_fee_deduction": "Konfirmasi pengurangan biaya", "confirm_fee_deduction_content": "Apakah Anda setuju untuk mengurangi biaya dari output?", + "confirm_passphrase": "Konfirmasi frasa sandi", "confirm_sending": "Konfirmasi pengiriman", "confirm_silent_payments_switch_node": "Node Anda saat ini tidak mendukung pembayaran diam \\ ncake Wallet akan beralih ke simpul yang kompatibel, hanya untuk pemindaian", "confirmations": "Konfirmasi", @@ -160,6 +163,7 @@ "contact_name": "Nama Kontak", "contact_support": "Hubungi Dukungan", "continue_text": "Lanjutkan", + "contract_warning": "Alamat kontrak ini telah ditandai sebagai berpotensi curang. Silakan memproses dengan hati -hati.", "contractName": "Nama Kontrak", "contractSymbol": "Simbol Kontrak", "copied_key_to_clipboard": "Kunci ${key} disalin ke Clipboard", @@ -175,6 +179,7 @@ "create_invoice": "Buat faktur", "create_new": "Buat Dompet Baru", "create_new_account": "Buat akun baru", + "create_new_seed": "Buat benih baru", "creating_new_wallet": "Membuat dompet baru", "creating_new_wallet_error": "Error: ${description}", "creation_date": "Tanggal Pembuatan", @@ -195,6 +200,7 @@ "delete_wallet_confirm_message": "Apakah Anda yakin ingin menghapus dompet ${wallet_name}?", "deleteConnectionConfirmationPrompt": "Apakah Anda yakin ingin menghapus koneksi ke", "denominations": "Denominasi", + "derivationpath": "Jalur derivasi", "descending": "Menurun", "description": "Keterangan", "destination_tag": "Tag tujuan:", @@ -207,6 +213,7 @@ "disable_buy": "Nonaktifkan tindakan beli", "disable_cake_2fa": "Nonaktifkan Kue 2FA", "disable_exchange": "Nonaktifkan pertukaran", + "disable_fee_api_warning": "Dengan mematikan ini, tarif biaya mungkin tidak akurat dalam beberapa kasus, jadi Anda mungkin akan membayar lebih atau membayar biaya untuk transaksi Anda", "disable_fiat": "Nonaktifkan fiat", "disable_sell": "Nonaktifkan aksi jual", "disableBatteryOptimization": "Nonaktifkan optimasi baterai", @@ -228,8 +235,10 @@ "edit_token": "Mengedit token", "electrum_address_disclaimer": "Kami menghasilkan alamat baru setiap kali Anda menggunakan satu, tetapi alamat sebelumnya tetap berfungsi", "email_address": "Alamat Email", + "enable": "Memungkinkan", + "enable_mempool_api": "API Mempool untuk biaya dan tanggal yang akurat", "enable_replace_by_fee": "Aktifkan ganti-by-fee", - "enable_silent_payments_scanning": "Aktifkan pemindaian pembayaran diam", + "enable_silent_payments_scanning": "Mulailah memindai pembayaran diam, sampai ujung tercapai", "enabled": "Diaktifkan", "enter_amount": "Masukkan Jumlah", "enter_backup_password": "Masukkan kata sandi cadangan di sini", @@ -272,8 +281,8 @@ "etherscan_history": "Sejarah Etherscan", "event": "Peristiwa", "events": "Acara", - "exchange": "Tukar", - "exchange_incorrect_current_wallet_for_xmr": "Jika Anda ingin menukar XMR dari saldo Monero Cake Wallet Anda, silakan beralih ke dompet Monero Anda terlebih dahulu.", + "exchange": "Menukar", + "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!", "exchange_result_confirm": "Dengan menekan tombol konfirmasi, Anda akan mengirimkan ${fetchingLabel} ${from} dari dompet Anda yang disebut ${walletName} ke alamat yang ditampilkan di bawah. Anda juga dapat mengirim dari dompet eksternal Anda ke alamat/QR code di bawah.\n\nSilakan tekan konfirmasi untuk melanjutkan atau kembali untuk mengubah jumlah.", @@ -291,6 +300,8 @@ "failed_authentication": "Otentikasi gagal. ${state_error}", "faq": "Pertanyaan yang Sering Diajukan", "features": "Fitur", + "fee_less_than_min": "Biaya yang dipilih kurang dari minimum, harap tingkatkan biaya untuk dapat mengirim transaksi", + "fee_rate": "Tarif biaya", "fetching": "Mengambil", "fiat_api": "API fiat", "fiat_balance": "Saldo Fiat", @@ -298,7 +309,7 @@ "fill_code": "Silakan isi kode verifikasi yang diterima di email Anda", "filter_by": "Filter berdasarkan", "first_wallet_text": "Dompet luar biasa untuk Monero, Bitcoin, Ethereum, Litecoin, dan Haven", - "fixed_pair_not_supported": "Pasangan tetap ini tidak didukung dengan bursa yang dipilih", + "fixed_pair_not_supported": "Pasangan tetap ini tidak didukung dengan layanan swap yang dipilih", "fixed_rate": "Rate tetap", "fixed_rate_alert": "Anda akan dapat memasukkan jumlah penerimaan saat mode rate tetap dicentang. Apakah Anda ingin beralih ke mode rate tetap?", "forgot_password": "Lupa Kata Sandi", @@ -354,6 +365,20 @@ "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", "light_theme": "Terang", + "litecoin_enable_mweb_sync": "Aktifkan pemindaian MWEB", + "litecoin_mweb": "Mweb", + "litecoin_mweb_always_scan": "Atur mWeb selalu memindai", + "litecoin_mweb_description": "MWEB adalah protokol baru yang membawa transaksi yang lebih cepat, lebih murah, dan lebih pribadi ke Litecoin", + "litecoin_mweb_dismiss": "Membubarkan", + "litecoin_mweb_display_card": "Tunjukkan kartu mWeb", + "litecoin_mweb_enable_later": "Anda dapat memilih untuk mengaktifkan MWEB lagi di bawah pengaturan tampilan.", + "litecoin_mweb_pegin": "Pasak masuk", + "litecoin_mweb_pegout": "Mati", + "litecoin_mweb_scanning": "Pemindaian MWEB", + "litecoin_mweb_settings": "Pengaturan MWEB", + "litecoin_mweb_warning": "Menggunakan MWEB pada awalnya akan mengunduh ~ 600MB data, dan dapat memakan waktu hingga 30 menit tergantung pada kecepatan jaringan. Data awal ini hanya akan mengunduh sekali dan tersedia untuk semua dompet litecoin", + "litecoin_what_is_mweb": "Apa itu MWEB?", + "live_fee_rates": "Tarif biaya langsung melalui API", "load_more": "Muat lebih banyak", "loading_your_wallet": "Memuat dompet Anda", "login": "Masuk", @@ -383,6 +408,8 @@ "monero_light_theme": "Tema Cahaya Monero", "moonpay_alert_text": "Nilai jumlah harus lebih atau sama dengan ${minAmount} ${fiatCurrency}", "more_options": "Opsi Lainnya", + "mweb_confirmed": "Mengkonfirmasi mWeb", + "mweb_unconfirmed": "MWEB yang belum dikonfirmasi", "name": "Nama", "nano_current_rep": "Perwakilan saat ini", "nano_gpt_thanks_message": "Terima kasih telah menggunakan Nanogpt! Ingatlah untuk kembali ke browser setelah transaksi Anda selesai!", @@ -412,7 +439,7 @@ "node_test": "Uji", "nodes": "Node", "nodes_list_reset_to_default_message": "Apakah Anda yakin ingin mengatur ulang pengaturan ke default?", - "none_of_selected_providers_can_exchange": "Tidak ada dari penyedia yang dipilih yang dapat melakukan pertukaran ini", + "none_of_selected_providers_can_exchange": "Tak satu pun dari penyedia yang dipilih dapat melakukan pertukaran ini", "noNFTYet": "Belum ada NFT", "normal": "Normal", "note_optional": "Catatan (opsional)", @@ -444,6 +471,7 @@ "overwrite_amount": "Timpa jumlah", "pairingInvalidEvent": "Menyandingkan Acara Tidak Valid", "passphrase": "Frasa sandi (opsional)", + "passphrases_doesnt_match": "Sandi tidak cocok, coba lagi", "password": "Kata Sandi", "paste": "Tempel", "pause_wallet_creation": "Kemampuan untuk membuat Haven Wallet saat ini dijeda.", @@ -596,8 +624,11 @@ "seed_share": "Bagikan bibit", "seed_title": "Bibit", "seedtype": "Seedtype", + "seedtype_alert_content": "Berbagi biji dengan dompet lain hanya dimungkinkan dengan BIP39 seedtype.", + "seedtype_alert_title": "Peringatan seedtype", "seedtype_legacy": "Legacy (25 kata)", "seedtype_polyseed": "Polyseed (16 kata)", + "seedtype_wownero": "Wownero (14 kata)", "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.", @@ -608,6 +639,7 @@ "send": "Mengirim", "send_address": "Alamat ${cryptoCurrency}", "send_amount": "Jumlah:", + "send_change_to_you": "Ubah, untukmu:", "send_creating_transaction": "Membuat transaksi", "send_error_currency": "Mata uang hanya dapat berisi angka", "send_error_minimum_value": "Nilai minimum jumlah adalah 0.01", @@ -661,19 +693,21 @@ "setup_your_debit_card": "Pasang kartu debit Anda", "share": "Membagikan", "share_address": "Bagikan alamat", + "shared_seed_wallet_groups": "Kelompok dompet benih bersama", "show_details": "Tampilkan Rincian", "show_keys": "Tampilkan seed/kunci", "show_market_place": "Tampilkan Pasar", "show_seed": "Tampilkan seed", "sign_message": "Pesan tanda", "sign_up": "Daftar", - "sign_verify_message": "Tanda tangan atau verifikasi pesan", + "sign_verify_message": "Tanda / verifikasi", "sign_verify_message_sub": "Menandatangani atau memverifikasi pesan menggunakan kunci pribadi Anda", "sign_verify_title": "Tanda / verifikasi", "signature": "Tanda tangan", "signature_invalid_error": "Tanda tangan tidak valid untuk pesan yang diberikan", "signTransaction": "Tandatangani Transaksi", "signup_for_card_accept_terms": "Daftar untuk kartu dan terima syarat dan ketentuan.", + "silent_payment": "Pembayaran diam", "silent_payments": "Pembayaran diam", "silent_payments_always_scan": "Tetapkan pembayaran diam selalu pemindaian", "silent_payments_disclaimer": "Alamat baru bukanlah identitas baru. Ini adalah penggunaan kembali identitas yang ada dengan label yang berbeda.", @@ -684,6 +718,7 @@ "silent_payments_scanned_tip": "Pindai untuk memberi tip! (${tip})", "silent_payments_scanning": "Pemindaian pembayaran diam", "silent_payments_settings": "Pengaturan pembayaran diam", + "single_seed_wallets_group": "Dompet biji tunggal", "slidable": "Dapat digeser", "sort_by": "Sortir dengan", "spend_key_private": "Kunci pengeluaran (privat)", @@ -706,12 +741,13 @@ "switchToEVMCompatibleWallet": "Silakan beralih ke dompet yang kompatibel dengan EVM dan coba lagi (Ethereum, Polygon)", "symbol": "Simbol", "sync_all_wallets": "Sinkronkan semua dompet", + "sync_status_attempting_scan": "Mencoba memindai", "sync_status_attempting_sync": "MENCOBA SINKRONISASI", "sync_status_connected": "TERHUBUNG", "sync_status_connecting": "MENGHUBUNGKAN", "sync_status_failed_connect": "GAGAL TERHUBUNG", "sync_status_not_connected": "TIDAK TERHUBUNG", - "sync_status_starting_scan": "Mulai pindai", + "sync_status_starting_scan": "Mulai pemindaian (dari ${height})", "sync_status_starting_sync": "MULAI SINKRONISASI", "sync_status_syncronized": "SUDAH TERSINKRONISASI", "sync_status_syncronizing": "SEDANG SINKRONISASI", @@ -798,6 +834,7 @@ "trusted": "Dipercayai", "tx_commit_exception_no_dust_on_change": "Transaksi ditolak dengan jumlah ini. Dengan koin ini Anda dapat mengirim ${min} tanpa perubahan atau ${max} yang mengembalikan perubahan.", "tx_commit_failed": "Transaksi Gagal. Silakan hubungi Dukungan.", + "tx_commit_failed_no_peers": "Transaksi gagal untuk disiarkan, silakan coba lagi sebentar lagi", "tx_invalid_input": "Anda menggunakan jenis input yang salah untuk jenis pembayaran ini", "tx_no_dust_exception": "Transaksi ditolak dengan mengirimkan jumlah yang terlalu kecil. Silakan coba tingkatkan jumlahnya.", "tx_not_enough_inputs_exception": "Tidak cukup input yang tersedia. Pilih lebih banyak lagi di bawah Kontrol Koin", @@ -842,8 +879,16 @@ "view_transaction_on": "Lihat Transaksi di ", "voting_weight": "Berat voting", "waitFewSecondForTxUpdate": "Mohon tunggu beberapa detik hingga transaksi terlihat di riwayat transaksi", + "wallet_group": "Kelompok dompet", + "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_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_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", "wallet_list_create_new_wallet": "Buat Dompet Baru", + "wallet_list_edit_group_name": "Edit Nama Grup", "wallet_list_edit_wallet": "Edit dompet", "wallet_list_failed_to_load": "Gagal memuat ${wallet_name} dompet. ${error}", "wallet_list_failed_to_remove": "Gagal menghapus ${wallet_name} dompet. ${error}", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index 0548b0ad2..08ae928af 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -36,6 +36,7 @@ "agree": "d'accordo", "agree_and_continue": "Accetta e continua", "agree_to": "Creando un account accetti il ​​", + "alert_notice": "Avviso", "all": "TUTTO", "all_trades": "Svi obrti", "all_transactions": "Sve transakcije", @@ -113,7 +114,7 @@ "change_currency": "Cambia Valuta", "change_current_node": "Sei sicuro di voler cambiare il nodo corrente con ${node}?", "change_current_node_title": "Cambia nodo corrente", - "change_exchange_provider": "Cambia Exchange", + "change_exchange_provider": "Provider di swap di cambiamento", "change_language": "Cambia lingua", "change_language_to": "Cambiare lingua in ${language}?", "change_password": "Cambia password", @@ -130,6 +131,7 @@ "choose_one": "Scegline uno", "choose_relay": "Scegli un relè da utilizzare", "choose_wallet_currency": "Gentilmente scegli la moneta del portafoglio:", + "choose_wallet_group": "Scegli il gruppo del portafoglio", "clear": "Pulisci", "clearnet_link": "Collegamento Clearnet", "close": "Chiudere", @@ -142,6 +144,7 @@ "confirm_delete_wallet": "Questa azione cancellerà questo portafoglio. Desideri continuare?", "confirm_fee_deduction": "Conferma la detrazione delle commissioni", "confirm_fee_deduction_content": "Accetti di detrarre la commissione dall'output?", + "confirm_passphrase": "Conferma passphrase", "confirm_sending": "Conferma l'invio", "confirm_silent_payments_switch_node": "Il tuo nodo corrente non supporta i pagamenti silenziosi \\ ncake Wallet passerà a un nodo compatibile, solo per la scansione", "confirmations": "Conferme", @@ -161,6 +164,7 @@ "contact_name": "Nome Contatto", "contact_support": "Contatta l'assistenza", "continue_text": "Continua", + "contract_warning": "Questo indirizzo del contratto è stato contrassegnato come potenzialmente fraudolento. Si prega di elaborare con cautela.", "contractName": "Nome del contratto", "contractSymbol": "Simbolo del contratto", "copied_key_to_clipboard": " ${key} copiata negli Appunti", @@ -176,6 +180,7 @@ "create_invoice": "Crea fattura", "create_new": "Genera nuovo Portafoglio", "create_new_account": "Crea nuovo account", + "create_new_seed": "Crea nuovo seme", "creating_new_wallet": "Creazione nuovo portafoglio", "creating_new_wallet_error": "Errore: ${description}", "creation_date": "Data di creazione", @@ -196,6 +201,7 @@ "delete_wallet_confirm_message": "Sei sicuro di voler eliminare il portafoglio ${wallet_name}?", "deleteConnectionConfirmationPrompt": "Sei sicuro di voler eliminare la connessione a", "denominations": "Denominazioni", + "derivationpath": "Percorso di derivazione", "descending": "Discendente", "description": "Descrizione", "destination_tag": "Tag destinazione:", @@ -208,6 +214,7 @@ "disable_buy": "Disabilita l'azione di acquisto", "disable_cake_2fa": "Disabilita Cake 2FA", "disable_exchange": "Disabilita scambio", + "disable_fee_api_warning": "Disattivando questo, i tassi delle commissioni potrebbero essere inaccurati in alcuni casi, quindi potresti finire in eccesso o sostenere le commissioni per le transazioni", "disable_fiat": "Disabilita fiat", "disable_sell": "Disabilita l'azione di vendita", "disableBatteryOptimization": "Disabilita l'ottimizzazione della batteria", @@ -229,8 +236,10 @@ "edit_token": "Modifica token", "electrum_address_disclaimer": "Generiamo nuovi indirizzi ogni volta che ne utilizzi uno, ma gli indirizzi precedenti continuano a funzionare", "email_address": "Indirizzo e-mail", + "enable": "Abilitare", + "enable_mempool_api": "API di Mempool per commissioni e date accurate", "enable_replace_by_fee": "Abilita sostituzione per fee", - "enable_silent_payments_scanning": "Abilita la scansione dei pagamenti silenziosi", + "enable_silent_payments_scanning": "Inizia a scansionare i pagamenti silenziosi, fino a raggiungere la punta", "enabled": "Abilitato", "enter_amount": "Inserisci importo", "enter_backup_password": "Inserisci la password di backup qui", @@ -273,8 +282,8 @@ "etherscan_history": "Storia Etherscan", "event": "Evento", "events": "Eventi", - "exchange": "Scambia", - "exchange_incorrect_current_wallet_for_xmr": "Se vuoi scambiare XMR dal tuo saldo Cake Wallet Monero, gentilmente passa al tuo portafoglio Monero.", + "exchange": "Scambio", + "exchange_incorrect_current_wallet_for_xmr": "Se vuoi scambiare XMR dal tuo portafoglio di torta Monero Balance, si prega di passare prima al portafoglio Monero.", "exchange_new_template": "Nuovo modello", "exchange_provider_unsupported": "${providerName} non è più supportato!", "exchange_result_confirm": "Cliccando su Conferma, invierai ${fetchingLabel} ${from} dal tuo portafoglio chiamato ${walletName} all'indirizzo mostrato qui in basso. O puoi inviare dal tuo portafoglio esterno all'indirizzo/codice QR mostrato in basso.\n\nGentilmente clicca su Conferma per continuare o torna indietro per cambiare l'ammontare.", @@ -292,6 +301,8 @@ "failed_authentication": "Autenticazione fallita. ${state_error}", "faq": "Domande Frequenti", "features": "Caratteristiche", + "fee_less_than_min": "La commissione selezionata è inferiore al minimo, aumenta le commissioni per poter inviare la transazione", + "fee_rate": "Tasso di commissione", "fetching": "Recupero", "fiat_api": "Fiat API", "fiat_balance": "Equilibrio fiat", @@ -299,7 +310,7 @@ "fill_code": "Compila il codice di verifica fornito alla tua email", "filter_by": "Filtrirati po", "first_wallet_text": "Portafoglio fantastico per Monero, Bitcoin, Ethereum, Litecoin, e Haven", - "fixed_pair_not_supported": "Questa coppia fissa non è supportata con gli scambi selezionati", + "fixed_pair_not_supported": "Questa coppia fissa non è supportata con i servizi di swap selezionati", "fixed_rate": "Tasso fisso", "fixed_rate_alert": "Potrai inserire l'ammontare da ricevere quando il tasso è fisso. Vuoi cambiare alla modalità tasso fisso?", "forgot_password": "Password dimenticata", @@ -355,6 +366,20 @@ "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", "light_theme": "Bianco", + "litecoin_enable_mweb_sync": "Abilita la scansione MWeb", + "litecoin_mweb": "MWeb", + "litecoin_mweb_always_scan": "Imposta MWeb per scansionare sempre", + "litecoin_mweb_description": "MWeb è un nuovo protocollo che porta transazioni più veloci, più economiche e più private a Litecoin", + "litecoin_mweb_dismiss": "Congedare", + "litecoin_mweb_display_card": "Mostra la scheda MWeb", + "litecoin_mweb_enable_later": "È possibile scegliere di abilitare nuovamente MWeb nelle impostazioni di visualizzazione.", + "litecoin_mweb_pegin": "Piolo in", + "litecoin_mweb_pegout": "PEG OUT", + "litecoin_mweb_scanning": "Scansione MWeb", + "litecoin_mweb_settings": "Impostazioni MWeb", + "litecoin_mweb_warning": "L'uso di MWeb inizialmente scaricherà ~ 600 MB di dati e potrebbe richiedere fino a 30 minuti a seconda della velocità di rete. Questi dati iniziali scaricheranno solo una volta e saranno disponibili per tutti i portafogli Litecoin", + "litecoin_what_is_mweb": "Cos'è MWeb?", + "live_fee_rates": "Tariffe delle commissioni dal vivo tramite API", "load_more": "Carica di più", "loading_your_wallet": "Caricamento portafoglio", "login": "Accedi", @@ -384,6 +409,8 @@ "monero_light_theme": "Tema leggero Monero", "moonpay_alert_text": "Il valore dell'importo deve essere maggiore o uguale a ${minAmount} ${fiatCurrency}", "more_options": "Altre opzioni", + "mweb_confirmed": "MWeb confermato", + "mweb_unconfirmed": "MWeb non confermato", "name": "Nome", "nano_current_rep": "Rappresentante attuale", "nano_gpt_thanks_message": "Grazie per aver usato il nanogpt! Ricorda di tornare al browser dopo il completamento della transazione!", @@ -413,7 +440,7 @@ "node_test": "Test", "nodes": "Nodi", "nodes_list_reset_to_default_message": "Sei sicuro di voler ripristinare le impostazioni predefinite?", - "none_of_selected_providers_can_exchange": "Nessuno dei fornitori selezionati può effettuare questo scambio", + "none_of_selected_providers_can_exchange": "Nessuno dei provider selezionati può fare questo scambio", "noNFTYet": "Nessun NFT ancora", "normal": "Normale", "note_optional": "Nota (opzionale)", @@ -444,6 +471,7 @@ "overwrite_amount": "Sovrascrivi quantità", "pairingInvalidEvent": "Associazione evento non valido", "passphrase": "Passphrase (opzionale)", + "passphrases_doesnt_match": "Le passphrasi non corrispondono, riprova", "password": "Password", "paste": "Incolla", "pause_wallet_creation": "La possibilità di creare Haven Wallet è attualmente sospesa.", @@ -595,8 +623,11 @@ "seed_share": "Condividi seme", "seed_title": "Seme", "seedtype": "Seedtype", + "seedtype_alert_content": "La condivisione di semi con altri portafogli è possibile solo con Bip39 SeedType.", + "seedtype_alert_title": "Avviso seedType", "seedtype_legacy": "Legacy (25 parole)", "seedtype_polyseed": "Polyseed (16 parole)", + "seedtype_wownero": "Wownero (14 parole)", "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.", @@ -607,6 +638,7 @@ "send": "Invia", "send_address": "${cryptoCurrency} indirizzo", "send_amount": "Ammontare:", + "send_change_to_you": "Cambiamento, a te:", "send_creating_transaction": "Creazione della transazione", "send_error_currency": "L'ammontare può contenere solo numeri", "send_error_minimum_value": "L'ammontare minimo è 0.01", @@ -660,19 +692,21 @@ "setup_your_debit_card": "Configura la tua carta di debito", "share": "Condividere", "share_address": "Condividi indirizzo", + "shared_seed_wallet_groups": "Gruppi di portafoglio di semi condivisi", "show_details": "Mostra dettagli", "show_keys": "Mostra seme/chiavi", "show_market_place": "Mostra mercato", "show_seed": "Mostra seme", "sign_message": "Messaggio di firma", "sign_up": "Registrati", - "sign_verify_message": "Firmare o verificare il messaggio", + "sign_verify_message": "Firmare / verificare", "sign_verify_message_sub": "Firma o verifica un messaggio utilizzando la chiave privata", "sign_verify_title": "Firmare / verificare", "signature": "Firma", "signature_invalid_error": "La firma non è valida per il messaggio dato", "signTransaction": "Firma la transazione", "signup_for_card_accept_terms": "Registrati per la carta e accetta i termini.", + "silent_payment": "Pagamento silenzioso", "silent_payments": "Pagamenti silenziosi", "silent_payments_always_scan": "Impostare i pagamenti silenziosi che scansionano sempre", "silent_payments_disclaimer": "I nuovi indirizzi non sono nuove identità. È un riutilizzo di un'identità esistente con un'etichetta diversa.", @@ -683,6 +717,7 @@ "silent_payments_scanned_tip": "Scansionato per dare la mancia! (${tip})", "silent_payments_scanning": "Scansione di pagamenti silenziosi", "silent_payments_settings": "Impostazioni di pagamenti silenziosi", + "single_seed_wallets_group": "Portafogli singoli", "slidable": "Scorrevole", "sort_by": "Ordina per", "spend_key_private": "Chiave di spesa (privata)", @@ -705,12 +740,13 @@ "switchToEVMCompatibleWallet": "Passa a un portafoglio compatibile con EVM e riprova (Ethereum, Polygon)", "symbol": "Simbolo", "sync_all_wallets": "Sincronizza tutti i portafogli", + "sync_status_attempting_scan": "Tentando la scansione", "sync_status_attempting_sync": "TENTATIVO DI SINCRONIZZAZIONE", "sync_status_connected": "CONNESSO", "sync_status_connecting": "CONNESSIONE", "sync_status_failed_connect": "DISCONNESSO", "sync_status_not_connected": "NON CONNESSO", - "sync_status_starting_scan": "Scansione di partenza", + "sync_status_starting_scan": "Avvia scansione (da ${height})", "sync_status_starting_sync": "INIZIO SINC", "sync_status_syncronized": "SINCRONIZZATO", "sync_status_syncronizing": "SINCRONIZZAZIONE", @@ -797,6 +833,7 @@ "trusted": "di fiducia", "tx_commit_exception_no_dust_on_change": "La transazione viene respinta con questo importo. Con queste monete è possibile inviare ${min} senza modifiche o ${max} che restituisce il cambiamento.", "tx_commit_failed": "Commit di transazione non riuscita. Si prega di contattare il supporto.", + "tx_commit_failed_no_peers": "La transazione non è riuscita a trasmettere, riprovare in un secondo o giù di lì", "tx_invalid_input": "Stai usando il tipo di input sbagliato per questo tipo di pagamento", "tx_no_dust_exception": "La transazione viene respinta inviando un importo troppo piccolo. Per favore, prova ad aumentare l'importo.", "tx_not_enough_inputs_exception": "Input non sufficienti disponibili. Seleziona di più sotto il controllo delle monete", @@ -842,8 +879,16 @@ "voting_weight": "Peso di voto", "waitFewSecondForTxUpdate": "Attendi qualche secondo affinché la transazione venga riflessa nella cronologia delle transazioni", "waiting_payment_confirmation": "In attesa di conferma del pagamento", + "wallet_group": "Gruppo di portafoglio", + "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_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_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", "wallet_list_create_new_wallet": "Crea Nuovo Portafoglio", + "wallet_list_edit_group_name": "Modifica nome del gruppo", "wallet_list_edit_wallet": "Modifica portafoglio", "wallet_list_failed_to_load": "Caricamento portafoglio ${wallet_name} fallito. ${error}", "wallet_list_failed_to_remove": "Rimozione portafoglio ${wallet_name} fallita. ${error}", diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index c53a6d001..d70eca31b 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -36,6 +36,7 @@ "agree": "同意する", "agree_and_continue": "同意して続行", "agree_to": "アカウントを作成することにより、", + "alert_notice": "知らせ", "all": "すべて", "all_trades": "すべての取引", "all_transactions": "全取引", @@ -113,7 +114,7 @@ "change_currency": "通貨を変更する", "change_current_node": "現在のノードを変更してよろしいですか ${node}?", "change_current_node_title": "現在のノードを変更する", - "change_exchange_provider": "Exchangeプロバイダーの変更", + "change_exchange_provider": "スワッププロバイダーを変更します", "change_language": "言語を変えてください", "change_language_to": "言語を変更 ${language}?", "change_password": "パスワードを変更する", @@ -130,6 +131,7 @@ "choose_one": "1 つ選択してください", "choose_relay": "使用するリレーを選択してください", "choose_wallet_currency": "ウォレット通貨を選択してください:", + "choose_wallet_group": "ウォレットグループを選択してください", "clear": "クリア", "clearnet_link": "クリアネット リンク", "close": "近い", @@ -142,6 +144,7 @@ "confirm_delete_wallet": "このアクションにより、このウォレットが削除されます。 続行しますか?", "confirm_fee_deduction": "料金控除を確認します", "confirm_fee_deduction_content": "出力から料金を差し引くことに同意しますか?", + "confirm_passphrase": "パスフレーズを確認します", "confirm_sending": "送信を確認", "confirm_silent_payments_switch_node": "現在のノードはサイレントペイメントをサポートしていません\\ ncakeウォレットは、スキャン用に互換性のあるノードに切り替えます", "confirmations": "確認", @@ -160,6 +163,7 @@ "contact_name": "連絡先", "contact_support": "サポートに連絡する", "continue_text": "持続する", + "contract_warning": "この契約住所は、潜在的に不正としてフラグが立てられています。注意して処理してください。", "contractName": "契約名", "contractSymbol": "契約記号", "copied_key_to_clipboard": "コピー済み ${key} クリップボードへ", @@ -175,6 +179,7 @@ "create_invoice": "請求書の作成", "create_new": "新しいウォレットを作成", "create_new_account": "新しいアカウントを作成する", + "create_new_seed": "新しい種を作成します", "creating_new_wallet": "新しいウォレットの作成", "creating_new_wallet_error": "エラー: ${description}", "creation_date": "作成日", @@ -195,6 +200,7 @@ "delete_wallet_confirm_message": "${wallet_name} ウォレットを削除してもよろしいですか?", "deleteConnectionConfirmationPrompt": "への接続を削除してもよろしいですか?", "denominations": "宗派", + "derivationpath": "派生パス", "descending": "下降", "description": "説明", "destination_tag": "宛先タグ:", @@ -207,6 +213,7 @@ "disable_buy": "購入アクションを無効にする", "disable_cake_2fa": "Cake 2FA を無効にする", "disable_exchange": "交換を無効にする", + "disable_fee_api_warning": "これをオフにすることで、料金金利は場合によっては不正確になる可能性があるため、取引の費用が過払いまたは不足している可能性があります", "disable_fiat": "フィアットを無効にする", "disable_sell": "販売アクションを無効にする", "disableBatteryOptimization": "バッテリーの最適化を無効にします", @@ -228,8 +235,10 @@ "edit_token": "トークンの編集", "electrum_address_disclaimer": "使用するたびに新しいアドレスが生成されますが、以前のアドレスは引き続き機能します", "email_address": "メールアドレス", + "enable": "有効にする", + "enable_mempool_api": "正確な料金と日付のMempool API", "enable_replace_by_fee": "交換ごとに有効にします", - "enable_silent_payments_scanning": "サイレントペイメントスキャンを有効にします", + "enable_silent_payments_scanning": "先端に達するまで、サイレント決済のスキャンを開始します", "enabled": "有効", "enter_amount": "金額を入力", "enter_backup_password": "ここにバックアップパスワードを入力してください", @@ -272,8 +281,8 @@ "etherscan_history": "イーサスキャンの歴史", "event": "イベント", "events": "イベント", - "exchange": "交換する", - "exchange_incorrect_current_wallet_for_xmr": "Cake Wallet Moneroの残高からXMRを交換する場合は、最初にMoneroウォレットに切り替えてください。", + "exchange": "スワップ", + "exchange_incorrect_current_wallet_for_xmr": "XMRをケーキウォレットモネロバランスから交換したい場合は、最初にMoneroウォレットに切り替えてください。", "exchange_new_template": "新しいテンプレート", "exchange_provider_unsupported": "${providerName}はサポートされなくなりました!", "exchange_result_confirm": "確認を押すと、送信されます ${fetchingLabel} ${from} と呼ばれるあなたの財布から ${walletName} 下記の住所へ。 または、外部ウォレットから以下のアドレスに送信することもできます/ QRコードに送信できます.\n\n確認を押して続行するか、戻って金額を変更してください.", @@ -291,6 +300,8 @@ "failed_authentication": "認証失敗. ${state_error}", "faq": "FAQ", "features": "特徴", + "fee_less_than_min": "選択した料金は最低額よりも少ないので、取引を送信できるように料金を引き上げてください", + "fee_rate": "料金金利", "fetching": "フェッチング", "fiat_api": "不換紙幣 API", "fiat_balance": "フィアットバランス", @@ -298,7 +309,7 @@ "fill_code": "メールアドレスに記載されている確認コードを入力してください", "filter_by": "でフィルタリング", "first_wallet_text": "Monero、Bitcoin、Ethereum、Litecoin、Haven用の素晴らしいウォレット", - "fixed_pair_not_supported": "この固定ペアは、選択したエクスチェンジではサポートされていません", + "fixed_pair_not_supported": "この固定ペアは、選択したスワップサービスではサポートされていません", "fixed_rate": "固定金利", "fixed_rate_alert": "固定金利モードにチェックを入れると、受取額を入力できるようになります。 固定金利モードに切り替えますか?", "forgot_password": "パスワードを忘れた", @@ -355,6 +366,20 @@ "ledger_error_wrong_app": "元帳に適切なアプリを開始するようにしてください", "ledger_please_enable_bluetooth": "Bluetoothが元帳を検出できるようにしてください", "light_theme": "光", + "litecoin_enable_mweb_sync": "MWEBスキャンを有効にします", + "litecoin_mweb": "mweb", + "litecoin_mweb_always_scan": "MWEBを常にスキャンします", + "litecoin_mweb_description": "MWEBは、Litecoinにより速く、より安価で、よりプライベートなトランザクションをもたらす新しいプロトコルです", + "litecoin_mweb_dismiss": "却下する", + "litecoin_mweb_display_card": "MWEBカードを表示します", + "litecoin_mweb_enable_later": "表示設定の下で、MWEBを再度有効にすることを選択できます。", + "litecoin_mweb_pegin": "ペグイン", + "litecoin_mweb_pegout": "ペグアウト", + "litecoin_mweb_scanning": "MWEBスキャン", + "litecoin_mweb_settings": "MWEB設定", + "litecoin_mweb_warning": "MWEBを使用すると、最初は〜600MBのデータをダウンロードし、ネットワーク速度に応じて最大30分かかる場合があります。この最初のデータは一度だけダウンロードされ、すべてのLitecoinウォレットで利用可能になります", + "litecoin_what_is_mweb": "MWEBとは何ですか?", + "live_fee_rates": "API経由のライブ料金", "load_more": "もっと読み込む", "loading_your_wallet": "ウォレットをロードしています", "login": "ログイン", @@ -384,6 +409,8 @@ "monero_light_theme": "モネロ ライト テーマ", "moonpay_alert_text": "金額の値は以上でなければなりません ${minAmount} ${fiatCurrency}", "more_options": "その他のオプション", + "mweb_confirmed": "確認されたMWEB", + "mweb_unconfirmed": "未確認のMWEB", "name": "名前", "nano_current_rep": "現在の代表", "nano_gpt_thanks_message": "NanoGptを使用してくれてありがとう!トランザクションが完了したら、ブラウザに戻ることを忘れないでください!", @@ -413,7 +440,7 @@ "node_test": "テスト", "nodes": "ノード", "nodes_list_reset_to_default_message": "設定をデフォルトにリセットしてもよろしいですか?", - "none_of_selected_providers_can_exchange": "選択したプロバイダーはいずれもこの交換を行うことができません", + "none_of_selected_providers_can_exchange": "選択したプロバイダーはいずれもこのスワップを作成できません", "noNFTYet": "NFTはまだありません", "normal": "普通", "note_optional": "注(オプション)", @@ -443,6 +470,7 @@ "overwrite_amount": "Overwrite amount", "pairingInvalidEvent": "ペアリング無効イベント", "passphrase": "パスフレーズ(オプション)", + "passphrases_doesnt_match": "パスフレーズは一致しません。もう一度やり直してください", "password": "パスワード", "paste": "ペースト", "pause_wallet_creation": "Haven Wallet を作成する機能は現在一時停止されています。", @@ -594,8 +622,11 @@ "seed_share": "シードを共有する", "seed_title": "シード", "seedtype": "SeedType", + "seedtype_alert_content": "他の財布と種子を共有することは、BIP39 SeedTypeでのみ可能です。", + "seedtype_alert_title": "SeedTypeアラート", "seedtype_legacy": "レガシー(25語)", "seedtype_polyseed": "ポリシード(16語)", + "seedtype_wownero": "wownero(14ワード)", "select_backup_file": "バックアップファイルを選択", "select_buy_provider_notice": "上記の購入プロバイダーを選択してください。デフォルトの購入プロバイダーをアプリ設定で設定して、この画面をスキップできます。", "select_destination": "バックアップファイルの保存先を選択してください。", @@ -606,6 +637,7 @@ "send": "送る", "send_address": "${cryptoCurrency} 住所", "send_amount": "量:", + "send_change_to_you": "あなたに変更:", "send_creating_transaction": "トランザクションを作成する", "send_error_currency": "通貨には数字のみを含めることができます", "send_error_minimum_value": "金額の最小値は0.01です", @@ -659,19 +691,21 @@ "setup_your_debit_card": "デビットカードを設定してください", "share": "共有", "share_address": "住所を共有する", + "shared_seed_wallet_groups": "共有シードウォレットグループ", "show_details": "詳細を表示", "show_keys": "シード/キーを表示する", "show_market_place": "マーケットプレイスを表示", "show_seed": "シードを表示", "sign_message": "署名メッセージ", "sign_up": "サインアップ", - "sign_verify_message": "メッセージに署名または確認します", + "sign_verify_message": "署名 /検証", "sign_verify_message_sub": "秘密鍵を使用してメッセージに署名または確認します", "sign_verify_title": "署名 /検証", "signature": "サイン", "signature_invalid_error": "署名は、指定されたメッセージに対して無効です", "signTransaction": "トランザクションに署名する", "signup_for_card_accept_terms": "カードにサインアップして、利用規約に同意してください。", + "silent_payment": "サイレント支払い", "silent_payments": "サイレント支払い", "silent_payments_always_scan": "サイレント決済を常にスキャンします", "silent_payments_disclaimer": "新しいアドレスは新しいアイデンティティではありません。これは、異なるラベルを持つ既存のアイデンティティの再利用です。", @@ -682,6 +716,7 @@ "silent_payments_scanned_tip": "チップをスキャンしました! (${tip})", "silent_payments_scanning": "サイレントペイメントスキャン", "silent_payments_settings": "サイレントペイメント設定", + "single_seed_wallets_group": "シングルシードウォレット", "slidable": "スライド可能", "sort_by": "並び替え", "spend_key_private": "キーを使う (プライベート)", @@ -704,12 +739,13 @@ "switchToEVMCompatibleWallet": "EVM 互換のウォレットに切り替えて再試行してください (イーサリアム、ポリゴン)", "symbol": "シンボル", "sync_all_wallets": "すべてのウォレットを同期", + "sync_status_attempting_scan": "スキャンの試み", "sync_status_attempting_sync": "同期を試みています", "sync_status_connected": "接続済み", "sync_status_connecting": "接続中", "sync_status_failed_connect": "切断されました", "sync_status_not_connected": "接続されていません", - "sync_status_starting_scan": "スキャンを開始します", + "sync_status_starting_scan": "スキャンを開始する(${height} から)", "sync_status_starting_sync": "同期の開始", "sync_status_syncronized": "同期された", "sync_status_syncronizing": "同期", @@ -796,6 +832,7 @@ "trusted": "信頼できる", "tx_commit_exception_no_dust_on_change": "この金額ではトランザクションは拒否されます。 これらのコインを使用すると、おつりなしの ${min} またはおつりを返す ${max} を送信できます。", "tx_commit_failed": "トランザクションコミットは失敗しました。サポートに連絡してください。", + "tx_commit_failed_no_peers": "トランザクションはブロードキャストに失敗しました。一瞬かそこらで再試行してください", "tx_invalid_input": "このタイプの支払いに間違った入力タイプを使用しています", "tx_no_dust_exception": "トランザクションは、小さすぎる金額を送信することにより拒否されます。量を増やしてみてください。", "tx_not_enough_inputs_exception": "利用可能な入力が十分ではありません。コイン制御下でもっと選択してください", @@ -840,8 +877,16 @@ "view_transaction_on": "View Transaction on ", "voting_weight": "投票重み", "waitFewSecondForTxUpdate": "取引履歴に取引が反映されるまで数秒お待ちください。", + "wallet_group": "ウォレットグループ", + "wallet_group_description_four": "まったく新しい種子の財布を作成します。", + "wallet_group_description_one": "ケーキウォレットでは、aを作成できます", + "wallet_group_description_three": "利用可能なウォレットおよび/またはウォレットグループの画面を表示します。または選択します", + "wallet_group_description_two": "既存のウォレットを選択して種子を共有します。各ウォレットグループには、各通貨タイプの単一のウォレットを含めることができます。\n\n選択できます", + "wallet_group_empty_state_text_one": "互換性のあるウォレットグループがないようです!\n\nタップ", + "wallet_group_empty_state_text_two": "以下に新しいものを作るために。", "wallet_keys": "ウォレットシード/キー", "wallet_list_create_new_wallet": "新しいウォレットを作成", + "wallet_list_edit_group_name": "グループ名を編集します", "wallet_list_edit_wallet": "ウォレットを編集する", "wallet_list_failed_to_load": "読み込みに失敗しました ${wallet_name} 財布. ${error}", "wallet_list_failed_to_remove": "削除できませんでした ${wallet_name} 財布. ${error}", diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index be6757d99..133ca1838 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -36,6 +36,7 @@ "agree": "동의하다", "agree_and_continue": "동의 및 계속", "agree_to": "계정을 생성하면 ", + "alert_notice": "알아채다", "all": "모든", "all_trades": "A모든 거래", "all_transactions": "모든 거래 창구", @@ -113,7 +114,7 @@ "change_currency": "통화 변경", "change_current_node": "현재 노드를 다음으로 변경 하시겠습니까 ${node}?", "change_current_node_title": "현재 노드 변경", - "change_exchange_provider": "교환 공급자 변경", + "change_exchange_provider": "스왑 제공 업체를 변경하십시오", "change_language": "언어 변경", "change_language_to": "언어를로 변경 ${language}?", "change_password": "비밀번호 변경", @@ -130,6 +131,7 @@ "choose_one": "하나 선택", "choose_relay": "사용할 릴레이를 선택해주세요", "choose_wallet_currency": "지갑 통화를 선택하십시오:", + "choose_wallet_group": "지갑 그룹을 선택하십시오", "clear": "명확한", "clearnet_link": "클리어넷 링크", "close": "닫다", @@ -142,6 +144,7 @@ "confirm_delete_wallet": "이 작업은이 지갑을 삭제합니다. 계속 하시겠습니까?", "confirm_fee_deduction": "수수료 공제를 확인하십시오", "confirm_fee_deduction_content": "출력에서 수수료를 공제하는 데 동의하십니까?", + "confirm_passphrase": "암호를 확인하십시오", "confirm_sending": "전송 확인", "confirm_silent_payments_switch_node": "현재 노드는 무음 지불을 지원하지 않습니다 \\ ncake 지갑은 스캔을 위해 호환 가능한 노드로 전환됩니다.", "confirmations": "확인", @@ -160,6 +163,7 @@ "contact_name": "담당자 이름", "contact_support": "지원팀에 문의", "continue_text": "잇다", + "contract_warning": "이 계약 주소는 잠재적으로 사기성으로 표시되었습니다. 주의해서 처리하십시오.", "contractName": "계약명", "contractSymbol": "계약 기호", "copied_key_to_clipboard": "복사 ${key} 클립 보드로", @@ -175,6 +179,7 @@ "create_invoice": "인보이스 생성", "create_new": "새 월렛 만들기", "create_new_account": "새 계정을 만들", + "create_new_seed": "새 씨앗을 만듭니다", "creating_new_wallet": "새 지갑 생성", "creating_new_wallet_error": "오류: ${description}", "creation_date": "생산 일", @@ -195,6 +200,7 @@ "delete_wallet_confirm_message": "${wallet_name} 지갑을 삭제하시겠습니까?", "deleteConnectionConfirmationPrompt": "다음 연결을 삭제하시겠습니까?", "denominations": "교파", + "derivationpath": "파생 경로", "descending": "내림차순", "description": "설명", "destination_tag": "목적지 태그:", @@ -207,6 +213,7 @@ "disable_buy": "구매 행동 비활성화", "disable_cake_2fa": "케이크 2FA 비활성화", "disable_exchange": "교환 비활성화", + "disable_fee_api_warning": "이것을 끄면 경우에 따라 수수료가 부정확 할 수 있으므로 거래 수수료를 초과 지불하거나 지불 할 수 있습니다.", "disable_fiat": "법정화폐 비활성화", "disable_sell": "판매 조치 비활성화", "disableBatteryOptimization": "배터리 최적화를 비활성화합니다", @@ -228,8 +235,10 @@ "edit_token": "토큰 편집", "electrum_address_disclaimer": "사용할 때마다 새 주소가 생성되지만 이전 주소는 계속 작동합니다.", "email_address": "이메일 주소", + "enable": "할 수 있게 하다", + "enable_mempool_api": "정확한 수수료 및 날짜에 대한 Mempool API", "enable_replace_by_fee": "대체별로 활성화하십시오", - "enable_silent_payments_scanning": "무음 지불 스캔을 활성화합니다", + "enable_silent_payments_scanning": "팁에 도달 할 때까지 사일런트 지불을 스캔하기 시작합니다.", "enabled": "사용", "enter_amount": "금액 입력", "enter_backup_password": "여기에 백업 비밀번호를 입력하세요.", @@ -273,7 +282,7 @@ "event": "이벤트", "events": "이벤트", "exchange": "교환", - "exchange_incorrect_current_wallet_for_xmr": "Cake Wallet Monero 잔액에서 XMR을 교환하려면 먼저 Monero 지갑으로 전환하십시오.", + "exchange_incorrect_current_wallet_for_xmr": "케이크 지갑 Monero Balance에서 XMR을 교체하려면 먼저 Monero 지갑으로 전환하십시오.", "exchange_new_template": "새 템플릿", "exchange_provider_unsupported": "${providerName}은 더 이상 지원되지 않습니다!", "exchange_result_confirm": "확인을 누르면 전송됩니다 ${fetchingLabel} ${from} 지갑에서 ${walletName} 아래 주소로. 또는 외부 지갑에서 아래 주소로 보낼 수 있습니다 / QR 코드로 보낼 수 있습니다.\n\n확인을 눌러 계속하거나 금액을 변경하려면 돌아가십시오.", @@ -291,6 +300,8 @@ "failed_authentication": "인증 실패. ${state_error}", "faq": "FAQ", "features": "특징", + "fee_less_than_min": "선택된 수수료는 최소값보다 적습니다. 거래를 보낼 수있는 수수료를 늘리십시오.", + "fee_rate": "수수료", "fetching": "가져 오는 중", "fiat_api": "명목 화폐 API", "fiat_balance": "피아트 잔액", @@ -298,7 +309,7 @@ "fill_code": "이메일에 제공된 인증 코드를 입력하세요.", "filter_by": "필터링 기준", "first_wallet_text": "Monero, Bitcoin, Ethereum, Litecoin 및 Haven을 위한 멋진 지갑", - "fixed_pair_not_supported": "이 고정 쌍은 선택한 교환에서 지원되지 않습니다.", + "fixed_pair_not_supported": "이 고정 쌍은 선택한 스왑 서비스에서 지원되지 않습니다.", "fixed_rate": "고정 비율", "fixed_rate_alert": "고정 금리 모드 체크시 수취 금액 입력이 가능합니다. 고정 속도 모드로 전환 하시겠습니까?", "forgot_password": "비밀번호 찾기", @@ -354,6 +365,20 @@ "ledger_error_wrong_app": "원장에서 올바른 앱을 반대하는지 확인하십시오.", "ledger_please_enable_bluetooth": "Bluetooth가 원장을 감지 할 수 있도록하십시오", "light_theme": "빛", + "litecoin_enable_mweb_sync": "mweb 스캔을 활성화합니다", + "litecoin_mweb": "mweb", + "litecoin_mweb_always_scan": "mweb는 항상 스캔을 설정합니다", + "litecoin_mweb_description": "MWEB는 Litecoin에 더 빠르고 저렴하며 개인 거래를 제공하는 새로운 프로토콜입니다.", + "litecoin_mweb_dismiss": "해고하다", + "litecoin_mweb_display_card": "mweb 카드를 보여주십시오", + "litecoin_mweb_enable_later": "디스플레이 설정에서 MWEB를 다시 활성화하도록 선택할 수 있습니다.", + "litecoin_mweb_pegin": "페그를 입력하십시오", + "litecoin_mweb_pegout": "죽다", + "litecoin_mweb_scanning": "mweb 스캔", + "litecoin_mweb_settings": "mweb 설정", + "litecoin_mweb_warning": "MWEB를 사용하면 처음에는 ~ 600MB의 데이터를 다운로드하며 네트워크 속도에 따라 최대 30 분이 소요될 수 있습니다. 이 초기 데이터는 한 번만 다운로드하여 모든 조명 지갑에 사용할 수 있습니다.", + "litecoin_what_is_mweb": "MWEB 란 무엇입니까?", + "live_fee_rates": "API를 통한 라이브 요금 요금", "load_more": "더로드하십시오", "loading_your_wallet": "지갑 넣기", "login": "로그인", @@ -383,6 +408,8 @@ "monero_light_theme": "모네로 라이트 테마", "moonpay_alert_text": "금액은 다음보다 크거나 같아야합니다 ${minAmount} ${fiatCurrency}", "more_options": "추가 옵션", + "mweb_confirmed": "확인 mweb", + "mweb_unconfirmed": "확인되지 않은 mweb", "name": "이름", "nano_current_rep": "현재 대표", "nano_gpt_thanks_message": "Nanogpt를 사용해 주셔서 감사합니다! 거래가 완료된 후 브라우저로 돌아가는 것을 잊지 마십시오!", @@ -412,7 +439,7 @@ "node_test": "테스트", "nodes": "노드", "nodes_list_reset_to_default_message": "설정을 기본값으로 재설정 하시겠습니까?", - "none_of_selected_providers_can_exchange": "선택한 공급자 중 누구도 이 교환을 할 수 없습니다.", + "none_of_selected_providers_can_exchange": "선택한 공급자 중 어느 것도이 교환을 할 수 없습니다", "noNFTYet": "아직 NFT가 없습니다", "normal": "정상", "note_optional": "참고 (선택 사항)", @@ -442,6 +469,7 @@ "overwrite_amount": "Overwrite amount", "pairingInvalidEvent": "잘못된 이벤트 페어링", "passphrase": "암호화 (선택 사항)", + "passphrases_doesnt_match": "패스 프레이즈가 일치하지 않습니다. 다시 시도하십시오", "password": "암호", "paste": "풀", "pause_wallet_creation": "Haven Wallet 생성 기능이 현재 일시 중지되었습니다.", @@ -594,8 +622,11 @@ "seed_share": "시드 공유", "seed_title": "씨", "seedtype": "시드 타입", + "seedtype_alert_content": "다른 지갑과 씨앗을 공유하는 것은 BIP39 SeedType에서만 가능합니다.", + "seedtype_alert_title": "종자 경보", "seedtype_legacy": "레거시 (25 단어)", "seedtype_polyseed": "다문 (16 단어)", + "seedtype_wownero": "Wownero (14 단어)", "select_backup_file": "백업 파일 선택", "select_buy_provider_notice": "위의 구매 제공자를 선택하십시오. 앱 설정에서 기본 구매 제공자를 설정 하여이 화면을 건너 뛸 수 있습니다.", "select_destination": "백업 파일의 대상을 선택하십시오.", @@ -606,6 +637,7 @@ "send": "보내다", "send_address": "${cryptoCurrency} 주소", "send_amount": "양:", + "send_change_to_you": "당신에게 변경 :", "send_creating_transaction": "거래 생성", "send_error_currency": "통화는 숫자 만 포함 할 수 있습니다", "send_error_minimum_value": "금액의 최소값은 0.01입니다", @@ -659,6 +691,7 @@ "setup_your_debit_card": "직불카드 설정", "share": "공유하다", "share_address": "주소 공유", + "shared_seed_wallet_groups": "공유 종자 지갑 그룹", "show_details": "세부정보 표시", "show_keys": "시드 / 키 표시", "show_market_place": "마켓플레이스 표시", @@ -672,6 +705,7 @@ "signature_invalid_error": "서명은 주어진 메시지에 유효하지 않습니다", "signTransaction": "거래 서명", "signup_for_card_accept_terms": "카드에 가입하고 약관에 동의합니다.", + "silent_payment": "조용한 지불", "silent_payments": "조용한 지불", "silent_payments_always_scan": "무음금을 항상 스캔합니다", "silent_payments_disclaimer": "새로운 주소는 새로운 정체성이 아닙니다. 다른 레이블로 기존 신원을 재사용하는 것입니다.", @@ -682,6 +716,7 @@ "silent_payments_scanned_tip": "팁을 스캔했습니다! (${tip})", "silent_payments_scanning": "조용한 지불 스캔", "silent_payments_settings": "조용한 지불 설정", + "single_seed_wallets_group": "단일 씨앗 지갑", "slidable": "슬라이딩 가능", "sort_by": "정렬 기준", "spend_key_private": "지출 키 (은밀한)", @@ -704,12 +739,13 @@ "switchToEVMCompatibleWallet": "EVM 호환 지갑으로 전환 후 다시 시도해 주세요. (이더리움, 폴리곤)", "symbol": "상징", "sync_all_wallets": "모든 지갑 동기화", + "sync_status_attempting_scan": "스캔 시도", "sync_status_attempting_sync": "동기화 시도 중", "sync_status_connected": "연결됨", "sync_status_connecting": "연결 중", "sync_status_failed_connect": "연결 해제", "sync_status_not_connected": "연결되지 않은", - "sync_status_starting_scan": "스캔 시작", + "sync_status_starting_scan": "시작 스캔 (${height} 에서)", "sync_status_starting_sync": "동기화 시작", "sync_status_syncronized": "동기화", "sync_status_syncronizing": "동기화", @@ -796,6 +832,7 @@ "trusted": "신뢰할 수 있는", "tx_commit_exception_no_dust_on_change": "이 금액으로 거래가 거부되었습니다. 이 코인을 사용하면 거스름돈 없이 ${min}를 보내거나 거스름돈을 반환하는 ${max}를 보낼 수 있습니다.", "tx_commit_failed": "거래 커밋이 실패했습니다. 지원에 연락하십시오.", + "tx_commit_failed_no_peers": "트랜잭션이 방송에 실패했는데 1 초 정도 후에 다시 시도하십시오.", "tx_invalid_input": "이 유형의 지불에 잘못 입력 유형을 사용하고 있습니다.", "tx_no_dust_exception": "너무 작은 금액을 보내면 거래가 거부됩니다. 금액을 늘리십시오.", "tx_not_enough_inputs_exception": "사용 가능한 입력이 충분하지 않습니다. 코인 컨트롤에서 더 많은 것을 선택하십시오", @@ -840,8 +877,16 @@ "view_transaction_on": "View Transaction on ", "voting_weight": "투표 중량", "waitFewSecondForTxUpdate": "거래 내역에 거래가 반영될 때까지 몇 초 정도 기다려 주세요.", + "wallet_group": "지갑 그룹", + "wallet_group_description_four": "완전히 새로운 씨앗으로 지갑을 만듭니다.", + "wallet_group_description_one": "케이크 지갑에서는 a를 만들 수 있습니다", + "wallet_group_description_three": "사용 가능한 지갑 및/또는 지갑 그룹 스크린을 볼 수 있습니다. 또는 선택하십시오", + "wallet_group_description_two": "씨앗을 공유 할 기존 지갑을 선택함으로써. 각 지갑 그룹은 각 통화 유형의 단일 지갑을 포함 할 수 있습니다. \n\n", + "wallet_group_empty_state_text_one": "호환 지갑 그룹이없는 것 같습니다 !\n\n TAP", + "wallet_group_empty_state_text_two": "아래에서 새로운 것을 만들기 위해.", "wallet_keys": "지갑 시드 / 키", "wallet_list_create_new_wallet": "새 월렛 만들기", + "wallet_list_edit_group_name": "그룹 이름 편집", "wallet_list_edit_wallet": "지갑 수정", "wallet_list_failed_to_load": "불러 오지 못했습니다 ${wallet_name} 지갑. ${error}", "wallet_list_failed_to_remove": "제거하지 못했습니다 ${wallet_name} 지갑. ${error}", diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index c53dffb6b..1727f0d71 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -36,6 +36,7 @@ "agree": "သဘောတူသည်။", "agree_and_continue": "သဘောတူပြီး ရှေ့ဆက်ပါ။", "agree_to": "အကောင့်ဖန်တီးခြင်းဖြင့် သင်သည် ဤအရာကို သဘောတူပါသည်။", + "alert_notice": "မှတ်သား", "all": "အားလုံး", "all_trades": "ကုန်သွယ်မှုအားလုံး", "all_transactions": "အရောင်းအဝယ်အားလုံး", @@ -113,7 +114,7 @@ "change_currency": "ငွေကြေးကိုပြောင်းပါ။", "change_current_node": "လက်ရှိ နှာခေါင်း ကို ${node} သို့ ပြောင်းရန် သေချာပါသလား။", "change_current_node_title": "လက်ရှိ နှာခေါင်း ကိုပြောင်းပါ။", - "change_exchange_provider": "အပြန်အလှန် လဲလှယ်ရေး ထောက်ပံ့ပေးသူကို ပြောင်းလဲပါ", + "change_exchange_provider": "SWAP ပံ့ပိုးသူပြောင်းလဲပါ", "change_language": "ဘာသာစကားပြောင်းပါ။", "change_language_to": "ဘာသာစကားကို ${language} သို့ ပြောင်းမလား။", "change_password": "စကားဝှက်ကိုပြောင်းရန်", @@ -130,6 +131,7 @@ "choose_one": "တစ်ခုရွေးပါ။", "choose_relay": "အသုံးပြုရန် relay ကိုရွေးချယ်ပါ။", "choose_wallet_currency": "ပိုက်ဆံအိတ်ငွေကြေးကို ရွေးပါ-", + "choose_wallet_group": "ပိုက်ဆံအိတ်အုပ်စုရွေးပါ", "clear": "ရှင်းလင်းသော", "clearnet_link": "Clearnet လင့်ခ်", "close": "အနီးကပ်", @@ -142,6 +144,7 @@ "confirm_delete_wallet": "ဤလုပ်ဆောင်ချက်သည် ဤပိုက်ဆံအိတ်ကို ဖျက်လိုက်ပါမည်။ ဆက်လုပ်လိုပါသလား။", "confirm_fee_deduction": "အခကြေးငွေကိုနှုတ်ယူခြင်း", "confirm_fee_deduction_content": "output မှအခကြေးငွေကိုယူရန်သဘောတူပါသလား။", + "confirm_passphrase": "passphrase အတည်ပြုပါ", "confirm_sending": "ပေးပို့အတည်ပြုပါ။", "confirm_silent_payments_switch_node": "သင်၏လက်ရှိ node သည်အသံတိတ်ငွေပေးချေမှုကိုမပံ့ပိုးပါဟု \\ t", "confirmations": "အတည်ပြုချက်များ", @@ -160,6 +163,7 @@ "contact_name": "ဆက်သွယ်ရန်အမည်", "contact_support": "ပံ့ပိုးကူညီမှုထံ ဆက်သွယ်ပါ။", "continue_text": "ဆက်လက်", + "contract_warning": "ဒီစာချုပ်လိပ်စာအလားအလာအလားအလာအလားအလာအလံများကိုအလံလွှင့်တင်ခဲ့သည်။ ကျေးဇူးပြုပြီးသတိဖြင့်လုပ်ငန်းစဉ်။", "contractName": "စာချုပ်အမည်", "contractSymbol": "စာချုပ်သင်္ကေတ", "copied_key_to_clipboard": "${key} ကို Clipboard သို့ ကူးယူထားသည်။", @@ -175,6 +179,7 @@ "create_invoice": "ပြေစာဖန်တီးပါ။", "create_new": "Wallet အသစ်ဖန်တီးပါ။", "create_new_account": "အကောင့်အသစ်ဖန်တီးပါ။", + "create_new_seed": "မျိုးစေ့အသစ်ကိုဖန်တီးပါ", "creating_new_wallet": "ပိုက်ဆံအိတ်အသစ်ဖန်တီးခြင်း။", "creating_new_wallet_error": "အမှား- ${description}", "creation_date": "ဖန်တီးမှုနေ့စွဲ", @@ -195,6 +200,7 @@ "delete_wallet_confirm_message": "${wallet_name} ပိုက်ဆံအိတ်ကို ဖျက်လိုသည်မှာ သေချာပါသလား။", "deleteConnectionConfirmationPrompt": "ချိတ်ဆက်မှုကို ဖျက်လိုသည်မှာ သေချာပါသလား။", "denominations": "ဂိုဏ်းချုပ်ပစ္စည်းများ", + "derivationpath": "derivation လမ်းကြောင်း", "descending": "ဆင်း", "description": "ဖော်ပြချက်", "destination_tag": "ခရီးဆုံးအမှတ်-", @@ -207,6 +213,7 @@ "disable_buy": "ဝယ်ယူမှု လုပ်ဆောင်ချက်ကို ပိတ်ပါ။", "disable_cake_2fa": "ကိတ်မုန့် 2FA ကို ပိတ်ပါ။", "disable_exchange": "လဲလှယ်မှုကို ပိတ်ပါ။", + "disable_fee_api_warning": "ဤအရာကိုဖွင့်ခြင်းအားဖြင့်အချို့သောကိစ္စရပ်များတွင်အခကြေးငွေနှုန်းထားများသည်တိကျမှုရှိနိုင်သည်,", "disable_fiat": "Fiat ကိုပိတ်ပါ။", "disable_sell": "ရောင်းချခြင်းလုပ်ဆောင်ချက်ကို ပိတ်ပါ။", "disableBatteryOptimization": "ဘက်ထရီ optimization ကိုပိတ်ပါ", @@ -228,8 +235,10 @@ "edit_token": "တိုကင်ကို တည်းဖြတ်ပါ။", "electrum_address_disclaimer": "သင်အသုံးပြုသည့်အချိန်တိုင်းတွင် ကျွန်ုပ်တို့သည် လိပ်စာအသစ်များကို ထုတ်ပေးသော်လည်း ယခင်လိပ်စာများသည် ဆက်လက်အလုပ်လုပ်နေပါသည်။", "email_address": "အီးမေးလ်လိပ်စာ", + "enable": "စွမ်းဆောင်နိုင်စေ", + "enable_mempool_api": "Mempool API တိကျသောအခကြေးငွေနှင့်ရက်စွဲများအတွက်", "enable_replace_by_fee": "အစားထိုး - by- အခကြေးငွေ enable", - "enable_silent_payments_scanning": "အသံတိတ်ငွေပေးချေမှုကို scanable လုပ်ပါ", + "enable_silent_payments_scanning": "အစွန်အဖျားသို့ရောက်ရှိသည်အထိအသံတိတ်ငွေပေးချေမှုကိုစကင်ဖတ်စစ်ဆေးပါ", "enabled": "ဖွင့်ထားသည်။", "enter_amount": "ပမာဏကို ထည့်ပါ။", "enter_backup_password": "အရန်စကားဝှက်ကို ဤနေရာတွင် ထည့်ပါ။", @@ -272,8 +281,8 @@ "etherscan_history": "Etherscan သမိုင်း", "event": "ပွဲ", "events": "အဲ့ဒါနဲ့", - "exchange": "ချိန်းတယ်။", - "exchange_incorrect_current_wallet_for_xmr": "သင်၏ Cake Wallet Monero လက်ကျန်မှ XMR ကိုလဲလှယ်လိုပါက၊ သင်၏ Monero ပိုက်ဆံအိတ်သို့ ဦးစွာပြောင်းပါ။", + "exchange": "လဲလှယ်", + "exchange_incorrect_current_wallet_for_xmr": "အကယ်. သင်သည် XMR ကိုသင်၏ကိတ်မုန့် Monero Balance မှ Swap ကိုလဲလှယ်လိုပါကသင်၏ Monero Wallet ကိုပထမဆုံးအကြိမ်ပြောင်းပါ။", "exchange_new_template": "ပုံစံအသစ်", "exchange_provider_unsupported": "${providerName} မရှိတော့ပါ!", "exchange_result_confirm": "အတည်ပြုချက်ကို နှိပ်ခြင်းဖြင့်၊ သင်သည် ${fetchingLabel} ${from} ဟုခေါ်သော သင့်ပိုက်ဆံအိတ်မှ ${walletName} ကို အောက်ဖော်ပြပါလိပ်စာသို့ ပေးပို့မည်ဖြစ်ပါသည်။ သို့မဟုတ် သင့်ပြင်ပပိုက်ဆံအိတ်မှ အောက်ပါလိပ်စာ/QR ကုဒ်သို့ ပေးပို့နိုင်ပါသည်။\n\nပမာဏများကို ပြောင်းလဲရန် ဆက်လက်လုပ်ဆောင်ရန် သို့မဟုတ် ပြန်သွားရန် အတည်ပြုချက်ကို နှိပ်ပါ။", @@ -291,6 +300,8 @@ "failed_authentication": "အထောက်အထားစိစစ်ခြင်း မအောင်မြင်ပါ။. ${state_error}", "faq": "အမြဲမေးလေ့ရှိသောမေးခွန်းများ", "features": "အင်္ဂါရပ်များ", + "fee_less_than_min": "ရွေးချယ်ထားသည့်အခကြေးငွေသည်အနည်းဆုံးထက်နည်းသည်, ငွေပေးချေမှုကိုပေးပို့နိုင်ရန်အခကြေးငွေကိုတိုးမြှင့်ပေးပါ", + "fee_rate": "ကြေးနှုန်း", "fetching": "ခေါ်ယူခြင်း။", "fiat_api": "Fiat API", "fiat_balance": "Fiat Balance", @@ -298,7 +309,7 @@ "fill_code": "သင့်အီးမေးလ်သို့ ပေးထားသည့် အတည်ပြုကုဒ်ကို ဖြည့်ပါ။", "filter_by": "အလိုက် စစ်ထုတ်ပါ။", "first_wallet_text": "Monero၊ Bitcoin၊ Ethereum၊ Litecoin နှင့် Haven အတွက် အလွန်ကောင်းမွန်သော ပိုက်ဆံအိတ်", - "fixed_pair_not_supported": "ရွေးချယ်ထားသော ဖလှယ်မှုများဖြင့် ဤပုံသေအတွဲကို ပံ့ပိုးမထားပါ။", + "fixed_pair_not_supported": "ဤပုံသေစုံတွဲသည်ရွေးချယ်ထားသောလဲလှယ်ရေးအစီအစဉ်များဖြင့်မထောက်ပံ့ပါ", "fixed_rate": "ပုံသေနှုန်း", "fixed_rate_alert": "ပုံသေနှုန်းထားမုဒ်ကို စစ်ဆေးသည့်အခါ လက်ခံပမာဏကို ထည့်သွင်းနိုင်မည်ဖြစ်သည်။ ပုံသေနှုန်းမုဒ်သို့ ပြောင်းလိုပါသလား။", "forgot_password": "စကားဝှက်မေ့နေပါသလား", @@ -354,6 +365,20 @@ "ledger_error_wrong_app": "ကျေးဇူးပြု. သင့်လက်ျာအက်ပ်ကိုသင်၏ Ledger တွင်ဖွင့်ရန်သေချာစေပါ", "ledger_please_enable_bluetooth": "သင်၏ Ledger ကိုရှာဖွေရန် Bluetooth ကိုဖွင့်ပါ", "light_theme": "အလင်း", + "litecoin_enable_mweb_sync": "mweb scanning ဖွင့်ပါ", + "litecoin_mweb": "မင်္ဂလာပါ", + "litecoin_mweb_always_scan": "Mweb အမြဲစကင်ဖတ်စစ်ဆေးပါ", + "litecoin_mweb_description": "Mweb သည် Protocol အသစ်ဖြစ်ပြီး LitCoin သို့ပိုမိုဈေးချိုသာသော, စျေးသက်သက်သာသာသုံးခြင်းနှင့်ပိုမိုများပြားသောပုဂ္ဂလိကငွေပို့ဆောင်မှုများကိုဖြစ်ပေါ်စေသည်", + "litecoin_mweb_dismiss": "ထုတ်ပစ်", + "litecoin_mweb_display_card": "MweB ကဒ်ကိုပြပါ", + "litecoin_mweb_enable_later": "သင် MweB ကို display settings အောက်ရှိ ထပ်မံ. ခွင့်ပြုရန်ရွေးချယ်နိုင်သည်။", + "litecoin_mweb_pegin": "တံစို့", + "litecoin_mweb_pegout": "တံစို့", + "litecoin_mweb_scanning": "mweb scanning", + "litecoin_mweb_settings": "Mweb ဆက်တင်များ", + "litecoin_mweb_warning": "MweB ကိုအသုံးပြုခြင်းသည်အစပိုင်းတွင် ~ 600MB ဒေတာများကို download လုပ်ပြီးကွန်ယက်အမြန်နှုန်းပေါ် မူတည်. မိနစ် 30 အထိကြာနိုင်သည်။ ဤကန ဦး ဒေတာကိုတစ်ကြိမ်သာ download လုပ်ပြီး litecoin Walkets အားလုံးအတွက်ရနိုင်သည်", + "litecoin_what_is_mweb": "MweB ဆိုတာဘာလဲ။", + "live_fee_rates": "API မှတစ်ဆင့် Live အခကြေးငွေနှုန်းထားများ", "load_more": "ပိုပြီး load", "loading_your_wallet": "သင့်ပိုက်ဆံအိတ်ကို ဖွင့်နေသည်။", "login": "လော့ဂ်အင်", @@ -383,6 +408,8 @@ "monero_light_theme": "Monero Light အပြင်အဆင်", "moonpay_alert_text": "ပမာဏ၏တန်ဖိုးသည် ${minAmount} ${fiatCurrency} နှင့် ပိုနေရမည်", "more_options": "နောက်ထပ် ရွေးချယ်စရာများ", + "mweb_confirmed": "အတည်ပြုလိုက် mweb", + "mweb_unconfirmed": "အတည်မပြုနိုင်သော mweb", "name": "နာမည်", "nano_current_rep": "လက်ရှိကိုယ်စားလှယ်", "nano_gpt_thanks_message": "nanogpt ကိုသုံးပြီးကျေးဇူးတင်ပါတယ် သင်၏ငွေပေးငွေယူပြီးနောက် browser သို့ပြန်သွားရန်သတိရပါ။", @@ -412,7 +439,7 @@ "node_test": "စမ်း", "nodes": "ဆုံမှတ်များ", "nodes_list_reset_to_default_message": "ဆက်တင်များကို မူရင်းအတိုင်း ပြန်လည်သတ်မှတ်လိုသည်မှာ သေချာပါသလား။", - "none_of_selected_providers_can_exchange": "ရွေးချယ်ထားသော ဝန်ဆောင်မှုပေးသူများမှ ဤလဲလှယ်မှုကို ပြုလုပ်၍မရပါ။", + "none_of_selected_providers_can_exchange": "ရွေးချယ်ထားသောပံ့ပိုးပေးသူတစ် ဦး တစ်ယောက်မှဤအစီအစဉ်ကိုလုပ်နိုင်သည်", "noNFTYet": "NFTs မရှိသေးပါ။", "normal": "ပုံမှန်", "note_optional": "မှတ်ချက် (ချန်လှပ်ထားနိုင်သည်)", @@ -442,6 +469,7 @@ "overwrite_amount": "ပမာဏကို ထပ်ရေးပါ။", "pairingInvalidEvent": "မမှန်ကန်သောဖြစ်ရပ်ကို တွဲချိတ်ခြင်း။", "passphrase": "passphrase (optional)", + "passphrases_doesnt_match": "passphrases မကိုက်ညီဘူး, ကျေးဇူးပြုပြီးထပ်ကြိုးစားပါ", "password": "စကားဝှက်", "paste": "ငါးပိ", "pause_wallet_creation": "Haven Wallet ဖန်တီးနိုင်မှုကို လောလောဆယ် ခေတ္တရပ်ထားသည်။", @@ -593,8 +621,11 @@ "seed_share": "မျိုးစေ့မျှဝေပါ။", "seed_title": "မျိုးစေ့", "seedtype": "မျိုးပွားခြင်း", + "seedtype_alert_content": "အခြားပိုက်ဆံအိတ်များနှင့်မျိုးစေ့များကိုမျှဝေခြင်းသည် BIP39 sebyspe ဖြင့်သာဖြစ်သည်။", + "seedtype_alert_title": "ပျိုးပင်သတိပေးချက်", "seedtype_legacy": "အမွေအနှစ် (စကားလုံး 25 လုံး)", "seedtype_polyseed": "polyseed (စကားလုံး 16 လုံး)", + "seedtype_wownero": "Wownero (စကားလုံး 14 လုံး)", "select_backup_file": "အရန်ဖိုင်ကို ရွေးပါ။", "select_buy_provider_notice": "အပေါ်ကဝယ်သူတစ် ဦး ကိုရွေးချယ်ပါ။ သင်၏ default 0 ယ်သူအား app settings တွင် setting လုပ်ခြင်းဖြင့်ဤ screen ကိုကျော်သွားနိုင်သည်။", "select_destination": "အရန်ဖိုင်အတွက် ဦးတည်ရာကို ရွေးပါ။", @@ -605,6 +636,7 @@ "send": "ပို့ပါ။", "send_address": "${cryptoCurrency} လိပ်စာ", "send_amount": "ပမာဏ-", + "send_change_to_you": "ပြောင်းလဲမှု,", "send_creating_transaction": "အရောင်းအဝယ်ပြုလုပ်ခြင်း။", "send_error_currency": "ငွေကြေးတွင် နံပါတ်များသာ ပါဝင်နိုင်သည်။", "send_error_minimum_value": "ပမာဏ၏ အနည်းဆုံးတန်ဖိုးမှာ 0.01 ဖြစ်သည်။", @@ -658,19 +690,21 @@ "setup_your_debit_card": "သင့်ဒက်ဘစ်ကတ်ကို စနစ်ထည့်သွင်းပါ။", "share": "မျှဝေပါ။", "share_address": "လိပ်စာမျှဝေပါ။", + "shared_seed_wallet_groups": "shared မျိုးစေ့ပိုက်ဆံအိတ်အုပ်စုများ", "show_details": "အသေးစိတ်ပြ", "show_keys": "မျိုးစေ့ /သော့များကို ပြပါ။", "show_market_place": "စျေးကွက်ကိုပြသပါ။", "show_seed": "မျိုးစေ့ကိုပြပါ။", "sign_message": "လက်မှတ်စာ", "sign_up": "ဆိုင်းအပ်", - "sign_verify_message": "မက်ဆေ့ခ်ျကိုလက်မှတ်ထိုးသို့မဟုတ်အတည်ပြုရန်", + "sign_verify_message": "လက်မှတ်ထိုး / အတည်ပြုရန်", "sign_verify_message_sub": "သင်၏ကိုယ်ပိုင်သော့ကို သုံး. မက်ဆေ့ခ်ျကိုလက်မှတ်ထိုးပါ", "sign_verify_title": "လက်မှတ်ထိုး / အတည်ပြုရန်", "signature": "လက်မှတ်", "signature_invalid_error": "အဆိုပါလက်မှတ်ပေးထားသောမက်ဆေ့ခ်ျကိုများအတွက်မမှန်ကန်ပါ", "signTransaction": "ငွေလွှဲဝင်ပါ။", "signup_for_card_accept_terms": "ကတ်အတွက် စာရင်းသွင်းပြီး စည်းကမ်းချက်များကို လက်ခံပါ။", + "silent_payment": "အသံတိတ်ငွေပေးချေမှု", "silent_payments": "အသံတိတ်ငွေပေးချေမှု", "silent_payments_always_scan": "အမြဲတမ်း scanning အမြဲ scanning", "silent_payments_disclaimer": "လိပ်စာအသစ်များသည်အထောက်အထားအသစ်များမဟုတ်ပါ။ ၎င်းသည်ကွဲပြားခြားနားသောတံဆိပ်ဖြင့်ရှိပြီးသားဝိသေသလက်ခဏာကိုပြန်လည်အသုံးပြုခြင်းဖြစ်သည်။", @@ -681,6 +715,7 @@ "silent_payments_scanned_tip": "အစွန်အဖျားမှ scan ဖတ်! (${tip})", "silent_payments_scanning": "အသံတိတ်ငွေပေးချေမှု scanning", "silent_payments_settings": "အသံတိတ်ငွေပေးချေမှုဆက်တင်များ", + "single_seed_wallets_group": "တစ်ခုတည်းမျိုးစေ့ပိုက်ဆံအိတ်", "slidable": "လျှောချနိုင်သည်။", "sort_by": "အလိုက်စဥ်သည်", "spend_key_private": "သော့သုံးရန် (သီးသန့်)", @@ -703,12 +738,13 @@ "switchToEVMCompatibleWallet": "ကျေးဇူးပြု၍ EVM တွဲဖက်သုံးနိုင်သော ပိုက်ဆံအိတ်သို့ ပြောင်းပြီး ထပ်စမ်းကြည့်ပါ (Ethereum၊ Polygon)", "symbol": "သင်္ကေတ", "sync_all_wallets": "အားလုံးပိုက်ဆံအိတ်စည်းညှိ", + "sync_status_attempting_scan": "scan ကြိုးစားနေ", "sync_status_attempting_sync": "ချိန်ကိုက်ခြင်းကို ကြိုးစားနေသည်။", "sync_status_connected": "ချိတ်ဆက်ထားသည်။", "sync_status_connecting": "ချိတ်ဆက်ခြင်း။", "sync_status_failed_connect": "အဆက်အသွယ်ဖြတ်ထားသည်။", "sync_status_not_connected": "မချိတ်ဆက်ပါ။", - "sync_status_starting_scan": "စကင်ဖတ်စစ်ဆေးမှု", + "sync_status_starting_scan": "စကင်ဖတ်စစ်ဆေးမှုစတင်ခြင်း (${height})", "sync_status_starting_sync": "စင့်ခ်လုပ်ခြင်း။", "sync_status_syncronized": "ထပ်တူပြုထားသည်။", "sync_status_syncronizing": "ထပ်တူပြုခြင်း။", @@ -795,6 +831,7 @@ "trusted": "ယုံတယ်။", "tx_commit_exception_no_dust_on_change": "အဆိုပါငွေပေးငွေယူကဒီပမာဏနှင့်အတူပယ်ချခံရသည်။ ဤဒင်္ဂါးပြားများနှင့်အတူပြောင်းလဲမှုကိုပြန်လည်ပြောင်းလဲခြင်းသို့မဟုတ် ${min} မပါဘဲ ${max} ပေးပို့နိုင်သည်။", "tx_commit_failed": "ငွေပေးငွေယူကျူးလွန်မှုပျက်ကွက်။ ကျေးဇူးပြုပြီးပံ့ပိုးမှုဆက်သွယ်ပါ။", + "tx_commit_failed_no_peers": "ငွေပေးငွေယူထုတ်လွှင့်ရန်ပျက်ကွက်ပါက ကျေးဇူးပြု. ဒုတိယသို့မဟုတ်ထိုအတိုင်းထပ်မံကြိုးစားပါ", "tx_invalid_input": "သင်သည်ဤငွေပေးချေမှုအမျိုးအစားအတွက်မှားယွင်းသော input type ကိုအသုံးပြုနေသည်", "tx_no_dust_exception": "ငွေပမာဏကိုသေးငယ်လွန်းသောငွေပမာဏကိုပေးပို့ခြင်းဖြင့်ပယ်ဖျက်ခြင်းကိုငြင်းပယ်သည်။ ကျေးဇူးပြုပြီးငွေပမာဏကိုတိုးမြှင့်ကြိုးစားပါ။", "tx_not_enough_inputs_exception": "အလုံအလောက်သွင်းအားစုများမလုံလောက်။ ကျေးဇူးပြုပြီးဒင်္ဂါးပြားထိန်းချုပ်မှုအောက်တွင်ပိုမိုရွေးချယ်ပါ", @@ -839,8 +876,16 @@ "view_transaction_on": "ငွေလွှဲခြင်းကို ဖွင့်ကြည့်ပါ။", "voting_weight": "မဲပေးအလေးချိန်", "waitFewSecondForTxUpdate": "ငွေပေးငွေယူ မှတ်တမ်းတွင် ရောင်ပြန်ဟပ်ရန် စက္ကန့်အနည်းငယ်စောင့်ပါ။", + "wallet_group": "ပိုက်ဆံအိတ်အုပ်စု", + "wallet_group_description_four": "လုံးဝအသစ်သောမျိုးစေ့နှင့်အတူပိုက်ဆံအိတ်ဖန်တီးရန်။", + "wallet_group_description_one": "ကိတ်မုန့်၌, သင်တစ် ဦး ဖန်တီးနိုင်ပါတယ်", + "wallet_group_description_three": "ရရှိနိုင်သည့်ပိုက်ဆံအိတ်နှင့် / သို့မဟုတ်ပိုက်ဆံအိတ်အုပ်စုများမြင်ကွင်းကိုကြည့်ရှုရန်။ သို့မဟုတ်ရွေးချယ်ပါ", + "wallet_group_description_two": "နှင့်အတူမျိုးစေ့ဝေမျှဖို့ရှိပြီးသားပိုက်ဆံအိတ်တစ်ခုရွေးချယ်ခြင်းအားဖြင့်။ ပိုက်ဆံအိတ်အုပ်စုတစ်ခုစီတွင်ငွေကြေးအမျိုးအစားတစ်ခုစီ၏တစ်ခုတည်းသောပိုက်ဆံအိတ်တစ်ခုပါ 0 င်နိုင်သည်။ \n\n သင်ရွေးချယ်နိုင်သည်", + "wallet_group_empty_state_text_one": "သင့်တွင်သဟဇာတဖြစ်သောပိုက်ဆံအိတ်အုပ်စုများမရှိပါ။ !\n\n ကိုအသာပုတ်ပါ", + "wallet_group_empty_state_text_two": "အသစ်တစ်ခုကိုတစ်ခုလုပ်ဖို့အောက်တွင်ဖော်ပြထားသော။", "wallet_keys": "ပိုက်ဆံအိတ် အစေ့/သော့များ", "wallet_list_create_new_wallet": "Wallet အသစ်ဖန်တီးပါ။", + "wallet_list_edit_group_name": "အုပ်စုအမည်ကိုတည်းဖြတ်ပါ", "wallet_list_edit_wallet": "ပိုက်ဆံအိတ်ကို တည်းဖြတ်ပါ။", "wallet_list_failed_to_load": "${wallet_name} ပိုက်ဆံအိတ်ကို ဖွင့်၍မရပါ။ ${error}", "wallet_list_failed_to_remove": "${wallet_name} ပိုက်ဆံအိတ်ကို ဖယ်ရှား၍မရပါ။ ${error}", diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 2d55344f5..3f2df531b 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -36,6 +36,7 @@ "agree": "mee eens", "agree_and_continue": "Akkoord & doorgaan", "agree_to": "Door een account aan te maken gaat u akkoord met de ", + "alert_notice": "Kennisgeving", "all": "ALLE", "all_trades": "Alle transacties", "all_transactions": "Alle transacties", @@ -113,7 +114,7 @@ "change_currency": "Verander valuta", "change_current_node": "Weet u zeker dat u het huidige knooppunt wilt wijzigen in ${node}?", "change_current_node_title": "Wijzig het huidige knooppunt", - "change_exchange_provider": "Wijzig Exchange Provider", + "change_exchange_provider": "Verander Swap Provider", "change_language": "Verander de taal", "change_language_to": "Verander de taal in ${language}?", "change_password": "Wachtwoord wijzigen", @@ -130,6 +131,7 @@ "choose_one": "Kies er een", "choose_relay": "Kies een relais dat u wilt gebruiken", "choose_wallet_currency": "Kies een portemonnee-valuta:", + "choose_wallet_group": "Kies portemonnee groep", "clear": "Duidelijk", "clearnet_link": "Clearnet-link", "close": "Dichtbij", @@ -142,6 +144,7 @@ "confirm_delete_wallet": "Met deze actie wordt deze portemonnee verwijderd. Wilt u doorgaan?", "confirm_fee_deduction": "Bevestig de aftrek van de kosten", "confirm_fee_deduction_content": "Stemt u ermee in om de vergoeding af te trekken van de output?", + "confirm_passphrase": "Bevestig Passaspherase", "confirm_sending": "Bevestig verzending", "confirm_silent_payments_switch_node": "Uw huidige knooppunt ondersteunt geen stille betalingen \\ ncake -portemonnee schakelt over naar een compatibele knoop", "confirmations": "Bevestigingen", @@ -160,6 +163,7 @@ "contact_name": "Contactnaam", "contact_support": "Contact opnemen met ondersteuning", "continue_text": "Doorgaan met", + "contract_warning": "Dit contractadres is gemarkeerd als mogelijk frauduleus. Verwerk met voorzichtigheid.", "contractName": "Contractnaam", "contractSymbol": "Contractsymbool", "copied_key_to_clipboard": "Gekopieerd ${key} naar het klembord", @@ -175,6 +179,7 @@ "create_invoice": "Factuur maken", "create_new": "Maak een nieuwe portemonnee", "create_new_account": "Creëer een nieuw account", + "create_new_seed": "Maak nieuw zaadje", "creating_new_wallet": "Nieuwe portemonnee aanmaken", "creating_new_wallet_error": "Fout: ${description}", "creation_date": "Aanmaakdatum", @@ -195,6 +200,7 @@ "delete_wallet_confirm_message": "Weet u zeker dat u de portemonnee van ${wallet_name} wilt verwijderen?", "deleteConnectionConfirmationPrompt": "Weet u zeker dat u de verbinding met", "denominations": "Denominaties", + "derivationpath": "Afleidingspad", "descending": "Aflopend", "description": "Beschrijving", "destination_tag": "Bestemmingstag:", @@ -207,6 +213,7 @@ "disable_buy": "Koopactie uitschakelen", "disable_cake_2fa": "Taart 2FA uitschakelen", "disable_exchange": "Uitwisseling uitschakelen", + "disable_fee_api_warning": "Door dit uit te schakelen, kunnen de tarieven in sommige gevallen onnauwkeurig zijn, dus u kunt de vergoedingen voor uw transacties te veel betalen of te weinig betalen", "disable_fiat": "Schakel Fiat uit", "disable_sell": "Verkoopactie uitschakelen", "disableBatteryOptimization": "Schakel de batterijoptimalisatie uit", @@ -228,8 +235,10 @@ "edit_token": "Token bewerken", "electrum_address_disclaimer": "We genereren nieuwe adressen elke keer dat u er een gebruikt, maar eerdere adressen blijven werken", "email_address": "E-mailadres", + "enable": "Inschakelen", + "enable_mempool_api": "Mempool API voor nauwkeurige kosten en datums", "enable_replace_by_fee": "Schakel vervangen door een fee", - "enable_silent_payments_scanning": "Schakel stille betalingen in scannen in", + "enable_silent_payments_scanning": "Begin met het scannen van stille betalingen, totdat de tip is bereikt", "enabled": "Ingeschakeld", "enter_amount": "Voer Bedrag in", "enter_backup_password": "Voer hier een back-upwachtwoord in", @@ -272,8 +281,8 @@ "etherscan_history": "Etherscan-geschiedenis", "event": "Evenement", "events": "Evenementen", - "exchange": "Uitwisseling", - "exchange_incorrect_current_wallet_for_xmr": "Als u XMR wilt omwisselen van uw Cake Wallet Monero-saldo, moet u eerst overschakelen naar uw Monero-portemonnee.", + "exchange": "Ruil", + "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!", "exchange_result_confirm": "Door op bevestigen te drukken, wordt u verzonden ${fetchingLabel} ${from} uit je portemonnee genoemd ${walletName} naar het onderstaande adres. Of u kunt vanuit uw externe portemonnee naar het onderstaande adres verzenden / QR-code sturen.\n\nDruk op bevestigen om door te gaan of terug te gaan om de bedragen te wijzigen.", @@ -291,6 +300,8 @@ "failed_authentication": "Mislukte authenticatie. ${state_error}", "faq": "FAQ", "features": "Functies", + "fee_less_than_min": "Geselecteerde vergoeding is lager dan het minimum, verhoog de kosten om de transactie te kunnen verzenden", + "fee_rate": "Tarief", "fetching": "Ophalen", "fiat_api": "Fiat API", "fiat_balance": "Fiat Balans", @@ -298,7 +309,7 @@ "fill_code": "Vul de verificatiecode in die u in uw e-mail hebt ontvangen", "filter_by": "Filteren op", "first_wallet_text": "Geweldige portemonnee voor Monero, Bitcoin, Ethereum, Litecoin, en Haven", - "fixed_pair_not_supported": "Dit vaste paar wordt niet ondersteund bij de geselecteerde exchanges", + "fixed_pair_not_supported": "Dit vaste paar wordt niet ondersteund met de geselecteerde swap -services", "fixed_rate": "Vast tarief", "fixed_rate_alert": "U kunt het ontvangen bedrag invoeren wanneer de modus voor vaste tarieven is aangevinkt. Wilt u overschakelen naar de vaste-tariefmodus?", "forgot_password": "Wachtwoord vergeten", @@ -354,6 +365,20 @@ "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", "light_theme": "Licht", + "litecoin_enable_mweb_sync": "MWEB -scanning inschakelen", + "litecoin_mweb": "Mweb", + "litecoin_mweb_always_scan": "Stel mweb altijd op scannen", + "litecoin_mweb_description": "MWEB is een nieuw protocol dat snellere, goedkopere en meer privé -transacties naar Litecoin brengt", + "litecoin_mweb_dismiss": "Afwijzen", + "litecoin_mweb_display_card": "Toon MWEB -kaart", + "litecoin_mweb_enable_later": "U kunt ervoor kiezen om MWeb opnieuw in te schakelen onder weergave -instellingen.", + "litecoin_mweb_pegin": "Vastmaken", + "litecoin_mweb_pegout": "Uithakken", + "litecoin_mweb_scanning": "MWEB -scanning", + "litecoin_mweb_settings": "MWEB -instellingen", + "litecoin_mweb_warning": "Het gebruik van MWeb downloadt in eerste instantie ~ 600 MB aan gegevens en kan tot 30 minuten duren, afhankelijk van de netwerksnelheid. Deze eerste gegevens worden slechts eenmaal gedownload en zijn beschikbaar voor alle Litecoin -portefeuilles", + "litecoin_what_is_mweb": "Wat is Mweb?", + "live_fee_rates": "Live -tarieven via API", "load_more": "Meer laden", "loading_your_wallet": "Uw portemonnee laden", "login": "Log in", @@ -383,6 +408,8 @@ "monero_light_theme": "Monero Light-thema", "moonpay_alert_text": "Waarde van het bedrag moet meer of gelijk zijn aan ${minAmount} ${fiatCurrency}", "more_options": "Meer opties", + "mweb_confirmed": "Bevestigde MWEB", + "mweb_unconfirmed": "Onbevestigde MWEB", "name": "Naam", "nano_current_rep": "Huidige vertegenwoordiger", "nano_gpt_thanks_message": "Bedankt voor het gebruik van Nanogpt! Vergeet niet om terug te gaan naar de browser nadat uw transactie is voltooid!", @@ -412,7 +439,7 @@ "node_test": "Test", "nodes": "Knooppunten", "nodes_list_reset_to_default_message": "Weet u zeker dat u de standaardinstellingen wilt herstellen?", - "none_of_selected_providers_can_exchange": "Geen van de geselecteerde providers kan deze uitwisseling maken", + "none_of_selected_providers_can_exchange": "Geen van de geselecteerde providers kan deze swap maken", "noNFTYet": "Nog geen NFT's", "normal": "Normaal", "note_optional": "Opmerking (optioneel)", @@ -442,6 +469,7 @@ "overwrite_amount": "Overwrite amount", "pairingInvalidEvent": "Koppelen Ongeldige gebeurtenis", "passphrase": "PassaspHRASE (optioneel)", + "passphrases_doesnt_match": "Passaspelfiaal komt niet overeen, probeer het opnieuw", "password": "Wachtwoord", "paste": "Plakken", "pause_wallet_creation": "De mogelijkheid om Haven Wallet te maken is momenteel onderbroken.", @@ -593,8 +621,11 @@ "seed_share": "Deel zaad", "seed_title": "Zaad", "seedtype": "Zaadtype", + "seedtype_alert_content": "Het delen van zaden met andere portefeuilles is alleen mogelijk met BIP39 SeedType.", + "seedtype_alert_title": "Zaadtype alert", "seedtype_legacy": "Legacy (25 woorden)", "seedtype_polyseed": "Polyseed (16 woorden)", + "seedtype_wownero": "WOWNERO (14 woorden)", "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.", @@ -605,6 +636,7 @@ "send": "Sturen", "send_address": "${cryptoCurrency}-adres", "send_amount": "Bedrag:", + "send_change_to_you": "Verander, aan jou:", "send_creating_transaction": "Transactie maken", "send_error_currency": "Valuta kan alleen cijfers bevatten", "send_error_minimum_value": "Minimale waarde van bedrag is 0,01", @@ -658,6 +690,7 @@ "setup_your_debit_card": "Stel uw debetkaart in", "share": "Deel", "share_address": "Deel adres", + "shared_seed_wallet_groups": "Gedeelde zaadportelgroepen", "show_details": "Toon details", "show_keys": "Toon zaad/sleutels", "show_market_place": "Toon Marktplaats", @@ -671,6 +704,7 @@ "signature_invalid_error": "De handtekening is niet geldig voor het gegeven bericht", "signTransaction": "Transactie ondertekenen", "signup_for_card_accept_terms": "Meld je aan voor de kaart en accepteer de voorwaarden.", + "silent_payment": "Stille betaling", "silent_payments": "Stille betalingen", "silent_payments_always_scan": "Stel stille betalingen in het scannen", "silent_payments_disclaimer": "Nieuwe adressen zijn geen nieuwe identiteiten. Het is een hergebruik van een bestaande identiteit met een ander label.", @@ -681,6 +715,7 @@ "silent_payments_scanned_tip": "Gescand om te fooien! (${tip})", "silent_payments_scanning": "Stille betalingen scannen", "silent_payments_settings": "Stille betalingsinstellingen", + "single_seed_wallets_group": "Enkele zaadportefeuilles", "slidable": "Verschuifbaar", "sort_by": "Sorteer op", "spend_key_private": "Sleutel uitgeven (privaat)", @@ -703,12 +738,13 @@ "switchToEVMCompatibleWallet": "Schakel over naar een EVM-compatibele portemonnee en probeer het opnieuw (Ethereum, Polygon)", "symbol": "Symbool", "sync_all_wallets": "Alle portemonnees synchroniseren", + "sync_status_attempting_scan": "Proberen scan", "sync_status_attempting_sync": "SYNCHRONISATIE PROBEREN", "sync_status_connected": "VERBONDEN", "sync_status_connecting": "AANSLUITING", "sync_status_failed_connect": "LOSGEKOPPELD", "sync_status_not_connected": "NIET VERBONDEN", - "sync_status_starting_scan": "Startscan", + "sync_status_starting_scan": "SCAN starten (van ${height})", "sync_status_starting_sync": "BEGINNEN MET SYNCHRONISEREN", "sync_status_syncronized": "SYNCHRONIZED", "sync_status_syncronizing": "SYNCHRONISEREN", @@ -795,6 +831,7 @@ "trusted": "vertrouwd", "tx_commit_exception_no_dust_on_change": "De transactie wordt afgewezen met dit bedrag. Met deze munten kunt u ${min} verzenden zonder verandering of ${max} die wijziging retourneert.", "tx_commit_failed": "Transactiebewissing is mislukt. Neem contact op met de ondersteuning.", + "tx_commit_failed_no_peers": "De transactie is niet uitgezonden, probeer het opnieuw binnen een seconde of zo", "tx_invalid_input": "U gebruikt het verkeerde invoertype voor dit type betaling", "tx_no_dust_exception": "De transactie wordt afgewezen door een te klein bedrag te verzenden. Probeer het bedrag te verhogen.", "tx_not_enough_inputs_exception": "Niet genoeg ingangen beschikbaar. Selecteer meer onder muntenbesturing", @@ -840,8 +877,16 @@ "voting_weight": "Stemgewicht", "waitFewSecondForTxUpdate": "Wacht een paar seconden totdat de transactie wordt weergegeven in de transactiegeschiedenis", "waiting_payment_confirmation": "In afwachting van betalingsbevestiging", + "wallet_group": "Portemonnee", + "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_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_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", "wallet_list_create_new_wallet": "Maak een nieuwe portemonnee", + "wallet_list_edit_group_name": "Groepsnaam bewerken", "wallet_list_edit_wallet": "Portemonnee bewerken", "wallet_list_failed_to_load": "Laden mislukt ${wallet_name} portemonnee. ${error}", "wallet_list_failed_to_remove": "Verwijderen mislukt ${wallet_name} portemonnee. ${error}", diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index 833fc0308..91b265144 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -36,6 +36,7 @@ "agree": "Zgadzam się", "agree_and_continue": "Zgadzam się i kontynuuj", "agree_to": "Tworząc konto wyrażasz zgodę na ", + "alert_notice": "Ogłoszenie", "all": "WSZYSTKO", "all_trades": "Wszystkie operacje", "all_transactions": "Wszystkie transakcje", @@ -113,7 +114,7 @@ "change_currency": "Zmień walutę", "change_current_node": "Czy na pewno chcesz wybrać ten węzeł? ${node}?", "change_current_node_title": "Zmień bieżący węzeł", - "change_exchange_provider": "Zmień dostawcę wymiany", + "change_exchange_provider": "Zmień dostawca zamiany", "change_language": "Zmień język", "change_language_to": "Zmień język na ${language}?", "change_password": "Zmień hasło", @@ -130,6 +131,7 @@ "choose_one": "Wybierz jeden", "choose_relay": "Wybierz przekaźnik, którego chcesz użyć", "choose_wallet_currency": "Wybierz walutę portfela:", + "choose_wallet_group": "Wybierz grupę portfela", "clear": "Wyczyść", "clearnet_link": "łącze Clearnet", "close": "Zamknąć", @@ -142,6 +144,7 @@ "confirm_delete_wallet": "Ta czynność usunie ten portfel. Czy chcesz kontynuować?", "confirm_fee_deduction": "Potwierdź odliczenie opłaty", "confirm_fee_deduction_content": "Czy zgadzasz się odliczyć opłatę od wyników?", + "confirm_passphrase": "Potwierdź hasło", "confirm_sending": "Potwierdź wysłanie", "confirm_silent_payments_switch_node": "Twój obecny węzeł nie obsługuje cichych płatności \\ NCAKE Portfel przełączy się na kompatybilny węzeł, tylko do skanowania", "confirmations": "Potwierdzenia", @@ -160,6 +163,7 @@ "contact_name": "Nazwa Kontaktu", "contact_support": "Skontaktuj się z pomocą techniczną", "continue_text": "Dalej", + "contract_warning": "Ten adres umowy został oznaczony jako potencjalnie nieuczciwy. Prosimy o ostrożność.", "contractName": "Nazwa umowy", "contractSymbol": "Symbol kontraktu", "copied_key_to_clipboard": "Skopiowaneo ${key} do schowka", @@ -175,6 +179,7 @@ "create_invoice": "Wystaw fakturę", "create_new": "Utwórz nowy portfel", "create_new_account": "Stwórz nowe konto", + "create_new_seed": "Utwórz nowe ziarno", "creating_new_wallet": "Tworzenie nowego portfela", "creating_new_wallet_error": "Błąd: ${description}", "creation_date": "Data utworzenia", @@ -195,6 +200,7 @@ "delete_wallet_confirm_message": "Czy na pewno chcesz usunąć portfel ${wallet_name}?", "deleteConnectionConfirmationPrompt": "Czy na pewno chcesz usunąć połączenie z", "denominations": "Wyznaczenia", + "derivationpath": "Ścieżka pochodna", "descending": "Schodzenie", "description": "Opis", "destination_tag": "Tag docelowy:", @@ -207,6 +213,7 @@ "disable_buy": "Wyłącz akcję kupna", "disable_cake_2fa": "Wyłącz Cake 2FA", "disable_exchange": "Wyłącz wymianę", + "disable_fee_api_warning": "Wyłączając to, stawki opłaty mogą być w niektórych przypadkach niedokładne, więc możesz skończyć się przepłaceniem lub wynagrodzeniem opłat za transakcje", "disable_fiat": "Wyłącz waluty FIAT", "disable_sell": "Wyłącz akcję sprzedaży", "disableBatteryOptimization": "Wyłącz optymalizację baterii", @@ -228,8 +235,10 @@ "edit_token": "Edytuj token", "electrum_address_disclaimer": "Za każdym razem, gdy wykorzystasz adres, dla wiekszej prywatności generujemy nowy, ale poprzednie adresy nadal działają, i moga odbierać środki", "email_address": "Adres e-mail", + "enable": "Włączać", + "enable_mempool_api": "Mempool API dla dokładnych opłat i dat", "enable_replace_by_fee": "Włącz wymianę po lewej", - "enable_silent_payments_scanning": "Włącz skanowanie cichych płatności", + "enable_silent_payments_scanning": "Zacznij skanować ciche płatności, aż do osiągnięcia wskazówki", "enabled": "Włączone", "enter_amount": "Wprowadź kwotę", "enter_backup_password": "Wprowadź tutaj hasło kopii zapasowej", @@ -272,8 +281,8 @@ "etherscan_history": "Historia Etherscanu", "event": "Wydarzenie", "events": "Wydarzenia", - "exchange": "Wymień", - "exchange_incorrect_current_wallet_for_xmr": "Jeśli chcesz wymienić XMR z salda Cake Wallet Monero, najpierw przełącz się na portfel Monero.", + "exchange": "Zamieniać", + "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!", "exchange_result_confirm": "Naciskając Potwierdź, wyślesz ${fetchingLabel} ${from} z twojego portfela ${walletName} na adres podany poniżej. Lub możesz wysłać z zewnętrznego portfela na poniższy adres / kod QR.\n\nNaciśnij Potwierdź, aby kontynuować lub wróć, aby zmienić kwoty.", @@ -291,6 +300,8 @@ "failed_authentication": "Nieudane uwierzytelnienie. ${state_error}", "faq": "FAQ", "features": "Cechy", + "fee_less_than_min": "Wybrana opłata jest niższa niż minimum, proszę zwiększyć opłaty, aby móc wysłać transakcję", + "fee_rate": "Stawka opłaty", "fetching": "Pobieranie", "fiat_api": "API Walut FIAT", "fiat_balance": "Bilans Fiata", @@ -298,7 +309,7 @@ "fill_code": "Proszę wpisać kod weryfikacyjny który otrzymałeś w wiadomości e-mail", "filter_by": "Filtruj według", "first_wallet_text": "Świetny portfel na Monero, Bitcoin, Ethereum, Litecoin, i Haven", - "fixed_pair_not_supported": "Ta stała para nie jest obsługiwana na wybranych giełdach", + "fixed_pair_not_supported": "Ta stała para nie jest obsługiwana z wybranymi usługami swap", "fixed_rate": "Stała stawka", "fixed_rate_alert": "Będziesz mógł wprowadzić kwotę do otrzymania, gdy wybrany bedzie tryb stałego przeliczenia. Czy chcesz przejść do trybu stałej stawki?", "forgot_password": "Zapomniałem hasła", @@ -354,6 +365,20 @@ "ledger_error_wrong_app": "Upewnij się, że opisz odpowiednią aplikację na swojej księdze", "ledger_please_enable_bluetooth": "Włącz Bluetooth wykrywanie księgi", "light_theme": "Jasny", + "litecoin_enable_mweb_sync": "Włącz skanowanie MWEB", + "litecoin_mweb": "MWEB", + "litecoin_mweb_always_scan": "Ustaw MWEB zawsze skanowanie", + "litecoin_mweb_description": "MWEB to nowy protokół, który przynosi szybciej, tańsze i bardziej prywatne transakcje do Litecoin", + "litecoin_mweb_dismiss": "Odrzucać", + "litecoin_mweb_display_card": "Pokaż kartę MWEB", + "litecoin_mweb_enable_later": "Możesz ponownie włączyć MWEB w ustawieniach wyświetlania.", + "litecoin_mweb_pegin": "Kołek", + "litecoin_mweb_pegout": "Palikować", + "litecoin_mweb_scanning": "Skanowanie MWEB", + "litecoin_mweb_settings": "Ustawienia MWEB", + "litecoin_mweb_warning": "Korzystanie z MWEB początkowo pobiera ~ 600 MB danych i może potrwać do 30 minut w zależności od prędkości sieci. Te początkowe dane pobierają tylko raz i będą dostępne dla wszystkich portfeli Litecoin", + "litecoin_what_is_mweb": "Co to jest MWEB?", + "live_fee_rates": "Stawki opłaty na żywo za pośrednictwem API", "load_more": "Załaduj więcej", "loading_your_wallet": "Ładowanie portfela", "login": "Login", @@ -383,6 +408,8 @@ "monero_light_theme": "Lekki motyw Monero", "moonpay_alert_text": "Wartość kwoty musi być większa lub równa ${minAmount} ${fiatCurrency}", "more_options": "Więcej opcji", + "mweb_confirmed": "Potwierdził MWEB", + "mweb_unconfirmed": "Niepotwierdzone MWEB", "name": "Nazwa", "nano_current_rep": "Obecny przedstawiciel", "nano_gpt_thanks_message": "Dzięki za użycie Nanogpt! Pamiętaj, aby wrócić do przeglądarki po zakończeniu transakcji!", @@ -442,6 +469,7 @@ "overwrite_amount": "Nadpisz ilość", "pairingInvalidEvent": "Nieprawidłowe zdarzenie parowania", "passphrase": "PassPhraza (opcjonalnie)", + "passphrases_doesnt_match": "Passfrazy nie pasują, spróbuj ponownie", "password": "Hasło", "paste": "Wklej", "pause_wallet_creation": "Możliwość utworzenia Portfela Haven jest obecnie wstrzymana.", @@ -593,8 +621,11 @@ "seed_share": "Udostępnij seed", "seed_title": "Seed", "seedtype": "Sedtype", + "seedtype_alert_content": "Dzielenie się nasionami z innymi portfelami jest możliwe tylko z BIP39 sededType.", + "seedtype_alert_title": "Ustanowienie typu sedype", "seedtype_legacy": "Dziedzictwo (25 słów)", "seedtype_polyseed": "Poliqueed (16 słów)", + "seedtype_wownero": "Wowero (14 słów)", "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.", @@ -605,6 +636,7 @@ "send": "Wyślij", "send_address": "Adres ${cryptoCurrency}", "send_amount": "Ilość:", + "send_change_to_you": "Zmień do ciebie:", "send_creating_transaction": "Tworzenie transakcji", "send_error_currency": "Waluta może zawierać tylko cyfry", "send_error_minimum_value": "Minimalna wartość to 0,01", @@ -658,6 +690,7 @@ "setup_your_debit_card": "Skonfiguruj swoją kartę debetową", "share": "Udział", "share_address": "Udostępnij adres", + "shared_seed_wallet_groups": "Wspólne grupy portfeli nasion", "show_details": "Pokaż szczegóły", "show_keys": "Pokaż seed/klucze", "show_market_place": "Pokaż rynek", @@ -671,6 +704,7 @@ "signature_invalid_error": "Podpis nie jest ważny dla podanej wiadomości", "signTransaction": "Podpisz transakcję", "signup_for_card_accept_terms": "Zarejestruj się, aby otrzymać kartę i zaakceptuj warunki.", + "silent_payment": "Cicha płatność", "silent_payments": "Ciche płatności", "silent_payments_always_scan": "Ustaw ciche płatności zawsze skanowanie", "silent_payments_disclaimer": "Nowe adresy nie są nową tożsamością. Jest to ponowne wykorzystanie istniejącej tożsamości z inną etykietą.", @@ -681,6 +715,7 @@ "silent_payments_scanned_tip": "Zeskanowany do napiwku! (${tip})", "silent_payments_scanning": "Skanowanie cichych płatności", "silent_payments_settings": "Ustawienia o cichej płatności", + "single_seed_wallets_group": "Pojedyncze portfele nasion", "slidable": "Przesuwne", "sort_by": "Sortuj według", "spend_key_private": "Klucz prywatny", @@ -703,12 +738,13 @@ "switchToEVMCompatibleWallet": "Przejdź na portfel zgodny z EVM i spróbuj ponownie (Ethereum, Polygon)", "symbol": "Symbol", "sync_all_wallets": "Synchronizuj wszystkie portfele", + "sync_status_attempting_scan": "Próba skanowania", "sync_status_attempting_sync": "PRÓBA SYNCHRONIZACJI", "sync_status_connected": "POŁĄCZONY", "sync_status_connecting": "ŁĄCZENIE", "sync_status_failed_connect": "POŁĄCZENIE NIEUDANE", "sync_status_not_connected": "NIE POŁĄCZONY", - "sync_status_starting_scan": "Rozpoczęcie skanowania", + "sync_status_starting_scan": "Rozpoczęcie skanowania (od ${height})", "sync_status_starting_sync": "ROZPOCZĘCIE SYNCHRONIZACJI", "sync_status_syncronized": "ZSYNCHRONIZOWANO", "sync_status_syncronizing": "SYNCHRONIZACJA", @@ -795,6 +831,7 @@ "trusted": "Zaufany", "tx_commit_exception_no_dust_on_change": "Transakcja jest odrzucana z tą kwotą. Za pomocą tych monet możesz wysłać ${min} bez zmiany lub ${max}, które zwraca zmianę.", "tx_commit_failed": "Zatwierdzenie transakcji nie powiodło się. Skontaktuj się z obsługą.", + "tx_commit_failed_no_peers": "Transakcja nie była transmitowana, spróbuj ponownie za około sekundę", "tx_invalid_input": "Używasz niewłaściwego typu wejściowego dla tego rodzaju płatności", "tx_no_dust_exception": "Transakcja jest odrzucana przez wysyłanie zbyt małej ilości. Spróbuj zwiększyć kwotę.", "tx_not_enough_inputs_exception": "Za mało dostępnych danych wejściowych. Wybierz więcej pod kontrolą monet", @@ -839,8 +876,16 @@ "view_transaction_on": "Zobacz transakcje na ", "voting_weight": "Waga głosu", "waitFewSecondForTxUpdate": "Poczekaj kilka sekund, aż transakcja zostanie odzwierciedlona w historii transakcji", + "wallet_group": "Grupa portfela", + "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_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_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", "wallet_list_create_new_wallet": "Utwórz nowy portfel", + "wallet_list_edit_group_name": "Edytuj nazwę grupy", "wallet_list_edit_wallet": "Edytuj portfel", "wallet_list_failed_to_load": "Nie udało się załadować ${wallet_name} portfel. ${error}", "wallet_list_failed_to_remove": "Nie udało się usunąć ${wallet_name} portfel. ${error}", diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index bd6d9b506..524dbcace 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -36,6 +36,7 @@ "agree": "Concordo", "agree_and_continue": "Concordar e continuar", "agree_to": "Ao criar conta você concorda com ", + "alert_notice": "Perceber", "all": "TUDO", "all_trades": "Todas as negociações", "all_transactions": "Todas as transacções", @@ -113,7 +114,7 @@ "change_currency": "Alterar moeda", "change_current_node": "Você realmente deseja alterar o nó atual para ${node}?", "change_current_node_title": "Mudar o nó atual", - "change_exchange_provider": "Alterar o provedor de troca", + "change_exchange_provider": "Provedor de troca de alteração", "change_language": "Mudar idioma", "change_language_to": "Alterar idioma para ${language}?", "change_password": "Mudar senha", @@ -130,6 +131,7 @@ "choose_one": "Escolha um", "choose_relay": "Escolha um relé para usar", "choose_wallet_currency": "Escolha a moeda da carteira:", + "choose_wallet_group": "Escolha o Grupo de Wallet", "clear": "Limpar", "clearnet_link": "link clear net", "close": "Fechar", @@ -142,8 +144,9 @@ "confirm_delete_wallet": "Esta ação excluirá esta carteira. Você deseja continuar?", "confirm_fee_deduction": "Confirme dedução da taxa", "confirm_fee_deduction_content": "Você concorda em deduzir a taxa da saída?", + "confirm_passphrase": "Confirme a senha", "confirm_sending": "Confirmar o envio", - "confirm_silent_payments_switch_node": "Seu nó atual não suporta pagamentos silenciosos \\ Ncake Wallet mudará para um nó compatível, apenas para digitalização", + "confirm_silent_payments_switch_node": "Seu nó atual não suporta pagamentos silenciosos \n A Cake Wallet mudará para um nó compatível, apenas para escanear", "confirmations": "Confirmações", "confirmed": "Saldo Confirmado", "confirmed_tx": "Confirmado", @@ -160,6 +163,7 @@ "contact_name": "Nome do contato", "contact_support": "Contatar Suporte", "continue_text": "Continuar", + "contract_warning": "Este endereço do contrato foi sinalizado como potencialmente fraudulento. Por favor, processe com cautela.", "contractName": "Nome do contrato", "contractSymbol": "Símbolo do Contrato", "copied_key_to_clipboard": "${key} copiada para a área de transferência", @@ -175,6 +179,7 @@ "create_invoice": "Criar recibo", "create_new": "Criar nova carteira", "create_new_account": "Criar nova conta", + "create_new_seed": "Crie nova semente", "creating_new_wallet": "Criando nova carteira", "creating_new_wallet_error": "Erro: ${description}", "creation_date": "Data de criação", @@ -195,6 +200,7 @@ "delete_wallet_confirm_message": "Tem certeza de que deseja excluir a carteira ${wallet_name}?", "deleteConnectionConfirmationPrompt": "Tem certeza de que deseja excluir a conexão com", "denominations": "Denominações", + "derivationpath": "Caminho de derivação", "descending": "descendente", "description": "Descrição", "destination_tag": "Tag de destino:", @@ -207,6 +213,7 @@ "disable_buy": "Desativar ação de compra", "disable_cake_2fa": "Desabilitar o Cake 2FA", "disable_exchange": "Desativar troca", + "disable_fee_api_warning": "Ao desativar isso, as taxas de taxas podem ser imprecisas em alguns casos, para que você possa acabar pagando demais ou pagando as taxas por suas transações", "disable_fiat": "Desativar fiat", "disable_sell": "Desativar ação de venda", "disableBatteryOptimization": "Desative a otimização da bateria", @@ -228,8 +235,10 @@ "edit_token": "Editar símbolo", "electrum_address_disclaimer": "Geramos novos endereços cada vez que você usa um, mas os endereços anteriores continuam funcionando", "email_address": "Endereço de e-mail", + "enable": "Habilitar", + "enable_mempool_api": "Mempool API para taxas e datas precisas", "enable_replace_by_fee": "Habilite substituir por taxa", - "enable_silent_payments_scanning": "Ativar escaneamento de pagamentos silenciosos", + "enable_silent_payments_scanning": "Comece a escanear pagamentos silenciosos, até que o topo seja alcançada", "enabled": "Habilitado", "enter_amount": "Digite o valor", "enter_backup_password": "Digite a senha de backup aqui", @@ -273,7 +282,7 @@ "event": "Evento", "events": "Eventos", "exchange": "Trocar", - "exchange_incorrect_current_wallet_for_xmr": "Se você deseja trocar o XMR de seu saldo da Carteira Monero Cake, troque primeiro para sua carteira Monero.", + "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!", "exchange_result_confirm": "Ao confirmar, você enviará ${fetchingLabel} ${from} da sua carteira ${walletName} para o endereço mostrado abaixo. Ou você pode enviar de sua carteira externa para o endereço abaixo/código QR acima.\n\nPressione Confirmar para continuar ou volte para alterar os valores.", @@ -291,6 +300,8 @@ "failed_authentication": "Falha na autenticação. ${state_error}", "faq": "FAQ", "features": "Funcionalidades", + "fee_less_than_min": "A taxa selecionada é menor que o mínimo, aumente as taxas para poder enviar a transação", + "fee_rate": "Taxa de transação", "fetching": "Buscando", "fiat_api": "API da Fiat", "fiat_balance": "Equilíbrio Fiat", @@ -298,7 +309,7 @@ "fill_code": "Por favor, preencha o código de verificação fornecido ao seu e-mail", "filter_by": "Filtrar por", "first_wallet_text": "Carteira incrível para Monero, Bitcoin, Ethereum, Litecoin, e Haven", - "fixed_pair_not_supported": "Este par fixo não é compatível com as exchanges selecionadas", + "fixed_pair_not_supported": "Este par fixo não é suportado com os serviços de troca selecionados", "fixed_rate": "Taxa fixa", "fixed_rate_alert": "Você poderá inserir a quantia recebida quando o modo de taxa fixa estiver marcado. Quer mudar para o modo de taxa fixa?", "forgot_password": "Esqueci a senha", @@ -354,6 +365,20 @@ "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", "light_theme": "Luz", + "litecoin_enable_mweb_sync": "Ativar digitalização do MWEB", + "litecoin_mweb": "Mweb", + "litecoin_mweb_always_scan": "Definir mweb sempre digitalizando", + "litecoin_mweb_description": "MWEB é um novo protocolo que traz transações mais rápidas, baratas e mais privadas para o Litecoin", + "litecoin_mweb_dismiss": "Liberar", + "litecoin_mweb_display_card": "Mostre o cartão MWEB", + "litecoin_mweb_enable_later": "Você pode optar por ativar o MWEB novamente em Configurações de exibição.", + "litecoin_mweb_pegin": "Peg in", + "litecoin_mweb_pegout": "Peg fora", + "litecoin_mweb_scanning": "MWEB Scanning", + "litecoin_mweb_settings": "Configurações do MWEB", + "litecoin_mweb_warning": "O uso do MWEB baixará inicialmente ~ 600 MB de dados e pode levar até 30 minutos, dependendo da velocidade da rede. Esses dados iniciais serão baixados apenas uma vez e estarão disponíveis para todas as carteiras Litecoin", + "litecoin_what_is_mweb": "O que é MWeb?", + "live_fee_rates": "Taxas de taxas ao vivo via API", "load_more": "Carregue mais", "loading_your_wallet": "Abrindo sua carteira", "login": "Login", @@ -384,6 +409,8 @@ "monero_light_theme": "Monero Light Theme", "moonpay_alert_text": "O valor do montante deve ser maior ou igual a ${minAmount} ${fiatCurrency}", "more_options": "Mais opções", + "mweb_confirmed": "MWEB confirmado", + "mweb_unconfirmed": "MWEB não confirmado", "name": "Nome", "nano_current_rep": "Representante atual", "nano_gpt_thanks_message": "Obrigado por usar o Nanogpt! Lembre -se de voltar para o navegador após a conclusão da transação!", @@ -413,7 +440,7 @@ "node_test": "Teste", "nodes": "Nós", "nodes_list_reset_to_default_message": "Você realmente deseja redefinir as configurações para o padrão?", - "none_of_selected_providers_can_exchange": "Nenhum dos provedores selecionados pode fazer esta troca", + "none_of_selected_providers_can_exchange": "Nenhum dos fornecedores selecionados pode fazer essa troca", "noNFTYet": "Ainda não há NFT", "normal": "Normal", "note_optional": "Nota (opcional)", @@ -444,6 +471,7 @@ "overwrite_amount": "Overwrite amount", "pairingInvalidEvent": "Emparelhamento de evento inválido", "passphrase": "Senha (opcional)", + "passphrases_doesnt_match": "Passagases não correspondem, por favor tente novamente", "password": "Senha", "paste": "Colar", "pause_wallet_creation": "A capacidade de criar a Haven Wallet está atualmente pausada.", @@ -595,8 +623,11 @@ "seed_share": "Compartilhar semente", "seed_title": "Semente", "seedtype": "SeedType", + "seedtype_alert_content": "Compartilhar sementes com outras carteiras só é possível com o BIP39 SeedType.", + "seedtype_alert_title": "Alerta de SeedType", "seedtype_legacy": "Legado (25 palavras)", "seedtype_polyseed": "Polyseed (16 palavras)", + "seedtype_wownero": "Wowrone (14 palavras)", "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.", @@ -607,6 +638,7 @@ "send": "Enviar", "send_address": "Endereço ${cryptoCurrency}", "send_amount": "Montante:", + "send_change_to_you": "Troco, para você:", "send_creating_transaction": "Criando transação", "send_error_currency": "A moeda só pode conter números", "send_error_minimum_value": "O valor mínimo da quantia é 0,01", @@ -660,6 +692,7 @@ "setup_your_debit_card": "Configure seu cartão de débito", "share": "Compartilhar", "share_address": "Compartilhar endereço", + "shared_seed_wallet_groups": "Grupos de carteira de sementes compartilhados", "show_details": "Mostrar detalhes", "show_keys": "Mostrar semente/chaves", "show_market_place": "Mostrar mercado", @@ -673,8 +706,9 @@ "signature_invalid_error": "A assinatura não é válida para a mensagem dada", "signTransaction": "Assinar transação", "signup_for_card_accept_terms": "Cadastre-se no cartão e aceite os termos.", + "silent_payment": "Pagamento silencioso", "silent_payments": "Pagamentos silenciosos", - "silent_payments_always_scan": "Defina pagamentos silenciosos sempre digitalizando", + "silent_payments_always_scan": "Defina pagamentos silenciosos sempre escaneando", "silent_payments_disclaimer": "Novos endereços não são novas identidades. É uma reutilização de uma identidade existente com um rótulo diferente.", "silent_payments_display_card": "Mostrar cartão de pagamento silencioso", "silent_payments_scan_from_date": "Escanear a partir da data", @@ -683,6 +717,7 @@ "silent_payments_scanned_tip": "Escaneado até o fim! (${tip})", "silent_payments_scanning": "Escanear Pagamentos Silenciosos", "silent_payments_settings": "Configurações de pagamentos silenciosos", + "single_seed_wallets_group": "Carteiras de sementes únicas", "slidable": "Deslizável", "sort_by": "Ordenar por", "spend_key_private": "Chave de gastos (privada)", @@ -705,12 +740,13 @@ "switchToEVMCompatibleWallet": "Mude para uma carteira compatível com EVM e tente novamente (Ethereum, Polygon)", "symbol": "Símbolo", "sync_all_wallets": "Sincronize todas as carteiras", + "sync_status_attempting_scan": "TENTANDO ESCANEAR", "sync_status_attempting_sync": "TENTANDO SINCRONIZAR", "sync_status_connected": "CONECTADO", "sync_status_connecting": "CONECTANDO", "sync_status_failed_connect": "DESCONECTADO", "sync_status_not_connected": "DESCONECTADO", - "sync_status_starting_scan": "Diretor inicial", + "sync_status_starting_scan": "Começando scan (de ${height})", "sync_status_starting_sync": "INICIANDO SINCRONIZAÇÃO", "sync_status_syncronized": "SINCRONIZADO", "sync_status_syncronizing": "SINCRONIZANDO", @@ -797,6 +833,7 @@ "trusted": "confiável", "tx_commit_exception_no_dust_on_change": "A transação é rejeitada com esse valor. Com essas moedas, você pode enviar ${min} sem alteração ou ${max} que retorna alterações.", "tx_commit_failed": "A confirmação da transação falhou. Entre em contato com o suporte.", + "tx_commit_failed_no_peers": "A transação não foi transmitida, tente novamente em um segundo", "tx_invalid_input": "Você está usando o tipo de entrada errado para este tipo de pagamento", "tx_no_dust_exception": "A transação é rejeitada enviando uma quantia pequena demais. Por favor, tente aumentar o valor.", "tx_not_enough_inputs_exception": "Não há entradas disponíveis. Selecione mais sob controle de moedas", @@ -842,8 +879,16 @@ "voting_weight": "Peso de votação", "waitFewSecondForTxUpdate": "Aguarde alguns segundos para que a transação seja refletida no histórico de transações", "waiting_payment_confirmation": "Aguardando confirmação de pagamento", + "wallet_group": "Grupo de carteira", + "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_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_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", "wallet_list_create_new_wallet": "Criar nova carteira", + "wallet_list_edit_group_name": "Editar o nome do grupo", "wallet_list_edit_wallet": "Editar carteira", "wallet_list_failed_to_load": "Falha ao abrir a carteira ${wallet_name}. ${error}", "wallet_list_failed_to_remove": "Falha ao remover a carteira ${wallet_name}. ${error}", diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index d43572351..1a8c2447f 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -36,6 +36,7 @@ "agree": "согласен", "agree_and_continue": "Согласиться и продолжить", "agree_to": "Создавая аккаунт, вы соглашаетесь с ", + "alert_notice": "Уведомление", "all": "ВСЕ", "all_trades": "Все сделки", "all_transactions": "Все транзакции", @@ -113,7 +114,7 @@ "change_currency": "Изменить валюту", "change_current_node": "Вы уверены, что хотите изменить текущую ноду на ${node}?", "change_current_node_title": "Изменить текущую ноду", - "change_exchange_provider": "Изменить провайдера обмена", + "change_exchange_provider": "Изменить поставщика свопа", "change_language": "Изменить язык", "change_language_to": "Изменить язык на ${language}?", "change_password": "Изменить пароль", @@ -130,6 +131,7 @@ "choose_one": "Выбери один", "choose_relay": "Пожалуйста, выберите реле для использования", "choose_wallet_currency": "Пожалуйста, выберите валюту кошелька:", + "choose_wallet_group": "Выберите группу кошелька", "clear": "Очистить", "clearnet_link": "Клирнет ссылка", "close": "Закрывать", @@ -142,6 +144,7 @@ "confirm_delete_wallet": "Это действие удалит кошелек. Вы хотите продолжить?", "confirm_fee_deduction": "Подтвердите вычет платы", "confirm_fee_deduction_content": "Согласны ли вы вычесть плату из вывода?", + "confirm_passphrase": "Подтвердите Passfrase", "confirm_sending": "Подтвердить отправку", "confirm_silent_payments_switch_node": "Ваш текущий узел не поддерживает Silent Payments \\ ncake Wallet переключится на совместимый узел, только для сканирования", "confirmations": "Подтверждения", @@ -160,6 +163,7 @@ "contact_name": "Имя контакта", "contact_support": "Связаться со службой поддержки", "continue_text": "Продолжить", + "contract_warning": "Этот адрес контракта был отмечен как потенциально мошеннический. Пожалуйста, обработайтесь с осторожностью.", "contractName": "Название контракта", "contractSymbol": "Символ контракта", "copied_key_to_clipboard": "Скопировано ${key} в буфер обмена", @@ -175,6 +179,7 @@ "create_invoice": "Создать счет", "create_new": "Создать новый кошелёк", "create_new_account": "Создать новый аккаунт", + "create_new_seed": "Создать новое семя", "creating_new_wallet": "Создание нового кошелька", "creating_new_wallet_error": "Ошибка: ${description}", "creation_date": "Дата создания", @@ -195,6 +200,7 @@ "delete_wallet_confirm_message": "Вы уверены, что хотите удалить кошелек ${wallet_name}?", "deleteConnectionConfirmationPrompt": "Вы уверены, что хотите удалить подключение к", "denominations": "Деноминации", + "derivationpath": "Путь получения", "descending": "Нисходящий", "description": "Описание", "destination_tag": "Целевой тег:", @@ -207,6 +213,7 @@ "disable_buy": "Отключить действие покупки", "disable_cake_2fa": "Отключить торт 2FA", "disable_exchange": "Отключить обмен", + "disable_fee_api_warning": "Выключив это, в некоторых случаях ставки платы могут быть неточными, так что вы можете в конечном итоге переплачивать или недоплачивать сборы за ваши транзакции", "disable_fiat": "Отключить фиат", "disable_sell": "Отключить действие продажи", "disableBatteryOptimization": "Отключить оптимизацию батареи", @@ -228,8 +235,10 @@ "edit_token": "Изменить токен", "electrum_address_disclaimer": "Мы генерируем новые адреса каждый раз, когда вы их используете, но предыдущие адреса продолжают работать.", "email_address": "Адрес электронной почты", + "enable": "Давать возможность", + "enable_mempool_api": "Mempool API за точные сборы и даты", "enable_replace_by_fee": "Включить замену за пикой", - "enable_silent_payments_scanning": "Включить сканирование безмолвных платежей", + "enable_silent_payments_scanning": "Начните сканировать безмолвные платежи, пока не будет достигнут наконечник", "enabled": "Включено", "enter_amount": "Введите сумму", "enter_backup_password": "Введите пароль резервной копии", @@ -272,8 +281,8 @@ "etherscan_history": "История Эфириума", "event": "Событие", "events": "События", - "exchange": "Обмен", - "exchange_incorrect_current_wallet_for_xmr": "Если вы хотите обменять XMR со своего баланса Monero в Cake Wallet, сначала переключитесь на свой кошелек Monero.", + "exchange": "Менять", + "exchange_incorrect_current_wallet_for_xmr": "Если вы хотите поменять XMR с баланса с кошельком для торта Monero, сначала переключитесь на свой кошелек Monero.", "exchange_new_template": "Новый шаблон", "exchange_provider_unsupported": "${providerName} больше не поддерживается!", "exchange_result_confirm": "Нажимая подтвердить, вы отправите ${fetchingLabel} ${from} с вашего кошелька ${walletName} на адрес указанный ниже. Или вы можете отправить со своего внешнего кошелька на нижеуказанный адрес/QR-код.\n\nПожалуйста, нажмите подтвердить для продолжения, или вернитесь назад для изменения суммы.", @@ -291,6 +300,8 @@ "failed_authentication": "Ошибка аутентификации. ${state_error}", "faq": "FAQ", "features": "Функции", + "fee_less_than_min": "Выбранный сбор меньше, чем минимум, пожалуйста, увеличьте сборы, чтобы быть в состоянии отправить транзакцию", + "fee_rate": "Плата", "fetching": "Загрузка", "fiat_api": "Фиат API", "fiat_balance": "Фиатный баланс", @@ -298,7 +309,7 @@ "fill_code": "Пожалуйста, введите код подтверждения, отправленный на вашу электронную почту", "filter_by": "Фильтровать по", "first_wallet_text": "В самом удобном кошельке для Monero, Bitcoin, Ethereum, Litecoin, и Haven", - "fixed_pair_not_supported": "Эта фиксированная пара не поддерживается выбранными биржами.", + "fixed_pair_not_supported": "Эта фиксированная пара не поддерживается выбранными службами свопа", "fixed_rate": "Фиксированная ставка", "fixed_rate_alert": "Вы сможете ввести сумму получения тогда, когда будет установлен режим фиксированной ставки. Вы хотите перейти в режим фиксированной ставки?", "forgot_password": "Забыли пароль", @@ -354,6 +365,20 @@ "ledger_error_wrong_app": "Пожалуйста, убедитесь, что вы предлагаете правильное приложение в своей бухгалтерской книге", "ledger_please_enable_bluetooth": "Пожалуйста, включите Bluetooth обнаружить вашу бухгалтерскую книгу", "light_theme": "Светлая", + "litecoin_enable_mweb_sync": "Включить MWEB сканирование", + "litecoin_mweb": "Мвеб", + "litecoin_mweb_always_scan": "Установить MWEB всегда сканирование", + "litecoin_mweb_description": "MWEB - это новый протокол, который приносит быстрее, дешевле и более частные транзакции в Litecoin", + "litecoin_mweb_dismiss": "Увольнять", + "litecoin_mweb_display_card": "Показать карту MWEB", + "litecoin_mweb_enable_later": "Вы можете снова включить MWEB в настройках отображения.", + "litecoin_mweb_pegin": "Внедрять", + "litecoin_mweb_pegout": "Выкрикивать", + "litecoin_mweb_scanning": "MWEB сканирование", + "litecoin_mweb_settings": "Настройки MWEB", + "litecoin_mweb_warning": "Использование MWEB изначально загрузит ~ 600 МБ данных и может занять до 30 минут в зависимости от скорости сети. Эти начальные данные будут загружаться только один раз и будут доступны для всех кошельков Litecoin", + "litecoin_what_is_mweb": "Что такое MWEB?", + "live_fee_rates": "Ставки по сбору вживую через API", "load_more": "Загрузи больше", "loading_your_wallet": "Загрузка кошелька", "login": "Логин", @@ -383,6 +408,8 @@ "monero_light_theme": "Светлая тема Monero", "moonpay_alert_text": "Сумма должна быть больше или равна ${minAmount} ${fiatCurrency}", "more_options": "Дополнительные параметры", + "mweb_confirmed": "Подтверждено MWEB", + "mweb_unconfirmed": "Неподтвержденная MWEB", "name": "Имя", "nano_current_rep": "Нынешний представитель", "nano_gpt_thanks_message": "Спасибо за использование Nanogpt! Не забудьте вернуться в браузер после завершения транзакции!", @@ -412,7 +439,7 @@ "node_test": "Тест", "nodes": "Ноды", "nodes_list_reset_to_default_message": "Вы уверены, что хотите сбросить настройки до значений по умолчанию?", - "none_of_selected_providers_can_exchange": "Ни один из выбранных провайдеров не может совершить этот обмен", + "none_of_selected_providers_can_exchange": "Ни один из выбранных провайдеров не может сделать это обмен", "noNFTYet": "NFT пока нет", "normal": "Нормальный", "note_optional": "Примечание (необязательно)", @@ -443,6 +470,7 @@ "overwrite_amount": "Overwrite amount", "pairingInvalidEvent": "Недействительное событие сопряжения", "passphrase": "Passfrase (необязательно)", + "passphrases_doesnt_match": "Пасфразы не совпадают, попробуйте еще раз", "password": "Пароль", "paste": "Вставить", "pause_wallet_creation": "Возможность создания Haven Wallet в настоящее время приостановлена.", @@ -594,8 +622,11 @@ "seed_share": "Поделиться мнемонической фразой", "seed_title": "Мнемоническая фраза", "seedtype": "SEEDTYPE", + "seedtype_alert_content": "Обмен семенами с другими кошельками возможно только с BIP39 SeedType.", + "seedtype_alert_title": "SEEDTYPE ALERT", "seedtype_legacy": "Наследие (25 слов)", "seedtype_polyseed": "Полиса (16 слов)", + "seedtype_wownero": "Wownero (14 слов)", "select_backup_file": "Выберите файл резервной копии", "select_buy_provider_notice": "Выберите поставщика покупки выше. Вы можете пропустить этот экран, установив поставщика покупки по умолчанию в настройках приложения.", "select_destination": "Пожалуйста, выберите место для файла резервной копии.", @@ -606,6 +637,7 @@ "send": "Отправить", "send_address": "${cryptoCurrency} адрес", "send_amount": "Сумма:", + "send_change_to_you": "Изменить, для вас:", "send_creating_transaction": "Создать транзакцию", "send_error_currency": "Валюта может содержать только цифры", "send_error_minimum_value": "Mинимальная сумма 0.01", @@ -659,6 +691,7 @@ "setup_your_debit_card": "Настройте свою дебетовую карту", "share": "Делиться", "share_address": "Поделиться адресом", + "shared_seed_wallet_groups": "Общие группы кошелька семян", "show_details": "Показать детали", "show_keys": "Показать мнемоническую фразу/ключи", "show_market_place": "Показать торговую площадку", @@ -672,6 +705,7 @@ "signature_invalid_error": "Подпись недопустима для данного сообщения", "signTransaction": "Подписать транзакцию", "signup_for_card_accept_terms": "Подпишитесь на карту и примите условия.", + "silent_payment": "Молчаливый платеж", "silent_payments": "Молчаливые платежи", "silent_payments_always_scan": "Установить молчаливые платежи всегда сканирование", "silent_payments_disclaimer": "Новые адреса не являются новыми личностями. Это повторное использование существующей идентичности с другой этикеткой.", @@ -682,6 +716,7 @@ "silent_payments_scanned_tip": "Оканируется, чтобы чаевые! (${tip})", "silent_payments_scanning": "Сканирование безмолвных платежей", "silent_payments_settings": "Silent Payments Settings", + "single_seed_wallets_group": "Одиночные кошельки", "slidable": "Скользящий", "sort_by": "Сортировать по", "spend_key_private": "Приватный ключ траты", @@ -704,12 +739,13 @@ "switchToEVMCompatibleWallet": "Пожалуйста, переключитесь на кошелек, совместимый с EVM, и повторите попытку (Ethereum, Polygon).", "symbol": "Символ", "sync_all_wallets": "Синхронизировать все кошельки", + "sync_status_attempting_scan": "Попытка сканирования", "sync_status_attempting_sync": "ПОПЫТКА СИНХРОНИЗАЦИИ", "sync_status_connected": "ПОДКЛЮЧЕНО", "sync_status_connecting": "ПОДКЛЮЧЕНИЕ", "sync_status_failed_connect": "ОТКЛЮЧЕНО", "sync_status_not_connected": "НЕ ПОДКЛЮЧЁН", - "sync_status_starting_scan": "Начальное сканирование", + "sync_status_starting_scan": "Начальное сканирование (от ${height})", "sync_status_starting_sync": "НАЧАЛО СИНХРОНИЗАЦИИ", "sync_status_syncronized": "СИНХРОНИЗИРОВАН", "sync_status_syncronizing": "СИНХРОНИЗАЦИЯ", @@ -796,6 +832,7 @@ "trusted": "доверенный", "tx_commit_exception_no_dust_on_change": "Транзакция отклоняется с этой суммой. С этими монетами вы можете отправлять ${min} без изменения или ${max}, которые возвращают изменение.", "tx_commit_failed": "Комплект транзакции не удался. Пожалуйста, свяжитесь с поддержкой.", + "tx_commit_failed_no_peers": "Транзакция не смогла передать, попробуйте еще раз через секунду или около того", "tx_invalid_input": "Вы используете неправильный тип ввода для этого типа оплаты", "tx_no_dust_exception": "Транзакция отклоняется путем отправки слишком маленькой суммы. Пожалуйста, попробуйте увеличить сумму.", "tx_not_enough_inputs_exception": "Недостаточно входов доступны. Пожалуйста, выберите больше под контролем монет", @@ -840,8 +877,16 @@ "view_transaction_on": "View Transaction on ", "voting_weight": "Вес голоса", "waitFewSecondForTxUpdate": "Пожалуйста, подождите несколько секунд, чтобы транзакция отразилась в истории транзакций.", + "wallet_group": "Группа кошелька", + "wallet_group_description_four": "создать кошелек с совершенно новым семенем.", + "wallet_group_description_one": "В кошельке для торта вы можете создать", + "wallet_group_description_three": "Чтобы увидеть доступные кошельки и/или экраны групп кошельков. Или выберите", + "wallet_group_description_two": "выбирая существующий кошелек, чтобы поделиться семенами. Каждая группа кошелька может содержать один кошелек каждого типа валюты. \n\n Вы можете выбрать", + "wallet_group_empty_state_text_one": "Похоже, у вас нет никаких совместимых групп кошелька !\n\n tap", + "wallet_group_empty_state_text_two": "ниже, чтобы сделать новый.", "wallet_keys": "Мнемоническая фраза/ключи кошелька", "wallet_list_create_new_wallet": "Создать новый кошелёк", + "wallet_list_edit_group_name": "Редактировать название группы", "wallet_list_edit_wallet": "Изменить кошелек", "wallet_list_failed_to_load": "Ошибка при загрузке ${wallet_name} кошелька. ${error}", "wallet_list_failed_to_remove": "Ошибка при удалении ${wallet_name} кошелька. ${error}", diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index d948ba6d6..213f74530 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -36,6 +36,7 @@ "agree": "ยอมรับ", "agree_and_continue": "ยอมรับและดำเนินการต่อ", "agree_to": "การสร้างบัญชีของคุณยอมรับเงื่อนไขของ", + "alert_notice": "สังเกต", "all": "ทั้งหมด", "all_trades": "การซื้อขายทั้งหมด", "all_transactions": "การทำธุรกรรมทั้งหมด", @@ -130,6 +131,7 @@ "choose_one": "เลือกหนึ่งรายการ", "choose_relay": "กรุณาเลือกรีเลย์ที่จะใช้", "choose_wallet_currency": "โปรดเลือกสกุลเงินของกระเป๋า:", + "choose_wallet_group": "เลือกกลุ่มกระเป๋าเงิน", "clear": "ล้าง", "clearnet_link": "ลิงค์เคลียร์เน็ต", "close": "ปิด", @@ -142,6 +144,7 @@ "confirm_delete_wallet": "การดำเนินการนี้จะลบกระเป๋านี้ คุณต้องการดำเนินการต่อหรือไม่?", "confirm_fee_deduction": "ยืนยันการหักค่าธรรมเนียม", "confirm_fee_deduction_content": "คุณตกลงที่จะหักค่าธรรมเนียมจากผลลัพธ์หรือไม่?", + "confirm_passphrase": "ยืนยันวลีรหัสผ่าน", "confirm_sending": "ยืนยันการส่ง", "confirm_silent_payments_switch_node": "โหนดปัจจุบันของคุณไม่รองรับการชำระเงินแบบเงียบ \\ ncake กระเป๋าเงินจะเปลี่ยนเป็นโหนดที่เข้ากันได้เพียงเพื่อการสแกน", "confirmations": "การยืนยัน", @@ -160,6 +163,7 @@ "contact_name": "ชื่อผู้ติดต่อ", "contact_support": "ติดต่อฝ่ายสนับสนุน", "continue_text": "ดำเนินการต่อ", + "contract_warning": "ที่อยู่สัญญานี้ได้รับการตั้งค่าสถานะว่าเป็นการฉ้อโกง กรุณาดำเนินการด้วยความระมัดระวัง", "contractName": "ชื่อสัญญา", "contractSymbol": "สัญลักษณ์สัญญา", "copied_key_to_clipboard": "คัดลอก ${key} ไปยัง Clipboard แล้ว", @@ -175,6 +179,7 @@ "create_invoice": "สร้างใบแจ้งหนี้", "create_new": "สร้างกระเป๋าใหม่", "create_new_account": "สร้างบัญชีใหม่", + "create_new_seed": "สร้างเมล็ดพันธุ์ใหม่", "creating_new_wallet": "กำลังสร้างกระเป๋าใหม่", "creating_new_wallet_error": "ข้อผิดพลาด: ${description}", "creation_date": "วันที่สร้าง", @@ -195,6 +200,7 @@ "delete_wallet_confirm_message": "คุณแน่ใจหรือว่าต้องการลบกระเป๋า${wallet_name}?", "deleteConnectionConfirmationPrompt": "คุณแน่ใจหรือไม่ว่าต้องการลบการเชื่อมต่อไปยัง", "denominations": "นิกาย", + "derivationpath": "เส้นทางที่ได้มา", "descending": "ลงมา", "description": "คำอธิบาย", "destination_tag": "แท็กปลายทาง:", @@ -207,6 +213,7 @@ "disable_buy": "ปิดการใช้งานการซื้อ", "disable_cake_2fa": "ปิดการใช้งานเค้ก 2FA", "disable_exchange": "ปิดใช้งานการแลกเปลี่ยน", + "disable_fee_api_warning": "โดยการปิดสิ่งนี้อัตราค่าธรรมเนียมอาจไม่ถูกต้องในบางกรณีดังนั้นคุณอาจจบลงด้วยการจ่ายเงินมากเกินไปหรือจ่ายค่าธรรมเนียมสำหรับการทำธุรกรรมของคุณมากเกินไป", "disable_fiat": "ปิดใช้งานสกุลเงินตรา", "disable_sell": "ปิดการใช้งานการขาย", "disableBatteryOptimization": "ปิดใช้งานการเพิ่มประสิทธิภาพแบตเตอรี่", @@ -228,8 +235,10 @@ "edit_token": "แก้ไขโทเค็น", "electrum_address_disclaimer": "เราสร้างที่อยู่ใหม่ทุกครั้งที่คุณใช้หนึ่งอย่าง แต่ที่อยู่เก่ายังสามารถใช้ได้ต่อไป", "email_address": "ที่อยู่อีเมล", + "enable": "เปิดใช้งาน", + "enable_mempool_api": "Mempool API สำหรับค่าธรรมเนียมและวันที่ที่ถูกต้อง", "enable_replace_by_fee": "เปิดใช้งานการเปลี่ยนโดยค่าธรรมเนียม", - "enable_silent_payments_scanning": "เปิดใช้งานการสแกนการชำระเงินแบบเงียบ", + "enable_silent_payments_scanning": "เริ่มสแกนการชำระเงินแบบเงียบจนกว่าจะถึงปลาย", "enabled": "เปิดใช้งาน", "enter_amount": "กรอกจำนวน", "enter_backup_password": "ป้อนรหัสผ่านสำรองที่นี่", @@ -273,7 +282,7 @@ "event": "เหตุการณ์", "events": "กิจกรรม", "exchange": "แลกเปลี่ยน", - "exchange_incorrect_current_wallet_for_xmr": "หากคุณต้องการแลกเปลี่ยน XMR จากยอดคงเหลือ Monero ใน Cake Wallet ของคุณ กรุณาเปลี่ยนเป็นกระเป๋า Monero ก่อน", + "exchange_incorrect_current_wallet_for_xmr": "หากคุณต้องการสลับ XMR จาก Cake Wallet Monero Balance โปรดเปลี่ยนไปใช้กระเป๋าเงิน Monero ก่อน", "exchange_new_template": "เทมเพลทใหม่", "exchange_provider_unsupported": "${providerName} ไม่ได้รับการสนับสนุนอีกต่อไป!", "exchange_result_confirm": "โดยกดปุ่มยืนยัน, คุณจะส่ง ${fetchingLabel} ${from} จากกระเป๋าของคุณที่เรียกว่า ${walletName} ไปยังที่อยู่ที่แสดงข้างล่าง หรือคุณสามารถส่งจากกระเป๋าภายนอกไปยังที่อยู่/รหัส QR ด้านล่าง\n\nโปรดกดปุ่มยืนยันเพื่อดำเนินการต่อหรือกลับไปเปลี่ยนจำนวน", @@ -291,6 +300,8 @@ "failed_authentication": "การยืนยันสิทธิ์ล้มเหลว ${state_error}", "faq": "คำถามที่พบบ่อย", "features": "คุณสมบัติ", + "fee_less_than_min": "ค่าธรรมเนียมที่เลือกน้อยกว่าขั้นต่ำโปรดเพิ่มค่าธรรมเนียมเพื่อให้สามารถส่งธุรกรรมได้", + "fee_rate": "อัตราค่าธรรมเนียม", "fetching": "กำลังโหลด", "fiat_api": "API สกุลเงินตรา", "fiat_balance": "เฟียต บาลานซ์", @@ -298,7 +309,7 @@ "fill_code": "โปรดกรอกรหัสยืนยันที่ส่งไปยังอีเมลของคุณ", "filter_by": "กรองตาม", "first_wallet_text": "กระเป๋าสตางค์ที่สวยงามสำหรับ Monero, Bitcoin, Ethereum, Litecoin และ Haven", - "fixed_pair_not_supported": "คู่ความสัมพันธ์ที่ถูกกำหนดไว้นี้ไม่สนับสนุนกับหุ้นที่เลือก", + "fixed_pair_not_supported": "คู่คงที่นี้ไม่ได้รับการสนับสนุนด้วยบริการแลกเปลี่ยนที่เลือก", "fixed_rate": "อัตราคงที่", "fixed_rate_alert": "คุณจะสามารถป้อนจำนวนที่ได้รับเมื่อเลือกโหมดอัตราคงที่ คุณต้องการสลับไปที่โหมดอัตราคงที่?", "forgot_password": "ลืมรหัสผ่าน", @@ -354,6 +365,20 @@ "ledger_error_wrong_app": "โปรดตรวจสอบให้แน่ใจว่าคุณเปิดแอพที่เหมาะสมในบัญชีแยกประเภทของคุณ", "ledger_please_enable_bluetooth": "โปรดเปิดใช้งานบลูทู ธ ในการตรวจจับบัญชีแยกประเภทของคุณ", "light_theme": "สว่าง", + "litecoin_enable_mweb_sync": "เปิดใช้งานการสแกน MWEB", + "litecoin_mweb": "mweb", + "litecoin_mweb_always_scan": "ตั้งค่าการสแกน MWEB เสมอ", + "litecoin_mweb_description": "MWEB เป็นโปรโตคอลใหม่ที่นำการทำธุรกรรมที่เร็วกว่าราคาถูกกว่าและเป็นส่วนตัวมากขึ้นไปยัง Litecoin", + "litecoin_mweb_dismiss": "อนุญาตให้ออกไป", + "litecoin_mweb_display_card": "แสดงการ์ด mweb", + "litecoin_mweb_enable_later": "คุณสามารถเลือกเปิดใช้งาน MWEB อีกครั้งภายใต้การตั้งค่าการแสดงผล", + "litecoin_mweb_pegin": "หมุด", + "litecoin_mweb_pegout": "ตรึง", + "litecoin_mweb_scanning": "การสแกน MWEB", + "litecoin_mweb_settings": "การตั้งค่า MWEB", + "litecoin_mweb_warning": "การใช้ MWEB จะดาวน์โหลดข้อมูล ~ 600MB ในขั้นต้นและอาจใช้เวลาสูงสุด 30 นาทีขึ้นอยู่กับความเร็วเครือข่าย ข้อมูลเริ่มต้นนี้จะดาวน์โหลดได้เพียงครั้งเดียวและพร้อมใช้งานสำหรับกระเป๋าเงินทั้งหมดของ Litecoin", + "litecoin_what_is_mweb": "MWEB คืออะไร?", + "live_fee_rates": "อัตราค่าธรรมเนียมสดผ่าน API", "load_more": "โหลดมากขึ้น", "loading_your_wallet": "กำลังโหลดกระเป๋าของคุณ", "login": "เข้าสู่ระบบ", @@ -383,6 +408,8 @@ "monero_light_theme": "ธีมแสง Monero", "moonpay_alert_text": "มูลค่าของจำนวนต้องมากกว่าหรือเท่ากับ ${minAmount} ${fiatCurrency}", "more_options": "ตัวเลือกเพิ่มเติม", + "mweb_confirmed": "MWEB ยืนยันแล้ว", + "mweb_unconfirmed": "mweb ที่ไม่ได้รับการยืนยัน", "name": "ชื่อ", "nano_current_rep": "ตัวแทนปัจจุบัน", "nano_gpt_thanks_message": "ขอบคุณที่ใช้ Nanogpt! อย่าลืมกลับไปที่เบราว์เซอร์หลังจากการทำธุรกรรมของคุณเสร็จสิ้น!", @@ -412,7 +439,7 @@ "node_test": "ทดสอบ", "nodes": "โหนด", "nodes_list_reset_to_default_message": "คุณแน่ใจหรือว่าต้องการรีเซ็ตการตั้งค่าเป็นค่าเริ่มต้น?", - "none_of_selected_providers_can_exchange": "ไม่มีผู้ให้บริการที่เลือกที่สามารถแลกเปลี่ยนนี้ได้", + "none_of_selected_providers_can_exchange": "ผู้ให้บริการที่เลือกไม่สามารถทำการแลกเปลี่ยนนี้ได้", "noNFTYet": "ยังไม่มี NFT", "normal": "ปกติ", "note_optional": "บันทึก (ไม่จำเป็น)", @@ -442,6 +469,7 @@ "overwrite_amount": "เขียนทับจำนวน", "pairingInvalidEvent": "การจับคู่เหตุการณ์ที่ไม่ถูกต้อง", "passphrase": "ข้อความรหัสผ่าน (ไม่บังคับ)", + "passphrases_doesnt_match": "Passphrases ไม่ตรงกันโปรดลองอีกครั้ง", "password": "รหัสผ่าน", "paste": "วาง", "pause_wallet_creation": "ขณะนี้ความสามารถในการสร้าง Haven Wallet ถูกหยุดชั่วคราว", @@ -593,8 +621,11 @@ "seed_share": "แบ่งปัน seed", "seed_title": "Seed", "seedtype": "เมล็ดพันธุ์", + "seedtype_alert_content": "การแบ่งปันเมล็ดกับกระเป๋าเงินอื่น ๆ เป็นไปได้เฉพาะกับ bip39 seedtype", + "seedtype_alert_title": "การแจ้งเตือน seedtype", "seedtype_legacy": "มรดก (25 คำ)", "seedtype_polyseed": "โพลีส (16 คำ)", + "seedtype_wownero": "wownero (14 คำ)", "select_backup_file": "เลือกไฟล์สำรอง", "select_buy_provider_notice": "เลือกผู้ให้บริการซื้อด้านบน คุณสามารถข้ามหน้าจอนี้ได้โดยการตั้งค่าผู้ให้บริการซื้อเริ่มต้นในการตั้งค่าแอป", "select_destination": "โปรดเลือกปลายทางสำหรับไฟล์สำรอง", @@ -605,6 +636,7 @@ "send": "ส่ง", "send_address": "ที่อยู่ ${cryptoCurrency}", "send_amount": "จำนวน:", + "send_change_to_you": "เปลี่ยนเป็นคุณ:", "send_creating_transaction": "กำลังสร้างธุรกรรม", "send_error_currency": "สกุลเงินสามารถเป็นเลขเท่านั้น", "send_error_minimum_value": "จำนวนขั้นต่ำของจำนวนเงินคือ 0.01", @@ -658,6 +690,7 @@ "setup_your_debit_card": "ตั้งค่าบัตรเดบิตของคุณ", "share": "แบ่งปัน", "share_address": "แชร์ที่อยู่", + "shared_seed_wallet_groups": "กลุ่มกระเป๋าเงินที่ใช้ร่วมกัน", "show_details": "แสดงรายละเอียด", "show_keys": "แสดงซีด/คีย์", "show_market_place": "แสดงตลาดกลาง", @@ -671,6 +704,7 @@ "signature_invalid_error": "ลายเซ็นไม่ถูกต้องสำหรับข้อความที่ให้ไว้", "signTransaction": "ลงนามในการทำธุรกรรม", "signup_for_card_accept_terms": "ลงทะเบียนสำหรับบัตรและยอมรับเงื่อนไข", + "silent_payment": "การชำระเงินแบบเงียบ", "silent_payments": "การชำระเงินเงียบ", "silent_payments_always_scan": "ตั้งค่าการชำระเงินแบบเงียบเสมอ", "silent_payments_disclaimer": "ที่อยู่ใหม่ไม่ใช่ตัวตนใหม่ มันเป็นการใช้ซ้ำของตัวตนที่มีอยู่ด้วยฉลากที่แตกต่างกัน", @@ -681,6 +715,7 @@ "silent_payments_scanned_tip": "สแกนไปที่ปลาย! (${tip})", "silent_payments_scanning": "การสแกนการชำระเงินแบบเงียบ", "silent_payments_settings": "การตั้งค่าการชำระเงินแบบเงียบ", + "single_seed_wallets_group": "กระเป๋าเงินเดียว", "slidable": "เลื่อนได้", "sort_by": "เรียงตาม", "spend_key_private": "คีย์จ่าย (ส่วนตัว)", @@ -703,12 +738,13 @@ "switchToEVMCompatibleWallet": "โปรดเปลี่ยนไปใช้กระเป๋าเงินที่รองรับ EVM แล้วลองอีกครั้ง (Ethereum, Polygon)", "symbol": "เครื่องหมาย", "sync_all_wallets": "ซิงค์กระเป๋าเงินทั้งหมด", + "sync_status_attempting_scan": "พยายามสแกน", "sync_status_attempting_sync": "พยายามซิงโครไนซ์", "sync_status_connected": "เชื่อมต่อแล้ว", "sync_status_connecting": "กำลังเชื่อมต่อ", "sync_status_failed_connect": "การเชื่อมต่อล้มเหลว", "sync_status_not_connected": "ไม่ได้เชื่อมต่อ", - "sync_status_starting_scan": "เริ่มการสแกน", + "sync_status_starting_scan": "การสแกนเริ่มต้น (จาก ${height})", "sync_status_starting_sync": "กำลังเริ่มซิงโครไนซ์", "sync_status_syncronized": "ซิงโครไนซ์แล้ว", "sync_status_syncronizing": "กำลังซิงโครไนซ์", @@ -795,6 +831,7 @@ "trusted": "มั่นคง", "tx_commit_exception_no_dust_on_change": "ธุรกรรมถูกปฏิเสธด้วยจำนวนเงินนี้ ด้วยเหรียญเหล่านี้คุณสามารถส่ง ${min} โดยไม่ต้องเปลี่ยนแปลงหรือ ${max} ที่ส่งคืนการเปลี่ยนแปลง", "tx_commit_failed": "การทำธุรกรรมล้มเหลว กรุณาติดต่อฝ่ายสนับสนุน", + "tx_commit_failed_no_peers": "การทำธุรกรรมล้มเหลวในการออกอากาศโปรดลองอีกครั้งในวินาทีหรือมากกว่านั้น", "tx_invalid_input": "คุณกำลังใช้ประเภทอินพุตที่ไม่ถูกต้องสำหรับการชำระเงินประเภทนี้", "tx_no_dust_exception": "การทำธุรกรรมถูกปฏิเสธโดยการส่งจำนวนน้อยเกินไป โปรดลองเพิ่มจำนวนเงิน", "tx_not_enough_inputs_exception": "มีอินพุตไม่เพียงพอ โปรดเลือกเพิ่มเติมภายใต้การควบคุมเหรียญ", @@ -839,8 +876,16 @@ "view_transaction_on": "ดูการทำธุรกรรมบน ", "voting_weight": "น้ำหนักโหวต", "waitFewSecondForTxUpdate": "กรุณารอสักครู่เพื่อให้ธุรกรรมปรากฏในประวัติการทำธุรกรรม", + "wallet_group": "กลุ่มกระเป๋าเงิน", + "wallet_group_description_four": "เพื่อสร้างกระเป๋าเงินที่มีเมล็ดพันธุ์ใหม่ทั้งหมด", + "wallet_group_description_one": "ในกระเป๋าเงินเค้กคุณสามารถสร้างไฟล์", + "wallet_group_description_three": "หากต้องการดูกระเป๋าเงินและ/หรือกลุ่มกระเป๋าเงินที่มีอยู่ หรือเลือก", + "wallet_group_description_two": "โดยการเลือกกระเป๋าเงินที่มีอยู่เพื่อแบ่งปันเมล็ดด้วย แต่ละกลุ่มกระเป๋าเงินสามารถมีกระเป๋าเงินเดียวของแต่ละประเภทสกุลเงิน \n\n คุณสามารถเลือกได้", + "wallet_group_empty_state_text_one": "ดูเหมือนว่าคุณจะไม่มีกลุ่มกระเป๋าเงินที่เข้ากันได้ !\n\n แตะ", + "wallet_group_empty_state_text_two": "ด้านล่างเพื่อสร้างใหม่", "wallet_keys": "ซีดของกระเป๋า/คีย์", "wallet_list_create_new_wallet": "สร้างกระเป๋าใหม่", + "wallet_list_edit_group_name": "แก้ไขชื่อกลุ่ม", "wallet_list_edit_wallet": "แก้ไขกระเป๋าสตางค์", "wallet_list_failed_to_load": "ไม่สามารถโหลดกระเป๋า ${wallet_name} ได้ ${error}", "wallet_list_failed_to_remove": "ไม่สามารถลบกระเป๋า ${wallet_name} ได้ ${error}", diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index a0ab030c8..0ca8ee665 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -36,6 +36,7 @@ "agree": "Sumang-ayon", "agree_and_continue": "Sumang-ayon & Magpatuloy", "agree_to": "Sa pamamagitan ng paggawa ng account sumasang-ayon ka sa ", + "alert_notice": "PAUNAWA", "all": "LAHAT", "all_trades": "Lahat ng mga trade", "all_transactions": "Lahat ng mga transaksyon", @@ -113,7 +114,7 @@ "change_currency": "Baguhin ang pera", "change_current_node": "Sigurado ka bang baguhin ang kasalukuyang node sa ${node}?", "change_current_node_title": "Baguhin ang kasalukuyang node", - "change_exchange_provider": "Baguhin ang exchange provider", + "change_exchange_provider": "Baguhin ang Swap Provider", "change_language": "Baguhin ang wika", "change_language_to": "Baguhin ang wika sa ${language}?", "change_password": "Baguhin ang password", @@ -129,8 +130,9 @@ "choose_from_available_options": "Pumili mula sa magagamit na mga pagpipilian:", "choose_one": "Pumili ng isa", "choose_relay": "Mangyaring pumili ng relay na gagamitin", - "choose_wallet_currency": "Mangyaring piliin ang pera ng wallet:", - "clear": "Burahin", + "choose_wallet_currency": "Mangyaring piliin ang Pera ng Wallet:", + "choose_wallet_group": "Piliin ang pangkat ng Wallet", + "clear": "Malinaw", "clearnet_link": "Link ng Clearnet", "close": "Isara", "coin_control": "Coin control (opsyonal)", @@ -142,6 +144,7 @@ "confirm_delete_wallet": "Tatanggalin ng pagkilos na ito ang wallet na ito. Gusto mo bang magpatuloy?", "confirm_fee_deduction": "Kumpirmahin ang pagbabawas ng fee", "confirm_fee_deduction_content": "Sumasang-ayon ka bang bawasan ang fee mula sa output?", + "confirm_passphrase": "Kumpirma ang passphrase", "confirm_sending": "Kumpirmahin ang pagpapadala", "confirm_silent_payments_switch_node": "Ang iyong kasalukuyang node ay hindi sumusuporta sa tahimik na pagbabayad \\ nCake Wallet ay lilipat sa isang katugmang node, para lamang sa pag-scan", "confirmations": "Mga kumpirmasyon", @@ -160,6 +163,7 @@ "contact_name": "Pangalan ng Contact", "contact_support": "Makipag-ugnay sa Suporta", "continue_text": "Magpatuloy", + "contract_warning": "Ang address ng kontrata na ito ay na -flag bilang potensyal na mapanlinlang. Mangyaring iproseso nang may pag -iingat.", "contractName": "Pangalan ng Kontrata", "contractSymbol": "Simbolo ng Kontrata", "copied_key_to_clipboard": "Kinopya ang ${key} sa Clipboard", @@ -175,6 +179,7 @@ "create_invoice": "Lumikha ng invoice", "create_new": "Lumikha ng Bagong Wallet", "create_new_account": "Lumikha ng bagong account", + "create_new_seed": "Lumikha ng bagong binhi", "creating_new_wallet": "Lumikha ng bagong wallet", "creating_new_wallet_error": "Error: ${description}", "creation_date": "Petsa ng paglikha", @@ -195,6 +200,7 @@ "delete_wallet_confirm_message": "Sigurado ka ba na gusto mong tanggalin ang iyong ${wallet_name} wallet?", "deleteConnectionConfirmationPrompt": "Sigurado ka bang gusto mong tanggalin ang koneksyon sa", "denominations": "Mga Denominasyon", + "derivationpath": "Landas ng derivation", "descending": "Pababang", "description": "Paglalarawan", "destination_tag": "Tag ng patutunguhan:", @@ -207,6 +213,7 @@ "disable_buy": "Huwag paganahin ang pagkilos ng pagbili", "disable_cake_2fa": "Huwag paganahin ang Cake 2FA", "disable_exchange": "Huwag paganahin ang palitan", + "disable_fee_api_warning": "Sa pamamagitan ng pag -off nito, ang mga rate ng bayad ay maaaring hindi tumpak sa ilang mga kaso, kaya maaari mong tapusin ang labis na bayad o pagsuporta sa mga bayarin para sa iyong mga transaksyon", "disable_fiat": "Huwag paganahin ang fiat", "disable_sell": "Huwag paganahin ang pagkilos ng pagbebenta", "disableBatteryOptimization": "Huwag Paganahin ang Pag-optimize ng Baterya", @@ -228,8 +235,10 @@ "edit_token": "I-edit ang token", "electrum_address_disclaimer": "Bumubuo kami ng mga bagong address sa tuwing gagamit ka ng isa, ngunit ang mga nakaraang address ay patuloy na gumagana", "email_address": "Email Address", + "enable": "Paganahin", + "enable_mempool_api": "Mempool API para sa tumpak na bayad at mga petsa", "enable_replace_by_fee": "Paganahin ang Replace-By-Fee", - "enable_silent_payments_scanning": "Paganahin ang pag-scan ng mga tahimik na pagbabayad", + "enable_silent_payments_scanning": "Simulan ang pag -scan ng tahimik na pagbabayad, hanggang sa maabot ang tip", "enabled": "Pinagana", "enter_amount": "Ipasok ang halaga", "enter_backup_password": "Ipasok ang backup na password dito", @@ -273,7 +282,7 @@ "event": "Kaganapan", "events": "Mga kaganapan", "exchange": "Palitan", - "exchange_incorrect_current_wallet_for_xmr": "Kung gusto mong palitan ang XMR mula sa iyong balanse ng Monero ng Cake Wallet, mangyaring lumipat muna sa iyong Monero wallet.", + "exchange_incorrect_current_wallet_for_xmr": "Kung nais mong magpalit ng XMR mula sa iyong balanse ng Wallet Monero, mangyaring lumipat sa iyong Monero Wallet muna.", "exchange_new_template": "Bagong template", "exchange_provider_unsupported": "Ang ${providerName} ay hindi na suportado!", "exchange_result_confirm": "Sa pamamagitan ng pagpindot sa kumpirmahin, ikaw ay magpapadala ${fetchingLabel} ${from} mula sa inyong wallet na tinatawag ${walletName} sa wallet na ipinapakita sa ibaba. O pwede kang magpadala sa inyong external wallet sa ibabang address/QR code.\n\nPara magpatuloy, mangyaring pindutin upang kumpirmahin o bumalik para baguhin ang halaga.", @@ -291,6 +300,8 @@ "failed_authentication": "Nabigo ang pagpapatunay. ${state_error}", "faq": "FAQ", "features": "Mga tampok", + "fee_less_than_min": "Ang napiling bayad ay mas mababa sa minimum, mangyaring dagdagan ang mga bayarin upang maipadala ang transaksyon", + "fee_rate": "Rate ng bayad", "fetching": "Pagkuha", "fiat_api": "Fiat API", "fiat_balance": "Balanse ng fiat", @@ -298,7 +309,7 @@ "fill_code": "Mangyaring ilagay ang verfification code na ibinigay sa iyong email", "filter_by": "Filter ni", "first_wallet_text": "Kahanga-hangang wallet para sa Monero, Bitcoin, Litecoin, Ethereum, at Haven", - "fixed_pair_not_supported": "Ang nakapirming pares na ito ay hindi suportado sa mga napiling palitan", + "fixed_pair_not_supported": "Ang nakapirming pares na ito ay hindi suportado sa mga napiling serbisyo ng pagpapalit", "fixed_rate": "Fixed rate", "fixed_rate_alert": "Makakapagpasok ka ng halaga ng pagtanggap kapag nasuri ang fixed rate mode. Gusto mo bang lumipat sa fixed rate mode?", "forgot_password": "Nakalimutan ang Password", @@ -354,6 +365,20 @@ "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", "light_theme": "Light", + "litecoin_enable_mweb_sync": "Paganahin ang pag -scan ng MWeb", + "litecoin_mweb": "Mweb", + "litecoin_mweb_always_scan": "Itakda ang MWeb na laging nag -scan", + "litecoin_mweb_description": "Ang MWeb ay isang bagong protocol na nagdadala ng mas mabilis, mas mura, at mas maraming pribadong mga transaksyon sa Litecoin", + "litecoin_mweb_dismiss": "Tanggalin", + "litecoin_mweb_display_card": "Ipakita ang MWEB Card", + "litecoin_mweb_enable_later": "Maaari kang pumili upang paganahin muli ang MWeb sa ilalim ng mga setting ng pagpapakita.", + "litecoin_mweb_pegin": "Peg in", + "litecoin_mweb_pegout": "Peg out", + "litecoin_mweb_scanning": "Pag -scan ng Mweb", + "litecoin_mweb_settings": "Mga Setting ng Mweb", + "litecoin_mweb_warning": "Ang paggamit ng MWEB ay unang i -download ang ~ 600MB ng data, at maaaring tumagal ng hanggang sa 30 minuto depende sa bilis ng network. Ang paunang data na ito ay mag -download lamang ng isang beses at magagamit para sa lahat ng mga wallets ng Litecoin", + "litecoin_what_is_mweb": "Ano ang MWEB?", + "live_fee_rates": "Mga rate ng live na bayad sa pamamagitan ng API", "load_more": "Mag-load pa", "loading_your_wallet": "Naglo-load ng iyong wallet", "login": "Mag-login", @@ -383,6 +408,8 @@ "monero_light_theme": "Monero Light Theme", "moonpay_alert_text": "Ang halaga ay dapat na higit pa o katumbas ng ${minAmount} ${fiatCurrency}", "more_options": "Higit pang mga Pagpipilian", + "mweb_confirmed": "Nakumpirma na MWeb", + "mweb_unconfirmed": "Hindi nakumpirma si Mweb", "name": "Pangalan", "nano_current_rep": "Kasalukuyang Representative", "nano_gpt_thanks_message": "Salamat sa paggamit ng NanoGPT! Tandaan na bumalik sa browser matapos makumpleto ang iyong transaksyon!", @@ -412,7 +439,7 @@ "node_test": "Test", "nodes": "Mga node", "nodes_list_reset_to_default_message": "Sigurado ka bang gusto mo bang i-reset ang mga settings sa default?", - "none_of_selected_providers_can_exchange": "Wala sa mga napiling provider ang makakagawa ng palitan na ito", + "none_of_selected_providers_can_exchange": "Wala sa mga napiling tagapagbigay ng serbisyo ang maaaring gumawa ng pagpapalit na ito", "noNFTYet": "Wala pang NFT", "normal": "Normal", "note_optional": "Tala (opsyonal)", @@ -442,6 +469,7 @@ "overwrite_amount": "I-overwrite ang halaga", "pairingInvalidEvent": "Pairing Invalid Event", "passphrase": "Passphrase (opsyonal)", + "passphrases_doesnt_match": "Ang mga passphrases ay hindi tumutugma, mangyaring subukang muli", "password": "Password", "paste": "I-paste", "pause_wallet_creation": "Kasalukuyang naka-pause ang kakayahang gumawa ng Haven Wallet.", @@ -462,13 +490,13 @@ "please_try_to_connect_to_another_node": "Pakisubukang kumonekta sa iba pang node", "please_wait": "Mangyaring maghintay", "polygonscan_history": "Kasaysayan ng PolygonScan", - "powered_by": "Pinapatakbo ng${title}", - "pre_seed_button_text": "Naiitindihan ko. Ipakita ang aking seed", - "pre_seed_description": "Sa susunod na pahina ay makikita mo ang isang serye ng ${words} na salita. Ito ang iyong natatangi at pribadong seed at ito ang tanging paraan upang mabawi ang iyong wallet kung sakaling mawala o hindi gumana. Responsibilidad mong isulat ito sa isang ligtas na lugar sa labas ng Cake Wallet app.", - "pre_seed_title": "MAHALAGA", + "powered_by": "Pinapagana ng ${title}", + "pre_seed_button_text": "Naiintindihan ko. Ipakita sa akin ang aking binhi", + "pre_seed_description": "Sa susunod na pahina makikita mo ang isang serye ng mga ${words} na mga salita. Ito ang iyong natatangi at pribadong binhi at ito ang tanging paraan upang mabawi ang iyong pitaka kung sakaling mawala o madepektong paggawa. Responsibilidad mong isulat ito at itago ito sa isang ligtas na lugar sa labas ng cake wallet app.", + "pre_seed_title": "Mahalaga", "prepaid_cards": "Mga Prepaid Card", - "prevent_screenshots": "Maiwasan ang mga screenshot at pag-record ng screen", - "privacy": "Pagkapribado", + "prevent_screenshots": "Maiwasan ang mga screenshot at pag -record ng screen", + "privacy": "Privacy", "privacy_policy": "Patakaran sa Pagkapribado", "privacy_settings": "Settings para sa pagsasa-pribado", "private_key": "Private key", @@ -593,8 +621,11 @@ "seed_share": "Ibahagi ang seed", "seed_title": "Seed", "seedtype": "Seed type", + "seedtype_alert_content": "Ang pagbabahagi ng mga buto sa iba pang mga pitaka ay posible lamang sa bip39 seedtype.", + "seedtype_alert_title": "Alerto ng Seedtype", "seedtype_legacy": "Legacy (25 na salita)", "seedtype_polyseed": "Polyseed (16 na salita)", + "seedtype_wownero": "Wownero (14 na salita)", "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.", @@ -605,6 +636,7 @@ "send": "Ipadala", "send_address": "${cryptoCurrency} address", "send_amount": "Halaga:", + "send_change_to_you": "Baguhin, sa iyo:", "send_creating_transaction": "Paglikha ng transaksyon", "send_error_currency": "Ang halaga ay maaari lamang maglaman ng mga numero", "send_error_minimum_value": "Ang minimum na halaga ay 0.01", @@ -658,19 +690,21 @@ "setup_your_debit_card": "I-set up ang iyong debit card", "share": "Ibahagi", "share_address": "Ibahagi ang address", + "shared_seed_wallet_groups": "Ibinahaging mga pangkat ng pitaka ng binhi", "show_details": "Ipakita ang mga detalye", "show_keys": "Ipakita ang mga seed/key", "show_market_place": "Ipakita ang Marketplace", + "show_seed": "Ipakita ang seed", "sign_message": "Mag -sign Message", + "sign_up": "Mag-sign Up", "sign_verify_message": "Mag -sign o i -verify ang mensahe", "sign_verify_message_sub": "Mag -sign o i -verify ang isang mensahe gamit ang iyong pribadong key", "sign_verify_title": "Mag -sign / Mag -verify", "signature": "Lagda", "signature_invalid_error": "Ang lagda ay hindi wasto para sa ibinigay na mensahe", - "show_seed": "Ipakita ang seed", - "sign_up": "Mag-sign Up", "signTransaction": "Mag-sign ang Transaksyon", "signup_for_card_accept_terms": "Mag-sign up para sa card at tanggapin ang mga tuntunin.", + "silent_payment": "Tahimik na pagbabayad", "silent_payments": "Tahimik na pagbabayad", "silent_payments_always_scan": "Itakda ang mga tahimik na pagbabayad na laging nag-scan", "silent_payments_disclaimer": "Ang mga bagong address ay hindi mga bagong pagkakakilanlan. Ito ay isang muling paggamit ng isang umiiral na pagkakakilanlan na may ibang label.", @@ -681,6 +715,7 @@ "silent_payments_scanned_tip": "Na-scan sa tip! (${tip})", "silent_payments_scanning": "Pag-scan ng tahimik na pagbabayad", "silent_payments_settings": "Mga setting ng tahimik na pagbabayad", + "single_seed_wallets_group": "Solong mga pitaka ng binhi", "slidable": "Slidable", "sort_by": "Pag-uri-uriin sa pamamagitan ng", "spend_key_private": "Spend key (private)", @@ -703,12 +738,13 @@ "switchToEVMCompatibleWallet": "Mangyaring lumipat sa isang EVM compatible na wallet at subukang muli (Ethereum, Polygon)", "symbol": "Simbolo", "sync_all_wallets": "I-sync ang lahat ng mga wallet", + "sync_status_attempting_scan": "Pagtatangka ng pag -scan", "sync_status_attempting_sync": "SINUSUBUKANG I-SYNC", "sync_status_connected": "KONEKTADO", "sync_status_connecting": "KUMOKENEKTA", "sync_status_failed_connect": "NADISKONEKTA", "sync_status_not_connected": "HINDI KONEKTADO", - "sync_status_starting_scan": "SIMULA SA PAG-SCAN", + "sync_status_starting_scan": "Simula sa pag -scan (mula sa ${height})", "sync_status_starting_sync": "SIMULA SA PAG-SYNC", "sync_status_syncronized": "NAKA-SYNCHRONIZE", "sync_status_syncronizing": "PAG-SYNCHRONIZE", @@ -795,6 +831,7 @@ "trusted": "Pinagkakatiwalaan", "tx_commit_exception_no_dust_on_change": "Ang transaksyon ay tinanggihan sa halagang ito. Sa mga barya na ito maaari kang magpadala ng ${min} nang walang sukli o ${max} na nagbabalik ng sukli.", "tx_commit_failed": "Nabigo ang transaksyon. Mangyaring makipag-ugnay sa suporta.", + "tx_commit_failed_no_peers": "Nabigo ang transaksyon na mag -broadcast, mangyaring subukang muli sa isang segundo o higit pa", "tx_invalid_input": "Gumagamit ka ng maling uri ng pag-input para sa ganitong uri ng pagbabayad", "tx_no_dust_exception": "Ang transaksyon ay tinanggihan sa pamamagitan ng pagpapadala ng isang maliit na halaga. Mangyaring subukang dagdagan ang halaga.", "tx_not_enough_inputs_exception": "Hindi sapat na magagamit ang mga input. Mangyaring pumili ng higit pa sa ilalim ng Coin Control", @@ -827,9 +864,9 @@ "use_testnet": "Gumamit ng testnet", "value": "Halaga", "value_type": "Uri ng halaga", - "verify_message": "I -verify ang mensahe", "variable_pair_not_supported": "Ang variable na pares na ito ay hindi suportado sa mga napiling exchange", "verification": "Pag-verify", + "verify_message": "I -verify ang mensahe", "verify_with_2fa": "Mag-verify sa Cake 2FA", "version": "Bersyon ${currentVersion}", "view_all": "Tingnan lahat", @@ -839,8 +876,16 @@ "view_transaction_on": "Tingnan ang transaksyon sa ", "voting_weight": "Bigat ng pagboto", "waitFewSecondForTxUpdate": "Mangyaring maghintay ng ilang segundo para makita ang transaksyon sa history ng mga transaksyon", + "wallet_group": "Group ng Wallet", + "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_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_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", "wallet_list_create_new_wallet": "Lumikha ng bagong wallet", + "wallet_list_edit_group_name": "I -edit ang Pangalan ng Grupo", "wallet_list_edit_wallet": "I-edit ang wallet", "wallet_list_failed_to_load": "Nabigong na-load ang ${wallet_name} na wallet. ${error}", "wallet_list_failed_to_remove": "Nabigong alisin ang ${wallet_name} wallet. ${error}", diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index e7cdc5b12..b23f64d60 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -36,6 +36,7 @@ "agree": "Kabul Et", "agree_and_continue": "Kabul Et & Devam Et", "agree_to": "Hesap oluşturarak bunları kabul etmiş olursunuz ", + "alert_notice": "Fark etme", "all": "HEPSİ", "all_trades": "Tüm takaslar", "all_transactions": "Tüm transferler", @@ -113,7 +114,7 @@ "change_currency": "Para Birimini Değiştir", "change_current_node": "Şimdiki düğümü ${node} düğümüne değiştirmek istediğinizden emin misin?", "change_current_node_title": "Şimdiki düğümü değiştir", - "change_exchange_provider": "Takas sağlayıcısını değiştir", + "change_exchange_provider": "Takas Sağlayıcısı Değiştir", "change_language": "Dili değiştir", "change_language_to": "Dili şuna değiştir: ${language}?", "change_password": "Parolayı değiştir", @@ -130,6 +131,7 @@ "choose_one": "Birini seç", "choose_relay": "Lütfen kullanmak için bir röle seçin", "choose_wallet_currency": "Lütfen cüzdanın para birimini seç:", + "choose_wallet_group": "Cüzdan Grubu Seçin", "clear": "Temizle", "clearnet_link": "Net bağlantı", "close": "Kapalı", @@ -142,6 +144,7 @@ "confirm_delete_wallet": "Bu eylem, bu cüzdanı silecek. Devam etmek istiyor musun?", "confirm_fee_deduction": "Ücret kesintisini onaylayın", "confirm_fee_deduction_content": "Ücreti çıktıdan düşürmeyi kabul ediyor musunuz?", + "confirm_passphrase": "Parola onaylayın", "confirm_sending": "Göndermeyi onayla", "confirm_silent_payments_switch_node": "Mevcut düğümünüz sessiz ödemeleri desteklemiyor \\ nCake cüzdanı, sadece tarama için uyumlu bir düğüme geçecektir", "confirmations": "Onay", @@ -160,6 +163,7 @@ "contact_name": "Kişi ismi", "contact_support": "Destek ile İletişime Geç", "continue_text": "Devam et", + "contract_warning": "Bu sözleşme adresi potansiyel olarak hileli olarak işaretlenmiştir. Lütfen dikkatle işleyin.", "contractName": "Sözleşme Adı", "contractSymbol": "Sözleşme Sembolü", "copied_key_to_clipboard": "${key} panoya kopyalandı", @@ -175,6 +179,7 @@ "create_invoice": "Fatura oluşturmak", "create_new": "Yeni Cüzdan Oluştur", "create_new_account": "Yeni hesap oluştur", + "create_new_seed": "Yeni Tohum Oluştur", "creating_new_wallet": "Cüzdan oluşturuluyor", "creating_new_wallet_error": "Hata: ${description}", "creation_date": "Oluşturulma tarihi", @@ -195,6 +200,7 @@ "delete_wallet_confirm_message": "${wallet_name} isimli cüzdanını silmek istediğinden emin misin?", "deleteConnectionConfirmationPrompt": "Bağlantıyı silmek istediğinizden emin misiniz?", "denominations": "Mezhepler", + "derivationpath": "Türev yolu", "descending": "Azalan", "description": "Tanım", "destination_tag": "Hedef Etiketi:", @@ -207,6 +213,7 @@ "disable_buy": "Satın alma işlemini devre dışı bırak", "disable_cake_2fa": "Cake 2FA'yı Devre Dışı Bırak", "disable_exchange": "Borsayı devre dışı bırak", + "disable_fee_api_warning": "Bunu kapatarak, ücret oranları bazı durumlarda yanlış olabilir, bu nedenle işlemleriniz için ücretleri fazla ödeyebilir veya az ödeyebilirsiniz.", "disable_fiat": "İtibari paraları devre dışı bırak", "disable_sell": "Satış işlemini devre dışı bırak", "disableBatteryOptimization": "Pil optimizasyonunu devre dışı bırakın", @@ -228,8 +235,10 @@ "edit_token": "Belirteci düzenle", "electrum_address_disclaimer": "Adresini her kullandığında yeni adres oluşturuyoruz, ancak önceki adresler de çalışmaya devam eder", "email_address": "E-posta Adresi", + "enable": "Olanak vermek", + "enable_mempool_api": "Doğru ücretler ve tarihler için Mempool API'si", "enable_replace_by_fee": "Farklı Değiştir'i Etkinleştir", - "enable_silent_payments_scanning": "Sessiz ödeme taramasını etkinleştirin", + "enable_silent_payments_scanning": "Bahşiş ulaşılıncaya kadar sessiz ödemeleri taramaya başlayın", "enabled": "Etkin", "enter_amount": "Miktar Girin", "enter_backup_password": "Yedekleme parolasını buraya gir", @@ -273,7 +282,7 @@ "event": "Etkinlik", "events": "Olaylar", "exchange": "Takas", - "exchange_incorrect_current_wallet_for_xmr": "Cake Wallet'daki Monero bakiyenizi kullanarak takas yapmak istiyorsan, lütfen önce Monero cüzdanına geç.", + "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!", "exchange_result_confirm": "Onaylaya basarak, ${fetchingLabel} ${from} miktarında ${walletName} olarak adlandırılan cüzdanından aşağıda gösterilen adrese gönderilecek. Veya harici cüzdanından aşağıdaki adrese / QR koduna gönderebilirsin.\n\nLütfen devam etmek için onayla'ya bas veya tutarı değiştirmek için geri dön.", @@ -291,6 +300,8 @@ "failed_authentication": "Doğrulama başarısız oldu. ${state_error}", "faq": "SSS", "features": "Özellikler", + "fee_less_than_min": "Seçilen ücret asgari düzeyden azdır, lütfen işlemi gönderebilmek için ücretleri artırın", + "fee_rate": "Ücret oranı", "fetching": "Getiriliyor", "fiat_api": "İtibari Para API", "fiat_balance": "Fiat Bakiyesi", @@ -298,7 +309,7 @@ "fill_code": "Lütfen e-postanıza gelen doğrulama kodunu girin", "filter_by": "Şuna göre filtrele", "first_wallet_text": "Monero, Bitcoin, Ethereum, Litecoin ve Haven için harika cüzdan", - "fixed_pair_not_supported": "Bu sabit paritesi seçilen borsalarda desteklenmemekte", + "fixed_pair_not_supported": "Bu sabit çift seçilen takas hizmetleri ile desteklenmez", "fixed_rate": "Sabit oran", "fixed_rate_alert": "Sabit oran modunu işaretlersen alım tutarını girebilirsin. Sabit oran moduna geçmek ister misin?", "forgot_password": "Parolamı unuttum", @@ -354,6 +365,20 @@ "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", "light_theme": "Aydınlık", + "litecoin_enable_mweb_sync": "MWEB taramasını etkinleştir", + "litecoin_mweb": "Mweb", + "litecoin_mweb_always_scan": "MWEB'i her zaman taramayı ayarlayın", + "litecoin_mweb_description": "MWEB, Litecoin'e daha hızlı, daha ucuz ve daha fazla özel işlem getiren yeni bir protokoldür", + "litecoin_mweb_dismiss": "Azletmek", + "litecoin_mweb_display_card": "MWEB kartını göster", + "litecoin_mweb_enable_later": "Ekran ayarlarının altında MWEB'yi tekrar etkinleştirmeyi seçebilirsiniz.", + "litecoin_mweb_pegin": "Takılmak", + "litecoin_mweb_pegout": "Çiğnemek", + "litecoin_mweb_scanning": "MWEB taraması", + "litecoin_mweb_settings": "MWEB ayarları", + "litecoin_mweb_warning": "MWEB kullanmak başlangıçta ~ 600MB veri indirir ve ağ hızına bağlı olarak 30 dakikaya kadar sürebilir. Bu ilk veriler yalnızca bir kez indirilecek ve tüm Litecoin cüzdanları için kullanılabilir olacak", + "litecoin_what_is_mweb": "MWEB nedir?", + "live_fee_rates": "API üzerinden canlı ücret oranları", "load_more": "Daha fazla yükle", "loading_your_wallet": "Cüzdanın yükleniyor", "login": "Login", @@ -383,6 +408,8 @@ "monero_light_theme": "Monero Hafif Tema", "moonpay_alert_text": "Tutar ${minAmount} ${fiatCurrency} miktarına eşit veya daha fazla olmalıdır", "more_options": "Daha Fazla Seçenek", + "mweb_confirmed": "Onaylanmış mweb", + "mweb_unconfirmed": "Doğrulanmamış mweb", "name": "İsim", "nano_current_rep": "Mevcut temsilci", "nano_gpt_thanks_message": "Nanogpt kullandığınız için teşekkürler! İşleminiz tamamlandıktan sonra tarayıcıya geri dönmeyi unutmayın!", @@ -412,7 +439,7 @@ "node_test": "Test Et", "nodes": "Düğümler", "nodes_list_reset_to_default_message": "Ayarları varsayılana sıfırlamak istediğinizden emin misin?", - "none_of_selected_providers_can_exchange": "Seçilen sağlayıcılardan hiçbiri bu takası yapamaz", + "none_of_selected_providers_can_exchange": "Seçilen sağlayıcıların hiçbiri bu takas yapamaz", "noNFTYet": "Henüz NFT yok", "normal": "Normal", "note_optional": "Not (isteğe bağlı)", @@ -442,6 +469,7 @@ "overwrite_amount": "Miktarın üzerine yaz", "pairingInvalidEvent": "Geçersiz Etkinliği Eşleştirme", "passphrase": "Passfrase (isteğe bağlı)", + "passphrases_doesnt_match": "Passfrases eşleşmiyor, lütfen tekrar deneyin", "password": "Parola", "paste": "Yapıştır", "pause_wallet_creation": "Haven Cüzdanı oluşturma yeteneği şu anda duraklatıldı.", @@ -593,8 +621,11 @@ "seed_share": "Tohumu paylaş", "seed_title": "Tohum", "seedtype": "Tohum", + "seedtype_alert_content": "Tohumları diğer cüzdanlarla paylaşmak sadece BIP39 tohumu ile mümkündür.", + "seedtype_alert_title": "SeedType uyarısı", "seedtype_legacy": "Miras (25 kelime)", "seedtype_polyseed": "Polyseed (16 kelime)", + "seedtype_wownero": "Wownero (14 kelime)", "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.", @@ -605,6 +636,7 @@ "send": "Para Gönder", "send_address": "${cryptoCurrency} adresi", "send_amount": "Miktar:", + "send_change_to_you": "Değiştir, size:", "send_creating_transaction": "İşlem oluşturuluyor", "send_error_currency": "Para birimi sadece sayı içerebilir", "send_error_minimum_value": "Minimum tutar değeri 0.01'dir", @@ -658,6 +690,7 @@ "setup_your_debit_card": "Banka kartını ayarla", "share": "Paylaşmak", "share_address": "Adresi paylaş", + "shared_seed_wallet_groups": "Paylaşılan tohum cüzdan grupları", "show_details": "Detayları Göster", "show_keys": "Tohumları/anahtarları göster", "show_market_place": "Pazar Yerini Göster", @@ -671,6 +704,7 @@ "signature_invalid_error": "İmza verilen mesaj için geçerli değil", "signTransaction": "İşlem İmzala", "signup_for_card_accept_terms": "Kart için kaydol ve koşulları kabul et.", + "silent_payment": "Sessiz Ödeme", "silent_payments": "Sessiz ödemeler", "silent_payments_always_scan": "Sessiz ödemeleri her zaman tarama ayarlayın", "silent_payments_disclaimer": "Yeni adresler yeni kimlikler değildir. Farklı bir etikete sahip mevcut bir kimliğin yeniden kullanımıdır.", @@ -681,6 +715,7 @@ "silent_payments_scanned_tip": "Bahşiş için tarandı! (${tip})", "silent_payments_scanning": "Sessiz Ödemeler Taraması", "silent_payments_settings": "Sessiz Ödeme Ayarları", + "single_seed_wallets_group": "Tek tohum cüzdanları", "slidable": "kaydırılabilir", "sort_by": "Göre sırala", "spend_key_private": "Harcama anahtarı (özel)", @@ -703,12 +738,13 @@ "switchToEVMCompatibleWallet": "Lütfen EVM uyumlu bir cüzdana geçin ve tekrar deneyin (Ethereum, Polygon)", "symbol": "Sembol", "sync_all_wallets": "Tüm cüzdanları senkronize edin", + "sync_status_attempting_scan": "Tarama deneme", "sync_status_attempting_sync": "SENKRONİZE EDİLMEYE ÇALIŞILIYOR", "sync_status_connected": "BAĞLANILDI", "sync_status_connecting": "BAĞLANILIYOR", "sync_status_failed_connect": "BAĞLANTI KESİLDİ", "sync_status_not_connected": "BAĞLI DEĞİL", - "sync_status_starting_scan": "Başlangıç ​​taraması", + "sync_status_starting_scan": "Başlangıç ​​taraması (${height})", "sync_status_starting_sync": "SENKRONİZE BAŞLATILIYOR", "sync_status_syncronized": "SENKRONİZE EDİLDİ", "sync_status_syncronizing": "SENKRONİZE EDİLİYOR", @@ -795,6 +831,7 @@ "trusted": "Güvenilir", "tx_commit_exception_no_dust_on_change": "İşlem bu miktarla reddedilir. Bu madeni paralarla değişiklik yapmadan ${min} veya değişikliği döndüren ${max} gönderebilirsiniz.", "tx_commit_failed": "İşlem taahhüdü başarısız oldu. Lütfen Destek ile iletişime geçin.", + "tx_commit_failed_no_peers": "İşlem yayın yapamadı, lütfen bir saniye içinde tekrar deneyin", "tx_invalid_input": "Bu tür ödeme için yanlış giriş türünü kullanıyorsunuz", "tx_no_dust_exception": "İşlem, çok küçük bir miktar gönderilerek reddedilir. Lütfen miktarı artırmayı deneyin.", "tx_not_enough_inputs_exception": "Yeterli giriş yok. Lütfen madeni para kontrolü altında daha fazlasını seçin", @@ -839,8 +876,16 @@ "view_transaction_on": "İşlemi şurada görüntüle ", "voting_weight": "Oy kullanma", "waitFewSecondForTxUpdate": "İşlemin işlem geçmişine yansıması için lütfen birkaç saniye bekleyin", + "wallet_group": "Cüzdan grubu", + "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_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_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ı", "wallet_list_create_new_wallet": "Yeni Cüzdan Oluştur", + "wallet_list_edit_group_name": "Grup Adını Düzenle", "wallet_list_edit_wallet": "Cüzdanı düzenle", "wallet_list_failed_to_load": "Failed to load ${wallet_name} wallet. ${error}", "wallet_list_failed_to_remove": "${wallet_name} cüzdanı yüklenirken hata oluştu. ${error}", diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index 91b5b5266..79dc0543f 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -36,6 +36,7 @@ "agree": "Згоден", "agree_and_continue": "Погодитися та продовжити", "agree_to": "Створюючи обліковий запис, ви погоджуєтеся з ", + "alert_notice": "Ув'язнення", "all": "ВСЕ", "all_trades": "Всі операції", "all_transactions": "Всі транзакції", @@ -113,7 +114,7 @@ "change_currency": "Змінити валюту", "change_current_node": "Ви впевнені, що хочете змінити поточний вузол на ${node}?", "change_current_node_title": "Змінити поточний вузол", - "change_exchange_provider": "Змінити провайдера обміну", + "change_exchange_provider": "Змінити постачальник свопів", "change_language": "Змінити мову", "change_language_to": "Змінити мову на ${language}?", "change_password": "Змінити пароль", @@ -130,6 +131,7 @@ "choose_one": "Вибери один", "choose_relay": "Будь ласка, виберіть реле для використання", "choose_wallet_currency": "Будь ласка, виберіть валюту гаманця:", + "choose_wallet_group": "Виберіть групу гаманця", "clear": "Очистити", "clearnet_link": "Посилання Clearnet", "close": "Закрити", @@ -142,6 +144,7 @@ "confirm_delete_wallet": "Ця дія видалить гаманець. Ви хочете продовжити?", "confirm_fee_deduction": "Підтвердьте відрахування комісії", "confirm_fee_deduction_content": "Чи погоджуєтесь ви вирахувати комісію з сумми одержувача?", + "confirm_passphrase": "Підтвердьте пасфрази", "confirm_sending": "Підтвердити відправлення", "confirm_silent_payments_switch_node": "Ваш поточний вузол не підтримує мовчазні платежі \\ ncake Wallet перейде на сумісний вузол, лише для сканування", "confirmations": "Підтвердження", @@ -160,6 +163,7 @@ "contact_name": "Ім'я контакту", "contact_support": "Звернутися до служби підтримки", "continue_text": "Продовжити", + "contract_warning": "Ця адреса контракту була позначена як потенційно шахрайська. Будь ласка, обробляйте обережно.", "contractName": "Назва контракту", "contractSymbol": "Контракт символ", "copied_key_to_clipboard": "Скопійовано ${key} в буфер обміну", @@ -175,6 +179,7 @@ "create_invoice": "Створити рахунок-фактуру", "create_new": "Створити новий гаманець", "create_new_account": "Створити новий акаунт", + "create_new_seed": "Створіть нове насіння", "creating_new_wallet": "Створення нового гаманця", "creating_new_wallet_error": "Помилка: ${description}", "creation_date": "Дата створення", @@ -195,6 +200,7 @@ "delete_wallet_confirm_message": "Ви впевнені, що хочете видалити гаманець ${wallet_name}?", "deleteConnectionConfirmationPrompt": "Ви впевнені, що хочете видалити з’єднання з", "denominations": "Конфесія", + "derivationpath": "Шлях виведення", "descending": "Низхідний", "description": "опис", "destination_tag": "Тег призначення:", @@ -207,6 +213,7 @@ "disable_buy": "Вимкнути дію покупки", "disable_cake_2fa": "Вимкнути Cake 2FA", "disable_exchange": "Вимкнути exchange", + "disable_fee_api_warning": "Вимкнувши це, ставки плати в деяких випадках можуть бути неточними, тому ви можете переплатити або недооплатити плату за свої транзакції", "disable_fiat": "Вимкнути фиат", "disable_sell": "Вимкнути дію продажу", "disableBatteryOptimization": "Вимкнути оптимізацію акумулятора", @@ -228,8 +235,10 @@ "edit_token": "Редагувати маркер", "electrum_address_disclaimer": "Ми створюємо нові адреси щоразу, коли ви використовуєте їх, але попередні адреси продовжують працювати", "email_address": "Адреса електронної пошти", + "enable": "Ввімкнути", + "enable_mempool_api": "API Mempool для точних зборів та дат", "enable_replace_by_fee": "Увімкнути заміну з комісією", - "enable_silent_payments_scanning": "Увімкнути мовчазні платежі сканування", + "enable_silent_payments_scanning": "Почніть сканувати мовчазні платежі, поки не буде досягнуто наконечника", "enabled": "Увімкнено", "enter_amount": "Введіть суму", "enter_backup_password": "Введіть пароль резервної копії", @@ -272,8 +281,8 @@ "etherscan_history": "Історія Etherscan", "event": "Подія", "events": "Події", - "exchange": "Обмін", - "exchange_incorrect_current_wallet_for_xmr": "Якщо ви хочете обміняти XMR із вашого балансу Cake Wallet Monero, спочатку перейдіть на свій гаманець Monero.", + "exchange": "Обміняти", + "exchange_incorrect_current_wallet_for_xmr": "Якщо ви хочете поміняти XMR зі свого балансу для тортів Monero Balance, спочатку перейдіть на свій гаманець Monero.", "exchange_new_template": "Новий шаблон", "exchange_provider_unsupported": "${providerName} більше не підтримується!", "exchange_result_confirm": "Натиснувши підтвердити, ви відправите ${fetchingLabel} ${from} з вашого гаманця ${walletName} на адресу вказану нижче. Або ви можете відправити зі свого зовнішнього гаманця на нижчевказану адресу/QR-код.\n\nБудь ласка, натисніть підтвердити для продовження або поверніться назад щоб змінити суму.", @@ -291,6 +300,8 @@ "failed_authentication": "Помилка аутентифікації. ${state_error}", "faq": "FAQ", "features": "Особливості", + "fee_less_than_min": "Вибрана плата менша, ніж мінімум, будь ласка, збільште збори, щоб мати можливість відправити транзакцію", + "fee_rate": "Ставка плати", "fetching": "Завантаження", "fiat_api": "Фіат API", "fiat_balance": "Фіат Баланс", @@ -298,7 +309,7 @@ "fill_code": "Будь ласка, введіть код підтвердження, надісланий на вашу електронну адресу", "filter_by": "Фільтрувати по", "first_wallet_text": "В самому зручному гаманці для Monero, Bitcoin, Ethereum, Litecoin, та Haven", - "fixed_pair_not_supported": "Ця фіксована пара не підтримується вибраними біржами", + "fixed_pair_not_supported": "Ця фіксована пара не підтримується вибраними послугами Swap", "fixed_rate": "Фіксована ставка", "fixed_rate_alert": "Ви зможете ввести суму отримання тоді, коли буде встановлений режим фіксованої ставки. Ви хочете перейти в режим фіксованої ставки?", "forgot_password": "Забули пароль", @@ -354,6 +365,20 @@ "ledger_error_wrong_app": "Будь ласка, переконайтеся, що ви відкриваєте потрібну програму на своїй книзі", "ledger_please_enable_bluetooth": "Будь ласка, ввімкніть Bluetooth виявити свою книгу", "light_theme": "Світла", + "litecoin_enable_mweb_sync": "Увімкнути сканування MWEB", + "litecoin_mweb": "Мвеб", + "litecoin_mweb_always_scan": "Встановити mweb завжди сканувати", + "litecoin_mweb_description": "MWEB - це новий протокол, який приносить швидкі, дешевші та більш приватні транзакції Litecoin", + "litecoin_mweb_dismiss": "Звільнити", + "litecoin_mweb_display_card": "Показати карту MWeb", + "litecoin_mweb_enable_later": "Ви можете знову ввімкнути MWEB в налаштуваннях дисплея.", + "litecoin_mweb_pegin": "Подякувати", + "litecoin_mweb_pegout": "Подякувати", + "litecoin_mweb_scanning": "Сканування Mweb", + "litecoin_mweb_settings": "Налаштування MWEB", + "litecoin_mweb_warning": "Використання MWEB спочатку завантажить ~ 600 Мб даних і може зайняти до 30 хвилин залежно від швидкості мережі. Ці початкові дані завантажуються лише один раз і будуть доступні для всіх гаманців Litecoin", + "litecoin_what_is_mweb": "Що таке mweb?", + "live_fee_rates": "Ставки плати за живий через API", "load_more": "Завантажити ще", "loading_your_wallet": "Завантаження гаманця", "login": "Логін", @@ -383,6 +408,8 @@ "monero_light_theme": "Легка тема Monero", "moonpay_alert_text": "Значення суми має бути більшим або дорівнювати ${minAmount} ${fiatCurrency}", "more_options": "Більше параметрів", + "mweb_confirmed": "Підтвердив Mweb", + "mweb_unconfirmed": "Неперевірений MWEB", "name": "Ім'я", "nano_current_rep": "Поточний представник", "nano_gpt_thanks_message": "Дякуємо за використання наногпта! Не забудьте повернутися до браузера після завершення транзакції!", @@ -412,7 +439,7 @@ "node_test": "Тест", "nodes": "Вузли", "nodes_list_reset_to_default_message": "Ви впевнені, що хочете скинути до налаштувань за замовченням?", - "none_of_selected_providers_can_exchange": "Жоден із вибраних провайдерів не може здійснити цей обмін", + "none_of_selected_providers_can_exchange": "Жоден із вибраних постачальників не може зробити цей своп", "noNFTYet": "NFT ще немає", "normal": "нормальний", "note_optional": "Примітка (необов’язково)", @@ -442,6 +469,7 @@ "overwrite_amount": "Overwrite amount", "pairingInvalidEvent": "Недійсна подія сполучення", "passphrase": "Пасофрази (необов’язково)", + "passphrases_doesnt_match": "Пасофрази не відповідають, спробуйте ще раз", "password": "Пароль", "paste": "Вставити", "pause_wallet_creation": "Можливість створення гаманця Haven зараз призупинено.", @@ -594,8 +622,11 @@ "seed_share": "Поділитися мнемонічною фразою", "seed_title": "Мнемонічна фраза", "seedtype": "Насіннєвий тип", + "seedtype_alert_content": "Спільний доступ до інших гаманців можливе лише за допомогою BIP39 Seedtype.", + "seedtype_alert_title": "Попередження насінника", "seedtype_legacy": "Спадщина (25 слів)", "seedtype_polyseed": "Полісей (16 слів)", + "seedtype_wownero": "Влонеро (14 слів)", "select_backup_file": "Виберіть файл резервної копії", "select_buy_provider_notice": "Виберіть постачальника купівлі вище. Ви можете пропустити цей екран, встановивши свого постачальника купівлі за замовчуванням у налаштуваннях додатків.", "select_destination": "Виберіть місце призначення для файлу резервної копії.", @@ -606,6 +637,7 @@ "send": "Відправити", "send_address": "${cryptoCurrency} адреса", "send_amount": "Сума:", + "send_change_to_you": "Зміна, для вас:", "send_creating_transaction": "Створити транзакцію", "send_error_currency": "Валюта може містити тільки цифри", "send_error_minimum_value": "Мінімальна сума 0.01", @@ -659,6 +691,7 @@ "setup_your_debit_card": "Налаштуйте свою дебетову картку", "share": "Поділіться", "share_address": "Поділитися адресою", + "shared_seed_wallet_groups": "Спільні групи насіннєвих гаманців", "show_details": "Показати деталі", "show_keys": "Показати мнемонічну фразу/ключі", "show_market_place": "Відображати маркетплейс", @@ -672,6 +705,7 @@ "signature_invalid_error": "Підпис не є дійсним для наведеного повідомлення", "signTransaction": "Підписати транзакцію", "signup_for_card_accept_terms": "Зареєструйтеся на картку та прийміть умови.", + "silent_payment": "Мовчазний платіж", "silent_payments": "Мовчазні платежі", "silent_payments_always_scan": "Встановити мовчазні платежі завжди сканувати", "silent_payments_disclaimer": "Нові адреси - це не нові ідентичності. Це повторне використання існуючої ідентичності з іншою етикеткою.", @@ -682,6 +716,7 @@ "silent_payments_scanned_tip": "Сканований на підказку! (${tip})", "silent_payments_scanning": "Мовчазні платежі сканування", "silent_payments_settings": "Налаштування мовчазних платежів", + "single_seed_wallets_group": "Поодинокі насінні гаманці", "slidable": "Розсувний", "sort_by": "Сортувати за", "spend_key_private": "Приватний ключ витрати", @@ -704,12 +739,13 @@ "switchToEVMCompatibleWallet": "Перейдіть на гаманець, сумісний з EVM, і повторіть спробу (Ethereum, Polygon)", "symbol": "символ", "sync_all_wallets": "Синхронізувати всі гаманці", + "sync_status_attempting_scan": "Спроба сканування", "sync_status_attempting_sync": "СПРОБА СИНХРОНІЗАЦІЇ", "sync_status_connected": "ПІДКЛЮЧЕНО", "sync_status_connecting": "ПІДКЛЮЧЕННЯ", "sync_status_failed_connect": "ВІДКЛЮЧЕНО", "sync_status_not_connected": "НЕ ПІДКЛЮЧЕННИЙ", - "sync_status_starting_scan": "Початок сканування", + "sync_status_starting_scan": "Початок сканування (від ${height})", "sync_status_starting_sync": "ПОЧАТОК СИНХРОНІЗАЦІЇ", "sync_status_syncronized": "СИНХРОНІЗОВАНИЙ", "sync_status_syncronizing": "СИНХРОНІЗАЦІЯ", @@ -796,6 +832,7 @@ "trusted": "довіряють", "tx_commit_exception_no_dust_on_change": "Транзакція відхилена цією сумою. За допомогою цих монет ви можете надіслати ${min} без змін або ${max}, що повертає зміни.", "tx_commit_failed": "Транзакційна комісія не вдалося. Будь ласка, зв'яжіться з підтримкою.", + "tx_commit_failed_no_peers": "Транзакція не вдалося транслювати, спробуйте ще раз за секунду або близько того", "tx_invalid_input": "Ви використовуєте неправильний тип введення для цього типу оплати", "tx_no_dust_exception": "Угода відхиляється, відправивши суму занадто мала. Будь ласка, спробуйте збільшити суму.", "tx_not_enough_inputs_exception": "Недостатньо доступних входів. Виберіть більше під контролем монети", @@ -840,8 +877,16 @@ "view_transaction_on": "View Transaction on ", "voting_weight": "Вага голосування", "waitFewSecondForTxUpdate": "Будь ласка, зачекайте кілька секунд, поки транзакція відобразиться в історії транзакцій", + "wallet_group": "Група гаманців", + "wallet_group_description_four": "створити гаманець з абсолютно новим насінням.", + "wallet_group_description_one": "У гаманці тортів ви можете створити a", + "wallet_group_description_three": "Щоб побачити наявні гаманці та/або екран групи гаманців. Або вибрати", + "wallet_group_description_two": "Вибираючи існуючий гаманець, щоб поділитися насінням. Кожна група гаманця може містити один гаманець кожного типу валюти. \n\n Ви можете вибрати", + "wallet_group_empty_state_text_one": "Схоже, у вас немає сумісних груп гаманця !\n\n Торкніться", + "wallet_group_empty_state_text_two": "нижче, щоб зробити новий.", "wallet_keys": "Мнемонічна фраза/ключі гаманця", "wallet_list_create_new_wallet": "Створити новий гаманець", + "wallet_list_edit_group_name": "Назва групи редагування", "wallet_list_edit_wallet": "Редагувати гаманець", "wallet_list_failed_to_load": "Помилка при завантаженні ${wallet_name} гаманця. ${error}", "wallet_list_failed_to_remove": "Помилка при видаленні ${wallet_name} гаманця. ${error}", diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index e54a0db34..0a136d140 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -36,6 +36,7 @@ "agree": "متفق", "agree_and_continue": "اتفاق کریں اور جاری رکھیں", "agree_to": "اکاؤنٹ بنا کر آپ اس سے اتفاق کرتے ہیں۔", + "alert_notice": "نوٹس", "all": "تمام", "all_trades": "تمام تجارت", "all_transactions": "تمام لین دین", @@ -113,7 +114,7 @@ "change_currency": "کرنسی تبدیل کریں", "change_current_node": "کیا آپ یقینی طور پر موجودہ نوڈ کو ${node} میں تبدیل کرنا چاہتے ہیں؟", "change_current_node_title": "موجودہ نوڈ کو تبدیل کریں۔", - "change_exchange_provider": "ایکسچینج فراہم کنندہ کو تبدیل کریں۔", + "change_exchange_provider": "تبادلہ فراہم کرنے والے کو تبدیل کریں", "change_language": "زبان تبدیل کریں", "change_language_to": "زبان کو ${language} میں تبدیل کریں؟", "change_password": "پاس ورڈ تبدیل کریں", @@ -130,6 +131,7 @@ "choose_one": "ایک کا انتخاب کریں", "choose_relay": "۔ﮟﯾﺮﮐ ﺏﺎﺨﺘﻧﺍ ﺎﮐ ﮯﻠﯾﺭ ﮯﯿﻟ ﮯﮐ ﮯﻧﺮﮐ ﻝﺎﻤﻌﺘﺳﺍ ﻡﺮﮐ ﮦﺍﺮﺑ", "choose_wallet_currency": "براہ کرم والیٹ کرنسی کا انتخاب کریں:", + "choose_wallet_group": "پرس گروپ کا انتخاب کریں", "clear": "صاف", "clearnet_link": "کلیرنیٹ لنک", "close": "بند کریں", @@ -142,6 +144,7 @@ "confirm_delete_wallet": "اس کارروائی سے یہ پرس حذف ہو جائے گا۔ کیا آپ جاری رکھنا چاہتے ہیں؟", "confirm_fee_deduction": "فیس میں کٹوتی کی تصدیق کریں", "confirm_fee_deduction_content": "کیا آپ آؤٹ پٹ سے فیس کم کرنے پر راضی ہیں؟", + "confirm_passphrase": "پاسفریز کی تصدیق کریں", "confirm_sending": "بھیجنے کی تصدیق کریں۔", "confirm_silent_payments_switch_node": "آپ کا موجودہ نوڈ خاموش ادائیگیوں کی حمایت نہیں کرتا ہے۔", "confirmations": "تصدیقات", @@ -160,6 +163,7 @@ "contact_name": "رابطے کا نام", "contact_support": "سپورٹ سے رابطہ کریں۔", "continue_text": "جاری رہے", + "contract_warning": "اس معاہدے کے پتے کو ممکنہ طور پر جعلی قرار دیا گیا ہے۔ براہ کرم احتیاط کے ساتھ کارروائی کریں۔", "contractName": "ﻡﺎﻧ ﺎﮐ ﮦﺪﮨﺎﻌﻣ", "contractSymbol": "ﺖﻣﻼﻋ ﯽﮐ ﮦﺪﮨﺎﻌﻣ", "copied_key_to_clipboard": "${key} کو کلپ بورڈ پر کاپی کیا گیا۔", @@ -175,6 +179,7 @@ "create_invoice": "انوائس بنائیں", "create_new": "نیا والیٹ بنائیں", "create_new_account": "نیا اکاؤنٹ بنانے", + "create_new_seed": "نیا بیج بنائیں", "creating_new_wallet": "نیا پرس بنانا", "creating_new_wallet_error": "خرابی: ${description}", "creation_date": "بنانے کی تاریخ", @@ -195,6 +200,7 @@ "delete_wallet_confirm_message": "کیا آپ واقعی ${wallet_name} والیٹ کو حذف کرنا چاہتے ہیں؟", "deleteConnectionConfirmationPrompt": "۔ﮟﯿﮨ ﮯﺘﮨﺎﭼ ﺎﻧﺮﮐ ﻑﺬﺣ ﻮﮐ ﻦﺸﮑﻨﮐ ﭖﺁ ﮧﮐ ﮯﮨ ﻦﯿﻘﯾ ﻮﮐ ﭖﺁ ﺎﯿﮐ", "denominations": "فرق", + "derivationpath": "مشتق راستہ", "descending": "اترتے ہوئے", "description": "ﻞﯿﺼﻔﺗ", "destination_tag": "منزل کا ٹیگ:", @@ -207,6 +213,7 @@ "disable_buy": "خرید ایکشن کو غیر فعال کریں۔", "disable_cake_2fa": "کیک 2FA کو غیر فعال کریں۔", "disable_exchange": "تبادلے کو غیر فعال کریں۔", + "disable_fee_api_warning": "اس کو بند کرنے سے ، کچھ معاملات میں فیس کی شرح غلط ہوسکتی ہے ، لہذا آپ اپنے لین دین کے لئے فیسوں کو زیادہ ادائیگی یا ادائیگی ختم کرسکتے ہیں۔", "disable_fiat": "فیاٹ کو غیر فعال کریں۔", "disable_sell": "فروخت کی کارروائی کو غیر فعال کریں۔", "disableBatteryOptimization": "بیٹری کی اصلاح کو غیر فعال کریں", @@ -228,8 +235,10 @@ "edit_token": "ٹوکن میں ترمیم کریں۔", "electrum_address_disclaimer": "جب بھی آپ ایک کا استعمال کرتے ہیں تو ہم نئے پتے تیار کرتے ہیں، لیکن پچھلے پتے کام کرتے رہتے ہیں۔", "email_address": "ای میل اڈریس", + "enable": "قابل بنائیں", + "enable_mempool_api": "درست فیسوں اور تاریخوں کے لئے میمپول API", "enable_replace_by_fee": "فی فیس کو تبدیل کریں", - "enable_silent_payments_scanning": "خاموش ادائیگیوں کو اسکیننگ کے قابل بنائیں", + "enable_silent_payments_scanning": "خاموش ادائیگیوں کو اسکین کرنا شروع کریں ، جب تک کہ نوک نہ پہنچ جائے", "enabled": "فعال", "enter_amount": "رقم درج کریں۔", "enter_backup_password": "یہاں بیک اپ پاس ورڈ درج کریں۔", @@ -273,7 +282,7 @@ "event": "ﺐﯾﺮﻘﺗ", "events": "ﺕﺎﺒﯾﺮﻘﺗ", "exchange": "تبادلہ", - "exchange_incorrect_current_wallet_for_xmr": "اگر آپ اپنے Cake والیٹ Monero بیلنس سے XMR کا تبادلہ کرنا چاہتے ہیں، تو براہ کرم پہلے اپنے Monero والیٹ پر جائیں۔", + "exchange_incorrect_current_wallet_for_xmr": "اگر آپ اپنے کیک پرس مونیرو بیلنس سے XMR تبدیل کرنا چاہتے ہیں تو ، براہ کرم پہلے اپنے مونیرو پرس میں جائیں۔", "exchange_new_template": "نیا سانچہ", "exchange_provider_unsupported": "${providerName} اب تعاون نہیں کیا جاتا ہے!", "exchange_result_confirm": "تصدیق کو دبانے سے، آپ اپنے بٹوے سے ${fetchingLabel} ${from} بھیجیں گے جسے ${walletName} کہتے ہیں نیچے دکھائے گئے پتے پر۔ یا آپ اپنے بیرونی والیٹ سے نیچے دیئے گئے پتے/QR کوڈ پر بھیج سکتے ہیں۔\\n\\nجاری رکھنے کے لیے براہ کرم تصدیق کو دبائیں یا رقم تبدیل کرنے کے لیے واپس جائیں۔", @@ -291,6 +300,8 @@ "failed_authentication": "ناکام تصدیق۔ ${state_error}", "faq": "عمومی سوالات", "features": "خصوصیات", + "fee_less_than_min": "منتخب فیس کم سے کم سے کم ہے ، براہ کرم لین دین بھیجنے کے قابل فیس میں اضافہ کریں", + "fee_rate": "فیس کی شرح", "fetching": "لا رہا ہے۔", "fiat_api": "Fiat API", "fiat_balance": "فیاٹ بیلنس", @@ -298,7 +309,7 @@ "fill_code": "براہ کرم اپنے ای میل پر فراہم کردہ تصدیقی کوڈ کو پُر کریں۔", "filter_by": "کی طرف سے فلٹر", "first_wallet_text": "Monero، Bitcoin، Ethereum، Litecoin، اور Haven کے لیے زبردست پرس", - "fixed_pair_not_supported": "یہ مقررہ جوڑا منتخب کردہ تبادلے کے ساتھ تعاون یافتہ نہیں ہے۔", + "fixed_pair_not_supported": "یہ فکسڈ جوڑی منتخب شدہ تبادلہ خدمات کے ساتھ تعاون یافتہ نہیں ہے", "fixed_rate": "مقررہ شرح", "fixed_rate_alert": "فکسڈ ریٹ موڈ چیک ہونے پر آپ وصولی رقم درج کر سکیں گے۔ کیا آپ فکسڈ ریٹ موڈ پر سوئچ کرنا چاہتے ہیں؟", "forgot_password": "پاسورڈ بھول گے", @@ -354,6 +365,20 @@ "ledger_error_wrong_app": "براہ کرم یقینی بنائیں کہ آپ اپنے لیجر پر صحیح ایپ کو کھولتے ہیں", "ledger_please_enable_bluetooth": "براہ کرم بلوٹوتھ کو اپنے لیجر کا پتہ لگانے کے لئے اہل بنائیں", "light_theme": "روشنی", + "litecoin_enable_mweb_sync": "MWEB اسکیننگ کو فعال کریں", + "litecoin_mweb": "MWEB", + "litecoin_mweb_always_scan": "MWEB ہمیشہ اسکیننگ سیٹ کریں", + "litecoin_mweb_description": "MWEB ایک نیا پروٹوکول ہے جو لیٹیکوئن میں تیز ، سستا اور زیادہ نجی لین دین لاتا ہے", + "litecoin_mweb_dismiss": "خارج", + "litecoin_mweb_display_card": "MWEB کارڈ دکھائیں", + "litecoin_mweb_enable_later": "آپ ڈسپلے کی ترتیبات کے تحت MWEB کو دوبارہ فعال کرنے کا انتخاب کرسکتے ہیں۔", + "litecoin_mweb_pegin": "پیگ میں", + "litecoin_mweb_pegout": "پیگ آؤٹ", + "litecoin_mweb_scanning": "MWEB اسکیننگ", + "litecoin_mweb_settings": "MWEB کی ترتیبات", + "litecoin_mweb_warning": "MWEB کا استعمال ابتدائی طور پر m 600mb ڈیٹا ڈاؤن لوڈ کرے گا ، اور نیٹ ورک کی رفتار کے لحاظ سے 30 منٹ تک کا وقت لگ سکتا ہے۔ یہ ابتدائی اعداد و شمار صرف ایک بار ڈاؤن لوڈ کریں گے اور تمام لیٹیکوئن بٹوے کے لئے دستیاب ہوں گے", + "litecoin_what_is_mweb": "MWEB کیا ہے؟", + "live_fee_rates": "API کے ذریعے براہ راست فیس کی شرح", "load_more": "مزید لوڈ کریں", "loading_your_wallet": "آپ کا بٹوہ لوڈ ہو رہا ہے۔", "login": "لاگ ان کریں", @@ -383,6 +408,8 @@ "monero_light_theme": "مونیرو لائٹ تھیم", "moonpay_alert_text": "رقم کی قدر ${minAmount} ${fiatCurrency} کے برابر یا زیادہ ہونی چاہیے۔", "more_options": "مزید زرائے", + "mweb_confirmed": "تصدیق شدہ MWEB", + "mweb_unconfirmed": "غیر مصدقہ MWEB", "name": "ﻡﺎﻧ", "nano_current_rep": "موجودہ نمائندہ", "nano_gpt_thanks_message": "نانوگپٹ استعمال کرنے کا شکریہ! اپنے لین دین کی تکمیل کے بعد براؤزر کی طرف واپس جانا یاد رکھیں!", @@ -412,7 +439,7 @@ "node_test": "پرکھ", "nodes": "نوڈس", "nodes_list_reset_to_default_message": "کیا آپ واقعی ترتیبات کو ڈیفالٹ پر دوبارہ ترتیب دینا چاہتے ہیں؟", - "none_of_selected_providers_can_exchange": "منتخب فراہم کنندگان میں سے کوئی بھی یہ تبادلہ نہیں کر سکتا", + "none_of_selected_providers_can_exchange": "منتخب کردہ کوئی بھی فراہم کنندہ یہ تبادلہ نہیں کرسکتا", "noNFTYet": "۔ﮟﯿﮨ ﮟﯿﮩﻧ NFTs ﯽﺋﻮﮐ ﮏﺗ ﯽﮭﺑﺍ", "normal": "نارمل", "note_optional": "نوٹ (اختیاری)", @@ -444,6 +471,7 @@ "overwrite_amount": "رقم کو اوور رائٹ کریں۔", "pairingInvalidEvent": "ﭧﻧﻮﯾﺍ ﻂﻠﻏ ﺎﻧﺎﻨﺑ ﺍﮌﻮﺟ", "passphrase": "پاسفریز (اختیاری)", + "passphrases_doesnt_match": "پاسفریز مماثل نہیں ہیں ، براہ کرم دوبارہ کوشش کریں", "password": "پاس ورڈ", "paste": "چسپاں کریں۔", "pause_wallet_creation": "Haven Wallet ۔ﮯﮨ ﻑﻮﻗﻮﻣ ﻝﺎﺤﻟﺍ ﯽﻓ ﺖﯿﻠﮨﺍ ﯽﮐ ﮯﻧﺎﻨﺑ", @@ -595,8 +623,11 @@ "seed_share": "بیج بانٹیں۔", "seed_title": "بیج", "seedtype": "سیڈ ٹائپ", + "seedtype_alert_content": "دوسرے بٹوے کے ساتھ بیجوں کا اشتراک صرف BIP39 بیج ٹائپ کے ساتھ ہی ممکن ہے۔", + "seedtype_alert_title": "سیڈ ٹائپ الرٹ", "seedtype_legacy": "میراث (25 الفاظ)", "seedtype_polyseed": "پالیسیڈ (16 الفاظ)", + "seedtype_wownero": "واونرو (14 الفاظ)", "select_backup_file": "بیک اپ فائل کو منتخب کریں۔", "select_buy_provider_notice": "اوپر خریدنے والا خریدنے والا منتخب کریں۔ آپ ایپ کی ترتیبات میں اپنے پہلے سے طے شدہ خریدنے والے کو ترتیب دے کر اس اسکرین کو چھوڑ سکتے ہیں۔", "select_destination": "۔ﮟﯾﺮﮐ ﺏﺎﺨﺘﻧﺍ ﺎﮐ ﻝﺰﻨﻣ ﮯﯿﻟ ﮯﮐ ﻞﺋﺎﻓ ﭖﺍ ﮏﯿﺑ ﻡﺮﮐ ﮦﺍﺮﺑ", @@ -607,6 +638,7 @@ "send": "بھیجیں", "send_address": "${cryptoCurrency} پتہ", "send_amount": "رقم:", + "send_change_to_you": "آپ کو تبدیل کریں:", "send_creating_transaction": "لین دین کی تخلیق", "send_error_currency": "کرنسی صرف نمبروں پر مشتمل ہو سکتی ہے۔", "send_error_minimum_value": "رقم کی کم از کم قیمت 0.01 ہے۔", @@ -660,6 +692,7 @@ "setup_your_debit_card": "اپنا ڈیبٹ کارڈ ترتیب دیں۔", "share": "بانٹیں", "share_address": "پتہ شیئر کریں۔", + "shared_seed_wallet_groups": "مشترکہ بیج پرس گروپ", "show_details": "تفصیلات دکھائیں", "show_keys": "بیج / چابیاں دکھائیں۔", "show_market_place": "بازار دکھائیں۔", @@ -673,6 +706,7 @@ "signature_invalid_error": "دستخط دیئے گئے پیغام کے لئے درست نہیں ہے", "signTransaction": "۔ﮟﯾﺮﮐ ﻂﺨﺘﺳﺩ ﺮﭘ ﻦﯾﺩ ﻦﯿﻟ", "signup_for_card_accept_terms": "کارڈ کے لیے سائن اپ کریں اور شرائط کو قبول کریں۔", + "silent_payment": "خاموش ادائیگی", "silent_payments": "خاموش ادائیگی", "silent_payments_always_scan": "خاموش ادائیگی ہمیشہ اسکیننگ کریں", "silent_payments_disclaimer": "نئے پتے نئی شناخت نہیں ہیں۔ یہ ایک مختلف لیبل کے ساتھ موجودہ شناخت کا دوبارہ استعمال ہے۔", @@ -683,6 +717,7 @@ "silent_payments_scanned_tip": "نوکنے کے لئے اسکین! (${tip})", "silent_payments_scanning": "خاموش ادائیگی اسکیننگ", "silent_payments_settings": "خاموش ادائیگی کی ترتیبات", + "single_seed_wallets_group": "سنگل بیج کے بٹوے", "slidable": "سلائیڈ ایبل", "sort_by": "ترتیب دیں", "spend_key_private": "خرچ کی کلید (نجی)", @@ -705,12 +740,13 @@ "switchToEVMCompatibleWallet": "(Ethereum, Polygon) ﮟﯾﺮﮐ ﺶﺷﻮﮐ ﮦﺭﺎﺑﻭﺩ ﺭﻭﺍ ﮟﯾﺮﮐ ﭻﺋﻮﺳ ﺮﭘ ﭧﯿﻟﺍﻭ ﮯﻟﺍﻭ ﮯﻨﮭﮐﺭ ﺖﻘﺑﺎﻄﻣ ", "symbol": "ﺖﻣﻼﻋ", "sync_all_wallets": "تمام بٹوے کو ہم آہنگ کریں", + "sync_status_attempting_scan": "اسکین کی کوشش کرنا", "sync_status_attempting_sync": "ہم آہنگی کی کوشش کر رہا ہے۔", "sync_status_connected": "منسلک", "sync_status_connecting": "جڑ رہا ہے۔", "sync_status_failed_connect": "منقطع", "sync_status_not_connected": "منسلک نہیں", - "sync_status_starting_scan": "اسکین شروع کرنا", + "sync_status_starting_scan": "اسکین شروع کرنا (${height})", "sync_status_starting_sync": "مطابقت پذیری شروع کر رہا ہے۔", "sync_status_syncronized": "مطابقت پذیر", "sync_status_syncronizing": "مطابقت پذیری", @@ -797,6 +833,7 @@ "trusted": "قابل اعتماد", "tx_commit_exception_no_dust_on_change": "اس رقم سے لین دین کو مسترد کردیا گیا ہے۔ ان سککوں کے ذریعہ آپ بغیر کسی تبدیلی کے ${min} یا ${max} بھیج سکتے ہیں جو لوٹتے ہیں۔", "tx_commit_failed": "ٹرانزیکشن کمٹ ناکام ہوگیا۔ براہ کرم سپورٹ سے رابطہ کریں۔", + "tx_commit_failed_no_peers": "ٹرانزیکشن نشر کرنے میں ناکام ، براہ کرم ایک سیکنڈ یا اس میں دوبارہ کوشش کریں", "tx_invalid_input": "آپ اس قسم کی ادائیگی کے لئے غلط ان پٹ کی قسم استعمال کررہے ہیں", "tx_no_dust_exception": "لین دین کو بہت چھوٹی رقم بھیج کر مسترد کردیا جاتا ہے۔ براہ کرم رقم میں اضافہ کرنے کی کوشش کریں۔", "tx_not_enough_inputs_exception": "کافی ان پٹ دستیاب نہیں ہے۔ براہ کرم سکے کے کنٹرول میں مزید منتخب کریں", @@ -841,8 +878,16 @@ "view_transaction_on": "لین دین دیکھیں آن", "voting_weight": "ووٹ کا وزن", "waitFewSecondForTxUpdate": "۔ﮟﯾﺮﮐ ﺭﺎﻈﺘﻧﺍ ﺎﮐ ﮉﻨﮑﯿﺳ ﺪﻨﭼ ﻡﺮﮐ ﮦﺍﺮﺑ ﮯﯿﻟ ﮯﮐ ﮯﻧﺮﮐ ﯽﺳﺎﮑﻋ ﯽﮐ ﻦﯾﺩ ﻦﯿﻟ ﮟﯿﻣ ﺦﯾﺭﺎﺗ ﯽﮐ ﻦ", + "wallet_group": "پرس گروپ", + "wallet_group_description_four": "مکمل طور پر نئے بیج کے ساتھ پرس بنانے کے ل.", + "wallet_group_description_one": "کیک پرس میں ، آپ بنا سکتے ہیں", + "wallet_group_description_three": "دستیاب بٹوے اور/یا پرس گروپوں کی اسکرین کو دیکھنے کے لئے۔ یا منتخب کریں", + "wallet_group_description_two": "بیج کے ساتھ بانٹنے کے لئے موجودہ پرس کا انتخاب کرکے۔ ہر بٹوے گروپ میں ہر کرنسی کی قسم کا ایک بٹوے شامل ہوسکتا ہے۔ \n\n آپ منتخب کرسکتے ہیں", + "wallet_group_empty_state_text_one": "ایسا لگتا ہے کہ آپ کے پاس کوئی مطابقت پذیر والیٹ گروپس نہیں ہیں !\n\n نل", + "wallet_group_empty_state_text_two": "ایک نیا بنانے کے لئے ذیل میں.", "wallet_keys": "بٹوے کے بیج / چابیاں", "wallet_list_create_new_wallet": "نیا والیٹ بنائیں", + "wallet_list_edit_group_name": "گروپ کے نام میں ترمیم کریں", "wallet_list_edit_wallet": "بٹوے میں ترمیم کریں۔", "wallet_list_failed_to_load": "${wallet_name} والیٹ لوڈ کرنے میں ناکام۔ ${error}", "wallet_list_failed_to_remove": "${wallet_name} والیٹ کو ہٹانے میں ناکام۔ ${error}", diff --git a/res/values/strings_vi.arb b/res/values/strings_vi.arb new file mode 100644 index 000000000..8d28d48a2 --- /dev/null +++ b/res/values/strings_vi.arb @@ -0,0 +1,934 @@ +{ + "about_cake_pay": "Cake Pay cho phép bạn dễ dàng mua thẻ quà tặng bằng tài sản ảo, có thể sử dụng ngay lập tức tại hơn 150.000 cửa hàng tại Hoa Kỳ.", + "account": "Tài khoản", + "accounts": "Tài khoản", + "accounts_subaddresses": "Tài khoản và địa chỉ phụ", + "activate": "Kích hoạt", + "active": "Đang hoạt động", + "active_cards": "Thẻ đang hoạt động", + "activeConnectionsPrompt": "Các kết nối đang hoạt động sẽ xuất hiện ở đây", + "add": "Thêm", + "add_contact": "Thêm liên hệ", + "add_contact_to_address_book": "Bạn có muốn thêm liên hệ này vào sổ địa chỉ không?", + "add_custom_node": "Thêm nút tùy chỉnh mới", + "add_custom_redemption": "Thêm đổi thưởng tùy chỉnh", + "add_fund_to_card": "Thêm tiền trả trước vào thẻ (tối đa ${value})", + "add_new_node": "Thêm nút mới", + "add_new_word": "Thêm từ mới", + "add_receiver": "Thêm người nhận khác (tùy chọn)", + "add_secret_code": "Hoặc, thêm mã bí mật này vào ứng dụng xác thực", + "add_tip": "Thêm tiền boa", + "add_token_disclaimer_check": "Tôi đã xác nhận địa chỉ hợp đồng token và thông tin bằng nguồn đáng tin cậy. Thêm thông tin sai hoặc độc hại có thể dẫn đến mất tiền.", + "add_token_warning": "Không chỉnh sửa hoặc thêm token theo yêu cầu của kẻ lừa đảo.\nLuôn xác nhận địa chỉ token từ các nguồn đáng tin cậy!", + "add_value": "Thêm giá trị", + "address": "Địa chỉ", + "address_book": "Sổ địa chỉ", + "address_book_menu": "Sổ địa chỉ", + "address_detected": "Địa chỉ được phát hiện", + "address_from_domain": "Địa chỉ này đến từ ${domain} trên Unstoppable Domains", + "address_from_yat": "Địa chỉ này đến từ ${emoji} trên Yat", + "address_label": "Nhãn địa chỉ", + "address_remove_contact": "Xóa liên hệ", + "address_remove_content": "Bạn có chắc chắn muốn xóa liên hệ đã chọn không?", + "addresses": "Địa chỉ", + "advanced_settings": "Cài đặt nâng cao", + "aggressive": "Tích cực", + "agree": "Đồng ý", + "agree_and_continue": "Đồng ý & Tiếp tục", + "agree_to": "Bằng cách tạo tài khoản, bạn đồng ý với ", + "alert_notice": "Để ý", + "all": "TẤT CẢ", + "all_trades": "Tất cả giao dịch", + "all_transactions": "Tất cả giao dịch", + "alphabetical": "Theo thứ tự chữ cái", + "already_have_account": "Bạn đã có tài khoản?", + "always": "Luôn luôn", + "amount": "Số tiền: ", + "amount_is_below_minimum_limit": "Số dư của bạn sau khi trừ phí sẽ thấp hơn số tiền tối thiểu cần thiết cho giao dịch (${min})", + "amount_is_estimate": "Số tiền nhận được là ước tính", + "amount_is_guaranteed": "Số tiền nhận được được đảm bảo", + "and": "và", + "anonpay_description": "Tạo ${type}. Người nhận có thể ${method} với bất kỳ loại tiền điện tử nào được hỗ trợ, và bạn sẽ nhận được tiền trong ví này.", + "apk_update": "Cập nhật APK", + "approve": "Phê duyệt", + "arrive_in_this_address": "${currency} ${tag} sẽ đến địa chỉ này", + "ascending": "Tăng dần", + "ask_each_time": "Hỏi mỗi lần", + "auth_store_ban_timeout": "thời gian cấm", + "auth_store_banned_for": "Bị cấm trong ", + "auth_store_banned_minutes": " phút", + "auth_store_incorrect_password": "Sai PIN", + "authenticated": "Đã xác thực", + "authentication": "Xác thực", + "auto_generate_addresses": "Tự động tạo địa chỉ", + "auto_generate_subaddresses": "Tự động tạo địa chỉ phụ", + "automatic": "Tự động", + "available_balance": "Số dư khả dụng", + "available_balance_description": "Số dư khả dụng hoặc số dư đã xác nhận là số tiền có thể chi tiêu ngay lập tức. Nếu tiền xuất hiện ở số dư thấp hơn nhưng không phải ở số dư cao hơn, bạn phải đợi một vài phút để các khoản tiền đến có thêm xác nhận từ mạng lưới. Sau khi có thêm xác nhận, chúng sẽ có thể chi tiêu.", + "avg_savings": "Tiết kiệm trung bình", + "awaitDAppProcessing": "Vui lòng đợi ứng dụng phi tập trung hoàn thành xử lý.", + "awaiting_payment_confirmation": "Đang chờ xác nhận thanh toán", + "background_sync_mode": "Chế độ đồng bộ nền", + "backup": "Sao lưu", + "backup_file": "Tập tin sao lưu", + "backup_password": "Mật khẩu sao lưu", + "balance": "Số dư", + "balance_page": "Trang số dư", + "bill_amount": "Số tiền hóa đơn", + "billing_address_info": "Nếu được yêu cầu địa chỉ thanh toán, hãy cung cấp địa chỉ giao hàng của bạn", + "biometric_auth_reason": "Quét vân tay để xác thực", + "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_remaining": "1 khối còn lại", + "Blocks_remaining": "${status} khối còn lại", + "bluetooth": "Bluetooth", + "bright_theme": "Sáng", + "bump_fee": "Tăng phí", + "buy": "Mua", + "buy_alert_content": "Hiện tại chúng tôi chỉ hỗ trợ mua Bitcoin, Ethereum, Litecoin, và Monero. Vui lòng tạo hoặc chuyển sang ví Bitcoin, Ethereum, Litecoin, hoặc Monero của bạn.", + "buy_bitcoin": "Mua Bitcoin", + "buy_now": "Mua ngay", + "buy_provider_unavailable": "Nhà cung cấp hiện không khả dụng.", + "buy_with": "Mua bằng", + "by_cake_pay": "bởi Cake Pay", + "cake_2fa_preset": "Thiết lập sẵn Cake 2FA", + "cake_dark_theme": "Chủ đề Cake tối", + "cake_pay_account_note": "Đăng ký chỉ với một địa chỉ email để xem và mua thẻ. Một số thẻ còn được giảm giá!", + "cake_pay_learn_more": "Mua và đổi thẻ quà tặng ngay trong ứng dụng!\nVuốt từ trái sang phải để tìm hiểu thêm.", + "cake_pay_save_order": "Thẻ sẽ được gửi đến email của bạn trong vòng 1 ngày làm việc \n Lưu mã Đơn hàng của bạn:", + "cake_pay_subtitle": "Mua thẻ trả trước toàn cầu và thẻ quà tặng", + "cake_pay_web_cards_subtitle": "Mua thẻ trả trước toàn cầu và thẻ quà tặng", + "cake_pay_web_cards_title": "Thẻ Cake Pay Web", + "cake_wallet": "Ví Cake", + "cakepay_prepaid_card": "Thẻ Ghi Nợ Trả Trước CakePay", + "camera_consent": "Máy ảnh của bạn sẽ được sử dụng để chụp hình nhằm mục đích xác minh danh tính bởi ${provider}. Vui lòng kiểm tra Chính sách quyền riêng tư của họ để biết thêm chi tiết.", + "camera_permission_is_required": "Cần có quyền truy cập máy ảnh. \nVui lòng bật nó từ cài đặt ứng dụng.", + "cancel": "Hủy", + "card_address": "Địa chỉ:", + "cardholder_agreement": "Thỏa thuận Chủ Thẻ", + "cards": "Thẻ", + "chains": "Chuỗi", + "change": "Thay đổi", + "change_backup_password_alert": "Các tệp sao lưu trước đây sẽ không còn khả dụng để nhập với mật khẩu sao lưu mới. Mật khẩu sao lưu mới chỉ được sử dụng cho các tệp sao lưu mới. Bạn có chắc chắn muốn thay đổi mật khẩu sao lưu không?", + "change_currency": "Thay đổi Tiền tệ", + "change_current_node": "Bạn có chắc chắn muốn thay đổi nút hiện tại sang ${node} không?", + "change_current_node_title": "Thay đổi nút hiện tại", + "change_exchange_provider": "Thay đổi nhà cung cấp hoán đổi", + "change_language": "Thay đổi ngôn ngữ", + "change_language_to": "Thay đổi ngôn ngữ sang ${language}?", + "change_password": "Thay đổi mật khẩu", + "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_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", + "choose_address": "\n\nVui lòng chọn địa chỉ:", + "choose_card_value": "Chọn giá trị thẻ", + "choose_derivation": "Chọn Đường dẫn Ví", + "choose_from_available_options": "Chọn từ các tùy chọn có sẵn:", + "choose_one": "Chọn một", + "choose_relay": "Vui lòng chọn một relay để sử dụng", + "choose_wallet_currency": "Vui lòng chọn tiền tệ của ví:", + "choose_wallet_group": "Chọn nhóm ví", + "clear": "Xóa", + "clearnet_link": "Liên kết Clearnet", + "close": "Đóng", + "coin_control": "Kiểm soát đồng xu (tùy chọn)", + "cold_or_recover_wallet": "Thêm ví lạnh hoặc khôi phục ví giấy", + "color_theme": "Chủ đề màu sắc", + "commit_transaction_amount_fee": "Cam kết giao dịch\nSố tiền: ${amount}\nPhí: ${fee}", + "confirm": "Xác nhận", + "confirm_delete_template": "Thao tác này sẽ xóa mẫu này. Bạn có muốn tiếp tục không?", + "confirm_delete_wallet": "Thao tác này sẽ xóa ví này. Bạn có muốn tiếp tục không?", + "confirm_fee_dedction_content": "Bạn có đồng ý trừ phí từ đầu ra không?", + "confirm_fee_deduction": "Xác nhận Khấu trừ Phí", + "confirm_fee_deduction_content": "Bạn có đồng ý khấu trừ phí từ đầu ra không?", + "confirm_passphrase": "Xác nhận cụm mật khẩu", + "confirm_sending": "Xác nhận gửi", + "confirm_silent_payments_switch_node": "Nút hiện tại của bạn không hỗ trợ thanh toán im lặng\\nCake Wallet sẽ chuyển sang một nút tương thích chỉ để quét", + "confirmations": "Xác nhận", + "confirmed": "Số dư đã xác nhận", + "confirmed_tx": "Đã xác nhận", + "congratulations": "Chúc mừng!", + "connect_an_existing_yat": "Kết nối Yat hiện có", + "connect_yats": "Kết nối Yats", + "connect_your_hardware_wallet": "Kết nối ví phần cứng của bạn bằng Bluetooth hoặc USB", + "connect_your_hardware_wallet_ios": "Kết nối ví phần cứng của bạn bằng Bluetooth", + "connection_sync": "Kết nối và đồng bộ hóa", + "connectWalletPrompt": "Kết nối ví của bạn với WalletConnect để thực hiện giao dịch", + "contact": "Liên hệ", + "contact_list_contacts": "Danh bạ", + "contact_list_wallets": "Ví của tôi", + "contact_name": "Tên liên hệ", + "contact_support": "Liên hệ Hỗ trợ", + "continue_text": "Tiếp tục", + "contract_warning": "Địa chỉ hợp đồng này đã được gắn cờ là có khả năng lừa đảo. Vui lòng xử lý một cách thận trọng.", + "contractName": "Tên Hợp đồng", + "contractSymbol": "Ký hiệu Hợp đồng", + "copied_key_to_clipboard": "Đã sao chép ${key} vào khay nhớ tạm", + "copied_to_clipboard": "Đã sao chép vào khay nhớ tạm", + "copy": "Sao chép", + "copy_address": "Sao chép Địa chỉ", + "copy_id": "Sao chép ID", + "copyWalletConnectLink": "Sao chép liên kết WalletConnect từ dApp và dán vào đây", + "countries": "Quốc gia", + "create_account": "Tạo tài khoản", + "create_backup": "Tạo sao lưu", + "create_donation_link": "Tạo liên kết quyên góp", + "create_invoice": "Tạo hóa đơn", + "create_new": "Tạo Ví Mới", + "create_new_account": "Tạo tài khoản mới", + "create_new_seed": "Tạo hạt giống mới", + "creating_new_wallet": "Đang tạo ví mới", + "creating_new_wallet_error": "Lỗi: ${description}", + "creation_date": "Ngày Tạo", + "custom": "Tùy chỉnh", + "custom_drag": "Tùy chỉnh (Giữ và Kéo)", + "custom_redeem_amount": "Số tiền Chuộc Tùy chỉnh", + "custom_value": "Giá trị Tùy chỉnh", + "dark_theme": "Tối", + "debit_card": "Thẻ Ghi Nợ", + "debit_card_terms": "Việc lưu trữ và sử dụng số thẻ thanh toán của bạn (và thông tin xác thực tương ứng với số thẻ thanh toán của bạn) trong ví điện tử này phải tuân theo Điều khoản và Điều kiện của thỏa thuận chủ thẻ hiện hành với tổ chức phát hành thẻ thanh toán, theo thời gian.", + "decimal_places_error": "Quá nhiều chữ số thập phân", + "decimals_cannot_be_zero": "Chữ số thập phân không thể là số không.", + "default_buy_provider": "Nhà cung cấp Mua mặc định", + "default_sell_provider": "Nhà cung cấp Bán mặc định", + "delete": "Xóa", + "delete_account": "Xóa Tài khoản", + "delete_wallet": "Xóa ví", + "delete_wallet_confirm_message": "Bạn có chắc chắn muốn xóa ví ${wallet_name} không?", + "deleteConnectionConfirmationPrompt": "Bạn có chắc chắn muốn xóa kết nối với", + "denominations": "Mệnh giá", + "derivationpath": "Đường dẫn Derivation", + "descending": "Giảm dần", + "description": "Mô tả", + "destination_tag": "Thẻ đích:", + "dfx_option_description": "Mua tiền điện tử bằng EUR & CHF. Dành cho khách hàng bán lẻ và doanh nghiệp tại Châu Âu", + "didnt_get_code": "Không nhận được mã?", + "digit_pin": "Mã PIN - số", + "digital_and_physical_card": "thẻ ghi nợ trả trước kỹ thuật số và vật lý", + "disable": "Vô hiệu hóa", + "disable_bulletin": "Vô hiệu hóa bản tin tình trạng dịch vụ", + "disable_buy": "Vô hiệu hóa chức năng mua", + "disable_cake_2fa": "Vô hiệu hóa 2FA Cake", + "disable_exchange": "Vô hiệu hóa chức năng trao đổi", + "disable_fee_api_warning": "Khi tắt chức năng này, tỉ lệ phí có thể không chính xác trong một số trường hợp, dẫn đến bạn trả quá hoặc không đủ phí cho giao dịch của mình.", + "disable_fiat": "Vô hiệu hóa tiền tệ fiat", + "disable_sell": "Vô hiệu hóa chức năng bán", + "disableBatteryOptimization": "Vô hiệu hóa Tối ưu hóa Pin", + "disableBatteryOptimizationDescription": "Bạn có muốn vô hiệu hóa tối ưu hóa pin để đồng bộ hóa nền hoạt động mượt mà hơn không?", + "disabled": "Đã vô hiệu hóa", + "discount": "Tiết kiệm ${value}%", + "display_settings": "Cài đặt hiển thị", + "displayable": "Có thể hiển thị", + "do_not_have_enough_gas_asset": "Bạn không có đủ ${currency} để thực hiện giao dịch với điều kiện mạng blockchain hiện tại. Bạn cần thêm ${currency} để trả phí mạng blockchain, ngay cả khi bạn đang gửi tài sản khác.", + "do_not_send": "Đừng gửi", + "do_not_share_warning_text": "Không chia sẻ điều này với bất kỳ ai, bao gồm cả bộ phận hỗ trợ.\n\nTài sản của bạn có thể và sẽ bị đánh cắp!", + "do_not_show_me": "Không hiển thị lại cho tôi", + "domain_looks_up": "Tra cứu tên miền", + "donation_link_details": "Chi tiết liên kết quyên góp", + "e_sign_consent": "Đồng ý Ký Điện tử", + "edit": "Chỉnh sửa", + "edit_backup_password": "Chỉnh sửa mật khẩu sao lưu", + "edit_node": "Chỉnh sửa nút", + "edit_token": "Chỉnh sửa token", + "electrum_address_disclaimer": "Chúng tôi tạo địa chỉ mới mỗi khi bạn sử dụng, nhưng các địa chỉ cũ vẫn tiếp tục hoạt động", + "email_address": "Địa chỉ Email", + "enable": "Cho phép", + "enable_mempool_api": "API Mempool cho các khoản phí và ngày chính xác", + "enable_replace_by_fee": "Bật Thay thế Bằng Phí", + "enable_silent_payments_scanning": "Bật quét thanh toán im lặng", + "enabled": "Đã bật", + "enter_amount": "Nhập số tiền", + "enter_backup_password": "Nhập mật khẩu sao lưu tại đây", + "enter_code": "Nhập mã", + "enter_seed_phrase": "Nhập cụm từ hạt giống của bạn", + "enter_totp_code": "Vui lòng nhập mã TOTP.", + "enter_wallet_password": "Nhập mật khẩu ví", + "enter_your_note": "Nhập ghi chú của bạn...", + "enter_your_pin": "Nhập mã PIN của bạn", + "enter_your_pin_again": "Nhập lại mã PIN của bạn", + "enterTokenID": "Nhập ID token", + "enterWalletConnectURI": "Nhập URI WalletConnect", + "error": "Lỗi", + "error_dialog_content": "Oops, chúng tôi gặp một số lỗi.\n\nVui lòng gửi báo cáo sự cố cho nhóm hỗ trợ của chúng tôi để cải thiện ứng dụng.", + "error_text_account_name": "Tên tài khoản chỉ được chứa chữ cái, số\nvà phải từ 1 đến 15 ký tự", + "error_text_address": "Địa chỉ ví phải tương ứng với loại tiền điện tử", + "error_text_amount": "Số tiền chỉ được chứa số", + "error_text_contact_name": "Tên liên hệ không được chứa ký tự ` , ' \"\nvà phải từ 1 đến 32 ký tự", + "error_text_crypto_currency": "Số chữ số thập phân phải ít hơn hoặc bằng 12", + "error_text_fiat": "Giá trị số tiền không được vượt quá số dư khả dụng.\nSố chữ số thập phân phải ít hơn hoặc bằng 2", + "error_text_input_above_maximum_limit": "Số tiền vượt quá giới hạn tối đa", + "error_text_input_below_minimum_limit": "Số tiền dưới mức tối thiểu", + "error_text_keys": "Khóa ví chỉ được chứa 64 ký tự dạng hex", + "error_text_limits_loading_failed": "Giao dịch cho ${provider} không được tạo. Tải giới hạn không thành công", + "error_text_maximum_limit": "Giao dịch cho ${provider} không được tạo. Số tiền lớn hơn mức tối đa: ${max} ${currency}", + "error_text_minimal_limit": "Giao dịch cho ${provider} không được tạo. Số tiền nhỏ hơn mức tối thiểu: ${min} ${currency}", + "error_text_node_address": "Vui lòng nhập địa chỉ iPv4", + "error_text_node_port": "Cổng nút chỉ được chứa các số từ 0 đến 65535", + "error_text_node_proxy_address": "Vui lòng nhập <địa chỉ IPv4>:, ví dụ 127.0.0.1:9050", + "error_text_payment_id": "ID thanh toán chỉ được chứa từ 16 đến 64 ký tự dạng hex", + "error_text_subaddress_name": "Tên địa chỉ phụ không được chứa ký tự ` , ' \"\nvà phải từ 1 đến 20 ký tự", + "error_text_template": "Tên và địa chỉ mẫu không được chứa ký tự ` , ' \"\nvà phải từ 1 đến 106 ký tự", + "error_text_wallet_name": "Tên ví chỉ được chứa chữ cái, số, ký hiệu _ -\nvà phải từ 1 đến 33 ký tự", + "error_text_xmr": "Giá trị XMR không được vượt quá số dư khả dụng.\nSố chữ số thập phân phải ít hơn hoặc bằng 12", + "errorGettingCredentials": "Không thành công: Lỗi khi nhận thông tin xác thực", + "errorSigningTransaction": "Đã xảy ra lỗi khi ký giao dịch", + "estimated": "Ước tính", + "estimated_new_fee": "Phí mới ước tính", + "estimated_receive_amount": "Số tiền nhận ước tính", + "etherscan_history": "Lịch sử Etherscan", + "event": "Sự kiện", + "events": "Các sự kiện", + "exchange": "Tráo đổ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!", + "exchange_result_confirm": "Bằng cách nhấn xác nhận, bạn sẽ gửi ${fetchingLabel} ${from} từ ví có tên ${walletName} của mình đến địa chỉ dưới đây. Hoặc bạn có thể gửi từ ví bên ngoài của mình đến địa chỉ/mã QR bên dưới.\n\nVui lòng nhấn xác nhận để tiếp tục hoặc quay lại để thay đổi số tiền.", + "exchange_result_description": "Bạn phải gửi tối thiểu ${fetchingLabel} ${from} đến địa chỉ hiển thị trên trang tiếp theo. Nếu bạn gửi số tiền thấp hơn ${fetchingLabel} ${from} thì có thể không được chuyển đổi và không được hoàn trả.", + "exchange_result_write_down_ID": "*Vui lòng sao chép hoặc ghi lại ID hiển thị ở trên.", + "exchange_result_write_down_trade_id": "Vui lòng sao chép hoặc ghi lại ID giao dịch để tiếp tục.", + "exchange_sync_alert_content": "Vui lòng chờ cho đến khi ví của bạn được đồng bộ hóa", + "expired": "Đã hết hạn", + "expires": "Hết hạn", + "expiresOn": "Hết hạn vào", + "expiry_and_validity": "Hạn và hiệu lực", + "export_backup": "Xuất sao lưu", + "extra_id": "ID bổ sung:", + "extracted_address_content": "Bạn sẽ gửi tiền cho\n${recipient_name}", + "failed_authentication": "Xác thực không thành công. ${state_error}", + "faq": "FAQ", + "features": "Tính năng", + "fee_less_than_min": "Phí được chọn ít hơn mức tối thiểu, vui lòng tăng phí để có thể gửi giao dịch", + "fee_rate": "Tỷ lệ phí", + "fetching": "Đang tải", + "fiat_api": "API Fiat", + "fiat_balance": "Số dư Fiat", + "field_required": "Trường này là bắt buộc", + "fill_code": "Vui lòng điền mã xác minh được gửi đến email của bạn", + "filter_by": "Lọc theo", + "first_wallet_text": "Ví tuyệt vời cho Monero, Bitcoin, Ethereum, Litecoin, và Haven", + "fixed_pair_not_supported": "Cặp cố định này không được hỗ trợ với các dịch vụ hoán đổi đã chọn", + "fixed_rate": "Tỷ giá cố định", + "fixed_rate_alert": "Bạn sẽ có thể nhập số lượng nhận được khi chế độ tỷ giá cố định được chọn. Bạn có muốn chuyển sang chế độ tỷ giá cố định không?", + "forgot_password": "Quên mật khẩu", + "freeze": "Đóng băng", + "frequently_asked_questions": "Các câu hỏi thường gặp", + "frozen": "Đã đóng băng", + "full_balance": "Số dư đầy đủ", + "generate_name": "Tạo tên", + "generating_gift_card": "Đang tạo thẻ quà tặng", + "get_a": "Nhận một ", + "get_card_note": " mà bạn có thể nạp lại bằng tiền điện tử. Không cần thêm thông tin!", + "get_your_yat": "Nhận Yat của bạn", + "gift_card_amount": "Số tiền thẻ quà tặng", + "gift_card_balance_note": "Các thẻ quà tặng còn số dư sẽ xuất hiện ở đây", + "gift_card_is_generated": "Thẻ quà tặng đã được tạo", + "gift_card_number": "Số thẻ quà tặng", + "gift_card_redeemed_note": "Thẻ quà tặng bạn đã quy đổi sẽ xuất hiện ở đây", + "gift_cards": "Thẻ quà tặng", + "gift_cards_unavailable": "Thẻ quà tặng chỉ có thể mua bằng Monero, Bitcoin, và Litecoin vào lúc này", + "got_it": "Đã hiểu", + "gross_balance": "Số dư gộp", + "group_by_type": "Nhóm theo loại", + "haven_app": "Haven bởi Cake Wallet", + "haven_app_wallet_text": "Ví tuyệt vời cho Haven", + "help": "Trợ giúp", + "hidden_balance": "Số dư ẩn", + "hide_details": "Ẩn chi tiết", + "high_contrast_theme": "Chủ đề độ tương phản cao", + "home_screen_settings": "Cài đặt màn hình chính", + "how_to_use": "Cách sử dụng", + "how_to_use_card": "Cách sử dụng thẻ này", + "id": "ID: ", + "ignor": "Bỏ qua", + "import": "Nhập", + "importNFTs": "Nhập NFT", + "in_store": "Tại cửa hàng", + "incoming": "Đang nhận", + "incorrect_seed": "Văn bản nhập không hợp lệ.", + "inputs": "Đầu vào", + "insufficient_lamport_for_tx": "Bạn không có đủ SOL để thanh toán giao dịch và phí giao dịch. Vui lòng thêm SOL vào ví của bạn hoặc giảm số lượng SOL bạn đang gửi.", + "insufficient_lamports": "Bạn không có đủ SOL để thanh toán giao dịch và phí giao dịch. Bạn cần ít nhất ${solValueNeeded} SOL. Vui lòng thêm SOL vào ví của bạn hoặc giảm số lượng SOL bạn đang gửi", + "insufficientFundsForRentError": "Bạn không có đủ SOL để thanh toán phí giao dịch và phí thuê cho tài khoản. Vui lòng thêm SOL vào ví của bạn hoặc giảm số lượng SOL bạn đang gửi", + "introducing_cake_pay": "Giới thiệu Cake Pay!", + "invalid_input": "Nhập không hợp lệ", + "invalid_password": "Mật khẩu không hợp lệ", + "invoice_details": "Chi tiết hóa đơn", + "is_percentage": "là", + "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.", + "ledger_error_device_locked": "Ledger đã bị khóa", + "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", + "light_theme": "Chủ đề sáng", + "litecoin_mweb_description": "MWEB là một giao thức mới mang lại các giao dịch nhanh hơn, rẻ hơn và riêng tư hơn cho Litecoin", + "litecoin_mweb_dismiss": "Miễn nhiệm", + "litecoin_mweb_enable_later": "Bạn có thể chọn bật lại MWEB trong cài đặt hiển thị.", + "litecoin_mweb_pegin": "Chốt vào", + "litecoin_mweb_pegout": "Chốt ra", + "live_fee_rates": "Tỷ lệ phí hiện tại qua API", + "load_more": "Tải thêm", + "loading_your_wallet": "Đang tải ví của bạn", + "login": "Đăng nhập", + "logout": "Đăng xuất", + "low_fee": "Phí thấp", + "low_fee_alert": "Bạn hiện đang sử dụng mức ưu tiên phí mạng thấp. Điều này có thể dẫn đến thời gian chờ lâu, tỷ giá khác nhau, hoặc giao dịch bị hủy bỏ. Chúng tôi khuyến nghị bạn đặt mức phí cao hơn để có trải nghiệm tốt hơn.", + "manage_nodes": "Quản lý các nút", + "manage_pow_nodes": "Quản lý các nút PoW", + "manage_yats": "Quản lý Yats", + "mark_as_redeemed": "Đánh dấu là đã đổi", + "market_place": "Thị trường", + "matrix_green_dark_theme": "Chủ đề tối Matrix Green", + "max_amount": "Tối đa: ${value}", + "max_value": "Tối đa: ${value} ${currency}", + "memo": "Ghi chú:", + "message": "Tin nhắn", + "message_verified": "Tin nhắn đã được xác minh thành công", + "methods": "Phương pháp", + "min_amount": "Tối thiểu: ${value}", + "min_value": "Tối thiểu: ${value} ${currency}", + "minutes_to_pin_code": "${minute} phút", + "mm": "MM", + "modify_2fa": "Chỉnh sửa Cake 2FA", + "monero_com": "Monero.com bởi Cake Wallet", + "monero_com_wallet_text": "Ví tuyệt vời cho Monero", + "monero_dark_theme": "Chủ đề tối Monero", + "monero_light_theme": "Chủ đề sáng Monero", + "moonpay_alert_text": "Giá trị số tiền phải lớn hơn hoặc bằng ${minAmount} ${fiatCurrency}", + "more_options": "Thêm tùy chọn", + "mweb_confirmed": "Xác nhận MWEB", + "mweb_unconfirmed": "MWEB chưa được xác nhận", + "name": "Tên", + "nano_current_rep": "Đại diện hiện tại", + "nano_gpt_thanks_message": "Cảm ơn bạn đã sử dụng NanoGPT! Hãy nhớ quay lại trình duyệt sau khi giao dịch của bạn hoàn tất!", + "nano_pick_new_rep": "Chọn đại diện mới", + "nanogpt_subtitle": "Tất cả các mẫu mới nhất (GPT-4, Claude).\\nKhông cần đăng ký, thanh toán bằng tiền điện tử.", + "narrow": "Hẹp", + "new_first_wallet_text": "Giữ an toàn cho tiền điện tử của bạn, dễ dàng như ăn bánh", + "new_node_testing": "Đang thử nghiệm nút mới", + "new_subaddress_create": "Tạo", + "new_subaddress_label_name": "Tên nhãn", + "new_subaddress_title": "Địa chỉ mới", + "new_template": "Mẫu mới", + "new_wallet": "Ví mới", + "newConnection": "Kết nối mới", + "no_cards_found": "Không tìm thấy thẻ", + "no_id_needed": "Không cần ID!", + "no_id_required": "Không yêu cầu ID. Nạp tiền và chi tiêu ở bất kỳ đâu", + "no_relay_on_domain": "Không có relay cho miền của người dùng hoặc relay không khả dụng. Vui lòng chọn một relay để sử dụng.", + "no_relays": "Không có relay", + "no_relays_message": "Chúng tôi đã tìm thấy một bản ghi Nostr NIP-05 cho người dùng này, nhưng nó không chứa bất kỳ relay nào. Vui lòng hướng dẫn người nhận thêm relay vào bản ghi Nostr của họ.", + "node_address": "Địa chỉ nút", + "node_connection_failed": "Kết nối không thành công", + "node_connection_successful": "Kết nối thành công", + "node_new": "Nút mới", + "node_port": "Cổng nút", + "node_reset_settings_title": "Đặt lại cài đặt", + "node_test": "Kiểm tra", + "nodes": "Các nút", + "nodes_list_reset_to_default_message": "Bạn có chắc chắn muốn đặt lại cài đặt về mặc định không?", + "none_of_selected_providers_can_exchange": "Không có nhà cung cấp nào được chọn có thể thực hiện hoán đổi này", + "noNFTYet": "Chưa có NFT", + "normal": "Bình thường", + "note_optional": "Ghi chú (tùy chọn)", + "note_tap_to_change": "Ghi chú (nhấn để thay đổi)", + "nullURIError": "URI là null", + "offer_expires_in": "Ưu đãi hết hạn trong: ", + "offline": "Ngoại tuyến", + "ok": "OK", + "old_fee": "Phí cũ", + "onion_link": "Liên kết Onion", + "online": "Trực tuyến", + "onramper_option_description": "Mua tiền điện tử nhanh chóng với nhiều phương thức thanh toán. Có sẵn ở hầu hết các quốc gia. Chênh lệch và phí thay đổi.", + "open_gift_card": "Mở thẻ quà tặng", + "optional_description": "Mô tả tùy chọn", + "optional_email_hint": "Email thông báo cho người nhận (tùy chọn)", + "optional_name": "Tên người nhận (tùy chọn)", + "optionally_order_card": "Có thể đặt thẻ vật lý.", + "orbot_running_alert": "Vui lòng đảm bảo Orbot đang chạy trước khi kết nối với nút này.", + "order_by": "Sắp xếp theo", + "order_id": "ID đơn hàng", + "order_physical_card": "Đặt thẻ vật lý", + "other_settings": "Cài đặt khác", + "outdated_electrum_wallet_description": "Ví Bitcoin mới được tạo trong Cake hiện có hạt giống 24 từ. Bạn cần tạo một ví Bitcoin mới và chuyển tất cả số tiền của bạn vào ví 24 từ mới, và ngừng sử dụng ví có hạt giống 12 từ. Vui lòng thực hiện ngay để bảo vệ tài sản của bạn.", + "outdated_electrum_wallet_receive_warning": "Nếu ví này có hạt giống 12 từ và được tạo trong Cake, ĐỪNG gửi Bitcoin vào ví này. Bất kỳ BTC nào chuyển vào ví này có thể bị mất. Tạo ví 24 từ mới (nhấn menu ở góc trên bên phải, chọn Ví, chọn Tạo Ví Mới, sau đó chọn Bitcoin) và NGAY LẬP TỨC chuyển BTC của bạn vào đó. Ví BTC mới (24 từ) từ Cake là an toàn", + "outgoing": "Đang gửi", + "outputs": "Đầu ra", + "overwrite_amount": "Ghi đè số tiền", + "pairingInvalidEvent": "Sự kiện ghép nối không hợp lệ", + "passphrase": "Cụm từ bảo mật (Tùy chọn)", + "passphrases_doesnt_match": "Vòng thông không khớp, vui lòng thử lại", + "password": "Mật khẩu", + "paste": "Dán", + "pause_wallet_creation": "Khả năng tạo ví Haven hiện đang bị tạm dừng.", + "payment_id": "ID thanh toán: ", + "payment_was_received": "Thanh toán của bạn đã được nhận.", + "pending": " (đang chờ)", + "percentageOf": "của ${amount}", + "pin_at_top": "Ghim ${token} ở trên cùng", + "pin_is_incorrect": "PIN không chính xác", + "pin_number": "Số PIN", + "placeholder_contacts": "Danh bạ của bạn sẽ được hiển thị ở đây", + "placeholder_transactions": "Giao dịch của bạn sẽ được hiển thị ở đây", + "please_fill_totp": "Vui lòng điền mã 8 chữ số trên thiết bị khác của bạn", + "please_make_selection": "Vui lòng chọn một tùy chọn dưới đây để tạo hoặc khôi phục ví của bạn.", + "please_reference_document": "Vui lòng tham khảo các tài liệu dưới đây để biết thêm thông tin.", + "please_select": "Vui lòng chọn:", + "please_select_backup_file": "Vui lòng chọn tệp sao lưu và nhập mật khẩu sao lưu.", + "please_try_to_connect_to_another_node": "Vui lòng thử kết nối với một nút khác", + "please_wait": "Vui lòng chờ", + "polygonscan_history": "Lịch sử PolygonScan", + "powered_by": "Được cung cấp bởi ${title}", + "pre_seed_button_text": "Tôi hiểu. Hiển thị hạt giống của tôi", + "pre_seed_description": "Trên trang tiếp theo, bạn sẽ thấy một chuỗi ${words} từ. Đây là hạt giống riêng tư và duy nhất của bạn và là CÁCH DUY NHẤT để khôi phục ví của bạn trong trường hợp mất hoặc hỏng hóc. Đây là TRÁCH NHIỆM của bạn để ghi lại và lưu trữ nó ở một nơi an toàn ngoài ứng dụng Cake Wallet.", + "pre_seed_title": "QUAN TRỌNG", + "prepaid_cards": "Thẻ trả trước", + "prevent_screenshots": "Ngăn chặn ảnh chụp màn hình và ghi hình màn hình", + "privacy": "Quyền riêng tư", + "privacy_policy": "Chính sách quyền riêng tư", + "privacy_settings": "Cài đặt quyền riêng tư", + "private_key": "Khóa riêng", + "proceed_after_one_minute": "Nếu màn hình không tiếp tục sau 1 phút, hãy kiểm tra email của bạn.", + "proceed_on_device": "Tiếp tục trên thiết bị của bạn", + "proceed_on_device_description": "Vui lòng làm theo các hướng dẫn được nhắc trên ví phần cứng của bạn", + "profile": "Hồ sơ", + "provider_error": "Lỗi ${provider}", + "public_key": "Khóa công khai", + "purchase_gift_card": "Mua thẻ quà tặng", + "purple_dark_theme": "Chủ đề tối tím", + "qr_fullscreen": "Nhấn để mở mã QR toàn màn hình", + "qr_payment_amount": "Mã QR này chứa số tiền thanh toán. Bạn có muốn ghi đè giá trị hiện tại không?", + "quantity": "Số lượng", + "question_to_disable_2fa": "Bạn có chắc chắn muốn tắt Cake 2FA không? Mã 2FA sẽ không còn cần thiết để truy cập ví và một số chức năng nhất định.", + "receivable_balance": "Số dư có thể nhận", + "receive": "Nhận", + "receive_amount": "Số lượng", + "received": "Đã nhận", + "recipient_address": "Địa chỉ người nhận", + "reconnect": "Kết nối lại", + "reconnect_alert_text": "Bạn có chắc chắn muốn kết nối lại không?", + "reconnection": "Kết nối lại", + "red_dark_theme": "Chủ đề tối đỏ", + "red_light_theme": "Chủ đề sáng đỏ", + "redeemed": "Đã đổi", + "refund_address": "Địa chỉ hoàn tiền", + "reject": "Từ chối", + "remaining": "còn lại", + "remove": "Gỡ bỏ", + "remove_node": "Gỡ bỏ nút", + "remove_node_message": "Bạn có chắc chắn muốn gỡ bỏ nút đã chọn không?", + "rename": "Đổi tên", + "rep_warning": "Cảnh báo đại diện", + "rep_warning_sub": "Đại diện của bạn dường như không còn trong tình trạng tốt. Nhấn vào đây để chọn một cái mới", + "repeat_wallet_password": "Nhập lại mật khẩu ví", + "repeated_password_is_incorrect": "Mật khẩu nhập lại không chính xác. Vui lòng nhập lại mật khẩu ví.", + "require_for_adding_contacts": "Yêu cầu khi thêm danh bạ", + "require_for_all_security_and_backup_settings": "Yêu cầu cho tất cả các cài đặt bảo mật và sao lưu", + "require_for_assessing_wallet": "Yêu cầu khi truy cập ví", + "require_for_creating_new_wallets": "Yêu cầu khi tạo ví mới", + "require_for_exchanges_to_external_wallets": "Yêu cầu khi đổi sang ví ngoài", + "require_for_exchanges_to_internal_wallets": "Yêu cầu khi đổi sang ví nội bộ", + "require_for_sends_to_contacts": "Yêu cầu khi gửi đến danh bạ", + "require_for_sends_to_internal_wallets": "Yêu cầu khi gửi đến ví nội bộ", + "require_for_sends_to_non_contacts": "Yêu cầu khi gửi đến người không phải danh bạ", + "require_pin_after": "Yêu cầu PIN sau", + "rescan": "Quét lại", + "resend_code": "Vui lòng gửi lại", + "reset": "Đặt lại", + "reset_password": "Đặt lại mật khẩu", + "restore_active_seed": "Hạt giống hoạt động", + "restore_address": "Địa chỉ", + "restore_bitcoin_description_from_keys": "Khôi phục ví của bạn từ chuỗi WIF được tạo từ khóa riêng của bạn", + "restore_bitcoin_description_from_seed": "Khôi phục ví của bạn từ mã kết hợp 24 từ", + "restore_bitcoin_title_from_keys": "Khôi phục từ WIF", + "restore_description_from_backup": "Bạn có thể khôi phục toàn bộ ứng dụng Cake Wallet từ tệp sao lưu của bạn", + "restore_description_from_hardware_wallet": "Khôi phục từ ví phần cứng Ledger", + "restore_description_from_keys": "Khôi phục ví của bạn từ các thao tác nhập được lưu từ khóa riêng của bạn", + "restore_description_from_seed": "Khôi phục ví của bạn từ mã kết hợp 25 từ hoặc 13 từ", + "restore_description_from_seed_keys": "Khôi phục ví của bạn từ hạt giống/khóa mà bạn đã lưu ở nơi an toàn", + "restore_from_date_or_blockheight": "Vui lòng nhập một ngày vài ngày trước khi bạn tạo ví này. Hoặc nếu bạn biết chiều cao khối, hãy nhập nó thay thế", + "restore_from_seed_placeholder": "Vui lòng nhập hoặc dán hạt giống của bạn vào đây", + "restore_new_seed": "Hạt giống mới", + "restore_next": "Tiếp theo", + "restore_recover": "Khôi phục", + "restore_restore_wallet": "Khôi phục Ví", + "restore_seed_keys_restore": "Khôi phục hạt giống/khóa", + "restore_spend_key_private": "Khóa chi tiêu (riêng tư)", + "restore_title_from_backup": "Khôi phục từ sao lưu", + "restore_title_from_hardware_wallet": "Khôi phục từ ví phần cứng", + "restore_title_from_keys": "Khôi phục từ khóa", + "restore_title_from_seed": "Khôi phục từ hạt giống", + "restore_title_from_seed_keys": "Khôi phục từ hạt giống/khóa", + "restore_view_key_private": "Khóa xem (riêng tư)", + "restore_wallet": "Khôi phục Ví", + "restore_wallet_name": "Tên ví", + "restore_wallet_restore_description": "Mô tả khôi phục ví", + "robinhood_option_description": "Mua và chuyển ngay lập tức bằng thẻ ghi nợ, tài khoản ngân hàng hoặc số dư Robinhood của bạn. Chỉ có ở Mỹ.", + "router_no_route": "Không có tuyến đường nào được định nghĩa cho ${name}", + "save": "Lưu", + "save_backup_password": "Vui lòng đảm bảo rằng bạn đã lưu mật khẩu sao lưu của mình. Bạn sẽ không thể nhập tệp sao lưu của mình nếu không có nó.", + "save_backup_password_alert": "Lưu mật khẩu sao lưu", + "save_to_downloads": "Lưu vào Tải xuống", + "saved_the_trade_id": "Tôi đã lưu ID giao dịch", + "scan_one_block": "Quét một khối", + "scan_qr_code": "Quét mã QR", + "scan_qr_code_to_get_address": "Quét mã QR để nhận địa chỉ", + "scan_qr_on_device": "Quét mã QR này trên thiết bị khác", + "search": "Tìm kiếm", + "search_add_token": "Tìm kiếm / Thêm token", + "search_category": "Tìm kiếm danh mục", + "search_currency": "Tìm kiếm tiền tệ", + "search_language": "Tìm kiếm ngôn ngữ", + "second_intro_content": "Yat của bạn là một địa chỉ emoji duy nhất thay thế tất cả các địa chỉ hex dài của bạn cho tất cả các loại tiền tệ của bạn.", + "second_intro_title": "Một địa chỉ emoji để cai trị tất cả", + "security_and_backup": "Bảo mật và sao lưu", + "seed_alert_back": "Quay lại", + "seed_alert_content": "Hạt giống là cách duy nhất để khôi phục ví của bạn. Bạn đã ghi lại nó chưa?", + "seed_alert_title": "Chú ý", + "seed_alert_yes": "Có, tôi đã ghi lại", + "seed_choose": "Chọn ngôn ngữ hạt giống", + "seed_hex_form": "Hạt giống ví (dạng hex)", + "seed_key": "Khóa hạt giống", + "seed_language": "Ngôn ngữ hạt giống", + "seed_language_chinese": "Tiếng Trung", + "seed_language_chinese_traditional": "Tiếng Trung (Truyền thống)", + "seed_language_czech": "Tiếng Séc", + "seed_language_dutch": "Tiếng Hà Lan", + "seed_language_english": "Tiếng Anh", + "seed_language_french": "Tiếng Pháp", + "seed_language_german": "Tiếng Đức", + "seed_language_italian": "Tiếng Ý", + "seed_language_japanese": "Tiếng Nhật", + "seed_language_korean": "Tiếng Hàn", + "seed_language_next": "Tiếp theo", + "seed_language_portuguese": "Tiếng Bồ Đào Nha", + "seed_language_russian": "Tiếng Nga", + "seed_language_spanish": "Tiếng Tây Ban Nha", + "seed_phrase_length": "Độ dài cụm từ hạt giống", + "seed_reminder": "Vui lòng ghi lại những điều này phòng khi bạn mất hoặc xóa điện thoại của mình", + "seed_share": "Chia sẻ hạt giống", + "seed_title": "Hạt giống", + "seedtype": "Loại hạt giống", + "seedtype_alert_content": "Chia sẻ hạt giống với ví khác chỉ có thể với BIP39 SeedType.", + "seedtype_alert_title": "Cảnh báo hạt giống", + "seedtype_legacy": "Di sản (25 từ)", + "seedtype_polyseed": "Polyseed (16 từ)", + "seedtype_wownero": "Wownero (14 từ)", + "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_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.", + "sell": "Bán", + "sell_alert_content": "Hiện tại chúng tôi chỉ hỗ trợ bán Bitcoin, Ethereum và Litecoin. Vui lòng tạo hoặc chuyển sang ví Bitcoin, Ethereum hoặc Litecoin của bạn.", + "sell_monero_com_alert_content": "Bán Monero chưa được hỗ trợ", + "send": "Gửi", + "send_address": "Địa chỉ ${cryptoCurrency}", + "send_amount": "Số tiền:", + "send_change_to_you": "Thay đổi, cho bạn:", + "send_creating_transaction": "Tạo giao dịch", + "send_error_currency": "Tiền tệ chỉ có thể chứa số", + "send_error_minimum_value": "Giá trị tối thiểu của số tiền là 0.01", + "send_estimated_fee": "Phí ước lượng:", + "send_fee": "Phí:", + "send_name": "Tên", + "send_new": "Mới", + "send_payment_id": "ID thanh toán (tùy chọn)", + "send_priority": "Hiện tại phí được đặt ở mức ưu tiên ${transactionPriority}.\nƯu tiên giao dịch có thể được điều chỉnh trong cài đặt", + "send_sending": "Đang gửi...", + "send_success": "Đã gửi ${crypto} của bạn thành công", + "send_templates": "Mẫu", + "send_title": "Gửi", + "send_to_this_address": "Gửi ${currency} ${tag} đến địa chỉ này", + "send_xmr": "Gửi XMR", + "send_your_wallet": "Ví của bạn", + "sending": "Đang gửi", + "sent": "Đã gửi", + "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ư", + "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", + "settings_can_be_changed_later": "Các cài đặt này có thể được thay đổi sau trong cài đặt ứng dụng", + "settings_change_language": "Thay đổi ngôn ngữ", + "settings_change_pin": "Thay đổi PIN", + "settings_currency": "Tiền tệ", + "settings_current_node": "Nút hiện tại", + "settings_dark_mode": "Chế độ tối", + "settings_display_balance": "Hiển thị số dư", + "settings_display_on_dashboard_list": "Hiển thị trên danh sách bảng điều khiển", + "settings_fee_priority": "Ưu tiên phí", + "settings_nodes": "Các nút", + "settings_none": "Không có", + "settings_only_trades": "Chỉ giao dịch", + "settings_only_transactions": "Chỉ giao dịch", + "settings_personal": "Cá nhân", + "settings_save_recipient_address": "Lưu địa chỉ người nhận", + "settings_support": "Hỗ trợ", + "settings_terms_and_conditions": "Điều khoản và Điều kiện", + "settings_title": "Cài đặt", + "settings_trades": "Giao dịch", + "settings_transactions": "Giao dịch", + "settings_wallets": "Ví", + "setup_2fa": "Thiết lập Cake 2FA", + "setup_2fa_text": "Cake 2FA hoạt động bằng cách sử dụng TOTP làm yếu tố xác thực thứ hai.\n\nTOTP của Cake 2FA yêu cầu hỗ trợ SHA-512 và 8 chữ số; điều này cung cấp bảo mật cao hơn. Thông tin thêm và các ứng dụng hỗ trợ có thể được tìm thấy trong hướng dẫn.", + "setup_pin": "Thiết lập PIN", + "setup_successful": "PIN của bạn đã được thiết lập thành công!", + "setup_totp_recommended": "Thiết lập TOTP", + "setup_warning_2fa_text": "Cake 2FA là xác thực thứ hai cho một số hành động trong ví. Nó KHÔNG an toàn như lưu trữ lạnh.\n\nNếu bạn mất quyền truy cập vào ứng dụng 2FA hoặc các khóa TOTP, bạn SẼ mất quyền truy cập vào ví này. Bạn sẽ cần phải khôi phục ví của bạn từ hạt giống nhớ.\n\nHỗ trợ Cake sẽ không thể hỗ trợ bạn nếu bạn mất quyền truy cập vào 2FA hoặc hạt giống nhớ của bạn.\nTrước khi sử dụng Cake 2FA, chúng tôi khuyến nghị đọc kỹ hướng dẫn.", + "setup_your_debit_card": "Thiết lập thẻ ghi nợ của bạn", + "share": "Chia sẻ", + "share_address": "Chia sẻ địa chỉ", + "shared_seed_wallet_groups": "Nhóm ví hạt được chia sẻ", + "show_details": "Hiển thị chi tiết", + "show_keys": "Hiển thị hạt giống/khóa", + "show_market_place": "Hiển thị Thị trường", + "show_seed": "Hiển thị hạt giống", + "sign_message": "Ký tin nhắn", + "sign_up": "Đăng ký", + "sign_verify_message": "Ký hoặc xác minh tin nhắn", + "sign_verify_message_sub": "Ký hoặc xác minh một tin nhắn bằng khóa riêng của bạn", + "sign_verify_title": "Ký / Xác minh", + "signature": "Chữ ký", + "signature_invalid_error": "Chữ ký không hợp lệ cho tin nhắn đã cho", + "signTransaction": "Ký giao dịch", + "signup_for_card_accept_terms": "Đăng ký thẻ và chấp nhận các điều khoản.", + "silent_payment": "Thanh toán im lặng", + "silent_payments": "Thanh toán im lặng", + "silent_payments_always_scan": "Đặt Thanh toán im lặng luôn quét", + "silent_payments_disclaimer": "Địa chỉ mới không phải là danh tính mới. Đây là việc tái sử dụng một danh tính hiện có với nhãn khác.", + "silent_payments_display_card": "Hiển thị thẻ Thanh toán im lặng", + "silent_payments_scan_from_date": "Quét từ ngày", + "silent_payments_scan_from_date_or_blockheight": "Vui lòng nhập chiều cao khối bạn muốn bắt đầu quét cho các thanh toán im lặng đến, hoặc, sử dụng ngày thay thế. Bạn có thể chọn nếu ví tiếp tục quét mỗi khối, hoặc chỉ kiểm tra chiều cao đã chỉ định.", + "silent_payments_scan_from_height": "Quét từ chiều cao khối", + "silent_payments_scanned_tip": "ĐÃ QUÉT ĐỂ TIP! (${tip})", + "silent_payments_scanning": "Đang quét thanh toán im lặng", + "silent_payments_settings": "Cài đặt thanh toán im lặng", + "single_seed_wallets_group": "Ví hạt đơn", + "slidable": "Có thể kéo", + "sort_by": "Sắp xếp theo", + "spend_key_private": "Khóa chi tiêu (riêng tư)", + "spend_key_public": "Khóa chi tiêu (công khai)", + "status": "Trạng thái: ", + "string_default": "Mặc định", + "subaddress_title": "Danh sách địa chỉ phụ", + "subaddresses": "Địa chỉ phụ", + "submit_request": "gửi yêu cầu", + "successful": "Thành công", + "support_description_guides": "Tài liệu và hỗ trợ cho các vấn đề phổ biến", + "support_description_live_chat": "Miễn phí và nhanh chóng! Các đại diện hỗ trợ được đào tạo sẵn sàng hỗ trợ", + "support_description_other_links": "Tham gia cộng đồng của chúng tôi hoặc liên hệ với chúng tôi hoặc các đối tác của chúng tôi qua các phương pháp khác", + "support_title_guides": "Hướng dẫn Cake Wallet", + "support_title_live_chat": "Hỗ trợ trực tiếp", + "support_title_other_links": "Liên kết hỗ trợ khác", + "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", + "switchToEVMCompatibleWallet": "Vui lòng chuyển sang ví tương thích EVM và thử lại (Ethereum, Polygon)", + "symbol": "Ký hiệu", + "sync_all_wallets": "Đồng bộ tất cả các ví", + "sync_status_attempting_scan": "Cố gắng quét", + "sync_status_attempting_sync": "ĐANG THỬ ĐỒNG BỘ", + "sync_status_connected": "ĐÃ KẾT NỐI", + "sync_status_connecting": "ĐANG KẾT NỐI", + "sync_status_failed_connect": "ĐÃ NGẮT KẾT NỐI", + "sync_status_not_connected": "CHƯA KẾT NỐI", + "sync_status_starting_scan": "ĐANG BẮT ĐẦU QUÉT (${height})", + "sync_status_starting_sync": "ĐANG BẮT ĐẦU ĐỒNG BỘ", + "sync_status_syncronized": "ĐÃ ĐỒNG BỘ", + "sync_status_syncronizing": "ĐANG ĐỒNG BỘ", + "sync_status_timed_out": "HẾT THỜI GIAN", + "sync_status_unsupported": "NÓT KHÔNG ĐƯỢC HỖ TRỢ", + "syncing_wallet_alert_content": "Số dư và danh sách giao dịch của bạn có thể không đầy đủ cho đến khi nó hiển thị “ĐÃ ĐỒNG BỘ” ở trên cùng. Nhấn vào đây để tìm hiểu thêm.", + "syncing_wallet_alert_title": "Ví của bạn đang đồng bộ", + "template": "Mẫu", + "template_name": "Tên mẫu", + "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", + "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", + "tip": "Mẹo:", + "today": "Hôm nay", + "token_contract_address": "Địa chỉ hợp đồng token", + "token_decimal": "Số thập phân của token", + "token_name": "Tên token ví dụ: Tether", + "token_symbol": "Ký hiệu token ví dụ: USDT", + "tokenID": "ID", + "tor_connection": "Kết nối Tor", + "tor_only": "Chỉ Tor", + "total": "Tổng cộng", + "total_saving": "Tiết kiệm tổng cộng", + "totp_2fa_failure": "Mã không chính xác. Vui lòng thử mã khác hoặc tạo khóa bí mật mới. Sử dụng ứng dụng 2FA tương thích hỗ trợ mã 8 chữ số và SHA512.", + "totp_2fa_success": "Thành công! Cake 2FA đã được kích hoạt cho ví này. Hãy nhớ lưu hạt giống nhớ của bạn phòng trường hợp bạn mất quyền truy cập vào ví.", + "totp_auth_url": "URL XÁC THỰC TOTP", + "totp_code": "Mã TOTP", + "totp_secret_code": "Mã bí mật TOTP", + "totp_verification_success": "Xác minh thành công!", + "track": "Theo dõi", + "trade_details_copied": "${title} đã được sao chép vào clipboard", + "trade_details_created_at": "Tạo lúc", + "trade_details_fetching": "Đang lấy dữ liệu", + "trade_details_id": "ID", + "trade_details_pair": "Cặp", + "trade_details_provider": "Nhà cung cấp", + "trade_details_state": "Trạng thái", + "trade_details_title": "Chi tiết giao dịch", + "trade_for_not_created": "Giao dịch cho ${title} chưa được tạo.", + "trade_history_title": "Lịch sử giao dịch", + "trade_id": "ID giao dịch:", + "trade_id_not_found": "Giao dịch ${tradeId} của ${title} không tìm thấy.", + "trade_is_powered_by": "Giao dịch này được cung cấp bởi ${provider}", + "trade_not_created": "Giao dịch chưa được tạo", + "trade_not_found": "Giao dịch không tìm thấy.", + "trade_state_btc_sent": "BTC đã gửi", + "trade_state_complete": "Hoàn thành", + "trade_state_confirming": "Đang xác nhận", + "trade_state_created": "Đã tạo", + "trade_state_finished": "Đã hoàn tất", + "trade_state_paid": "Đã thanh toán", + "trade_state_paid_unconfirmed": "Đã thanh toán chưa xác nhận", + "trade_state_pending": "Đang chờ xử lý", + "trade_state_timeout": "Hết thời gian", + "trade_state_to_be_created": "Sẽ được tạo", + "trade_state_traded": "Đã giao dịch", + "trade_state_trading": "Đang giao dịch", + "trade_state_underpaid": "Thanh toán chưa đủ", + "trade_state_unpaid": "Chưa thanh toán", + "trades": "Giao dịch", + "transaction_details_amount": "Số tiền", + "transaction_details_copied": "${title} đã được sao chép vào clipboard", + "transaction_details_date": "Ngày", + "transaction_details_fee": "Phí", + "transaction_details_height": "Chiều cao", + "transaction_details_recipient_address": "Địa chỉ người nhận", + "transaction_details_source_address": "Địa chỉ nguồn", + "transaction_details_title": "Chi tiết giao dịch", + "transaction_details_transaction_id": "ID giao dịch", + "transaction_key": "Khóa giao dịch", + "transaction_priority_fast": "Nhanh", + "transaction_priority_fastest": "Nhanh nhất", + "transaction_priority_medium": "Trung bình", + "transaction_priority_regular": "Thông thường", + "transaction_priority_slow": "Chậm", + "transaction_sent": "Giao dịch đã được gửi!", + "transaction_sent_notice": "Nếu màn hình không tiếp tục sau 1 phút, hãy kiểm tra trình khám phá khối và email của bạn.", + "transactions": "Giao dịch", + "transactions_by_date": "Giao dịch theo ngày", + "trongrid_history": "Lịch sử TronGrid", + "trusted": "Đã tin cậy", + "tx_commit_exception_no_dust_on_change": "Giao dịch bị từ chối với số tiền này. Với số tiền này bạn có thể gửi ${min} mà không cần đổi tiền lẻ hoặc ${max} trả lại tiền lẻ.", + "tx_commit_failed": "Giao dịch không thành công. Vui lòng liên hệ với hỗ trợ.", + "tx_commit_failed_no_peers": "Giao dịch không phát sóng, vui lòng thử lại trong một giây hoặc lâu hơn", + "tx_invalid_input": "Bạn đang sử dụng loại đầu vào sai cho loại thanh toán này", + "tx_no_dust_exception": "Giao dịch bị từ chối vì gửi một số tiền quá nhỏ. Vui lòng thử tăng số tiền.", + "tx_not_enough_inputs_exception": "Không đủ đầu vào có sẵn. Vui lòng chọn thêm dưới Coin Control", + "tx_rejected_bip68_final": "Giao dịch có đầu vào chưa xác nhận và không thể thay thế bằng phí.", + "tx_rejected_dust_change": "Giao dịch bị từ chối bởi quy tắc mạng, số tiền thay đổi thấp (dust). Thử gửi TOÀN BỘ hoặc giảm số tiền.", + "tx_rejected_dust_output": "Giao dịch bị từ chối bởi quy tắc mạng, số tiền đầu ra thấp (dust). Vui lòng tăng số tiền.", + "tx_rejected_dust_output_send_all": "Giao dịch bị từ chối bởi quy tắc mạng, số tiền đầu ra thấp (dust). Vui lòng kiểm tra số dư của các đồng tiền được chọn dưới Coin Control.", + "tx_rejected_vout_negative": "Không đủ số dư để thanh toán phí giao dịch này. Vui lòng kiểm tra số dư của các đồng tiền dưới Coin Control.", + "tx_wrong_balance_exception": "Bạn không có đủ ${currency} để gửi số tiền này.", + "tx_wrong_balance_with_amount_exception": "Bạn không có đủ ${currency} để gửi tổng số tiền ${amount}", + "tx_zero_fee_exception": "Không thể gửi giao dịch với phí bằng 0. Thử tăng tỷ lệ phí hoặc kiểm tra kết nối của bạn để biết ước lượng mới nhất.", + "unavailable_balance": "Số dư không khả dụng", + "unavailable_balance_description": "Số dư không khả dụng: Tổng số này bao gồm các khoản tiền bị khóa trong các giao dịch chờ xử lý và những khoản bạn đã chủ động đóng băng trong cài đặt điều khiển tiền của bạn. Các số dư bị khóa sẽ trở nên khả dụng khi các giao dịch tương ứng của chúng hoàn tất, trong khi các số dư bị đóng băng vẫn không thể truy cập cho các giao dịch cho đến khi bạn quyết định mở khóa chúng.", + "unconfirmed": "Số dư chưa xác nhận", + "understand": "Tôi hiểu", + "unlock": "Mở khóa", + "unmatched_currencies": "Tiền tệ của ví hiện tại của bạn không khớp với QR đã quét", + "unspent_change": "Tiền thối", + "unspent_coins_details_title": "Chi tiết các đồng tiền chưa chi tiêu", + "unspent_coins_title": "Các đồng tiền chưa chi tiêu", + "unsupported_asset": "Chúng tôi không hỗ trợ hành động này cho tài sản này. Vui lòng tạo hoặc chuyển sang ví của loại tài sản được hỗ trợ.", + "uptime": "Thời gian hoạt động", + "upto": "lên đến ${value}", + "usb": "USB", + "use": "Chuyển sang", + "use_card_info_three": "Sử dụng thẻ kỹ thuật số trực tuyến hoặc với các phương thức thanh toán không tiếp xúc.", + "use_card_info_two": "Các khoản tiền được chuyển đổi thành USD khi chúng được giữ trong tài khoản trả trước, không phải trong các loại tiền kỹ thuật số.", + "use_ssl": "Sử dụng SSL", + "use_suggested": "Sử dụng đề xuất", + "use_testnet": "Sử dụng Testnet", + "value": "Giá trị", + "value_type": "Loại giá trị", + "variable_pair_not_supported": "Cặp biến này không được hỗ trợ với các sàn giao dịch đã chọn", + "verification": "Xác minh", + "verify_message": "Xác minh tin nhắn", + "verify_with_2fa": "Xác minh với Cake 2FA", + "version": "Phiên bản ${currentVersion}", + "view_all": "Xem tất cả", + "view_in_block_explorer": "Xem trong Block Explorer", + "view_key_private": "Xem khóa (riêng tư)", + "view_key_public": "Xem khóa (công khai)", + "view_transaction_on": "Xem giao dịch trên", + "voting_weight": "Trọng số bỏ phiếu", + "waitFewSecondForTxUpdate": "Vui lòng đợi vài giây để giao dịch được phản ánh trong lịch sử giao dịch", + "wallet_group": "Nhóm ví", + "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_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_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í", + "wallet_list_create_new_wallet": "Tạo ví mới", + "wallet_list_edit_group_name": "Chỉnh sửa tên nhóm", + "wallet_list_edit_wallet": "Chỉnh sửa ví", + "wallet_list_failed_to_load": "Tải ví ${wallet_name} không thành công. ${error}", + "wallet_list_failed_to_remove": "Xóa ví ${wallet_name} không thành công. ${error}", + "wallet_list_load_wallet": "Tải ví", + "wallet_list_loading_wallet": "Đang tải ví ${wallet_name}", + "wallet_list_removing_wallet": "Đang xóa ví ${wallet_name}", + "wallet_list_restore_wallet": "Khôi phục ví", + "wallet_list_title": "Ví Monero", + "wallet_list_wallet_name": "Tên ví", + "wallet_menu": "Menu", + "wallet_name": "Tên ví", + "wallet_name_exists": "Một ví với tên đó đã tồn tại. Vui lòng chọn tên khác hoặc đổi tên ví khác trước.", + "wallet_password_is_empty": "Mật khẩu ví bị bỏ trống. Mật khẩu ví không được để trống", + "wallet_recovery_height": "Chiều cao khôi phục", + "wallet_restoration_store_incorrect_seed_length": "Độ dài hạt giống không chính xác", + "wallet_seed": "Hạt giống ví", + "wallet_seed_legacy": "Hạt giống ví cũ", + "wallet_store_monero_wallet": "Ví Monero", + "walletConnect": "WalletConnect", + "wallets": "Các ví", + "warning": "Cảnh báo", + "welcome": "Chào mừng đến với", + "welcome_to_cakepay": "Chào mừng đến với Cake Pay!", + "what_is_silent_payments": "Thanh toán im lặng là gì?", + "widgets_address": "Địa chỉ", + "widgets_or": "hoặc", + "widgets_restore_from_blockheight": "Khôi phục từ chiều cao khối", + "widgets_restore_from_date": "Khôi phục từ ngày", + "widgets_seed": "Hạt giống", + "wouoldLikeToConnect": "muốn kết nối", + "write_down_backup_password": "Vui lòng ghi lại mật khẩu sao lưu của bạn, được sử dụng để nhập các tệp sao lưu của bạn.", + "xlm_extra_info": "Vui lòng đừng quên chỉ định Memo ID khi gửi giao dịch XLM để trao đổi", + "xmr_available_balance": "Số dư khả dụng", + "xmr_full_balance": "Số dư đầy đủ", + "xmr_hidden": "Bị ẩn", + "xmr_to_error": "Lỗi XMR.TO", + "xmr_to_error_description": "Số tiền không hợp lệ. Giới hạn tối đa 8 chữ số sau dấu thập phân", + "xrp_extra_info": "Vui lòng đừng quên chỉ định Destination Tag khi gửi giao dịch XRP để trao đổi", + "yat": "Yat", + "yat_address": "Địa chỉ Yat", + "yat_alert_content": "Người dùng Cake Wallet hiện có thể gửi và nhận tất cả các loại tiền yêu thích của họ với tên người dùng dựa trên emoji độc đáo.", + "yat_alert_title": "Gửi và nhận crypto dễ dàng hơn với Yat", + "yat_error": "Lỗi Yat", + "yat_error_content": "Không có địa chỉ liên kết với Yat này. Thử Yat khác", + "yat_popup_content": "Bây giờ bạn có thể gửi và nhận crypto trong Cake Wallet với Yat của bạn - một tên người dùng ngắn gọn dựa trên emoji. Quản lý Yats bất cứ lúc nào trên màn hình cài đặt", + "yat_popup_title": "Địa chỉ ví của bạn có thể được chuyển thành emoji.", + "yesterday": "Hôm qua", + "you_now_have_debit_card": "Bạn hiện có một thẻ ghi nợ", + "you_pay": "Bạn thanh toán", + "you_will_get": "Chuyển đổi thành", + "you_will_send": "Chuyển đổi từ", + "yy": "YY" +} \ No newline at end of file diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index 8f47d8543..14270120c 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -36,6 +36,7 @@ "agree": "Jọ rò", "agree_and_continue": "Jọ Rò àti Tẹ̀síwájú", "agree_to": "Tẹ́ ẹ bá dá àkáǹtì ẹ jọ rò ", + "alert_notice": "Akiyesi", "all": "Gbogbo", "all_trades": "Gbogbo àwọn pàṣípààrọ̀", "all_transactions": "Gbogbo àwọn àránṣẹ́", @@ -113,7 +114,7 @@ "change_currency": "Pààrọ̀ irú owó", "change_current_node": "Ṣé ó dá yín lójú pé ẹ fẹ́ pààrọ̀ apẹka lọ́wọ́ sí ${node}?", "change_current_node_title": "Pààrọ̀ apẹka lọwọ́", - "change_exchange_provider": "Pààrọ̀ Ilé Ìfowóṣòwò", + "change_exchange_provider": "Yipada olupese Swap", "change_language": "Pààrọ̀ èdè", "change_language_to": "Pààrọ̀ èdè sí ${language}?", "change_password": "Pààrọ̀ ọ̀rọ̀ aṣínà", @@ -130,6 +131,7 @@ "choose_one": "Ẹ yàn kan", "choose_relay": "Jọwọ yan yii lati lo", "choose_wallet_currency": "Ẹ jọ̀wọ́, yàn irú owó ti àpamọ́wọ́ yín:", + "choose_wallet_group": "Yan ẹgbẹ ogiri", "clear": "Pa gbogbo nǹkan", "clearnet_link": "Kọja ilọ oke", "close": "sunmo", @@ -142,6 +144,7 @@ "confirm_delete_wallet": "Ìṣe yìí máa yọ àpamọ́wọ́ yìí kúrò. Ṣé ẹ fẹ́ tẹ̀síwájú?", "confirm_fee_deduction": "Jẹrisi iyọkuro owo", "confirm_fee_deduction_content": "Ṣe o gba lati yọkuro idiyele naa kuro ni iṣejade?", + "confirm_passphrase": "Jẹrisi kọwe", "confirm_sending": "Jẹ́rìí sí ránṣẹ́", "confirm_silent_payments_switch_node": "Ilode rẹ ti lọwọlọwọ ko ṣe atilẹyin awọn sisanwo ti o dakẹ \\ owet apamọwọ yoo yipada si oju-ọrọ ibaramu, o kan fun Scning", "confirmations": "Àwọn ẹ̀rí", @@ -160,6 +163,7 @@ "contact_name": "Orúkọ olùbásọ̀rọ̀", "contact_support": "Bá ìranlọ́wọ́ sọ̀rọ̀", "continue_text": "Tẹ̀síwájú", + "contract_warning": "Adirẹsi adehun adehun yii ti samisi bi arekereke. Jọwọ ṣe ilana pẹlu iṣọra.", "contractName": "Orukọ adehun", "contractSymbol": "Aami adehun", "copied_key_to_clipboard": "Ti ṣeda ${key} sí àtẹ àkọsílẹ̀", @@ -175,6 +179,7 @@ "create_invoice": "Ṣe iwe iwe", "create_new": "Dá àpamọ́wọ́ tuntun", "create_new_account": "Dá àkáǹtì títun", + "create_new_seed": "Ṣẹda irugbin tuntun", "creating_new_wallet": "Ń dá àpamọ́wọ́ títun", "creating_new_wallet_error": "Àṣìṣe: ${description}", "creation_date": "Ọjọ ẹda", @@ -195,6 +200,7 @@ "delete_wallet_confirm_message": "Ṣó dá ẹ lójú pé ẹ fẹ́ pa àpamọ́wọ́ ${wallet_name}?", "deleteConnectionConfirmationPrompt": "Ṣe o da ọ loju pe o fẹ paarẹ asopọ si", "denominations": "Awọn ede", + "derivationpath": "Ọna Deriji", "descending": "Sọkalẹ", "description": "Apejuwe", "destination_tag": "Orúkọ tí ìbí tó a ránṣẹ́ sí:", @@ -207,6 +213,7 @@ "disable_buy": "Ko iṣọrọ ọja", "disable_cake_2fa": "Ko 2FA Cake sii", "disable_exchange": "Pa ilé pàṣípààrọ̀", + "disable_fee_api_warning": "Nipa yiyi eyi kuro, awọn oṣuwọn owo naa le jẹ aiṣe deede ni awọn ọrọ kan, nitorinaa o le pari apọju tabi awọn idiyele ti o ni agbara fun awọn iṣowo rẹ", "disable_fiat": "Pa owó tí ìjọba pàṣẹ wa lò", "disable_sell": "Ko iṣọrọ iṣọrọ", "disableBatteryOptimization": "Mu Ifasi batiri", @@ -229,8 +236,10 @@ "edit_token": "Ṣatunkọ àmi", "electrum_address_disclaimer": "A dá àwọn àdírẹ́sì títun ní gbogbo àwọn ìgbà t'ẹ́ lo ó kan ṣùgbọ́n ẹ lè tẹ̀síwájú lo àwọn àdírẹ́sì tẹ́lẹ̀tẹ́lẹ̀.", "email_address": "Àdírẹ́sì ímeèlì", + "enable": "Mu ṣiṣẹ", + "enable_mempool_api": "Mempool API fun awọn owo deede ati awọn ọjọ", "enable_replace_by_fee": "Mu ki o rọpo", - "enable_silent_payments_scanning": "Mu ki awọn sisanwo ipalọlọ", + "enable_silent_payments_scanning": "Bẹrẹ awọn sisanwo ipalọlọ, titi ti o fi de opin", "enabled": "Wọ́n tíwọn ti tan", "enter_amount": "Tẹ̀ iye", "enter_backup_password": "Tẹ̀ ọ̀rọ̀ aṣínà ti ẹ̀dà ḿbí", @@ -273,8 +282,8 @@ "etherscan_history": "Etherscan itan", "event": "Iṣẹlẹ", "events": "Awọn iṣẹlẹ", - "exchange": "Pàṣípààrọ̀", - "exchange_incorrect_current_wallet_for_xmr": "T'ẹ́ bá fẹ́ pàṣípààrọ̀ XMR láti ìyókù owó Cake Wallet yín, ẹ jọ̀wọ́ kọ́kọ́ sún àpamọ́wọ́ Monero mọ́.", + "exchange": "Eepo", + "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ọ!", "exchange_result_confirm": "T'ẹ́ bá tẹ̀ jẹ́rìí, ẹ máa fi ${fetchingLabel} ${from} ránṣẹ́ láti àpamọ́wọ́ yín t'á pe ${walletName} sí àdírẹ́sì t'ó ṣàfihàn òun lísàlẹ̀. Tàbí ẹ lè fi àpamọ́wọ́ mìíràn yín ránṣẹ́ sí àdírẹ́sì / àmì ìlujá lísàlẹ̀.\n\nẸ jọ̀wọ́ tẹ̀ jẹ́rìí́ tẹ̀síwájú tàbí padà sọ́dọ̀ pààrọ̀ iye náà.", @@ -292,6 +301,8 @@ "failed_authentication": "Ìfẹ̀rílàdí pipòfo. ${state_error}", "faq": "Àwọn ìbéèrè l'a máa ń bèèrè", "features": "Awọn ẹya", + "fee_less_than_min": "Owo ti a ti yan kere ju ti o kere ju, jọwọ mu awọn owo naa pọ si lati ni anfani lati firanṣẹ iṣowo naa", + "fee_rate": "Oṣuwọn owo ọya", "fetching": "ń wá", "fiat_api": "Ojú ètò áàpù owó tí ìjọba pàṣẹ wa lò", "fiat_balance": "Fiat Iwontunws.funfun", @@ -299,7 +310,7 @@ "fill_code": "Ẹ jọ̀wọ́ tẹ̀ ọ̀rọ̀ ìjẹ́rìísí t'á ti ránṣẹ́ sí ímeèlì yín.", "filter_by": "Ṣẹ́ láti", "first_wallet_text": "Àpamọ́wọ́ t'á fi Monero, Bitcoin, Ethereum, Litecoin, àti Haven pamọ́ wà pa", - "fixed_pair_not_supported": "A kì í ṣe k'á fi àwọn ilé pàṣípààrọ̀ yìí ṣe pàṣípààrọ̀ irú owó méji yìí", + "fixed_pair_not_supported": "Bata ti o wa titi ko ṣe atilẹyin pẹlu awọn iṣẹ SWAP ti o yan", "fixed_rate": "Iye t'á ṣẹ́ owó sí ò ní pààrọ̀", "fixed_rate_alert": "Ẹ lè tẹ̀ iye owó tó ń bọ̀ tí iye t'a ṣẹ́ owó sí bá is checked. Ṣé ẹ fẹ́ sún ipò ti iye t'á ṣẹ́ owó sí ò ní pààrọ̀ mọ́?", "forgot_password": "Ẹ ti gbàgbé ọ̀rọ̀ aṣínà", @@ -355,6 +366,20 @@ "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ẹ", "light_theme": "Funfun bí eérú", + "litecoin_enable_mweb_sync": "Mu mweb ọlọjẹ", + "litecoin_mweb": "Mweb", + "litecoin_mweb_always_scan": "Ṣeto mweb nigbagbogbo n ṣayẹwo", + "litecoin_mweb_description": "Mweb jẹ ilana ilana tuntun ti o mu iyara wa yiyara, din owo, ati awọn iṣowo ikọkọ diẹ sii si Livcoin", + "litecoin_mweb_dismiss": "Tuka", + "litecoin_mweb_display_card": "Fihan kaadi Mweb", + "litecoin_mweb_enable_later": "O le yan lati ṣiṣẹ Mweb lẹẹkansi labẹ awọn eto ifihan.", + "litecoin_mweb_pegin": "Peg in", + "litecoin_mweb_pegout": "Peg jade", + "litecoin_mweb_scanning": "Mweb scanning", + "litecoin_mweb_settings": "Awọn eto Mweb", + "litecoin_mweb_warning": "Lilo Mweb yoo wa lakoko igbasilẹ ~ 600MB ti data, o le gba to iṣẹju 30 da lori iyara nẹtiwọọki. Awọn data akọkọ yii yoo ṣe igbasilẹ lẹẹkan si ki o wa fun gbogbo awọn Wolinkun LiveCooin", + "litecoin_what_is_mweb": "Kini mweb?", + "live_fee_rates": "Awọn oṣuwọn Owo laaye laaye nipasẹ API", "load_more": "Ẹru diẹ sii", "loading_your_wallet": "A ń ṣí àpamọ́wọ́ yín", "login": "Orúkọ", @@ -384,6 +409,8 @@ "monero_light_theme": "Monero Light Akori", "moonpay_alert_text": "Iye owó kò gbọ́dọ̀ kéré ju ${minAmount} ${fiatCurrency}", "more_options": "Ìyàn àfikún", + "mweb_confirmed": "Jẹrisi Mweb", + "mweb_unconfirmed": "Ajopo Mweb", "name": "Oruko", "nano_current_rep": "Aṣoju lọwọlọwọ", "nano_gpt_thanks_message": "O ṣeun fun lilo Nonnogt! Ranti lati tẹle pada si ẹrọ lilọ kiri ayelujara lẹhin iṣowo rẹ pari!", @@ -413,7 +440,7 @@ "node_test": "Dánwò", "nodes": "Àwọn apẹka", "nodes_list_reset_to_default_message": "Ṣé ó dá yín lójú pé ẹ fẹ́ yí àwọn ààtò padà?", - "none_of_selected_providers_can_exchange": "Àwọn ilé pàṣípààrọ̀ yíyàn kò lè ṣe pàṣípààrọ̀ yìí", + "none_of_selected_providers_can_exchange": "Ko si ọkan ninu awọn olupese ti a yan le ṣe ina yii", "noNFTYet": "Ko si awọn NFT sibẹsibẹ", "normal": "Deede", "note_optional": "Àkọsílẹ̀ (ìyàn nìyí)", @@ -443,6 +470,7 @@ "overwrite_amount": "Pààrọ̀ iye owó", "pairingInvalidEvent": "Pipọpọ Iṣẹlẹ Ti ko tọ", "passphrase": "Ọrọ kukuru (iyan)", + "passphrases_doesnt_match": "Awọn ọrọ kukuru ko baamu, jọwọ gbiyanju lẹẹkansi", "password": "Ọ̀rọ̀ aṣínà", "paste": "Fikún ẹ̀dà yín", "pause_wallet_creation": "Agbara lati ṣẹda Haven Wallet ti wa ni idaduro lọwọlọwọ.", @@ -594,8 +622,11 @@ "seed_share": "Pín hóró", "seed_title": "Hóró", "seedtype": "Irugbin-seetypu", + "seedtype_alert_content": "Pinpin awọn irugbin pẹlu awọn gedo miiran ṣee ṣe pẹlu Bip39 irugbin.", + "seedtype_alert_title": "Ṣajọpọ Seeytype", "seedtype_legacy": "Legacy (awọn ọrọ 25)", "seedtype_polyseed": "Polyseed (awọn ọrọ 16)", + "seedtype_wownero": "Wowero (awọn ọrọ 14)", "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.", @@ -606,6 +637,7 @@ "send": "Ránṣẹ́", "send_address": "${cryptoCurrency} àdírẹ́sì", "send_amount": "Iye:", + "send_change_to_you": "Yipada, si ọ:", "send_creating_transaction": "Ńṣe àránṣẹ́", "send_error_currency": "Ó yẹ kí òǹkà dá wà nínu iye", "send_error_minimum_value": "Ránṣẹ́ owó kò kéré dé 0.01", @@ -659,6 +691,7 @@ "setup_your_debit_card": "Dá àwọn káàdì ìrajà yín", "share": "Pinpin", "share_address": "Pín àdírẹ́sì", + "shared_seed_wallet_groups": "Awọn ẹgbẹ ti a pin irugbin", "show_details": "Fi ìsọfúnni kékeré hàn", "show_keys": "Wo hóró / àwọn kọ́kọ́rọ́", "show_market_place": "Wa Sopọ Pataki", @@ -672,6 +705,7 @@ "signature_invalid_error": "Ibuwọlu ko wulo fun ifiranṣẹ ti a fun", "signTransaction": "Wole Idunadura", "signup_for_card_accept_terms": "Ẹ f'orúkọ sílẹ̀ láti gba káàdì àti àjọrò.", + "silent_payment": "Isanwo dakẹ", "silent_payments": "Awọn sisanwo ipalọlọ", "silent_payments_always_scan": "Ṣeto awọn sisanwo ipalọlọ nigbagbogbo n ṣatunṣe", "silent_payments_disclaimer": "Awọn adirẹsi tuntun kii ṣe awọn idanimọ tuntun. O jẹ yiyan ti idanimọ ti o wa pẹlu aami oriṣiriṣi.", @@ -682,6 +716,7 @@ "silent_payments_scanned_tip": "Ṣayẹwo si sample! (${tip})", "silent_payments_scanning": "Awọn sisanwo ipalọlọ", "silent_payments_settings": "Awọn eto isanwo ti o dakẹ", + "single_seed_wallets_group": "Awọn Wowei Awọn gige", "slidable": "Slidable", "sort_by": "Sa pelu", "spend_key_private": "Kọ́kọ́rọ́ sísan (àdáni)", @@ -704,12 +739,13 @@ "switchToEVMCompatibleWallet": "Jọwọ yipada si apamọwọ ibaramu EVM ki o tun gbiyanju lẹẹkansi (Ethereum, Polygon)", "symbol": "Aami", "sync_all_wallets": "Muṣiṣẹpọ gbogbo awọn Woleti", + "sync_status_attempting_scan": "Igbiyanju ọlọjẹ", "sync_status_attempting_sync": "Ń GBÌYÀNJÚ MÚDỌ́GBA", "sync_status_connected": "TI DÁRAPỌ̀ MỌ́", "sync_status_connecting": "Ń DÁRAPỌ̀ MỌ́", "sync_status_failed_connect": "ÌKÀNPỌ̀ TI KÚ", "sync_status_not_connected": "KÒ TI DÁRAPỌ̀ MỌ́ Ọ", - "sync_status_starting_scan": "Bibẹrẹ ọlọjẹ", + "sync_status_starting_scan": "Ibẹrẹ ọlọjẹ (lati ${height})", "sync_status_starting_sync": "Ń BẸ̀RẸ̀ RẸ́", "sync_status_syncronized": "TI MÚDỌ́GBA", "sync_status_syncronizing": "Ń MÚDỌ́GBA", @@ -796,6 +832,7 @@ "trusted": "A ti fọkàn ẹ̀ tán", "tx_commit_exception_no_dust_on_change": "Iṣowo naa ti kọ pẹlu iye yii. Pẹlu awọn owó wọnyi o le firanṣẹ ${min} laisi ayipada tabi ${max} ni iyipada iyipada.", "tx_commit_failed": "Idunadura iṣowo kuna. Jọwọ kan si atilẹyin.", + "tx_commit_failed_no_peers": "Idunadura kuna lati wa igbohungbe, jọwọ gbiyanju lẹẹkansi ni iṣẹju keji tabi bẹẹ", "tx_invalid_input": "O nlo iru titẹ nkan ti ko tọ fun iru isanwo yii", "tx_no_dust_exception": "Iṣowo naa ni kọ nipa fifiranṣẹ iye ti o kere ju. Jọwọ gbiyanju pọ si iye naa.", "tx_not_enough_inputs_exception": "Ko to awọn titẹsi to. Jọwọ yan diẹ sii labẹ iṣakoso owo", @@ -840,8 +877,16 @@ "view_transaction_on": "Wo pàṣípààrọ̀ lórí ", "voting_weight": "Idibo iwuwo", "waitFewSecondForTxUpdate": "Fi inurere duro fun awọn iṣeju diẹ fun idunadura lati ṣe afihan ninu itan-akọọlẹ iṣowo", + "wallet_group": "Ẹgbẹ apamọwọ", + "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_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_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ọ́", "wallet_list_create_new_wallet": "Ṣe àpamọ́wọ́ títun", + "wallet_list_edit_group_name": "Ṣatunṣe Orukọ Ẹgbẹ", "wallet_list_edit_wallet": "Ṣatunkọ apamọwọ", "wallet_list_failed_to_load": "Ti kùnà ṣí́ àpamọ́wọ́ ${wallet_name}. ${error}", "wallet_list_failed_to_remove": "Ti kùnà yọ ${wallet_name} àpamọ́wọ́ kúrò. ${error}", diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index c864a529b..65047b4fe 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -36,6 +36,7 @@ "agree": "同意", "agree_and_continue": "同意并继续", "agree_to": "创建账户即表示您同意 ", + "alert_notice": "注意", "all": "全部", "all_trades": "所有的变化", "all_transactions": "所有交易", @@ -113,7 +114,7 @@ "change_currency": "更改币种", "change_current_node": "您确定将当前节点更改为 ${node}?", "change_current_node_title": "更改当前节点", - "change_exchange_provider": "更改交易所", + "change_exchange_provider": "更改交换提供商", "change_language": "修改语言", "change_language_to": "修改语言为 ${language}?", "change_password": "更改密码", @@ -130,6 +131,7 @@ "choose_one": "选一个", "choose_relay": "请选择要使用的继电器", "choose_wallet_currency": "请选择钱包货币:", + "choose_wallet_group": "选择钱包组", "clear": "清空", "clearnet_link": "明网链接", "close": "关闭", @@ -142,6 +144,7 @@ "confirm_delete_wallet": "此操作将刪除此钱包。确定吗?", "confirm_fee_deduction": "确认费用扣除", "confirm_fee_deduction_content": "您是否同意从产出中扣除费用?", + "confirm_passphrase": "确认密码", "confirm_sending": "确认发送", "confirm_silent_payments_switch_node": "您当前的节点不支持无声付款\\ ncake钱包将切换到兼容节点,仅用于扫描", "confirmations": "确认", @@ -160,6 +163,7 @@ "contact_name": "联系人姓名", "contact_support": "联系支持", "continue_text": "继续", + "contract_warning": "该合同地址已被标记为潜在的欺诈性。请谨慎处理。", "contractName": "合约名称", "contractSymbol": "合约符号", "copied_key_to_clipboard": "复制 ${key} 到剪贴板", @@ -175,6 +179,7 @@ "create_invoice": "创建发票", "create_new": "创建新钱包", "create_new_account": "建立新账户", + "create_new_seed": "创建新种子", "creating_new_wallet": "创建新钱包", "creating_new_wallet_error": "错误: ${description}", "creation_date": "创建日期", @@ -195,6 +200,7 @@ "delete_wallet_confirm_message": "您确定要删除 ${wallet_name} 钱包吗?", "deleteConnectionConfirmationPrompt": "您确定要删除与", "denominations": "教派", + "derivationpath": "推导路径", "descending": "下降", "description": "描述", "destination_tag": "目标Tag:", @@ -207,6 +213,7 @@ "disable_buy": "禁用购买操作", "disable_cake_2fa": "禁用蛋糕 2FA", "disable_exchange": "禁用交换", + "disable_fee_api_warning": "通过将其关闭,在某些情况下,收费率可能不准确,因此您最终可能会超额付款或支付交易费用", "disable_fiat": "禁用法令", "disable_sell": "禁用卖出操作", "disableBatteryOptimization": "禁用电池优化", @@ -228,8 +235,10 @@ "edit_token": "编辑令牌", "electrum_address_disclaimer": "每次您使用一个地址时,我们都会生成新地址,但之前的地址仍然有效", "email_address": "电子邮件地址", + "enable": "使能够", + "enable_mempool_api": "Mempool API获得准确的费用和日期", "enable_replace_by_fee": "启用by-Fee替换", - "enable_silent_payments_scanning": "启用无声付款扫描", + "enable_silent_payments_scanning": "开始扫描无声付款,直到达到提示", "enabled": "启用", "enter_amount": "输入金额", "enter_backup_password": "在此处输入備用密码", @@ -272,8 +281,8 @@ "etherscan_history": "以太扫描历史", "event": "事件", "events": "活动", - "exchange": "兑换", - "exchange_incorrect_current_wallet_for_xmr": "如果要从Cake Wallet Monero余额中兑换XMR,请先切换到Monero钱包。", + "exchange": "交换", + "exchange_incorrect_current_wallet_for_xmr": "如果您想从蛋糕钱包Monero余额中交换XMR,请先切换到Monero Wallet。", "exchange_new_template": "新模板", "exchange_provider_unsupported": "${providerName}不再支持!", "exchange_result_confirm": "点击确认 您将发送 ${fetchingLabel} ${from} 从你的钱包里 ${walletName} 到下面显示的地址。 或者您可以从外部钱包发送到以下地址/ QR码。\n\n请按确认继续或返回以更改金额", @@ -291,6 +300,8 @@ "failed_authentication": "身份验证失败. ${state_error}", "faq": "FAQ", "features": "特征", + "fee_less_than_min": "选定费用小于最低费用,请增加费用以发送交易", + "fee_rate": "费率", "fetching": "正在获取", "fiat_api": "法币API", "fiat_balance": "法币余额", @@ -298,7 +309,7 @@ "fill_code": "请填写提供给您邮箱的验证码", "filter_by": "过滤", "first_wallet_text": "适用于门罗币、比特币、以太坊、莱特币和避风港的超棒钱包", - "fixed_pair_not_supported": "所选交易所不支持此固定货币对", + "fixed_pair_not_supported": "所选的交换服务不支持这对固定对", "fixed_rate": "固定汇率", "fixed_rate_alert": "选中固定汇率模式后,您将可以输入接收金额。 您要切换到固定汇率模式吗?", "forgot_password": "忘记密码", @@ -354,6 +365,20 @@ "ledger_error_wrong_app": "请确保您在分类帐中操作正确的应用程序", "ledger_please_enable_bluetooth": "请启用蓝牙来检测您的分类帐", "light_theme": "艳丽", + "litecoin_enable_mweb_sync": "启用MWEB扫描", + "litecoin_mweb": "MWEB", + "litecoin_mweb_always_scan": "设置MWEB总是扫描", + "litecoin_mweb_description": "MWEB是一项新协议,它将更快,更便宜和更多的私人交易带给Litecoin", + "litecoin_mweb_dismiss": "解雇", + "litecoin_mweb_display_card": "显示MWEB卡", + "litecoin_mweb_enable_later": "您可以选择在显示设置下再次启用MWEB。", + "litecoin_mweb_pegin": "钉进", + "litecoin_mweb_pegout": "昏倒", + "litecoin_mweb_scanning": "MWEB扫描", + "litecoin_mweb_settings": "MWEB设置", + "litecoin_mweb_warning": "使用MWEB最初将下载约600MB的数据,并且最多可能需要30分钟的时间,具体取决于网络速度。此初始数据只能下载一次,并适用于所有莱特币钱包", + "litecoin_what_is_mweb": "什么是MWEB?", + "live_fee_rates": "通过API的实时费率", "load_more": "装载更多", "loading_your_wallet": "加载您的钱包", "login": "登录", @@ -383,6 +408,8 @@ "monero_light_theme": "门罗币浅色主题", "moonpay_alert_text": "金额的价值必须大于或等于 ${minAmount} ${fiatCurrency}", "more_options": "更多选项", + "mweb_confirmed": "确认的MWEB", + "mweb_unconfirmed": "未经证实的MWEB", "name": "姓名", "nano_current_rep": "当前代表", "nano_gpt_thanks_message": "感谢您使用Nanogpt!事务完成后,请记住回到浏览器!", @@ -412,7 +439,7 @@ "node_test": "测试", "nodes": "节点", "nodes_list_reset_to_default_message": "您确定要将设置重设为默认值吗?", - "none_of_selected_providers_can_exchange": "选定的供应商都不能进行此交换", + "none_of_selected_providers_can_exchange": "选定的提供商都无法进行此交换", "noNFTYet": "还没有 NFT", "normal": "普通的", "note_optional": "注释(可选)", @@ -442,6 +469,7 @@ "overwrite_amount": "Overwrite amount", "pairingInvalidEvent": "配对无效事件", "passphrase": "密码(可选)", + "passphrases_doesnt_match": "密码不匹配,请重试", "password": "密码", "paste": "粘贴", "pause_wallet_creation": "创建 Haven 钱包的功能当前已暂停。", @@ -593,8 +621,11 @@ "seed_share": "分享种子", "seed_title": "种子", "seedtype": "籽粒", + "seedtype_alert_content": "只有BIP39籽粒可以与其他钱包共享种子。", + "seedtype_alert_title": "籽粒警报", "seedtype_legacy": "遗产(25个单词)", "seedtype_polyseed": "多种物品(16个单词)", + "seedtype_wownero": "沃恩罗(14个单词)", "select_backup_file": "选择备份文件", "select_buy_provider_notice": "在上面选择买入提供商。您可以通过在应用程序设置中设置默认的购买提供商来跳过此屏幕。", "select_destination": "请选择备份文件的目的地。", @@ -605,6 +636,7 @@ "send": "发送", "send_address": "${cryptoCurrency} 地址", "send_amount": "金额:", + "send_change_to_you": "改变,向您:", "send_creating_transaction": "创建交易", "send_error_currency": "货币只能包含数字", "send_error_minimum_value": "最小金额为0.01", @@ -658,6 +690,7 @@ "setup_your_debit_card": "设置你的借记卡", "share": "分享", "share_address": "分享地址", + "shared_seed_wallet_groups": "共享种子钱包组", "show_details": "显示详细信息", "show_keys": "显示种子/密钥", "show_market_place": "显示市场", @@ -671,6 +704,7 @@ "signature_invalid_error": "签名对于给出的消息无效", "signTransaction": "签署交易", "signup_for_card_accept_terms": "注册卡并接受条款。", + "silent_payment": "无声付款", "silent_payments": "无声付款", "silent_payments_always_scan": "设置无声付款总是扫描", "silent_payments_disclaimer": "新地址不是新的身份。这是重复使用具有不同标签的现有身份。", @@ -681,6 +715,7 @@ "silent_payments_scanned_tip": "扫描到小费! (${tip})", "silent_payments_scanning": "无声付款扫描", "silent_payments_settings": "无声付款设置", + "single_seed_wallets_group": "单个种子钱包", "slidable": "可滑动", "sort_by": "排序方式", "spend_key_private": "Spend 密钥 (私钥)", @@ -703,12 +738,13 @@ "switchToEVMCompatibleWallet": "请切换到 EVM 兼容钱包并重试(以太坊、Polygon)", "symbol": "象征", "sync_all_wallets": "同步所有钱包", + "sync_status_attempting_scan": "尝试扫描", "sync_status_attempting_sync": "嘗試同步", "sync_status_connected": "已连接", "sync_status_connecting": "连接中", "sync_status_failed_connect": "断线", "sync_status_not_connected": "未连接", - "sync_status_starting_scan": "开始扫描", + "sync_status_starting_scan": "启动扫描(来自 ${height})", "sync_status_starting_sync": "开始同步", "sync_status_syncronized": "已同步", "sync_status_syncronizing": "正在同步", @@ -795,6 +831,7 @@ "trusted": "值得信赖", "tx_commit_exception_no_dust_on_change": "交易被此金额拒绝。使用这些硬币,您可以发送${min}无需更改或返回${max}的变化。", "tx_commit_failed": "交易承诺失败。请联系支持。", + "tx_commit_failed_no_peers": "交易无法广播,请在一秒钟左右的时间内重试", "tx_invalid_input": "您正在使用错误的输入类型进行此类付款", "tx_no_dust_exception": "通过发送太小的金额来拒绝交易。请尝试增加金额。", "tx_not_enough_inputs_exception": "没有足够的输入。请在硬币控制下选择更多", @@ -839,8 +876,16 @@ "view_transaction_on": "View Transaction on ", "voting_weight": "投票权重", "waitFewSecondForTxUpdate": "请等待几秒钟,交易才会反映在交易历史记录中", + "wallet_group": "钱包组", + "wallet_group_description_four": "创建一个带有全新种子的钱包。", + "wallet_group_description_one": "在蛋糕钱包中,您可以创建一个", + "wallet_group_description_three": "查看可用的钱包和/或钱包组屏幕。或选择", + "wallet_group_description_two": "通过选择现有的钱包与种子共享。每个钱包组都可以包含每种货币类型的单个钱包。\n\n您可以选择", + "wallet_group_empty_state_text_one": "看起来您没有任何兼容的钱包组!\n\n tap", + "wallet_group_empty_state_text_two": "下面是一个新的。", "wallet_keys": "钱包种子/密钥", "wallet_list_create_new_wallet": "创建新钱包", + "wallet_list_edit_group_name": "编辑组名称", "wallet_list_edit_wallet": "编辑钱包", "wallet_list_failed_to_load": "加载失败 ${wallet_name} 钱包. ${error}", "wallet_list_failed_to_remove": "删除失败 ${wallet_name} 钱包. ${error}", diff --git a/scripts/android/app_env.sh b/scripts/android/app_env.sh index 324d2d2ae..8beffbcc2 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.16.4" -MONERO_COM_BUILD_NUMBER=98 +MONERO_COM_VERSION="1.17.0" +MONERO_COM_BUILD_NUMBER=103 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.19.4" -CAKEWALLET_BUILD_NUMBER=225 +CAKEWALLET_VERSION="4.20.1" +CAKEWALLET_BUILD_NUMBER=233 CAKEWALLET_BUNDLE_ID="com.cakewallet.cake_wallet" CAKEWALLET_PACKAGE="com.cakewallet.cake_wallet" CAKEWALLET_SCHEME="cakewallet" diff --git a/scripts/android/build_all.sh b/scripts/android/build_all.sh index ec70f02a6..ad4ec984b 100755 --- a/scripts/android/build_all.sh +++ b/scripts/android/build_all.sh @@ -10,6 +10,7 @@ DIR=$(dirname "$0") case $APP_ANDROID_TYPE in "monero.com") $DIR/build_monero_all.sh ;; "cakewallet") $DIR/build_monero_all.sh - $DIR/build_haven_all.sh ;; + $DIR/build_haven_all.sh + $DIR/build_mwebd.sh ;; "haven") $DIR/build_haven_all.sh ;; esac diff --git a/scripts/android/build_mwebd.sh b/scripts/android/build_mwebd.sh new file mode 100755 index 000000000..4434e30f1 --- /dev/null +++ b/scripts/android/build_mwebd.sh @@ -0,0 +1,22 @@ +if [[ "$1" == "--dont-install" ]]; then + echo "Skipping Go installation as per --dont-install flag" +else + # install go > 1.23: + wget https://go.dev/dl/go1.23.1.linux-amd64.tar.gz + sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go1.23.1.linux-amd64.tar.gz + export PATH=$PATH:/usr/local/go/bin + export PATH=$PATH:~/go/bin + go install golang.org/x/mobile/cmd/gomobile@latest + gomobile init +fi + +# build mwebd: +git clone https://github.com/ltcmweb/mwebd +cd mwebd +git reset --hard 555349415f76a42ec5c76152b64c4ab9aabc448f +gomobile bind -target=android -androidapi 21 . +mkdir -p ../../../cw_mweb/android/libs/ +mv ./mwebd.aar $_ +# cleanup: +cd .. +rm -rf mwebd \ No newline at end of file diff --git a/scripts/ios/app_config.sh b/scripts/ios/app_config.sh index 67375c914..2232491a6 100755 --- a/scripts/ios/app_config.sh +++ b/scripts/ios/app_config.sh @@ -22,7 +22,7 @@ cp -rf ./ios/Runner/InfoBase.plist ./ios/Runner/Info.plist /usr/libexec/PlistBuddy -c "Add :CFBundleURLTypes:1:CFBundleURLName string ${APP_IOS_TYPE}" ./ios/Runner/Info.plist /usr/libexec/PlistBuddy -c "Add :CFBundleURLTypes:1:CFBundleURLSchemes array" ./ios/Runner/Info.plist /usr/libexec/PlistBuddy -c "Add :CFBundleURLTypes:1:CFBundleURLSchemes: string ${APP_IOS_TYPE}" ./ios/Runner/Info.plist -sed -i '' "s/\${PRODUCT_BUNDLE_IDENTIFIER}/${APP_IOS_BUNDLE_ID}/g" ./ios/Runner.xcodeproj/project.pbxproj +sed -i '' "s/PRODUCT_BUNDLE_IDENTIFIER = .*;/PRODUCT_BUNDLE_IDENTIFIER = $APP_IOS_BUNDLE_ID;/g" ./ios/Runner.xcodeproj/project.pbxproj CONFIG_ARGS="" diff --git a/scripts/ios/app_env.sh b/scripts/ios/app_env.sh index 1405f7939..bc3b39747 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.16.4" -MONERO_COM_BUILD_NUMBER=96 +MONERO_COM_VERSION="1.17.0" +MONERO_COM_BUILD_NUMBER=101 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="4.19.4" -CAKEWALLET_BUILD_NUMBER=263 +CAKEWALLET_VERSION="4.20.1" +CAKEWALLET_BUILD_NUMBER=277 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" HAVEN_NAME="Haven" diff --git a/scripts/ios/build_all.sh b/scripts/ios/build_all.sh index 565679e2d..ba5c55a1f 100755 --- a/scripts/ios/build_all.sh +++ b/scripts/ios/build_all.sh @@ -9,6 +9,6 @@ DIR=$(dirname "$0") case $APP_IOS_TYPE in "monero.com") $DIR/build_monero_all.sh ;; - "cakewallet") $DIR/build_monero_all.sh && $DIR/build_haven.sh ;; + "cakewallet") $DIR/build_monero_all.sh && $DIR/build_haven.sh && $DIR/build_mwebd.sh ;; "haven") $DIR/build_haven_all.sh ;; esac diff --git a/scripts/ios/build_mwebd.sh b/scripts/ios/build_mwebd.sh new file mode 100755 index 000000000..e13c4931c --- /dev/null +++ b/scripts/ios/build_mwebd.sh @@ -0,0 +1,20 @@ +#!/bin/bash +if [[ "$1" == "--dont-install" ]]; then + echo "Skipping Go installation as per --dont-install flag" +else + # install go > 1.23: + brew install go + export PATH=$PATH:~/go/bin + go install golang.org/x/mobile/cmd/gomobile@latest + gomobile init +fi + +# build mwebd: +git clone https://github.com/ltcmweb/mwebd +cd mwebd +git reset --hard 555349415f76a42ec5c76152b64c4ab9aabc448f +gomobile bind -target=ios . +mv -fn ./Mwebd.xcframework ../../../ios/ +# cleanup: +cd .. +rm -rf mwebd \ No newline at end of file diff --git a/scripts/linux/app_env.sh b/scripts/linux/app_env.sh index bc2965193..2dabf083b 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.9.4" -CAKEWALLET_BUILD_NUMBER=31 +CAKEWALLET_VERSION="1.10.1" +CAKEWALLET_BUILD_NUMBER=37 if ! [[ " ${TYPES[*]} " =~ " ${APP_LINUX_TYPE} " ]]; then echo "Wrong app type." diff --git a/scripts/macos/app_config.sh b/scripts/macos/app_config.sh index b8785a9be..92a8636bd 100755 --- a/scripts/macos/app_config.sh +++ b/scripts/macos/app_config.sh @@ -28,7 +28,7 @@ sed -i '' "s/\${BUNDLE_ID}/${APP_MACOS_BUNDLE_ID}/g" ./macos/Runner/DebugProfile sed -i '' "s/\${BUNDLE_ID}/${APP_MACOS_BUNDLE_ID}/g" ./macos/Runner/Release.entitlements sed -i '' "s/\${BUNDLE_ID}/${APP_MACOS_BUNDLE_ID}/g" ./macos/Runner/Runner.entitlements sed -i '' "s/\${PRODUCT_NAME}/${APP_MACOS_NAME}/g" ./macos/Runner/Configs/AppInfo.xcconfig -sed -i '' "s/\${PRODUCT_BUNDLE_IDENTIFIER}/${APP_MACOS_BUNDLE_ID}/g" ./macos/Runner/Configs/AppInfo.xcconfig +sed -i '' "s/PRODUCT_BUNDLE_IDENTIFIER = .*;/PRODUCT_BUNDLE_IDENTIFIER = $APP_MACOS_BUNDLE_ID;/g" ./macos/Runner/Configs/AppInfo.xcconfig CONFIG_ARGS="" case $APP_MACOS_TYPE in diff --git a/scripts/macos/app_env.sh b/scripts/macos/app_env.sh index 342831d65..026ea034b 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.6.4" -MONERO_COM_BUILD_NUMBER=28 +MONERO_COM_VERSION="1.7.0" +MONERO_COM_BUILD_NUMBER=34 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="1.12.4" -CAKEWALLET_BUILD_NUMBER=85 +CAKEWALLET_VERSION="1.13.1" +CAKEWALLET_BUILD_NUMBER=93 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" if ! [[ " ${TYPES[*]} " =~ " ${APP_MACOS_TYPE} " ]]; then diff --git a/scripts/prepare_moneroc.sh b/scripts/prepare_moneroc.sh index 94754c935..24f4d201c 100755 --- a/scripts/prepare_moneroc.sh +++ b/scripts/prepare_moneroc.sh @@ -8,7 +8,7 @@ if [[ ! -d "monero_c" ]]; then git clone https://github.com/mrcyjanek/monero_c --branch rewrite-wip cd monero_c - git checkout 5de323b1ba7387cf73973042f06383d4dbe619f5 + git checkout 6eb571ea498ed7b854934785f00fabfd0dadf75b git reset --hard git submodule update --init --force --recursive ./apply_patches.sh monero diff --git a/scripts/windows/build_exe_installer.iss b/scripts/windows/build_exe_installer.iss index 4d7838723..f65900cca 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.0.5" +#define MyAppVersion "0.1.1" #define MyAppPublisher "Cake Labs LLC" #define MyAppURL "https://cakewallet.com/" #define MyAppExeName "CakeWallet.exe" diff --git a/test_driver/integration_test.dart b/test_driver/integration_test.dart new file mode 100644 index 000000000..2e67d866f --- /dev/null +++ b/test_driver/integration_test.dart @@ -0,0 +1,33 @@ +import 'dart:convert'; + +import 'package:integration_test/integration_test_driver.dart'; +import 'package:path/path.dart' as path; + +import 'package:flutter_driver/flutter_driver.dart'; + +Future main() async { + integrationDriver( + responseDataCallback: (Map? data) async { + await fs.directory(_destinationDirectory).create(recursive: true); + + final file = fs.file( + path.join( + _destinationDirectory, + '$_testOutputFilename.json', + ), + ); + + final resultString = _encodeJson(data); + await file.writeAsString(resultString); + }, + writeResponseOnFailure: true, + ); +} + +String _encodeJson(Map? jsonObject) { + return _prettyEncoder.convert(jsonObject); +} + +const _prettyEncoder = JsonEncoder.withIndent(' '); +const _testOutputFilename = 'integration_response_data'; +const _destinationDirectory = 'integration_test'; diff --git a/tool/append_translation.dart b/tool/append_translation.dart index 24778746c..d196421e9 100644 --- a/tool/append_translation.dart +++ b/tool/append_translation.dart @@ -5,13 +5,14 @@ import 'utils/translation/translation_utils.dart'; /// flutter packages pub run tool/append_translation.dart "hello_world" "Hello World!" void main(List args) async { - if (args.length != 2) { + if (args.length < 2) { throw Exception( - 'Insufficient arguments!\n\nTry to run `./append_translation.dart greetings "Hello World!"`'); + 'Insufficient arguments!\n\nTry to run `./append_translation.dart "greetings" "Hello World!"`'); } final name = args.first; - final text = args.last; + final text = args[1]; + final force = args.last == "--force"; print('Appending "$name": "$text"'); @@ -20,7 +21,7 @@ void main(List args) async { final fileName = getArbFileName(lang); final translation = await getTranslation(text, lang); - appendStringToArbFile(fileName, name, translation); + appendStringToArbFile(fileName, name, translation, force: force); } print('Alphabetizing all files...'); diff --git a/tool/configure.dart b/tool/configure.dart index a0104c34e..97541c2fa 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -75,6 +75,7 @@ Future main(List args) async { Future generateBitcoin(bool hasImplementation) async { final outputFile = File(bitcoinOutputPath); const bitcoinCommonHeaders = """ +import 'dart:io' show Platform; import 'dart:typed_data'; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart'; @@ -84,7 +85,9 @@ import 'package:cw_core/node.dart'; import 'package:cw_core/output_info.dart'; import 'package:cw_core/pending_transaction.dart'; import 'package:cw_core/receive_page_option.dart'; +import 'package:cw_core/transaction_info.dart'; import 'package:cw_core/transaction_priority.dart'; +import 'package:cw_core/unspent_coin_type.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/unspent_transaction_output.dart'; import 'package:cw_core/wallet_base.dart'; @@ -92,8 +95,9 @@ 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_core/get_height_by_date.dart'; import 'package:hive/hive.dart'; -import 'package:ledger_flutter/ledger_flutter.dart'; +import 'package:ledger_flutter_plus/ledger_flutter_plus.dart' as ledger; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:bip39/bip39.dart' as bip39; """; @@ -101,6 +105,7 @@ import 'package:bip39/bip39.dart' as bip39; import 'package:cw_bitcoin/utils.dart'; import 'package:cw_bitcoin/electrum_derivations.dart'; import 'package:cw_bitcoin/electrum.dart'; +import 'package:cw_bitcoin/electrum_transaction_info.dart'; import 'package:cw_bitcoin/pending_bitcoin_transaction.dart'; import 'package:cw_bitcoin/bitcoin_receive_page_option.dart'; import 'package:cw_bitcoin/bitcoin_wallet.dart'; @@ -114,9 +119,9 @@ import 'package:cw_bitcoin/bitcoin_amount_format.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart'; import 'package:cw_bitcoin/litecoin_wallet_service.dart'; -import 'package:cw_core/get_height_by_date.dart'; -import 'package:cw_bitcoin/script_hash.dart'; +import 'package:cw_bitcoin/litecoin_wallet.dart'; import 'package:cw_bitcoin/bitcoin_hardware_wallet_service.dart'; +import 'package:cw_bitcoin/litecoin_hardware_wallet_service.dart'; import 'package:mobx/mobx.dart'; """; const bitcoinCwPart = "part 'cw_bitcoin.dart';"; @@ -150,7 +155,7 @@ abstract class Bitcoin { String? passphrase, }); WalletCredentials createBitcoinRestoreWalletFromWIFCredentials({required String name, required String password, required String wif, WalletInfo? walletInfo}); - WalletCredentials createBitcoinNewWalletCredentials({required String name, WalletInfo? walletInfo, String? password}); + WalletCredentials createBitcoinNewWalletCredentials({required String name, WalletInfo? walletInfo, String? password, String? passphrase, String? mnemonic, String? parentAddress}); WalletCredentials createBitcoinHardwareWalletCredentials({required String name, required HardwareAccountData accountData, WalletInfo? walletInfo}); List getWordList(); Map getWalletKeys(Object wallet); @@ -161,8 +166,7 @@ abstract class Bitcoin { int getFeeRate(Object wallet, TransactionPriority priority); Future generateNewAddress(Object wallet, String label); Future updateAddress(Object wallet,String address, String label); - Object createBitcoinTransactionCredentials(List outputs, {required TransactionPriority priority, int? feeRate}); - Object createBitcoinTransactionCredentialsRaw(List outputs, {TransactionPriority? priority, required int feeRate}); + Object createBitcoinTransactionCredentials(List outputs, {required TransactionPriority priority, int? feeRate, UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any}); String getAddress(Object wallet); List getSilentPaymentAddresses(Object wallet); @@ -176,11 +180,11 @@ abstract class Bitcoin { int formatterStringDoubleToBitcoinAmount(String amount); String bitcoinTransactionPriorityWithLabel(TransactionPriority priority, int rate, {int? customRate}); - List getUnspents(Object wallet); + List getUnspents(Object wallet, {UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any}); Future updateUnspents(Object wallet); WalletService createBitcoinWalletService( - Box walletInfoSource, Box unspentCoinSource, bool alwaysScan, bool isDirect); - WalletService createLitecoinWalletService(Box walletInfoSource, Box unspentCoinSource, bool isDirect); + Box walletInfoSource, Box unspentCoinSource, bool alwaysScan, bool isDirect); + WalletService createLitecoinWalletService(Box walletInfoSource, Box unspentCoinSource, bool alwaysScan, bool isDirect); TransactionPriority getBitcoinTransactionPriorityMedium(); TransactionPriority getBitcoinTransactionPriorityCustom(); TransactionPriority getLitecoinTransactionPriorityMedium(); @@ -194,6 +198,7 @@ abstract class Bitcoin { Future setAddressType(Object wallet, dynamic option); ReceivePageOption getSelectedAddressType(Object wallet); List getBitcoinReceivePageOptions(); + List getLitecoinReceivePageOptions(); BitcoinAddressType getBitcoinAddressType(ReceivePageOption option); bool hasSelectedSilentPayments(Object wallet); bool isBitcoinReceivePageOption(ReceivePageOption option); @@ -204,21 +209,31 @@ abstract class Bitcoin { bool isTestnet(Object wallet); Future replaceByFee(Object wallet, String transactionHash, String fee); - Future canReplaceByFee(Object wallet, String transactionHash); + Future canReplaceByFee(Object wallet, Object tx); + int getTransactionVSize(Object wallet, String txHex); Future isChangeSufficientForFee(Object wallet, String txId, String newFee); int getFeeAmountForPriority(Object wallet, TransactionPriority priority, int inputsCount, int outputsCount, {int? size}); int getEstimatedFeeWithFeeRate(Object wallet, int feeRate, int? amount, {int? outputsCount, int? size}); int feeAmountWithFeeRate(Object wallet, int feeRate, int inputsCount, int outputsCount, {int? size}); - int getHeightByDate({required DateTime date}); + Future checkIfMempoolAPIIsEnabled(Object wallet); + Future getHeightByDate({required DateTime date, bool? bitcoinMempoolAPIEnabled}); + int getLitecoinHeightByDate({required DateTime date}); Future rescan(Object wallet, {required int height, bool? doSingleScan}); Future getNodeIsElectrsSPEnabled(Object wallet); void deleteSilentPaymentAddress(Object wallet, String address); Future updateFeeRates(Object wallet); int getMaxCustomFeeRate(Object wallet); - - void setLedger(WalletBase wallet, Ledger ledger, LedgerDevice device); - Future> getHardwareWalletAccounts(LedgerViewModel ledgerVM, {int index = 0, int limit = 5}); + void setLedgerConnection(WalletBase wallet, ledger.LedgerConnection connection); + Future> getHardwareWalletBitcoinAccounts(LedgerViewModel ledgerVM, {int index = 0, int limit = 5}); + Future> getHardwareWalletLitecoinAccounts(LedgerViewModel ledgerVM, {int index = 0, int limit = 5}); + List updateOutputs(PendingTransaction pendingTransaction, List outputs); + bool txIsReceivedSilentPayment(TransactionInfo txInfo); + bool txIsMweb(TransactionInfo txInfo); + Future setMwebEnabled(Object wallet, bool enabled); + bool getMwebEnabled(Object wallet); + String? getUnusedMwebAddress(Object wallet); + String? getUnusedSegwitAddress(Object wallet); } """; @@ -293,10 +308,14 @@ class Subaddress { Subaddress({ required this.id, required this.label, - required this.address}); + required this.address, + required this.received, + required this.txCount}); final int id; final String label; final String address; + final String? received; + final int txCount; } class MoneroBalance extends Balance { @@ -554,7 +573,7 @@ abstract class Wownero { 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 createWowneroNewWalletCredentials({required String name, required String language, required bool isPolyseed, String? password}); Map getKeys(Object wallet); Object createWowneroTransactionCreationCredentials({required List outputs, required TransactionPriority priority}); Object createWowneroTransactionCreationCredentialsRaw({required List outputs, required TransactionPriority priority}); @@ -802,7 +821,7 @@ import 'package:cw_core/wallet_credentials.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_service.dart'; import 'package:hive/hive.dart'; -import 'package:ledger_flutter/ledger_flutter.dart'; +import 'package:ledger_flutter_plus/ledger_flutter_plus.dart' as ledger; import 'package:web3dart/web3dart.dart'; """; @@ -829,8 +848,8 @@ import 'package:eth_sig_util/util/utils.dart'; abstract class Ethereum { List getEthereumWordList(String language); WalletService createEthereumWalletService(Box walletInfoSource, bool isDirect); - WalletCredentials createEthereumNewWalletCredentials({required String name, WalletInfo? walletInfo, String? password}); - WalletCredentials createEthereumRestoreWalletFromSeedCredentials({required String name, required String mnemonic, required String password}); + WalletCredentials createEthereumNewWalletCredentials({required String name, WalletInfo? walletInfo, String? password, String? mnemonic, String? parentAddress, String? passphrase}); + WalletCredentials createEthereumRestoreWalletFromSeedCredentials({required String name, required String mnemonic, required String password, String? passphrase}); WalletCredentials createEthereumRestoreWalletFromPrivateKey({required String name, required String privateKey, required String password}); WalletCredentials createEthereumHardwareWalletCredentials({required String name, required HardwareAccountData hwAccountData, WalletInfo? walletInfo}); String getAddress(WalletBase wallet); @@ -868,7 +887,7 @@ abstract class Ethereum { Web3Client? getWeb3Client(WalletBase wallet); String getTokenAddress(CryptoCurrency asset); - void setLedger(WalletBase wallet, Ledger ledger, LedgerDevice device); + void setLedgerConnection(WalletBase wallet, ledger.LedgerConnection connection); Future> getHardwareWalletAccounts(LedgerViewModel ledgerVM, {int index = 0, int limit = 5}); } """; @@ -906,7 +925,7 @@ import 'package:cw_core/wallet_credentials.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_service.dart'; import 'package:hive/hive.dart'; -import 'package:ledger_flutter/ledger_flutter.dart'; +import 'package:ledger_flutter_plus/ledger_flutter_plus.dart' as ledger; import 'package:web3dart/web3dart.dart'; """; @@ -933,8 +952,8 @@ import 'package:eth_sig_util/util/utils.dart'; abstract class Polygon { List getPolygonWordList(String language); WalletService createPolygonWalletService(Box walletInfoSource, bool isDirect); - WalletCredentials createPolygonNewWalletCredentials({required String name, WalletInfo? walletInfo, String? password}); - WalletCredentials createPolygonRestoreWalletFromSeedCredentials({required String name, required String mnemonic, required String password}); + WalletCredentials createPolygonNewWalletCredentials({required String name, WalletInfo? walletInfo, String? password, String? mnemonic, String? parentAddress, String? passphrase}); + WalletCredentials createPolygonRestoreWalletFromSeedCredentials({required String name, required String mnemonic, required String password, String? passphrase}); WalletCredentials createPolygonRestoreWalletFromPrivateKey({required String name, required String privateKey, required String password}); WalletCredentials createPolygonHardwareWalletCredentials({required String name, required HardwareAccountData hwAccountData, WalletInfo? walletInfo}); String getAddress(WalletBase wallet); @@ -972,7 +991,7 @@ abstract class Polygon { Web3Client? getWeb3Client(WalletBase wallet); String getTokenAddress(CryptoCurrency asset); - void setLedger(WalletBase wallet, Ledger ledger, LedgerDevice device); + void setLedgerConnection(WalletBase wallet, ledger.LedgerConnection connection); Future> getHardwareWalletAccounts(LedgerViewModel ledgerVM, {int index = 0, int limit = 5}); } """; @@ -1020,10 +1039,10 @@ abstract class BitcoinCash { Box walletInfoSource, Box unspentCoinSource, bool isDirect); WalletCredentials createBitcoinCashNewWalletCredentials( - {required String name, WalletInfo? walletInfo, String? password}); + {required String name, WalletInfo? walletInfo, String? password, String? passphrase, String? mnemonic, String? parentAddress}); WalletCredentials createBitcoinCashRestoreWalletFromSeedCredentials( - {required String name, required String mnemonic, required String password}); + {required String name, required String mnemonic, required String password, String? passphrase}); TransactionPriority deserializeBitcoinCashTransactionPriority(int raw); @@ -1102,6 +1121,10 @@ abstract class Nano { WalletCredentials createNanoNewWalletCredentials({ required String name, String? password, + String? mnemonic, + String? parentAddress, + WalletInfo? walletInfo, + String? passphrase, }); WalletCredentials createNanoRestoreWalletFromSeedCredentials({ @@ -1109,6 +1132,7 @@ abstract class Nano { required String password, required String mnemonic, required DerivationType derivationType, + String? passphrase, }); WalletCredentials createNanoRestoreWalletFromKeysCredentials({ @@ -1217,9 +1241,9 @@ abstract class Solana { List getSolanaWordList(String language); WalletService createSolanaWalletService(Box walletInfoSource, bool isDirect); WalletCredentials createSolanaNewWalletCredentials( - {required String name, WalletInfo? walletInfo, String? password}); + {required String name, WalletInfo? walletInfo, String? password, String? mnemonic, String? parentAddress, String? passphrase}); WalletCredentials createSolanaRestoreWalletFromSeedCredentials( - {required String name, required String mnemonic, required String password}); + {required String name, required String mnemonic, required String password, String? passphrase}); WalletCredentials createSolanaRestoreWalletFromPrivateKey( {required String name, required String privateKey, required String password}); @@ -1303,8 +1327,8 @@ import 'package:cw_tron/tron_wallet_service.dart'; abstract class Tron { List getTronWordList(String language); WalletService createTronWalletService(Box walletInfoSource, bool isDirect); - WalletCredentials createTronNewWalletCredentials({required String name, WalletInfo? walletInfo, String? password}); - WalletCredentials createTronRestoreWalletFromSeedCredentials({required String name, required String mnemonic, required String password}); + WalletCredentials createTronNewWalletCredentials({required String name, WalletInfo? walletInfo, String? password, String? mnemonic, String? parentAddress, String? passphrase}); + WalletCredentials createTronRestoreWalletFromSeedCredentials({required String name, required String mnemonic, required String password, String? passphrase}); WalletCredentials createTronRestoreWalletFromPrivateKey({required String name, required String privateKey, required String password}); String getAddress(WalletBase wallet); diff --git a/tool/download_moneroc_prebuilds.dart b/tool/download_moneroc_prebuilds.dart index 58e8d4b03..9df0b4e7b 100644 --- a/tool/download_moneroc_prebuilds.dart +++ b/tool/download_moneroc_prebuilds.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:dio/dio.dart'; import 'package:archive/archive_io.dart'; @@ -47,4 +49,12 @@ Future main() async { outputStream.writeBytes(archive); } } + if (Platform.isMacOS) { + print("Generating ios framework"); + final result = Process.runSync("bash", [ + "-c", + "cd scripts/ios && ./gen_framework.sh && cd ../.." + ]); + print((result.stdout+result.stderr).toString().trim()); + } } \ No newline at end of file diff --git a/tool/utils/secret_key.dart b/tool/utils/secret_key.dart index a1d93fcf9..d3b652935 100644 --- a/tool/utils/secret_key.dart +++ b/tool/utils/secret_key.dart @@ -39,10 +39,17 @@ class SecretKey { SecretKey('moralisApiKey', () => ''), SecretKey('ankrApiKey', () => ''), SecretKey('quantexExchangeMarkup', () => ''), + SecretKey('seeds', () => ''), SecretKey('testCakePayApiKey', () => ''), SecretKey('cakePayApiKey', () => ''), SecretKey('CSRFToken', () => ''), SecretKey('authorization', () => ''), + SecretKey('etherScanApiKey', () => ''), + SecretKey('polygonScanApiKey', () => ''), + SecretKey('letsExchangeBearerToken', () => ''), + SecretKey('letsExchangeAffiliateId', () => ''), + SecretKey('stealthExBearerToken', () => ''), + SecretKey('stealthExAdditionalFeePercent', () => ''), ]; static final evmChainsSecrets = [ diff --git a/tool/utils/translation/arb_file_utils.dart b/tool/utils/translation/arb_file_utils.dart index b54dab423..414d318dd 100644 --- a/tool/utils/translation/arb_file_utils.dart +++ b/tool/utils/translation/arb_file_utils.dart @@ -1,11 +1,11 @@ import 'dart:convert'; import 'dart:io'; -void appendStringToArbFile(String fileName, String name, String text) { +void appendStringToArbFile(String fileName, String name, String text, {bool force = false}) { final file = File(fileName); final arbObj = readArbFile(file); - if (arbObj.containsKey(name)) { + if (arbObj.containsKey(name) && !force) { print("String $name already exists in $fileName!"); return; } diff --git a/tool/utils/translation/translation_constants.dart b/tool/utils/translation/translation_constants.dart index 3a472d8c4..3f392c639 100644 --- a/tool/utils/translation/translation_constants.dart +++ b/tool/utils/translation/translation_constants.dart @@ -1,6 +1,6 @@ const defaultLang = "en"; const langs = [ - "ar", "bg", "cs", "de", "en", "es", "fr", "ha", "hi", "hr", "id", "it", + "ar", "bg", "cs", "de", "en", "es", "fr", "ha", "hi", "hr", "hy", "id", "it", "ja", "ko", "my", "nl", "pl", "pt", "ru", "th", "tl", "tr", "uk", "ur", "yo", - "zh-cn" // zh, but Google Translate uses zh-cn for Chinese (Simplified) + "vi", "zh-cn" // zh, but Google Translate uses zh-cn for Chinese (Simplified) ]; diff --git a/windows/CMakeLists.txt b/windows/CMakeLists.txt index 7d1c03451..09bc8cfb6 100644 --- a/windows/CMakeLists.txt +++ b/windows/CMakeLists.txt @@ -87,18 +87,12 @@ 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_libgcc_s_seh-1.dll" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" RENAME "libgcc_s_seh-1.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) -install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/../scripts/monero_c/release/monero/x86_64-w64-mingw32_libstdc++-6.dll" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" RENAME "libstdc++-6.dll" - COMPONENT Runtime) - install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/../scripts/monero_c/release/monero/x86_64-w64-mingw32_libwinpthread-1.dll" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" RENAME "libwinpthread-1.dll" COMPONENT Runtime) diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index c6444e09c..ad540a359 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -11,6 +11,7 @@ #include #include #include +#include #include void RegisterPlugins(flutter::PluginRegistry* registry) { @@ -24,6 +25,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); SharePlusWindowsPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); + UniversalBlePluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UniversalBlePluginCApi")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 0a0b2f9eb..92431a6fb 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -8,6 +8,7 @@ list(APPEND FLUTTER_PLUGIN_LIST flutter_secure_storage_windows permission_handler_windows share_plus + universal_ble url_launcher_windows )