diff --git a/.github/workflows/cache_dependencies.yml b/.github/workflows/cache_dependencies.yml index 5373bbc87..bf0d0f7bc 100644 --- a/.github/workflows/cache_dependencies.yml +++ b/.github/workflows/cache_dependencies.yml @@ -14,7 +14,7 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-java@v1 with: - java-version: '8.x' + java-version: '11.x' - name: Flutter action uses: subosito/flutter-action@v1 diff --git a/.github/workflows/pr_test_build.yml b/.github/workflows/pr_test_build.yml index c8827d545..d74b85ce4 100644 --- a/.github/workflows/pr_test_build.yml +++ b/.github/workflows/pr_test_build.yml @@ -2,11 +2,10 @@ name: PR Test Build on: pull_request: - branches: [ main ] + branches: [main] jobs: PR_test_build: - runs-on: ubuntu-20.04 env: STORE_PASS: test@cake_wallet @@ -23,12 +22,12 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-java@v1 with: - java-version: '8.x' + java-version: "11.x" - name: Flutter action uses: subosito/flutter-action@v1 with: - flutter-version: '3.10.x' + flutter-version: "3.10.x" channel: stable - name: Install package dependencies @@ -39,10 +38,13 @@ jobs: sudo mkdir -p /opt/android sudo chown $USER /opt/android cd /opt/android + -y curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh + cargo install cargo-ndk git clone https://github.com/cake-tech/cake_wallet.git --branch $GITHUB_HEAD_REF cd cake_wallet/scripts/android/ ./install_ndk.sh source ./app_env.sh cakewallet + chmod +x pubspec_gen.sh ./app_config.sh - name: Cache Externals @@ -93,6 +95,7 @@ jobs: cd cw_bitcoin && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. cd cw_haven && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. cd cw_ethereum && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. + cd cw_bitcoin_cash && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. cd cw_nano && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. flutter packages pub run build_runner build --delete-conflicting-outputs @@ -129,6 +132,10 @@ jobs: echo "const payfuraApiKey = '${{ secrets.PAYFURA_API_KEY }}';" >> lib/.secrets.g.dart echo "const etherScanApiKey = '${{ secrets.ETHER_SCAN_API_KEY }}';" >> cw_ethereum/lib/.secrets.g.dart echo "const chatwootWebsiteToken = '${{ secrets.CHATWOOT_WEBSITE_TOKEN }}';" >> lib/.secrets.g.dart + echo "const exolixApiKey = '${{ secrets.EXOLIX_API_KEY }}';" >> lib/.secrets.g.dart + echo "const robinhoodApplicationId = '${{ secrets.ROBINHOOD_APPLICATION_ID }}';" >> lib/.secrets.g.dart + echo "const robinhoodCIdApiSecret = '${{ secrets.ROBINHOOD_CID_CLIENT_SECRET }}';" >> lib/.secrets.g.dart + echo "const walletConnectProjectId = '${{ secrets.WALLET_CONNECT_PROJECT_ID }}';" >> lib/.secrets.g.dart - name: Rename app run: echo -e "id=com.cakewallet.test\nname=$GITHUB_HEAD_REF" > /opt/android/cake_wallet/android/app.properties @@ -138,18 +145,18 @@ jobs: cd /opt/android/cake_wallet flutter build apk --release -# - name: Push to App Center -# run: | -# echo 'Installing App Center CLI tools' -# npm install -g appcenter-cli -# echo "Publishing test to App Center" -# appcenter distribute release \ -# --group "Testers" \ -# --file "/opt/android/cake_wallet/build/app/outputs/apk/release/app-release.apk" \ -# --release-notes ${GITHUB_HEAD_REF} \ -# --app Cake-Labs/Cake-Wallet \ -# --token ${{ secrets.APP_CENTER_TOKEN }} \ -# --quiet +# - name: Push to App Center +# run: | +# echo 'Installing App Center CLI tools' +# npm install -g appcenter-cli +# echo "Publishing test to App Center" +# appcenter distribute release \ +# --group "Testers" \ +# --file "/opt/android/cake_wallet/build/app/outputs/apk/release/app-release.apk" \ +# --release-notes ${GITHUB_HEAD_REF} \ +# --app Cake-Labs/Cake-Wallet \ +# --token ${{ secrets.APP_CENTER_TOKEN }} \ +# --quiet - name: Rename apk file run: | @@ -169,6 +176,6 @@ jobs: token: ${{ secrets.SLACK_APP_TOKEN }} path: /opt/android/cake_wallet/build/app/outputs/apk/release/app-release.apk channel: ${{ secrets.SLACK_APK_CHANNEL }} - title: '${{github.head_ref}}.apk' + title: "${{github.head_ref}}.apk" filename: ${{github.head_ref}}.apk initial_comment: ${{ github.event.head_commit.message }} diff --git a/.gitignore b/.gitignore index 09583004b..c735d4058 100644 --- a/.gitignore +++ b/.gitignore @@ -124,6 +124,8 @@ lib/bitcoin/bitcoin.dart lib/monero/monero.dart lib/haven/haven.dart lib/ethereum/ethereum.dart +lib/bitcoin_cash/bitcoin_cash.dart +lib/nano/nano.dart ios/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_180.png ios/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_120.png diff --git a/android/app/build.gradle b/android/app/build.gradle index 946c53697..e6b9eb2f8 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -75,7 +75,6 @@ android { shrinkResources false minifyEnabled false - useProguard false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } diff --git a/android/app/src/main/AndroidManifestBase.xml b/android/app/src/main/AndroidManifestBase.xml index b40aeb7c8..910149f60 100644 --- a/android/app/src/main/AndroidManifestBase.xml +++ b/android/app/src/main/AndroidManifestBase.xml @@ -3,6 +3,7 @@ + @@ -25,10 +26,6 @@ android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize" android:exported="true"> - @@ -46,6 +43,7 @@ + @@ -55,6 +53,15 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/drawable/ic_launcher_adaptive_back.xml b/android/app/src/main/res/drawable/ic_launcher_adaptive_back.xml new file mode 100644 index 000000000..ca3826a46 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_launcher_adaptive_back.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml index 304732f88..50af1a418 100644 --- a/android/app/src/main/res/drawable/launch_background.xml +++ b/android/app/src/main/res/drawable/launch_background.xml @@ -1,12 +1,6 @@ - - - - - + + + diff --git a/android/build.gradle b/android/build.gradle index bd4ebd770..8286d9cb9 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,12 +1,12 @@ buildscript { - ext.kotlin_version = '1.6.21' + ext.kotlin_version = '1.7.10' repositories { google() jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:4.1.3' + classpath 'com.android.tools.build:gradle:7.3.0' classpath 'com.google.gms:google-services:4.3.8' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } diff --git a/android/gradle.properties b/android/gradle.properties index a5965ab8d..38c8d4544 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,4 +1,4 @@ org.gradle.jvmargs=-Xmx1536M android.enableR8=true android.useAndroidX=true -android.enableJetifier=true \ No newline at end of file +android.enableJetifier=true diff --git a/assets/bitcoin_cash_electrum_server_list.yml b/assets/bitcoin_cash_electrum_server_list.yml new file mode 100644 index 000000000..d76668169 --- /dev/null +++ b/assets/bitcoin_cash_electrum_server_list.yml @@ -0,0 +1,3 @@ +- + uri: bitcoincash.stackwallet.com:50002 + is_default: true \ No newline at end of file diff --git a/assets/faq/faq_de.json b/assets/faq/faq_de.json index be9b567b7..14d703e4f 100644 --- a/assets/faq/faq_de.json +++ b/assets/faq/faq_de.json @@ -1,7 +1,7 @@ [ { "question" : "Was ist der Unterschied zwischen verfügbarem Guthaben und vollständigem Guthaben?", - "answer" : "Nachdem Sie eine Transaktion getätigt oder Monero erhalten haben, muss die Transaktion noch bestätigt werden. In ungefähr 20 Minuten sollte Ihr \"verfügbares Guthaben\" aktualisiert werden!\nWenn Sie Monero senden, verringert sich manchmal Ihr verfügbares Guthaben um mehr als den Betrag, den Sie gesendet haben. Dies ist normal und zum Schutz Ihrer Privatsphäre erforderlich. Ihr \"vollständiges Gleichgewicht\" sollte in 20 Minuten wieder normal sein.\n" + "answer" : "Nachdem Sie eine Transaktion getätigt oder Monero erhalten haben, muss die Transaktion noch bestätigt werden. In ungefähr 20 Minuten sollte Ihr \"verfügbares Guthaben\" aktualisiert werden!\nWenn Sie Monero senden, verringert sich manchmal Ihr verfügbares Guthaben um mehr als den Betrag, den Sie gesendet haben. Dies ist normal und zum Schutz Ihrer Privatsphäre erforderlich. Ihr \"vollständiges Guthaben\" sollte in 20 Minuten wieder normal sein.\n" }, { "question" : "Wie sende ich Monero an eine Börse, für die eine Zahlungs-ID erforderlich ist?", @@ -12,24 +12,24 @@ "answer" : "Obwohl unser Support Sie bei diesem Problem nicht direkt unterstützen kann, ist es ein sehr häufiges Problem, mit dem die meisten Börsen vertraut sind. Wenden Sie sich einfach an den Support der Börse, erklären Sie, dass Sie vergessen haben, Ihre Zahlungs-ID anzugeben, und senden Sie ihnen dann Ihre Transaktions-ID als Nachweis. Sie finden die Transaktions-ID, indem Sie auf die Transaktion in Ihrem Wallet-Bildschirm tippen.\n" }, { - "question" : "Was bedeuten \"Samen\" und \"Schlüssel\"?", - "answer" : "Ihre Schlüssel verschlüsseln die privaten Informationen in Ihrer Brieftasche und ermöglichen es Ihnen, Münzen auszugeben und eingehende Transaktionen anzuzeigen.\nIhr Startwert ist nur eine Version Ihres privaten Schlüssels, die so geschrieben wurde, dass Sie sie leichter notieren können. Ihr Same und Schlüssel sind tatsächlich dasselbe, nur in verschiedenen Formen!\nGeben Sie niemals Ihren Samen oder Schlüssel an jemanden weiter. Ihr Geld wird gestohlen, wenn Sie Ihren Samen oder Schlüssel herausgeben. Bitte notieren Sie sich jedoch Ihren Samen und bewahren Sie ihn an einem sicheren Ort auf (so können Sie Ihre Brieftasche wiederherstellen, wenn Sie Ihr Telefon verlieren.)\n" + "question" : "Was bedeuten \"Seed\" und \"Schlüssel\"?", + "answer" : "Ihre Schlüssel verschlüsseln die privaten Informationen in Ihrer Brieftasche und ermöglichen es Ihnen, Münzen auszugeben und eingehende Transaktionen anzuzeigen.\nIhr Startwert ist nur eine Version Ihres privaten Schlüssels, die so geschrieben wurde, dass Sie sie leichter notieren können. Ihr Same und Schlüssel sind tatsächlich dasselbe, nur in verschiedenen Formen!\nGeben Sie niemals Ihren Seed oder Schlüssel an jemanden weiter. Ihr Geld wird gestohlen, wenn Sie Ihren Seed oder Schlüssel herausgeben. Bitte notieren Sie sich jedoch Ihren Seed und bewahren Sie ihn an einem sicheren Ort auf (so können Sie Ihr Wallet wiederherstellen, wenn Sie Ihr Telefon verlieren.)\n" }, { "question" : "Wie viele Geldbörsen kann ich erstellen?", "answer" : "Es gibt keine Grenzen! Sie können so viele Brieftaschen erstellen, wie Sie möchten.\n" }, { - "question" : "Wie kann ich meine Brieftasche wiederherstellen?", - "answer" : "Tippen Sie auf das Menü •••, wählen Sie „Brieftaschen“ und dann „Brieftasche wiederherstellen“. Geben Sie dann Ihren Startwert (oder Ihre Schlüssel) und optional ein Datum vor der ersten Transaktion in Ihrer Brieftasche ein (dies beschleunigt den Synchronisierungsvorgang) .) Möglicherweise müssen Sie die App 15 bis 30 Minuten geöffnet lassen, um Ihr Portemonnaie vollständig wiederherzustellen.\n" + "question" : "Wie kann ich mein Wallet wiederherstellen?", + "answer" : "Tippen Sie auf das Menü •••, wählen Sie „Wallest“ und dann „Wallet wiederherstellen“. Geben Sie dann Ihren Seed (oder Ihre Schlüssel) und optional ein Datum vor der ersten Transaktion in Ihrem Wallet ein (dies beschleunigt den Synchronisierungsvorgang) .) Möglicherweise müssen Sie die App 15 bis 30 Minuten geöffnet lassen, um Ihr Wallet vollständig wiederherzustellen.\n" }, { - "question" : "Was kann ich tun, wenn ich meinen Samen verliere?", - "answer" : "Wenn Sie Ihren Samen vergessen haben, haben Sie ihn wahrscheinlich irgendwo aufgeschrieben. Bitte überprüfen Sie Ihre Notizen und schauen Sie sich auf Ihrem Computer um. Wenn Sie es nirgendwo finden, haben Sie möglicherweise Cake Wallet gesichert (in diesem Fall können Sie es aus diesem Backup wiederherstellen.) Wenn keines dieser Probleme auftritt, können wir leider nichts tun.\n" + "question" : "Was kann ich tun, wenn ich meinen Seed verliere?", + "answer" : "Wenn Sie Ihren Seed vergessen haben, haben Sie ihn wahrscheinlich irgendwo aufgeschrieben. Bitte überprüfen Sie Ihre Notizen und schauen Sie sich auf Ihrem Computer um. Wenn Sie es nirgendwo finden, haben Sie möglicherweise Cake Wallet gesichert (in diesem Fall können Sie es aus diesem Backup wiederherstellen.) Wenn keines dieser Probleme auftritt, können wir leider nichts tun.\n" }, { - "question" : "Sammeln Sie Informationen zu meiner Brieftasche?", - "answer" : "Cake Wallet sammelt oder zeichnet keine Informationen über Ihre Brieftasche auf. Ihre Privatsphäre ist uns wichtig.\n" + "question" : "Sammeln Sie Informationen zu meinem Wallet?", + "answer" : "Cake Wallet sammelt oder zeichnet keine Informationen über Ihr Wallet auf. Ihre Privatsphäre ist uns wichtig.\n" }, { "question" : "Kann ich eine Transaktion stornieren?", @@ -37,7 +37,7 @@ }, { "question" : "Was sind Subadressen und wie verwende ich sie?", - "answer" : "Eine Unteradresse ist im Grunde eine eindeutige Adresse, die Sie jederzeit generieren können. An sie gesendete Münzen landen weiterhin in Ihrer Hauptbrieftasche, aber die Person, die die Münzen sendet, kann Ihre Hauptadresse nicht ermitteln. Unteradressen beginnen immer mit „8“.\nSie können eine neue Unteradresse im Empfangsbildschirm erstellen, indem Sie auf das „+“ neben der Schaltfläche Unteradressen tippen. Geben Sie einen Namen für die Unteradresse ein und tippen Sie auf \"Hinzufügen\". Dann tippen Sie einfach auf den Namen der Subadresse, wenn Sie ihn verwenden möchten!\nWenn Sie paranoid sind, sollten Sie wahrscheinlich jedes Mal, wenn Sie Monero erhalten, eine neue Unteradresse erstellen.\n" + "answer" : "Eine Unteradresse ist im Grunde eine eindeutige Adresse, die Sie jederzeit generieren können. An sie gesendete Münzen landen weiterhin in Ihrer Hauptwallet, aber die Person, die die Coins sendet, kann Ihre Hauptadresse nicht ermitteln. Unteradressen beginnen immer mit „8“.\nSie können eine neue Unteradresse im Empfangsbildschirm erstellen, indem Sie auf das „+“ neben der Schaltfläche Unteradressen tippen. Geben Sie einen Namen für die Unteradresse ein und tippen Sie auf \"Hinzufügen\". Dann tippen Sie einfach auf den Namen der Subadresse, wenn Sie ihn verwenden möchten!\nWenn Sie paranoid sind, sollten Sie wahrscheinlich jedes Mal, wenn Sie Monero erhalten, eine neue Unteradresse erstellen.\n" }, { "question" : "Was ist eine Transaktions-ID?", @@ -48,11 +48,11 @@ "answer" : "Wenn Sie Ihren Monero nicht erhalten haben, möchten Sie möglicherweise auf das Menü ••• tippen und auf Reconnect (Neu verbinden) klicken. Wenn dies nicht funktioniert, gehen Sie in das Einstellungsmenü, tippen Sie auf das Feld \"Aktueller Knoten\" und wählen Sie einen Knoten mit einem grünen Punkt daneben aus.\n" }, { - "question" : "Ich habe in der App keine Münzen aus dem Umtausch erhalten. Was kann ich tun?", - "answer" : "Wenn Sie Probleme mit einem Austausch haben, wenden Sie sich am besten an den Austausch. Wir sind eine Partnerschaft mit XMR.TO, Morph und ChangeNow eingegangen. Rufen Sie daher am besten http://xmr.to, http://changenow.io oder http://morphtoken.com auf und wenden Sie sich an deren Support.\n" + "question" : "Ich habe in der App keine Coins aus dem Umtausch erhalten. Was kann ich tun?", + "answer" : "Wenn Sie Probleme mit einem Austausch haben, besteht die beste Option, den Austausch selbst zu kontaktieren. Wir haben uns mit Chechenow, Simpleswap, Sideshift und Trocador zusammengetan. Am besten wechseln Sie zu https://changenow.io, https://simpleswap.io/, https://sideshift.ai/, https://trocador.app/ und kontaktieren Sie ihre Unterstützung.\n" }, { "question" : "Wie kontaktiere ich den Cake Wallet-Support?", "answer" : "Senden Sie eine E-Mail an support@cakewallet.com, schließen Sie sich dem Telegramm unter @cakewallet_bot an oder twittern Sie @CakeWalletXMR!\n" } -] \ No newline at end of file +] diff --git a/assets/faq/faq_en.json b/assets/faq/faq_en.json index aae40802c..741d31798 100644 --- a/assets/faq/faq_en.json +++ b/assets/faq/faq_en.json @@ -49,10 +49,10 @@ }, { "question" : "I didn't receive my coins from the exchange in the app. What can I do?", - "answer" : "If you're having issues with an exchange, the best option is to contact the exchange itself. We're partnered with XMR.TO, Morph and ChangeNow, so your best bet is to go to http://xmr.to, http://changenow.io, or http://morphtoken.com and contact their support.\n" + "answer" : "If you're having issues with an exchange, the best option is to contact the exchange itself. We're partnered with ChangeNow, SimpleSwap, SideShift and Trocador. So your best bet is to go to https://changenow.io, https://simpleswap.io/, https://sideshift.ai/, https://trocador.app/ and contact their support.\n" }, { "question" : "How do I contact Cake Wallet support?", "answer" : "Email support@cakewallet.com, join the Telegram at @cakewallet_bot, or tweet @CakeWalletXMR!\n" } -] \ No newline at end of file +] diff --git a/assets/faq/faq_es.json b/assets/faq/faq_es.json index 28074662c..968853a0b 100644 --- a/assets/faq/faq_es.json +++ b/assets/faq/faq_es.json @@ -49,10 +49,10 @@ }, { "question" : "No recibí mis monedas del intercambio en la aplicación. ¿Que puedo hacer?", - "answer" : "Si tiene problemas con un intercambio, la mejor opción es ponerse en contacto con el intercambio en sí. Estamos asociados con XMR.TO, Morph y ChangeNow, por lo que su mejor opción es ir a http://xmr.to, http://changenow.io o http://morphtoken.com y contactar a su soporte.\n" + "answer" : "Si tiene problemas con un intercambio, la mejor opción es comunicarse con el intercambio en sí. Estamos asociados con ChangeNow, SimpleSwap, SideShift y Trocador. Entonces, su mejor opción es ir a https://changenow.io, https://simplewap.io/, https://sideshift.ai/, https://trocador.app/ y contactar su soporte.\n" }, { "question" : "¿Cómo contacto al soporte de Cake Wallet?", "answer" : "¡Envíe un correo electrónico a support@cakewallet.com, únase al Telegram en @cakewallet_bot o envíe un tweet a @CakeWalletXMR!\n" } -] \ No newline at end of file +] diff --git a/assets/faq/faq_fr.json b/assets/faq/faq_fr.json index cc4e52873..6b6ac5227 100644 --- a/assets/faq/faq_fr.json +++ b/assets/faq/faq_fr.json @@ -50,7 +50,7 @@ }, { "question" : "Je n'ai pas reçu mes fonds en provenance de la plateforme d'échange dans l'application. Que puis-je faire ?", - "answer" : "Si vous avez des soucis avec une plateforme d'échange, le mieux est de contacter la plateforme d'échange directement. Nous avons des partenariats avec XMR.TO, Morph et ChangeNow, donc essayez http://xmr.to, http://changenow.io, ou http://morphtoken.com et contactez leur support.\n" + "answer" : "Si vous rencontrez des problèmes avec un échange, la meilleure option est de contacter l'échange lui-même. Nous sommes en partenariat avec Changenow, Simpleswap, Sideshift et le Trocador. Donc, votre meilleur pari est d'aller sur https://changenow.io, https://simpleswap.io/, https://sideshift.ai/, https://trocador.app/ et contactez leur support.\n" }, { "question" : "Comment puis-je contacter le support de Cake Wallet ?", diff --git a/assets/faq/faq_hi.json b/assets/faq/faq_hi.json index cd9eb3fb9..f1ede3374 100644 --- a/assets/faq/faq_hi.json +++ b/assets/faq/faq_hi.json @@ -49,10 +49,10 @@ }, { "question" : "मुझे ऐप में एक्सचेंज से मेरे सिक्के नहीं मिले। मैं क्या कर सकता हूँ?", - "answer" : "यदि आप एक एक्सचेंज के साथ समस्या कर रहे हैं, तो सबसे अच्छा विकल्प एक्सचेंज से संपर्क करना है। हम XMR.TO, Morph और ChangeNow के साथ भागीदारी कर रहे हैं, इसलिए आपका सबसे अच्छा दांव http://xmr.to, http://changenow.io, या http://morphtoken.com पर जाना है और उनके समर्थन से संपर्क करना है।\n" + "answer" : "यदि आप एक एक्सचेंज के साथ समस्याएं कर रहे हैं, तो सबसे अच्छा विकल्प एक्सचेंज से संपर्क करना है। हम चंगेनो, सिम्प्लेवैप, सिडशिफ्ट और ट्रोकैडर के साथ भागीदारी कर रहे हैं। तो आपका सबसे अच्छा दांव https://changenow.io, https://simpleswap.io/, https://sideshift.ai/, https://trocador.app/ पर जाना और उनके समर्थन से संपर्क करना है।\n" }, { "question" : "मैं केक वॉलेट से कैसे संपर्क करूं?", "answer" : "ईमेल support@cakewallet.com, @cakewallet_bot पर टेलीग्राम में शामिल हों, या @CakeWalletXMR पर ट्वीट करें!\n" } -] \ No newline at end of file +] diff --git a/assets/faq/faq_ja.json b/assets/faq/faq_ja.json index 8a5b48763..6d83e61d3 100644 --- a/assets/faq/faq_ja.json +++ b/assets/faq/faq_ja.json @@ -49,10 +49,10 @@ }, { "question" : "アプリの取引所からコインを受け取りませんでした。 私に何ができる?", - "answer" : "取引所に問題がある場合、最良の選択肢は取引所自体に連絡することです。 XMR.TO、Morph、ChangeNowと提携しているため、最善の策はhttp://xmr.to、http://changenow.io、またはhttp://morphtoken.comにアクセスしてサポートに連絡することです。\n" + "answer" : "交換に問題がある場合、最良の選択肢は、交換自体に連絡することです。 Changenow、SimpleSwap、Sideshift、Trocadorと提携しています。したがって、あなたの最善の策は、https://changenow.io、https://simpleswap.io/、https://sideshift.ai/、https://trocador.app/に行くことです。\n" }, { "question" : "Cake Walletサポートに連絡するにはどうすればよいですか?", "answer" : "support@cakewallet.comにメールを送信するか、@cakewallet_botで電報に参加するか、@CakeWalletXMRにツイートしてください。\n" } -] \ No newline at end of file +] diff --git a/assets/faq/faq_ko.json b/assets/faq/faq_ko.json index 7d6f36589..640567284 100644 --- a/assets/faq/faq_ko.json +++ b/assets/faq/faq_ko.json @@ -49,10 +49,10 @@ }, { "question" : "앱의 거래소에서 동전을받지 못했습니다. 내가 무엇을 할 수 있을지?", - "answer" : "교환에 문제가있는 경우 교환기에 연락하는 것이 가장 좋습니다. 우리는 XMR.TO, Morph 및 ChangeNow와 파트너 관계를 맺고 있으므로 가장 좋은 방법은 http://xmr.to, http://changenow.io 또는 http://morphtoken.com으로 이동하여 지원 부서에 문의하는 것입니다.\n" + "answer" : "교환에 문제가있는 경우 가장 좋은 선택은 Exchange 자체에 연락하는 것입니다. 우리는 Changenow, Simpleswap, Sideshift 및 Trocador와 파트너 관계를 맺고 있습니다. 따라서 가장 좋은 방법은 https://changenow.io, https://simpleswap.io/, https://sideshift.ai/, https://trocador.app/로 이동하여 지원에 연락하는 것입니다.\n" }, { "question" : "Cake Wallet 지원팀에 연락하려면 어떻게해야합니까?", "answer" : "support@cakewallet.com로 이메일을 보내거나 @cakewallet_bot에서 전보에 가입하거나 @CakeWalletXMR을 트윗하십시오!\n" } -] \ No newline at end of file +] diff --git a/assets/faq/faq_nl.json b/assets/faq/faq_nl.json index bb4f70216..47cf8fdf0 100644 --- a/assets/faq/faq_nl.json +++ b/assets/faq/faq_nl.json @@ -49,10 +49,10 @@ }, { "question" : "Ik heb mijn munten niet ontvangen van de beurs in de app. Wat kan ik doen?", - "answer" : "Als u problemen ondervindt met een uitwisseling, kunt u het beste contact opnemen met de uitwisseling zelf. We werken samen met XMR.TO, Morph en ChangeNow, dus u kunt het beste naar http://xmr.to, http://changenow.io of http://morphtoken.com gaan en contact opnemen met hun ondersteuning.\n" + "answer" : "Als u problemen heeft met een uitwisseling, is de beste optie om contact op te nemen met de uitwisseling zelf. We werken samen met ChangeNow, SimpleSwap, SideShift en Trocador. Dus het beste is om naar https://changenow.io, https://simpleswap.io/, https://sideshift.ai/, https://trocador.app/ te gaan en contact op te nemen met hun ondersteuning.\n" }, { "question" : "Hoe neem ik contact op met Cake Wallet-ondersteuning?", "answer" : "E-mail support@cakewallet.com, word lid van het Telegram op @cakewallet_bot of tweet @CakeWalletXMR!\n" } -] \ No newline at end of file +] diff --git a/assets/faq/faq_pl.json b/assets/faq/faq_pl.json index 1934f4d1a..a38d79068 100644 --- a/assets/faq/faq_pl.json +++ b/assets/faq/faq_pl.json @@ -49,10 +49,10 @@ }, { "question" : "Nie otrzymałem moich monet z wymiany w aplikacji. Co mogę zrobić?", - "answer" : "Jeśli masz problemy z wymianą, najlepszym rozwiązaniem jest skontaktowanie się z samą giełdą. Współpracujemy z XMR.TO, Morph i ChangeNow, więc najlepiej postawić się na stronie http://xmr.to, http://changenow.io lub http://morphtoken.com i skontaktować się z ich wsparciem.\n" + "answer" : "Jeśli masz problemy z wymianą, najlepszą opcją jest skontaktowanie się z samą wymianą. Współpracujemy z Changenow, Simpleswap, Sideshift i Trocador. Więc najlepszym rozwiązaniem jest przejście na https://changenow.io, https://simpleswap.io/, https://sideshift.ai/, https://trocador.app/ i skontaktować się z ich obsługą.\n" }, { "question" : "Jak skontaktować się z obsługą Cake Wallet?", "answer" : "Wyślij e-mail na adres support@cakewallet.com, dołącz do telegramu na @cakewallet_bot lub tweet @CakeWalletXMR!\n" } -] \ No newline at end of file +] diff --git a/assets/faq/faq_pt.json b/assets/faq/faq_pt.json index 06ddab25e..66d3a6aaf 100644 --- a/assets/faq/faq_pt.json +++ b/assets/faq/faq_pt.json @@ -49,10 +49,10 @@ }, { "question" : "Não recebi minhas moedas da troca no aplicativo. O que eu posso fazer?", - "answer" : "Se você estiver tendo problemas com uma troca, a melhor opção é entrar em contato com a troca. Somos parceiros do XMR.TO, Morph e ChangeNow, portanto, sua melhor aposta é ir para http://xmr.to, http://changenow.io ou http://morphtoken.com e entrar em contato com o suporte deles.\n" + "answer" : "Se você estiver com problemas com uma troca, a melhor opção é entrar em contato com a própria troca. Estamos em parceria com ChangeNow, SimpleSwap, Sideshift e Trocador. Portanto, sua melhor aposta é ir para https://changenow.io, https://simpleswap.io/, https://sideshift.ai/, https://trocador.app/ e entre em contato com seu suporte.\n" }, { "question" : "Como entro em contato com o suporte da Cake Wallet?", "answer" : "Envie um e-mail para support@cakewallet.com, participe do Telegram em @cakewallet_bot ou envie um tweet para @CakeWalletXMR!\n" } -] \ No newline at end of file +] diff --git a/assets/faq/faq_ru.json b/assets/faq/faq_ru.json index 4b8b18e32..af5ba32a6 100644 --- a/assets/faq/faq_ru.json +++ b/assets/faq/faq_ru.json @@ -49,10 +49,10 @@ }, { "question" : "Я не получил свои монеты после обмена в приложении. Что я могу сделать?", - "answer" : "Если у вас возникли проблемы с обменом, лучше всего связаться с провайдером обмена. Мы сотрудничаем с XMR.TO, Morph и ChangeNow, поэтому вам лучше всего зайти на http://xmr.to, http://changenow.io или http://morphtoken.com и связаться с их поддержкой.\n" + "answer" : "Если у вас есть проблемы с обменом, лучший вариант - связаться с самой биржей. Мы сотрудничаем с Changenow, Simpleswap, SideShift и Trocador. Так что лучше всего пойти по адресу https://changenow.io, https://simpleswap.io/, https://sideshift.ai/, https://trocador.app/ и свяжитесь с их поддержкой.\n" }, { "question" : "Как мне связаться со службой поддержки Cake Wallet?", "answer" : "По электронной почте support@cakewallet.com, присоединитесь к Telegram по адресу @cakewallet_bot или отправьте твит @CakeWalletXMR!\n" } -] \ No newline at end of file +] diff --git a/assets/faq/faq_uk.json b/assets/faq/faq_uk.json index c481e0538..3e209d2ad 100644 --- a/assets/faq/faq_uk.json +++ b/assets/faq/faq_uk.json @@ -49,10 +49,10 @@ }, { "question" : "Я не отримав свої монети після обміну в додатку. Що я можу зробити?", - "answer" : "Якщо у вас виникли проблеми з обміном, найкраще зв'язатися з провайдером обміну. Ми співпрацюємо з XMR.TO, Morph і ChangeNow, тому вам найкраще зайти на http://xmr.to, http://changenow.io або http://morphtoken.com і зв'язатися з їх підтримкою.\n" + "answer" : "Якщо у вас є проблеми з обміном, найкращим варіантом є зв’язок із самою біржею. Ми співпрацюємо з Changenow, Simplewap, Sideshift та Trocador. Тож найкраща ставка - перейти на https://changenow.io, https://simpleswap.io/, https://sideshift.ai/, https://trocador.app/ та звернутися до їх підтримки.\n" }, { "question" : "Як мені зв'язатися зі службою підтримки Cake Wallet?", "answer" : "По електронній пошті support@cakewallet.com, приєднайтеся до Telegram за адресою @cakewallet_bot або надішліть твіт @CakeWalletXMR!\n" } -] \ No newline at end of file +] diff --git a/assets/faq/faq_zh.json b/assets/faq/faq_zh.json index 22977f22c..8debe1873 100644 --- a/assets/faq/faq_zh.json +++ b/assets/faq/faq_zh.json @@ -49,10 +49,10 @@ }, { "question" : "我没有从应用程序中的交易所收到硬币。 我能做什么?", - "answer" : "如果您对交易所有疑问,最好的选择是与交易所本身联系。 我们与XMR.TO,Morph和ChangeNow合作,因此最好的选择是访问http://xmr.to、http://changenow.io或http://morphtoken.com,并与他们的支持部门联系。\n" + "answer" : "如果您对交易所有问题,最好的选择是与交易所本身联系。我们与ChangeNow,SimplesWap,SideShift和Trocador合作。因此,最好的选择是访问https://changenow.io,https://simpleswap.io/,https://sideshift.ai/,https://trocador.app/并联系他们的支持。\n" }, { "question" : "如何联系蛋糕钱包支持?", "answer" : "电子邮件support@cakewallet.com,通过@cakewallet_bot加入电报,或在@CakeWalletXMR上发布推文!\n" } -] \ No newline at end of file +] diff --git a/assets/images/cakewallet_android_icon.png b/assets/images/cakewallet_android_icon.png index a96724f1c..59cc69414 100755 Binary files a/assets/images/cakewallet_android_icon.png and b/assets/images/cakewallet_android_icon.png differ diff --git a/assets/images/cakewallet_android_icon/mipmap-anydpi-v26/ic_launcher.xml b/assets/images/cakewallet_android_icon/mipmap-anydpi-v26/ic_launcher.xml index 90f958096..00d924171 100644 --- a/assets/images/cakewallet_android_icon/mipmap-anydpi-v26/ic_launcher.xml +++ b/assets/images/cakewallet_android_icon/mipmap-anydpi-v26/ic_launcher.xml @@ -1,5 +1,5 @@ - - + + \ No newline at end of file diff --git a/assets/images/cakewallet_android_icon/mipmap-hdpi/ic_launcher.png b/assets/images/cakewallet_android_icon/mipmap-hdpi/ic_launcher.png index 2bcdb4427..10d0a1a82 100644 Binary files a/assets/images/cakewallet_android_icon/mipmap-hdpi/ic_launcher.png and b/assets/images/cakewallet_android_icon/mipmap-hdpi/ic_launcher.png differ diff --git a/assets/images/cakewallet_android_icon/mipmap-hdpi/ic_launcher_adaptive_back.png b/assets/images/cakewallet_android_icon/mipmap-hdpi/ic_launcher_adaptive_back.png index a025d330c..5b0fde827 100644 Binary files a/assets/images/cakewallet_android_icon/mipmap-hdpi/ic_launcher_adaptive_back.png and b/assets/images/cakewallet_android_icon/mipmap-hdpi/ic_launcher_adaptive_back.png differ diff --git a/assets/images/cakewallet_android_icon/mipmap-hdpi/ic_launcher_adaptive_fore.png b/assets/images/cakewallet_android_icon/mipmap-hdpi/ic_launcher_adaptive_fore.png index 32891557d..9c16f0a27 100644 Binary files a/assets/images/cakewallet_android_icon/mipmap-hdpi/ic_launcher_adaptive_fore.png and b/assets/images/cakewallet_android_icon/mipmap-hdpi/ic_launcher_adaptive_fore.png differ diff --git a/assets/images/cakewallet_android_icon/mipmap-mdpi/ic_launcher.png b/assets/images/cakewallet_android_icon/mipmap-mdpi/ic_launcher.png index 4c4131065..8c59ec33e 100644 Binary files a/assets/images/cakewallet_android_icon/mipmap-mdpi/ic_launcher.png and b/assets/images/cakewallet_android_icon/mipmap-mdpi/ic_launcher.png differ diff --git a/assets/images/cakewallet_android_icon/mipmap-mdpi/ic_launcher_adaptive_back.png b/assets/images/cakewallet_android_icon/mipmap-mdpi/ic_launcher_adaptive_back.png index 9f0bf4907..5d25e42e7 100644 Binary files a/assets/images/cakewallet_android_icon/mipmap-mdpi/ic_launcher_adaptive_back.png and b/assets/images/cakewallet_android_icon/mipmap-mdpi/ic_launcher_adaptive_back.png differ diff --git a/assets/images/cakewallet_android_icon/mipmap-mdpi/ic_launcher_adaptive_fore.png b/assets/images/cakewallet_android_icon/mipmap-mdpi/ic_launcher_adaptive_fore.png index af6a1e312..021fe65de 100644 Binary files a/assets/images/cakewallet_android_icon/mipmap-mdpi/ic_launcher_adaptive_fore.png and b/assets/images/cakewallet_android_icon/mipmap-mdpi/ic_launcher_adaptive_fore.png differ diff --git a/assets/images/cakewallet_android_icon/mipmap-xhdpi/ic_launcher.png b/assets/images/cakewallet_android_icon/mipmap-xhdpi/ic_launcher.png index f857a604e..10c3acd7f 100644 Binary files a/assets/images/cakewallet_android_icon/mipmap-xhdpi/ic_launcher.png and b/assets/images/cakewallet_android_icon/mipmap-xhdpi/ic_launcher.png differ diff --git a/assets/images/cakewallet_android_icon/mipmap-xhdpi/ic_launcher_adaptive_back.png b/assets/images/cakewallet_android_icon/mipmap-xhdpi/ic_launcher_adaptive_back.png index 65a3f3a1c..c4b66dc58 100644 Binary files a/assets/images/cakewallet_android_icon/mipmap-xhdpi/ic_launcher_adaptive_back.png and b/assets/images/cakewallet_android_icon/mipmap-xhdpi/ic_launcher_adaptive_back.png differ diff --git a/assets/images/cakewallet_android_icon/mipmap-xhdpi/ic_launcher_adaptive_fore.png b/assets/images/cakewallet_android_icon/mipmap-xhdpi/ic_launcher_adaptive_fore.png index 5e783f26f..b440b154d 100644 Binary files a/assets/images/cakewallet_android_icon/mipmap-xhdpi/ic_launcher_adaptive_fore.png and b/assets/images/cakewallet_android_icon/mipmap-xhdpi/ic_launcher_adaptive_fore.png differ diff --git a/assets/images/cakewallet_android_icon/mipmap-xxhdpi/ic_launcher.png b/assets/images/cakewallet_android_icon/mipmap-xxhdpi/ic_launcher.png index 7fe35095c..813a3678d 100644 Binary files a/assets/images/cakewallet_android_icon/mipmap-xxhdpi/ic_launcher.png and b/assets/images/cakewallet_android_icon/mipmap-xxhdpi/ic_launcher.png differ diff --git a/assets/images/cakewallet_android_icon/mipmap-xxhdpi/ic_launcher_adaptive_back.png b/assets/images/cakewallet_android_icon/mipmap-xxhdpi/ic_launcher_adaptive_back.png index 66e3bf6b9..75dc0219d 100644 Binary files a/assets/images/cakewallet_android_icon/mipmap-xxhdpi/ic_launcher_adaptive_back.png and b/assets/images/cakewallet_android_icon/mipmap-xxhdpi/ic_launcher_adaptive_back.png differ diff --git a/assets/images/cakewallet_android_icon/mipmap-xxhdpi/ic_launcher_adaptive_fore.png b/assets/images/cakewallet_android_icon/mipmap-xxhdpi/ic_launcher_adaptive_fore.png index 2c0ecc492..90afb19e8 100644 Binary files a/assets/images/cakewallet_android_icon/mipmap-xxhdpi/ic_launcher_adaptive_fore.png and b/assets/images/cakewallet_android_icon/mipmap-xxhdpi/ic_launcher_adaptive_fore.png differ diff --git a/assets/images/cakewallet_android_icon/mipmap-xxxhdpi/ic_launcher.png b/assets/images/cakewallet_android_icon/mipmap-xxxhdpi/ic_launcher.png index 7f632717c..671422b96 100644 Binary files a/assets/images/cakewallet_android_icon/mipmap-xxxhdpi/ic_launcher.png and b/assets/images/cakewallet_android_icon/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/assets/images/cakewallet_android_icon/mipmap-xxxhdpi/ic_launcher_adaptive_back.png b/assets/images/cakewallet_android_icon/mipmap-xxxhdpi/ic_launcher_adaptive_back.png index 9e40681b1..46b1e2cb1 100644 Binary files a/assets/images/cakewallet_android_icon/mipmap-xxxhdpi/ic_launcher_adaptive_back.png and b/assets/images/cakewallet_android_icon/mipmap-xxxhdpi/ic_launcher_adaptive_back.png differ diff --git a/assets/images/cakewallet_android_icon/mipmap-xxxhdpi/ic_launcher_adaptive_fore.png b/assets/images/cakewallet_android_icon/mipmap-xxxhdpi/ic_launcher_adaptive_fore.png index a2104217c..0a2025220 100644 Binary files a/assets/images/cakewallet_android_icon/mipmap-xxxhdpi/ic_launcher_adaptive_fore.png and b/assets/images/cakewallet_android_icon/mipmap-xxxhdpi/ic_launcher_adaptive_fore.png differ diff --git a/assets/images/cakewallet_app_logo.png b/assets/images/cakewallet_app_logo.png index 64682cd1d..59cc69414 100644 Binary files a/assets/images/cakewallet_app_logo.png and b/assets/images/cakewallet_app_logo.png differ diff --git a/assets/images/exolix.png b/assets/images/exolix.png new file mode 100644 index 000000000..29e5f2db1 Binary files /dev/null and b/assets/images/exolix.png differ diff --git a/assets/images/monero.com_android_icon.png b/assets/images/monero.com_android_icon.png index af47453c1..9e1fa0a65 100644 Binary files a/assets/images/monero.com_android_icon.png and b/assets/images/monero.com_android_icon.png differ diff --git a/assets/images/monero.com_logo.png b/assets/images/monero.com_logo.png index ecc703781..9e1fa0a65 100644 Binary files a/assets/images/monero.com_logo.png and b/assets/images/monero.com_logo.png differ diff --git a/assets/images/monerocom_android_icon/mipmap-anydpi-v26/ic_launcher.xml b/assets/images/monerocom_android_icon/mipmap-anydpi-v26/ic_launcher.xml index 90f958096..00d924171 100644 --- a/assets/images/monerocom_android_icon/mipmap-anydpi-v26/ic_launcher.xml +++ b/assets/images/monerocom_android_icon/mipmap-anydpi-v26/ic_launcher.xml @@ -1,5 +1,5 @@ - - + + \ No newline at end of file diff --git a/assets/images/monerocom_android_icon/mipmap-hdpi/ic_launcher.png b/assets/images/monerocom_android_icon/mipmap-hdpi/ic_launcher.png index 583765d8f..1785f4b05 100644 Binary files a/assets/images/monerocom_android_icon/mipmap-hdpi/ic_launcher.png and b/assets/images/monerocom_android_icon/mipmap-hdpi/ic_launcher.png differ diff --git a/assets/images/monerocom_android_icon/mipmap-hdpi/ic_launcher_adaptive_back.png b/assets/images/monerocom_android_icon/mipmap-hdpi/ic_launcher_adaptive_back.png index a025d330c..5b0fde827 100644 Binary files a/assets/images/monerocom_android_icon/mipmap-hdpi/ic_launcher_adaptive_back.png and b/assets/images/monerocom_android_icon/mipmap-hdpi/ic_launcher_adaptive_back.png differ diff --git a/assets/images/monerocom_android_icon/mipmap-hdpi/ic_launcher_adaptive_fore.png b/assets/images/monerocom_android_icon/mipmap-hdpi/ic_launcher_adaptive_fore.png index 646a13ff0..884745809 100644 Binary files a/assets/images/monerocom_android_icon/mipmap-hdpi/ic_launcher_adaptive_fore.png and b/assets/images/monerocom_android_icon/mipmap-hdpi/ic_launcher_adaptive_fore.png differ diff --git a/assets/images/monerocom_android_icon/mipmap-mdpi/ic_launcher.png b/assets/images/monerocom_android_icon/mipmap-mdpi/ic_launcher.png index 50e1973f5..12c31ef57 100644 Binary files a/assets/images/monerocom_android_icon/mipmap-mdpi/ic_launcher.png and b/assets/images/monerocom_android_icon/mipmap-mdpi/ic_launcher.png differ diff --git a/assets/images/monerocom_android_icon/mipmap-mdpi/ic_launcher_adaptive_back.png b/assets/images/monerocom_android_icon/mipmap-mdpi/ic_launcher_adaptive_back.png index 9f0bf4907..5d25e42e7 100644 Binary files a/assets/images/monerocom_android_icon/mipmap-mdpi/ic_launcher_adaptive_back.png and b/assets/images/monerocom_android_icon/mipmap-mdpi/ic_launcher_adaptive_back.png differ diff --git a/assets/images/monerocom_android_icon/mipmap-mdpi/ic_launcher_adaptive_fore.png b/assets/images/monerocom_android_icon/mipmap-mdpi/ic_launcher_adaptive_fore.png index 2230fe0ec..28bbd57b8 100644 Binary files a/assets/images/monerocom_android_icon/mipmap-mdpi/ic_launcher_adaptive_fore.png and b/assets/images/monerocom_android_icon/mipmap-mdpi/ic_launcher_adaptive_fore.png differ diff --git a/assets/images/monerocom_android_icon/mipmap-xhdpi/ic_launcher.png b/assets/images/monerocom_android_icon/mipmap-xhdpi/ic_launcher.png index 505971bbb..2d8878946 100644 Binary files a/assets/images/monerocom_android_icon/mipmap-xhdpi/ic_launcher.png and b/assets/images/monerocom_android_icon/mipmap-xhdpi/ic_launcher.png differ diff --git a/assets/images/monerocom_android_icon/mipmap-xhdpi/ic_launcher_adaptive_back.png b/assets/images/monerocom_android_icon/mipmap-xhdpi/ic_launcher_adaptive_back.png index 65a3f3a1c..c4b66dc58 100644 Binary files a/assets/images/monerocom_android_icon/mipmap-xhdpi/ic_launcher_adaptive_back.png and b/assets/images/monerocom_android_icon/mipmap-xhdpi/ic_launcher_adaptive_back.png differ diff --git a/assets/images/monerocom_android_icon/mipmap-xhdpi/ic_launcher_adaptive_fore.png b/assets/images/monerocom_android_icon/mipmap-xhdpi/ic_launcher_adaptive_fore.png index 3d414fcd3..811848c27 100644 Binary files a/assets/images/monerocom_android_icon/mipmap-xhdpi/ic_launcher_adaptive_fore.png and b/assets/images/monerocom_android_icon/mipmap-xhdpi/ic_launcher_adaptive_fore.png differ diff --git a/assets/images/monerocom_android_icon/mipmap-xxhdpi/ic_launcher.png b/assets/images/monerocom_android_icon/mipmap-xxhdpi/ic_launcher.png index 1ad794a7b..2ceba55ad 100644 Binary files a/assets/images/monerocom_android_icon/mipmap-xxhdpi/ic_launcher.png and b/assets/images/monerocom_android_icon/mipmap-xxhdpi/ic_launcher.png differ diff --git a/assets/images/monerocom_android_icon/mipmap-xxhdpi/ic_launcher_adaptive_back.png b/assets/images/monerocom_android_icon/mipmap-xxhdpi/ic_launcher_adaptive_back.png index 66e3bf6b9..75dc0219d 100644 Binary files a/assets/images/monerocom_android_icon/mipmap-xxhdpi/ic_launcher_adaptive_back.png and b/assets/images/monerocom_android_icon/mipmap-xxhdpi/ic_launcher_adaptive_back.png differ diff --git a/assets/images/monerocom_android_icon/mipmap-xxhdpi/ic_launcher_adaptive_fore.png b/assets/images/monerocom_android_icon/mipmap-xxhdpi/ic_launcher_adaptive_fore.png index cab647d0c..e078655c9 100644 Binary files a/assets/images/monerocom_android_icon/mipmap-xxhdpi/ic_launcher_adaptive_fore.png and b/assets/images/monerocom_android_icon/mipmap-xxhdpi/ic_launcher_adaptive_fore.png differ diff --git a/assets/images/monerocom_android_icon/mipmap-xxxhdpi/ic_launcher.png b/assets/images/monerocom_android_icon/mipmap-xxxhdpi/ic_launcher.png index 43ab3ac30..beca2d29b 100644 Binary files a/assets/images/monerocom_android_icon/mipmap-xxxhdpi/ic_launcher.png and b/assets/images/monerocom_android_icon/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/assets/images/monerocom_android_icon/mipmap-xxxhdpi/ic_launcher_adaptive_back.png b/assets/images/monerocom_android_icon/mipmap-xxxhdpi/ic_launcher_adaptive_back.png index 9e40681b1..46b1e2cb1 100644 Binary files a/assets/images/monerocom_android_icon/mipmap-xxxhdpi/ic_launcher_adaptive_back.png and b/assets/images/monerocom_android_icon/mipmap-xxxhdpi/ic_launcher_adaptive_back.png differ diff --git a/assets/images/monerocom_android_icon/mipmap-xxxhdpi/ic_launcher_adaptive_fore.png b/assets/images/monerocom_android_icon/mipmap-xxxhdpi/ic_launcher_adaptive_fore.png index a790c3a74..255b1b71f 100644 Binary files a/assets/images/monerocom_android_icon/mipmap-xxxhdpi/ic_launcher_adaptive_fore.png and b/assets/images/monerocom_android_icon/mipmap-xxxhdpi/ic_launcher_adaptive_fore.png differ diff --git a/assets/images/onramper_dark.png b/assets/images/onramper_dark.png new file mode 100644 index 000000000..62f37cd29 Binary files /dev/null and b/assets/images/onramper_dark.png differ diff --git a/assets/images/onramper_light.png b/assets/images/onramper_light.png new file mode 100644 index 000000000..f07fc3709 Binary files /dev/null and b/assets/images/onramper_light.png differ diff --git a/assets/images/robinhood_dark.png b/assets/images/robinhood_dark.png new file mode 100644 index 000000000..0d7273fc4 Binary files /dev/null and b/assets/images/robinhood_dark.png differ diff --git a/assets/images/robinhood_light.png b/assets/images/robinhood_light.png new file mode 100644 index 000000000..24aa345f1 Binary files /dev/null and b/assets/images/robinhood_light.png differ diff --git a/assets/images/walletconnect_logo.png b/assets/images/walletconnect_logo.png new file mode 100644 index 000000000..9024b972c Binary files /dev/null and b/assets/images/walletconnect_logo.png differ diff --git a/assets/nano_pow_node_list.yml b/assets/nano_pow_node_list.yml index 674518073..3bbc7c3fb 100644 --- a/assets/nano_pow_node_list.yml +++ b/assets/nano_pow_node_list.yml @@ -5,4 +5,5 @@ - uri: workers.perish.co - - uri: worker.nanoriver.cc:443 \ No newline at end of file + uri: worker.nanoriver.cc + useSSL: true diff --git a/assets/node_list.yml b/assets/node_list.yml index 2cbd7f780..bc7a9dc4a 100644 --- a/assets/node_list.yml +++ b/assets/node_list.yml @@ -5,7 +5,8 @@ uri: cakexmrl7bonq7ovjka5kuwuyd3f7qnkz6z6s6dmsy3uckwra7bvggyd.onion:18081 is_default: false - - uri: node.sethforprivacy.com:18089 + uri: node.sethforprivacy.com:443 + useSSL: true is_default: false - uri: nodes.hashvault.pro:18081 diff --git a/assets/text/Monerocom_Release_Notes.txt b/assets/text/Monerocom_Release_Notes.txt index a8b55383d..9c09278a5 100644 --- a/assets/text/Monerocom_Release_Notes.txt +++ b/assets/text/Monerocom_Release_Notes.txt @@ -1,5 +1,4 @@ -Ability to Auto generate new Monero subaddress when used -Coin Control for Monero -In-app Live Chat support -Additional themes -Bug Fixes and performance enhancements \ No newline at end of file +UI enhancements +Privacy settings enhancements +Tablet/iPad fixes +Bug fixes \ No newline at end of file diff --git a/assets/text/Release_Notes.txt b/assets/text/Release_Notes.txt index 263689318..8b8ab36a1 100644 --- a/assets/text/Release_Notes.txt +++ b/assets/text/Release_Notes.txt @@ -1,6 +1,5 @@ -Restore Ethereum from private key and QR -Ability to Auto generate new Monero subaddress when used -Coin Control for Monero -In-app Live Chat support -Additional themes -Bug Fixes and performance enhancements \ No newline at end of file +WalletConnect enhancements +UI enhancements +Privacy settings enhancements +Tablet/iPad fixes +Bug fixes \ No newline at end of file diff --git a/configure_cake_wallet_android.sh b/configure_cake_wallet_android.sh index b80ebc46e..b8aa433de 100755 --- a/configure_cake_wallet_android.sh +++ b/configure_cake_wallet_android.sh @@ -7,4 +7,6 @@ cd cw_monero && flutter pub get && flutter packages pub run build_runner build - cd cw_bitcoin && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. cd cw_haven && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. cd cw_ethereum && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. +cd cw_nano && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. +cd cw_bitcoin_cash && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. flutter packages pub run build_runner build --delete-conflicting-outputs diff --git a/cw_bitcoin/lib/bitcoin_mnemonic.dart b/cw_bitcoin/lib/bitcoin_mnemonic.dart index d3bc23c06..48a0bc875 100644 --- a/cw_bitcoin/lib/bitcoin_mnemonic.dart +++ b/cw_bitcoin/lib/bitcoin_mnemonic.dart @@ -2304,4 +2304,4 @@ final englishWordlist = [ 'zero', 'zone', 'zoo' -]; +]; \ No newline at end of file diff --git a/cw_bitcoin/lib/bitcoin_transaction_priority.dart b/cw_bitcoin/lib/bitcoin_transaction_priority.dart index d82ea429e..10953a2e0 100644 --- a/cw_bitcoin/lib/bitcoin_transaction_priority.dart +++ b/cw_bitcoin/lib/bitcoin_transaction_priority.dart @@ -1,5 +1,4 @@ import 'package:cw_core/transaction_priority.dart'; -//import 'package:cake_wallet/generated/i18n.dart'; class BitcoinTransactionPriority extends TransactionPriority { const BitcoinTransactionPriority({required String title, required int raw}) @@ -100,4 +99,55 @@ class LitecoinTransactionPriority extends BitcoinTransactionPriority { return label; } + } +class BitcoinCashTransactionPriority extends BitcoinTransactionPriority { + const BitcoinCashTransactionPriority({required String title, required int raw}) + : super(title: title, raw: raw); + + static const List all = [fast, medium, slow]; + static const BitcoinCashTransactionPriority slow = + BitcoinCashTransactionPriority(title: 'Slow', raw: 0); + static const BitcoinCashTransactionPriority medium = + BitcoinCashTransactionPriority(title: 'Medium', raw: 1); + static const BitcoinCashTransactionPriority fast = + BitcoinCashTransactionPriority(title: 'Fast', raw: 2); + + static BitcoinCashTransactionPriority deserialize({required int raw}) { + switch (raw) { + case 0: + return slow; + case 1: + return medium; + case 2: + return fast; + default: + throw Exception('Unexpected token: $raw for BitcoinCashTransactionPriority deserialize'); + } + } + + @override + String get units => 'Satoshi'; + + @override + String toString() { + var label = ''; + + switch (this) { + case BitcoinCashTransactionPriority.slow: + label = 'Slow'; // S.current.transaction_priority_slow; + break; + case BitcoinCashTransactionPriority.medium: + label = 'Medium'; // S.current.transaction_priority_medium; + break; + case BitcoinCashTransactionPriority.fast: + label = 'Fast'; // S.current.transaction_priority_fast; + break; + default: + break; + } + + return label; + } +} + diff --git a/cw_bitcoin/lib/bitcoin_unspent.dart b/cw_bitcoin/lib/bitcoin_unspent.dart index e5a0e8cac..9c198c27c 100644 --- a/cw_bitcoin/lib/bitcoin_unspent.dart +++ b/cw_bitcoin/lib/bitcoin_unspent.dart @@ -1,24 +1,15 @@ import 'package:cw_bitcoin/bitcoin_address_record.dart'; +import 'package:cw_core/unspent_transaction_output.dart'; -class BitcoinUnspent { - BitcoinUnspent(this.address, this.hash, this.value, this.vout) - : isSending = true, - isFrozen = false, - note = ''; +class BitcoinUnspent extends Unspent { + BitcoinUnspent(BitcoinAddressRecord addressRecord, String hash, int value, int vout) + : bitcoinAddressRecord = addressRecord, + super(addressRecord.address, hash, value, vout, null); factory BitcoinUnspent.fromJSON( - BitcoinAddressRecord address, Map json) => + BitcoinAddressRecord address, Map json) => BitcoinUnspent(address, json['tx_hash'] as String, json['value'] as int, json['tx_pos'] as int); - final BitcoinAddressRecord address; - final String hash; - final int value; - final int vout; - - bool get isP2wpkh => - address.address.startsWith('bc') || address.address.startsWith('ltc'); - bool isSending; - bool isFrozen; - String note; + final BitcoinAddressRecord bitcoinAddressRecord; } diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index 2b445b696..0fa105eac 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -125,4 +125,4 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { initialRegularAddressIndex: snp.regularAddressIndex, initialChangeAddressIndex: snp.changeAddressIndex); } -} +} \ No newline at end of file diff --git a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart index de3fdfbca..36d37127d 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart @@ -1,39 +1,34 @@ import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; -import 'package:cw_bitcoin/electrum.dart'; -import 'package:cw_bitcoin/utils.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; +import 'package:cw_bitcoin/electrum.dart'; import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; +import 'package:cw_bitcoin/utils.dart'; import 'package:cw_core/wallet_info.dart'; -import 'package:flutter/foundation.dart'; import 'package:mobx/mobx.dart'; part 'bitcoin_wallet_addresses.g.dart'; -class BitcoinWalletAddresses = BitcoinWalletAddressesBase - with _$BitcoinWalletAddresses; +class BitcoinWalletAddresses = BitcoinWalletAddressesBase with _$BitcoinWalletAddresses; -abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses - with Store { - BitcoinWalletAddressesBase( - WalletInfo walletInfo, +abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with Store { + BitcoinWalletAddressesBase(WalletInfo walletInfo, {required bitcoin.HDWallet mainHd, - required bitcoin.HDWallet sideHd, - required bitcoin.NetworkType networkType, - required ElectrumClient electrumClient, - List? initialAddresses, - int initialRegularAddressIndex = 0, - int initialChangeAddressIndex = 0}) - : super( - walletInfo, - initialAddresses: initialAddresses, - initialRegularAddressIndex: initialRegularAddressIndex, - initialChangeAddressIndex: initialChangeAddressIndex, - mainHd: mainHd, - sideHd: sideHd, - electrumClient: electrumClient, - networkType: networkType); + required bitcoin.HDWallet sideHd, + required bitcoin.NetworkType networkType, + required ElectrumClient electrumClient, + List? initialAddresses, + int initialRegularAddressIndex = 0, + int initialChangeAddressIndex = 0}) + : super(walletInfo, + initialAddresses: initialAddresses, + initialRegularAddressIndex: initialRegularAddressIndex, + initialChangeAddressIndex: initialChangeAddressIndex, + mainHd: mainHd, + sideHd: sideHd, + electrumClient: electrumClient, + networkType: networkType); @override String getAddress({required int index, required bitcoin.HDWallet hd}) => generateP2WPKHAddress(hd: hd, index: index, networkType: networkType); -} \ No newline at end of file +} diff --git a/cw_bitcoin/lib/bitcoin_wallet_creation_credentials.dart b/cw_bitcoin/lib/bitcoin_wallet_creation_credentials.dart index 450ed31dd..6eaf55586 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_creation_credentials.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_creation_credentials.dart @@ -31,4 +31,4 @@ class BitcoinRestoreWalletFromWIFCredentials extends WalletCredentials { : super(name: name, password: password, walletInfo: walletInfo); final String wif; -} +} \ No newline at end of file diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index f9437e668..05486aa20 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -2,45 +2,49 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:math'; -import 'package:cw_core/unspent_coins_info.dart'; -import 'package:hive/hive.dart'; -import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; -import 'package:mobx/mobx.dart'; -import 'package:rxdart/subjects.dart'; -import 'package:flutter/foundation.dart'; + import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; -import 'package:cw_bitcoin/electrum_transaction_info.dart'; -import 'package:cw_core/pathForWallet.dart'; +import 'package:collection/collection.dart'; import 'package:cw_bitcoin/address_to_output_script.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; -import 'package:cw_bitcoin/electrum_balance.dart'; import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart'; -import 'package:cw_bitcoin/electrum_transaction_history.dart'; import 'package:cw_bitcoin/bitcoin_transaction_no_inputs_exception.dart'; import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; import 'package:cw_bitcoin/bitcoin_transaction_wrong_balance_exception.dart'; import 'package:cw_bitcoin/bitcoin_unspent.dart'; import 'package:cw_bitcoin/bitcoin_wallet_keys.dart'; +import 'package:cw_bitcoin/electrum.dart'; +import 'package:cw_bitcoin/electrum_balance.dart'; +import 'package:cw_bitcoin/electrum_transaction_history.dart'; +import 'package:cw_bitcoin/electrum_transaction_info.dart'; +import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; import 'package:cw_bitcoin/file.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/wallet_base.dart'; -import 'package:cw_core/node.dart'; -import 'package:cw_core/sync_status.dart'; -import 'package:cw_core/transaction_priority.dart'; -import 'package:cw_core/wallet_info.dart'; -import 'package:cw_bitcoin/electrum.dart'; -import 'package:hex/hex.dart'; import 'package:cw_core/crypto_currency.dart'; -import 'package:collection/collection.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'; +import 'package:flutter/foundation.dart'; +import 'package:hex/hex.dart'; +import 'package:hive/hive.dart'; +import 'package:mobx/mobx.dart'; +import 'package:rxdart/subjects.dart'; part 'electrum_wallet.g.dart'; class ElectrumWallet = ElectrumWalletBase with _$ElectrumWallet; -abstract class ElectrumWalletBase extends WalletBase with Store { +abstract class ElectrumWalletBase + extends WalletBase + with Store { ElectrumWalletBase( {required String password, required WalletInfo walletInfo, @@ -52,27 +56,31 @@ abstract class ElectrumWalletBase extends WalletBase[], _isTransactionUpdating = false, unspentCoins = [], _scripthashesUpdateSubject = {}, - balance = ObservableMap.of( - currency != null - ? {currency: initialBalance ?? const ElectrumBalance(confirmed: 0, unconfirmed: 0, - frozen: 0)} - : {}), + balance = ObservableMap.of(currency != null + ? { + currency: + initialBalance ?? const ElectrumBalance(confirmed: 0, unconfirmed: 0, frozen: 0) + } + : {}), this.unspentCoinsInfo = unspentCoinsInfo, super(walletInfo) { this.electrumClient = electrumClient ?? ElectrumClient(); this.walletInfo = walletInfo; - transactionHistory = - ElectrumTransactionHistory(walletInfo: walletInfo, password: password); + transactionHistory = ElectrumTransactionHistory(walletInfo: walletInfo, password: password); } + static bitcoin.HDWallet bitcoinCashHDWallet(Uint8List seedBytes) => + bitcoin.HDWallet.fromSeed(seedBytes).derivePath("m/44'/145'/0'/0"); + static int estimatedTransactionSize(int inputsCount, int outputsCounts) => inputsCount * 146 + outputsCounts * 33 + 8; @@ -98,9 +106,9 @@ abstract class ElectrumWalletBase extends WalletBase get publicScriptHashes => walletAddresses.addresses - .where((addr) => !addr.isHidden) - .map((addr) => scriptHash(addr.address, networkType: networkType)) - .toList(); + .where((addr) => !addr.isHidden) + .map((addr) => scriptHash(addr.address, networkType: networkType)) + .toList(); String get xpub => hd.base58!; @@ -110,8 +118,8 @@ abstract class ElectrumWalletBase extends WalletBase BitcoinWalletKeys( - wif: hd.wif!, privateKey: hd.privKey!, publicKey: hd.pubKey!); + BitcoinWalletKeys get keys => + BitcoinWalletKeys(wif: hd.wif!, privateKey: hd.privKey!, publicKey: hd.pubKey!); String _password; List unspentCoins; @@ -139,8 +147,8 @@ abstract class ElectrumWalletBase extends WalletBase _feeRates = await electrumClient.feeRates()); + Timer.periodic( + const Duration(minutes: 1), (timer) async => _feeRates = await electrumClient.feeRates()); syncStatus = SyncedSyncStatus(); } catch (e, stacktrace) { @@ -169,8 +177,7 @@ abstract class ElectrumWalletBase extends WalletBase createTransaction( - Object credentials) async { + Future createTransaction(Object credentials) async { const minAmount = 546; final transactionCredentials = credentials as BitcoinTransactionCredentials; final inputs = []; @@ -204,13 +211,11 @@ abstract class ElectrumWalletBase extends WalletBase item.sendAll - || item.formattedCryptoAmount! <= 0)) { + if (outputs.any((item) => item.sendAll || item.formattedCryptoAmount! <= 0)) { throw BitcoinTransactionWrongBalanceException(currency); } - credentialsAmount = outputs.fold(0, (acc, value) => - acc + value.formattedCryptoAmount!); + credentialsAmount = outputs.fold(0, (acc, value) => acc + value.formattedCryptoAmount!); if (allAmount - credentialsAmount < minAmount) { throw BitcoinTransactionWrongBalanceException(currency); @@ -227,9 +232,7 @@ abstract class ElectrumWalletBase extends WalletBase allAmount) { throw BitcoinTransactionWrongBalanceException(currency); @@ -290,10 +293,12 @@ abstract class ElectrumWalletBase extends WalletBase + int feeAmountForPriority( + BitcoinTransactionPriority priority, int inputsCount, int outputsCount) => feeRate(priority) * estimatedTransactionSize(inputsCount, outputsCount); - int feeAmountWithFeeRate(int feeRate, int inputsCount, - int outputsCount) => + int feeAmountWithFeeRate(int feeRate, int inputsCount, int outputsCount) => feeRate * estimatedTransactionSize(inputsCount, outputsCount); @override - int calculateEstimatedFee(TransactionPriority? priority, int? amount, - {int? outputsCount}) { + int calculateEstimatedFee(TransactionPriority? priority, int? amount, {int? outputsCount}) { if (priority is BitcoinTransactionPriority) { - return calculateEstimatedFeeWithFeeRate( - feeRate(priority), - amount, - outputsCount: outputsCount); + return calculateEstimatedFeeWithFeeRate(feeRate(priority), amount, + outputsCount: outputsCount); } return 0; } - int calculateEstimatedFeeWithFeeRate(int feeRate, int? amount, - {int? outputsCount}) { + int calculateEstimatedFeeWithFeeRate(int feeRate, int? amount, {int? outputsCount}) { int inputsCount = 0; if (amount != null) { @@ -420,8 +413,7 @@ abstract class ElectrumWalletBase extends WalletBase makePath() async => - pathForWallet(name: walletInfo.name, type: walletInfo.type); + Future makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type); Future updateUnspent() async { - final unspent = await Future.wait(walletAddresses - .addresses.map((address) => electrumClient + final unspent = await Future.wait(walletAddresses.addresses.map((address) => electrumClient .getListUnspentWithAddress(address.address, networkType) - .then((unspent) => unspent - .map((unspent) { + .then((unspent) => unspent.map((unspent) { try { return BitcoinUnspent.fromJSON(address, unspent); - } catch(_) { + } catch (_) { return null; } }).whereNotNull()))); unspentCoins = unspent.expand((e) => e).toList(); + unspentCoins.forEach((coin) async { + final tx = await fetchTransactionInfo(hash: coin.hash, height: 0); + coin.isChange = tx!.direction == TransactionDirection.outgoing; + }); if (unspentCoinsInfo.isEmpty) { unspentCoins.forEach((coin) => _addCoinInfo(coin)); @@ -498,8 +490,8 @@ abstract class ElectrumWalletBase extends WalletBase - element.walletId.contains(id) && element.hash.contains(coin.hash)); + final coinInfoList = unspentCoinsInfo.values + .where((element) => element.walletId.contains(id) && element.hash.contains(coin.hash)); if (coinInfoList.isNotEmpty) { final coinInfo = coinInfoList.first; @@ -518,14 +510,15 @@ abstract class ElectrumWalletBase extends WalletBase _addCoinInfo(BitcoinUnspent coin) async { final newInfo = UnspentCoinsInfo( - walletId: id, - hash: coin.hash, - isFrozen: coin.isFrozen, - isSending: coin.isSending, - noteRaw: coin.note, - address: coin.address.address, - value: coin.value, - vout: coin.vout, + walletId: id, + hash: coin.hash, + isFrozen: coin.isFrozen, + isSending: coin.isSending, + noteRaw: coin.note, + address: coin.bitcoinAddressRecord.address, + value: coin.value, + vout: coin.vout, + isChange: coin.isChange, ); await unspentCoinsInfo.add(newInfo); @@ -534,8 +527,8 @@ abstract class ElectrumWalletBase extends WalletBase _refreshUnspentCoinsInfo() async { try { final List keys = []; - final currentWalletUnspentCoins = unspentCoinsInfo.values - .where((element) => element.walletId.contains(id)); + final currentWalletUnspentCoins = + unspentCoinsInfo.values.where((element) => element.walletId.contains(id)); if (currentWalletUnspentCoins.isNotEmpty) { currentWalletUnspentCoins.forEach((element) { @@ -571,27 +564,19 @@ abstract class ElectrumWalletBase extends WalletBase fetchTransactionInfo( {required String hash, required int height}) async { - try { - final tx = await getTransactionExpanded(hash: hash, height: height); - final addresses = walletAddresses.addresses.map((addr) => addr.address).toSet(); - return ElectrumTransactionInfo.fromElectrumBundle( - tx, - walletInfo.type, - networkType, - addresses: addresses, - height: height); - } catch(_) { - return null; - } + try { + final tx = await getTransactionExpanded(hash: hash, height: height); + final addresses = walletAddresses.addresses.map((addr) => addr.address).toSet(); + return ElectrumTransactionInfo.fromElectrumBundle(tx, walletInfo.type, networkType, + addresses: addresses, height: height); + } catch (_) { + return null; + } } @override @@ -602,10 +587,8 @@ abstract class ElectrumWalletBase extends WalletBase electrumClient - .getHistory(scriptHash) - .then((history) => {scriptHash: history})); + final histories = addressHashes.keys.map((scriptHash) => + electrumClient.getHistory(scriptHash).then((history) => {scriptHash: history})); final historyResults = await Future.wait(histories); historyResults.forEach((history) { history.entries.forEach((historyItem) { @@ -616,19 +599,16 @@ abstract class ElectrumWalletBase extends WalletBase>( - {}, (acc, tx) { + final historiesWithDetails = await Future.wait(normalizedHistories.map((transaction) { + try { + return fetchTransactionInfo( + hash: transaction['tx_hash'] as String, height: transaction['height'] as int); + } catch (_) { + return Future.value(null); + } + })); + return historiesWithDetails + .fold>({}, (acc, tx) { if (tx == null) { return acc; } @@ -680,7 +660,6 @@ abstract class ElectrumWalletBase extends WalletBase _fetchBalances() async { final addresses = walletAddresses.addresses.toList(); final balanceFutures = >>[]; - for (var i = 0; i < addresses.length; i++) { final addressRecord = addresses[i]; final sh = scriptHash(addressRecord.address, networkType: networkType); @@ -691,8 +670,10 @@ abstract class ElectrumWalletBase extends WalletBase updateBalance() async { @@ -727,9 +708,7 @@ abstract class ElectrumWalletBase extends WalletBase addr.isHidden) - .toList(); + var addresses = walletAddresses.addresses.where((addr) => addr.isHidden).toList(); if (addresses.length < minCountOfHiddenAddresses) { addresses = walletAddresses.addresses.toList(); @@ -740,4 +719,14 @@ abstract class ElectrumWalletBase extends WalletBase _onError = onError; + + @override + String signMessage(String message, {String? address = null}) { + final index = address != null + ? walletAddresses.addresses.firstWhere((element) => element.address == address).index + : null; + return index == null + ? base64Encode(hd.sign(message)) + : base64Encode(hd.derive(index).sign(message)); + } } diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index 741c2fe1c..ab99a875c 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -1,9 +1,11 @@ import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; +import 'package:bitbox/bitbox.dart' as bitbox; import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/electrum.dart'; import 'package:cw_bitcoin/script_hash.dart'; import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/wallet_type.dart'; import 'package:mobx/mobx.dart'; part 'electrum_wallet_addresses.g.dart'; @@ -38,6 +40,8 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { static const defaultChangeAddressesCount = 17; static const gap = 20; + static String toCashAddr(String address) => bitbox.Address.toCashAddress(address); + final ObservableList addresses; final ObservableList receiveAddresses; final ObservableList changeAddresses; @@ -50,10 +54,12 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { @computed String get address { if (receiveAddresses.isEmpty) { - return generateNewAddress().address; + final address = generateNewAddress().address; + return walletInfo.type == WalletType.bitcoinCash ? toCashAddr(address) : address; } + final receiveAddress = receiveAddresses.first.address; - return receiveAddresses.first.address; + return walletInfo.type == WalletType.bitcoinCash ? toCashAddr(receiveAddress) : receiveAddress; } @override @@ -105,10 +111,9 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { @action Future getChangeAddress() async { updateChangeAddresses(); - + if (changeAddresses.isEmpty) { - final newAddresses = await _createNewAddresses( - gap, + final newAddresses = await _createNewAddresses(gap, hd: sideHd, startIndex: totalCountOfChangeAddresses > 0 ? totalCountOfChangeAddresses - 1 @@ -179,7 +184,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { } else { addrs = await _createNewAddresses( isHidden - ? defaultChangeAddressesCount + ? defaultChangeAddressesCount : defaultReceiveAddressesCount, startIndex: 0, hd: hd, diff --git a/cw_bitcoin/lib/electrum_wallet_snapshot.dart b/cw_bitcoin/lib/electrum_wallet_snapshot.dart index 0e9859927..0f570071b 100644 --- a/cw_bitcoin/lib/electrum_wallet_snapshot.dart +++ b/cw_bitcoin/lib/electrum_wallet_snapshot.dart @@ -67,4 +67,4 @@ class ElectrumWallletSnapshot { derivationType: derivationType, derivationPath: derivationPath); } -} +} \ No newline at end of file diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock index 843daa771..43391881f 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -66,6 +66,15 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.6" + bitbox: + dependency: "direct main" + description: + path: "." + ref: master + resolved-ref: ea65073efbaf395a5557e8cd7bd72f195cd7eb11 + url: "https://github.com/cake-tech/bitbox-flutter.git" + source: git + version: "1.0.1" bitcoin_flutter: dependency: "direct main" description: @@ -472,50 +481,50 @@ packages: dependency: "direct main" description: name: path_provider - sha256: dcea5feb97d8abf90cab9e9030b497fb7c3cbf26b7a1fe9e3ef7dcb0a1ddec95 + sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa url: "https://pub.dev" source: hosted - version: "2.0.12" + version: "2.1.1" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: a776c088d671b27f6e3aa8881d64b87b3e80201c64e8869b811325de7a76c15e + sha256: e595b98692943b4881b219f0a9e3945118d3c16bd7e2813f98ec6e532d905f72 url: "https://pub.dev" source: hosted - version: "2.0.22" + version: "2.2.1" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "62a68e7e1c6c459f9289859e2fae58290c981ce21d1697faf54910fe1faa4c74" + sha256: "19314d595120f82aca0ba62787d58dde2cc6b5df7d2f0daf72489e38d1b57f2d" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.3.1" path_provider_linux: dependency: transitive description: name: path_provider_linux - sha256: ab0987bf95bc591da42dffb38c77398fc43309f0b9b894dcc5d6f40c4b26c379 + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 url: "https://pub.dev" source: hosted - version: "2.1.7" + version: "2.2.1" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - sha256: f0abc8ebd7253741f05488b4813d936b4d07c6bae3e86148a09e342ee4b08e76 + sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c" url: "https://pub.dev" source: hosted - version: "2.0.5" + version: "2.1.1" path_provider_windows: dependency: transitive description: name: path_provider_windows - sha256: bcabbe399d4042b8ee687e17548d5d3f527255253b4a639f5f8d2094a9c2b45c + sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.2.1" platform: dependency: transitive description: @@ -601,6 +610,14 @@ packages: description: flutter source: sdk version: "0.0.99" + socks5_proxy: + dependency: transitive + description: + name: socks5_proxy + sha256: "1d21b5606169654bbf4cfb904e8e6ed897e9f763358709f87310c757096d909a" + url: "https://pub.dev" + source: hosted + version: "1.0.4" source_gen: dependency: transitive description: @@ -681,6 +698,15 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" + tor: + dependency: transitive + description: + path: "." + ref: main + resolved-ref: "09ba92cb11d4e3cacf97256e57863b805f79f2e5" + url: "https://github.com/cake-tech/tor.git" + source: git + version: "0.0.1" typed_data: dependency: transitive description: @@ -747,4 +773,4 @@ packages: version: "3.1.1" sdks: dart: ">=3.0.0 <4.0.0" - flutter: ">=3.0.0" + flutter: ">=3.7.0" diff --git a/cw_bitcoin/pubspec.yaml b/cw_bitcoin/pubspec.yaml index dae0af39b..693d5af7a 100644 --- a/cw_bitcoin/pubspec.yaml +++ b/cw_bitcoin/pubspec.yaml @@ -23,6 +23,10 @@ dependencies: git: url: https://github.com/cake-tech/bitcoin_flutter.git ref: cake-update-v3 + bitbox: + git: + url: https://github.com/cake-tech/bitbox-flutter.git + ref: master rxdart: ^0.27.5 unorm_dart: ^0.2.0 cryptography: ^2.0.5 diff --git a/cw_bitcoin_cash/.gitignore b/cw_bitcoin_cash/.gitignore new file mode 100644 index 000000000..96486fd93 --- /dev/null +++ b/cw_bitcoin_cash/.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_bitcoin_cash/.metadata b/cw_bitcoin_cash/.metadata new file mode 100644 index 000000000..4161da6ea --- /dev/null +++ b/cw_bitcoin_cash/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: b06b8b2710955028a6b562f5aa6fe62941d6febf + channel: stable + +project_type: package diff --git a/cw_bitcoin_cash/CHANGELOG.md b/cw_bitcoin_cash/CHANGELOG.md new file mode 100644 index 000000000..41cc7d819 --- /dev/null +++ b/cw_bitcoin_cash/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/cw_bitcoin_cash/LICENSE b/cw_bitcoin_cash/LICENSE new file mode 100644 index 000000000..ba75c69f7 --- /dev/null +++ b/cw_bitcoin_cash/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/cw_bitcoin_cash/README.md b/cw_bitcoin_cash/README.md new file mode 100644 index 000000000..02fe8ecab --- /dev/null +++ b/cw_bitcoin_cash/README.md @@ -0,0 +1,39 @@ + + +TODO: Put a short description of the package here that helps potential users +know whether this package might be useful for them. + +## Features + +TODO: List what your package can do. Maybe include images, gifs, or videos. + +## Getting started + +TODO: List prerequisites and provide or point to information on how to +start using the package. + +## Usage + +TODO: Include short and useful examples for package users. Add longer examples +to `/example` folder. + +```dart +const like = 'sample'; +``` + +## Additional information + +TODO: Tell users more about the package: where to find more information, how to +contribute to the package, how to file issues, what response they can expect +from the package authors, and more. diff --git a/cw_bitcoin_cash/analysis_options.yaml b/cw_bitcoin_cash/analysis_options.yaml new file mode 100644 index 000000000..a5744c1cf --- /dev/null +++ b/cw_bitcoin_cash/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_bitcoin_cash/lib/cw_bitcoin_cash.dart b/cw_bitcoin_cash/lib/cw_bitcoin_cash.dart new file mode 100644 index 000000000..732474ac4 --- /dev/null +++ b/cw_bitcoin_cash/lib/cw_bitcoin_cash.dart @@ -0,0 +1,9 @@ +library cw_bitcoin_cash; + +export 'src/bitcoin_cash_base.dart'; + +/// A Calculator. +class Calculator { + /// Returns [value] plus 1. + int addOne(int value) => value + 1; +} diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_address_utils.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_address_utils.dart new file mode 100644 index 000000000..5832835eb --- /dev/null +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_address_utils.dart @@ -0,0 +1,6 @@ +import 'package:bitbox/bitbox.dart' as bitbox; + +class AddressUtils { + static String getCashAddrFormat(String address) => bitbox.Address.toCashAddress(address); + static String toLegacyAddress(String address) => bitbox.Address.toLegacyAddress(address); +} diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_base.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_base.dart new file mode 100644 index 000000000..4699b1649 --- /dev/null +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_base.dart @@ -0,0 +1,7 @@ +export 'bitcoin_cash_wallet.dart'; +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 new file mode 100644 index 000000000..5f2a33ab6 --- /dev/null +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart @@ -0,0 +1,311 @@ +import 'dart:convert'; + +import 'package:bitbox/bitbox.dart' as bitbox; +import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; +import 'package:cw_bitcoin/bitcoin_address_record.dart'; +import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart'; +import 'package:cw_bitcoin/bitcoin_transaction_no_inputs_exception.dart'; +import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; +import 'package:cw_bitcoin/bitcoin_transaction_wrong_balance_exception.dart'; +import 'package:cw_bitcoin/bitcoin_unspent.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_cash/src/pending_bitcoin_cash_transaction.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/transaction_priority.dart'; +import 'package:cw_core/unspent_coins_info.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:flutter/foundation.dart'; +import 'package:hive/hive.dart'; +import 'package:mobx/mobx.dart'; + +import 'bitcoin_cash_base.dart'; + +part 'bitcoin_cash_wallet.g.dart'; + +class BitcoinCashWallet = BitcoinCashWalletBase with _$BitcoinCashWallet; + +abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { + BitcoinCashWalletBase( + {required String mnemonic, + required String password, + required WalletInfo walletInfo, + required Box unspentCoinsInfo, + required Uint8List seedBytes, + List? initialAddresses, + ElectrumBalance? initialBalance, + int initialRegularAddressIndex = 0, + int initialChangeAddressIndex = 0}) + : super( + mnemonic: mnemonic, + password: password, + walletInfo: walletInfo, + unspentCoinsInfo: unspentCoinsInfo, + networkType: bitcoin.bitcoin, + initialAddresses: initialAddresses, + initialBalance: initialBalance, + seedBytes: seedBytes, + currency: CryptoCurrency.bch) { + walletAddresses = BitcoinCashWalletAddresses(walletInfo, + electrumClient: electrumClient, + initialAddresses: initialAddresses, + initialRegularAddressIndex: initialRegularAddressIndex, + initialChangeAddressIndex: initialChangeAddressIndex, + mainHd: hd, + sideHd: bitcoin.HDWallet.fromSeed(seedBytes) + .derivePath("m/44'/145'/0'/1"), + networkType: networkType); + } + + + static Future create( + {required String mnemonic, + required String password, + required WalletInfo walletInfo, + required Box unspentCoinsInfo, + List? initialAddresses, + ElectrumBalance? initialBalance, + int initialRegularAddressIndex = 0, + int initialChangeAddressIndex = 0}) async { + return BitcoinCashWallet( + mnemonic: mnemonic, + password: password, + walletInfo: walletInfo, + unspentCoinsInfo: unspentCoinsInfo, + initialAddresses: initialAddresses, + initialBalance: initialBalance, + seedBytes: await Mnemonic.toSeed(mnemonic), + initialRegularAddressIndex: initialRegularAddressIndex, + initialChangeAddressIndex: initialChangeAddressIndex); + } + + static Future open({ + required String name, + required WalletInfo walletInfo, + required Box unspentCoinsInfo, + required String password, + }) async { + final snp = await ElectrumWallletSnapshot.load(name, walletInfo.type, password); + return BitcoinCashWallet( + mnemonic: snp.mnemonic, + password: password, + walletInfo: walletInfo, + unspentCoinsInfo: unspentCoinsInfo, + initialAddresses: snp.addresses, + initialBalance: snp.balance, + seedBytes: await Mnemonic.toSeed(snp.mnemonic), + initialRegularAddressIndex: snp.regularAddressIndex, + initialChangeAddressIndex: snp.changeAddressIndex); + } + + @override + Future createTransaction(Object credentials) async { + const minAmount = 546; + final transactionCredentials = credentials as BitcoinTransactionCredentials; + final inputs = []; + final outputs = transactionCredentials.outputs; + final hasMultiDestination = outputs.length > 1; + + var allInputsAmount = 0; + + if (unspentCoins.isEmpty) await updateUnspent(); + + for (final utx in unspentCoins) { + if (utx.isSending) { + allInputsAmount += utx.value; + inputs.add(utx); + } + } + + if (inputs.isEmpty) throw BitcoinTransactionNoInputsException(); + + final allAmountFee = transactionCredentials.feeRate != null + ? feeAmountWithFeeRate(transactionCredentials.feeRate!, inputs.length, outputs.length) + : feeAmountForPriority(transactionCredentials.priority!, inputs.length, outputs.length); + + final allAmount = allInputsAmount - allAmountFee; + + var credentialsAmount = 0; + var amount = 0; + var fee = 0; + + if (hasMultiDestination) { + if (outputs.any((item) => item.sendAll || item.formattedCryptoAmount! <= 0)) { + throw BitcoinTransactionWrongBalanceException(currency); + } + + credentialsAmount = outputs.fold(0, (acc, value) => acc + value.formattedCryptoAmount!); + + if (allAmount - credentialsAmount < minAmount) { + throw BitcoinTransactionWrongBalanceException(currency); + } + + amount = credentialsAmount; + + if (transactionCredentials.feeRate != null) { + fee = calculateEstimatedFeeWithFeeRate(transactionCredentials.feeRate!, amount, + outputsCount: outputs.length + 1); + } else { + fee = calculateEstimatedFee(transactionCredentials.priority, amount, + outputsCount: outputs.length + 1); + } + } else { + final output = outputs.first; + credentialsAmount = !output.sendAll ? output.formattedCryptoAmount! : 0; + + if (credentialsAmount > allAmount) { + throw BitcoinTransactionWrongBalanceException(currency); + } + + amount = output.sendAll || allAmount - credentialsAmount < minAmount + ? allAmount + : credentialsAmount; + + if (output.sendAll || amount == allAmount) { + fee = allAmountFee; + } else if (transactionCredentials.feeRate != null) { + fee = calculateEstimatedFeeWithFeeRate(transactionCredentials.feeRate!, amount); + } else { + fee = calculateEstimatedFee(transactionCredentials.priority, amount); + } + } + + if (fee == 0) { + throw BitcoinTransactionWrongBalanceException(currency); + } + + final totalAmount = amount + fee; + + if (totalAmount > balance[currency]!.confirmed || totalAmount > allInputsAmount) { + throw BitcoinTransactionWrongBalanceException(currency); + } + final txb = bitbox.Bitbox.transactionBuilder(testnet: false); + + final changeAddress = await walletAddresses.getChangeAddress(); + var leftAmount = totalAmount; + var totalInputAmount = 0; + + inputs.clear(); + + for (final utx in unspentCoins) { + if (utx.isSending) { + leftAmount = leftAmount - utx.value; + totalInputAmount += utx.value; + inputs.add(utx); + + if (leftAmount <= 0) { + break; + } + } + } + + if (inputs.isEmpty) throw BitcoinTransactionNoInputsException(); + + if (amount <= 0 || totalInputAmount < totalAmount) { + throw BitcoinTransactionWrongBalanceException(currency); + } + + inputs.forEach((input) { + txb.addInput(input.hash, input.vout); + }); + + outputs.forEach((item) { + final outputAmount = hasMultiDestination ? item.formattedCryptoAmount : amount; + final outputAddress = item.isParsedAddress ? item.extractedAddress! : item.address; + txb.addOutput(outputAddress, outputAmount!); + }); + + final estimatedSize = bitbox.BitcoinCash.getByteCount(inputs.length, outputs.length + 1); + + var feeAmount = 0; + + if (transactionCredentials.feeRate != null) { + feeAmount = transactionCredentials.feeRate! * estimatedSize; + } else { + feeAmount = feeRate(transactionCredentials.priority!) * estimatedSize; + } + + final changeValue = totalInputAmount - amount - feeAmount; + + if (changeValue > minAmount) { + txb.addOutput(changeAddress, changeValue); + } + + for (var i = 0; i < inputs.length; i++) { + final input = inputs[i]; + final keyPair = generateKeyPair( + hd: input.bitcoinAddressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd, + index: input.bitcoinAddressRecord.index); + txb.sign(i, keyPair, input.value); + } + + // Build the transaction + final tx = txb.build(); + + return PendingBitcoinCashTransaction(tx, type, + electrumClient: electrumClient, amount: amount, fee: fee); + } + + bitbox.ECPair generateKeyPair( + {required bitcoin.HDWallet hd, + required int index}) => + bitbox.ECPair.fromWIF(hd.derive(index).wif!); + + @override + int feeAmountForPriority( + BitcoinTransactionPriority priority, int inputsCount, int outputsCount) => + feeRate(priority) * bitbox.BitcoinCash.getByteCount(inputsCount, outputsCount); + + int feeAmountWithFeeRate(int feeRate, int inputsCount, int outputsCount) => + feeRate * bitbox.BitcoinCash.getByteCount(inputsCount, outputsCount); + + int calculateEstimatedFeeWithFeeRate(int feeRate, int? amount, {int? outputsCount}) { + int inputsCount = 0; + int totalValue = 0; + + for (final input in unspentCoins) { + if (input.isSending) { + inputsCount++; + totalValue += input.value; + } + if (amount != null && totalValue >= amount) { + break; + } + } + + if (amount != null && totalValue < amount) return 0; + + final _outputsCount = outputsCount ?? (amount != null ? 2 : 1); + + return feeAmountWithFeeRate(feeRate, inputsCount, _outputsCount); + } + + @override + int feeRate(TransactionPriority priority) { + if (priority is BitcoinCashTransactionPriority) { + switch (priority) { + case BitcoinCashTransactionPriority.slow: + return 1; + case BitcoinCashTransactionPriority.medium: + return 5; + case BitcoinCashTransactionPriority.fast: + return 10; + } + } + + return 0; + } + + @override + String signMessage(String message, {String? address = null}) { + final index = address != null + ? walletAddresses.addresses + .firstWhere((element) => element.address == AddressUtils.toLegacyAddress(address)) + .index + : null; + return index == null + ? base64Encode(hd.sign(message)) + : base64Encode(hd.derive(index).sign(message)); + } +} diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart new file mode 100644 index 000000000..1709c4d8f --- /dev/null +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart @@ -0,0 +1,34 @@ +import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; +import 'package:cw_bitcoin/bitcoin_address_record.dart'; +import 'package:cw_bitcoin/electrum.dart'; +import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; +import 'package:cw_bitcoin/utils.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:mobx/mobx.dart'; + +part 'bitcoin_cash_wallet_addresses.g.dart'; + +class BitcoinCashWalletAddresses = BitcoinCashWalletAddressesBase with _$BitcoinCashWalletAddresses; + +abstract class BitcoinCashWalletAddressesBase extends ElectrumWalletAddresses with Store { + BitcoinCashWalletAddressesBase(WalletInfo walletInfo, + {required bitcoin.HDWallet mainHd, + required bitcoin.HDWallet sideHd, + required bitcoin.NetworkType networkType, + required ElectrumClient electrumClient, + List? initialAddresses, + int initialRegularAddressIndex = 0, + int initialChangeAddressIndex = 0}) + : super(walletInfo, + initialAddresses: initialAddresses, + initialRegularAddressIndex: initialRegularAddressIndex, + initialChangeAddressIndex: initialChangeAddressIndex, + mainHd: mainHd, + sideHd: sideHd, + electrumClient: electrumClient, + networkType: networkType); + + @override + String getAddress({required int index, required bitcoin.HDWallet hd}) => + generateP2PKHAddress(hd: hd, index: index, networkType: networkType); +} 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 new file mode 100644 index 000000000..72caa6c58 --- /dev/null +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_creation_credentials.dart @@ -0,0 +1,26 @@ +import 'package:cw_core/wallet_credentials.dart'; +import 'package:cw_core/wallet_info.dart'; + +class BitcoinCashNewWalletCredentials extends WalletCredentials { + BitcoinCashNewWalletCredentials({required String name, WalletInfo? walletInfo}) + : super(name: name, walletInfo: walletInfo); +} + +class BitcoinCashRestoreWalletFromSeedCredentials extends WalletCredentials { + BitcoinCashRestoreWalletFromSeedCredentials( + {required String name, + required String password, + required this.mnemonic, + WalletInfo? walletInfo}) + : super(name: name, password: password, walletInfo: walletInfo); + + final String mnemonic; +} + +class BitcoinCashRestoreWalletFromWIFCredentials extends WalletCredentials { + BitcoinCashRestoreWalletFromWIFCredentials( + {required String name, required String password, required this.wif, WalletInfo? walletInfo}) + : super(name: name, password: password, walletInfo: walletInfo); + + final String wif; +} diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_service.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_service.dart new file mode 100644 index 000000000..8cc469a3a --- /dev/null +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_service.dart @@ -0,0 +1,112 @@ +import 'dart:io'; + +import 'package:bip39/bip39.dart'; +import 'package:cw_bitcoin_cash/cw_bitcoin_cash.dart'; +import 'package:cw_core/balance.dart'; +import 'package:cw_core/pathForWallet.dart'; +import 'package:cw_core/transaction_history.dart'; +import 'package:cw_core/transaction_info.dart'; +import 'package:cw_core/unspent_coins_info.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_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 { + BitcoinCashWalletService(this.walletInfoSource, this.unspentCoinsInfoSource); + + final Box walletInfoSource; + final Box unspentCoinsInfoSource; + + @override + WalletType getType() => WalletType.bitcoinCash; + + @override + Future isWalletExit(String name) async => + File(await pathForWallet(name: name, type: getType())).existsSync(); + + @override + Future create( + credentials) async { + final strength = (credentials.seedPhraseLength == 12) + ? 128 + : (credentials.seedPhraseLength == 24) + ? 256 + : 128; + final wallet = await BitcoinCashWalletBase.create( + mnemonic: await Mnemonic.generate(strength: strength), + password: credentials.password!, + walletInfo: credentials.walletInfo!, + unspentCoinsInfo: unspentCoinsInfoSource); + await wallet.save(); + await wallet.init(); + return wallet; + } + + @override + Future openWallet(String name, String password) async { + final walletInfo = walletInfoSource.values.firstWhereOrNull( + (info) => info.id == WalletBase.idFor(name, getType()))!; + final wallet = await BitcoinCashWalletBase.open( + password: password, name: name, walletInfo: walletInfo, + unspentCoinsInfo: unspentCoinsInfoSource); + await wallet.init(); + return wallet; + } + + @override + Future remove(String wallet) async { + File(await pathForWalletDir(name: wallet, type: getType())) + .delete(recursive: true); + final walletInfo = walletInfoSource.values.firstWhereOrNull( + (info) => info.id == WalletBase.idFor(wallet, getType()))!; + await walletInfoSource.delete(walletInfo.key); + } + + @override + Future rename(String currentName, String password, String newName) async { + final currentWalletInfo = walletInfoSource.values.firstWhereOrNull( + (info) => info.id == WalletBase.idFor(currentName, getType()))!; + final currentWallet = await BitcoinCashWalletBase.open( + password: password, + name: currentName, + walletInfo: currentWalletInfo, + unspentCoinsInfo: unspentCoinsInfoSource); + + await currentWallet.renameWalletFiles(newName); + + final newWalletInfo = currentWalletInfo; + newWalletInfo.id = WalletBase.idFor(newName, getType()); + newWalletInfo.name = newName; + + await walletInfoSource.put(currentWalletInfo.key, newWalletInfo); + } + + @override + Future + restoreFromKeys(credentials) { + // TODO: implement restoreFromKeys + throw UnimplementedError('restoreFromKeys() is not implemented'); + } + + @override + Future restoreFromSeed( + BitcoinCashRestoreWalletFromSeedCredentials credentials) async { + if (!validateMnemonic(credentials.mnemonic)) { + throw BitcoinCashMnemonicIsIncorrectException(); + } + + final wallet = await BitcoinCashWalletBase.create( + password: credentials.password!, + mnemonic: credentials.mnemonic, + walletInfo: credentials.walletInfo!, + unspentCoinsInfo: unspentCoinsInfoSource); + await wallet.save(); + await wallet.init(); + return wallet; + } +} diff --git a/cw_bitcoin_cash/lib/src/exceptions/bitcoin_cash_mnemonic_is_incorrect_exception.dart b/cw_bitcoin_cash/lib/src/exceptions/bitcoin_cash_mnemonic_is_incorrect_exception.dart new file mode 100644 index 000000000..7cce59085 --- /dev/null +++ b/cw_bitcoin_cash/lib/src/exceptions/bitcoin_cash_mnemonic_is_incorrect_exception.dart @@ -0,0 +1,5 @@ +class BitcoinCashMnemonicIsIncorrectException implements Exception { + @override + String toString() => + 'Bitcoin Cash mnemonic has incorrect format. Mnemonic should contain 12 or 24 words separated by space.'; +} diff --git a/cw_bitcoin_cash/lib/src/exceptions/exceptions.dart b/cw_bitcoin_cash/lib/src/exceptions/exceptions.dart new file mode 100644 index 000000000..746e3248a --- /dev/null +++ b/cw_bitcoin_cash/lib/src/exceptions/exceptions.dart @@ -0,0 +1 @@ +export 'bitcoin_cash_mnemonic_is_incorrect_exception.dart'; \ No newline at end of file diff --git a/cw_bitcoin_cash/lib/src/mnemonic.dart b/cw_bitcoin_cash/lib/src/mnemonic.dart new file mode 100644 index 000000000..b1f1ee984 --- /dev/null +++ b/cw_bitcoin_cash/lib/src/mnemonic.dart @@ -0,0 +1,11 @@ +import 'dart:typed_data'; + +import 'package:bip39/bip39.dart' as bip39; + +class Mnemonic { + /// Generate bip39 mnemonic + static String generate({int strength = 128}) => bip39.generateMnemonic(strength: strength); + + /// Create root seed from mnemonic + static Uint8List toSeed(String mnemonic) => bip39.mnemonicToSeed(mnemonic); +} diff --git a/cw_bitcoin_cash/lib/src/pending_bitcoin_cash_transaction.dart b/cw_bitcoin_cash/lib/src/pending_bitcoin_cash_transaction.dart new file mode 100644 index 000000000..d5ac36ce2 --- /dev/null +++ b/cw_bitcoin_cash/lib/src/pending_bitcoin_cash_transaction.dart @@ -0,0 +1,62 @@ +import 'package:cw_bitcoin/bitcoin_commit_transaction_exception.dart'; +import 'package:bitbox/bitbox.dart' as bitbox; +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'; + +class PendingBitcoinCashTransaction with PendingTransaction { + PendingBitcoinCashTransaction(this._tx, this.type, + {required this.electrumClient, + required this.amount, + required this.fee}) + : _listeners = []; + + final WalletType type; + final bitbox.Transaction _tx; + final ElectrumClient electrumClient; + final int amount; + final int fee; + + @override + String get id => _tx.getId(); + + @override + String get hex => _tx.toHex(); + + @override + String get amountFormatted => bitcoinAmountToString(amount: amount); + + @override + String get feeFormatted => bitcoinAmountToString(amount: fee); + + final List _listeners; + + @override + Future commit() async { + final result = + await electrumClient.broadcastTransaction(transactionRaw: _tx.toHex()); + + if (result.isEmpty) { + throw BitcoinCommitTransactionException(); + } + + _listeners?.forEach((listener) => listener(transactionInfo())); + } + + void addListener( + void Function(ElectrumTransactionInfo transaction) listener) => + _listeners.add(listener); + + ElectrumTransactionInfo transactionInfo() => ElectrumTransactionInfo(type, + id: id, + height: 0, + amount: amount, + direction: TransactionDirection.outgoing, + date: DateTime.now(), + isPending: true, + confirmations: 0, + fee: fee); +} diff --git a/cw_bitcoin_cash/linux/flutter/ephemeral/.plugin_symlinks/path_provider_linux b/cw_bitcoin_cash/linux/flutter/ephemeral/.plugin_symlinks/path_provider_linux new file mode 120000 index 000000000..17553f81e --- /dev/null +++ b/cw_bitcoin_cash/linux/flutter/ephemeral/.plugin_symlinks/path_provider_linux @@ -0,0 +1 @@ +/Users/omarhatem/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/ \ No newline at end of file diff --git a/cw_bitcoin_cash/linux/flutter/generated_plugin_registrant.cc b/cw_bitcoin_cash/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 000000000..e71a16d23 --- /dev/null +++ b/cw_bitcoin_cash/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,11 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + + +void fl_register_plugins(FlPluginRegistry* registry) { +} diff --git a/cw_bitcoin_cash/linux/flutter/generated_plugin_registrant.h b/cw_bitcoin_cash/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 000000000..e0f0a47bc --- /dev/null +++ b/cw_bitcoin_cash/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/cw_bitcoin_cash/linux/flutter/generated_plugins.cmake b/cw_bitcoin_cash/linux/flutter/generated_plugins.cmake new file mode 100644 index 000000000..3d57782b2 --- /dev/null +++ b/cw_bitcoin_cash/linux/flutter/generated_plugins.cmake @@ -0,0 +1,24 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST + tor +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/cw_bitcoin_cash/macos/Flutter/GeneratedPluginRegistrant.swift b/cw_bitcoin_cash/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 000000000..e777c67df --- /dev/null +++ b/cw_bitcoin_cash/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,12 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + +import path_provider_foundation + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) +} diff --git a/cw_bitcoin_cash/macos/Flutter/ephemeral/Flutter-Generated.xcconfig b/cw_bitcoin_cash/macos/Flutter/ephemeral/Flutter-Generated.xcconfig new file mode 100644 index 000000000..2f46994d3 --- /dev/null +++ b/cw_bitcoin_cash/macos/Flutter/ephemeral/Flutter-Generated.xcconfig @@ -0,0 +1,11 @@ +// This is a generated file; do not edit or check into version control. +FLUTTER_ROOT=C:\Users\borod\flutter +FLUTTER_APPLICATION_PATH=C:\cake_wallet\cw_bitcoin_cash +COCOAPODS_PARALLEL_CODE_SIGN=true +FLUTTER_BUILD_DIR=build +FLUTTER_BUILD_NAME=0.0.1 +FLUTTER_BUILD_NUMBER=0.0.1 +DART_OBFUSCATION=false +TRACK_WIDGET_CREATION=true +TREE_SHAKE_ICONS=false +PACKAGE_CONFIG=.dart_tool/package_config.json diff --git a/cw_bitcoin_cash/macos/Flutter/ephemeral/flutter_export_environment.sh b/cw_bitcoin_cash/macos/Flutter/ephemeral/flutter_export_environment.sh new file mode 100644 index 000000000..2a3bcca5a --- /dev/null +++ b/cw_bitcoin_cash/macos/Flutter/ephemeral/flutter_export_environment.sh @@ -0,0 +1,12 @@ +#!/bin/sh +# This is a generated file; do not edit or check into version control. +export "FLUTTER_ROOT=C:\Users\borod\flutter" +export "FLUTTER_APPLICATION_PATH=C:\cake_wallet\cw_bitcoin_cash" +export "COCOAPODS_PARALLEL_CODE_SIGN=true" +export "FLUTTER_BUILD_DIR=build" +export "FLUTTER_BUILD_NAME=0.0.1" +export "FLUTTER_BUILD_NUMBER=0.0.1" +export "DART_OBFUSCATION=false" +export "TRACK_WIDGET_CREATION=true" +export "TREE_SHAKE_ICONS=false" +export "PACKAGE_CONFIG=.dart_tool/package_config.json" diff --git a/cw_bitcoin_cash/pubspec.yaml b/cw_bitcoin_cash/pubspec.yaml new file mode 100644 index 000000000..30ed49e80 --- /dev/null +++ b/cw_bitcoin_cash/pubspec.yaml @@ -0,0 +1,76 @@ +name: cw_bitcoin_cash +description: A new Flutter package project. +version: 0.0.1 +publish_to: none +author: Cake Wallet +homepage: https://cakewallet.com + +environment: + sdk: '>=2.19.0 <3.0.0' + flutter: ">=1.17.0" + +dependencies: + flutter: + sdk: flutter + bip39: ^1.0.6 + bip32: ^2.0.0 + path_provider: ^2.0.11 + mobx: ^2.0.7+4 + flutter_mobx: ^2.0.6+1 + cw_core: + path: ../cw_core + cw_bitcoin: + path: ../cw_bitcoin + bitcoin_flutter: + git: + url: https://github.com/cake-tech/bitcoin_flutter.git + ref: cake-update-v3 + bitbox: + git: + url: https://github.com/cake-tech/bitbox-flutter.git + ref: master + + + +dev_dependencies: + flutter_test: + sdk: flutter + build_runner: ^2.1.11 + mobx_codegen: ^2.0.7 + hive_generator: ^1.1.3 + +# 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: + +# To add assets to your 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 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 +# + diff --git a/cw_bitcoin_cash/test/cw_bitcoin_cash_test.dart b/cw_bitcoin_cash/test/cw_bitcoin_cash_test.dart new file mode 100644 index 000000000..f06646a8f --- /dev/null +++ b/cw_bitcoin_cash/test/cw_bitcoin_cash_test.dart @@ -0,0 +1,12 @@ +import 'package:flutter_test/flutter_test.dart'; + +import 'package:cw_bitcoin_cash/cw_bitcoin_cash.dart'; + +void main() { + test('adds one to input values', () { + final calculator = Calculator(); + expect(calculator.addOne(2), 3); + expect(calculator.addOne(-7), -6); + expect(calculator.addOne(0), 1); + }); +} diff --git a/cw_bitcoin_cash/windows/flutter/generated_plugin_registrant.cc b/cw_bitcoin_cash/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 000000000..8b6d4680a --- /dev/null +++ b/cw_bitcoin_cash/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,11 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + + +void RegisterPlugins(flutter::PluginRegistry* registry) { +} diff --git a/cw_bitcoin_cash/windows/flutter/generated_plugin_registrant.h b/cw_bitcoin_cash/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 000000000..dc139d85a --- /dev/null +++ b/cw_bitcoin_cash/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/cw_bitcoin_cash/windows/flutter/generated_plugins.cmake b/cw_bitcoin_cash/windows/flutter/generated_plugins.cmake new file mode 100644 index 000000000..b93c4c30c --- /dev/null +++ b/cw_bitcoin_cash/windows/flutter/generated_plugins.cmake @@ -0,0 +1,23 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/cw_core/lib/amount_converter.dart b/cw_core/lib/amount_converter.dart index a11907ef2..6fd43dd82 100644 --- a/cw_core/lib/amount_converter.dart +++ b/cw_core/lib/amount_converter.dart @@ -80,6 +80,7 @@ class AmountConverter { case CryptoCurrency.xmr: return _moneroAmountToString(amount); case CryptoCurrency.btc: + case CryptoCurrency.bch: return _bitcoinAmountToString(amount); case CryptoCurrency.xhv: case CryptoCurrency.xag: diff --git a/cw_core/lib/balance.dart b/cw_core/lib/balance.dart index 6145411c4..431aff515 100644 --- a/cw_core/lib/balance.dart +++ b/cw_core/lib/balance.dart @@ -9,5 +9,5 @@ abstract class Balance { String get formattedAdditionalBalance; - String get formattedFrozenBalance => ''; + String get formattedUnAvailableBalance => ''; } diff --git a/cw_core/lib/crypto_currency.dart b/cw_core/lib/crypto_currency.dart index f6ffcdc8b..1936a87cf 100644 --- a/cw_core/lib/crypto_currency.dart +++ b/cw_core/lib/crypto_currency.dart @@ -6,6 +6,7 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen String title = '', int raw = -1, required this.name, + required this.decimals, this.fullName, this.iconPath, this.tag}) @@ -15,6 +16,7 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen final String? tag; final String? fullName; final String? iconPath; + final int decimals; static const all = [ CryptoCurrency.xmr, @@ -110,96 +112,96 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen ]; // title, tag (if applicable), fullName (if unique), raw, name, iconPath - static const xmr = CryptoCurrency(title: 'XMR', fullName: 'Monero', raw: 0, name: 'xmr', iconPath: 'assets/images/monero_icon.png'); - static const ada = CryptoCurrency(title: 'ADA', fullName: 'Cardano', raw: 1, name: 'ada', iconPath: 'assets/images/ada_icon.png'); - static const bch = CryptoCurrency(title: 'BCH', fullName: 'Bitcoin Cash', raw: 2, name: 'bch', iconPath: 'assets/images/bch_icon.png'); - static const bnb = CryptoCurrency(title: 'BNB', tag: 'BSC', fullName: 'Binance Coin', raw: 3, name: 'bnb', iconPath: 'assets/images/bnb_icon.png'); - static const btc = CryptoCurrency(title: 'BTC', fullName: 'Bitcoin', raw: 4, name: 'btc', iconPath: 'assets/images/btc.png'); - static const dai = CryptoCurrency(title: 'DAI', tag: 'ETH', fullName: 'Dai', raw: 5, name: 'dai', iconPath: 'assets/images/dai_icon.png'); - static const dash = CryptoCurrency(title: 'DASH', fullName: 'Dash', raw: 6, name: 'dash', iconPath: 'assets/images/dash_icon.png'); - static const eos = CryptoCurrency(title: 'EOS', fullName: 'EOS', raw: 7, name: 'eos', iconPath: 'assets/images/eos_icon.png'); - static const eth = CryptoCurrency(title: 'ETH', fullName: 'Ethereum', raw: 8, name: 'eth', iconPath: 'assets/images/eth_icon.png'); - static const ltc = CryptoCurrency(title: 'LTC', fullName: 'Litecoin', raw: 9, name: 'ltc', iconPath: 'assets/images/litecoin-ltc_icon.png'); - static const nano = CryptoCurrency(title: 'XNO', raw: 10, fullName: 'Nano', name: 'xno', iconPath: 'assets/images/nano_icon.png'); - static const trx = CryptoCurrency(title: 'TRX', fullName: 'TRON', raw: 11, name: 'trx', iconPath: 'assets/images/trx_icon.png'); - static const usdt = CryptoCurrency(title: 'USDT', tag: 'OMNI', fullName: 'USDT Tether', raw: 12, name: 'usdt', iconPath: 'assets/images/usdt_icon.png'); - static const usdterc20 = CryptoCurrency(title: 'USDT', tag: 'ETH', fullName: 'USDT Tether', raw: 13, name: 'usdterc20', iconPath: 'assets/images/usdterc20_icon.png'); - static const xlm = CryptoCurrency(title: 'XLM', fullName: 'Stellar', raw: 14, name: 'xlm', iconPath: 'assets/images/xlm_icon.png'); - static const xrp = CryptoCurrency(title: 'XRP', fullName: 'Ripple', raw: 15, name: 'xrp', iconPath: 'assets/images/xrp_icon.png'); - static const xhv = CryptoCurrency(title: 'XHV', fullName: 'Haven Protocol', raw: 16, name: 'xhv', iconPath: 'assets/images/xhv_logo.png'); + static const xmr = CryptoCurrency(title: 'XMR', fullName: 'Monero', raw: 0, name: 'xmr', iconPath: 'assets/images/monero_icon.png', decimals: 12); + static const ada = CryptoCurrency(title: 'ADA', fullName: 'Cardano', raw: 1, name: 'ada', iconPath: 'assets/images/ada_icon.png', decimals: 6); + static const bch = CryptoCurrency(title: 'BCH', fullName: 'Bitcoin Cash', raw: 2, name: 'bch', iconPath: 'assets/images/bch_icon.png', decimals: 8); + static const bnb = CryptoCurrency(title: 'BNB', tag: 'BSC', fullName: 'Binance Coin', raw: 3, name: 'bnb', iconPath: 'assets/images/bnb_icon.png', decimals: 8); + static const btc = CryptoCurrency(title: 'BTC', fullName: 'Bitcoin', raw: 4, name: 'btc', iconPath: 'assets/images/btc.png', decimals: 8); + static const dai = CryptoCurrency(title: 'DAI', tag: 'ETH', fullName: 'Dai', raw: 5, name: 'dai', iconPath: 'assets/images/dai_icon.png', decimals: 18); + static const dash = CryptoCurrency(title: 'DASH', fullName: 'Dash', raw: 6, name: 'dash', iconPath: 'assets/images/dash_icon.png', decimals: 8); + static const eos = CryptoCurrency(title: 'EOS', fullName: 'EOS', raw: 7, name: 'eos', iconPath: 'assets/images/eos_icon.png', decimals: 4); + static const eth = CryptoCurrency(title: 'ETH', fullName: 'Ethereum', raw: 8, name: 'eth', iconPath: 'assets/images/eth_icon.png', decimals: 18); + static const ltc = CryptoCurrency(title: 'LTC', fullName: 'Litecoin', raw: 9, name: 'ltc', iconPath: 'assets/images/litecoin-ltc_icon.png', decimals: 8); + static const nano = CryptoCurrency(title: 'XNO', fullName: 'Nano', raw: 10, name: 'xno', iconPath: 'assets/images/nano_icon.png', decimals: 30); + static const trx = CryptoCurrency(title: 'TRX', fullName: 'TRON', raw: 11, name: 'trx', iconPath: 'assets/images/trx_icon.png', decimals: 6); + static const usdt = CryptoCurrency(title: 'USDT', tag: 'OMNI', fullName: 'USDT Tether', raw: 12, name: 'usdt', iconPath: 'assets/images/usdt_icon.png', decimals: 6); + static const usdterc20 = CryptoCurrency(title: 'USDT', tag: 'ETH', fullName: 'USDT Tether', raw: 13, name: 'usdterc20', iconPath: 'assets/images/usdterc20_icon.png', decimals: 6); + static const xlm = CryptoCurrency(title: 'XLM', fullName: 'Stellar', raw: 14, name: 'xlm', iconPath: 'assets/images/xlm_icon.png', decimals: 7); + static const xrp = CryptoCurrency(title: 'XRP', fullName: 'Ripple', raw: 15, name: 'xrp', iconPath: 'assets/images/xrp_icon.png', decimals: 6); + static const xhv = CryptoCurrency(title: 'XHV', fullName: 'Haven Protocol', raw: 16, name: 'xhv', iconPath: 'assets/images/xhv_logo.png', decimals: 12); - static const xag = CryptoCurrency(title: 'XAG', tag: 'XHV', raw: 17, name: 'xag'); - static const xau = CryptoCurrency(title: 'XAU', tag: 'XHV', raw: 18, name: 'xau'); - static const xaud = CryptoCurrency(title: 'XAUD', tag: 'XHV', raw: 19, name: 'xaud'); - static const xbtc = CryptoCurrency(title: 'XBTC', tag: 'XHV', raw: 20, name: 'xbtc'); - static const xcad = CryptoCurrency(title: 'XCAD', tag: 'XHV', raw: 21, name: 'xcad'); - static const xchf = CryptoCurrency(title: 'XCHF', tag: 'XHV', raw: 22, name: 'xchf'); - static const xcny = CryptoCurrency(title: 'XCNY', tag: 'XHV', raw: 23, name: 'xcny'); - static const xeur = CryptoCurrency(title: 'XEUR', tag: 'XHV', raw: 24, name: 'xeur'); - static const xgbp = CryptoCurrency(title: 'XGBP', tag: 'XHV', raw: 25, name: 'xgbp'); - static const xjpy = CryptoCurrency(title: 'XJPY', tag: 'XHV', raw: 26, name: 'xjpy'); - static const xnok = CryptoCurrency(title: 'XNOK', tag: 'XHV', raw: 27, name: 'xnok'); - static const xnzd = CryptoCurrency(title: 'XNZD', tag: 'XHV', raw: 28, name: 'xnzd'); - static const xusd = CryptoCurrency(title: 'XUSD', tag: 'XHV', raw: 29, name: 'xusd'); + static const xag = CryptoCurrency(title: 'XAG', tag: 'XHV', raw: 17, name: 'xag', decimals: 12); + static const xau = CryptoCurrency(title: 'XAU', tag: 'XHV', raw: 18, name: 'xau', decimals: 12); + static const xaud = CryptoCurrency(title: 'XAUD', tag: 'XHV', raw: 19, name: 'xaud', decimals: 12); + static const xbtc = CryptoCurrency(title: 'XBTC', tag: 'XHV', raw: 20, name: 'xbtc', decimals: 12); + static const xcad = CryptoCurrency(title: 'XCAD', tag: 'XHV', raw: 21, name: 'xcad', decimals: 12); + static const xchf = CryptoCurrency(title: 'XCHF', tag: 'XHV', raw: 22, name: 'xchf', decimals: 12); + static const xcny = CryptoCurrency(title: 'XCNY', tag: 'XHV', raw: 23, name: 'xcny', decimals: 12); + static const xeur = CryptoCurrency(title: 'XEUR', tag: 'XHV', raw: 24, name: 'xeur', decimals: 12); + static const xgbp = CryptoCurrency(title: 'XGBP', tag: 'XHV', raw: 25, name: 'xgbp', decimals: 12); + static const xjpy = CryptoCurrency(title: 'XJPY', tag: 'XHV', raw: 26, name: 'xjpy', decimals: 12); + static const xnok = CryptoCurrency(title: 'XNOK', tag: 'XHV', raw: 27, name: 'xnok', decimals: 12); + static const xnzd = CryptoCurrency(title: 'XNZD', tag: 'XHV', raw: 28, name: 'xnzd', decimals: 12); + static const xusd = CryptoCurrency(title: 'XUSD', tag: 'XHV', raw: 29, name: 'xusd', decimals: 12); - static const ape = CryptoCurrency(title: 'APE', tag: 'ETH', fullName: 'ApeCoin', raw: 30, name: 'ape', iconPath: 'assets/images/ape_icon.png'); - static const avaxc = CryptoCurrency(title: 'AVAX', tag: 'AVAXC', raw: 31, name: 'avaxc', iconPath: 'assets/images/avaxc_icon.png'); - static const btt = CryptoCurrency(title: 'BTT', tag: 'ETH', fullName: 'BitTorrent', raw: 32, name: 'btt', iconPath: 'assets/images/btt_icon.png'); - static const bttc = CryptoCurrency(title: 'BTTC', tag: 'TRX', fullName: 'BitTorrent-NEW', raw: 33, name: 'bttc', iconPath: 'assets/images/btt_icon.png'); - static const doge = CryptoCurrency(title: 'DOGE', fullName: 'Dogecoin', raw: 34, name: 'doge', iconPath: 'assets/images/doge_icon.png'); - static const firo = CryptoCurrency(title: 'FIRO', raw: 35, name: 'firo', iconPath: 'assets/images/firo_icon.png'); - static const usdttrc20 = CryptoCurrency(title: 'USDT', tag: 'TRX', fullName: 'USDT Tether', raw: 36, name: 'usdttrc20', iconPath: 'assets/images/usdttrc20_icon.png'); - static const hbar = CryptoCurrency(title: 'HBAR', fullName: 'Hedera', raw: 37, name: 'hbar', iconPath: 'assets/images/hbar_icon.png', ); - static const sc = CryptoCurrency(title: 'SC', fullName: 'Siacoin', raw: 38, name: 'sc', iconPath: 'assets/images/sc_icon.png'); - static const sol = CryptoCurrency(title: 'SOL', fullName: 'Solana', raw: 39, name: 'sol', iconPath: 'assets/images/sol_icon.png'); - static const usdc = CryptoCurrency(title: 'USDC', tag: 'ETH', fullName: 'USD Coin', raw: 40, name: 'usdc', iconPath: 'assets/images/usdc_icon.png'); - static const usdcsol = CryptoCurrency(title: 'USDC', tag: 'SOL', fullName: 'USDC Coin', raw: 41, name: 'usdcsol', iconPath: 'assets/images/usdc_icon.png'); - static const zaddr = CryptoCurrency(title: 'ZZEC', tag: 'ZEC', fullName: 'Shielded Zcash', raw: 42, name: 'zaddr', iconPath: 'assets/images/zec_icon.png'); - static const zec = CryptoCurrency(title: 'TZEC', tag: 'ZEC', fullName: 'Transparent Zcash', raw: 43, name: 'zec', iconPath: 'assets/images/zec_icon.png'); - static const zen = CryptoCurrency(title: 'ZEN', fullName: 'Horizen', raw: 44, name: 'zen', iconPath: 'assets/images/zen_icon.png'); - static const xvg = CryptoCurrency(title: 'XVG', fullName: 'Verge', raw: 45, name: 'xvg', iconPath: 'assets/images/xvg_icon.png'); + static const ape = CryptoCurrency(title: 'APE', tag: 'ETH', fullName: 'ApeCoin', raw: 30, name: 'ape', iconPath: 'assets/images/ape_icon.png', decimals: 18); + static const avaxc = CryptoCurrency(title: 'AVAX', tag: 'AVAXC', fullName: 'Avalanche', raw: 31, name: 'avaxc', iconPath: 'assets/images/avaxc_icon.png', decimals: 9); + static const btt = CryptoCurrency(title: 'BTT', tag: 'ETH', fullName: 'BitTorrent', raw: 32, name: 'btt', iconPath: 'assets/images/btt_icon.png', decimals: 18); + static const bttc = CryptoCurrency(title: 'BTTC', tag: 'TRX', fullName: 'BitTorrent-NEW', raw: 33, name: 'bttc', iconPath: 'assets/images/btt_icon.png', decimals: 18); + static const doge = CryptoCurrency(title: 'DOGE', fullName: 'Dogecoin', raw: 34, name: 'doge', iconPath: 'assets/images/doge_icon.png', decimals: 8); + static const firo = CryptoCurrency(title: 'FIRO', raw: 35, name: 'firo', iconPath: 'assets/images/firo_icon.png', decimals: 8); + static const usdttrc20 = CryptoCurrency(title: 'USDT', tag: 'TRX', fullName: 'USDT Tether', raw: 36, name: 'usdttrc20', iconPath: 'assets/images/usdttrc20_icon.png', decimals: 6); + static const hbar = CryptoCurrency(title: 'HBAR', fullName: 'Hedera', raw: 37, name: 'hbar', iconPath: 'assets/images/hbar_icon.png', decimals: 8); + static const sc = CryptoCurrency(title: 'SC', fullName: 'Siacoin', raw: 38, name: 'sc', iconPath: 'assets/images/sc_icon.png', decimals: 16); + static const sol = CryptoCurrency(title: 'SOL', fullName: 'Solana', raw: 39, name: 'sol', iconPath: 'assets/images/sol_icon.png', decimals: 9); + static const usdc = CryptoCurrency(title: 'USDC', tag: 'ETH', fullName: 'USD Coin', raw: 40, name: 'usdc', iconPath: 'assets/images/usdc_icon.png', decimals: 6); + static const usdcsol = CryptoCurrency(title: 'USDC', tag: 'SOL', fullName: 'USDC Coin', raw: 41, name: 'usdcsol', iconPath: 'assets/images/usdc_icon.png', decimals: 6); + static const zaddr = CryptoCurrency(title: 'ZZEC', tag: 'ZEC', fullName: 'Shielded Zcash', raw: 42, name: 'zaddr', iconPath: 'assets/images/zec_icon.png', decimals: 8); + static const zec = CryptoCurrency(title: 'TZEC', tag: 'ZEC', fullName: 'Transparent Zcash', raw: 43, name: 'zec', iconPath: 'assets/images/zec_icon.png', decimals: 8); + 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'); - static const dcr = CryptoCurrency(title: 'DCR', fullName: 'Decred', raw: 47, name: 'dcr', iconPath: 'assets/images/dcr_icon.png'); - static const kmd = CryptoCurrency(title: 'KMD', fullName: 'Komodo', raw: 48, name: 'kmd', iconPath: 'assets/images/kmd_icon.png'); - static const mana = CryptoCurrency(title: 'MANA', tag: 'ETH', fullName: 'Decentraland', raw: 49, name: 'mana', iconPath: 'assets/images/mana_icon.png'); - static const maticpoly = CryptoCurrency(title: 'MATIC', tag: 'POLY', fullName: 'Polygon', raw: 50, name: 'maticpoly', iconPath: 'assets/images/matic_icon.png'); - static const matic = CryptoCurrency(title: 'MATIC', tag: 'ETH', fullName: 'Polygon', raw: 51, name: 'matic', iconPath: 'assets/images/matic_icon.png'); - static const mkr = CryptoCurrency(title: 'MKR', tag: 'ETH', fullName: 'Maker', raw: 52, name: 'mkr', iconPath: 'assets/images/mkr_icon.png'); - static const near = CryptoCurrency(title: 'NEAR', fullName: 'NEAR Protocol', raw: 53, name: 'near', iconPath: 'assets/images/near_icon.png'); - static const oxt = CryptoCurrency(title: 'OXT', tag: 'ETH', fullName: 'Orchid', raw: 54, name: 'oxt', iconPath: 'assets/images/oxt_icon.png'); - static const paxg = CryptoCurrency(title: 'PAXG', tag: 'ETH', fullName: 'Pax Gold', raw: 55, name: 'paxg', iconPath: 'assets/images/paxg_icon.png'); - static const pivx = CryptoCurrency(title: 'PIVX', raw: 56, name: 'pivx', iconPath: 'assets/images/pivx_icon.png'); - static const rune = CryptoCurrency(title: 'RUNE', fullName: 'Thorchain', raw: 57, name: 'rune', iconPath: 'assets/images/rune_icon.png'); - static const rvn = CryptoCurrency(title: 'RVN', fullName: 'Ravencoin', raw: 58, name: 'rvn', iconPath: 'assets/images/rvn_icon.png'); - static const scrt = CryptoCurrency(title: 'SCRT', fullName: 'Secret Network', raw: 59, name: 'scrt', iconPath: 'assets/images/scrt_icon.png'); - static const uni = CryptoCurrency(title: 'UNI', tag: 'ETH', fullName: 'Uniswap', raw: 60, name: 'uni', iconPath: 'assets/images/uni_icon.png'); - static const stx = CryptoCurrency(title: 'STX', fullName: 'Stacks', raw: 61, name: 'stx', iconPath: 'assets/images/stx_icon.png'); - static const btcln = CryptoCurrency(title: 'BTC', tag: 'LN', fullName: 'Bitcoin Lightning Network', raw: 62, name: 'btcln', iconPath: 'assets/images/btc.png'); - static const shib = CryptoCurrency(title: 'SHIB', tag: 'ETH', fullName: 'Shiba Inu', raw: 63, name: 'shib', iconPath: 'assets/images/shib_icon.png'); - static const aave = CryptoCurrency(title: 'AAVE', tag: 'ETH', fullName: 'Aave', raw: 64, name: 'aave', iconPath: 'assets/images/aave_icon.png'); - static const arb = CryptoCurrency(title: 'ARB', fullName: 'Arbitrum', raw: 65, name: 'arb', iconPath: 'assets/images/arb_icon.png'); - static const bat = CryptoCurrency(title: 'BAT', tag: 'ETH', fullName: 'Basic Attention Token', raw: 66, name: 'bat', iconPath: 'assets/images/bat_icon.png'); - static const comp = CryptoCurrency(title: 'COMP', tag: 'ETH', fullName: 'Compound', raw: 67, name: 'comp', iconPath: 'assets/images/comp_icon.png'); - static const cro = CryptoCurrency(title: 'CRO', tag: 'ETH', fullName: 'Crypto.com Cronos', raw: 68, name: 'cro', iconPath: 'assets/images/cro_icon.png'); - static const ens = CryptoCurrency(title: 'ENS', tag: 'ETH', fullName: 'Ethereum Name Service', raw: 69, name: 'ens', iconPath: 'assets/images/ens_icon.png'); - static const ftm = CryptoCurrency(title: 'FTM', tag: 'ETH', fullName: 'Fantom', raw: 70, name: 'ftm', iconPath: 'assets/images/ftm_icon.png'); - static const frax = CryptoCurrency(title: 'FRAX', tag: 'ETH', fullName: 'Frax', raw: 71, name: 'frax', iconPath: 'assets/images/frax_icon.png'); - static const gusd = CryptoCurrency(title: 'GUSD', tag: 'ETH', fullName: 'Gemini USD', raw: 72, name: 'gusd', iconPath: 'assets/images/gusd_icon.png'); - static const gtc = CryptoCurrency(title: 'GTC', tag: 'ETH', fullName: 'Gitcoin', raw: 73, name: 'gtc', iconPath: 'assets/images/gtc_icon.png'); - static const grt = CryptoCurrency(title: 'GRT', tag: 'ETH', fullName: 'The Graph', raw: 74, name: 'grt', iconPath: 'assets/images/grt_icon.png'); - static const ldo = CryptoCurrency(title: 'LDO', tag: 'ETH', fullName: 'Lido DAO', raw: 75, name: 'ldo', iconPath: 'assets/images/ldo_icon.png'); - static const nexo = CryptoCurrency(title: 'NEXO', tag: 'ETH', fullName: 'Nexo', raw: 76, name: 'nexo', iconPath: 'assets/images/nexo_icon.png'); - static const cake = CryptoCurrency(title: 'CAKE', tag: 'BSC', fullName: 'PancakeSwap', raw: 77, name: 'cake', iconPath: 'assets/images/cake_icon.png'); - static const pepe = CryptoCurrency(title: 'PEPE', tag: 'ETH', fullName: 'Pepe', raw: 78, name: 'pepe', iconPath: 'assets/images/pepe_icon.png'); - static const storj = CryptoCurrency(title: 'STORJ', tag: 'ETH', fullName: 'Storj', raw: 79, name: 'storj', iconPath: 'assets/images/storj_icon.png'); - static const tusd = CryptoCurrency(title: 'TUSD', tag: 'ETH', fullName: 'TrueUSD', raw: 80, name: 'tusd', iconPath: 'assets/images/tusd_icon.png'); - static const wbtc = CryptoCurrency(title: 'WBTC', tag: 'ETH', fullName: 'Wrapped Bitcoin', raw: 81, name: 'wbtc', iconPath: 'assets/images/wbtc_icon.png'); - static const weth = CryptoCurrency(title: 'WETH', tag: 'ETH', fullName: 'Wrapped Ethereum', raw: 82, name: 'weth', iconPath: 'assets/images/weth_icon.png'); - static const zrx = CryptoCurrency(title: 'ZRX', tag: 'ETH', fullName: '0x Protocol', raw: 83, name: 'zrx', iconPath: 'assets/images/zrx_icon.png'); - static const dydx = CryptoCurrency(title: 'DYDX', tag: 'ETH', fullName: 'dYdX', raw: 84, name: 'dydx', iconPath: 'assets/images/dydx_icon.png'); - static const steth = CryptoCurrency(title: 'STETH', tag: 'ETH', fullName: 'Lido Staked Ethereum', raw: 85, name: 'steth', iconPath: 'assets/images/steth_icon.png'); - static const banano = CryptoCurrency(title: 'BAN', raw: 86, name: 'banano', iconPath: 'assets/images/nano_icon.png'); + 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 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 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); + static const oxt = CryptoCurrency(title: 'OXT', tag: 'ETH', fullName: 'Orchid', raw: 54, name: 'oxt', iconPath: 'assets/images/oxt_icon.png', decimals: 18); + static const paxg = CryptoCurrency(title: 'PAXG', tag: 'ETH', fullName: 'Pax Gold', raw: 55, name: 'paxg', iconPath: 'assets/images/paxg_icon.png', decimals: 18); + static const pivx = CryptoCurrency(title: 'PIVX', raw: 56, name: 'pivx', iconPath: 'assets/images/pivx_icon.png', decimals: 8); + static const rune = CryptoCurrency(title: 'RUNE', fullName: 'Thorchain', raw: 57, name: 'rune', iconPath: 'assets/images/rune_icon.png', decimals: 18); + static const rvn = CryptoCurrency(title: 'RVN', fullName: 'Ravencoin', raw: 58, name: 'rvn', iconPath: 'assets/images/rvn_icon.png', decimals: 8); + static const scrt = CryptoCurrency(title: 'SCRT', fullName: 'Secret Network', raw: 59, name: 'scrt', iconPath: 'assets/images/scrt_icon.png', decimals: 6); + static const uni = CryptoCurrency(title: 'UNI', tag: 'ETH', fullName: 'Uniswap', raw: 60, name: 'uni', iconPath: 'assets/images/uni_icon.png', decimals: 18); + static const stx = CryptoCurrency(title: 'STX', fullName: 'Stacks', raw: 61, name: 'stx', iconPath: 'assets/images/stx_icon.png', decimals: 8); + static const btcln = CryptoCurrency(title: 'BTC', tag: 'LN', fullName: 'Bitcoin Lightning Network', raw: 62, name: 'btcln', iconPath: 'assets/images/btc.png', decimals: 8); + static const shib = CryptoCurrency(title: 'SHIB', tag: 'ETH', fullName: 'Shiba Inu', raw: 63, name: 'shib', iconPath: 'assets/images/shib_icon.png', decimals: 18); + static const aave = CryptoCurrency(title: 'AAVE', tag: 'ETH', fullName: 'Aave', raw: 64, name: 'aave', iconPath: 'assets/images/aave_icon.png', decimals: 18); + static const arb = CryptoCurrency(title: 'ARB', fullName: 'Arbitrum', raw: 65, name: 'arb', iconPath: 'assets/images/arb_icon.png', decimals: 18); + static const bat = CryptoCurrency(title: 'BAT', tag: 'ETH', fullName: 'Basic Attention Token', raw: 66, name: 'bat', iconPath: 'assets/images/bat_icon.png', decimals: 18); + static const comp = CryptoCurrency(title: 'COMP', tag: 'ETH', fullName: 'Compound', raw: 67, name: 'comp', iconPath: 'assets/images/comp_icon.png', decimals: 18); + static const cro = CryptoCurrency(title: 'CRO', tag: 'ETH', fullName: 'Crypto.com Cronos', raw: 68, name: 'cro', iconPath: 'assets/images/cro_icon.png', decimals: 8); + static const ens = CryptoCurrency(title: 'ENS', tag: 'ETH', fullName: 'Ethereum Name Service', raw: 69, name: 'ens', iconPath: 'assets/images/ens_icon.png', decimals: 18); + static const ftm = CryptoCurrency(title: 'FTM', tag: 'ETH', fullName: 'Fantom', raw: 70, name: 'ftm', iconPath: 'assets/images/ftm_icon.png', decimals: 18); + static const frax = CryptoCurrency(title: 'FRAX', tag: 'ETH', fullName: 'Frax', raw: 71, name: 'frax', iconPath: 'assets/images/frax_icon.png', decimals: 18); + static const gusd = CryptoCurrency(title: 'GUSD', tag: 'ETH', fullName: 'Gemini USD', raw: 72, name: 'gusd', iconPath: 'assets/images/gusd_icon.png', decimals: 2); + static const gtc = CryptoCurrency(title: 'GTC', tag: 'ETH', fullName: 'Gitcoin', raw: 73, name: 'gtc', iconPath: 'assets/images/gtc_icon.png', decimals: 18); + static const grt = CryptoCurrency(title: 'GRT', tag: 'ETH', fullName: 'The Graph', raw: 74, name: 'grt', iconPath: 'assets/images/grt_icon.png', decimals: 18); + static const ldo = CryptoCurrency(title: 'LDO', tag: 'ETH', fullName: 'Lido DAO', raw: 75, name: 'ldo', iconPath: 'assets/images/ldo_icon.png', decimals: 18); + static const nexo = CryptoCurrency(title: 'NEXO', tag: 'ETH', fullName: 'Nexo', raw: 76, name: 'nexo', iconPath: 'assets/images/nexo_icon.png', decimals: 18); + static const cake = CryptoCurrency(title: 'CAKE', tag: 'BSC', fullName: 'PancakeSwap', raw: 77, name: 'cake', iconPath: 'assets/images/cake_icon.png', decimals: 18); + static const pepe = CryptoCurrency(title: 'PEPE', tag: 'ETH', fullName: 'Pepe', raw: 78, name: 'pepe', iconPath: 'assets/images/pepe_icon.png', decimals: 18); + static const storj = CryptoCurrency(title: 'STORJ', tag: 'ETH', fullName: 'Storj', raw: 79, name: 'storj', iconPath: 'assets/images/storj_icon.png', decimals: 8); + static const tusd = CryptoCurrency(title: 'TUSD', tag: 'ETH', fullName: 'TrueUSD', raw: 80, name: 'tusd', iconPath: 'assets/images/tusd_icon.png', decimals: 18); + static const wbtc = CryptoCurrency(title: 'WBTC', tag: 'ETH', fullName: 'Wrapped Bitcoin', raw: 81, name: 'wbtc', iconPath: 'assets/images/wbtc_icon.png', decimals: 8); + static const weth = CryptoCurrency(title: 'WETH', tag: 'ETH', fullName: 'Wrapped Ethereum', raw: 82, name: 'weth', iconPath: 'assets/images/weth_icon.png', decimals: 18); + static const zrx = CryptoCurrency(title: 'ZRX', tag: 'ETH', fullName: '0x Protocol', raw: 83, name: 'zrx', iconPath: 'assets/images/zrx_icon.png', decimals: 18); + 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 final Map _rawCurrencyMap = @@ -223,7 +225,6 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen }); static CryptoCurrency deserialize({required int raw}) { - if (CryptoCurrency._rawCurrencyMap[raw] == null) { final s = 'Unexpected token: $raw for CryptoCurrency deserialize'; throw ArgumentError.value(raw, 'raw', s); @@ -232,7 +233,6 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen } static CryptoCurrency fromString(String name) { - if (CryptoCurrency._nameCurrencyMap[name.toLowerCase()] == null) { final s = 'Unexpected token: $name for CryptoCurrency fromString'; throw ArgumentError.value(name, 'name', s); @@ -241,14 +241,12 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen } static CryptoCurrency fromFullName(String name) { - if (CryptoCurrency._fullNameCurrencyMap[name.toLowerCase()] == null) { final s = 'Unexpected token: $name for CryptoCurrency fromFullName'; throw ArgumentError.value(name, 'Fullname', s); } return CryptoCurrency._fullNameCurrencyMap[name.toLowerCase()]!; } - @override String toString() => title; diff --git a/cw_core/lib/currency_for_wallet_type.dart b/cw_core/lib/currency_for_wallet_type.dart index 2db858b30..4c330b073 100644 --- a/cw_core/lib/currency_for_wallet_type.dart +++ b/cw_core/lib/currency_for_wallet_type.dart @@ -13,6 +13,8 @@ CryptoCurrency currencyForWalletType(WalletType type) { return CryptoCurrency.xhv; case WalletType.ethereum: return CryptoCurrency.eth; + case WalletType.bitcoinCash: + return CryptoCurrency.bch; case WalletType.nano: return CryptoCurrency.nano; case WalletType.banano: diff --git a/cw_core/lib/erc20_token.dart b/cw_core/lib/erc20_token.dart index fd27aaba6..011fdef1d 100644 --- a/cw_core/lib/erc20_token.dart +++ b/cw_core/lib/erc20_token.dart @@ -37,6 +37,7 @@ class Erc20Token extends CryptoCurrency with HiveObjectMixin { fullName: name, tag: "ETH", iconPath: iconPath, + decimals: decimal ); Erc20Token.copyWith(Erc20Token other, String? icon) @@ -52,6 +53,7 @@ class Erc20Token extends CryptoCurrency with HiveObjectMixin { fullName: other.name, tag: "ETH", iconPath: icon, + decimals: other.decimal ); static const typeId = ERC20_TOKEN_TYPE_ID; diff --git a/cw_core/lib/monero_balance.dart b/cw_core/lib/monero_balance.dart index bf30110a3..98a7f134a 100644 --- a/cw_core/lib/monero_balance.dart +++ b/cw_core/lib/monero_balance.dart @@ -4,17 +4,18 @@ import 'package:cw_core/monero_amount_format.dart'; class MoneroBalance extends Balance { MoneroBalance({required this.fullBalance, required this.unlockedBalance, this.frozenBalance = 0}) : formattedFullBalance = moneroAmountToString(amount: fullBalance), - formattedUnlockedBalance = moneroAmountToString(amount: unlockedBalance), - frozenFormatted = moneroAmountToString(amount: frozenBalance), + formattedUnlockedBalance = moneroAmountToString(amount: unlockedBalance - frozenBalance), + formattedLockedBalance = + moneroAmountToString(amount: frozenBalance + fullBalance - unlockedBalance), super(unlockedBalance, fullBalance); MoneroBalance.fromString( {required this.formattedFullBalance, required this.formattedUnlockedBalance, - this.frozenFormatted = '0.0'}) + this.formattedLockedBalance = '0.0'}) : fullBalance = moneroParseAmount(amount: formattedFullBalance), unlockedBalance = moneroParseAmount(amount: formattedUnlockedBalance), - frozenBalance = moneroParseAmount(amount: frozenFormatted), + frozenBalance = moneroParseAmount(amount: formattedLockedBalance), super(moneroParseAmount(amount: formattedUnlockedBalance), moneroParseAmount(amount: formattedFullBalance)); @@ -23,10 +24,11 @@ class MoneroBalance extends Balance { final int frozenBalance; final String formattedFullBalance; final String formattedUnlockedBalance; - final String frozenFormatted; + final String formattedLockedBalance; @override - String get formattedFrozenBalance => frozenFormatted == '0.0' ? '' : frozenFormatted; + String get formattedUnAvailableBalance => + formattedLockedBalance == '0.0' ? '' : formattedLockedBalance; @override String get formattedAvailableBalance => formattedUnlockedBalance; diff --git a/cw_core/lib/nano_account_info_response.dart b/cw_core/lib/nano_account_info_response.dart new file mode 100644 index 000000000..319bbb861 --- /dev/null +++ b/cw_core/lib/nano_account_info_response.dart @@ -0,0 +1,23 @@ +class AccountInfoResponse { + String frontier; + int confirmationHeight; + String balance; + String representative; + String? address; + + AccountInfoResponse({ + required this.frontier, + required this.balance, + required this.representative, + required this.confirmationHeight, + }); + + factory AccountInfoResponse.fromJson(Map json) { + return AccountInfoResponse( + frontier: json['frontier'] as String, + representative: json['representative'] as String, + balance: json['balance'] as String, + confirmationHeight: int.parse(json['confirmation_height'] as String), + ); + } +} diff --git a/cw_core/lib/node.dart b/cw_core/lib/node.dart index a07030d64..3f6056ae1 100644 --- a/cw_core/lib/node.dart +++ b/cw_core/lib/node.dart @@ -6,6 +6,7 @@ import 'package:hive/hive.dart'; import 'package:cw_core/hive_type_ids.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:http/io_client.dart' as ioc; +import 'package:tor/tor.dart'; part 'node.g.dart'; @@ -78,6 +79,8 @@ class Node extends HiveObject with Keyable { return Uri.http(uriRaw, ''); case WalletType.ethereum: return Uri.https(uriRaw, ''); + case WalletType.bitcoinCash: + return createUriFromElectrumAddress(uriRaw); case WalletType.nano: case WalletType.banano: if (isSSL) { @@ -127,9 +130,7 @@ class Node extends HiveObject with Keyable { try { switch (type) { case WalletType.monero: - return useSocksProxy - ? requestNodeWithProxy(socksProxyAddress ?? '') - : requestMoneroNode(); + return requestMoneroNode(); case WalletType.bitcoin: return requestElectrumServer(); case WalletType.litecoin: @@ -138,6 +139,8 @@ class Node extends HiveObject with Keyable { return requestMoneroNode(); case WalletType.ethereum: return requestElectrumServer(); + case WalletType.bitcoinCash: + return requestElectrumServer(); case WalletType.nano: case WalletType.banano: return requestNanoNode(); @@ -150,6 +153,9 @@ class Node extends HiveObject with Keyable { } Future requestMoneroNode() async { + if (uri.toString().contains(".onion") || useSocksProxy) { + return await requestNodeWithProxy(); + } final path = '/json_rpc'; final rpcUri = isSSL ? Uri.https(uri.authority, path) : Uri.http(uri.authority, path); final realm = 'monero-rpc'; @@ -158,6 +164,9 @@ class Node extends HiveObject with Keyable { try { final authenticatingClient = HttpClient(); + authenticatingClient.badCertificateCallback = + ((X509Certificate cert, String host, int port) => true); + authenticatingClient.addCredentials( rpcUri, realm, @@ -198,11 +207,20 @@ class Node extends HiveObject with Keyable { } } - Future requestNodeWithProxy(String proxy) async { - if (proxy.isEmpty || !proxy.contains(':')) { + Future requestNodeWithProxy() async { + if ((socksProxyAddress == null || + socksProxyAddress!.isEmpty || + !socksProxyAddress!.contains(':')) && + !Tor.instance.enabled) { return false; } - final proxyAddress = proxy.split(':')[0]; + + String? proxy = socksProxyAddress; + + if ((proxy?.isEmpty ?? true) && Tor.instance.enabled) { + proxy = "${InternetAddress.loopbackIPv4.address}:${Tor.instance.port}"; + } + final proxyAddress = proxy!.split(':')[0]; final proxyPort = int.parse(proxy.split(':')[1]); try { final socket = await Socket.connect(proxyAddress, proxyPort, timeout: Duration(seconds: 5)); diff --git a/cw_core/lib/transaction_info.dart b/cw_core/lib/transaction_info.dart index b8e4a5e0c..38b4b799d 100644 --- a/cw_core/lib/transaction_info.dart +++ b/cw_core/lib/transaction_info.dart @@ -14,6 +14,7 @@ abstract class TransactionInfo extends Object with Keyable { String fiatAmount(); String? feeFormatted(); void changeFiatAmount(String amount); + String? to; @override dynamic get keyIndex => id; diff --git a/cw_core/lib/unspent_coins_info.dart b/cw_core/lib/unspent_coins_info.dart index 68bbcbfd2..25abd3e48 100644 --- a/cw_core/lib/unspent_coins_info.dart +++ b/cw_core/lib/unspent_coins_info.dart @@ -14,7 +14,9 @@ class UnspentCoinsInfo extends HiveObject { required this.address, required this.vout, required this.value, - this.keyImage = null + this.keyImage = null, + this.isChange = false, + this.accountIndex = 0 }); static const typeId = UNSPENT_COINS_INFO_TYPE_ID; @@ -47,6 +49,12 @@ class UnspentCoinsInfo extends HiveObject { @HiveField(8, defaultValue: null) String? keyImage; + + @HiveField(9, defaultValue: false) + bool isChange; + + @HiveField(10, defaultValue: 0) + int accountIndex; String get note => noteRaw ?? ''; diff --git a/lib/entities/unspent_transaction_output.dart b/cw_core/lib/unspent_transaction_output.dart similarity index 90% rename from lib/entities/unspent_transaction_output.dart rename to cw_core/lib/unspent_transaction_output.dart index 6827f4c01..b52daf43c 100644 --- a/lib/entities/unspent_transaction_output.dart +++ b/cw_core/lib/unspent_transaction_output.dart @@ -2,6 +2,7 @@ class Unspent { Unspent(this.address, this.hash, this.value, this.vout, this.keyImage) : isSending = true, isFrozen = false, + isChange = false, note = ''; final String address; @@ -10,6 +11,7 @@ class Unspent { final int vout; final String? keyImage; + bool isChange; bool isSending; bool isFrozen; String note; diff --git a/cw_core/lib/wallet_addresses.dart b/cw_core/lib/wallet_addresses.dart index 27b5468c5..632eb1332 100644 --- a/cw_core/lib/wallet_addresses.dart +++ b/cw_core/lib/wallet_addresses.dart @@ -36,4 +36,6 @@ abstract class WalletAddresses { print(e.toString()); } } -} \ No newline at end of file + + bool containsAddress(String address) => addressesMap.containsKey(address); +} diff --git a/cw_core/lib/wallet_base.dart b/cw_core/lib/wallet_base.dart index 1da90a700..d15ea42cd 100644 --- a/cw_core/lib/wallet_base.dart +++ b/cw_core/lib/wallet_base.dart @@ -13,9 +13,7 @@ import 'package:cw_core/sync_status.dart'; import 'package:cw_core/node.dart'; import 'package:cw_core/wallet_type.dart'; -abstract class WalletBase< - BalanceType extends Balance, - HistoryType extends TransactionHistoryBase, +abstract class WalletBase { WalletBase(this.walletInfo); @@ -86,4 +84,6 @@ abstract class WalletBase< void setExceptionHandler(void Function(FlutterErrorDetails) onError) => null; Future renameWalletFiles(String newWalletName); + + String signMessage(String message, {String? address = null}) => throw UnimplementedError(); } diff --git a/cw_core/lib/wallet_credentials.dart b/cw_core/lib/wallet_credentials.dart index 0cdf892bd..4d5f331c9 100644 --- a/cw_core/lib/wallet_credentials.dart +++ b/cw_core/lib/wallet_credentials.dart @@ -4,6 +4,7 @@ abstract class WalletCredentials { WalletCredentials({ required this.name, this.height, + this.seedPhraseLength, this.walletInfo, this.password, this.derivationType, @@ -12,6 +13,7 @@ abstract class WalletCredentials { final String name; final int? height; + int? seedPhraseLength; String? password; DerivationType? derivationType; String? derivationPath; diff --git a/cw_core/lib/wallet_service.dart b/cw_core/lib/wallet_service.dart index f95bc1a44..f6d0ca192 100644 --- a/cw_core/lib/wallet_service.dart +++ b/cw_core/lib/wallet_service.dart @@ -1,9 +1,11 @@ +import 'package:cw_core/node.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_credentials.dart'; +import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; -abstract class WalletService { +abstract class WalletService { WalletType getType(); Future create(N credentials); diff --git a/cw_core/lib/wallet_type.dart b/cw_core/lib/wallet_type.dart index 0125facaf..debf92e11 100644 --- a/cw_core/lib/wallet_type.dart +++ b/cw_core/lib/wallet_type.dart @@ -10,6 +10,7 @@ const walletTypes = [ WalletType.litecoin, WalletType.haven, WalletType.ethereum, + WalletType.bitcoinCash, WalletType.nano, WalletType.banano, ]; @@ -39,6 +40,10 @@ enum WalletType { @HiveField(7) banano, + + @HiveField(8) + bitcoinCash, + } int serializeToInt(WalletType type) { @@ -57,6 +62,8 @@ int serializeToInt(WalletType type) { return 5; case WalletType.banano: return 6; + case WalletType.bitcoinCash: + return 7; default: return -1; } @@ -78,6 +85,8 @@ WalletType deserializeFromInt(int raw) { return WalletType.nano; case 6: return WalletType.banano; + case 7: + return WalletType.bitcoinCash; default: throw Exception('Unexpected token: $raw for WalletType deserializeFromInt'); } @@ -95,6 +104,8 @@ String walletTypeToString(WalletType type) { return 'Haven'; case WalletType.ethereum: return 'Ethereum'; + case WalletType.bitcoinCash: + return 'Bitcoin Cash'; case WalletType.nano: return 'Nano'; case WalletType.banano: @@ -116,6 +127,8 @@ String walletTypeToDisplayName(WalletType type) { return 'Haven (XHV)'; case WalletType.ethereum: return 'Ethereum (ETH)'; + case WalletType.bitcoinCash: + return 'Bitcoin Cash (BCH)'; case WalletType.nano: return 'Nano (XNO)'; case WalletType.banano: @@ -137,6 +150,8 @@ CryptoCurrency walletTypeToCryptoCurrency(WalletType type) { return CryptoCurrency.xhv; case WalletType.ethereum: return CryptoCurrency.eth; + case WalletType.bitcoinCash: + return CryptoCurrency.bch; case WalletType.nano: return CryptoCurrency.nano; case WalletType.banano: diff --git a/cw_core/pubspec.lock b/cw_core/pubspec.lock index e399526fd..058f33ba4 100644 --- a/cw_core/pubspec.lock +++ b/cw_core/pubspec.lock @@ -407,50 +407,50 @@ packages: dependency: "direct main" description: name: path_provider - sha256: dcea5feb97d8abf90cab9e9030b497fb7c3cbf26b7a1fe9e3ef7dcb0a1ddec95 + sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa url: "https://pub.dev" source: hosted - version: "2.0.12" + version: "2.1.1" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: a776c088d671b27f6e3aa8881d64b87b3e80201c64e8869b811325de7a76c15e + sha256: e595b98692943b4881b219f0a9e3945118d3c16bd7e2813f98ec6e532d905f72 url: "https://pub.dev" source: hosted - version: "2.0.22" + version: "2.2.1" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "62a68e7e1c6c459f9289859e2fae58290c981ce21d1697faf54910fe1faa4c74" + sha256: "19314d595120f82aca0ba62787d58dde2cc6b5df7d2f0daf72489e38d1b57f2d" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.3.1" path_provider_linux: dependency: transitive description: name: path_provider_linux - sha256: ab0987bf95bc591da42dffb38c77398fc43309f0b9b894dcc5d6f40c4b26c379 + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 url: "https://pub.dev" source: hosted - version: "2.1.7" + version: "2.2.1" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - sha256: f0abc8ebd7253741f05488b4813d936b4d07c6bae3e86148a09e342ee4b08e76 + sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c" url: "https://pub.dev" source: hosted - version: "2.0.5" + version: "2.1.1" path_provider_windows: dependency: transitive description: name: path_provider_windows - sha256: bcabbe399d4042b8ee687e17548d5d3f527255253b4a639f5f8d2094a9c2b45c + sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.2.1" platform: dependency: transitive description: @@ -528,6 +528,14 @@ packages: description: flutter source: sdk version: "0.0.99" + socks5_proxy: + dependency: "direct main" + description: + name: socks5_proxy + sha256: "1d21b5606169654bbf4cfb904e8e6ed897e9f763358709f87310c757096d909a" + url: "https://pub.dev" + source: hosted + version: "1.0.4" source_gen: dependency: transitive description: @@ -608,6 +616,15 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" + tor: + dependency: "direct main" + description: + path: "." + ref: main + resolved-ref: "09ba92cb11d4e3cacf97256e57863b805f79f2e5" + url: "https://github.com/cake-tech/tor.git" + source: git + version: "0.0.1" typed_data: dependency: transitive description: @@ -666,4 +683,4 @@ packages: version: "3.1.1" sdks: dart: ">=3.0.0 <4.0.0" - flutter: ">=3.0.0" + flutter: ">=3.7.0" diff --git a/cw_core/pubspec.yaml b/cw_core/pubspec.yaml index 9dcb7eaba..3763a7318 100644 --- a/cw_core/pubspec.yaml +++ b/cw_core/pubspec.yaml @@ -19,6 +19,11 @@ dependencies: flutter_mobx: ^2.0.6+1 intl: ^0.18.0 encrypt: ^5.0.1 + socks5_proxy: ^1.0.4 + tor: + git: + url: https://github.com/cake-tech/tor.git + ref: main dev_dependencies: flutter_test: diff --git a/cw_ethereum/lib/ethereum_client.dart b/cw_ethereum/lib/ethereum_client.dart index 7eba43aa7..f0c7381e8 100644 --- a/cw_ethereum/lib/ethereum_client.dart +++ b/cw_ethereum/lib/ethereum_client.dart @@ -38,17 +38,30 @@ class EthereumClient { // }); } - Future getBalance(EthereumAddress address) async => - await _client!.getBalance(address); + Future getBalance(EthereumAddress address) async { + try { + return await _client!.getBalance(address); + } catch (_) { + return EtherAmount.zero(); + } + } Future getGasUnitPrice() async { - final gasPrice = await _client!.getGasPrice(); - return gasPrice.getInWei.toInt(); + try { + final gasPrice = await _client!.getGasPrice(); + return gasPrice.getInWei.toInt(); + } catch (_) { + return 0; + } } Future getEstimatedGas() async { - final estimatedGas = await _client!.estimateGas(); - return estimatedGas.toInt(); + try { + final estimatedGas = await _client!.estimateGas(); + return estimatedGas.toInt(); + } catch (_) { + return 0; + } } Future signTransaction({ @@ -65,13 +78,11 @@ class EthereumClient { bool _isEthereum = currency == CryptoCurrency.eth; - final price = await _client!.getGasPrice(); + final price = _client!.getGasPrice(); final Transaction transaction = Transaction( from: privateKey.address, to: EthereumAddress.fromHex(toAddress), - maxGas: gas, - gasPrice: price, maxPriorityFeePerGas: EtherAmount.fromInt(EtherUnit.gwei, priority.tip), value: _isEthereum ? EtherAmount.inWei(BigInt.parse(amount)) : EtherAmount.zero(), ); @@ -93,6 +104,7 @@ class EthereumClient { EthereumAddress.fromHex(toAddress), BigInt.parse(amount), credentials: privateKey, + transaction: transaction, ); }; } @@ -100,14 +112,14 @@ class EthereumClient { return PendingEthereumTransaction( signedTransaction: signedTransaction, amount: amount, - fee: BigInt.from(gas) * price.getInWei, + fee: BigInt.from(gas) * (await price).getInWei, sendTransaction: _sendTransaction, exponent: exponent, ); } Future sendTransaction(Uint8List signedTransaction) async => - await _client!.sendRawTransaction(signedTransaction); + await _client!.sendRawTransaction(prependTransactionType(0x02, signedTransaction)); Future getTransactionDetails(String transactionHash) async { // Wait for the transaction receipt to become available @@ -209,6 +221,10 @@ I/flutter ( 4474): Gas Used: 53000 } } + Web3Client? getWeb3Client() { + return _client; + } + // Future _getDecimalPlacesForContract(DeployedContract contract) async { // final String abi = await rootBundle.loadString("assets/abi_json/erc20_abi.json"); // final contractAbi = ContractAbi.fromJson(abi, "ERC20"); diff --git a/cw_ethereum/lib/ethereum_transaction_info.dart b/cw_ethereum/lib/ethereum_transaction_info.dart index efdc61407..a0649ba25 100644 --- a/cw_ethereum/lib/ethereum_transaction_info.dart +++ b/cw_ethereum/lib/ethereum_transaction_info.dart @@ -14,6 +14,7 @@ class EthereumTransactionInfo extends TransactionInfo { required this.isPending, required this.date, required this.confirmations, + required this.to, }) : this.amount = ethAmount.toInt(), this.fee = ethFee.toInt(); @@ -30,6 +31,7 @@ class EthereumTransactionInfo extends TransactionInfo { final int confirmations; final String tokenSymbol; String? _fiatAmount; + final String? to; @override String amountFormatted() => @@ -56,6 +58,7 @@ class EthereumTransactionInfo extends TransactionInfo { isPending: data['isPending'] as bool, confirmations: data['confirmations'] as int, tokenSymbol: data['tokenSymbol'] as String, + to: data['to'], ); } @@ -70,5 +73,6 @@ class EthereumTransactionInfo extends TransactionInfo { 'isPending': isPending, 'confirmations': confirmations, 'tokenSymbol': tokenSymbol, + 'to': to, }; } diff --git a/cw_ethereum/lib/ethereum_wallet.dart b/cw_ethereum/lib/ethereum_wallet.dart index 8d7c477e1..21bde1233 100644 --- a/cw_ethereum/lib/ethereum_wallet.dart +++ b/cw_ethereum/lib/ethereum_wallet.dart @@ -31,6 +31,7 @@ import 'package:hive/hive.dart'; import 'package:hex/hex.dart'; import 'package:mobx/mobx.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:web3dart/crypto.dart'; import 'package:web3dart/web3dart.dart'; import 'package:bip39/bip39.dart' as bip39; import 'package:bip32/bip32.dart' as bip32; @@ -76,6 +77,8 @@ abstract class EthereumWalletBase late final EthPrivateKey _ethPrivateKey; + EthPrivateKey get ethPrivateKey => _ethPrivateKey; + late EthereumClient _client; int? _gasPrice; @@ -283,6 +286,7 @@ abstract class EthereumWalletBase ethFee: BigInt.from(transactionModel.gasUsed) * transactionModel.gasPrice, exponent: transactionModel.tokenDecimal ?? 18, tokenSymbol: transactionModel.tokenSymbol ?? "ETH", + to: transactionModel.to, ); } @@ -502,4 +506,10 @@ abstract class EthereumWalletBase _transactionsUpdateTimer?.cancel(); } } + + @override + String signMessage(String message, {String? address = null}) => + bytesToHex(_ethPrivateKey.signPersonalMessageToUint8List(ascii.encode(message))); + + Web3Client? getWeb3Client() => _client.getWeb3Client(); } diff --git a/cw_ethereum/lib/ethereum_wallet_service.dart b/cw_ethereum/lib/ethereum_wallet_service.dart index 16dbc0b04..8810d6014 100644 --- a/cw_ethereum/lib/ethereum_wallet_service.dart +++ b/cw_ethereum/lib/ethereum_wallet_service.dart @@ -20,7 +20,14 @@ class EthereumWalletService extends WalletService create(EthereumNewWalletCredentials credentials) async { - final mnemonic = bip39.generateMnemonic(); + + final strength = (credentials.seedPhraseLength == 12) + ? 128 + : (credentials.seedPhraseLength == 24) + ? 256 + : 128; + + final mnemonic = bip39.generateMnemonic(strength: strength); final wallet = EthereumWallet( walletInfo: credentials.walletInfo!, mnemonic: mnemonic, diff --git a/cw_haven/android/build.gradle b/cw_haven/android/build.gradle index 91da1b857..fb941f657 100644 --- a/cw_haven/android/build.gradle +++ b/cw_haven/android/build.gradle @@ -2,14 +2,14 @@ group 'com.cakewallet.cw_haven' version '1.0-SNAPSHOT' buildscript { - ext.kotlin_version = '1.3.50' + ext.kotlin_version = '1.7.10' repositories { google() jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:4.1.0' + classpath 'com.android.tools.build:gradle:7.3.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } diff --git a/cw_haven/lib/haven_wallet_addresses.dart b/cw_haven/lib/haven_wallet_addresses.dart index ff6b6aa3f..eeeb763cf 100644 --- a/cw_haven/lib/haven_wallet_addresses.dart +++ b/cw_haven/lib/haven_wallet_addresses.dart @@ -63,14 +63,14 @@ abstract class HavenWalletAddressesBase extends WalletAddressesWithAccount + addressInfos[account?.id ?? 0]?.any((it) => it.address == address) ?? false; +} diff --git a/cw_haven/pubspec.lock b/cw_haven/pubspec.lock index a63aa3237..525e8e5fa 100644 --- a/cw_haven/pubspec.lock +++ b/cw_haven/pubspec.lock @@ -414,50 +414,50 @@ packages: dependency: "direct main" description: name: path_provider - sha256: dcea5feb97d8abf90cab9e9030b497fb7c3cbf26b7a1fe9e3ef7dcb0a1ddec95 + sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa url: "https://pub.dev" source: hosted - version: "2.0.12" + version: "2.1.1" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: a776c088d671b27f6e3aa8881d64b87b3e80201c64e8869b811325de7a76c15e + sha256: e595b98692943b4881b219f0a9e3945118d3c16bd7e2813f98ec6e532d905f72 url: "https://pub.dev" source: hosted - version: "2.0.22" + version: "2.2.1" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "62a68e7e1c6c459f9289859e2fae58290c981ce21d1697faf54910fe1faa4c74" + sha256: "19314d595120f82aca0ba62787d58dde2cc6b5df7d2f0daf72489e38d1b57f2d" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.3.1" path_provider_linux: dependency: transitive description: name: path_provider_linux - sha256: ab0987bf95bc591da42dffb38c77398fc43309f0b9b894dcc5d6f40c4b26c379 + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 url: "https://pub.dev" source: hosted - version: "2.1.7" + version: "2.2.1" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - sha256: f0abc8ebd7253741f05488b4813d936b4d07c6bae3e86148a09e342ee4b08e76 + sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c" url: "https://pub.dev" source: hosted - version: "2.0.5" + version: "2.1.1" path_provider_windows: dependency: transitive description: name: path_provider_windows - sha256: f53720498d5a543f9607db4b0e997c4b5438884de25b0f73098cc2671a51b130 + sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" url: "https://pub.dev" source: hosted - version: "2.1.5" + version: "2.2.1" platform: dependency: transitive description: @@ -535,6 +535,14 @@ packages: description: flutter source: sdk version: "0.0.99" + socks5_proxy: + dependency: transitive + description: + name: socks5_proxy + sha256: "1d21b5606169654bbf4cfb904e8e6ed897e9f763358709f87310c757096d909a" + url: "https://pub.dev" + source: hosted + version: "1.0.4" source_gen: dependency: transitive description: @@ -615,6 +623,15 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" + tor: + dependency: transitive + description: + path: "." + ref: main + resolved-ref: "09ba92cb11d4e3cacf97256e57863b805f79f2e5" + url: "https://github.com/cake-tech/tor.git" + source: git + version: "0.0.1" typed_data: dependency: transitive description: @@ -673,4 +690,4 @@ packages: version: "3.1.1" sdks: dart: ">=3.0.0 <4.0.0" - flutter: ">=3.0.0" + flutter: ">=3.7.0" diff --git a/cw_monero/android/build.gradle b/cw_monero/android/build.gradle index 1d7ae93d8..fc4835e81 100644 --- a/cw_monero/android/build.gradle +++ b/cw_monero/android/build.gradle @@ -2,14 +2,14 @@ group 'com.cakewallet.monero' version '1.0-SNAPSHOT' buildscript { - ext.kotlin_version = '1.3.50' + ext.kotlin_version = '1.7.10' repositories { google() jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.5.4' + classpath 'com.android.tools.build:gradle:7.3.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } diff --git a/cw_monero/example/pubspec.lock b/cw_monero/example/pubspec.lock index 1aae6b0c9..661341e96 100644 --- a/cw_monero/example/pubspec.lock +++ b/cw_monero/example/pubspec.lock @@ -237,50 +237,50 @@ packages: dependency: transitive description: name: path_provider - sha256: dcea5feb97d8abf90cab9e9030b497fb7c3cbf26b7a1fe9e3ef7dcb0a1ddec95 + sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa url: "https://pub.dev" source: hosted - version: "2.0.12" + version: "2.1.1" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: a776c088d671b27f6e3aa8881d64b87b3e80201c64e8869b811325de7a76c15e + sha256: e595b98692943b4881b219f0a9e3945118d3c16bd7e2813f98ec6e532d905f72 url: "https://pub.dev" source: hosted - version: "2.0.22" + version: "2.2.1" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "62a68e7e1c6c459f9289859e2fae58290c981ce21d1697faf54910fe1faa4c74" + sha256: "19314d595120f82aca0ba62787d58dde2cc6b5df7d2f0daf72489e38d1b57f2d" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.3.1" path_provider_linux: dependency: transitive description: name: path_provider_linux - sha256: ab0987bf95bc591da42dffb38c77398fc43309f0b9b894dcc5d6f40c4b26c379 + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 url: "https://pub.dev" source: hosted - version: "2.1.7" + version: "2.2.1" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - sha256: f0abc8ebd7253741f05488b4813d936b4d07c6bae3e86148a09e342ee4b08e76 + sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c" url: "https://pub.dev" source: hosted - version: "2.0.5" + version: "2.1.1" path_provider_windows: dependency: transitive description: name: path_provider_windows - sha256: f53720498d5a543f9607db4b0e997c4b5438884de25b0f73098cc2671a51b130 + sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" url: "https://pub.dev" source: hosted - version: "2.1.5" + version: "2.2.1" platform: dependency: transitive description: @@ -318,6 +318,14 @@ packages: description: flutter source: sdk version: "0.0.99" + socks5_proxy: + dependency: transitive + description: + name: socks5_proxy + sha256: "1d21b5606169654bbf4cfb904e8e6ed897e9f763358709f87310c757096d909a" + url: "https://pub.dev" + source: hosted + version: "1.0.4" source_span: dependency: transitive description: @@ -366,6 +374,15 @@ packages: url: "https://pub.dev" source: hosted version: "0.5.1" + tor: + dependency: transitive + description: + path: "." + ref: main + resolved-ref: "09ba92cb11d4e3cacf97256e57863b805f79f2e5" + url: "https://github.com/cake-tech/tor.git" + source: git + version: "0.0.1" typed_data: dependency: transitive description: @@ -400,4 +417,4 @@ packages: version: "0.2.0+3" sdks: dart: ">=3.0.0 <4.0.0" - flutter: ">=3.0.0" + flutter: ">=3.7.0" diff --git a/cw_monero/ios/Classes/monero_api.cpp b/cw_monero/ios/Classes/monero_api.cpp index e04282fe8..a0712255a 100644 --- a/cw_monero/ios/Classes/monero_api.cpp +++ b/cw_monero/ios/Classes/monero_api.cpp @@ -841,6 +841,12 @@ extern "C" return m_transaction_history->count(); } + TransactionInfoRow* get_transaction(char * txId) + { + Monero::TransactionInfo *row = m_transaction_history->transaction(std::string(txId)); + return new TransactionInfoRow(row); + } + int LedgerExchange( unsigned char *command, unsigned int cmd_len, @@ -970,6 +976,15 @@ extern "C" return result; } + void freeze_coin(int index) + { + m_coins->setFrozen(index); + } + + void thaw_coin(int index) + { + m_coins->thaw(index); + } #ifdef __cplusplus } diff --git a/cw_monero/lib/api/coins_info.dart b/cw_monero/lib/api/coins_info.dart index 9a5303f9d..d7350a6e2 100644 --- a/cw_monero/lib/api/coins_info.dart +++ b/cw_monero/lib/api/coins_info.dart @@ -16,8 +16,20 @@ final coinNative = moneroApi .lookup>('coin') .asFunction(); +final freezeCoinNative = moneroApi + .lookup>('freeze_coin') + .asFunction(); + +final thawCoinNative = moneroApi + .lookup>('thaw_coin') + .asFunction(); + void refreshCoins(int accountIndex) => refreshCoinsNative(accountIndex); int countOfCoins() => coinsCountNative(); CoinsInfoRow getCoin(int index) => coinNative(index).ref; + +void freezeCoin(int index) => freezeCoinNative(index); + +void thawCoin(int index) => thawCoinNative(index); diff --git a/cw_monero/lib/api/signatures.dart b/cw_monero/lib/api/signatures.dart index e208414c8..9be828df0 100644 --- a/cw_monero/lib/api/signatures.dart +++ b/cw_monero/lib/api/signatures.dart @@ -1,6 +1,7 @@ import 'dart:ffi'; import 'package:cw_monero/api/structs/coins_info_row.dart'; import 'package:cw_monero/api/structs/pending_transaction.dart'; +import 'package:cw_monero/api/structs/transaction_info_row.dart'; import 'package:cw_monero/api/structs/ut8_box.dart'; import 'package:ffi/ffi.dart'; @@ -81,6 +82,8 @@ typedef account_set_label = Void Function(Int32 accountIndex, Pointer labe typedef transactions_refresh = Void Function(); +typedef get_transaction = Pointer Function(Pointer txId); + typedef get_tx_key = Pointer? Function(Pointer txId); typedef transactions_count = Int64 Function(); @@ -139,3 +142,7 @@ typedef coins_count = Int64 Function(); // typedef coins_from_txid = Pointer Function(Pointer txid); typedef coin = Pointer Function(Int32 index); + +typedef freeze_coin = Void Function(Int32 index); + +typedef thaw_coin = Void Function(Int32 index); diff --git a/cw_monero/lib/api/transaction_history.dart b/cw_monero/lib/api/transaction_history.dart index 1964c4067..73c8de801 100644 --- a/cw_monero/lib/api/transaction_history.dart +++ b/cw_monero/lib/api/transaction_history.dart @@ -1,15 +1,16 @@ import 'dart:ffi'; + import 'package:cw_monero/api/convert_utf8_to_string.dart'; +import 'package:cw_monero/api/exceptions/creation_transaction_exception.dart'; +import 'package:cw_monero/api/monero_api.dart'; import 'package:cw_monero/api/monero_output.dart'; +import 'package:cw_monero/api/signatures.dart'; +import 'package:cw_monero/api/structs/pending_transaction.dart'; +import 'package:cw_monero/api/structs/transaction_info_row.dart'; import 'package:cw_monero/api/structs/ut8_box.dart'; +import 'package:cw_monero/api/types.dart'; import 'package:ffi/ffi.dart'; import 'package:flutter/foundation.dart'; -import 'package:cw_monero/api/signatures.dart'; -import 'package:cw_monero/api/types.dart'; -import 'package:cw_monero/api/monero_api.dart'; -import 'package:cw_monero/api/structs/transaction_info_row.dart'; -import 'package:cw_monero/api/structs/pending_transaction.dart'; -import 'package:cw_monero/api/exceptions/creation_transaction_exception.dart'; final transactionsRefreshNative = moneroApi .lookup>('transactions_refresh') @@ -38,6 +39,10 @@ final transactionCommitNative = moneroApi final getTxKeyNative = moneroApi.lookup>('get_tx_key').asFunction(); +final getTransactionNative = moneroApi + .lookup>('get_transaction') + .asFunction(); + String getTxKey(String txId) { final txIdPointer = txId.toNativeUtf8(); final keyPointer = getTxKeyNative(txIdPointer); @@ -65,6 +70,11 @@ List getAllTransactions() { .toList(); } +TransactionInfoRow getTransaction(String txId) { + final txIdPointer = txId.toNativeUtf8(); + return getTransactionNative(txIdPointer).ref; +} + PendingTransactionDescription createTransactionSync( {required String address, required String paymentId, diff --git a/cw_monero/lib/api/types.dart b/cw_monero/lib/api/types.dart index 2c92f2d80..4c0c980dc 100644 --- a/cw_monero/lib/api/types.dart +++ b/cw_monero/lib/api/types.dart @@ -1,6 +1,7 @@ import 'dart:ffi'; import 'package:cw_monero/api/structs/coins_info_row.dart'; import 'package:cw_monero/api/structs/pending_transaction.dart'; +import 'package:cw_monero/api/structs/transaction_info_row.dart'; import 'package:cw_monero/api/structs/ut8_box.dart'; import 'package:ffi/ffi.dart'; @@ -81,6 +82,8 @@ typedef AccountSetLabel = void Function(int accountIndex, Pointer label); typedef TransactionsRefresh = void Function(); +typedef GetTransaction = Pointer Function(Pointer txId); + typedef GetTxKey = Pointer? Function(Pointer txId); typedef TransactionsCount = int Function(); @@ -139,3 +142,7 @@ typedef RefreshCoins = void Function(int); typedef CoinsCount = int Function(); typedef GetCoin = Pointer Function(int); + +typedef FreezeCoin = void Function(int); + +typedef ThawCoin = void Function(int); diff --git a/cw_monero/lib/exceptions/monero_transaction_no_inputs_exception.dart b/cw_monero/lib/exceptions/monero_transaction_no_inputs_exception.dart index 5d808be8f..453482e0a 100644 --- a/cw_monero/lib/exceptions/monero_transaction_no_inputs_exception.dart +++ b/cw_monero/lib/exceptions/monero_transaction_no_inputs_exception.dart @@ -1,4 +1,8 @@ class MoneroTransactionNoInputsException implements Exception { + MoneroTransactionNoInputsException(this.inputsSize); + + int inputsSize; + @override - String toString() => 'Not enough inputs available. Please select more under Coin Control'; + String toString() => 'Not enough inputs ($inputsSize) selected. Please select more under Coin Control'; } diff --git a/cw_monero/lib/monero_unspent.dart b/cw_monero/lib/monero_unspent.dart index c2ff9f9db..65b5c595d 100644 --- a/cw_monero/lib/monero_unspent.dart +++ b/cw_monero/lib/monero_unspent.dart @@ -1,28 +1,20 @@ +import 'package:cw_core/unspent_transaction_output.dart'; import 'package:cw_monero/api/structs/coins_info_row.dart'; -class MoneroUnspent { - MoneroUnspent(this.address, this.hash, this.keyImage, this.value, this.isFrozen, this.isUnlocked) - : isSending = true, - note = ''; +class MoneroUnspent extends Unspent { + MoneroUnspent( + String address, String hash, String keyImage, int value, bool isFrozen, this.isUnlocked) + : super(address, hash, value, 0, keyImage) { + this.isFrozen = isFrozen; + } - MoneroUnspent.fromCoinsInfoRow(CoinsInfoRow coinsInfoRow) - : address = coinsInfoRow.getAddress(), - hash = coinsInfoRow.getHash(), - keyImage = coinsInfoRow.getKeyImage(), - value = coinsInfoRow.amount, - isFrozen = coinsInfoRow.frozen == 1, - isUnlocked = coinsInfoRow.unlocked == 1, - isSending = true, - note = ''; - - final String address; - final String hash; - final String keyImage; - final int value; + factory MoneroUnspent.fromCoinsInfoRow(CoinsInfoRow coinsInfoRow) => MoneroUnspent( + coinsInfoRow.getAddress(), + coinsInfoRow.getHash(), + coinsInfoRow.getKeyImage(), + coinsInfoRow.amount, + coinsInfoRow.frozen == 1, + coinsInfoRow.unlocked == 1); final bool isUnlocked; - - bool isFrozen; - bool isSending; - String note; } diff --git a/cw_monero/lib/monero_wallet.dart b/cw_monero/lib/monero_wallet.dart index 69cd5458e..d7e66ecec 100644 --- a/cw_monero/lib/monero_wallet.dart +++ b/cw_monero/lib/monero_wallet.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:io'; + import 'package:cw_core/account.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/monero_amount_format.dart'; @@ -22,14 +23,14 @@ import 'package:cw_monero/api/transaction_history.dart' as transaction_history; import 'package:cw_monero/api/wallet.dart' as monero_wallet; import 'package:cw_monero/exceptions/monero_transaction_creation_exception.dart'; import 'package:cw_monero/exceptions/monero_transaction_no_inputs_exception.dart'; -import 'package:cw_monero/pending_monero_transaction.dart'; import 'package:cw_monero/monero_transaction_creation_credentials.dart'; import 'package:cw_monero/monero_transaction_history.dart'; import 'package:cw_monero/monero_transaction_info.dart'; import 'package:cw_monero/monero_unspent.dart'; import 'package:cw_monero/monero_wallet_addresses.dart'; -import 'package:mobx/mobx.dart'; +import 'package:cw_monero/pending_monero_transaction.dart'; import 'package:hive/hive.dart'; +import 'package:mobx/mobx.dart'; part 'monero_wallet.g.dart'; @@ -37,10 +38,10 @@ const moneroBlockSize = 1000; class MoneroWallet = MoneroWalletBase with _$MoneroWallet; -abstract class MoneroWalletBase extends WalletBase with Store { - MoneroWalletBase({required WalletInfo walletInfo, - required Box unspentCoinsInfo}) +abstract class MoneroWalletBase + extends WalletBase with Store { + MoneroWalletBase( + {required WalletInfo walletInfo, required Box unspentCoinsInfo}) : balance = ObservableMap.of({ CryptoCurrency.xmr: MoneroBalance( fullBalance: monero_wallet.getFullBalance(accountIndex: 0), @@ -57,9 +58,7 @@ abstract class MoneroWalletBase extends WalletBase walletAddresses.account, (Account? account) { - if (account == null) { - return; - } + if (account == null) return; balance = ObservableMap.of({ currency: MoneroBalance( @@ -67,6 +66,7 @@ abstract class MoneroWalletBase extends WalletBase isEnabledAutoGenerateSubaddress, (bool enabled) { @@ -112,12 +112,12 @@ abstract class MoneroWalletBase extends WalletBase init() async { await walletAddresses.init(); - balance = ObservableMap.of( - { - currency: MoneroBalance( - fullBalance: monero_wallet.getFullBalance(accountIndex: walletAddresses.account!.id), - unlockedBalance: monero_wallet.getUnlockedBalance(accountIndex: walletAddresses.account!.id)) - }); + balance = ObservableMap.of({ + currency: MoneroBalance( + fullBalance: monero_wallet.getFullBalance(accountIndex: walletAddresses.account!.id), + unlockedBalance: + monero_wallet.getUnlockedBalance(accountIndex: walletAddresses.account!.id)) + }); _setListeners(); await updateTransactions(); @@ -125,15 +125,14 @@ abstract class MoneroWalletBase extends WalletBase await save()); + _autoSaveTimer = + Timer.periodic(Duration(seconds: _autoSaveInterval), (_) async => await save()); } + @override Future? updateBalance() => null; @@ -153,7 +152,8 @@ abstract class MoneroWalletBase extends WalletBase 1; final unlockedBalance = - monero_wallet.getUnlockedBalance(accountIndex: walletAddresses.account!.id); + monero_wallet.getUnlockedBalance(accountIndex: walletAddresses.account!.id); var allInputsAmount = 0; PendingTransactionDescription pendingTransactionDescription; @@ -205,54 +205,45 @@ abstract class MoneroWalletBase extends WalletBase item.sendAll - || (item.formattedCryptoAmount ?? 0) <= 0)) { + if (outputs.any((item) => item.sendAll || (item.formattedCryptoAmount ?? 0) <= 0)) { throw MoneroTransactionCreationException('You do not have enough XMR to send this amount.'); } - final int totalAmount = outputs.fold(0, (acc, value) => - acc + (value.formattedCryptoAmount ?? 0)); + final int totalAmount = + outputs.fold(0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0)); + final estimatedFee = calculateEstimatedFee(_credentials.priority, totalAmount); if (unlockedBalance < totalAmount) { throw MoneroTransactionCreationException('You do not have enough XMR to send this amount.'); } - final moneroOutputs = outputs.map((output) { - final outputAddress = output.isParsedAddress - ? output.extractedAddress - : output.address; + if (!spendAllCoins && (allInputsAmount < totalAmount + estimatedFee)) { + throw MoneroTransactionNoInputsException(inputs.length); + } - return MoneroOutput( - address: outputAddress!, - amount: output.cryptoAmount!.replaceAll(',', '.')); + final moneroOutputs = outputs.map((output) { + final outputAddress = output.isParsedAddress ? output.extractedAddress : output.address; + + return MoneroOutput( + address: outputAddress!, amount: output.cryptoAmount!.replaceAll(',', '.')); }).toList(); - pendingTransactionDescription = - await transaction_history.createTransactionMultDest( + pendingTransactionDescription = await transaction_history.createTransactionMultDest( outputs: moneroOutputs, priorityRaw: _credentials.priority.serialize(), accountIndex: walletAddresses.account!.id, preferredInputs: inputs); } else { final output = outputs.first; - final address = output.isParsedAddress - ? output.extractedAddress - : output.address; - final amount = output.sendAll - ? null - : output.cryptoAmount!.replaceAll(',', '.'); - final formattedAmount = output.sendAll - ? null - : output.formattedCryptoAmount; + final address = output.isParsedAddress ? output.extractedAddress : output.address; + final amount = output.sendAll ? null : output.cryptoAmount!.replaceAll(',', '.'); + final formattedAmount = output.sendAll ? null : output.formattedCryptoAmount; if ((formattedAmount != null && unlockedBalance < formattedAmount) || (formattedAmount == null && unlockedBalance <= 0)) { @@ -262,6 +253,13 @@ abstract class MoneroWalletBase extends WalletBase changePassword(String password) async { - monero_wallet.setPasswordSync(password); - } + Future changePassword(String password) async => monero_wallet.setPasswordSync(password); Future getNodeHeight() async => monero_wallet.getNodeHeight(); @@ -404,7 +396,9 @@ abstract class MoneroWalletBase extends WalletBase - element.walletId.contains(id) && element.hash.contains(coin.hash)); + element.walletId.contains(id) && + element.accountIndex == walletAddresses.account!.id && + element.keyImage!.contains(coin.keyImage!)); if (coinInfoList.isNotEmpty) { final coinInfo = coinInfoList.first; @@ -436,16 +432,17 @@ abstract class MoneroWalletBase extends WalletBase _addCoinInfo(MoneroUnspent coin) async { final newInfo = UnspentCoinsInfo( - walletId: id, - hash: coin.hash, - isFrozen: coin.isFrozen, - isSending: coin.isSending, - noteRaw: coin.note, - address: coin.address, - value: coin.value, - vout: 0, - keyImage: coin.keyImage - ); + walletId: id, + hash: coin.hash, + isFrozen: coin.isFrozen, + isSending: coin.isSending, + noteRaw: coin.note, + address: coin.address, + value: coin.value, + vout: 0, + keyImage: coin.keyImage, + isChange: coin.isChange, + accountIndex: walletAddresses.account!.id); await unspentCoinsInfo.add(newInfo); } @@ -453,12 +450,13 @@ abstract class MoneroWalletBase extends WalletBase _refreshUnspentCoinsInfo() async { try { final List keys = []; - final currentWalletUnspentCoins = unspentCoinsInfo.values - .where((element) => element.walletId.contains(id)); + final currentWalletUnspentCoins = unspentCoinsInfo.values.where((element) => + element.walletId.contains(id) && element.accountIndex == walletAddresses.account!.id); if (currentWalletUnspentCoins.isNotEmpty) { currentWalletUnspentCoins.forEach((element) { - final existUnspentCoins = unspentCoins.where((coin) => element.hash.contains(coin.hash)); + final existUnspentCoins = + unspentCoins.where((coin) => element.keyImage!.contains(coin.keyImage!)); if (existUnspentCoins.isEmpty) { keys.add(element.key); @@ -475,16 +473,14 @@ abstract class MoneroWalletBase extends WalletBase - monero_wallet.getAddress( - accountIndex: accountIndex, - addressIndex: addressIndex); + monero_wallet.getAddress(accountIndex: accountIndex, addressIndex: addressIndex); @override Future> fetchTransactions() async { transaction_history.refreshTransactions(); - return _getAllTransactionsOfAccount(walletAddresses.account?.id).fold>( - {}, - (Map acc, MoneroTransactionInfo tx) { + return _getAllTransactionsOfAccount(walletAddresses.account?.id) + .fold>({}, + (Map acc, MoneroTransactionInfo tx) { acc[tx.id] = tx; return acc; }); @@ -508,16 +504,14 @@ abstract class MoneroWalletBase extends WalletBase + monero_wallet.getSubaddressLabel(accountIndex, addressIndex); - List _getAllTransactionsOfAccount(int? accountIndex) => - transaction_history - .getAllTransactions() - .map((row) => MoneroTransactionInfo.fromRow(row)) - .where((element) => element.accountIndex == (accountIndex ?? 0)) - .toList(); + List _getAllTransactionsOfAccount(int? accountIndex) => transaction_history + .getAllTransactions() + .map((row) => MoneroTransactionInfo.fromRow(row)) + .where((element) => element.accountIndex == (accountIndex ?? 0)) + .toList(); void _setListeners() { _listener?.stop(); @@ -539,8 +533,7 @@ abstract class MoneroWalletBase extends WalletBase _askForUpdateTransactionHistory() async => - await updateTransactions(); + Future _askForUpdateTransactionHistory() async => await updateTransactions(); - int _getFullBalance() => - monero_wallet.getFullBalance(accountIndex: walletAddresses.account!.id); + int _getFullBalance() => monero_wallet.getFullBalance(accountIndex: walletAddresses.account!.id); int _getUnlockedBalance() => monero_wallet.getUnlockedBalance(accountIndex: walletAddresses.account!.id); @@ -583,9 +574,9 @@ abstract class MoneroWalletBase extends WalletBase + element.walletId == id && element.accountIndex == walletAddresses.account!.id)) { + if (coin.isFrozen) frozenBalance += coin.value; } return frozenBalance; @@ -606,9 +597,9 @@ abstract class MoneroWalletBase extends WalletBase + addressInfos[account?.id ?? 0]?.any((it) => it.address == address) ?? false; } diff --git a/cw_monero/lib/monero_wallet_service.dart b/cw_monero/lib/monero_wallet_service.dart index 9ee4cf374..218cd2496 100644 --- a/cw_monero/lib/monero_wallet_service.dart +++ b/cw_monero/lib/monero_wallet_service.dart @@ -135,9 +135,18 @@ class MoneroWalletService extends WalletService< (e is WalletOpeningException && e.message.contains('basic_string')); final bool isMissingCacheFilesAndroid = e.toString().contains('input_stream') || - (e is WalletOpeningException && e.message.contains('input_stream')); + e.toString().contains('input stream error') || + (e is WalletOpeningException && + (e.message.contains('input_stream') || e.message.contains('input stream error'))); - if (isBadAlloc || doesNotCorrespond || isMissingCacheFilesIOS || isMissingCacheFilesAndroid) { + final bool invalidSignature = e.toString().contains('invalid signature') || + (e is WalletOpeningException && e.message.contains('invalid signature')); + + if (isBadAlloc || + doesNotCorrespond || + isMissingCacheFilesIOS || + isMissingCacheFilesAndroid || + invalidSignature) { await restoreOrResetWalletFiles(name); return openWallet(name, password); } diff --git a/cw_monero/pubspec.lock b/cw_monero/pubspec.lock index 37e08e7ca..dc0f9aeed 100644 --- a/cw_monero/pubspec.lock +++ b/cw_monero/pubspec.lock @@ -414,50 +414,50 @@ packages: dependency: "direct main" description: name: path_provider - sha256: dcea5feb97d8abf90cab9e9030b497fb7c3cbf26b7a1fe9e3ef7dcb0a1ddec95 + sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa url: "https://pub.dev" source: hosted - version: "2.0.12" + version: "2.1.1" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: a776c088d671b27f6e3aa8881d64b87b3e80201c64e8869b811325de7a76c15e + sha256: e595b98692943b4881b219f0a9e3945118d3c16bd7e2813f98ec6e532d905f72 url: "https://pub.dev" source: hosted - version: "2.0.22" + version: "2.2.1" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "62a68e7e1c6c459f9289859e2fae58290c981ce21d1697faf54910fe1faa4c74" + sha256: "19314d595120f82aca0ba62787d58dde2cc6b5df7d2f0daf72489e38d1b57f2d" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.3.1" path_provider_linux: dependency: transitive description: name: path_provider_linux - sha256: ab0987bf95bc591da42dffb38c77398fc43309f0b9b894dcc5d6f40c4b26c379 + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 url: "https://pub.dev" source: hosted - version: "2.1.7" + version: "2.2.1" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - sha256: f0abc8ebd7253741f05488b4813d936b4d07c6bae3e86148a09e342ee4b08e76 + sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c" url: "https://pub.dev" source: hosted - version: "2.0.5" + version: "2.1.1" path_provider_windows: dependency: transitive description: name: path_provider_windows - sha256: f53720498d5a543f9607db4b0e997c4b5438884de25b0f73098cc2671a51b130 + sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" url: "https://pub.dev" source: hosted - version: "2.1.5" + version: "2.2.1" platform: dependency: transitive description: @@ -535,6 +535,14 @@ packages: description: flutter source: sdk version: "0.0.99" + socks5_proxy: + dependency: transitive + description: + name: socks5_proxy + sha256: "1d21b5606169654bbf4cfb904e8e6ed897e9f763358709f87310c757096d909a" + url: "https://pub.dev" + source: hosted + version: "1.0.4" source_gen: dependency: transitive description: @@ -615,6 +623,15 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" + tor: + dependency: transitive + description: + path: "." + ref: main + resolved-ref: "09ba92cb11d4e3cacf97256e57863b805f79f2e5" + url: "https://github.com/cake-tech/tor.git" + source: git + version: "0.0.1" typed_data: dependency: transitive description: @@ -673,4 +690,4 @@ packages: version: "3.1.1" sdks: dart: ">=3.0.0 <4.0.0" - flutter: ">=3.0.0" + flutter: ">=3.7.0" diff --git a/cw_nano/lib/banano_balance.dart b/cw_nano/lib/banano_balance.dart index f0023a376..b85609b60 100644 --- a/cw_nano/lib/banano_balance.dart +++ b/cw_nano/lib/banano_balance.dart @@ -1,32 +1,13 @@ import 'package:cw_core/balance.dart'; -import 'package:cw_core/currency.dart'; import 'package:cw_nano/nano_util.dart'; -String rawToFormattedAmount(BigInt amount, Currency currency) { - return ""; -} - -BigInt stringAmountToBigInt(String amount) { - return BigInt.zero; -} - class BananoBalance extends Balance { final BigInt currentBalance; final BigInt receivableBalance; - late String formattedCurrentBalance; - late String formattedReceivableBalance; BananoBalance({required this.currentBalance, required this.receivableBalance}) : super(0, 0) { - this.formattedCurrentBalance = ""; - this.formattedReceivableBalance = ""; } - BananoBalance.fromString( - {required this.formattedCurrentBalance, required this.formattedReceivableBalance}) - : currentBalance = stringAmountToBigInt(formattedCurrentBalance), - receivableBalance = stringAmountToBigInt(formattedReceivableBalance), - super(0, 0); - @override String get formattedAvailableBalance { return NanoUtil.getRawAsUsableString(currentBalance.toString(), NanoUtil.rawPerBanano); diff --git a/cw_nano/lib/nano_balance.dart b/cw_nano/lib/nano_balance.dart index 231aaa480..dbb39d2a3 100644 --- a/cw_nano/lib/nano_balance.dart +++ b/cw_nano/lib/nano_balance.dart @@ -1,6 +1,4 @@ import 'package:cw_core/balance.dart'; -import 'package:cw_core/currency.dart'; -import 'package:cw_core/monero_amount_format.dart'; import 'package:cw_nano/nano_util.dart'; BigInt stringAmountToBigInt(String amount) { diff --git a/cw_nano/lib/nano_client.dart b/cw_nano/lib/nano_client.dart index 01f9ba9f9..f1d08204a 100644 --- a/cw_nano/lib/nano_client.dart +++ b/cw_nano/lib/nano_client.dart @@ -1,19 +1,35 @@ import 'dart:async'; import 'dart:convert'; +import 'package:cw_core/nano_account_info_response.dart'; import 'package:cw_nano/nano_balance.dart'; import 'package:cw_nano/nano_transaction_model.dart'; import 'package:cw_nano/nano_util.dart'; import 'package:http/http.dart' as http; import 'package:nanodart/nanodart.dart'; import 'package:cw_core/node.dart'; +import 'package:shared_preferences/shared_preferences.dart'; class NanoClient { - static const String DEFAULT_REPRESENTATIVE = - "nano_38713x95zyjsqzx6nm1dsom1jmm668owkeb9913ax6nfgj15az3nu8xkx579"; + static const Map CAKE_HEADERS = { + "Content-Type": "application/json", + "nano-app": "cake-wallet" + }; + NanoClient() { + SharedPreferences.getInstance().then((value) => prefs = value); + } + + late SharedPreferences prefs; Node? _node; Node? _powNode; + static const String _defaultDefaultRepresentative = + "nano_38713x95zyjsqzx6nm1dsom1jmm668owkeb9913ax6nfgj15az3nu8xkx579"; + + String getRepFromPrefs() { + // from preferences_key.dart "defaultNanoRep" key: + return prefs.getString("default_nano_representative") ?? _defaultDefaultRepresentative; + } bool connect(Node node) { try { @@ -36,7 +52,7 @@ class NanoClient { Future getBalance(String address) async { final response = await http.post( _node!.uri, - headers: {"Content-Type": "application/json"}, + headers: CAKE_HEADERS, body: jsonEncode( { "action": "account_balance", @@ -52,11 +68,11 @@ class NanoClient { return NanoBalance(currentBalance: cur, receivableBalance: rec); } - Future getAccountInfo(String address) async { + Future getAccountInfo(String address) async { try { final response = await http.post( _node!.uri, - headers: {"Content-Type": "application/json"}, + headers: CAKE_HEADERS, body: jsonEncode( { "action": "account_info", @@ -66,10 +82,10 @@ class NanoClient { ), ); final data = await jsonDecode(response.body); - return data; + return AccountInfoResponse.fromJson(data as Map); } catch (e) { print("error while getting account info"); - rethrow; + return null; } } @@ -78,66 +94,68 @@ class NanoClient { required String repAddress, required String ourAddress, }) async { + AccountInfoResponse? accountInfo = await getAccountInfo(ourAddress); + + if (accountInfo == null) { + throw Exception( + "error while getting account info, you can't change the rep of an unopened account"); + } + + // construct the change block: + Map changeBlock = { + "type": "state", + "account": ourAddress, + "previous": accountInfo.frontier, + "representative": repAddress, + "balance": accountInfo.balance, + "link": "0000000000000000000000000000000000000000000000000000000000000000", + "link_as_account": "nano_1111111111111111111111111111111111111111111111111111hifc8npp", + }; + + // sign the change block: + final String hash = NanoBlocks.computeStateHash( + NanoAccountType.NANO, + changeBlock["account"]!, + changeBlock["previous"]!, + changeBlock["representative"]!, + BigInt.parse(changeBlock["balance"]!), + changeBlock["link"]!, + ); + final String signature = NanoSignatures.signBlock(hash, privateKey); + + // get PoW for the send block: + final String work = await requestWork(accountInfo.frontier); + + changeBlock["signature"] = signature; + changeBlock["work"] = work; + try { - final accountInfo = await getAccountInfo(ourAddress); - - // construct the change block: - Map changeBlock = { - "type": "state", - "account": ourAddress, - "previous": accountInfo["frontier"] as String, - "representative": repAddress, - "balance": accountInfo["balance"] as String, - "link": "0000000000000000000000000000000000000000000000000000000000000000", - "link_as_account": "nano_1111111111111111111111111111111111111111111111111111hifc8npp", - }; - - // sign the change block: - final String hash = NanoBlocks.computeStateHash( - NanoAccountType.NANO, - changeBlock["account"]!, - changeBlock["previous"]!, - changeBlock["representative"]!, - BigInt.parse(changeBlock["balance"]!), - changeBlock["link"]!, - ); - final String signature = NanoSignatures.signBlock(hash, privateKey); - - // get PoW for the send block: - final String work = await requestWork(accountInfo["frontier"] as String); - - changeBlock["signature"] = signature; - changeBlock["work"] = work; - return await processBlock(changeBlock, "change"); } catch (e) { - throw Exception("error while changing representative"); + throw Exception("error while changing representative: $e"); } } Future requestWork(String hash) async { - return http - .post( + final response = await http.post( _powNode!.uri, - headers: {'Content-type': 'application/json'}, + headers: CAKE_HEADERS, body: json.encode( { "action": "work_generate", "hash": hash, }, ), - ) - .then((http.Response response) { - if (response.statusCode == 200) { - final Map decoded = json.decode(response.body) as Map; - if (decoded.containsKey("error")) { - throw Exception("Received error ${decoded["error"]}"); - } - return decoded["work"] as String; - } else { - throw Exception("Received error ${response.statusCode}"); + ); + if (response.statusCode == 200) { + final Map decoded = json.decode(response.body) as Map; + if (decoded.containsKey("error")) { + throw Exception("Received error ${decoded["error"]}"); } - }); + return decoded["work"] as String; + } else { + throw Exception("Received work error ${response.body}"); + } } Future send({ @@ -155,7 +173,6 @@ class NanoClient { } Future processBlock(Map block, String subtype) async { - final headers = {"Content-Type": "application/json"}; final processBody = jsonEncode({ "action": "process", "json_block": "true", @@ -165,7 +182,7 @@ class NanoClient { final processResponse = await http.post( _node!.uri, - headers: headers, + headers: CAKE_HEADERS, body: processBody, ); @@ -185,74 +202,63 @@ class NanoClient { BigInt? balanceAfterTx, String? previousHash, }) async { - try { - // our address: - final String publicAddress = NanoUtil.privateKeyToAddress(privateKey); + // our address: + final String publicAddress = NanoUtil.privateKeyToAddress(privateKey); - // first get the current account balance: - if (balanceAfterTx == null) { - final BigInt currentBalance = (await getBalance(publicAddress)).currentBalance; - final BigInt txAmount = BigInt.parse(amountRaw); - balanceAfterTx = currentBalance - txAmount; - } - - // get the account info (we need the frontier and representative): - final headers = {"Content-Type": "application/json"}; - final infoBody = jsonEncode({ - "action": "account_info", - "representative": "true", - "account": publicAddress, - }); - final infoResponse = await http.post( - _node!.uri, - headers: headers, - body: infoBody, - ); - - String frontier = jsonDecode(infoResponse.body)["frontier"].toString(); - // override if provided: - if (previousHash != null) { - frontier = previousHash; - } - final String representative = jsonDecode(infoResponse.body)["representative"].toString(); - // link = destination address: - final String link = NanoAccounts.extractPublicKey(destinationAddress); - final String linkAsAccount = destinationAddress; - - // construct the send block: - Map sendBlock = { - "type": "state", - "account": publicAddress, - "previous": frontier, - "representative": representative, - "balance": balanceAfterTx.toString(), - "link": link, - }; - - // sign the send block: - final String hash = NanoBlocks.computeStateHash( - NanoAccountType.NANO, - sendBlock["account"]!, - sendBlock["previous"]!, - sendBlock["representative"]!, - BigInt.parse(sendBlock["balance"]!), - sendBlock["link"]!, - ); - final String signature = NanoSignatures.signBlock(hash, privateKey); - - // get PoW for the send block: - final String work = await requestWork(frontier); - - sendBlock["link_as_account"] = linkAsAccount; - sendBlock["signature"] = signature; - sendBlock["work"] = work; - - // ready to post send block: - return sendBlock; - } catch (e) { - print(e); - rethrow; + // first get the current account balance: + if (balanceAfterTx == null) { + final BigInt currentBalance = (await getBalance(publicAddress)).currentBalance; + final BigInt txAmount = BigInt.parse(amountRaw); + balanceAfterTx = currentBalance - txAmount; } + + // get the account info (we need the frontier and representative): + AccountInfoResponse? infoResponse = await getAccountInfo(publicAddress); + if (infoResponse == null) { + throw Exception( + "error while getting account info! (we probably don't have an open account yet)"); + } + + String frontier = infoResponse.frontier; + // override if provided: + if (previousHash != null) { + frontier = previousHash; + } + final String representative = infoResponse.representative; + // link = destination address: + final String link = NanoAccounts.extractPublicKey(destinationAddress); + final String linkAsAccount = destinationAddress; + + // construct the send block: + Map sendBlock = { + "type": "state", + "account": publicAddress, + "previous": frontier, + "representative": representative, + "balance": balanceAfterTx.toString(), + "link": link, + }; + + // sign the send block: + final String hash = NanoBlocks.computeStateHash( + NanoAccountType.NANO, + sendBlock["account"]!, + sendBlock["previous"]!, + sendBlock["representative"]!, + BigInt.parse(sendBlock["balance"]!), + sendBlock["link"]!, + ); + final String signature = NanoSignatures.signBlock(hash, privateKey); + + // get PoW for the send block: + final String work = await requestWork(frontier); + + sendBlock["link_as_account"] = linkAsAccount; + sendBlock["signature"] = signature; + sendBlock["work"] = work; + + // ready to post send block: + return sendBlock; } Future receiveBlock({ @@ -264,54 +270,29 @@ class NanoClient { }) async { bool openBlock = false; - final headers = { - "Content-Type": "application/json", - }; - // first check if the account is open: // get the account info (we need the frontier and representative): - final infoBody = jsonEncode({ - "action": "account_info", - "representative": "true", - "account": destinationAddress, - }); - final infoResponse = await http.post( - _node!.uri, - headers: headers, - body: infoBody, - ); - final infoData = jsonDecode(infoResponse.body); + AccountInfoResponse? infoData = await getAccountInfo(destinationAddress); + String? frontier; + String? representative; - if (infoData["error"] != null) { + if (infoData == null) { // account is not open yet, we need to create an open block: openBlock = true; + // we don't have a representative set yet: + representative = await getRepFromPrefs(); + // we don't have a frontier yet: + frontier = "0000000000000000000000000000000000000000000000000000000000000000"; + } else { + frontier = infoData.frontier; + representative = infoData.representative; } // first get the account balance: - final balanceBody = jsonEncode({ - "action": "account_balance", - "account": destinationAddress, - }); - - final balanceResponse = await http.post( - _node!.uri, - headers: headers, - body: balanceBody, - ); - - final balanceData = jsonDecode(balanceResponse.body); - final BigInt currentBalance = BigInt.parse(balanceData["balance"].toString()); + final BigInt currentBalance = (await getBalance(destinationAddress)).currentBalance; final BigInt txAmount = BigInt.parse(amountRaw); final BigInt balanceAfterTx = currentBalance + txAmount; - String frontier = infoData["frontier"].toString(); - String representative = infoData["representative"].toString(); - - if (openBlock) { - // we don't have a representative set yet: - representative = DEFAULT_REPRESENTATIVE; - } - // link = send block hash: final String link = blockHash; // this "linkAsAccount" is meaningless: @@ -321,8 +302,7 @@ class NanoClient { Map receiveBlock = { "type": "state", "account": destinationAddress, - "previous": - openBlock ? "0000000000000000000000000000000000000000000000000000000000000000" : frontier, + "previous": frontier, "representative": representative, "balance": balanceAfterTx.toString(), "link": link, @@ -361,7 +341,7 @@ class NanoClient { }); final processResponse = await http.post( _node!.uri, - headers: headers, + headers: CAKE_HEADERS, body: processBody, ); @@ -377,7 +357,7 @@ class NanoClient { required String privateKey, }) async { final receivableResponse = await http.post(_node!.uri, - headers: {"Content-Type": "application/json"}, + headers: CAKE_HEADERS, body: jsonEncode({ "action": "receivable", "account": destinationAddress, @@ -422,16 +402,12 @@ class NanoClient { return blocks.keys.length; } - Future getTransactionDetails(String transactionHash) async { - throw UnimplementedError(); - } - void stop() {} Future> fetchTransactions(String address) async { try { final response = await http.post(_node!.uri, - headers: {"Content-Type": "application/json"}, + headers: CAKE_HEADERS, body: jsonEncode({ "action": "account_history", "account": address, diff --git a/cw_nano/lib/nano_transaction_history.dart b/cw_nano/lib/nano_transaction_history.dart index b201d6d13..dadd353c4 100644 --- a/cw_nano/lib/nano_transaction_history.dart +++ b/cw_nano/lib/nano_transaction_history.dart @@ -3,7 +3,6 @@ import 'dart:core'; import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_nano/file.dart'; -import 'package:flutter/material.dart'; import 'package:mobx/mobx.dart'; import 'package:cw_core/transaction_history.dart'; import 'package:cw_nano/nano_transaction_info.dart'; diff --git a/cw_nano/lib/nano_util.dart b/cw_nano/lib/nano_util.dart index 257a2e5c5..13d6f5649 100644 --- a/cw_nano/lib/nano_util.dart +++ b/cw_nano/lib/nano_util.dart @@ -3,18 +3,13 @@ import 'dart:typed_data'; import 'package:convert/convert.dart'; import "package:ed25519_hd_key/ed25519_hd_key.dart"; -import 'package:flutter/material.dart'; import 'package:libcrypto/libcrypto.dart'; import 'package:nanodart/nanodart.dart'; -import 'package:ed25519_hd_key/ed25519_hd_key.dart'; -import 'package:nanodart/nanodart.dart'; import 'package:decimal/decimal.dart'; class NanoUtil { // standard: static String seedToPrivate(String seed, int index) { - // return NanoHelpers.byteToHex(Ed25519Blake2b.derivePrivkey(NanoHelpers.hexToBytes(seed), index)!) - // .toUpperCase(); return NanoKeys.seedToPrivate(seed, index); } @@ -31,10 +26,6 @@ class NanoUtil { return NanoMnemomics.mnemonicListToSeed(mnemonic.split(' ')); } - // static String createPublicKey(String privateKey) { - // return NanoHelpers.byteToHex(Ed25519Blake2b.getPubkey(NanoHelpers.hexToBytes(privateKey))!); - // } - static String privateKeyToPublic(String privateKey) { // return NanoHelpers.byteToHex(Ed25519Blake2b.getPubkey(NanoHelpers.hexToBytes(privateKey))!); return NanoKeys.createPublicKey(privateKey); @@ -105,26 +96,9 @@ class NanoUtil { } } - - - // static String hdSeedToPrivate(String seed, int index) { - // // List seedBytes = hex.decode(seed); - // // KeyData data = await ED25519_HD_KEY.derivePath("m/44'/165'/$index'", seedBytes); - // // return hex.encode(data.key); - // Chain chain = Chain.seed(hex.encode(utf8.encode(seed))); - // ExtendedKey key = chain.forPath("m/44'/165'/$index'"); - // print(key.privateKeyHex()); - // return ""; - // } - - // static String hdSeedToAddress(String seed, int index) { - // // return NanoAccounts.createAccount(NanoAccountType.NANO, NanoKeys.createPublicKey(seedToPrivate(seed, index))); - // return ""; - // } - static bool isValidBip39Seed(String seed) { // Ensure seed is 128 characters long - if (seed == null || seed.length != 128) { + if (seed.length != 128) { return false; } // Ensure seed only contains hex characters, 0-9;A-F diff --git a/cw_nano/lib/nano_wallet.dart b/cw_nano/lib/nano_wallet.dart index 2d67c63d3..44d564fc6 100644 --- a/cw_nano/lib/nano_wallet.dart +++ b/cw_nano/lib/nano_wallet.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:cw_core/cake_hive.dart'; import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/nano_account_info_response.dart'; import 'package:cw_core/node.dart'; import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/pending_transaction.dart'; @@ -68,7 +69,7 @@ abstract class NanoWalletBase String? _representativeAddress; Timer? _receiveTimer; - late NanoClient _client; + late final NanoClient _client; bool _isTransactionUpdating; @override @@ -168,8 +169,8 @@ abstract class NanoWalletBase if (txOut.sendAll) { amt = balance[currency]?.currentBalance ?? BigInt.zero; } else { - amt = BigInt.tryParse( - NanoUtil.getAmountAsRaw(txOut.cryptoAmount ?? "0", NanoUtil.rawPerNano)) ?? + amt = BigInt.tryParse(NanoUtil.getAmountAsRaw( + txOut.cryptoAmount?.replaceAll(',', '.') ?? "0", NanoUtil.rawPerNano)) ?? BigInt.zero; } @@ -181,7 +182,9 @@ abstract class NanoWalletBase final block = await _client.constructSendBlock( amountRaw: amt.toString(), - destinationAddress: txOut.extractedAddress ?? txOut.address, + destinationAddress: credentials.outputs.first.isParsedAddress + ? credentials.outputs.first.extractedAddress! + : credentials.outputs.first.address, privateKey: _privateKey!, balanceAfterTx: runningBalance, previousHash: previousHash, @@ -237,7 +240,6 @@ abstract class NanoWalletBase _isTransactionUpdating = true; final transactions = await fetchTransactions(); - transactionHistory.clear(); transactionHistory.addMany(transactions); await transactionHistory.save(); _isTransactionUpdating = false; @@ -281,7 +283,7 @@ abstract class NanoWalletBase @override Future rescan({required int height}) async { - fetchTransactions(); + updateTransactions(); _updateBalance(); return; } @@ -376,14 +378,11 @@ abstract class NanoWalletBase Future _updateRep() async { try { - final accountInfo = await _client.getAccountInfo(_publicAddress!); - if (accountInfo["error"] != null) { - // account not found: - _representativeAddress = NanoClient.DEFAULT_REPRESENTATIVE; - } else { - _representativeAddress = accountInfo["representative"] as String; - } + AccountInfoResponse accountInfo = (await _client.getAccountInfo(_publicAddress!))!; + _representativeAddress = accountInfo.representative; } catch (e) { + // account not found: + _representativeAddress = await _client.getRepFromPrefs(); throw Exception("Failed to get representative address $e"); } } diff --git a/cw_nano/lib/nano_wallet_addresses.dart b/cw_nano/lib/nano_wallet_addresses.dart index d67e28055..cc532d2c7 100644 --- a/cw_nano/lib/nano_wallet_addresses.dart +++ b/cw_nano/lib/nano_wallet_addresses.dart @@ -3,10 +3,7 @@ import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/nano_account.dart'; import 'package:cw_nano/nano_account_list.dart'; -// import 'package:cw_core/account.dart'; -// import 'package:cw_core/subaddress.dart'; import 'package:mobx/mobx.dart'; -import 'package:hive/hive.dart'; part 'nano_wallet_addresses.g.dart'; @@ -41,5 +38,13 @@ abstract class NanoWalletAddressesBase extends WalletAddresses with Store { } @override - Future updateAddressesInBox() async {} + Future updateAddressesInBox() async { + try { + addressesMap.clear(); + addressesMap[address] = ''; + await saveAddressesInBox(); + } catch (e) { + print(e.toString()); + } + } } diff --git a/cw_nano/lib/nano_wallet_creation_credentials.dart b/cw_nano/lib/nano_wallet_creation_credentials.dart index 84531e24a..be6bafa74 100644 --- a/cw_nano/lib/nano_wallet_creation_credentials.dart +++ b/cw_nano/lib/nano_wallet_creation_credentials.dart @@ -38,4 +38,4 @@ class NanoRestoreWalletFromKeysCredentials extends WalletCredentials { final String seedKey; final DerivationType? derivationType; -} \ No newline at end of file +} diff --git a/cw_nano/lib/nano_wallet_service.dart b/cw_nano/lib/nano_wallet_service.dart index 3a81ed6fb..2f183d1cc 100644 --- a/cw_nano/lib/nano_wallet_service.dart +++ b/cw_nano/lib/nano_wallet_service.dart @@ -1,12 +1,10 @@ import 'dart:io'; -import 'package:cw_core/node.dart'; import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_service.dart'; import 'package:cw_core/wallet_type.dart'; -import 'package:cw_nano/nano_client.dart'; import 'package:cw_nano/nano_mnemonic.dart' as nm; import 'package:cw_nano/nano_util.dart'; import 'package:cw_nano/nano_wallet.dart'; @@ -34,10 +32,6 @@ class NanoWalletService extends WalletService info.id == WalletBase.idFor(currentName, getType())); - currentWalletInfo.derivationType = DerivationType.nano; // doesn't matter for the rename action - String randomWords = (List.from(nm.NanoMnemomics.WORDLIST)..shuffle()).take(24).join(' '); final currentWallet = @@ -85,111 +77,6 @@ class NanoWalletService extends WalletService getInfoFromSeedOrMnemonic( - DerivationType derivationType, { - String? seedKey, - String? mnemonic, - required Node node, - }) async { - NanoClient nanoClient = NanoClient(); - nanoClient.connect(node); - late String publicAddress; - - if (seedKey != null) { - if (derivationType == DerivationType.bip39) { - publicAddress = await NanoUtil.hdSeedToAddress(seedKey, 0); - } else if (derivationType == DerivationType.nano) { - publicAddress = await NanoUtil.seedToAddress(seedKey, 0); - } - } - - if (derivationType == DerivationType.bip39) { - if (mnemonic != null) { - seedKey = await NanoUtil.hdMnemonicListToSeed(mnemonic.split(' ')); - publicAddress = await NanoUtil.hdSeedToAddress(seedKey, 0); - } - } - - if (derivationType == DerivationType.nano) { - if (mnemonic != null) { - seedKey = await NanoUtil.mnemonicToSeed(mnemonic); - publicAddress = await NanoUtil.seedToAddress(seedKey, 0); - } - } - - var accountInfo = await nanoClient.getAccountInfo(publicAddress); - accountInfo["address"] = publicAddress; - return accountInfo; - } - - static Future> compareDerivationMethods( - {String? mnemonic, String? seedKey, required Node node}) async { - if (mnemonic?.split(' ').length == 12) { - return [DerivationType.bip39]; - } - if (seedKey?.length == 128) { - return [DerivationType.bip39]; - } else if (seedKey?.length == 64) { - return [DerivationType.nano]; - } - - late String publicAddressStandard; - late String publicAddressBip39; - - try { - NanoClient nanoClient = NanoClient(); - nanoClient.connect(node); - - if (mnemonic != null) { - seedKey = await NanoUtil.hdMnemonicListToSeed(mnemonic.split(' ')); - publicAddressBip39 = await NanoUtil.hdSeedToAddress(seedKey, 0); - - seedKey = await NanoUtil.mnemonicToSeed(mnemonic); - publicAddressStandard = await NanoUtil.seedToAddress(seedKey, 0); - } else if (seedKey != null) { - try { - publicAddressBip39 = await NanoUtil.hdSeedToAddress(seedKey, 0); - } catch (e) { - return [DerivationType.nano]; - } - try { - publicAddressStandard = await NanoUtil.seedToAddress(seedKey, 0); - } catch (e) { - return [DerivationType.bip39]; - } - } - - // check if account has a history: - var bip39Info; - var standardInfo; - - try { - bip39Info = await nanoClient.getAccountInfo(publicAddressBip39); - } catch (e) { - bip39Info = null; - } - try { - standardInfo = await nanoClient.getAccountInfo(publicAddressStandard); - } catch (e) { - standardInfo = null; - } - - // one of these is *probably* null: - if ((bip39Info == null || bip39Info["error"] != null) && - (standardInfo != null && standardInfo["error"] == null)) { - return [DerivationType.nano]; - } else if ((standardInfo == null || standardInfo["error"] != null) && - (bip39Info != null && bip39Info["error"] == null)) { - return [DerivationType.bip39]; - } - - // we don't know for sure: - return [DerivationType.nano, DerivationType.bip39]; - } catch (e) { - return [DerivationType.unknown]; - } - } - @override Future restoreFromKeys(NanoRestoreWalletFromKeysCredentials credentials) async { if (credentials.seedKey.contains(' ')) { @@ -203,9 +90,20 @@ class NanoWalletService extends WalletService=2.19.0 <3.0.0" - flutter: ">=3.3.0" + dart: ">=3.0.0 <4.0.0" + flutter: ">=3.7.0" diff --git a/cw_nano/pubspec.yaml b/cw_nano/pubspec.yaml index 054dd5df4..f06177d3c 100644 --- a/cw_nano/pubspec.yaml +++ b/cw_nano/pubspec.yaml @@ -21,6 +21,7 @@ dependencies: ed25519_hd_key: ^2.2.0 hex: ^0.2.0 http: ^1.1.0 + shared_preferences: ^2.0.15 cw_core: path: ../cw_core diff --git a/cw_shared_external/android/build.gradle b/cw_shared_external/android/build.gradle index d6cdaf658..8db51f0e6 100644 --- a/cw_shared_external/android/build.gradle +++ b/cw_shared_external/android/build.gradle @@ -2,14 +2,14 @@ group 'com.cakewallet.cw_shared_external' version '1.0-SNAPSHOT' buildscript { - ext.kotlin_version = '1.3.50' + ext.kotlin_version = '1.7.10' repositories { google() jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:4.1.0' + classpath 'com.android.tools.build:gradle:7.3.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 2f51f68f7..cc60cad08 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -8,6 +8,25 @@ PODS: - Flutter - ReachabilitySwift - CryptoSwift (1.7.1) + - cw_haven (0.0.1): + - cw_haven/Boost (= 0.0.1) + - cw_haven/Haven (= 0.0.1) + - cw_haven/OpenSSL (= 0.0.1) + - cw_haven/Sodium (= 0.0.1) + - cw_shared_external + - Flutter + - cw_haven/Boost (0.0.1): + - cw_shared_external + - Flutter + - cw_haven/Haven (0.0.1): + - cw_shared_external + - Flutter + - cw_haven/OpenSSL (0.0.1): + - cw_shared_external + - Flutter + - cw_haven/Sodium (0.0.1): + - cw_shared_external + - Flutter - cw_monero (0.0.2): - cw_monero/Boost (= 0.0.2) - cw_monero/Monero (= 0.0.2) @@ -96,6 +115,9 @@ PODS: - Flutter - flutter_secure_storage (6.0.0): - Flutter + - fluttertoast (0.0.2): + - Flutter + - Toast - in_app_review (0.2.0): - Flutter - local_auth_ios (0.0.1): @@ -124,6 +146,9 @@ PODS: - FlutterMacOS - SwiftProtobuf (1.22.0) - SwiftyGif (5.4.4) + - Toast (4.0.0) + - tor (0.0.1): + - Flutter - uni_links (0.0.1): - Flutter - UnstoppableDomainsResolution (4.0.0): @@ -140,6 +165,7 @@ DEPENDENCIES: - barcode_scan2 (from `.symlinks/plugins/barcode_scan2/ios`) - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) - CryptoSwift + - cw_haven (from `.symlinks/plugins/cw_haven/ios`) - cw_monero (from `.symlinks/plugins/cw_monero/ios`) - cw_shared_external (from `.symlinks/plugins/cw_shared_external/ios`) - device_display_brightness (from `.symlinks/plugins/device_display_brightness/ios`) @@ -151,6 +177,7 @@ DEPENDENCIES: - flutter_inappwebview (from `.symlinks/plugins/flutter_inappwebview/ios`) - flutter_mailer (from `.symlinks/plugins/flutter_mailer/ios`) - 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`) - local_auth_ios (from `.symlinks/plugins/local_auth_ios/ios`) - package_info (from `.symlinks/plugins/package_info/ios`) @@ -160,6 +187,7 @@ DEPENDENCIES: - sensitive_clipboard (from `.symlinks/plugins/sensitive_clipboard/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) + - tor (from `.symlinks/plugins/tor/ios`) - uni_links (from `.symlinks/plugins/uni_links/ios`) - UnstoppableDomainsResolution (~> 4.0.0) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) @@ -178,6 +206,7 @@ SPEC REPOS: - SDWebImage - SwiftProtobuf - SwiftyGif + - Toast - UnstoppableDomainsResolution EXTERNAL SOURCES: @@ -185,6 +214,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/barcode_scan2/ios" connectivity_plus: :path: ".symlinks/plugins/connectivity_plus/ios" + cw_haven: + :path: ".symlinks/plugins/cw_haven/ios" cw_monero: :path: ".symlinks/plugins/cw_monero/ios" cw_shared_external: @@ -207,6 +238,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/flutter_mailer/ios" flutter_secure_storage: :path: ".symlinks/plugins/flutter_secure_storage/ios" + fluttertoast: + :path: ".symlinks/plugins/fluttertoast/ios" in_app_review: :path: ".symlinks/plugins/in_app_review/ios" local_auth_ios: @@ -225,6 +258,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/share_plus/ios" shared_preferences_foundation: :path: ".symlinks/plugins/shared_preferences_foundation/darwin" + tor: + :path: ".symlinks/plugins/tor/ios" uni_links: :path: ".symlinks/plugins/uni_links/ios" url_launcher_ios: @@ -239,6 +274,7 @@ SPEC CHECKSUMS: BigInt: f668a80089607f521586bbe29513d708491ef2f7 connectivity_plus: 413a8857dd5d9f1c399a39130850d02fe0feaf7e CryptoSwift: d3d18dc357932f7e6d580689e065cf1f176007c1 + cw_haven: b3e54e1fbe7b8e6fda57a93206bc38f8e89b898a cw_monero: 4cf3b96f2da8e95e2ef7d6703dd4d2c509127b7d cw_shared_external: 2972d872b8917603478117c9957dfca611845a92 device_display_brightness: 1510e72c567a1f6ce6ffe393dcd9afd1426034f7 @@ -249,9 +285,10 @@ SPEC CHECKSUMS: DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 file_picker: ce3938a0df3cc1ef404671531facef740d03f920 Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 - flutter_inappwebview: bfd58618f49dc62f2676de690fc6dcda1d6c3721 + flutter_inappwebview: 3d32228f1304635e7c028b0d4252937730bbc6cf flutter_mailer: 2ef5a67087bc8c6c4cefd04a178bf1ae2c94cd83 flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be + fluttertoast: eb263d302cc92e04176c053d2385237e9f43fad0 in_app_review: 318597b3a06c22bb46dc454d56828c85f444f99d local_auth_ios: c6cf091ded637a88f24f86a8875d8b0f526e2605 MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb @@ -267,9 +304,11 @@ SPEC CHECKSUMS: shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126 SwiftProtobuf: 40bd808372cb8706108f22d28f8ab4a6b9bc6989 SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f + Toast: 91b396c56ee72a5790816f40d3a94dd357abc196 + tor: 662a9f5b980b5c86decb8ba611de9bcd4c8286eb uni_links: d97da20c7701486ba192624d99bffaaffcfc298a UnstoppableDomainsResolution: c3c67f4d0a5e2437cb00d4bd50c2e00d6e743841 - url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4 + url_launcher_ios: 68d46cc9766d0c41dbdc884310529557e3cd7a86 wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6 diff --git a/ios/Runner/InfoBase.plist b/ios/Runner/InfoBase.plist index 821df195e..6cea7a730 100644 --- a/ios/Runner/InfoBase.plist +++ b/ios/Runner/InfoBase.plist @@ -36,6 +36,10 @@ cakewallet + + CFBundleTypeRole + Editor + CFBundleTypeRole Editor @@ -96,6 +100,66 @@ litecoin-wallet + + CFBundleTypeRole + Editor + CFBundleURLName + ethereum + CFBundleURLSchemes + + ethereum + + + + CFBundleTypeRole + Viewer + CFBundleURLName + ethereum-wallet + CFBundleURLSchemes + + ethereum-wallet + + + + CFBundleTypeRole + Editor + CFBundleURLName + nano + CFBundleURLSchemes + + nano + + + + CFBundleTypeRole + Viewer + CFBundleURLName + nano-wallet + CFBundleURLSchemes + + nano-wallet + + + + CFBundleTypeRole + Editor + CFBundleURLName + bitcoincash + CFBundleURLSchemes + + bitcoincash + + + + CFBundleTypeRole + Viewer + CFBundleURLName + bitcoincash-wallet + CFBundleURLSchemes + + bitcoincash-wallet + + CFBundleVersion $(CURRENT_PROJECT_VERSION) diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index b92e3fd22..83abb883a 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -136,14 +136,11 @@ class CWBitcoin extends Bitcoin { @override int formatterStringDoubleToBitcoinAmount(String amount) => stringDoubleToBitcoinAmount(amount); - @override - List getUnspents(Object wallet) { - final bitcoinWallet = wallet as ElectrumWallet; - return bitcoinWallet.unspentCoins - .map((BitcoinUnspent bitcoinUnspent) => Unspent(bitcoinUnspent.address.address, - bitcoinUnspent.hash, bitcoinUnspent.value, bitcoinUnspent.vout, null)) - .toList(); - } + @override + List getUnspents(Object wallet) { + final bitcoinWallet = wallet as ElectrumWallet; + return bitcoinWallet.unspentCoins; + } WalletService createBitcoinWalletService( Box walletInfoSource, Box unspentCoinSource) { diff --git a/lib/bitcoin_cash/cw_bitcoin_cash.dart b/lib/bitcoin_cash/cw_bitcoin_cash.dart new file mode 100644 index 000000000..7dbb8614f --- /dev/null +++ b/lib/bitcoin_cash/cw_bitcoin_cash.dart @@ -0,0 +1,45 @@ +part of 'bitcoin_cash.dart'; + +class CWBitcoinCash extends BitcoinCash { + @override + String getMnemonic(int? strength) => Mnemonic.generate(); + + @override + Uint8List getSeedFromMnemonic(String seed) => Mnemonic.toSeed(seed); + + @override + String getCashAddrFormat(String address) => AddressUtils.getCashAddrFormat(address); + + @override + WalletService createBitcoinCashWalletService( + Box walletInfoSource, Box unspentCoinSource) { + return BitcoinCashWalletService(walletInfoSource, unspentCoinSource); + } + + @override + WalletCredentials createBitcoinCashNewWalletCredentials({ + required String name, + WalletInfo? walletInfo, + }) => + BitcoinCashNewWalletCredentials(name: name, walletInfo: walletInfo); + + @override + WalletCredentials createBitcoinCashRestoreWalletFromSeedCredentials( + {required String name, required String mnemonic, required String password}) => + BitcoinCashRestoreWalletFromSeedCredentials( + name: name, mnemonic: mnemonic, password: password); + + @override + TransactionPriority deserializeBitcoinCashTransactionPriority(int raw) => + BitcoinCashTransactionPriority.deserialize(raw: raw); + + @override + TransactionPriority getDefaultTransactionPriority() => BitcoinCashTransactionPriority.medium; + + @override + List getTransactionPriorities() => BitcoinCashTransactionPriority.all; + + @override + TransactionPriority getBitcoinCashTransactionPrioritySlow() => + BitcoinCashTransactionPriority.slow; +} diff --git a/lib/buy/onramper/onramper_buy_provider.dart b/lib/buy/onramper/onramper_buy_provider.dart index 685d31376..872fcebf5 100644 --- a/lib/buy/onramper/onramper_buy_provider.dart +++ b/lib/buy/onramper/onramper_buy_provider.dart @@ -1,10 +1,13 @@ import 'package:cake_wallet/.secrets.g.dart' as secrets; import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; +import 'package:cake_wallet/utils/device_info.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:flutter/material.dart'; +import 'package:url_launcher/url_launcher.dart'; class OnRamperBuyProvider { OnRamperBuyProvider({required SettingsStore settingsStore, required WalletBase wallet}) @@ -24,6 +27,8 @@ class OnRamperBuyProvider { return "LTC_LITECOIN"; case CryptoCurrency.xmr: return "XMR_MONERO"; + case CryptoCurrency.bch: + return "BCH_BITCOINCASH"; case CryptoCurrency.nano: return "XNO_NANO"; default: @@ -71,4 +76,13 @@ class OnRamperBuyProvider { 'cardColor': cardColor }); } + + Future launchProvider(BuildContext context) async { + final uri = requestUrl(context); + if (DeviceInfo.instance.isMobile) { + Navigator.of(context).pushNamed(Routes.webViewPage, arguments: [S.of(context).buy, uri]); + } else { + await launchUrl(uri); + } + } } diff --git a/lib/buy/robinhood/robinhood_buy_provider.dart b/lib/buy/robinhood/robinhood_buy_provider.dart new file mode 100644 index 000000000..ade0bf99f --- /dev/null +++ b/lib/buy/robinhood/robinhood_buy_provider.dart @@ -0,0 +1,96 @@ +import 'dart:convert'; + +import 'package:cake_wallet/.secrets.g.dart' as secrets; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; +import 'package:url_launcher/url_launcher.dart'; + +class RobinhoodBuyProvider { + RobinhoodBuyProvider({required WalletBase wallet}) : this._wallet = wallet; + + final WalletBase _wallet; + + static const _baseUrl = 'applink.robinhood.com'; + static const _cIdBaseUrl = 'exchange-helper.cakewallet.com'; + + String get _applicationId => secrets.robinhoodApplicationId; + + String get _apiSecret => secrets.robinhoodCIdApiSecret; + + bool get isAvailable => [ + WalletType.bitcoin, + WalletType.bitcoinCash, + WalletType.litecoin, + WalletType.ethereum + ].contains(_wallet.type); + + String getSignature(String message) { + switch (_wallet.type) { + case WalletType.ethereum: + return _wallet.signMessage(message); + case WalletType.litecoin: + case WalletType.bitcoin: + case WalletType.bitcoinCash: + return _wallet.signMessage(message, address: _wallet.walletAddresses.address); + default: + throw Exception("WalletType is not available for Robinhood ${_wallet.type}"); + } + } + + Future getConnectId() async { + final walletAddress = _wallet.walletAddresses.address; + final valid_until = (DateTime.now().millisecondsSinceEpoch / 1000).round() + 10; + final message = "$_apiSecret:${valid_until}"; + + final signature = getSignature(message); + + final uri = Uri.https(_cIdBaseUrl, "/api/robinhood"); + + var response = await http.post(uri, + headers: {'Content-Type': 'application/json'}, + body: json + .encode({'valid_until': valid_until, 'wallet': walletAddress, 'signature': signature})); + + if (response.statusCode == 200) { + return (jsonDecode(response.body) as Map)['connectId'] as String; + } else { + throw Exception( + 'Provider currently unavailable. Status: ${response.statusCode} ${response.body}'); + } + } + + Future requestUrl() async { + final connectId = await getConnectId(); + final networkName = _wallet.currency.fullName?.toUpperCase().replaceAll(" ", "_"); + + return Uri.https(_baseUrl, '/u/connect', { + 'applicationId': _applicationId, + 'connectId': connectId, + 'walletAddress': _wallet.walletAddresses.address, + 'userIdentifier': _wallet.walletAddresses.address, + 'supportedNetworks': networkName + }); + } + + Future launchProvider(BuildContext context) async { + try { + final uri = await requestUrl(); + await launchUrl(uri, mode: LaunchMode.externalApplication); + } catch (_) { + await showPopUp( + context: context, + builder: (BuildContext context) { + return AlertWithOneAction( + alertTitle: "Robinhood Connect", + alertContent: S.of(context).buy_provider_unavailable, + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(context).pop()); + }); + } + } +} diff --git a/lib/core/address_validator.dart b/lib/core/address_validator.dart index 02e50b1ca..fcb881943 100644 --- a/lib/core/address_validator.dart +++ b/lib/core/address_validator.dart @@ -88,7 +88,9 @@ class AddressValidator extends TextValidator { case CryptoCurrency.dai: case CryptoCurrency.dash: case CryptoCurrency.eos: + return '[0-9a-zA-Z]'; case CryptoCurrency.bch: + return '^(?!bitcoincash:)[0-9a-zA-Z]*\$|^(?!bitcoincash:)q[0-9a-zA-Z]{41}\$|^(?!bitcoincash:)q[0-9a-zA-Z]{42}\$|^bitcoincash:q[0-9a-zA-Z]{41}\$|^bitcoincash:q[0-9a-zA-Z]{42}\$'; case CryptoCurrency.bnb: return '[0-9a-zA-Z]'; case CryptoCurrency.ltc: @@ -172,7 +174,9 @@ class AddressValidator extends TextValidator { case CryptoCurrency.steth: case CryptoCurrency.shib: case CryptoCurrency.avaxc: + return [42]; case CryptoCurrency.bch: + return [42, 43, 44, 54, 55]; case CryptoCurrency.bnb: return [42]; case CryptoCurrency.ltc: @@ -267,8 +271,17 @@ class AddressValidator extends TextValidator { '|([^0-9a-zA-Z]|^)ltc[a-zA-Z0-9]{26,45}([^0-9a-zA-Z]|\$)'; case CryptoCurrency.eth: return '0x[0-9a-zA-Z]{42}'; + case CryptoCurrency.nano: + return 'nano_[0-9a-zA-Z]{60}'; + case CryptoCurrency.banano: + return '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]|\$)'; default: return null; } } -} +} \ No newline at end of file diff --git a/lib/core/auth_service.dart b/lib/core/auth_service.dart index 854640015..c072bf65e 100644 --- a/lib/core/auth_service.dart +++ b/lib/core/auth_service.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:cake_wallet/core/totp_request_details.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/auth/auth_page.dart'; @@ -38,6 +40,11 @@ class AuthService with Store { Future setPassword(String password) async { final key = generateStoreKeyFor(key: SecretStoreKey.pinCodePassword); final encodedPassword = encodedPinCode(pin: password); + // secure storage has a weird bug on macOS, where overwriting a key doesn't work, unless + // we delete what's there first: + if (Platform.isMacOS) { + await secureStorage.delete(key: key); + } await secureStorage.write(key: key, value: encodedPassword); } @@ -104,9 +111,8 @@ class AuthService with Store { } return; } -} + } - Navigator.of(context).pushNamed(Routes.auth, arguments: (bool isAuthenticatedSuccessfully, AuthPageState auth) async { if (!isAuthenticatedSuccessfully) { @@ -140,8 +146,6 @@ class AuthService with Store { } } } - - }); - + }); } } diff --git a/lib/core/backup_service.dart b/lib/core/backup_service.dart index 3f430b7e9..b13a01889 100644 --- a/lib/core/backup_service.dart +++ b/lib/core/backup_service.dart @@ -1,6 +1,8 @@ import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; +import 'package:cake_wallet/themes/theme_list.dart'; +import 'package:cake_wallet/utils/device_info.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; @@ -208,6 +210,7 @@ class BackupService { final isAppSecure = data[PreferencesKey.isAppSecureKey] as bool?; final disableBuy = data[PreferencesKey.disableBuyKey] as bool?; final disableSell = data[PreferencesKey.disableSellKey] as bool?; + final defaultBuyProvider = data[PreferencesKey.defaultBuyProvider] as int?; final currentTransactionPriorityKeyLegacy = data[PreferencesKey.currentTransactionPriorityKeyLegacy] as int?; final allowBiometricalAuthentication = @@ -244,9 +247,18 @@ class BackupService { final sortBalanceTokensBy = data[PreferencesKey.sortBalanceBy] as int?; final pinNativeTokenAtTop = data[PreferencesKey.pinNativeTokenAtTop] as bool?; final useEtherscan = data[PreferencesKey.useEtherscan] as bool?; + final defaultNanoRep = data[PreferencesKey.defaultNanoRep] as String?; + final defaultBananoRep = data[PreferencesKey.defaultBananoRep] as String?; + final lookupsTwitter = data[PreferencesKey.lookupsTwitter] as bool?; + final lookupsMastodon = data[PreferencesKey.lookupsMastodon] as bool?; + final lookupsYatService = data[PreferencesKey.lookupsYatService] as bool?; + final lookupsUnstoppableDomains = data[PreferencesKey.lookupsUnstoppableDomains] as bool?; + final lookupsOpenAlias = data[PreferencesKey.lookupsOpenAlias] as bool?; + final lookupsENS = data[PreferencesKey.lookupsENS] as bool?; final syncAll = data[PreferencesKey.syncAllKey] as bool?; final syncMode = data[PreferencesKey.syncModeKey] as int?; - final autoGenerateSubaddressStatus = data[PreferencesKey.autoGenerateSubaddressStatusKey] as int?; + final autoGenerateSubaddressStatus = + data[PreferencesKey.autoGenerateSubaddressStatusKey] as int?; await _sharedPreferences.setString(PreferencesKey.currentWalletName, currentWalletName); @@ -276,13 +288,19 @@ class BackupService { if (disableSell != null) await _sharedPreferences.setBool(PreferencesKey.disableSellKey, disableSell); + if (defaultBuyProvider != null) + await _sharedPreferences.setInt(PreferencesKey.defaultBuyProvider, defaultBuyProvider); + if (currentTransactionPriorityKeyLegacy != null) await _sharedPreferences.setInt( PreferencesKey.currentTransactionPriorityKeyLegacy, currentTransactionPriorityKeyLegacy); - if (allowBiometricalAuthentication != null) + if (DeviceInfo.instance.isDesktop) { + await _sharedPreferences.setBool(PreferencesKey.allowBiometricalAuthenticationKey, false); + } else if (allowBiometricalAuthentication != null) { await _sharedPreferences.setBool( PreferencesKey.allowBiometricalAuthenticationKey, allowBiometricalAuthentication); + } if (currentBitcoinElectrumSererId != null) await _sharedPreferences.setInt( @@ -298,14 +316,18 @@ class BackupService { if (fiatApiMode != null) await _sharedPreferences.setInt(PreferencesKey.currentFiatApiModeKey, fiatApiMode); if (autoGenerateSubaddressStatus != null) - await _sharedPreferences.setInt(PreferencesKey.autoGenerateSubaddressStatusKey, - autoGenerateSubaddressStatus); + await _sharedPreferences.setInt( + PreferencesKey.autoGenerateSubaddressStatusKey, autoGenerateSubaddressStatus); if (currentPinLength != null) await _sharedPreferences.setInt(PreferencesKey.currentPinLength, currentPinLength); - if (currentTheme != null) + if (currentTheme != null && DeviceInfo.instance.isMobile) { await _sharedPreferences.setInt(PreferencesKey.currentTheme, currentTheme); + // enforce dark theme on desktop platforms until the design is ready: + } else if (DeviceInfo.instance.isDesktop) { + await _sharedPreferences.setInt(PreferencesKey.currentTheme, ThemeList.darkTheme.raw); + } if (exchangeStatus != null) await _sharedPreferences.setInt(PreferencesKey.exchangeStatusKey, exchangeStatus); @@ -368,11 +390,34 @@ class BackupService { if (useEtherscan != null) await _sharedPreferences.setBool(PreferencesKey.useEtherscan, useEtherscan); - if (syncAll != null) - await _sharedPreferences.setBool(PreferencesKey.syncAllKey, syncAll); + if (defaultNanoRep != null) + await _sharedPreferences.setString(PreferencesKey.defaultNanoRep, defaultNanoRep); - if (syncMode != null) - await _sharedPreferences.setInt(PreferencesKey.syncModeKey, syncMode); + if (defaultBananoRep != null) + await _sharedPreferences.setString(PreferencesKey.defaultBananoRep, defaultBananoRep); + + if (syncAll != null) await _sharedPreferences.setBool(PreferencesKey.syncAllKey, syncAll); + if (lookupsTwitter != null) + await _sharedPreferences.setBool(PreferencesKey.lookupsTwitter, lookupsTwitter); + + if (lookupsMastodon != null) + await _sharedPreferences.setBool(PreferencesKey.lookupsMastodon, lookupsMastodon); + + if (lookupsYatService != null) + await _sharedPreferences.setBool(PreferencesKey.lookupsYatService, lookupsYatService); + + if (lookupsUnstoppableDomains != null) + await _sharedPreferences.setBool( + PreferencesKey.lookupsUnstoppableDomains, lookupsUnstoppableDomains); + + if (lookupsOpenAlias != null) + await _sharedPreferences.setBool(PreferencesKey.lookupsOpenAlias, lookupsOpenAlias); + + if (lookupsENS != null) await _sharedPreferences.setBool(PreferencesKey.lookupsENS, lookupsENS); + + if (syncAll != null) await _sharedPreferences.setBool(PreferencesKey.syncAllKey, syncAll); + + if (syncMode != null) await _sharedPreferences.setInt(PreferencesKey.syncModeKey, syncMode); await preferencesFile.delete(); } @@ -476,6 +521,8 @@ class BackupService { _sharedPreferences.getBool(PreferencesKey.shouldSaveRecipientAddressKey), PreferencesKey.disableBuyKey: _sharedPreferences.getBool(PreferencesKey.disableBuyKey), PreferencesKey.disableSellKey: _sharedPreferences.getBool(PreferencesKey.disableSellKey), + PreferencesKey.defaultBuyProvider: + _sharedPreferences.getInt(PreferencesKey.defaultBuyProvider), PreferencesKey.isDarkThemeLegacy: _sharedPreferences.getBool(PreferencesKey.isDarkThemeLegacy), PreferencesKey.currentPinLength: _sharedPreferences.getInt(PreferencesKey.currentPinLength), @@ -517,16 +564,23 @@ class BackupService { _sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForCreatingNewWallets), PreferencesKey.shouldRequireTOTP2FAForAllSecurityAndBackupSettings: _sharedPreferences .getBool(PreferencesKey.shouldRequireTOTP2FAForAllSecurityAndBackupSettings), - PreferencesKey.sortBalanceBy: - _sharedPreferences.getInt(PreferencesKey.sortBalanceBy), + PreferencesKey.sortBalanceBy: _sharedPreferences.getInt(PreferencesKey.sortBalanceBy), PreferencesKey.pinNativeTokenAtTop: _sharedPreferences.getBool(PreferencesKey.pinNativeTokenAtTop), - PreferencesKey.useEtherscan: - _sharedPreferences.getBool(PreferencesKey.useEtherscan), - PreferencesKey.syncModeKey: - _sharedPreferences.getInt(PreferencesKey.syncModeKey), - PreferencesKey.syncAllKey: - _sharedPreferences.getBool(PreferencesKey.syncAllKey), + PreferencesKey.useEtherscan: _sharedPreferences.getBool(PreferencesKey.useEtherscan), + PreferencesKey.defaultNanoRep: _sharedPreferences.getString(PreferencesKey.defaultNanoRep), + PreferencesKey.defaultBananoRep: + _sharedPreferences.getString(PreferencesKey.defaultBananoRep), + PreferencesKey.lookupsTwitter: _sharedPreferences.getBool(PreferencesKey.lookupsTwitter), + PreferencesKey.lookupsMastodon: _sharedPreferences.getBool(PreferencesKey.lookupsMastodon), + PreferencesKey.lookupsYatService: + _sharedPreferences.getBool(PreferencesKey.lookupsYatService), + PreferencesKey.lookupsUnstoppableDomains: + _sharedPreferences.getBool(PreferencesKey.lookupsUnstoppableDomains), + PreferencesKey.lookupsOpenAlias: _sharedPreferences.getBool(PreferencesKey.lookupsOpenAlias), + PreferencesKey.lookupsENS: _sharedPreferences.getBool(PreferencesKey.lookupsENS), + PreferencesKey.syncModeKey: _sharedPreferences.getInt(PreferencesKey.syncModeKey), + PreferencesKey.syncAllKey: _sharedPreferences.getBool(PreferencesKey.syncAllKey), PreferencesKey.autoGenerateSubaddressStatusKey: _sharedPreferences.getInt(PreferencesKey.autoGenerateSubaddressStatusKey), }; diff --git a/lib/core/seed_validator.dart b/lib/core/seed_validator.dart index 1c6e7cd20..95ccf89ac 100644 --- a/lib/core/seed_validator.dart +++ b/lib/core/seed_validator.dart @@ -29,6 +29,8 @@ class SeedValidator extends Validator { return haven!.getMoneroWordList(language); case WalletType.ethereum: return ethereum!.getEthereumWordList(language); + case WalletType.bitcoinCash: + return getBitcoinWordList(language); case WalletType.nano: case WalletType.banano: return nano!.getNanoWordList(language); diff --git a/lib/core/wallet_connect/chain_service.dart b/lib/core/wallet_connect/chain_service.dart new file mode 100644 index 000000000..1e3ce3efd --- /dev/null +++ b/lib/core/wallet_connect/chain_service.dart @@ -0,0 +1,5 @@ +abstract class ChainService { + String getNamespace(); + String getChainId(); + List getEvents(); +} diff --git a/lib/core/wallet_connect/eth_transaction_model.dart b/lib/core/wallet_connect/eth_transaction_model.dart new file mode 100644 index 000000000..deb33586f --- /dev/null +++ b/lib/core/wallet_connect/eth_transaction_model.dart @@ -0,0 +1,60 @@ +class WCEthereumTransactionModel { + final String from; + final String to; + final String value; + final String? nonce; + final String? gasPrice; + final String? maxFeePerGas; + final String? maxPriorityFeePerGas; + final String? gas; + final String? gasLimit; + final String? data; + + WCEthereumTransactionModel({ + required this.from, + required this.to, + required this.value, + this.nonce, + this.gasPrice, + this.maxFeePerGas, + this.maxPriorityFeePerGas, + this.gas, + this.gasLimit, + this.data, + }); + + factory WCEthereumTransactionModel.fromJson(Map json) { + return WCEthereumTransactionModel( + from: json['from'] as String, + to: json['to'] as String, + value: json['value'] as String, + nonce: json['nonce'] as String?, + gasPrice: json['gasPrice'] as String?, + maxFeePerGas: json['maxFeePerGas'] as String?, + maxPriorityFeePerGas: json['maxPriorityFeePerGas'] as String?, + gas: json['gas'] as String?, + gasLimit: json['gasLimit'] as String?, + data: json['data'] as String?, + ); + } + + Map toJson() { + return { + 'from': from, + 'to': to, + 'value': value, + 'nonce': nonce, + 'gasPrice': gasPrice, + 'maxFeePerGas': maxFeePerGas, + 'maxPriorityFeePerGas': maxPriorityFeePerGas, + 'gas': gas, + 'gasLimit': gasLimit, + 'data': data, + }; + } + + @override + String toString() { + return 'EthereumTransactionModel(from: $from, to: $to, nonce: $nonce, gasPrice: $gasPrice, maxFeePerGas: $maxFeePerGas, maxPriorityFeePerGas: $maxPriorityFeePerGas, gas: $gas, gasLimit: $gasLimit, value: $value, data: $data)'; + } +} diff --git a/lib/core/wallet_connect/evm_chain_id.dart b/lib/core/wallet_connect/evm_chain_id.dart new file mode 100644 index 000000000..b71fb562e --- /dev/null +++ b/lib/core/wallet_connect/evm_chain_id.dart @@ -0,0 +1,35 @@ +import 'package:cake_wallet/core/wallet_connect/evm_chain_service.dart'; + +enum EVMChainId { + ethereum, + polygon, + goerli, + mumbai, + arbitrum, +} + +extension EVMChainIdX on EVMChainId { + String chain() { + String name = ''; + + switch (this) { + case EVMChainId.ethereum: + name = '1'; + break; + case EVMChainId.polygon: + name = '137'; + break; + case EVMChainId.goerli: + name = '5'; + break; + case EVMChainId.arbitrum: + name = '42161'; + break; + case EVMChainId.mumbai: + name = '80001'; + break; + } + + return '${EvmChainServiceImpl.namespace}:$name'; + } +} diff --git a/lib/core/wallet_connect/evm_chain_service.dart b/lib/core/wallet_connect/evm_chain_service.dart new file mode 100644 index 000000000..dc22e3dda --- /dev/null +++ b/lib/core/wallet_connect/evm_chain_service.dart @@ -0,0 +1,294 @@ +import 'dart:convert'; +import 'dart:developer'; +import 'dart:typed_data'; + +import 'package:cake_wallet/core/wallet_connect/eth_transaction_model.dart'; +import 'package:cake_wallet/core/wallet_connect/evm_chain_id.dart'; +import 'package:cake_wallet/core/wallet_connect/wc_bottom_sheet_service.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/screens/wallet_connect/widgets/message_display_widget.dart'; +import 'package:cake_wallet/store/app_store.dart'; +import 'package:cake_wallet/core/wallet_connect/models/chain_key_model.dart'; +import 'package:cake_wallet/core/wallet_connect/models/connection_model.dart'; +import 'package:cake_wallet/src/screens/wallet_connect/widgets/connection_widget.dart'; +import 'package:cake_wallet/src/screens/wallet_connect/widgets/modals/web3_request_modal.dart'; +import 'package:cake_wallet/src/screens/wallet_connect/utils/string_parsing.dart'; +import 'package:convert/convert.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:eth_sig_util/eth_sig_util.dart'; +import 'package:eth_sig_util/util/utils.dart'; +import 'package:http/http.dart' as http; +import 'package:walletconnect_flutter_v2/walletconnect_flutter_v2.dart'; +import 'package:web3dart/web3dart.dart'; +import 'chain_service.dart'; +import 'wallet_connect_key_service.dart'; + +class EvmChainServiceImpl implements ChainService { + final AppStore appStore; + final BottomSheetService bottomSheetService; + final Web3Wallet wallet; + final WalletConnectKeyService wcKeyService; + + static const namespace = 'eip155'; + static const pSign = 'personal_sign'; + static const eSign = 'eth_sign'; + static const eSignTransaction = 'eth_signTransaction'; + static const eSignTypedData = 'eth_signTypedData_v4'; + static const eSendTransaction = 'eth_sendTransaction'; + + final EVMChainId reference; + + final Web3Client ethClient; + + EvmChainServiceImpl({ + required this.reference, + required this.appStore, + required this.wcKeyService, + required this.bottomSheetService, + required this.wallet, + Web3Client? ethClient, + }) : ethClient = ethClient ?? + Web3Client( + appStore.settingsStore.getCurrentNode(WalletType.ethereum).uri.toString(), + http.Client(), + ) { + + for (final String event in getEvents()) { + wallet.registerEventEmitter(chainId: getChainId(), event: event); + } + wallet.registerRequestHandler( + chainId: getChainId(), + method: pSign, + handler: personalSign, + ); + wallet.registerRequestHandler( + chainId: getChainId(), + method: eSign, + handler: ethSign, + ); + wallet.registerRequestHandler( + chainId: getChainId(), + method: eSignTransaction, + handler: ethSignTransaction, + ); + wallet.registerRequestHandler( + chainId: getChainId(), + method: eSendTransaction, + handler: ethSignTransaction, + ); + wallet.registerRequestHandler( + chainId: getChainId(), + method: eSignTypedData, + handler: ethSignTypedData, + ); + } + + @override + String getNamespace() { + return namespace; + } + + @override + String getChainId() { + return reference.chain(); + } + + @override + List getEvents() { + return ['chainChanged', 'accountsChanged']; + } + + Future requestAuthorization(String? text) async { + // Show the bottom sheet + final bool? isApproved = await bottomSheetService.queueBottomSheet( + widget: Web3RequestModal( + child: ConnectionWidget( + title: S.current.signTransaction, + info: [ + ConnectionModel( + text: text, + ), + ], + ), + ), + ) as bool?; + + if (isApproved != null && isApproved == false) { + return 'User rejected signature'; + } + + return null; + } + + Future personalSign(String topic, dynamic parameters) async { + log('received personal sign request: $parameters'); + + final String message; + if (parameters[0] == null) { + message = ''; + } else { + message = parameters[0].toString().utf8Message; + } + + final String? authError = await requestAuthorization(message); + + if (authError != null) { + return authError; + } + + try { + // Load the private key + final List keys = wcKeyService.getKeysForChain(getChainId()); + + final Credentials credentials = EthPrivateKey.fromHex(keys[0].privateKey); + + final String signature = hex.encode( + credentials.signPersonalMessageToUint8List(Uint8List.fromList(utf8.encode(message))), + ); + + return '0x$signature'; + } catch (e) { + log(e.toString()); + bottomSheetService.queueBottomSheet( + isModalDismissible: true, + widget: BottomSheetMessageDisplayWidget( + message: '${S.current.errorGettingCredentials} ${e.toString()}', + ), + ); + return 'Failed: Error while getting credentials'; + } + } + + Future ethSign(String topic, dynamic parameters) async { + log('received eth sign request: $parameters'); + + final String message; + if (parameters[1] == null) { + message = ''; + } else { + message = parameters[1].toString().utf8Message; + } + + final String? authError = await requestAuthorization(message); + if (authError != null) { + return authError; + } + + try { + // Load the private key + final List keys = wcKeyService.getKeysForChain(getChainId()); + + final EthPrivateKey credentials = EthPrivateKey.fromHex(keys[0].privateKey); + + final String signature = hex.encode( + credentials.signPersonalMessageToUint8List( + Uint8List.fromList(utf8.encode(message)), + ), + ); + log(signature); + + return '0x$signature'; + } catch (e) { + log('error: ${e.toString()}'); + bottomSheetService.queueBottomSheet( + isModalDismissible: true, + widget: BottomSheetMessageDisplayWidget(message: '${S.current.error}: ${e.toString()}'), + ); + return 'Failed'; + } + } + + Future ethSignTransaction(String topic, dynamic parameters) async { + log('received eth sign transaction request: $parameters'); + + final paramsData = parameters[0] as Map; + + final message = _convertToReadable(paramsData); + + final String? authError = await requestAuthorization(message); + + if (authError != null) { + return authError; + } + + // Load the private key + final List keys = wcKeyService.getKeysForChain(getChainId()); + + final Credentials credentials = EthPrivateKey.fromHex(keys[0].privateKey); + + WCEthereumTransactionModel ethTransaction = + WCEthereumTransactionModel.fromJson(parameters[0] as Map); + + final transaction = Transaction( + from: EthereumAddress.fromHex(ethTransaction.from), + to: EthereumAddress.fromHex(ethTransaction.to), + maxGas: ethTransaction.gasLimit != null ? int.tryParse(ethTransaction.gasLimit ?? "") : null, + gasPrice: ethTransaction.gasPrice != null + ? EtherAmount.inWei(BigInt.parse(ethTransaction.gasPrice ?? "")) + : null, + value: EtherAmount.inWei(BigInt.parse(ethTransaction.value)), + data: hexToBytes(ethTransaction.data ?? ""), + nonce: ethTransaction.nonce != null ? int.tryParse(ethTransaction.nonce ?? "") : null, + ); + + try { + final result = await ethClient.sendTransaction(credentials, transaction); + + log('Result: $result'); + + bottomSheetService.queueBottomSheet( + isModalDismissible: true, + widget: BottomSheetMessageDisplayWidget( + message: S.current.awaitDAppProcessing, + isError: false, + ), + ); + + return result; + } catch (e) { + log('An error has occured while signing transaction: ${e.toString()}'); + bottomSheetService.queueBottomSheet( + isModalDismissible: true, + widget: BottomSheetMessageDisplayWidget( + message: '${S.current.errorSigningTransaction}: ${e.toString()}', + ), + ); + return 'Failed'; + } + } + + Future ethSignTypedData(String topic, dynamic parameters) async { + log('received eth sign typed data request: $parameters'); + final String? data = parameters[1] as String?; + + final String? authError = await requestAuthorization(data); + + if (authError != null) { + return authError; + } + + final List keys = wcKeyService.getKeysForChain(getChainId()); + + return EthSigUtil.signTypedData( + privateKey: keys[0].privateKey, + jsonData: data ?? '', + version: TypedDataVersion.V4, + ); + } + + String _convertToReadable(Map data) { + String gas = int.parse((data['gas'] as String).substring(2), radix: 16).toString(); + String value = data['value'] != null + ? (int.parse((data['value'] as String).substring(2), radix: 16) / 1e18).toString() + ' ETH' + : '0 ETH'; + String from = data['from'] as String; + String to = data['to'] as String; + + return ''' + Gas: $gas\n + Value: $value\n + From: $from\n + To: $to + '''; + } +} diff --git a/lib/core/wallet_connect/models/auth_request_model.dart b/lib/core/wallet_connect/models/auth_request_model.dart new file mode 100644 index 000000000..f7fd984c8 --- /dev/null +++ b/lib/core/wallet_connect/models/auth_request_model.dart @@ -0,0 +1,16 @@ +import 'package:walletconnect_flutter_v2/walletconnect_flutter_v2.dart'; + +class AuthRequestModel { + final String iss; + final AuthRequest request; + + AuthRequestModel({ + required this.iss, + required this.request, + }); + + @override + String toString() { + return 'AuthRequestModel(iss: $iss, request: $request)'; + } +} diff --git a/lib/core/wallet_connect/models/bottom_sheet_queue_item_model.dart b/lib/core/wallet_connect/models/bottom_sheet_queue_item_model.dart new file mode 100644 index 000000000..49eecac0f --- /dev/null +++ b/lib/core/wallet_connect/models/bottom_sheet_queue_item_model.dart @@ -0,0 +1,20 @@ +import 'dart:async'; + +import 'package:flutter/cupertino.dart'; + +class BottomSheetQueueItemModel { + final Widget widget; + final bool isModalDismissible; + final Completer completer; + + BottomSheetQueueItemModel({ + required this.widget, + required this.completer, + this.isModalDismissible = false, + }); + + @override + String toString() { + return 'BottomSheetQueueItemModel(widget: $widget, completer: $completer)'; + } +} diff --git a/lib/core/wallet_connect/models/chain_key_model.dart b/lib/core/wallet_connect/models/chain_key_model.dart new file mode 100644 index 000000000..5cd2764da --- /dev/null +++ b/lib/core/wallet_connect/models/chain_key_model.dart @@ -0,0 +1,16 @@ +class ChainKeyModel { + final List chains; + final String privateKey; + final String publicKey; + + ChainKeyModel({ + required this.chains, + required this.privateKey, + required this.publicKey, + }); + + @override + String toString() { + return 'ChainKeyModel(chains: $chains, privateKey: $privateKey, publicKey: $publicKey)'; + } +} diff --git a/lib/core/wallet_connect/models/connection_model.dart b/lib/core/wallet_connect/models/connection_model.dart new file mode 100644 index 000000000..63cc8260f --- /dev/null +++ b/lib/core/wallet_connect/models/connection_model.dart @@ -0,0 +1,18 @@ +class ConnectionModel { + final String? title; + final String? text; + final List? elements; + final Map? elementActions; + + ConnectionModel({ + this.title, + this.text, + this.elements, + this.elementActions, + }); + + @override + String toString() { + return 'WalletConnectRequestModel(title: $title, text: $text, elements: $elements, elementActions: $elementActions)'; + } +} diff --git a/lib/core/wallet_connect/models/session_request_model.dart b/lib/core/wallet_connect/models/session_request_model.dart new file mode 100644 index 000000000..0c7a5d876 --- /dev/null +++ b/lib/core/wallet_connect/models/session_request_model.dart @@ -0,0 +1,14 @@ +import 'package:walletconnect_flutter_v2/walletconnect_flutter_v2.dart'; + +class SessionRequestModel { + final ProposalData request; + + SessionRequestModel({ + required this.request, + }); + + @override + String toString() { + return 'SessionRequestModel(request: $request)'; + } +} diff --git a/lib/core/wallet_connect/wallet_connect_key_service.dart b/lib/core/wallet_connect/wallet_connect_key_service.dart new file mode 100644 index 000000000..2e61ebb99 --- /dev/null +++ b/lib/core/wallet_connect/wallet_connect_key_service.dart @@ -0,0 +1,72 @@ +import 'package:cake_wallet/ethereum/ethereum.dart'; +import 'package:cake_wallet/core/wallet_connect/models/chain_key_model.dart'; +import 'package:cw_core/balance.dart'; +import 'package:cw_core/transaction_history.dart'; +import 'package:cw_core/transaction_info.dart'; +import 'package:cw_core/wallet_base.dart'; + +abstract class WalletConnectKeyService { + /// Returns a list of all the keys. + List getKeys(); + + /// Returns a list of all the chain ids. + List getChains(); + + /// Returns a list of all the keys for a given chain id. + /// If the chain is not found, returns an empty list. + /// - [chain]: The chain to get the keys for. + List getKeysForChain(String chain); + + /// Returns a list of all the accounts in namespace:chainId:address format. + List getAllAccounts(); +} + +class KeyServiceImpl implements WalletConnectKeyService { + KeyServiceImpl(this.wallet) + : _keys = [ + ChainKeyModel( + chains: [ + 'eip155:1', + 'eip155:5', + 'eip155:137', + 'eip155:42161', + 'eip155:80001', + ], + privateKey: ethereum!.getPrivateKey(wallet), + publicKey: ethereum!.getPublicKey(wallet), + ), + + ]; + + late final WalletBase, TransactionInfo> wallet; + + late final List _keys; + + @override + List getChains() { + final List chainIds = []; + for (final ChainKeyModel key in _keys) { + chainIds.addAll(key.chains); + } + return chainIds; + } + + @override + List getKeys() => _keys; + + @override + List getKeysForChain(String chain) { + return _keys.where((e) => e.chains.contains(chain)).toList(); + } + + @override + List getAllAccounts() { + final List accounts = []; + for (final ChainKeyModel key in _keys) { + for (final String chain in key.chains) { + accounts.add('$chain:${key.publicKey}'); + } + } + return accounts; + } +} diff --git a/lib/core/wallet_connect/wc_bottom_sheet_service.dart b/lib/core/wallet_connect/wc_bottom_sheet_service.dart new file mode 100644 index 000000000..3da8660f0 --- /dev/null +++ b/lib/core/wallet_connect/wc_bottom_sheet_service.dart @@ -0,0 +1,43 @@ +import 'dart:async'; +import 'package:cake_wallet/core/wallet_connect/models/bottom_sheet_queue_item_model.dart'; +import 'package:flutter/material.dart'; + +abstract class BottomSheetService { + abstract final ValueNotifier currentSheet; + + Future queueBottomSheet({ + required Widget widget, + bool isModalDismissible = false, + }); + + void resetCurrentSheet(); +} + +class BottomSheetServiceImpl implements BottomSheetService { + + @override + final ValueNotifier currentSheet = ValueNotifier(null); + + @override + Future queueBottomSheet({ + required Widget widget, + bool isModalDismissible = false, + }) async { + // Create the bottom sheet queue item + final completer = Completer(); + final queueItem = BottomSheetQueueItemModel( + widget: widget, + completer: completer, + isModalDismissible: isModalDismissible, + ); + + currentSheet.value = queueItem; + + return await completer.future; + } + + @override + void resetCurrentSheet() { + currentSheet.value = null; + } +} diff --git a/lib/core/wallet_connect/web3wallet_service.dart b/lib/core/wallet_connect/web3wallet_service.dart new file mode 100644 index 000000000..c69692c9d --- /dev/null +++ b/lib/core/wallet_connect/web3wallet_service.dart @@ -0,0 +1,295 @@ +import 'dart:async'; +import 'dart:developer'; +import 'dart:typed_data'; + +import 'package:cake_wallet/core/wallet_connect/evm_chain_id.dart'; +import 'package:cake_wallet/core/wallet_connect/evm_chain_service.dart'; +import 'package:cake_wallet/core/wallet_connect/wallet_connect_key_service.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/core/wallet_connect/models/auth_request_model.dart'; +import 'package:cake_wallet/core/wallet_connect/models/chain_key_model.dart'; +import 'package:cake_wallet/core/wallet_connect/models/session_request_model.dart'; +import 'package:cake_wallet/src/screens/wallet_connect/widgets/connection_request_widget.dart'; +import 'package:cake_wallet/src/screens/wallet_connect/widgets/message_display_widget.dart'; +import 'package:cake_wallet/src/screens/wallet_connect/widgets/modals/web3_request_modal.dart'; +import 'package:cake_wallet/store/app_store.dart'; +import 'package:eth_sig_util/eth_sig_util.dart'; +import 'package:flutter/material.dart'; +import 'package:mobx/mobx.dart'; +import 'package:walletconnect_flutter_v2/walletconnect_flutter_v2.dart'; + +import 'wc_bottom_sheet_service.dart'; +import 'package:cake_wallet/.secrets.g.dart' as secrets; + +part 'web3wallet_service.g.dart'; + +class Web3WalletService = Web3WalletServiceBase with _$Web3WalletService; + +abstract class Web3WalletServiceBase with Store { + final AppStore appStore; + final BottomSheetService _bottomSheetHandler; + final WalletConnectKeyService walletKeyService; + + late Web3Wallet _web3Wallet; + + @observable + bool isInitialized; + + /// The list of requests from the dapp + /// Potential types include, but aren't limited to: + /// [SessionProposalEvent], [AuthRequest] + @observable + ObservableList pairings; + + @observable + ObservableList sessions; + + @observable + ObservableList auth; + + Web3WalletServiceBase(this._bottomSheetHandler, this.walletKeyService, this.appStore) + : pairings = ObservableList(), + sessions = ObservableList(), + auth = ObservableList(), + isInitialized = false; + + @action + void create() { + // Create the web3wallet client + _web3Wallet = Web3Wallet( + core: Core(projectId: secrets.walletConnectProjectId), + metadata: const PairingMetadata( + name: 'Cake Wallet', + description: 'Cake Wallet', + url: 'https://cakewallet.com', + icons: ['https://cakewallet.com/assets/image/cake_logo.png'], + ), + ); + + // Setup our accounts + List chainKeys = walletKeyService.getKeys(); + for (final chainKey in chainKeys) { + for (final chainId in chainKey.chains) { + _web3Wallet.registerAccount( + chainId: chainId, + accountAddress: chainKey.publicKey, + ); + } + } + + // Setup our listeners + log('Created instance of web3wallet'); + _web3Wallet.core.pairing.onPairingInvalid.subscribe(_onPairingInvalid); + _web3Wallet.core.pairing.onPairingCreate.subscribe(_onPairingCreate); + _web3Wallet.core.pairing.onPairingDelete.subscribe(_onPairingDelete); + _web3Wallet.core.pairing.onPairingExpire.subscribe(_onPairingDelete); + _web3Wallet.pairings.onSync.subscribe(_onPairingsSync); + _web3Wallet.onSessionProposal.subscribe(_onSessionProposal); + _web3Wallet.onSessionProposalError.subscribe(_onSessionProposalError); + _web3Wallet.onSessionConnect.subscribe(_onSessionConnect); + _web3Wallet.onAuthRequest.subscribe(_onAuthRequest); + } + + @action + Future init() async { + // Await the initialization of the web3wallet + log('Intializing web3wallet'); + if (!isInitialized) { + try { + await _web3Wallet.init(); + log('Initialized'); + isInitialized = true; + } catch (e) { + log('Experimentallllll: $e'); + isInitialized = false; + } + } + + _refreshPairings(); + + final newSessions = _web3Wallet.sessions.getAll(); + sessions.addAll(newSessions); + + final newAuthRequests = _web3Wallet.completeRequests.getAll(); + auth.addAll(newAuthRequests); + + for (final cId in EVMChainId.values) { + EvmChainServiceImpl( + reference: cId, + appStore: appStore, + wcKeyService: walletKeyService, + bottomSheetService: _bottomSheetHandler, + wallet: _web3Wallet, + ); + } + } + + @action + FutureOr onDispose() { + log('web3wallet dispose'); + _web3Wallet.core.pairing.onPairingInvalid.unsubscribe(_onPairingInvalid); + _web3Wallet.pairings.onSync.unsubscribe(_onPairingsSync); + _web3Wallet.onSessionProposal.unsubscribe(_onSessionProposal); + _web3Wallet.onSessionProposalError.unsubscribe(_onSessionProposalError); + _web3Wallet.onSessionConnect.unsubscribe(_onSessionConnect); + _web3Wallet.onAuthRequest.unsubscribe(_onAuthRequest); + _web3Wallet.core.pairing.onPairingDelete.unsubscribe(_onPairingDelete); + _web3Wallet.core.pairing.onPairingExpire.unsubscribe(_onPairingDelete); + } + + Web3Wallet getWeb3Wallet() { + return _web3Wallet; + } + + void _onPairingsSync(StoreSyncEvent? args) { + if (args != null) { + _refreshPairings(); + } + } + + void _onPairingDelete(PairingEvent? event) { + _refreshPairings(); + } + + @action + void _refreshPairings() { + pairings.clear(); + final allPairings = _web3Wallet.pairings.getAll(); + pairings.addAll(allPairings); + } + + Future _onSessionProposalError(SessionProposalErrorEvent? args) async { + log(args.toString()); + } + + void _onSessionProposal(SessionProposalEvent? args) async { + if (args != null) { + final Widget modalWidget = Web3RequestModal( + child: ConnectionRequestWidget( + wallet: _web3Wallet, + sessionProposal: SessionRequestModel(request: args.params), + ), + ); + // show the bottom sheet + final bool? isApproved = await _bottomSheetHandler.queueBottomSheet( + widget: modalWidget, + ) as bool?; + + if (isApproved != null && isApproved) { + _web3Wallet.approveSession( + id: args.id, + namespaces: args.params.generatedNamespaces!, + ); + } else { + _web3Wallet.rejectSession( + id: args.id, + reason: Errors.getSdkError( + Errors.USER_REJECTED, + ), + ); + } + } + } + + @action + void _onPairingInvalid(PairingInvalidEvent? args) { + log('Pairing Invalid Event: $args'); + _bottomSheetHandler.queueBottomSheet( + isModalDismissible: true, + widget: BottomSheetMessageDisplayWidget(message: '${S.current.pairingInvalidEvent}: $args'), + ); + } + + @action + Future pairWithUri(Uri uri) async { + try { + log('Pairing with URI: $uri'); + await _web3Wallet.pair(uri: uri); + } on WalletConnectError catch (e) { + _bottomSheetHandler.queueBottomSheet( + isModalDismissible: true, + widget: BottomSheetMessageDisplayWidget(message: e.message), + ); + } catch (e) { + _bottomSheetHandler.queueBottomSheet( + isModalDismissible: true, + widget: BottomSheetMessageDisplayWidget(message: e.toString()), + ); + } + } + + void _onPairingCreate(PairingEvent? args) { + log('Pairing Create Event: $args'); + } + + @action + void _onSessionConnect(SessionConnect? args) { + if (args != null) { + sessions.add(args.session); + } + } + + @action + Future _onAuthRequest(AuthRequest? args) async { + if (args != null) { + List chainKeys = walletKeyService.getKeysForChain('eip155:1'); + // Create the message to be signed + final String iss = 'did:pkh:eip155:1:${chainKeys.first.publicKey}'; + + final Widget modalWidget = Web3RequestModal( + child: ConnectionRequestWidget( + wallet: _web3Wallet, + authRequest: AuthRequestModel(iss: iss, request: args), + ), + ); + final bool? isAuthenticated = await _bottomSheetHandler.queueBottomSheet( + widget: modalWidget, + ) as bool?; + + if (isAuthenticated != null && isAuthenticated) { + final String message = _web3Wallet.formatAuthMessage( + iss: iss, + cacaoPayload: CacaoRequestPayload.fromPayloadParams( + args.payloadParams, + ), + ); + + final String sig = EthSigUtil.signPersonalMessage( + message: Uint8List.fromList(message.codeUnits), + privateKey: chainKeys.first.privateKey, + ); + + await _web3Wallet.respondAuthRequest( + id: args.id, + iss: iss, + signature: CacaoSignature( + t: CacaoSignature.EIP191, + s: sig, + ), + ); + } else { + await _web3Wallet.respondAuthRequest( + id: args.id, + iss: iss, + error: Errors.getSdkError( + Errors.USER_REJECTED_AUTH, + ), + ); + } + } + } + + @action + Future disconnectSession(String topic) async { + final session = sessions.firstWhere((element) => element.pairingTopic == topic); + + await _web3Wallet.core.pairing.disconnect(topic: topic); + await _web3Wallet.disconnectSession( + topic: session.topic, reason: Errors.getSdkError(Errors.USER_DISCONNECTED)); + } + + @action + List getSessionsForPairingInfo(PairingInfo pairing) { + return sessions.where((element) => element.pairingTopic == pairing.topic).toList(); + } +} diff --git a/lib/core/wallet_creation_service.dart b/lib/core/wallet_creation_service.dart index dda591115..8548f079f 100644 --- a/lib/core/wallet_creation_service.dart +++ b/lib/core/wallet_creation_service.dart @@ -1,4 +1,5 @@ import 'package:cake_wallet/di.dart'; +import 'package:cake_wallet/store/settings_store.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cake_wallet/entities/preferences_key.dart'; import 'package:flutter/foundation.dart'; @@ -18,6 +19,7 @@ class WalletCreationService { required this.secureStorage, required this.keyService, required this.sharedPreferences, + required this.settingsStore, required this.walletInfoSource}) : type = initialType { changeWalletType(type: type); @@ -26,6 +28,7 @@ class WalletCreationService { WalletType type; final FlutterSecureStorage secureStorage; final SharedPreferences sharedPreferences; + final SettingsStore settingsStore; final KeyService keyService; final Box walletInfoSource; WalletService? _service; @@ -56,6 +59,9 @@ class WalletCreationService { checkIfExists(credentials.name); final password = generateWalletPassword(); credentials.password = password; + if (type == WalletType.bitcoinCash || type == WalletType.ethereum) { + credentials.seedPhraseLength = settingsStore.seedPhraseLength.value; + } await keyService.saveWalletPassword(password: password, walletName: credentials.name); final wallet = await _service!.create(credentials); diff --git a/lib/di.dart b/lib/di.dart index 9a58f54dd..cd9315636 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -2,10 +2,14 @@ import 'package:cake_wallet/anonpay/anonpay_api.dart'; import 'package:cake_wallet/anonpay/anonpay_info_base.dart'; import 'package:cake_wallet/anonpay/anonpay_invoice_info.dart'; import 'package:cake_wallet/buy/onramper/onramper_buy_provider.dart'; +import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart'; import 'package:cake_wallet/buy/payfura/payfura_buy_provider.dart'; +import 'package:cake_wallet/core/wallet_connect/wallet_connect_key_service.dart'; +import 'package:cake_wallet/core/wallet_connect/wc_bottom_sheet_service.dart'; +import 'package:cake_wallet/buy/robinhood/robinhood_buy_provider.dart'; +import 'package:cake_wallet/core/wallet_connect/web3wallet_service.dart'; import 'package:cake_wallet/core/yat_service.dart'; import 'package:cake_wallet/entities/background_tasks.dart'; -import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart'; import 'package:cake_wallet/entities/exchange_api_mode.dart'; import 'package:cake_wallet/entities/parse_address_from_domain.dart'; import 'package:cake_wallet/entities/receive_page_option.dart'; @@ -16,6 +20,7 @@ import 'package:cake_wallet/ionia/ionia_gift_card.dart'; import 'package:cake_wallet/ionia/ionia_tip.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/anonpay_details/anonpay_details_page.dart'; +import 'package:cake_wallet/src/screens/buy/buy_options_page.dart'; import 'package:cake_wallet/src/screens/buy/webview_page.dart'; import 'package:cake_wallet/src/screens/dashboard/desktop_dashboard_page.dart'; import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/desktop_sidebar_wrapper.dart'; @@ -31,6 +36,7 @@ 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/restore/wallet_restore_choose_derivation.dart'; 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/manage_pow_nodes_page.dart'; import 'package:cake_wallet/src/screens/settings/other_settings_page.dart'; @@ -40,6 +46,8 @@ import 'package:cake_wallet/src/screens/ionia/cards/ionia_custom_redeem_page.dar import 'package:cake_wallet/src/screens/ionia/cards/ionia_gift_card_detail_page.dart'; import 'package:cake_wallet/src/screens/ionia/cards/ionia_more_options_page.dart'; import 'package:cake_wallet/src/screens/settings/connection_sync_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_qr_page.dart'; import 'package:cake_wallet/src/screens/setup_2fa/setup_2fa.dart'; @@ -47,6 +55,7 @@ import 'package:cake_wallet/src/screens/setup_2fa/setup_2fa_enter_code_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/wallet/wallet_edit_page.dart'; +import 'package:cake_wallet/src/screens/wallet_connect/wc_connections_listing_view.dart'; import 'package:cake_wallet/themes/theme_list.dart'; import 'package:cake_wallet/utils/device_info.dart'; import 'package:cake_wallet/store/anonpay/anonpay_transactions_store.dart'; @@ -78,7 +87,6 @@ import 'package:cake_wallet/view_model/ionia/ionia_gift_cards_list_view_model.da import 'package:cake_wallet/view_model/ionia/ionia_purchase_merch_view_model.dart'; import 'package:cake_wallet/view_model/nano_account_list/nano_account_edit_or_create_view_model.dart'; import 'package:cake_wallet/view_model/nano_account_list/nano_account_list_view_model.dart'; -import 'package:cake_wallet/view_model/node_list/pow_node_create_or_edit_view_model.dart'; import 'package:cake_wallet/view_model/node_list/pow_node_list_view_model.dart'; import 'package:cake_wallet/view_model/set_up_2fa_viewmodel.dart'; import 'package:cake_wallet/view_model/restore/restore_from_qr_vm.dart'; @@ -87,6 +95,7 @@ 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'; 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'; @@ -108,7 +117,6 @@ import 'package:cake_wallet/reactions/on_authentication_state_change.dart'; import 'package:cake_wallet/src/screens/backup/backup_page.dart'; import 'package:cake_wallet/src/screens/backup/edit_backup_password_page.dart'; import 'package:cake_wallet/src/screens/buy/buy_webview_page.dart'; -import 'package:cake_wallet/src/screens/buy/pre_order_page.dart'; import 'package:cake_wallet/src/screens/contact/contact_list_page.dart'; import 'package:cake_wallet/src/screens/contact/contact_page.dart'; import 'package:cake_wallet/src/screens/exchange_trade/exchange_confirm_page.dart'; @@ -219,6 +227,7 @@ import 'package:cake_wallet/entities/qr_view_data.dart'; import 'package:cake_wallet/nano/nano.dart' as nanoNano; import 'core/totp_request_details.dart'; +import 'src/screens/settings/desktop_settings/desktop_settings_page.dart'; final getIt = GetIt.instance; @@ -247,6 +256,7 @@ Future setup({ required Box ordersSource, required Box unspentCoinsInfoSource, required Box anonpayInvoiceInfoSource, + required FlutterSecureStorage secureStorage, }) async { _walletInfoSource = walletInfoSource; _nodeSource = nodeSource; @@ -276,7 +286,7 @@ Future setup({ powNodeSource: _powNodeSource, isBitcoinBuyEnabled: isBitcoinBuyEnabled, // Enforce darkTheme on platforms other than mobile till the design for other themes is completed - initialTheme: ResponsiveLayoutUtil.instance.isMobile && DeviceInfo.instance.isMobile + initialTheme: responsiveLayoutUtil.shouldRenderMobileUI && DeviceInfo.instance.isMobile ? null : ThemeList.darkTheme, ); @@ -288,7 +298,7 @@ Future setup({ getIt.registerFactory>(() => _nodeSource); getIt.registerFactory>(() => _powNodeSource, instanceName: Node.boxName + "pow"); - getIt.registerSingleton(FlutterSecureStorage()); + getIt.registerSingleton(secureStorage); getIt.registerSingleton(AuthenticationStore()); getIt.registerSingleton(WalletListStore()); getIt.registerSingleton(NodeListStoreBase.instance); @@ -326,6 +336,7 @@ Future setup({ keyService: getIt.get(), secureStorage: getIt.get(), sharedPreferences: getIt.get(), + settingsStore: getIt.get(), walletInfoSource: _walletInfoSource)); getIt.registerFactory(() => WalletLoadingService( @@ -333,9 +344,10 @@ Future setup({ getIt.get(), (WalletType type) => getIt.get(param1: type))); - getIt.registerFactoryParam((type, _) => WalletNewVM( - getIt.get(), getIt.get(param1: type), _walletInfoSource, - type: type)); + getIt.registerFactoryParam((type, _) => + WalletNewVM(getIt.get(), + getIt.get(param1: type), _walletInfoSource, + type: type)); getIt.registerFactoryParam((WalletType type, _) { return WalletRestorationFromQRVM(getIt.get(), @@ -378,7 +390,7 @@ Future setup({ (onAuthFinished, closable) => AuthPage(getIt.get(), onAuthenticationFinished: onAuthFinished, closable: closable)); - getIt.registerFactory( + getIt.registerLazySingleton( () => Setup2FAViewModel( getIt.get(), getIt.get(), @@ -456,11 +468,28 @@ Future setup({ }, closable: false); }, instanceName: 'login'); + getIt.registerSingleton(BottomSheetServiceImpl()); + + final appStore = getIt.get(); + + getIt.registerLazySingleton(() => KeyServiceImpl(appStore.wallet!)); + + getIt.registerLazySingleton(() { + final Web3WalletService web3WalletService = Web3WalletService( + getIt.get(), + getIt.get(), + appStore, + ); + web3WalletService.create(); + return web3WalletService; + }); + getIt.registerFactory(() => BalancePage( dashboardViewModel: getIt.get(), settingsStore: getIt.get())); getIt.registerFactory(() => DashboardPage( + bottomSheetService: getIt.get(), balancePage: getIt.get(), dashboardViewModel: getIt.get(), addressListViewModel: getIt.get(), @@ -469,6 +498,7 @@ Future setup({ getIt.registerFactory(() { final GlobalKey _navigatorKey = GlobalKey(); return DesktopSidebarWrapper( + bottomSheetService: getIt.get(), dashboardViewModel: getIt.get(), desktopSidebarViewModel: getIt.get(), child: getIt.get(param1: _navigatorKey), @@ -495,6 +525,8 @@ Future setup({ getIt.registerFactory( () => Modify2FAPage(setup2FAViewModel: getIt.get())); + getIt.registerFactory(() => DesktopSettingsPage()); + getIt.registerFactoryParam( (pageOption, _) => ReceiveOptionViewModel(getIt.get().wallet!, pageOption)); @@ -602,37 +634,22 @@ Future setup({ editingWallet: editingWallet); }); - // getIt.registerFactory(() { - // final wallet = getIt.get().wallet!; - - // if (wallet.type == WalletType.monero || wallet.type == WalletType.haven) { - // return MoneroAccountListViewModel(wallet); - // } - - // if (wallet.type == WalletType.nano || wallet.type == WalletType.banano) { - // return NanoAccountListViewModel(wallet); - // } - - // throw Exception( - // 'Unexpected wallet type: ${wallet.type} for generate Nano/Monero AccountListViewModel'); - // }); - - getIt.registerFactory(() { + getIt.registerFactory(() { final wallet = getIt.get().wallet!; if (wallet.type == WalletType.nano || wallet.type == WalletType.banano) { return NanoAccountListViewModel(wallet); } throw Exception( - 'Unexpected wallet type: ${wallet.type} for generate Nano/Monero AccountListViewModel'); + 'Unexpected wallet type: ${wallet.type} for generate Nano/Banano AccountListViewModel'); }); - getIt.registerFactory(() { + getIt.registerFactory(() { final wallet = getIt.get().wallet!; if (wallet.type == WalletType.monero || wallet.type == WalletType.haven) { return MoneroAccountListViewModel(wallet); } throw Exception( - 'Unexpected wallet type: ${wallet.type} for generate Nano/Monero AccountListViewModel'); + 'Unexpected wallet type: ${wallet.type} for generate Monero AccountListViewModel'); }); getIt.registerFactory( @@ -688,6 +705,8 @@ Future setup({ return PrivacySettingsViewModel(getIt.get(), getIt.get().wallet!); }); + getIt.registerFactory(() => TrocadorProvidersViewModel(getIt.get())); + getIt.registerFactory(() { return OtherSettingsViewModel(getIt.get(), getIt.get().wallet!); }); @@ -728,22 +747,37 @@ Future setup({ return PowNodeListViewModel(_powNodeSource, appStore); }); - getIt.registerFactory(() => ConnectionSyncPage(getIt.get())); + getIt.registerFactory(() { + final wallet = getIt.get().wallet; + return ConnectionSyncPage( + getIt.get(), + wallet?.type == WalletType.ethereum ? getIt.get() : null, + ); + }); getIt.registerFactory( () => SecurityBackupPage(getIt.get(), getIt.get())); getIt.registerFactory(() => PrivacyPage(getIt.get())); + getIt.registerFactory(() => TrocadorProvidersPage(getIt.get())); + + getIt.registerFactory(() => DomainLookupsPage(getIt.get())); + getIt.registerFactory(() => DisplaySettingsPage(getIt.get())); getIt.registerFactory(() => OtherSettingsPage(getIt.get())); - getIt.registerFactory(() => NanoChangeRepPage()); + getIt.registerFactory(() => NanoChangeRepPage( + settingsStore: getIt.get().settingsStore, + wallet: getIt.get().wallet!, + )); - getIt.registerFactoryParam((WalletType? type, _) => - NodeCreateOrEditViewModel( - _nodeSource, type ?? getIt.get().wallet!.type, getIt.get())); + getIt.registerFactoryParam( + (WalletType? type, bool? isPow) => NodeCreateOrEditViewModel( + (isPow ?? false) ? _powNodeSource : _nodeSource, + type ?? getIt.get().wallet!.type, + getIt.get())); getIt.registerFactoryParam( (WalletType? type, _) => PowNodeCreateOrEditViewModel( @@ -751,16 +785,19 @@ Future setup({ getIt.registerFactoryParam( (Node? editingNode, bool? isSelected) => NodeCreateOrEditPage( - nodeCreateOrEditViewModel: getIt.get(), + nodeCreateOrEditViewModel: getIt.get(param2: false), editingNode: editingNode, isSelected: isSelected)); getIt.registerFactoryParam( (Node? editingNode, bool? isSelected) => PowNodeCreateOrEditPage( - nodeCreateOrEditViewModel: getIt.get(), + nodeCreateOrEditViewModel: getIt.get(param2: true), editingNode: editingNode, isSelected: isSelected)); + getIt.registerFactory( + () => RobinhoodBuyProvider(wallet: getIt.get().wallet!)); + getIt.registerFactory(() => OnRamperBuyProvider( settingsStore: getIt.get().settingsStore, wallet: getIt.get().wallet!, @@ -810,6 +847,9 @@ Future setup({ return bitcoin!.createLitecoinWalletService(_walletInfoSource, _unspentCoinsInfoSource); case WalletType.ethereum: return ethereum!.createEthereumWalletService(_walletInfoSource); + case WalletType.bitcoinCash: + return bitcoinCash! + .createBitcoinCashWalletService(_walletInfoSource, _unspentCoinsInfoSource!); case WalletType.nano: return nano!.createNanoWalletService(_walletInfoSource); default: @@ -840,10 +880,9 @@ Future setup({ (type, _) => WalletRestorePage(getIt.get(param1: type))); getIt.registerFactoryParam, void>( - (derivations, _) => - WalletRestoreChooseDerivationViewModel(derivationInfos: derivations)); + (derivations, _) => WalletRestoreChooseDerivationViewModel(derivationInfos: derivations)); - getIt.registerFactoryParam( + getIt.registerFactoryParam, void>( (credentials, _) => WalletRestoreChooseDerivationPage(getIt.get( param1: credentials, @@ -867,8 +906,9 @@ Future setup({ getIt.registerFactoryParam( (param1, isCreate) => NewWalletTypePage(onTypeSelected: param1, isCreate: isCreate ?? true)); - getIt.registerFactoryParam( - (WalletType type, _) => PreSeedPage(type)); + getIt.registerFactoryParam( + (WalletType type, AdvancedPrivacySettingsViewModel advancedPrivacySettingsViewModel) + => PreSeedPage(type, advancedPrivacySettingsViewModel)); getIt.registerFactoryParam((trade, _) => TradeDetailsViewModel( @@ -901,6 +941,8 @@ Future setup({ getIt.registerFactory(() => BuyAmountViewModel()); + getIt.registerFactory(() => BuyOptionsPage()); + getIt.registerFactory(() { final wallet = getIt.get().wallet; @@ -909,11 +951,7 @@ Future setup({ wallet: wallet!); }); - getIt.registerFactory(() { - return PreOrderPage(buyViewModel: getIt.get()); - }); - - getIt.registerFactoryParam((List args, _) { + getIt.registerFactoryParam, void>((List args, _) { final url = args.first as String; final buyViewModel = args[1] as BuyViewModel; @@ -934,16 +972,15 @@ Future setup({ getIt.registerFactory(() => SupportPage(getIt.get())); - getIt.registerFactory(() => - SupportChatPage( - getIt.get(), secureStorage: getIt.get())); + getIt.registerFactory(() => SupportChatPage(getIt.get(), + secureStorage: getIt.get())); getIt.registerFactory(() => SupportOtherLinksPage(getIt.get())); getIt.registerFactory(() { final wallet = getIt.get().wallet; - return UnspentCoinsListViewModel(wallet: wallet!, unspentCoinsInfo: _unspentCoinsInfoSource!); + return UnspentCoinsListViewModel(wallet: wallet!, unspentCoinsInfo: _unspentCoinsInfoSource); }); getIt.registerFactory(() => @@ -954,7 +991,7 @@ Future setup({ (item, model) => UnspentCoinsDetailsViewModel(unspentCoinsItem: item, unspentCoinsListViewModel: model)); - getIt.registerFactoryParam((List args, _) { + getIt.registerFactoryParam, void>((List args, _) { final item = args.first as UnspentCoinsItem; final unspentCoinsListViewModel = args[1] as UnspentCoinsListViewModel; @@ -965,8 +1002,11 @@ Future setup({ getIt.registerFactory(() => YatService()); - getIt.registerFactory(() => AddressResolver( - yatService: getIt.get(), walletType: getIt.get().wallet!.type)); + getIt.registerFactory(() => + AddressResolver( + yatService: getIt.get(), + wallet: getIt.get().wallet!, + settingsStore: getIt.get())); getIt.registerFactoryParam( (QrViewData viewData, _) => FullscreenQRPage(qrViewData: viewData)); @@ -1007,7 +1047,7 @@ Future setup({ getIt.registerFactory(() => IoniaLoginPage(getIt.get())); - getIt.registerFactoryParam((List args, _) { + getIt.registerFactoryParam, void>((List args, _) { final email = args.first as String; final isSignIn = args[1] as bool; @@ -1016,13 +1056,14 @@ Future setup({ getIt.registerFactory(() => IoniaWelcomePage()); - getIt.registerFactoryParam((List args, _) { + getIt.registerFactoryParam, void>((List args, _) { final merchant = args.first as IoniaMerchant; return IoniaBuyGiftCardPage(getIt.get(param1: merchant)); }); - getIt.registerFactoryParam((List args, _) { + getIt.registerFactoryParam, void>( + (List args, _) { final amount = args.first as double; final merchant = args.last as IoniaMerchant; return IoniaBuyGiftCardDetailPage( @@ -1035,7 +1076,7 @@ Future setup({ ioniaService: getIt.get(), giftCard: giftCard); }); - getIt.registerFactoryParam((List args, _) { + getIt.registerFactoryParam, void>((List args, _) { final amount = args[0] as double; final merchant = args[1] as IoniaMerchant; final tip = args[2] as IoniaTip; @@ -1048,7 +1089,7 @@ Future setup({ return IoniaGiftCardDetailPage(getIt.get(param1: giftCard)); }); - getIt.registerFactoryParam((List args, _) { + getIt.registerFactoryParam, void>((List args, _) { final giftCard = args.first as IoniaGiftCard; return IoniaMoreOptionsPage(giftCard); @@ -1058,13 +1099,13 @@ Future setup({ (IoniaGiftCard giftCard, _) => IoniaCustomRedeemViewModel(giftCard: giftCard, ioniaService: getIt.get())); - getIt.registerFactoryParam((List args, _) { + getIt.registerFactoryParam, void>((List args, _) { final giftCard = args.first as IoniaGiftCard; return IoniaCustomRedeemPage(getIt.get(param1: giftCard)); }); - getIt.registerFactoryParam((List args, _) { + getIt.registerFactoryParam, void>((List args, _) { return IoniaCustomTipPage(getIt.get(param1: args)); }); @@ -1131,9 +1172,17 @@ Future setup({ ), ); - getIt.registerFactory(() => ManageNodesPage(getIt.get())); - getIt.registerFactory( - () => ManagePowNodesPage(getIt.get())); + getIt.registerFactoryParam((bool isPow, _) { + if (isPow) { + return ManageNodesPage(isPow, powNodeListViewModel: getIt.get()); + } + return ManageNodesPage(isPow, nodeListViewModel: getIt.get()); + }); + + getIt.registerFactory( + () => WalletConnectConnectionsView(web3walletService: getIt.get())); + + getIt.registerFactory(() => TorPage(getIt.get())); _isSetupFinished = true; } diff --git a/lib/entities/buy_provider_types.dart b/lib/entities/buy_provider_types.dart new file mode 100644 index 000000000..90c070e86 --- /dev/null +++ b/lib/entities/buy_provider_types.dart @@ -0,0 +1,19 @@ +import 'package:cake_wallet/generated/i18n.dart'; + +enum BuyProviderType { + AskEachTime, + Robinhood, + Onramper; + + @override + String toString() { + switch (this) { + case BuyProviderType.AskEachTime: + return S.current.ask_each_time; + case BuyProviderType.Robinhood: + return "Robinhood"; + case BuyProviderType.Onramper: + return "Onramper"; + } + } +} diff --git a/lib/entities/cake_2fa_preset_options.dart b/lib/entities/cake_2fa_preset_options.dart index 2aa6c4215..feb593fbe 100644 --- a/lib/entities/cake_2fa_preset_options.dart +++ b/lib/entities/cake_2fa_preset_options.dart @@ -6,6 +6,7 @@ class Cake2FAPresetsOptions extends EnumerableItem with Serializable { static const narrow = Cake2FAPresetsOptions(title: 'Narrow', raw: 0); static const normal = Cake2FAPresetsOptions(title: 'Normal', raw: 1); static const aggressive = Cake2FAPresetsOptions(title: 'Aggressive', raw: 2); + static const none = Cake2FAPresetsOptions(title: 'None', raw: 3); static Cake2FAPresetsOptions deserialize({required int raw}) { switch (raw) { @@ -15,6 +16,8 @@ class Cake2FAPresetsOptions extends EnumerableItem with Serializable { return Cake2FAPresetsOptions.normal; case 2: return Cake2FAPresetsOptions.aggressive; + case 3: + return Cake2FAPresetsOptions.none; default: throw Exception( 'Incorrect Cake 2FA Preset $raw for Cake2FAPresetOptions deserialize', @@ -30,6 +33,7 @@ enum VerboseControlSettings { sendsToNonContacts, sendsToInternalWallets, exchangesToInternalWallets, + exchangesToExternalWallets, securityAndBackupSettings, creatingNewWallets, } diff --git a/lib/entities/default_settings_migration.dart b/lib/entities/default_settings_migration.dart index 7d1859492..97757b93e 100644 --- a/lib/entities/default_settings_migration.dart +++ b/lib/entities/default_settings_migration.dart @@ -26,8 +26,9 @@ const cakeWalletBitcoinElectrumUri = 'electrum.cakewallet.com:50002'; const cakeWalletLitecoinElectrumUri = 'ltc-electrum.cakewallet.com:50002'; const havenDefaultNodeUri = 'nodes.havenprotocol.org:443'; const ethereumDefaultNodeUri = 'ethereum.publicnode.com'; -const nanoDefaultNodeUri = 'rpc.nano.to:443'; -const nanoDefaultPowNodeUri = 'rpc.nano.to:443'; +const cakeWalletBitcoinCashDefaultNodeUri = 'bitcoincash.stackwallet.com:50002'; +const nanoDefaultNodeUri = 'rpc.nano.to'; +const nanoDefaultPowNodeUri = 'rpc.nano.to'; Future defaultSettingsMigration( {required int version, @@ -52,6 +53,8 @@ Future defaultSettingsMigration( await sharedPreferences.setBool(PreferencesKey.isNewInstall, isNewInstall); + await sharedPreferences.setBool(PreferencesKey.isNewInstall, isNewInstall); + final currentVersion = sharedPreferences.getInt(PreferencesKey.currentDefaultSettingsMigrationVersion) ?? 0; @@ -81,7 +84,10 @@ Future defaultSettingsMigration( sharedPreferences: sharedPreferences, nodes: nodes); await changeLitecoinCurrentElectrumServerToDefault( sharedPreferences: sharedPreferences, nodes: nodes); - await changeHavenCurrentNodeToDefault(sharedPreferences: sharedPreferences, nodes: nodes); + await changeHavenCurrentNodeToDefault( + sharedPreferences: sharedPreferences, nodes: nodes); + await changeBitcoinCashCurrentNodeToDefault( + sharedPreferences: sharedPreferences, nodes: nodes); break; case 2: @@ -161,10 +167,15 @@ Future defaultSettingsMigration( break; case 22: await addNanoNodeList(nodes: nodes); + await addNanoPowNodeList(nodes: powNodes); await changeNanoCurrentNodeToDefault(sharedPreferences: sharedPreferences, nodes: nodes); await changeNanoCurrentPowNodeToDefault( sharedPreferences: sharedPreferences, nodes: powNodes); - await resetPowToDefault(powNodes); + break; + case 23: + await addBitcoinCashElectrumServerList(nodes: nodes); + await changeBitcoinCurrentElectrumServerToDefault( + sharedPreferences: sharedPreferences, nodes: nodes); break; default: @@ -182,63 +193,70 @@ Future defaultSettingsMigration( } Future _validateWalletInfoBoxData(Box walletInfoSource) async { - final root = await getApplicationDocumentsDirectory(); + try { + final root = await getApplicationDocumentsDirectory(); - for (var type in WalletType.values) { - if (type == WalletType.none) { - continue; - } - - String prefix = walletTypeToString(type).toLowerCase(); - Directory walletsDir = Directory('${root.path}/wallets/$prefix/'); - - if (!walletsDir.existsSync()) { - continue; - } - - List walletNames = walletsDir.listSync().map((e) => e.path.split("/").last).toList(); - - for (var name in walletNames) { - final dir = Directory(await pathForWalletDir(name: name, type: type)); - - final walletFiles = dir.listSync(); - final hasCacheFile = walletFiles.any((element) => element.path.contains("$name/$name")); - - if (!hasCacheFile) { + for (var type in WalletType.values) { + if (type == WalletType.none) { continue; } - if (type == WalletType.monero || type == WalletType.haven) { - final hasKeysFile = walletFiles.any((element) => element.path.contains(".keys")); + String prefix = walletTypeToString(type).toLowerCase(); + Directory walletsDir = Directory('${root.path}/wallets/$prefix/'); - if (!hasKeysFile) { + if (!walletsDir.existsSync()) { + continue; + } + + List walletNames = walletsDir.listSync().map((e) => e.path.split("/").last).toList(); + + for (var name in walletNames) { + final Directory dir; + try { + dir = Directory(await pathForWalletDir(name: name, type: type)); + } catch (_) { continue; } + + final walletFiles = dir.listSync(); + final hasCacheFile = walletFiles.any((element) => element.path.contains("$name/$name")); + + if (!hasCacheFile) { + continue; + } + + if (type == WalletType.monero || type == WalletType.haven) { + final hasKeysFile = walletFiles.any((element) => element.path.contains(".keys")); + + if (!hasKeysFile) { + continue; + } + } + + final id = prefix + '_' + name; + final exist = walletInfoSource.values.any((el) => el.id == id); + + if (exist) { + continue; + } + + final walletInfo = WalletInfo.external( + id: id, + type: type, + name: name, + isRecovery: true, + restoreHeight: 0, + date: DateTime.now(), + dirPath: dir.path, + path: '${dir.path}/$name', + address: '', + showIntroCakePayCard: false, + ); + + walletInfoSource.add(walletInfo); } - - final id = prefix + '_' + name; - final exist = walletInfoSource.values.any((el) => el.id == id); - - if (exist) { - continue; - } - - final walletInfo = WalletInfo.external( - id: id, - type: type, - name: name, - isRecovery: true, - restoreHeight: 0, - date: DateTime.now(), - dirPath: dir.path, - path: '${dir.path}/$name', - address: '', - showIntroCakePayCard: false, - ); - - walletInfoSource.add(walletInfo); } - } + } catch (_) {} } Future validateBitcoinSavedTransactionPriority(SharedPreferences sharedPreferences) async { @@ -323,6 +341,12 @@ Node? getNanoDefaultPowNode({required Box nodes}) { nodes.values.firstWhereOrNull((node) => (node.type == WalletType.nano)); } +Node? getBitcoinCashDefaultElectrumServer({required Box nodes}) { + return nodes.values.firstWhereOrNull( + (Node node) => node.uriRaw == cakeWalletBitcoinCashDefaultNodeUri) + ?? nodes.values.firstWhereOrNull((node) => node.type == WalletType.bitcoinCash); +} + Node getMoneroDefaultNode({required Box nodes}) { final timeZone = DateTime.now().timeZoneOffset.inHours; var nodeUri = ''; @@ -358,6 +382,15 @@ Future changeLitecoinCurrentElectrumServerToDefault( await sharedPreferences.setInt(PreferencesKey.currentLitecoinElectrumSererIdKey, serverId); } +Future changeBitcoinCashCurrentNodeToDefault( + {required SharedPreferences sharedPreferences, + required Box nodes}) async { + final server = getBitcoinCashDefaultElectrumServer(nodes: nodes); + final serverId = server?.key as int ?? 0; + + await sharedPreferences.setInt(PreferencesKey.currentBitcoinCashNodeIdKey, serverId); +} + Future changeHavenCurrentNodeToDefault( {required SharedPreferences sharedPreferences, required Box nodes}) async { final node = getHavenDefaultNode(nodes: nodes); @@ -411,6 +444,15 @@ Future addLitecoinElectrumServerList({required Box nodes}) async { } } +Future addBitcoinCashElectrumServerList({required Box nodes}) async { + final serverList = await loadBitcoinCashElectrumServerList(); + for (var node in serverList) { + if (nodes.values.firstWhereOrNull((element) => element.uriRaw == node.uriRaw) == null) { + await nodes.add(node); + } + } +} + Future addHavenNodeList({required Box nodes}) async { final nodeList = await loadDefaultHavenNodes(); for (var node in nodeList) { @@ -497,27 +539,34 @@ Future checkCurrentNodes( final currentMoneroNodeId = sharedPreferences.getInt(PreferencesKey.currentNodeIdKey); final currentBitcoinElectrumSeverId = sharedPreferences.getInt(PreferencesKey.currentBitcoinElectrumSererIdKey); - final currentLitecoinElectrumSeverId = - sharedPreferences.getInt(PreferencesKey.currentLitecoinElectrumSererIdKey); - final currentHavenNodeId = sharedPreferences.getInt(PreferencesKey.currentHavenNodeIdKey); - final currentEthereumNodeId = sharedPreferences.getInt(PreferencesKey.currentEthereumNodeIdKey); - final currentNanoNodeId = sharedPreferences.getInt(PreferencesKey.currentNanoNodeIdKey); - final currentNanoPowNodeId = sharedPreferences.getInt(PreferencesKey.currentNanoPowNodeIdKey); - final currentMoneroNode = - nodeSource.values.firstWhereOrNull((node) => node.key == currentMoneroNodeId); - final currentBitcoinElectrumServer = - nodeSource.values.firstWhereOrNull((node) => node.key == currentBitcoinElectrumSeverId); - final currentLitecoinElectrumServer = - nodeSource.values.firstWhereOrNull((node) => node.key == currentLitecoinElectrumSeverId); - final currentHavenNodeServer = - nodeSource.values.firstWhereOrNull((node) => node.key == currentHavenNodeId); - final currentEthereumNodeServer = - nodeSource.values.firstWhereOrNull((node) => node.key == currentEthereumNodeId); + final currentLitecoinElectrumSeverId = sharedPreferences + .getInt(PreferencesKey.currentLitecoinElectrumSererIdKey); + final currentHavenNodeId = sharedPreferences + .getInt(PreferencesKey.currentHavenNodeIdKey); + final currentEthereumNodeId = sharedPreferences + .getInt(PreferencesKey.currentEthereumNodeIdKey); + final currentNanoNodeId = sharedPreferences + .getInt(PreferencesKey.currentNanoNodeIdKey); + final currentNanoPowNodeId = sharedPreferences + .getInt(PreferencesKey.currentNanoPowNodeIdKey); + final currentBitcoinCashNodeId = sharedPreferences + .getInt(PreferencesKey.currentBitcoinCashNodeIdKey); + final currentMoneroNode = nodeSource.values.firstWhereOrNull( + (node) => node.key == currentMoneroNodeId); + final currentBitcoinElectrumServer = nodeSource.values.firstWhereOrNull( + (node) => node.key == currentBitcoinElectrumSeverId); + final currentLitecoinElectrumServer = nodeSource.values.firstWhereOrNull( + (node) => node.key == currentLitecoinElectrumSeverId); + final currentHavenNodeServer = nodeSource.values.firstWhereOrNull( + (node) => node.key == currentHavenNodeId); + final currentEthereumNodeServer = nodeSource.values.firstWhereOrNull( + (node) => node.key == currentEthereumNodeId); final currentNanoNodeServer = - nodeSource.values.firstWhereOrNull((node) => node.key == currentNanoNodeId); + nodeSource.values.firstWhereOrNull((node) => node.key == currentNanoNodeId); final currentNanoPowNodeServer = - nodeSource.values.firstWhereOrNull((node) => node.key == currentNanoPowNodeId); - + powNodeSource.values.firstWhereOrNull((node) => node.key == currentNanoPowNodeId); + final currentBitcoinCashNodeServer = nodeSource.values.firstWhereOrNull( + (node) => node.key == currentBitcoinCashNodeId); if (currentMoneroNode == null) { final newCakeWalletNode = Node(uri: newCakeWalletMoneroUri, type: WalletType.monero); await nodeSource.add(newCakeWalletNode); @@ -551,7 +600,7 @@ Future checkCurrentNodes( } if (currentNanoNodeServer == null) { - final node = Node(uri: nanoDefaultNodeUri, type: WalletType.nano); + final node = Node(uri: nanoDefaultNodeUri, useSSL: true, type: WalletType.nano); await nodeSource.add(node); await sharedPreferences.setInt(PreferencesKey.currentNanoNodeIdKey, node.key as int); } @@ -560,11 +609,18 @@ Future checkCurrentNodes( Node? node = powNodeSource.values .firstWhereOrNull((node) => node.uri.toString() == nanoDefaultPowNodeUri); if (node == null) { - node = Node(uri: nanoDefaultPowNodeUri, type: WalletType.nano); + node = Node(uri: nanoDefaultPowNodeUri, useSSL: true, type: WalletType.nano); await powNodeSource.add(node); } await sharedPreferences.setInt(PreferencesKey.currentNanoPowNodeIdKey, node.key as int); } + + if (currentBitcoinCashNodeServer == null) { + final node = Node(uri: cakeWalletBitcoinCashDefaultNodeUri, type: WalletType.bitcoinCash); + await nodeSource.add(node); + await sharedPreferences.setInt( + PreferencesKey.currentBitcoinCashNodeIdKey, node.key as int); + } } Future resetBitcoinElectrumServer( @@ -636,6 +692,15 @@ Future addNanoNodeList({required Box nodes}) async { } } +Future addNanoPowNodeList({required Box nodes}) async { + final nodeList = await loadDefaultNanoPowNodes(); + for (var node in nodeList) { + if (nodes.values.firstWhereOrNull((element) => element.uriRaw == node.uriRaw) == null) { + await nodes.add(node); + } + } +} + Future changeNanoCurrentNodeToDefault( {required SharedPreferences sharedPreferences, required Box nodes}) async { final node = getNanoDefaultNode(nodes: nodes); diff --git a/lib/entities/encrypt.dart b/lib/entities/encrypt.dart index 3e644a2b7..891f7a92d 100644 --- a/lib/entities/encrypt.dart +++ b/lib/entities/encrypt.dart @@ -2,18 +2,18 @@ import 'package:encrypt/encrypt.dart'; // import 'package:password/password.dart'; import 'package:cake_wallet/.secrets.g.dart' as secrets; -String encrypt({required String source, required String key, int keyLength = 16}) { +String encrypt({required String source, required String key}) { final _key = Key.fromUtf8(key); - final iv = IV.fromLength(keyLength); + final iv = IV.allZerosOfLength(16); final encrypter = Encrypter(AES(_key)); final encrypted = encrypter.encrypt(source, iv: iv); return encrypted.base64; } -String decrypt({required String source, required String key, int keyLength = 16}) { +String decrypt({required String source, required String key}) { final _key = Key.fromUtf8(key); - final iv = IV.fromLength(keyLength); + final iv = IV.allZerosOfLength(16); final encrypter = Encrypter(AES(_key)); final decrypted = encrypter.decrypt64(source, iv: iv); diff --git a/lib/entities/ens_record.dart b/lib/entities/ens_record.dart new file mode 100644 index 000000000..8cf62d79b --- /dev/null +++ b/lib/entities/ens_record.dart @@ -0,0 +1,46 @@ +import 'package:cake_wallet/ethereum/ethereum.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:ens_dart/ens_dart.dart'; +import 'package:http/http.dart'; +import 'package:web3dart/web3dart.dart'; + +class EnsRecord { + static Future fetchEnsAddress(String name, {WalletBase? wallet}) async { + Web3Client? _client; + + if (wallet != null && wallet.type == WalletType.ethereum) { + _client = ethereum!.getWeb3Client(wallet); + } + + if (_client == null) { + _client = Web3Client("https://ethereum.publicnode.com", Client()); + } + + try { + final ens = Ens(client: _client); + + if (wallet != null) { + switch (wallet.type) { + case WalletType.monero: + return await ens.withName(name).getCoinAddress(CoinType.XMR); + case WalletType.bitcoin: + return await ens.withName(name).getCoinAddress(CoinType.BTC); + case WalletType.litecoin: + return await ens.withName(name).getCoinAddress(CoinType.LTC); + case WalletType.haven: + return await ens.withName(name).getCoinAddress(CoinType.XHV); + case WalletType.ethereum: + default: + return (await ens.withName(name).getAddress()).hex; + } + } + + final addr = await ens.withName(name).getAddress(); + return addr.hex; + } catch (e) { + print(e); + return ""; + } + } +} diff --git a/lib/entities/language_service.dart b/lib/entities/language_service.dart index 87ee99ff5..cfb850889 100644 --- a/lib/entities/language_service.dart +++ b/lib/entities/language_service.dart @@ -28,7 +28,8 @@ class LanguageService { 'ur': 'اردو (Urdu)', 'id': 'Bahasa Indonesia (Indonesian)', 'yo': 'Yorùbá (Yoruba)', - 'ha': 'Hausa Najeriya (Nigeria)' + 'ha': 'Hausa Najeriya (Nigeria)', + 'tl': 'Filipino (Tagalog)' }; static const Map localeCountryCode = { @@ -56,7 +57,8 @@ class LanguageService { 'ur': 'pak', 'id': 'idn', 'yo': 'nga', - 'ha': 'hau' + 'ha': 'hau', + 'tl': 'phl' }; static final list = {}; diff --git a/lib/entities/main_actions.dart b/lib/entities/main_actions.dart index 276b1ff2a..46865cbcc 100644 --- a/lib/entities/main_actions.dart +++ b/lib/entities/main_actions.dart @@ -1,6 +1,8 @@ import 'package:cake_wallet/buy/moonpay/moonpay_buy_provider.dart'; import 'package:cake_wallet/buy/onramper/onramper_buy_provider.dart'; +import 'package:cake_wallet/buy/robinhood/robinhood_buy_provider.dart'; import 'package:cake_wallet/di.dart'; +import 'package:cake_wallet/entities/buy_provider_types.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; @@ -41,24 +43,32 @@ class MainActions { isEnabled: (viewModel) => viewModel.isEnabledBuyAction, canShow: (viewModel) => viewModel.hasBuyAction, onTap: (BuildContext context, DashboardViewModel viewModel) async { + final defaultBuyProvider = viewModel.defaultBuyProvider; final walletType = viewModel.type; + if (!viewModel.isEnabledBuyAction) return; + switch (walletType) { case WalletType.bitcoin: case WalletType.litecoin: case WalletType.ethereum: + case WalletType.bitcoinCash: + switch (defaultBuyProvider) { + case BuyProviderType.AskEachTime: + Navigator.pushNamed(context, Routes.buy); + break; + case BuyProviderType.Onramper: + await getIt.get().launchProvider(context); + break; + case BuyProviderType.Robinhood: + await getIt.get().launchProvider(context); + break; + } + break; case WalletType.nano: case WalletType.banano: case WalletType.monero: - if (viewModel.isEnabledBuyAction) { - final uri = getIt.get().requestUrl(context); - if (DeviceInfo.instance.isMobile) { - Navigator.of(context) - .pushNamed(Routes.webViewPage, arguments: [S.of(context).buy, uri]); - } else { - await launchUrl(uri); - } - } + await getIt.get().launchProvider(context); break; default: await showPopUp( @@ -114,6 +124,7 @@ class MainActions { case WalletType.bitcoin: case WalletType.litecoin: case WalletType.ethereum: + case WalletType.bitcoinCash: if (viewModel.isEnabledSellAction) { final moonPaySellProvider = MoonPaySellProvider(); final uri = await moonPaySellProvider.requestUrl( diff --git a/lib/entities/node_list.dart b/lib/entities/node_list.dart index 0641c0846..53facf18c 100644 --- a/lib/entities/node_list.dart +++ b/lib/entities/node_list.dart @@ -84,6 +84,23 @@ Future> loadDefaultEthereumNodes() async { return nodes; } +Future> loadBitcoinCashElectrumServerList() async { + final serverListRaw = + await rootBundle.loadString('assets/bitcoin_cash_electrum_server_list.yml'); + final loadedServerList = loadYaml(serverListRaw) as YamlList; + final serverList = []; + + for (final raw in loadedServerList) { + if (raw is Map) { + final node = Node.fromMap(Map.from(raw)); + node.type = WalletType.bitcoinCash; + serverList.add(node); + } + } + + return serverList; +} + Future> loadDefaultNanoNodes() async { final nodesRaw = await rootBundle.loadString('assets/nano_node_list.yml'); final loadedNodes = loadYaml(nodesRaw) as YamlList; @@ -116,10 +133,11 @@ Future> loadDefaultNanoPowNodes() async { return nodes; } -Future resetToDefault(Box nodeSource) async { +Future resetToDefault(Box nodeSource) async { final moneroNodes = await loadDefaultNodes(); final bitcoinElectrumServerList = await loadBitcoinElectrumServerList(); final litecoinElectrumServerList = await loadLitecoinElectrumServerList(); + final bitcoinCashElectrumServerList = await loadBitcoinCashElectrumServerList(); final havenNodes = await loadDefaultHavenNodes(); final ethereumNodes = await loadDefaultEthereumNodes(); final nanoNodes = await loadDefaultNanoNodes(); @@ -129,13 +147,14 @@ Future resetToDefault(Box nodeSource) async { litecoinElectrumServerList + havenNodes + ethereumNodes + + bitcoinCashElectrumServerList + nanoNodes; await nodeSource.clear(); await nodeSource.addAll(nodes); } -Future resetPowToDefault(Box powNodeSource) async { +Future resetPowToDefault(Box powNodeSource) async { final nanoPowNodes = await loadDefaultNanoPowNodes(); final nodes = nanoPowNodes; await powNodeSource.clear(); diff --git a/lib/entities/parse_address_from_domain.dart b/lib/entities/parse_address_from_domain.dart index c0eab6d65..e184f5649 100644 --- a/lib/entities/parse_address_from_domain.dart +++ b/lib/entities/parse_address_from_domain.dart @@ -1,19 +1,26 @@ import 'package:cake_wallet/core/address_validator.dart'; import 'package:cake_wallet/core/yat_service.dart'; +import 'package:cake_wallet/entities/ens_record.dart'; import 'package:cake_wallet/entities/openalias_record.dart'; import 'package:cake_wallet/entities/parsed_address.dart'; import 'package:cake_wallet/entities/unstoppable_domain_address.dart'; import 'package:cake_wallet/entities/emoji_string_extension.dart'; +import 'package:cake_wallet/mastodon/mastodon_api.dart'; +import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/twitter/twitter_api.dart'; import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:cake_wallet/entities/fio_address_provider.dart'; class AddressResolver { - AddressResolver({required this.yatService, required this.walletType}); + AddressResolver({required this.yatService, required this.wallet, required this.settingsStore}) + : walletType = wallet.type; final YatService yatService; final WalletType walletType; + final WalletBase wallet; + final SettingsStore settingsStore; static const unstoppableDomains = [ 'crypto', @@ -42,34 +49,73 @@ class AddressResolver { } final match = RegExp(addressPattern).firstMatch(raw); - return match?.group(0)?.replaceAll(RegExp('[^0-9a-zA-Z]'), ''); + return match?.group(0)?.replaceAllMapped(RegExp('[^0-9a-zA-Z]|bitcoincash:|nano_'), (Match match) { + String group = match.group(0)!; + if (group.startsWith('bitcoincash:') || group.startsWith('nano_')) { + return group; + } + return ''; + }); } Future resolve(String text, String ticker) async { try { if (text.startsWith('@') && !text.substring(1).contains('@')) { - final formattedName = text.substring(1); - final twitterUser = await TwitterApi.lookupUserByName(userName: formattedName); - final addressFromBio = extractAddressByType( - raw: twitterUser.description, type: CryptoCurrency.fromString(ticker)); - if (addressFromBio != null) { - return ParsedAddress.fetchTwitterAddress(address: addressFromBio, name: text); - } - final tweets = twitterUser.tweets; - if (tweets != null) { - var subString = StringBuffer(); - tweets.forEach((item) { - subString.writeln(item.text); - }); - final userTweetsText = subString.toString(); - final addressFromPinnedTweet = - extractAddressByType(raw: userTweetsText, type: CryptoCurrency.fromString(ticker)); + if(settingsStore.lookupsTwitter) { + final formattedName = text.substring(1); + final twitterUser = await TwitterApi.lookupUserByName(userName: formattedName); + final addressFromBio = extractAddressByType( + raw: twitterUser.description, type: CryptoCurrency.fromString(ticker)); + if (addressFromBio != null) { + return ParsedAddress.fetchTwitterAddress(address: addressFromBio, name: text); + } - if (addressFromPinnedTweet != null) { - return ParsedAddress.fetchTwitterAddress(address: addressFromPinnedTweet, name: text); + final pinnedTweet = twitterUser.pinnedTweet?.text; + if (pinnedTweet != null) { + final addressFromPinnedTweet = + extractAddressByType(raw: pinnedTweet, type: CryptoCurrency.fromString(ticker)); + if (addressFromPinnedTweet != null) { + return ParsedAddress.fetchTwitterAddress(address: addressFromPinnedTweet, name: text); + } } } } + + if (text.startsWith('@') && text.contains('@', 1) && text.contains('.', 1)) { + if (settingsStore.lookupsMastodon) { + final subText = text.substring(1); + final hostNameIndex = subText.indexOf('@'); + final hostName = subText.substring(hostNameIndex + 1); + final userName = subText.substring(0, hostNameIndex); + + final mastodonUser = + await MastodonAPI.lookupUserByUserName(userName: userName, apiHost: hostName); + + if (mastodonUser != null) { + String? addressFromBio = + extractAddressByType(raw: mastodonUser.note, type: CryptoCurrency.fromString(ticker)); + + if (addressFromBio != null) { + return ParsedAddress.fetchMastodonAddress(address: addressFromBio, name: text); + } else { + final pinnedPosts = + await MastodonAPI.getPinnedPosts(userId: mastodonUser.id, apiHost: hostName); + + if (pinnedPosts.isNotEmpty) { + final userPinnedPostsText = pinnedPosts.map((item) => item.content).join('\n'); + String? addressFromPinnedPost = extractAddressByType( + raw: userPinnedPostsText, type: CryptoCurrency.fromString(ticker)); + + if (addressFromPinnedPost != null) { + return ParsedAddress.fetchMastodonAddress( + address: addressFromPinnedPost, name: text); + } + } + } + } + } + } + if (!text.startsWith('@') && text.contains('@') && !text.contains('.')) { final bool isFioRegistered = await FioAddressProvider.checkAvail(text); if (isFioRegistered) { @@ -78,9 +124,11 @@ class AddressResolver { } } if (text.hasOnlyEmojis) { - if (walletType != WalletType.haven) { - final addresses = await yatService.fetchYatAddress(text, ticker); - return ParsedAddress.fetchEmojiAddress(addresses: addresses, name: text); + if(settingsStore.lookupsYatService) { + if (walletType != WalletType.haven) { + final addresses = await yatService.fetchYatAddress(text, ticker); + return ParsedAddress.fetchEmojiAddress(addresses: addresses, name: text); + } } } final formattedName = OpenaliasRecord.formatDomainName(text); @@ -92,16 +140,29 @@ class AddressResolver { } if (unstoppableDomains.any((domain) => name.trim() == domain)) { - final address = await fetchUnstoppableDomainAddress(text, ticker); - return ParsedAddress.fetchUnstoppableDomainAddress(address: address, name: text); + if(settingsStore.lookupsUnstoppableDomains) { + final address = await fetchUnstoppableDomainAddress(text, ticker); + return ParsedAddress.fetchUnstoppableDomainAddress(address: address, name: text); + } + } + + if (text.endsWith(".eth")) { + if (settingsStore.lookupsENS) { + final address = await EnsRecord.fetchEnsAddress(text, wallet: wallet); + if (address.isNotEmpty && address != "0x0000000000000000000000000000000000000000") { + return ParsedAddress.fetchEnsAddress(name: text, address: address); + } + } } if (formattedName.contains(".")) { - final txtRecord = await OpenaliasRecord.lookupOpenAliasRecord(formattedName); - if (txtRecord != null) { - final record = await OpenaliasRecord.fetchAddressAndName( - formattedName: formattedName, ticker: ticker, txtRecord: txtRecord); - return ParsedAddress.fetchOpenAliasAddress(record: record, name: text); + if(settingsStore.lookupsOpenAlias) { + final txtRecord = await OpenaliasRecord.lookupOpenAliasRecord(formattedName); + if (txtRecord != null) { + final record = await OpenaliasRecord.fetchAddressAndName( + formattedName: formattedName, ticker: ticker, txtRecord: txtRecord); + return ParsedAddress.fetchOpenAliasAddress(record: record, name: text); + } } } } catch (e) { diff --git a/lib/entities/parsed_address.dart b/lib/entities/parsed_address.dart index 67caebcb5..df20dd9ee 100644 --- a/lib/entities/parsed_address.dart +++ b/lib/entities/parsed_address.dart @@ -1,7 +1,8 @@ import 'package:cake_wallet/entities/openalias_record.dart'; import 'package:cake_wallet/entities/yat_record.dart'; -enum ParseFrom { unstoppableDomains, openAlias, yatRecord, fio, notParsed, twitter, contact } + +enum ParseFrom { unstoppableDomains, openAlias, yatRecord, fio, notParsed, twitter, ens, contact, mastodon } class ParsedAddress { ParsedAddress({ @@ -69,6 +70,14 @@ class ParsedAddress { ); } + factory ParsedAddress.fetchMastodonAddress({required String address, required String name}){ + return ParsedAddress( + addresses: [address], + name: name, + parseFrom: ParseFrom.mastodon + ); + } + factory ParsedAddress.fetchContactAddress({required String address, required String name}){ return ParsedAddress( addresses: [address], @@ -77,8 +86,17 @@ class ParsedAddress { ); } + factory ParsedAddress.fetchEnsAddress({required String address, required String name}) { + return ParsedAddress( + addresses: [address], + name: name, + parseFrom: ParseFrom.ens, + ); + } + final List addresses; final String name; final String description; final ParseFrom parseFrom; + } diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index aa1ebe6ce..85df63b69 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -11,18 +11,18 @@ class PreferencesKey { static const currentBananoNodeIdKey = 'current_node_id_banano'; static const currentBananoPowNodeIdKey = 'current_node_id_banano_pow'; static const currentFiatCurrencyKey = 'current_fiat_currency'; + static const currentBitcoinCashNodeIdKey = 'current_node_id_bch'; static const currentTransactionPriorityKeyLegacy = 'current_fee_priority'; static const currentBalanceDisplayModeKey = 'current_balance_display_mode'; static const shouldSaveRecipientAddressKey = 'save_recipient_address'; static const isAppSecureKey = 'is_app_secure'; static const disableBuyKey = 'disable_buy'; static const disableSellKey = 'disable_sell'; + static const defaultBuyProvider = 'default_buy_provider'; static const currentFiatApiModeKey = 'current_fiat_api_mode'; - static const allowBiometricalAuthenticationKey = - 'allow_biometrical_authentication'; + static const allowBiometricalAuthenticationKey = 'allow_biometrical_authentication'; static const useTOTP2FA = 'use_totp_2fa'; static const failedTotpTokenTrials = 'failed_token_trials'; - static const totpSecretKey = 'totp_qr_secret_key'; static const disableExchangeKey = 'disable_exchange'; static const exchangeStatusKey = 'exchange_status'; static const currentTheme = 'current_theme'; @@ -37,6 +37,7 @@ class PreferencesKey { static const havenTransactionPriority = 'current_fee_priority_haven'; static const litecoinTransactionPriority = 'current_fee_priority_litecoin'; static const ethereumTransactionPriority = 'current_fee_priority_ethereum'; + static const bitcoinCashTransactionPriority = 'current_fee_priority_bitcoin_cash'; static const shouldShowReceiveWarning = 'should_show_receive_warning'; static const shouldShowYatPopup = 'should_show_yat_popup'; static const moneroWalletPasswordUpdateV1Base = 'monero_wallet_update_v1'; @@ -49,6 +50,14 @@ class PreferencesKey { static const sortBalanceBy = 'sort_balance_by'; static const pinNativeTokenAtTop = 'pin_native_token_at_top'; static const useEtherscan = 'use_etherscan'; + static const defaultNanoRep = 'default_nano_representative'; + static const defaultBananoRep = 'default_banano_representative'; + static const lookupsTwitter = 'looks_up_twitter'; + static const lookupsMastodon = 'looks_up_mastodon'; + static const lookupsYatService = 'looks_up_mastodon'; + static const lookupsUnstoppableDomains = 'looks_up_mastodon'; + static const lookupsOpenAlias = 'looks_up_mastodon'; + static const lookupsENS = 'looks_up_ens'; static String moneroWalletUpdateV1Key(String name) => '${PreferencesKey.moneroWalletPasswordUpdateV1Base}_${name}'; @@ -58,8 +67,7 @@ class PreferencesKey { static const clearnetDonationLink = 'clearnet_donation_link'; static const onionDonationLink = 'onion_donation_link'; static const lastSeenAppVersion = 'last_seen_app_version'; - static const shouldShowMarketPlaceInDashboard = - 'should_show_marketplace_in_dashboard'; + static const shouldShowMarketPlaceInDashboard = 'should_show_marketplace_in_dashboard'; static const isNewInstall = 'is_new_install'; static const shouldRequireTOTP2FAForAccessingWallet = 'should_require_totp_2fa_for_accessing_wallets'; @@ -71,6 +79,8 @@ class PreferencesKey { 'should_require_totp_2fa_for_sends_to_internal_wallets'; static const shouldRequireTOTP2FAForExchangesToInternalWallets = 'should_require_totp_2fa_for_exchanges_to_internal_wallets'; + static const shouldRequireTOTP2FAForExchangesToExternalWallets = + 'should_require_totp_2fa_for_exchanges_to_external_wallets'; static const shouldRequireTOTP2FAForAddingContacts = 'should_require_totp_2fa_for_adding_contacts'; static const shouldRequireTOTP2FAForCreatingNewWallets = @@ -78,4 +88,6 @@ class PreferencesKey { static const shouldRequireTOTP2FAForAllSecurityAndBackupSettings = 'should_require_totp_2fa_for_all_security_and_backup_settings'; static const selectedCake2FAPreset = 'selected_cake_2fa_preset'; + static const totpSecretKey = 'totp_secret_key'; + static const currentSeedPhraseLength = 'current_seed_phrase_length'; } diff --git a/lib/entities/priority_for_wallet_type.dart b/lib/entities/priority_for_wallet_type.dart index 378cf0ea2..bf6f8157d 100644 --- a/lib/entities/priority_for_wallet_type.dart +++ b/lib/entities/priority_for_wallet_type.dart @@ -1,4 +1,5 @@ import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart'; import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/haven/haven.dart'; import 'package:cake_wallet/monero/monero.dart'; @@ -17,6 +18,8 @@ List priorityForWalletType(WalletType type) { return haven!.getTransactionPriorities(); case WalletType.ethereum: return ethereum!.getTransactionPriorities(); + case WalletType.bitcoinCash: + return bitcoinCash!.getTransactionPriorities(); // no such thing for nano/banano: case WalletType.nano: case WalletType.banano: diff --git a/lib/entities/seed_phrase_length.dart b/lib/entities/seed_phrase_length.dart new file mode 100644 index 000000000..65e9fac40 --- /dev/null +++ b/lib/entities/seed_phrase_length.dart @@ -0,0 +1,26 @@ +import 'package:cake_wallet/generated/i18n.dart'; + +enum SeedPhraseLength { + twelveWords(12), + twentyFourWords(24); + + const SeedPhraseLength(this.value); + final int value; + + static SeedPhraseLength deserialize({required int raw}) => + SeedPhraseLength.values.firstWhere((e) => e.value == raw); + + @override + String toString() { + String label = ''; + switch (this) { + case SeedPhraseLength.twelveWords: + label = '12 Words'; + break; + case SeedPhraseLength.twentyFourWords: + label = '24 Words'; + break; + } + return label; + } +} \ No newline at end of file diff --git a/lib/ethereum/cw_ethereum.dart b/lib/ethereum/cw_ethereum.dart index 349d14647..abafc2f26 100644 --- a/lib/ethereum/cw_ethereum.dart +++ b/lib/ethereum/cw_ethereum.dart @@ -33,9 +33,26 @@ class CWEthereum extends Ethereum { @override String getAddress(WalletBase wallet) => (wallet as EthereumWallet).walletAddresses.address; + @override + String getPrivateKey(WalletBase wallet) { + final privateKeyHolder = (wallet as EthereumWallet).ethPrivateKey; + String stringKey = bytesToHex(privateKeyHolder.privateKey); + return stringKey; + } + + @override + String getPublicKey(WalletBase wallet) { + final privateKeyInUnitInt = (wallet as EthereumWallet).ethPrivateKey; + final publicKey = privateKeyInUnitInt.address.hex; + return publicKey; + } + @override TransactionPriority getDefaultTransactionPriority() => EthereumTransactionPriority.medium; + @override + TransactionPriority getEthereumTransactionPrioritySlow() => EthereumTransactionPriority.slow; + @override List getTransactionPriorities() => EthereumTransactionPriority.all; @@ -131,4 +148,9 @@ class CWEthereum extends Ethereum { void updateEtherscanUsageState(WalletBase wallet, bool isEnabled) { (wallet as EthereumWallet).updateEtherscanUsageState(isEnabled); } -} \ No newline at end of file + + @override + Web3Client? getWeb3Client(WalletBase wallet) { + return (wallet as EthereumWallet).getWeb3Client(); + } +} diff --git a/lib/exchange/changenow/changenow_request.dart b/lib/exchange/changenow/changenow_request.dart deleted file mode 100644 index 7806dff55..000000000 --- a/lib/exchange/changenow/changenow_request.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:cw_core/crypto_currency.dart'; -import 'package:cake_wallet/exchange/trade_request.dart'; - -class ChangeNowRequest extends TradeRequest { - ChangeNowRequest( - {required this.from, - required this.to, - required this.address, - required this.fromAmount, - required this.toAmount, - required this.refundAddress, - required this.isReverse}); - - CryptoCurrency from; - CryptoCurrency to; - String address; - String fromAmount; - String toAmount; - String refundAddress; - bool isReverse; -} diff --git a/lib/exchange/exchange_pair.dart b/lib/exchange/exchange_pair.dart index bb43498b1..686de21ee 100644 --- a/lib/exchange/exchange_pair.dart +++ b/lib/exchange/exchange_pair.dart @@ -1,10 +1,7 @@ import 'package:cw_core/crypto_currency.dart'; class ExchangePair { - ExchangePair({ - required this.from, - required this.to, - this.reverse = true}); + ExchangePair({required this.from, required this.to, this.reverse = true}); final CryptoCurrency from; final CryptoCurrency to; diff --git a/lib/exchange/exchange_provider_description.dart b/lib/exchange/exchange_provider_description.dart index e545f69ce..abfac3a6b 100644 --- a/lib/exchange/exchange_provider_description.dart +++ b/lib/exchange/exchange_provider_description.dart @@ -14,17 +14,16 @@ class ExchangeProviderDescription extends EnumerableItem with Serializable< ExchangeProviderDescription(title: 'ChangeNOW', raw: 1, image: 'assets/images/changenow.png'); static const morphToken = ExchangeProviderDescription(title: 'MorphToken', raw: 2, image: 'assets/images/morph.png'); - static const sideShift = ExchangeProviderDescription(title: 'SideShift', raw: 3, image: 'assets/images/sideshift.png'); - static const simpleSwap = ExchangeProviderDescription( title: 'SimpleSwap', raw: 4, image: 'assets/images/simpleSwap.png'); - static const trocador = ExchangeProviderDescription(title: 'Trocador', raw: 5, image: 'assets/images/trocador.png'); + static const exolix = + ExchangeProviderDescription(title: 'Exolix', raw: 6, image: 'assets/images/exolix.png'); - static const all = ExchangeProviderDescription(title: 'All trades', raw: 6, image: ''); + static const all = ExchangeProviderDescription(title: 'All trades', raw: 7, image: ''); static ExchangeProviderDescription deserialize({required int raw}) { switch (raw) { @@ -41,6 +40,8 @@ class ExchangeProviderDescription extends EnumerableItem with Serializable< case 5: return trocador; case 6: + return exolix; + case 7: return all; default: throw Exception('Unexpected token: $raw for ExchangeProviderDescription deserialize'); diff --git a/lib/exchange/exchange_trade_state.dart b/lib/exchange/exchange_trade_state.dart index d2f2840bc..44f66a33d 100644 --- a/lib/exchange/exchange_trade_state.dart +++ b/lib/exchange/exchange_trade_state.dart @@ -1,4 +1,3 @@ -import 'package:flutter/foundation.dart'; import 'package:cake_wallet/exchange/trade.dart'; abstract class ExchangeTradeState {} @@ -18,4 +17,4 @@ class TradeIsCreatedFailure extends ExchangeTradeState { final String title; final String error; -} \ No newline at end of file +} diff --git a/lib/exchange/limits.dart b/lib/exchange/limits.dart index 7e077c3cd..4b76f0206 100644 --- a/lib/exchange/limits.dart +++ b/lib/exchange/limits.dart @@ -1,6 +1,6 @@ class Limits { const Limits({this.min, this.max}); - + final double? min; final double? max; -} \ No newline at end of file +} diff --git a/lib/exchange/limits_state.dart b/lib/exchange/limits_state.dart index 1a551e7ee..993933d88 100644 --- a/lib/exchange/limits_state.dart +++ b/lib/exchange/limits_state.dart @@ -1,4 +1,3 @@ -import 'package:flutter/foundation.dart'; import 'package:cake_wallet/exchange/limits.dart'; abstract class LimitsState {} diff --git a/lib/exchange/morphtoken/morphtoken_exchange_provider.dart b/lib/exchange/morphtoken/morphtoken_exchange_provider.dart deleted file mode 100644 index a2a72b24f..000000000 --- a/lib/exchange/morphtoken/morphtoken_exchange_provider.dart +++ /dev/null @@ -1,225 +0,0 @@ -import 'dart:convert'; -import 'package:cw_core/amount_converter.dart'; -import 'package:hive/hive.dart'; -import 'package:cake_wallet/exchange/trade_not_found_exeption.dart'; -import 'package:flutter/foundation.dart'; -import 'package:http/http.dart'; -import 'package:cw_core/crypto_currency.dart'; -import 'package:cake_wallet/exchange/exchange_pair.dart'; -import 'package:cake_wallet/exchange/exchange_provider.dart'; -import 'package:cake_wallet/exchange/limits.dart'; -import 'package:cake_wallet/exchange/trade.dart'; -import 'package:cake_wallet/exchange/trade_request.dart'; -import 'package:cake_wallet/exchange/trade_state.dart'; -import 'package:cake_wallet/exchange/morphtoken/morphtoken_request.dart'; -import 'package:cake_wallet/exchange/exchange_provider_description.dart'; -import 'package:cake_wallet/exchange/trade_not_created_exeption.dart'; - -class MorphTokenExchangeProvider extends ExchangeProvider { - MorphTokenExchangeProvider({required this.trades}) - : super(pairList: [ - ExchangePair(from: CryptoCurrency.xmr, to: CryptoCurrency.eth), - ExchangePair(from: CryptoCurrency.xmr, to: CryptoCurrency.bch), - ExchangePair(from: CryptoCurrency.xmr, to: CryptoCurrency.ltc), - ExchangePair(from: CryptoCurrency.xmr, to: CryptoCurrency.dash), - ExchangePair(from: CryptoCurrency.dash, to: CryptoCurrency.btc), - ExchangePair(from: CryptoCurrency.dash, to: CryptoCurrency.eth), - ExchangePair(from: CryptoCurrency.dash, to: CryptoCurrency.bch), - ExchangePair(from: CryptoCurrency.dash, to: CryptoCurrency.ltc), - ExchangePair(from: CryptoCurrency.dash, to: CryptoCurrency.xmr), - ExchangePair(from: CryptoCurrency.ltc, to: CryptoCurrency.btc), - ExchangePair(from: CryptoCurrency.ltc, to: CryptoCurrency.eth), - ExchangePair(from: CryptoCurrency.ltc, to: CryptoCurrency.bch), - ExchangePair(from: CryptoCurrency.ltc, to: CryptoCurrency.dash), - ExchangePair(from: CryptoCurrency.ltc, to: CryptoCurrency.xmr), - ExchangePair(from: CryptoCurrency.bch, to: CryptoCurrency.btc), - ExchangePair(from: CryptoCurrency.bch, to: CryptoCurrency.eth), - ExchangePair(from: CryptoCurrency.bch, to: CryptoCurrency.ltc), - ExchangePair(from: CryptoCurrency.bch, to: CryptoCurrency.dash), - ExchangePair(from: CryptoCurrency.bch, to: CryptoCurrency.xmr), - ExchangePair(from: CryptoCurrency.eth, to: CryptoCurrency.btc), - ExchangePair(from: CryptoCurrency.eth, to: CryptoCurrency.bch), - ExchangePair(from: CryptoCurrency.eth, to: CryptoCurrency.ltc), - ExchangePair(from: CryptoCurrency.eth, to: CryptoCurrency.dash), - ExchangePair(from: CryptoCurrency.eth, to: CryptoCurrency.xmr), - ExchangePair(from: CryptoCurrency.btc, to: CryptoCurrency.eth), - ExchangePair(from: CryptoCurrency.btc, to: CryptoCurrency.bch), - ExchangePair(from: CryptoCurrency.btc, to: CryptoCurrency.ltc), - ExchangePair(from: CryptoCurrency.btc, to: CryptoCurrency.dash), - ExchangePair(from: CryptoCurrency.btc, to: CryptoCurrency.xmr) - ]); - - Box trades; - - static const apiUri = 'https://api.morphtoken.com'; - static const _morphURISuffix = '/morph'; - static const _limitsURISuffix = '/limits'; - static const _ratesURISuffix = '/rates'; - static const weight = 10000; - - @override - String get title => 'MorphToken'; - - @override - bool get isAvailable => true; - - @override - bool get isEnabled => true; - - @override - bool get supportsFixedRate => false; - - @override - ExchangeProviderDescription get description => - ExchangeProviderDescription.morphToken; - - @override - Future checkIsAvailable() async => true; - - @override - Future fetchLimits({ - required CryptoCurrency from, - required CryptoCurrency to, - required bool isFixedRateMode}) async { - final url = apiUri + _limitsURISuffix; - final uri = Uri.parse(url); - final headers = {'Content-type': 'application/json'}; - final body = json.encode({ - "input": {"asset": from.toString()}, - "output": [ - {"asset": to.toString(), "weight": weight} - ] - }); - final response = await post(uri, headers: headers, body: body); - final responseJSON = json.decode(response.body) as Map; - - final min = responseJSON['input']['limits']['min'] as int; - int max = 0; - double ethMax; - - if (from == CryptoCurrency.eth) { - ethMax = responseJSON['input']['limits']['max'] as double; - } else { - max = responseJSON['input']['limits']['max'] as int; - } - - final minFormatted = AmountConverter.amountIntToDouble(from, min); - final maxFormatted = AmountConverter.amountIntToDouble(from, max); - - return Limits(min: minFormatted, max: maxFormatted); - } - - @override - Future createTrade({ - required TradeRequest request, - required bool isFixedRateMode}) async { - const url = apiUri + _morphURISuffix; - final _request = request as MorphTokenRequest; - final body = { - "input": { - "asset": _request.from.toString(), - "refund": _request.refundAddress - }, - "output": [ - { - "asset": _request.to.toString(), - "weight": weight, - "address": _request.address - } - ], - "tag": "cakewallet" - }; - final uri = Uri.parse(url); - final response = await post(uri, - headers: {'Content-Type': 'application/json'}, body: json.encode(body)); - - if (response.statusCode != 200) { - if (response.statusCode == 400) { - final responseJSON = json.decode(response.body) as Map; - final error = responseJSON['description'] as String; - - throw TradeNotCreatedException(description, description: error); - } - - throw TradeNotCreatedException(description); - } - - final responseJSON = json.decode(response.body) as Map; - final id = responseJSON['id'] as String; - - return Trade( - id: id, - provider: description, - from: _request.from, - to: _request.to, - state: TradeState.created, - amount: _request.amount, - createdAt: DateTime.now()); - } - - @override - Future findTradeById({required String id}) async { - final url = apiUri + _morphURISuffix + '/' + id; - final uri = Uri.parse(url); - final response = await get(uri); - - if (response.statusCode != 200) { - if (response.statusCode == 400) { - final responseJSON = json.decode(response.body) as Map; - final error = responseJSON['description'] as String; - - throw TradeNotFoundException(id, - provider: description, description: error); - } - - throw TradeNotFoundException(id, provider: description); - } - - final responseJSON = json.decode(response.body) as Map; - final fromCurrency = responseJSON['input']['asset'] as String; - final from = CryptoCurrency.fromString(fromCurrency.toLowerCase()); - final toCurrency = responseJSON['output'][0]['asset'] as String; - final to = CryptoCurrency.fromString(toCurrency.toLowerCase()); - final inputAddress = responseJSON['input']['deposit_address'] as String; - final status = responseJSON['state'] as String; - final state = TradeState.deserialize(raw: status.toLowerCase()); - - String amount = ""; - for (final trade in trades.values) { - if (trade.id == id) { - amount = trade.amount; - break; - } - } - - return Trade( - id: id, - from: from, - to: to, - provider: description, - inputAddress: inputAddress, - amount: amount, - state: state); - } - - @override - Future fetchRate( - {required CryptoCurrency from, - required CryptoCurrency to, - required double amount, - required bool isFixedRateMode, - required bool isReceiveAmount}) async { - final url = apiUri + _ratesURISuffix; - final uri = Uri.parse(url); - final response = await get(uri); - final responseJSON = json.decode(response.body) as Map; - final rate = responseJSON['data'][from.toString()][to.toString()] as String; - - try { - final estimatedAmount = double.parse(rate) * amount; - return estimatedAmount; - } catch (_) { - return 0.0; - } - } -} diff --git a/lib/exchange/morphtoken/morphtoken_request.dart b/lib/exchange/morphtoken/morphtoken_request.dart deleted file mode 100644 index 7698755ec..000000000 --- a/lib/exchange/morphtoken/morphtoken_request.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:cw_core/crypto_currency.dart'; -import 'package:cake_wallet/exchange/trade_request.dart'; - -class MorphTokenRequest extends TradeRequest { - MorphTokenRequest( - {required this.from, - required this.to, - required this.address, - required this.amount, - required this.refundAddress}); - - CryptoCurrency from; - CryptoCurrency to; - String address; - String amount; - String refundAddress; -} diff --git a/lib/exchange/changenow/changenow_exchange_provider.dart b/lib/exchange/provider/changenow_exchange_provider.dart similarity index 79% rename from lib/exchange/changenow/changenow_exchange_provider.dart rename to lib/exchange/provider/changenow_exchange_provider.dart index 6166a8875..300741a08 100644 --- a/lib/exchange/changenow/changenow_exchange_provider.dart +++ b/lib/exchange/provider/changenow_exchange_provider.dart @@ -1,33 +1,32 @@ import 'dart:convert'; import 'dart:io'; -import 'package:cake_wallet/exchange/trade_not_found_exeption.dart'; + +import 'package:cake_wallet/.secrets.g.dart' as secrets; +import 'package:cake_wallet/exchange/exchange_provider_description.dart'; +import 'package:cake_wallet/exchange/limits.dart'; +import 'package:cake_wallet/exchange/provider/exchange_provider.dart'; +import 'package:cake_wallet/exchange/trade.dart'; +import 'package:cake_wallet/exchange/trade_not_found_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:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/utils/device_info.dart'; import 'package:cake_wallet/utils/distribution_info.dart'; import 'package:cake_wallet/wallet_type_utils.dart'; -import 'package:http/http.dart'; -import 'package:cake_wallet/.secrets.g.dart' as secrets; import 'package:cw_core/crypto_currency.dart'; -import 'package:cake_wallet/exchange/exchange_pair.dart'; -import 'package:cake_wallet/exchange/exchange_provider.dart'; -import 'package:cake_wallet/exchange/limits.dart'; -import 'package:cake_wallet/exchange/trade.dart'; -import 'package:cake_wallet/exchange/trade_request.dart'; -import 'package:cake_wallet/exchange/trade_state.dart'; -import 'package:cake_wallet/exchange/changenow/changenow_request.dart'; -import 'package:cake_wallet/exchange/exchange_provider_description.dart'; +import 'package:http/http.dart'; class ChangeNowExchangeProvider extends ExchangeProvider { - ChangeNowExchangeProvider({required this.settingsStore}) - : _lastUsedRateId = '', - super( - pairList: CryptoCurrency.all - .where((i) => i != CryptoCurrency.xhv) - .map((i) => CryptoCurrency.all - .where((i) => i != CryptoCurrency.xhv) - .map((k) => ExchangePair(from: i, to: k, reverse: true))) - .expand((i) => i) - .toList()); + ChangeNowExchangeProvider({required SettingsStore settingsStore}) + : _settingsStore = settingsStore, + _lastUsedRateId = '', + super(pairList: supportedPairs(_notSupported)); + + static const List _notSupported = [ + CryptoCurrency.zaddr, + CryptoCurrency.xhv, + ]; static final apiKey = DeviceInfo.instance.isMobile ? secrets.changeNowApiKey : secrets.changeNowApiKeyDesktop; @@ -38,6 +37,9 @@ class ChangeNowExchangeProvider extends ExchangeProvider { static const rangePath = '/v2/exchange/range'; static const apiHeaderKey = 'x-changenow-api-key'; + final SettingsStore _settingsStore; + String _lastUsedRateId; + @override String get title => 'ChangeNOW'; @@ -56,25 +58,18 @@ class ChangeNowExchangeProvider extends ExchangeProvider { @override Future checkIsAvailable() async => true; - final SettingsStore settingsStore; - - String _lastUsedRateId; - - static String getFlow(bool isFixedRate) => isFixedRate ? 'fixed-rate' : 'standard'; - @override Future fetchLimits( {required CryptoCurrency from, required CryptoCurrency to, required bool isFixedRateMode}) async { final headers = {apiHeaderKey: apiKey}; - final flow = getFlow(isFixedRateMode); final params = { 'fromCurrency': _normalizeCurrency(from), 'toCurrency': _normalizeCurrency(to), 'fromNetwork': _networkFor(from), 'toNetwork': _networkFor(to), - 'flow': flow + 'flow': _getFlow(isFixedRateMode) }; final uri = Uri.https(apiAuthority, rangePath, params); final response = await get(uri, headers: headers); @@ -86,20 +81,61 @@ class ChangeNowExchangeProvider extends ExchangeProvider { throw Exception('${error}\n$message'); } - if (response.statusCode != 200) { + if (response.statusCode != 200) throw Exception('Unexpected http status: ${response.statusCode}'); - } final responseJSON = json.decode(response.body) as Map; return Limits( min: responseJSON['minAmount'] as double?, max: responseJSON['maxAmount'] as double?); } + @override + Future fetchRate( + {required CryptoCurrency from, + required CryptoCurrency to, + required double amount, + required bool isFixedRateMode, + required bool isReceiveAmount}) async { + try { + if (amount == 0) return 0.0; + + final headers = {apiHeaderKey: apiKey}; + final isReverse = isReceiveAmount; + final type = isReverse ? 'reverse' : 'direct'; + final params = { + 'fromCurrency': _normalizeCurrency(from), + 'toCurrency': _normalizeCurrency(to), + 'fromNetwork': _networkFor(from), + 'toNetwork': _networkFor(to), + 'type': type, + 'flow': _getFlow(isFixedRateMode) + }; + + if (isReverse) + params['toAmount'] = amount.toString(); + else + params['fromAmount'] = amount.toString(); + + final uri = Uri.https(apiAuthority, estimatedAmountPath, params); + final response = await get(uri, headers: headers); + final responseJSON = json.decode(response.body) as Map; + final fromAmount = double.parse(responseJSON['fromAmount'].toString()); + final toAmount = double.parse(responseJSON['toAmount'].toString()); + final rateId = responseJSON['rateId'] as String? ?? ''; + + if (rateId.isNotEmpty) _lastUsedRateId = rateId; + + return isReverse ? (amount / fromAmount) : (toAmount / amount); + } catch (e) { + print(e.toString()); + return 0.0; + } + } + @override Future createTrade({required TradeRequest request, required bool isFixedRateMode}) async { - final _request = request as ChangeNowRequest; final distributionPath = await DistributionInfo.instance.getDistributionPath(); - final formattedAppVersion = int.tryParse(settingsStore.appVersion.replaceAll('.', '')) ?? 0; + final formattedAppVersion = int.tryParse(_settingsStore.appVersion.replaceAll('.', '')) ?? 0; final payload = { 'app': isMoneroOnly ? 'monerocom' : 'cakewallet', 'device': Platform.operatingSystem, @@ -107,19 +143,18 @@ class ChangeNowExchangeProvider extends ExchangeProvider { 'version': formattedAppVersion }; final headers = {apiHeaderKey: apiKey, 'Content-Type': 'application/json'}; - final flow = getFlow(isFixedRateMode); final type = isFixedRateMode ? 'reverse' : 'direct'; final body = { - 'fromCurrency': _normalizeCurrency(_request.from), - 'toCurrency': _normalizeCurrency(_request.to), - 'fromNetwork': _networkFor(_request.from), - 'toNetwork': _networkFor(_request.to), - if (!isFixedRateMode) 'fromAmount': _request.fromAmount, - if (isFixedRateMode) 'toAmount': _request.toAmount, - 'address': _request.address, - 'flow': flow, + 'fromCurrency': _normalizeCurrency(request.fromCurrency), + 'toCurrency': _normalizeCurrency(request.toCurrency), + 'fromNetwork': _networkFor(request.fromCurrency), + 'toNetwork': _networkFor(request.toCurrency), + if (!isFixedRateMode) 'fromAmount': request.fromAmount, + if (isFixedRateMode) 'toAmount': request.toAmount, + 'address': request.toAddress, + 'flow': _getFlow(isFixedRateMode), 'type': type, - 'refundAddress': _request.refundAddress, + 'refundAddress': request.refundAddress, 'payload': payload, }; @@ -127,9 +162,9 @@ class ChangeNowExchangeProvider extends ExchangeProvider { // since we schedule to calculate the rate every 5 seconds we need to ensure that // we have the latest rate id with the given inputs before creating the trade await fetchRate( - from: _request.from, - to: _request.to, - amount: double.tryParse(_request.toAmount) ?? 0, + from: request.fromCurrency, + to: request.toCurrency, + amount: double.tryParse(request.toAmount) ?? 0, isFixedRateMode: true, isReceiveAmount: true, ); @@ -146,9 +181,8 @@ class ChangeNowExchangeProvider extends ExchangeProvider { throw Exception('${error}\n$message'); } - if (response.statusCode != 200) { + if (response.statusCode != 200) throw Exception('Unexpected http status: ${response.statusCode}'); - } final responseJSON = json.decode(response.body) as Map; final id = responseJSON['id'] as String; @@ -159,14 +193,14 @@ class ChangeNowExchangeProvider extends ExchangeProvider { return Trade( id: id, - from: _request.from, - to: _request.to, + from: request.fromCurrency, + to: request.toCurrency, provider: description, inputAddress: inputAddress, refundAddress: refundAddress, extraId: extraId, createdAt: DateTime.now(), - amount: responseJSON['fromAmount']?.toString() ?? _request.fromAmount, + amount: responseJSON['fromAmount']?.toString() ?? request.fromAmount, state: TradeState.created, payoutAddress: payoutAddress); } @@ -178,9 +212,7 @@ class ChangeNowExchangeProvider extends ExchangeProvider { final uri = Uri.https(apiAuthority, findTradeByIdPath, params); final response = await get(uri, headers: headers); - if (response.statusCode == 404) { - throw TradeNotFoundException(id, provider: description); - } + if (response.statusCode == 404) throw TradeNotFoundException(id, provider: description); if (response.statusCode == 400) { final responseJSON = json.decode(response.body) as Map; @@ -189,9 +221,8 @@ class ChangeNowExchangeProvider extends ExchangeProvider { throw TradeNotFoundException(id, provider: description, description: error); } - if (response.statusCode != 200) { + if (response.statusCode != 200) throw Exception('Unexpected http status: ${response.statusCode}'); - } final responseJSON = json.decode(response.body) as Map; final fromCurrency = responseJSON['fromCurrency'] as String; @@ -222,54 +253,7 @@ class ChangeNowExchangeProvider extends ExchangeProvider { payoutAddress: payoutAddress); } - @override - Future fetchRate( - {required CryptoCurrency from, - required CryptoCurrency to, - required double amount, - required bool isFixedRateMode, - required bool isReceiveAmount}) async { - try { - if (amount == 0) { - return 0.0; - } - - final headers = {apiHeaderKey: apiKey}; - final isReverse = isReceiveAmount; - final type = isReverse ? 'reverse' : 'direct'; - final flow = getFlow(isFixedRateMode); - final params = { - 'fromCurrency': _normalizeCurrency(from), - 'toCurrency': _normalizeCurrency(to), - 'fromNetwork': _networkFor(from), - 'toNetwork': _networkFor(to), - 'type': type, - 'flow': flow - }; - - if (isReverse) { - params['toAmount'] = amount.toString(); - } else { - params['fromAmount'] = amount.toString(); - } - - final uri = Uri.https(apiAuthority, estimatedAmountPath, params); - final response = await get(uri, headers: headers); - final responseJSON = json.decode(response.body) as Map; - final fromAmount = double.parse(responseJSON['fromAmount'].toString()); - final toAmount = double.parse(responseJSON['toAmount'].toString()); - final rateId = responseJSON['rateId'] as String? ?? ''; - - if (rateId.isNotEmpty) { - _lastUsedRateId = rateId; - } - - return isReverse ? (amount / fromAmount) : (toAmount / amount); - } catch (e) { - print(e.toString()); - return 0.0; - } - } + String _getFlow(bool isFixedRate) => isFixedRate ? 'fixed-rate' : 'standard'; String _networkFor(CryptoCurrency currency) { switch (currency) { @@ -301,4 +285,4 @@ class ChangeNowExchangeProvider extends ExchangeProvider { return tag.toLowerCase(); } } -} \ No newline at end of file +} diff --git a/lib/exchange/exchange_provider.dart b/lib/exchange/provider/exchange_provider.dart similarity index 71% rename from lib/exchange/exchange_provider.dart rename to lib/exchange/provider/exchange_provider.dart index cc81a21f6..d1f69689d 100644 --- a/lib/exchange/exchange_provider.dart +++ b/lib/exchange/provider/exchange_provider.dart @@ -1,37 +1,43 @@ -import 'package:cw_core/crypto_currency.dart'; -import 'package:cake_wallet/exchange/trade_request.dart'; import 'package:cake_wallet/exchange/exchange_pair.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/exchange_provider_description.dart'; +import 'package:cake_wallet/exchange/trade_request.dart'; +import 'package:cw_core/crypto_currency.dart'; abstract class ExchangeProvider { ExchangeProvider({required this.pairList}); - + String get title; + List pairList; + ExchangeProviderDescription get description; + bool get isAvailable; + bool get isEnabled; + bool get supportsFixedRate; + bool get supportsOnionAddress => false; @override String toString() => title; Future fetchLimits( + {required CryptoCurrency from, required CryptoCurrency to, required bool isFixedRateMode}); + + Future createTrade({required TradeRequest request, required bool isFixedRateMode}); + + Future findTradeById({required String id}); + + Future fetchRate( {required CryptoCurrency from, required CryptoCurrency to, - required bool isFixedRateMode}); - Future createTrade({ - required TradeRequest request, - required bool isFixedRateMode}); - Future findTradeById({required String id}); - Future fetchRate({ - required CryptoCurrency from, - required CryptoCurrency to, - required double amount, - required bool isFixedRateMode, - required bool isReceiveAmount}); + required double amount, + required bool isFixedRateMode, + required bool isReceiveAmount}); + Future checkIsAvailable(); } diff --git a/lib/exchange/provider/exolix_exchange_provider.dart b/lib/exchange/provider/exolix_exchange_provider.dart new file mode 100644 index 000000000..eb40aff73 --- /dev/null +++ b/lib/exchange/provider/exolix_exchange_provider.dart @@ -0,0 +1,269 @@ +import 'dart:convert'; + +import 'package:cake_wallet/.secrets.g.dart' as secrets; +import 'package:cake_wallet/exchange/exchange_provider_description.dart'; +import 'package:cake_wallet/exchange/limits.dart'; +import 'package:cake_wallet/exchange/provider/exchange_provider.dart'; +import 'package:cake_wallet/exchange/trade.dart'; +import 'package:cake_wallet/exchange/trade_not_found_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'; + +class ExolixExchangeProvider extends ExchangeProvider { + ExolixExchangeProvider() : super(pairList: supportedPairs(_notSupported)); + + static final apiKey = secrets.exolixApiKey; + static const apiBaseUrl = 'exolix.com'; + static const transactionsPath = '/api/v2/transactions'; + static const ratePath = '/api/v2/rate'; + + static const List _notSupported = [ + CryptoCurrency.usdt, + CryptoCurrency.xhv, + CryptoCurrency.btt, + CryptoCurrency.firo, + CryptoCurrency.zaddr, + CryptoCurrency.xvg, + CryptoCurrency.kmd, + CryptoCurrency.paxg, + CryptoCurrency.rune, + CryptoCurrency.scrt, + CryptoCurrency.btcln, + CryptoCurrency.cro, + CryptoCurrency.ftm, + CryptoCurrency.frax, + CryptoCurrency.gusd, + CryptoCurrency.gtc, + CryptoCurrency.weth, + ]; + + @override + String get title => 'Exolix'; + + @override + bool get isAvailable => true; + + @override + bool get isEnabled => true; + + @override + bool get supportsFixedRate => true; + + @override + ExchangeProviderDescription get description => ExchangeProviderDescription.exolix; + + @override + Future checkIsAvailable() async => true; + + @override + Future fetchLimits( + {required CryptoCurrency from, + required CryptoCurrency to, + required bool isFixedRateMode}) async { + final params = { + 'rateType': _getRateType(isFixedRateMode), + 'amount': '1', + }; + if (isFixedRateMode) { + params['coinFrom'] = _normalizeCurrency(to); + params['coinTo'] = _normalizeCurrency(from); + params['networkFrom'] = _networkFor(to); + params['networkTo'] = _networkFor(from); + } else { + params['coinFrom'] = _normalizeCurrency(from); + params['coinTo'] = _normalizeCurrency(to); + params['networkFrom'] = _networkFor(from); + params['networkTo'] = _networkFor(to); + } + final uri = Uri.https(apiBaseUrl, ratePath, params); + final response = await get(uri); + + if (response.statusCode != 200) + throw Exception('Unexpected http status: ${response.statusCode}'); + + final responseJSON = json.decode(response.body) as Map; + return Limits(min: responseJSON['minAmount'] as double?); + } + + @override + Future fetchRate( + {required CryptoCurrency from, + required CryptoCurrency to, + required double amount, + required bool isFixedRateMode, + required bool isReceiveAmount}) async { + try { + if (amount == 0) return 0.0; + + final params = { + 'coinFrom': _normalizeCurrency(from), + 'coinTo': _normalizeCurrency(to), + 'networkFrom': _networkFor(from), + 'networkTo': _networkFor(to), + 'rateType': _getRateType(isFixedRateMode), + 'apiToken': apiKey, + }; + + if (isReceiveAmount) + params['withdrawalAmount'] = amount.toString(); + else + params['amount'] = amount.toString(); + + final uri = Uri.https(apiBaseUrl, ratePath, params); + final response = await get(uri); + final responseJSON = json.decode(response.body) as Map; + + if (response.statusCode != 200) { + final message = responseJSON['message'] as String?; + throw Exception(message); + } + + return responseJSON['rate'] as double; + } catch (e) { + print(e.toString()); + return 0.0; + } + } + + @override + Future createTrade({required TradeRequest request, required bool isFixedRateMode}) async { + final headers = {'Content-Type': 'application/json'}; + final body = { + 'coinFrom': _normalizeCurrency(request.fromCurrency), + 'coinTo': _normalizeCurrency(request.toCurrency), + 'networkFrom': _networkFor(request.fromCurrency), + 'networkTo': _networkFor(request.toCurrency), + 'withdrawalAddress': request.toAddress, + 'refundAddress': request.refundAddress, + 'rateType': _getRateType(isFixedRateMode), + 'apiToken': apiKey, + }; + + if (isFixedRateMode) + body['withdrawalAmount'] = request.toAmount; + else + body['amount'] = request.fromAmount; + + final uri = Uri.https(apiBaseUrl, transactionsPath); + final response = await post(uri, headers: headers, body: json.encode(body)); + + if (response.statusCode == 400) { + final responseJSON = json.decode(response.body) as Map; + final errors = responseJSON['errors'] as Map; + final errorMessage = errors.values.join(', '); + throw Exception(errorMessage); + } + + if (response.statusCode != 200 && response.statusCode != 201) + throw Exception('Unexpected http status: ${response.statusCode}'); + + final responseJSON = json.decode(response.body) as Map; + final id = responseJSON['id'] as String; + final inputAddress = responseJSON['depositAddress'] as String; + final refundAddress = responseJSON['refundAddress'] as String?; + final extraId = responseJSON['depositExtraId'] as String?; + final payoutAddress = responseJSON['withdrawalAddress'] as String; + final amount = responseJSON['amount'].toString(); + + return Trade( + id: id, + from: request.fromCurrency, + to: request.toCurrency, + provider: description, + inputAddress: inputAddress, + refundAddress: refundAddress, + extraId: extraId, + createdAt: DateTime.now(), + amount: amount, + state: TradeState.created, + payoutAddress: payoutAddress); + } + + @override + Future findTradeById({required String id}) async { + final findTradeByIdPath = '$transactionsPath/$id'; + final uri = Uri.https(apiBaseUrl, findTradeByIdPath); + final response = await get(uri); + + if (response.statusCode == 404) throw TradeNotFoundException(id, provider: description); + + if (response.statusCode == 400) { + final responseJSON = json.decode(response.body) as Map; + final errors = responseJSON['errors'] as Map; + final errorMessage = errors.values.join(', '); + + throw TradeNotFoundException(id, provider: description, description: errorMessage); + } + + if (response.statusCode != 200) + throw Exception('Unexpected http status: ${response.statusCode}'); + + final responseJSON = json.decode(response.body) as Map; + final coinFrom = responseJSON['coinFrom']['coinCode'] as String; + final coinTo = responseJSON['coinTo']['coinCode'] as String; + final inputAddress = responseJSON['depositAddress'] as String; + final amount = responseJSON['amount'].toString(); + final status = responseJSON['status'] as String; + final extraId = responseJSON['depositExtraId'] as String?; + final outputTransaction = responseJSON['hashOut']['hash'] as String?; + final payoutAddress = responseJSON['withdrawalAddress'] as String; + + return Trade( + id: id, + from: CryptoCurrency.fromString(coinFrom), + to: CryptoCurrency.fromString(coinTo), + provider: description, + inputAddress: inputAddress, + amount: amount, + state: TradeState.deserialize(raw: _prepareStatus(status)), + extraId: extraId, + outputTransaction: outputTransaction, + payoutAddress: payoutAddress); + } + + String _getRateType(bool isFixedRate) => isFixedRate ? 'fixed' : 'float'; + + String _prepareStatus(String status) { + switch (status) { + case 'deleted': + case 'error': + return 'overdue'; + default: + return status; + } + } + + String _networkFor(CryptoCurrency currency) { + switch (currency) { + case CryptoCurrency.arb: + return 'ARBITRUM'; + default: + return currency.tag != null ? _normalizeTag(currency.tag!) : currency.title; + } + } + + String _normalizeCurrency(CryptoCurrency currency) { + switch (currency) { + case CryptoCurrency.nano: + return 'XNO'; + case CryptoCurrency.bttc: + return 'BTT'; + case CryptoCurrency.zec: + return 'ZEC'; + default: + return currency.title; + } + } + + String _normalizeTag(String tag) { + switch (tag) { + case 'POLY': + return 'Polygon'; + default: + return tag; + } + } +} diff --git a/lib/exchange/sideshift/sideshift_exchange_provider.dart b/lib/exchange/provider/sideshift_exchange_provider.dart similarity index 75% rename from lib/exchange/sideshift/sideshift_exchange_provider.dart rename to lib/exchange/provider/sideshift_exchange_provider.dart index 257d339cf..7a466e213 100644 --- a/lib/exchange/sideshift/sideshift_exchange_provider.dart +++ b/lib/exchange/provider/sideshift_exchange_provider.dart @@ -1,28 +1,20 @@ import 'dart:convert'; -import 'package:cake_wallet/exchange/exchange_pair.dart'; -import 'package:cake_wallet/exchange/exchange_provider.dart'; -import 'package:cake_wallet/exchange/exchange_provider_description.dart'; -import 'package:cake_wallet/exchange/sideshift/sideshift_request.dart'; -import 'package:cake_wallet/exchange/trade_not_created_exeption.dart'; -import 'package:cake_wallet/exchange/trade_not_found_exeption.dart'; -import 'package:cake_wallet/exchange/trade_state.dart'; import 'package:cake_wallet/.secrets.g.dart' as secrets; -import 'package:cw_core/crypto_currency.dart'; -import 'package:cake_wallet/exchange/trade_request.dart'; -import 'package:cake_wallet/exchange/trade.dart'; +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_not_found_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'; class SideShiftExchangeProvider extends ExchangeProvider { - SideShiftExchangeProvider() : super(pairList: _supportedPairs()); - - static const affiliateId = secrets.sideShiftAffiliateId; - static const apiBaseUrl = 'https://sideshift.ai/api'; - static const rangePath = '/v2/pair'; - static const orderPath = '/v2/shifts'; - static const quotePath = '/v2/quotes'; - static const permissionPath = '/v2/permissions'; + SideShiftExchangeProvider() : super(pairList: supportedPairs(_notSupported)); static const List _notSupported = [ CryptoCurrency.xhv, @@ -39,49 +31,28 @@ class SideShiftExchangeProvider extends ExchangeProvider { CryptoCurrency.eos, ]; - static List _supportedPairs() { - final supportedCurrencies = - CryptoCurrency.all.where((element) => !_notSupported.contains(element)).toList(); + static const affiliateId = secrets.sideShiftAffiliateId; + static const apiBaseUrl = 'https://sideshift.ai/api'; + static const rangePath = '/v2/pair'; + static const orderPath = '/v2/shifts'; + static const quotePath = '/v2/quotes'; + static const permissionPath = '/v2/permissions'; - return supportedCurrencies - .map((i) => supportedCurrencies.map((k) => ExchangePair(from: i, to: k, reverse: true))) - .expand((i) => i) - .toList(); - } + @override + String get title => 'SideShift'; + + @override + bool get isAvailable => true; + + @override + bool get isEnabled => true; + + @override + bool get supportsFixedRate => true; @override ExchangeProviderDescription get description => ExchangeProviderDescription.sideShift; - @override - Future fetchRate( - {required CryptoCurrency from, - required CryptoCurrency to, - required double amount, - required bool isFixedRateMode, - required bool isReceiveAmount}) async { - try { - if (amount == 0) { - return 0.0; - } - - final fromCurrency = from.title.toLowerCase(); - final toCurrency = to.title.toLowerCase(); - final depositNetwork = _networkFor(from); - final settleNetwork = _networkFor(to); - - final url = "$apiBaseUrl$rangePath/$fromCurrency-$depositNetwork/$toCurrency-$settleNetwork"; - - final uri = Uri.parse(url); - final response = await get(uri); - final responseJSON = json.decode(response.body) as Map; - final rate = double.parse(responseJSON['rate'] as String); - - return rate; - } catch (_) { - return 0.00; - } - } - @override Future checkIsAvailable() async { const url = apiBaseUrl + permissionPath; @@ -95,110 +66,10 @@ class SideShiftExchangeProvider extends ExchangeProvider { throw Exception('$error'); } - if (response.statusCode != 200) { - return false; - } + if (response.statusCode != 200) return false; final responseJSON = json.decode(response.body) as Map; - final cancreateShift = responseJSON['createShift'] as bool; - return cancreateShift; - } - - @override - Future createTrade({required TradeRequest request, required bool isFixedRateMode}) async { - final _request = request as SideShiftRequest; - String url = ''; - final depositCoin = request.depositMethod.title.toLowerCase(); - final settleCoin = request.settleMethod.title.toLowerCase(); - final body = { - 'affiliateId': affiliateId, - 'settleAddress': _request.settleAddress, - 'refundAddress': _request.refundAddress, - }; - - if (isFixedRateMode) { - final quoteId = await _createQuote(_request); - body['quoteId'] = quoteId; - - url = apiBaseUrl + orderPath + '/fixed'; - } else { - url = apiBaseUrl + orderPath + '/variable'; - final depositNetwork = _networkFor(request.depositMethod); - final settleNetwork = _networkFor(request.settleMethod); - body["depositCoin"] = depositCoin; - body["settleCoin"] = settleCoin; - body["settleNetwork"] = settleNetwork; - body["depositNetwork"] = depositNetwork; - } - final headers = {'Content-Type': 'application/json'}; - - final uri = Uri.parse(url); - final response = await post(uri, headers: headers, body: json.encode(body)); - - if (response.statusCode != 201) { - if (response.statusCode == 400) { - final responseJSON = json.decode(response.body) as Map; - final error = responseJSON['error']['message'] as String; - - throw TradeNotCreatedException(description, description: error); - } - - throw TradeNotCreatedException(description); - } - - final responseJSON = json.decode(response.body) as Map; - final id = responseJSON['id'] as String; - final inputAddress = responseJSON['depositAddress'] as String; - final settleAddress = responseJSON['settleAddress'] as String; - final depositAmount = responseJSON['depositAmount'] as String?; - - return Trade( - id: id, - provider: description, - from: _request.depositMethod, - to: _request.settleMethod, - inputAddress: inputAddress, - refundAddress: settleAddress, - state: TradeState.created, - amount: depositAmount ?? _request.depositAmount, - payoutAddress: settleAddress, - createdAt: DateTime.now(), - ); - } - - Future _createQuote(SideShiftRequest request) async { - final url = apiBaseUrl + quotePath; - final headers = {'Content-Type': 'application/json'}; - final depositMethod = request.depositMethod.title.toLowerCase(); - final settleMethod = request.settleMethod.title.toLowerCase(); - final depositNetwork = _networkFor(request.depositMethod); - final settleNetwork = _networkFor(request.settleMethod); - final body = { - 'depositCoin': depositMethod, - 'settleCoin': settleMethod, - 'affiliateId': affiliateId, - 'settleAmount': request.depositAmount, - 'settleNetwork': settleNetwork, - 'depositNetwork': depositNetwork, - }; - final uri = Uri.parse(url); - final response = await post(uri, headers: headers, body: json.encode(body)); - - if (response.statusCode != 201) { - if (response.statusCode == 400) { - final responseJSON = json.decode(response.body) as Map; - final error = responseJSON['error']['message'] as String; - - throw TradeNotCreatedException(description, description: error); - } - - throw TradeNotCreatedException(description); - } - - final responseJSON = json.decode(response.body) as Map; - final quoteId = responseJSON['id'] as String; - - return quoteId; + return responseJSON['createShift'] as bool; } @override @@ -244,6 +115,91 @@ class SideShiftExchangeProvider extends ExchangeProvider { return Limits(min: min, max: max); } + @override + Future fetchRate( + {required CryptoCurrency from, + required CryptoCurrency to, + required double amount, + required bool isFixedRateMode, + required bool isReceiveAmount}) async { + try { + if (amount == 0) return 0.0; + + final fromCurrency = from.title.toLowerCase(); + final toCurrency = to.title.toLowerCase(); + final depositNetwork = _networkFor(from); + final settleNetwork = _networkFor(to); + + final url = + "$apiBaseUrl$rangePath/$fromCurrency-$depositNetwork/$toCurrency-$settleNetwork?amount=$amount"; + + final uri = Uri.parse(url); + final response = await get(uri); + final responseJSON = json.decode(response.body) as Map; + + return double.parse(responseJSON['rate'] as String); + } catch (_) { + return 0.00; + } + } + + @override + Future createTrade({required TradeRequest request, required bool isFixedRateMode}) async { + String url = ''; + final body = { + 'affiliateId': affiliateId, + 'settleAddress': request.toAddress, + 'refundAddress': request.refundAddress, + }; + + if (isFixedRateMode) { + final quoteId = await _createQuote(request); + body['quoteId'] = quoteId; + + url = apiBaseUrl + orderPath + '/fixed'; + } else { + url = apiBaseUrl + orderPath + '/variable'; + body["depositCoin"] = request.fromCurrency.title.toLowerCase(); + body["settleCoin"] = request.toCurrency.title.toLowerCase(); + body["settleNetwork"] = _networkFor(request.toCurrency); + body["depositNetwork"] = _networkFor(request.fromCurrency); + } + final headers = {'Content-Type': 'application/json'}; + + final uri = Uri.parse(url); + final response = await post(uri, headers: headers, body: json.encode(body)); + + if (response.statusCode != 201) { + if (response.statusCode == 400) { + final responseJSON = json.decode(response.body) as Map; + final error = responseJSON['error']['message'] as String; + + throw TradeNotCreatedException(description, description: error); + } + + throw TradeNotCreatedException(description); + } + + final responseJSON = json.decode(response.body) as Map; + final id = responseJSON['id'] as String; + final inputAddress = responseJSON['depositAddress'] as String; + final settleAddress = responseJSON['settleAddress'] as String; + final depositAmount = responseJSON['depositAmount'] as String?; + + return Trade( + id: id, + provider: description, + from: request.fromCurrency, + to: request.toCurrency, + inputAddress: inputAddress, + refundAddress: settleAddress, + state: TradeState.created, + amount: depositAmount ?? request.fromAmount, + payoutAddress: settleAddress, + createdAt: DateTime.now(), + ); + } + @override Future findTradeById({required String id}) async { final url = apiBaseUrl + orderPath + '/' + id; @@ -267,44 +223,56 @@ class SideShiftExchangeProvider extends ExchangeProvider { final responseJSON = json.decode(response.body) as Map; final fromCurrency = responseJSON['depositCoin'] as String; - final from = CryptoCurrency.fromString(fromCurrency); final toCurrency = responseJSON['settleCoin'] as String; - final to = CryptoCurrency.fromString(toCurrency); final inputAddress = responseJSON['depositAddress'] as String; final expectedSendAmount = responseJSON['depositAmount'] as String?; final status = responseJSON['status'] as String?; final settleAddress = responseJSON['settleAddress'] as String; - TradeState? state; - - state = TradeState.deserialize(raw: status ?? 'created'); final isVariable = (responseJSON['type'] as String) == 'variable'; - final expiredAtRaw = responseJSON['expiresAt'] as String; final expiredAt = isVariable ? null : DateTime.tryParse(expiredAtRaw)?.toLocal(); return Trade( id: id, - from: from, - to: to, + from: CryptoCurrency.fromString(fromCurrency), + to: CryptoCurrency.fromString(toCurrency), provider: description, inputAddress: inputAddress, amount: expectedSendAmount ?? '', - state: state, + state: TradeState.deserialize(raw: status ?? 'created'), expiredAt: expiredAt, payoutAddress: settleAddress); } - @override - bool get isAvailable => true; + Future _createQuote(TradeRequest request) async { + final url = apiBaseUrl + quotePath; + final headers = {'Content-Type': 'application/json'}; + final body = { + 'depositCoin': request.fromCurrency.title.toLowerCase(), + 'settleCoin': request.toCurrency.title.toLowerCase(), + 'affiliateId': affiliateId, + 'settleAmount': request.toAmount, + 'settleNetwork': _networkFor(request.toCurrency), + 'depositNetwork': _networkFor(request.fromCurrency), + }; + final uri = Uri.parse(url); + final response = await post(uri, headers: headers, body: json.encode(body)); - @override - bool get isEnabled => true; + if (response.statusCode != 201) { + if (response.statusCode == 400) { + final responseJSON = json.decode(response.body) as Map; + final error = responseJSON['error']['message'] as String; - @override - bool get supportsFixedRate => true; + throw TradeNotCreatedException(description, description: error); + } - @override - String get title => 'SideShift'; + throw TradeNotCreatedException(description); + } + + final responseJSON = json.decode(response.body) as Map; + + return responseJSON['id'] as String; + } String _networkFor(CryptoCurrency currency) => currency.tag != null ? _normalizeTag(currency.tag!) : 'mainnet'; diff --git a/lib/exchange/simpleswap/simpleswap_exchange_provider.dart b/lib/exchange/provider/simpleswap_exchange_provider.dart similarity index 72% rename from lib/exchange/simpleswap/simpleswap_exchange_provider.dart rename to lib/exchange/provider/simpleswap_exchange_provider.dart index 4c1072d11..091c3a913 100644 --- a/lib/exchange/simpleswap/simpleswap_exchange_provider.dart +++ b/lib/exchange/provider/simpleswap_exchange_provider.dart @@ -1,71 +1,49 @@ import 'dart:convert'; -import 'package:cake_wallet/exchange/exchange_pair.dart'; -import 'package:cake_wallet/exchange/exchange_provider.dart'; + +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/simpleswap/simpleswap_request.dart'; -import 'package:cake_wallet/exchange/trade_not_created_exeption.dart'; -import 'package:cake_wallet/exchange/trade_not_found_exeption.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_not_found_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:cake_wallet/utils/device_info.dart'; import 'package:cw_core/crypto_currency.dart'; -import 'package:cake_wallet/exchange/trade_request.dart'; -import 'package:cake_wallet/exchange/trade.dart'; -import 'package:cake_wallet/exchange/limits.dart'; -import 'package:cake_wallet/.secrets.g.dart' as secrets; import 'package:http/http.dart'; class SimpleSwapExchangeProvider extends ExchangeProvider { - SimpleSwapExchangeProvider() - : super( - pairList: CryptoCurrency.all - .where((i) => i != CryptoCurrency.zaddr) - .map((i) => CryptoCurrency.all - .where((i) => i != CryptoCurrency.zaddr) - .map((k) => ExchangePair(from: i, to: k, reverse: true))) - .expand((i) => i) - .toList()); + SimpleSwapExchangeProvider() : super(pairList: supportedPairs(_notSupported)); + static const List _notSupported = [ + CryptoCurrency.zaddr, + CryptoCurrency.xhv, + ]; + + static final apiKey = + DeviceInfo.instance.isMobile ? secrets.simpleSwapApiKey : secrets.simpleSwapApiKeyDesktop; static const apiAuthority = 'api.simpleswap.io'; static const getEstimatePath = '/v1/get_estimated'; static const rangePath = '/v1/get_ranges'; static const getExchangePath = '/v1/get_exchange'; static const createExchangePath = '/v1/create_exchange'; - static final apiKey = DeviceInfo.instance.isMobile ? secrets.simpleSwapApiKey : secrets.simpleSwapApiKeyDesktop; @override - ExchangeProviderDescription get description => - ExchangeProviderDescription.simpleSwap; + String get title => 'SimpleSwap'; @override - Future fetchRate( - {required CryptoCurrency from, - required CryptoCurrency to, - required double amount, - required bool isFixedRateMode, - required bool isReceiveAmount}) async { - try { - if (amount == 0) { - return 0.0; - } - final fromCurrency = _normalizeCryptoCurrency(from); - final toCurrency = _normalizeCryptoCurrency(to); - final params = { - 'api_key': apiKey, - 'currency_from': fromCurrency, - 'currency_to': toCurrency, - 'amount': amount.toString(), - 'fixed': isFixedRateMode.toString() - }; - final uri = Uri.https(apiAuthority, getEstimatePath, params); - final response = await get(uri); + bool get isAvailable => true; - if (response.body == "null") return 0.00; - final data = json.decode(response.body) as String; - return double.parse(data) / amount; - } catch (_) { - return 0.00; - } - } + @override + bool get isEnabled => true; + + @override + bool get supportsFixedRate => false; + + @override + ExchangeProviderDescription get description => ExchangeProviderDescription.simpleSwap; @override Future checkIsAvailable() async { @@ -76,20 +54,79 @@ class SimpleSwapExchangeProvider extends ExchangeProvider { } @override - Future createTrade({required TradeRequest request, required bool isFixedRateMode}) async { - final _request = request as SimpleSwapRequest; - final headers = { - 'Content-Type': 'application/json'}; - final params = { + Future fetchLimits( + {required CryptoCurrency from, + required CryptoCurrency to, + required bool isFixedRateMode}) async { + final params = { 'api_key': apiKey, + 'fixed': isFixedRateMode.toString(), + 'currency_from': _normalizeCurrency(from), + 'currency_to': _normalizeCurrency(to), }; + final uri = Uri.https(apiAuthority, rangePath, params); + + final response = await get(uri); + + if (response.statusCode == 500) { + final responseJSON = json.decode(response.body) as Map; + final error = responseJSON['message'] as String; + + throw Exception('$error'); + } + + if (response.statusCode != 200) { + throw Exception('Unexpected http status: ${response.statusCode}'); + } + + final responseJSON = json.decode(response.body) as Map; + final min = double.tryParse(responseJSON['min'] as String? ?? ''); + final max = double.tryParse(responseJSON['max'] as String? ?? ''); + + return Limits(min: min, max: max); + } + + @override + Future fetchRate( + {required CryptoCurrency from, + required CryptoCurrency to, + required double amount, + required bool isFixedRateMode, + required bool isReceiveAmount}) async { + try { + if (amount == 0) return 0.0; + + final params = { + 'api_key': apiKey, + 'currency_from': _normalizeCurrency(from), + 'currency_to': _normalizeCurrency(to), + 'amount': amount.toString(), + 'fixed': isFixedRateMode.toString() + }; + final uri = Uri.https(apiAuthority, getEstimatePath, params); + final response = await get(uri); + + if (response.body == "null") return 0.00; + + final data = json.decode(response.body) as String; + + return double.parse(data) / amount; + } catch (_) { + return 0.00; + } + } + + @override + Future createTrade({required TradeRequest request, required bool isFixedRateMode}) async { + final headers = {'Content-Type': 'application/json'}; + final params = {'api_key': apiKey}; final body = { - "currency_from": _normalizeCryptoCurrency(_request.from), - "currency_to": _normalizeCryptoCurrency(_request.to), - "amount": _request.amount, + "currency_from": _normalizeCurrency(request.fromCurrency), + "currency_to": _normalizeCurrency(request.toCurrency), + "amount": request.fromAmount, "fixed": isFixedRateMode, - "user_refund_address": _request.refundAddress, - "address_to": _request.address + "user_refund_address": request.refundAddress, + "address_to": request.toAddress }; final uri = Uri.https(apiAuthority, createExchangePath, params); @@ -112,56 +149,22 @@ class SimpleSwapExchangeProvider extends ExchangeProvider { final payoutAddress = responseJSON['address_to'] as String; final settleAddress = responseJSON['user_refund_address'] as String; final extraId = responseJSON['extra_id_from'] as String?; + return Trade( id: id, provider: description, - from: _request.from, - to: _request.to, + from: request.fromCurrency, + to: request.toCurrency, inputAddress: inputAddress, refundAddress: settleAddress, extraId: extraId, state: TradeState.created, - amount: _request.amount, + amount: request.fromAmount, payoutAddress: payoutAddress, createdAt: DateTime.now(), ); } - @override - Future fetchLimits({ - required CryptoCurrency from, - required CryptoCurrency to, - required bool isFixedRateMode}) async { - final fromCurrency = _normalizeCryptoCurrency(from); - final toCurrency = _normalizeCryptoCurrency(to); - final params = { - 'api_key': apiKey, - 'fixed': isFixedRateMode.toString(), - 'currency_from': fromCurrency, - 'currency_to': toCurrency, - }; - final uri = Uri.https(apiAuthority, rangePath, params); - - final response = await get(uri); - - if (response.statusCode == 500) { - final responseJSON = json.decode(response.body) as Map; - final error = responseJSON['message'] as String; - - throw Exception('$error'); - } - - if (response.statusCode != 200) { - throw Exception('Unexpected http status: ${response.statusCode}'); - } - - final responseJSON = json.decode(response.body) as Map; - final min = double.tryParse(responseJSON['min'] as String? ?? ''); - final max = double.tryParse(responseJSON['max'] as String? ?? ''); - - return Limits(min: min, max: max); - } - @override Future findTradeById({required String id}) async { final params = {'api_key': apiKey, 'id': id}; @@ -185,42 +188,27 @@ class SimpleSwapExchangeProvider extends ExchangeProvider { final responseJSON = json.decode(response.body) as Map; final fromCurrency = responseJSON['currency_from'] as String; - final from = CryptoCurrency.fromString(fromCurrency); final toCurrency = responseJSON['currency_to'] as String; - final to = CryptoCurrency.fromString(toCurrency); final inputAddress = responseJSON['address_from'] as String; final expectedSendAmount = responseJSON['expected_amount'].toString(); final extraId = responseJSON['extra_id_from'] as String?; final status = responseJSON['status'] as String; final payoutAddress = responseJSON['address_to'] as String; - final state = TradeState.deserialize(raw: status); return Trade( id: id, - from: from, - to: to, + from: CryptoCurrency.fromString(fromCurrency), + to: CryptoCurrency.fromString(toCurrency), extraId: extraId, provider: description, inputAddress: inputAddress, amount: expectedSendAmount, - state: state, + state: TradeState.deserialize(raw: status), payoutAddress: payoutAddress, ); } - @override - bool get isAvailable => true; - - @override - bool get isEnabled => true; - - @override - bool get supportsFixedRate => false; - - @override - String get title => 'SimpleSwap'; - - static String _normalizeCryptoCurrency(CryptoCurrency currency) { + static String _normalizeCurrency(CryptoCurrency currency) { switch (currency) { case CryptoCurrency.zaddr: return 'zec'; diff --git a/lib/exchange/trocador/trocador_exchange_provider.dart b/lib/exchange/provider/trocador_exchange_provider.dart similarity index 77% rename from lib/exchange/trocador/trocador_exchange_provider.dart rename to lib/exchange/provider/trocador_exchange_provider.dart index b42291ed7..52e38ecc8 100644 --- a/lib/exchange/trocador/trocador_exchange_provider.dart +++ b/lib/exchange/provider/trocador_exchange_provider.dart @@ -1,158 +1,97 @@ import 'dart:convert'; -import 'package:cake_wallet/exchange/exchange_pair.dart'; -import 'package:cake_wallet/exchange/exchange_provider.dart'; -import 'package:cake_wallet/exchange/trade_state.dart'; -import 'package:cake_wallet/exchange/trocador/trocador_request.dart'; -import 'package:cw_core/crypto_currency.dart'; -import 'package:cake_wallet/exchange/trade_request.dart'; -import 'package:cake_wallet/exchange/trade.dart'; -import 'package:cake_wallet/exchange/limits.dart'; -import 'package:cake_wallet/exchange/exchange_provider_description.dart'; import 'package:cake_wallet/.secrets.g.dart' as secrets; +import 'package:cake_wallet/exchange/exchange_provider_description.dart'; +import 'package:cake_wallet/exchange/limits.dart'; +import 'package:cake_wallet/exchange/provider/exchange_provider.dart'; +import 'package:cake_wallet/exchange/trade.dart'; +import 'package:cake_wallet/exchange/trade_request.dart'; +import 'package:cake_wallet/exchange/trade_state.dart'; +import 'package:cake_wallet/exchange/utils/currency_pairs_utils.dart'; +import 'package:cw_core/crypto_currency.dart'; import 'package:http/http.dart'; class TrocadorExchangeProvider extends ExchangeProvider { - TrocadorExchangeProvider({this.useTorOnly = false}) - : _lastUsedRateId = '', - super(pairList: _supportedPairs()); + TrocadorExchangeProvider({this.useTorOnly = false, this.providerStates = const {}}) + : _lastUsedRateId = '', _provider = [], + super(pairList: supportedPairs(_notSupported)); bool useTorOnly; + final Map providerStates; + + static const List availableProviders = [ + 'Swapter', + 'StealthEx', + 'Simpleswap', + 'Swapuz' + 'ChangeNow', + 'Changehero', + 'FixedFloat', + 'LetsExchange', + 'Exolix', + 'Godex', + 'Exch', + 'CoinCraddle' + ]; static const List _notSupported = [ CryptoCurrency.stx, CryptoCurrency.zaddr, ]; - static List _supportedPairs() { - final supportedCurrencies = - CryptoCurrency.all.where((element) => !_notSupported.contains(element)).toList(); - - return supportedCurrencies - .map((i) => supportedCurrencies.map((k) => ExchangePair(from: i, to: k, reverse: true))) - .expand((i) => i) - .toList(); - } - + static const apiKey = secrets.trocadorApiKey; static const onionApiAuthority = 'trocadorfyhlu27aefre5u7zri66gudtzdyelymftvr4yjwcxhfaqsid.onion'; static const clearNetAuthority = 'trocador.app'; - static const apiKey = secrets.trocadorApiKey; static const markup = secrets.trocadorExchangeMarkup; static const newRatePath = '/api/new_rate'; static const createTradePath = 'api/new_trade'; static const tradePath = 'api/trade'; static const coinPath = 'api/coin'; + String _lastUsedRateId; + List _provider; @override - Future checkIsAvailable() async => true; + String get title => 'Trocador'; @override - Future createTrade({required TradeRequest request, required bool isFixedRateMode}) { - final _request = request as TrocadorRequest; - return _createTrade(request: _request, isFixedRateMode: isFixedRateMode); - } + bool get isAvailable => true; - Future _createTrade({ - required TrocadorRequest request, - required bool isFixedRateMode, - }) async { - final params = { - 'api_key': apiKey, - 'ticker_from': _normalizeCurrency(request.from), - 'ticker_to': _normalizeCurrency(request.to), - 'network_from': _networkFor(request.from), - 'network_to': _networkFor(request.to), - 'payment': isFixedRateMode ? 'True' : 'False', - 'min_kycrating': 'C', - 'markup': markup, - 'best_only': 'True', - if (!isFixedRateMode) 'amount_from': request.fromAmount, - if (isFixedRateMode) 'amount_to': request.toAmount, - 'address': request.address, - 'refund': request.refundAddress - }; + @override + bool get isEnabled => true; - if (isFixedRateMode) { - await fetchRate( - from: request.from, - to: request.to, - amount: double.tryParse(request.toAmount) ?? 0, - isFixedRateMode: true, - isReceiveAmount: true, - ); - params['id'] = _lastUsedRateId; - } + @override + bool get supportsFixedRate => true; - final uri = await _getUri(createTradePath, params); - final response = await get(uri); - - if (response.statusCode == 400) { - final responseJSON = json.decode(response.body) as Map; - final error = responseJSON['error'] as String; - final message = responseJSON['message'] as String; - throw Exception('${error}\n$message'); - } - - if (response.statusCode != 200) { - throw Exception('Unexpected http status: ${response.statusCode}'); - } - - final responseJSON = json.decode(response.body) as Map; - final id = responseJSON['trade_id'] as String; - final inputAddress = responseJSON['address_provider'] as String; - final refundAddress = responseJSON['refund_address'] as String; - final status = responseJSON['status'] as String; - final state = TradeState.deserialize(raw: status); - final payoutAddress = responseJSON['address_user'] as String; - final date = responseJSON['date'] as String; - final password = responseJSON['password'] as String; - final providerId = responseJSON['id_provider'] as String; - final providerName = responseJSON['provider'] as String; - - return Trade( - id: id, - from: request.from, - to: request.to, - provider: description, - inputAddress: inputAddress, - refundAddress: refundAddress, - state: state, - password: password, - providerId: providerId, - providerName: providerName, - createdAt: DateTime.tryParse(date)?.toLocal(), - amount: responseJSON['amount_from']?.toString() ?? request.fromAmount, - payoutAddress: payoutAddress); - } + @override + bool get supportsOnionAddress => true; @override ExchangeProviderDescription get description => ExchangeProviderDescription.trocador; + @override + Future checkIsAvailable() async => true; + @override Future fetchLimits( {required CryptoCurrency from, required CryptoCurrency to, required bool isFixedRateMode}) async { - final params = { + final params = { 'api_key': apiKey, 'ticker': _normalizeCurrency(from), 'name': from.name, }; final uri = await _getUri(coinPath, params); - final response = await get(uri); - if (response.statusCode != 200) { + if (response.statusCode != 200) throw Exception('Unexpected http status: ${response.statusCode}'); - } final responseJSON = json.decode(response.body) as List; - if (responseJSON.isEmpty) { - throw Exception('No data'); - } + if (responseJSON.isEmpty) throw Exception('No data'); final coinJson = responseJSON.first as Map; @@ -170,9 +109,7 @@ class TrocadorExchangeProvider extends ExchangeProvider { required bool isFixedRateMode, required bool isReceiveAmount}) async { try { - if (amount == 0) { - return 0.0; - } + if (amount == 0) return 0.0; final params = { 'api_key': apiKey, @@ -185,7 +122,6 @@ class TrocadorExchangeProvider extends ExchangeProvider { 'payment': isFixedRateMode ? 'True' : 'False', 'min_kycrating': 'C', 'markup': markup, - 'best_only': 'True', }; final uri = await _getUri(newRatePath, params); @@ -195,9 +131,10 @@ class TrocadorExchangeProvider extends ExchangeProvider { final toAmount = double.parse(responseJSON['amount_to'].toString()); final rateId = responseJSON['trade_id'] as String? ?? ''; - if (rateId.isNotEmpty) { - _lastUsedRateId = rateId; - } + var quotes = responseJSON['quotes']['quotes'] as List; + _provider = quotes.map((quote) => quote['provider']).toList(); + + if (rateId.isNotEmpty) _lastUsedRateId = rateId; return isReceiveAmount ? (amount / fromAmount) : (toAmount / amount); } catch (e) { @@ -206,40 +143,119 @@ class TrocadorExchangeProvider extends ExchangeProvider { } } + @override + Future createTrade({required TradeRequest request, required bool isFixedRateMode}) async { + + final params = { + 'api_key': apiKey, + 'ticker_from': _normalizeCurrency(request.fromCurrency), + 'ticker_to': _normalizeCurrency(request.toCurrency), + 'network_from': _networkFor(request.fromCurrency), + 'network_to': _networkFor(request.toCurrency), + 'payment': isFixedRateMode ? 'True' : 'False', + 'min_kycrating': 'C', + 'markup': markup, + if (!isFixedRateMode) 'amount_from': request.fromAmount, + if (isFixedRateMode) 'amount_to': request.toAmount, + 'address': request.toAddress, + 'refund': request.refundAddress + }; + + if (isFixedRateMode) { + await fetchRate( + from: request.fromCurrency, + to: request.toCurrency, + amount: double.tryParse(request.toAmount) ?? 0, + isFixedRateMode: true, + isReceiveAmount: true, + ); + params['id'] = _lastUsedRateId; + } + + + String firstAvailableProvider = ''; + + for (var provider in _provider) { + if (providerStates.containsKey(provider) && providerStates[provider] == true) { + firstAvailableProvider = provider as String; + break; + } + } + + if (firstAvailableProvider.isEmpty) { + throw Exception('No available provider is enabled'); + } + + params['provider'] = firstAvailableProvider; + + final uri = await _getUri(createTradePath, params); + final response = await get(uri); + + if (response.statusCode == 400) { + final responseJSON = json.decode(response.body) as Map; + final error = responseJSON['error'] as String; + final message = responseJSON['message'] as String; + throw Exception('${error}\n$message'); + } + + if (response.statusCode != 200) + throw Exception('Unexpected http status: ${response.statusCode}'); + + final responseJSON = json.decode(response.body) as Map; + final id = responseJSON['trade_id'] as String; + final inputAddress = responseJSON['address_provider'] as String; + final refundAddress = responseJSON['refund_address'] as String; + final status = responseJSON['status'] as String; + final payoutAddress = responseJSON['address_user'] as String; + final date = responseJSON['date'] as String; + final password = responseJSON['password'] as String; + final providerId = responseJSON['id_provider'] as String; + final providerName = responseJSON['provider'] as String; + + return Trade( + id: id, + from: request.fromCurrency, + to: request.toCurrency, + provider: description, + inputAddress: inputAddress, + refundAddress: refundAddress, + state: TradeState.deserialize(raw: status), + password: password, + providerId: providerId, + providerName: providerName, + createdAt: DateTime.tryParse(date)?.toLocal(), + amount: responseJSON['amount_from']?.toString() ?? request.fromAmount, + payoutAddress: payoutAddress); + } + @override Future findTradeById({required String id}) async { final uri = await _getUri(tradePath, {'api_key': apiKey, 'id': id}); return get(uri).then((response) { - if (response.statusCode != 200) { + if (response.statusCode != 200) throw Exception('Unexpected http status: ${response.statusCode}'); - } final responseListJson = json.decode(response.body) as List; - final responseJSON = responseListJson.first; final id = responseJSON['trade_id'] as String; final payoutAddress = responseJSON['address_user'] as String; final refundAddress = responseJSON['refund_address'] as String; final inputAddress = responseJSON['address_provider'] as String; final fromAmount = responseJSON['amount_from']?.toString() ?? '0'; - final from = CryptoCurrency.fromString(responseJSON['ticker_from'] as String); - final to = CryptoCurrency.fromString(responseJSON['ticker_to'] as String); - final state = TradeState.deserialize(raw: responseJSON['status'] as String); - final date = DateTime.parse(responseJSON['date'] as String); final password = responseJSON['password'] as String; final providerId = responseJSON['id_provider'] as String; final providerName = responseJSON['provider'] as String; return Trade( id: id, - from: from, - to: to, + from: CryptoCurrency.fromString(responseJSON['ticker_from'] as String), + to: CryptoCurrency.fromString(responseJSON['ticker_to'] as String), provider: description, inputAddress: inputAddress, refundAddress: refundAddress, - createdAt: date, + createdAt: DateTime.parse(responseJSON['date'] as String), amount: fromAmount, - state: state, + state: TradeState.deserialize(raw: responseJSON['status'] as String), payoutAddress: payoutAddress, password: password, providerId: providerId, @@ -248,21 +264,6 @@ class TrocadorExchangeProvider extends ExchangeProvider { }); } - @override - bool get isAvailable => true; - - @override - bool get isEnabled => true; - - @override - bool get supportsFixedRate => true; - - @override - bool get supportsOnionAddress => true; - - @override - String get title => 'Trocador'; - String _networkFor(CryptoCurrency currency) { switch (currency) { case CryptoCurrency.eth: @@ -301,15 +302,9 @@ class TrocadorExchangeProvider extends ExchangeProvider { } Future _getUri(String path, Map queryParams) async { - if (!supportsOnionAddress) { - return Uri.https(clearNetAuthority, path, queryParams); - } - final uri = Uri.http(onionApiAuthority, path, queryParams); - if (useTorOnly) { - return uri; - } + if (useTorOnly) return uri; try { await get(uri); diff --git a/lib/exchange/sideshift/sideshift_request.dart b/lib/exchange/sideshift/sideshift_request.dart deleted file mode 100644 index 2f468a6aa..000000000 --- a/lib/exchange/sideshift/sideshift_request.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:cake_wallet/exchange/trade_request.dart'; -import 'package:cw_core/crypto_currency.dart'; - -class SideShiftRequest extends TradeRequest { - SideShiftRequest( - {required this.depositMethod, - required this.settleMethod, - required this.depositAmount, - required this.settleAddress, - required this.refundAddress}); - - final CryptoCurrency depositMethod; - final CryptoCurrency settleMethod; - final String depositAmount; - final String settleAddress; - final String refundAddress; -} diff --git a/lib/exchange/simpleswap/simpleswap_request.dart b/lib/exchange/simpleswap/simpleswap_request.dart deleted file mode 100644 index 9dc81bb2d..000000000 --- a/lib/exchange/simpleswap/simpleswap_request.dart +++ /dev/null @@ -1,21 +0,0 @@ -import 'package:cake_wallet/exchange/trade_request.dart'; -import 'package:cw_core/crypto_currency.dart'; -import 'package:flutter/material.dart'; - -class SimpleSwapRequest extends TradeRequest { - SimpleSwapRequest({ - required this.from, - required this.to, - required this.address, - required this.amount, - required this.refundAddress, - this.toAmount = '' - }); - - CryptoCurrency from; - CryptoCurrency to; - String address; - String amount; - String toAmount; - String refundAddress; -} diff --git a/lib/exchange/trade.dart b/lib/exchange/trade.dart index db8c8fb3b..4eb48c248 100644 --- a/lib/exchange/trade.dart +++ b/lib/exchange/trade.dart @@ -1,6 +1,6 @@ -import 'package:cw_core/crypto_currency.dart'; import 'package:cake_wallet/exchange/exchange_provider_description.dart'; import 'package:cake_wallet/exchange/trade_state.dart'; +import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/format_amount.dart'; import 'package:cw_core/hive_type_ids.dart'; import 'package:hive/hive.dart'; @@ -27,19 +27,15 @@ class Trade extends HiveObject { this.password, this.providerId, this.providerName, + this.fromWalletAddress }) { - if (provider != null) { - providerRaw = provider.raw; - } - if (from != null) { - fromRaw = from.raw; - } - if (to != null) { - toRaw = to.raw; - } - if (state != null) { - stateRaw = state.raw; - } + if (provider != null) providerRaw = provider.raw; + + if (from != null) fromRaw = from.raw; + + if (to != null) toRaw = to.raw; + + if (state != null) stateRaw = state.raw; } static const typeId = TRADE_TYPE_ID; @@ -106,6 +102,9 @@ class Trade extends HiveObject { @HiveField(16) String? providerName; + @HiveField(17) + String? fromWalletAddress; + static Trade fromMap(Map map) { return Trade( id: map['id'] as String, @@ -115,7 +114,9 @@ class Trade extends HiveObject { createdAt: map['date'] != null ? DateTime.fromMillisecondsSinceEpoch(map['date'] as int) : null, amount: map['amount'] as String, - walletId: map['wallet_id'] as String); + walletId: map['wallet_id'] as String, + fromWalletAddress: map['from_wallet_address'] as String? + ); } Map toMap() { @@ -126,7 +127,8 @@ class Trade extends HiveObject { 'output': to.serialize(), 'date': createdAt != null ? createdAt!.millisecondsSinceEpoch : null, 'amount': amount, - 'wallet_id': walletId + 'wallet_id': walletId, + 'from_wallet_address': fromWalletAddress }; } diff --git a/lib/exchange/trade_not_created_exeption.dart b/lib/exchange/trade_not_created_exception.dart similarity index 61% rename from lib/exchange/trade_not_created_exeption.dart rename to lib/exchange/trade_not_created_exception.dart index 181d05397..2c4a36d5f 100644 --- a/lib/exchange/trade_not_created_exeption.dart +++ b/lib/exchange/trade_not_created_exception.dart @@ -8,12 +8,5 @@ class TradeNotCreatedException implements Exception { String description; @override - String toString() { - var text = provider != null - ? S.current.trade_for_not_created(provider.title) - : S.current.trade_not_created; - text += ' $description'; - - return text; - } + String toString() => '${S.current.trade_for_not_created(provider.title)} $description'; } diff --git a/lib/exchange/trade_not_found_exception.dart b/lib/exchange/trade_not_found_exception.dart new file mode 100644 index 000000000..cad8afd78 --- /dev/null +++ b/lib/exchange/trade_not_found_exception.dart @@ -0,0 +1,13 @@ +import 'package:cake_wallet/exchange/exchange_provider_description.dart'; +import 'package:cake_wallet/generated/i18n.dart'; + +class TradeNotFoundException implements Exception { + TradeNotFoundException(this.tradeId, {required this.provider, this.description = ''}); + + String tradeId; + ExchangeProviderDescription provider; + String description; + + @override + String toString() => '${S.current.trade_id_not_found(tradeId, provider.title)} $description'; +} diff --git a/lib/exchange/trade_not_found_exeption.dart b/lib/exchange/trade_not_found_exeption.dart deleted file mode 100644 index 17e38627a..000000000 --- a/lib/exchange/trade_not_found_exeption.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:cake_wallet/exchange/exchange_provider_description.dart'; -import 'package:cake_wallet/generated/i18n.dart'; - -class TradeNotFoundException implements Exception { - TradeNotFoundException(this.tradeId, {this.provider, this.description = ''}); - - String? tradeId; - ExchangeProviderDescription? provider; - String description; - - @override - String toString() { - var text = tradeId != null && provider != null - ? S.current.trade_id_not_found(tradeId!, provider!.title) - : S.current.trade_not_found; - text += ' $description'; - - return text; - } -} diff --git a/lib/exchange/trade_request.dart b/lib/exchange/trade_request.dart index fb75ef14c..826bd8733 100644 --- a/lib/exchange/trade_request.dart +++ b/lib/exchange/trade_request.dart @@ -1 +1,20 @@ -abstract class TradeRequest {} \ No newline at end of file +import 'package:cw_core/crypto_currency.dart'; + +class TradeRequest { + TradeRequest( + {required this.fromCurrency, + required this.toCurrency, + required this.toAddress, + required this.refundAddress, + required this.fromAmount, + this.toAmount = '', + this.isFixedRate = false}); + + final CryptoCurrency fromCurrency; + final CryptoCurrency toCurrency; + final String toAddress; + final String refundAddress; + final String fromAmount; + final String toAmount; + final bool isFixedRate; +} diff --git a/lib/exchange/trade_state.dart b/lib/exchange/trade_state.dart index 98737339c..ed56d9845 100644 --- a/lib/exchange/trade_state.dart +++ b/lib/exchange/trade_state.dart @@ -1,9 +1,7 @@ -import 'package:flutter/foundation.dart'; import 'package:cw_core/enumerable_item.dart'; class TradeState extends EnumerableItem with Serializable { - const TradeState({required String raw, required String title}) - : super(raw: raw, title: title); + const TradeState({required String raw, required String title}) : super(raw: raw, title: title); @override bool operator ==(Object other) => other is TradeState && other.raw == raw; @@ -13,12 +11,10 @@ class TradeState extends EnumerableItem with Serializable { static const trading = TradeState(raw: 'trading', title: 'Trading'); static const traded = TradeState(raw: 'traded', title: 'Traded'); static const complete = TradeState(raw: 'complete', title: 'Complete'); - static const toBeCreated = - TradeState(raw: 'TO_BE_CREATED', title: 'To be created'); + static const toBeCreated = TradeState(raw: 'TO_BE_CREATED', title: 'To be created'); static const unpaid = TradeState(raw: 'UNPAID', title: 'Unpaid'); static const underpaid = TradeState(raw: 'UNDERPAID', title: 'Underpaid'); - static const paidUnconfirmed = - TradeState(raw: 'PAID_UNCONFIRMED', title: 'Paid unconfirmed'); + static const paidUnconfirmed = TradeState(raw: 'PAID_UNCONFIRMED', title: 'Paid unconfirmed'); static const paid = TradeState(raw: 'PAID', title: 'Paid'); static const btcSent = TradeState(raw: 'BTC_SENT', title: 'Btc sent'); static const timeout = TradeState(raw: 'TIMED_OUT', title: 'Timeout'); @@ -27,14 +23,22 @@ class TradeState extends EnumerableItem with Serializable { static const finished = TradeState(raw: 'finished', title: 'Finished'); static const waiting = TradeState(raw: 'waiting', title: 'Waiting'); static const processing = TradeState(raw: 'processing', title: 'Processing'); - static const waitingPayment = - TradeState(raw: 'waitingPayment', title: 'Waiting payment'); + static const waitingPayment = TradeState(raw: 'waitingPayment', title: 'Waiting payment'); static const waitingAuthorization = TradeState(raw: 'waitingAuthorization', title: 'Waiting authorization'); static const failed = TradeState(raw: 'failed', title: 'Failed'); static const completed = TradeState(raw: 'completed', title: 'Completed'); static const settling = TradeState(raw: 'settling', title: 'Settlement in progress'); static const settled = TradeState(raw: 'settled', title: 'Settlement completed'); + static const wait = TradeState(raw: 'wait', title: 'Waiting'); + static const overdue = TradeState(raw: 'overdue', title: 'Overdue'); + static const refund = TradeState(raw: 'refund', title: 'Refund'); + static const refunded = TradeState(raw: 'refunded', title: 'Refunded'); + static const confirmation = TradeState(raw: 'confirmation', title: 'Confirmation'); + static const confirmed = TradeState(raw: 'confirmed', title: 'Confirmed'); + 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) { case 'pending': @@ -77,6 +81,24 @@ class TradeState extends EnumerableItem with Serializable { return failed; case 'completed': return completed; + case 'wait': + return wait; + case 'overdue': + return overdue; + case 'refund': + return refund; + case 'refunded': + return refunded; + case 'confirmation': + return confirmation; + case 'confirmed': + return confirmed; + case 'exchanging': + return exchanging; + case 'sending': + return sending; + case 'success': + return success; default: throw Exception('Unexpected token: $raw in TradeState deserialize'); } diff --git a/lib/exchange/trocador/trocador_request.dart b/lib/exchange/trocador/trocador_request.dart deleted file mode 100644 index fbb8fdc84..000000000 --- a/lib/exchange/trocador/trocador_request.dart +++ /dev/null @@ -1,21 +0,0 @@ -import 'package:cake_wallet/exchange/trade_request.dart'; -import 'package:cw_core/crypto_currency.dart'; - -class TrocadorRequest extends TradeRequest { - TrocadorRequest( - {required this.from, - required this.to, - required this.address, - required this.fromAmount, - required this.toAmount, - required this.refundAddress, - required this.isReverse}); - - CryptoCurrency from; - CryptoCurrency to; - String address; - String fromAmount; - String toAmount; - String refundAddress; - bool isReverse; -} diff --git a/lib/exchange/utils/currency_pairs_utils.dart b/lib/exchange/utils/currency_pairs_utils.dart new file mode 100644 index 000000000..eeb1b5232 --- /dev/null +++ b/lib/exchange/utils/currency_pairs_utils.dart @@ -0,0 +1,12 @@ +import 'package:cake_wallet/exchange/exchange_pair.dart'; +import 'package:cw_core/crypto_currency.dart'; + +List supportedPairs(List notSupported) { + final supportedCurrencies = + CryptoCurrency.all.where((element) => !notSupported.contains(element)).toList(); + + return supportedCurrencies + .map((i) => supportedCurrencies.map((k) => ExchangePair(from: i, to: k, reverse: true))) + .expand((i) => i) + .toList(); +} diff --git a/lib/exchange/xmrto/xmrto_exchange_provider.dart b/lib/exchange/xmrto/xmrto_exchange_provider.dart deleted file mode 100644 index 536754e18..000000000 --- a/lib/exchange/xmrto/xmrto_exchange_provider.dart +++ /dev/null @@ -1,236 +0,0 @@ -import 'dart:convert'; -import 'package:flutter/foundation.dart'; -import 'package:http/http.dart'; -import 'package:cw_core/crypto_currency.dart'; -import 'package:cake_wallet/exchange/exchange_pair.dart'; -import 'package:cake_wallet/exchange/exchange_provider.dart'; -import 'package:cake_wallet/exchange/limits.dart'; -import 'package:cake_wallet/exchange/trade.dart'; -import 'package:cake_wallet/exchange/trade_request.dart'; -import 'package:cake_wallet/exchange/trade_state.dart'; -import 'package:cake_wallet/exchange/xmrto/xmrto_trade_request.dart'; -import 'package:cake_wallet/exchange/trade_not_created_exeption.dart'; -import 'package:cake_wallet/exchange/exchange_provider_description.dart'; -import 'package:cake_wallet/exchange/trade_not_found_exeption.dart'; -import 'package:cake_wallet/generated/i18n.dart'; - -class XMRTOExchangeProvider extends ExchangeProvider { - XMRTOExchangeProvider() - : _isAvailable = false, - super(pairList: [ - ExchangePair( - from: CryptoCurrency.xmr, to: CryptoCurrency.btc, reverse: false) - ]); - - static const userAgent = 'CakeWallet/XMR iOS'; - static const originalApiUri = 'https://xmr.to/api/v3/xmr2btc'; - static const _orderParameterUriSuffix = '/order_parameter_query'; - static const _orderStatusUriSuffix = '/order_status_query/'; - static const _orderCreateUriSuffix = '/order_create/'; - static const _headers = { - 'Content-Type': 'application/json', - 'User-Agent': userAgent - }; - - static Future _checkIsAvailable() async { - const url = originalApiUri + _orderParameterUriSuffix; - final uri = Uri.parse(url); - final response = await get(uri, headers: _headers); - return !(response.statusCode == 403); - } - - @override - String get title => 'XMR.TO'; - - @override - bool get isAvailable => _isAvailable; - - @override - bool get isEnabled => true; - - @override - bool get supportsFixedRate => false; - - @override - ExchangeProviderDescription get description => - ExchangeProviderDescription.xmrto; - - double _rate = 0; - bool _isAvailable; - - @override - Future checkIsAvailable() async { - _isAvailable = await _checkIsAvailable(); - return isAvailable; - } - - @override - Future fetchLimits({ - required CryptoCurrency from, - required CryptoCurrency to, - required bool isFixedRateMode}) async { - final url = originalApiUri + _orderParameterUriSuffix; - final uri = Uri.parse(url); - final response = await get(uri); - final correction = 0.001; - - if (response.statusCode != 200) { - return Limits(min: 0, max: 0); - } - - final responseJSON = json.decode(response.body) as Map; - double min = double.parse(responseJSON['lower_limit'] as String); - double max = double.parse(responseJSON['upper_limit'] as String); - final price = double.parse(responseJSON['price'] as String); - - if (price > 0) { - try { - min = min / price + correction; - min = _limitsFormat(min); - max = max / price - correction; - max = _limitsFormat(max); - } catch (e) { - min = 0; - max = 0; - } - } else { - min = 0; - max = 0; - } - - return Limits(min: min, max: max); - } - - @override - Future createTrade({required TradeRequest request, required bool isFixedRateMode}) async { - final _request = request as XMRTOTradeRequest; - final url = originalApiUri + _orderCreateUriSuffix; - final _amount = - _request.isBTCRequest ? _request.receiveAmount : _request.amount; - final _amountCurrency = _request.isBTCRequest - ? _request.to.toString() - : _request.from.toString(); - final pattern = '^([0-9]+([.\,][0-9]{0,8})?|[.\,][0-9]{1,8})\$'; - final isValid = RegExp(pattern).hasMatch(_amount); - - if (!isValid) { - throw TradeNotCreatedException(description, - description: S.current.xmr_to_error_description); - } - - final body = { - 'amount': _amount, - 'amount_currency': _amountCurrency, - 'btc_dest_address': _request.address}; - final uri = Uri.parse(url); - final response = - await post(uri, headers: _headers, body: json.encode(body)); - - if (response.statusCode != 201) { - if (response.statusCode == 400) { - final responseJSON = json.decode(response.body) as Map; - final error = responseJSON['error_msg'] as String; - - throw TradeNotCreatedException(description, description: error); - } - - throw TradeNotCreatedException(description); - } - - final responseJSON = json.decode(response.body) as Map; - final uuid = responseJSON["uuid"] as String; - - return Trade( - id: uuid, - provider: description, - from: _request.from, - to: _request.to, - state: TradeState.created, - amount: _request.amount, - createdAt: DateTime.now()); - } - - @override - Future findTradeById({required String id}) async { - final url = originalApiUri + _orderStatusUriSuffix; - final uri = Uri.parse(url); - final body = {'uuid': id}; - final response = - await post(uri, headers: _headers, body: json.encode(body)); - - if (response.statusCode != 200) { - if (response.statusCode == 400) { - final responseJSON = json.decode(response.body) as Map; - final error = responseJSON['error_msg'] as String; - - throw TradeNotFoundException(id, - provider: description, description: error); - } - - throw TradeNotFoundException(id, provider: description); - } - - final responseJSON = json.decode(response.body) as Map; - final address = responseJSON['receiving_subaddress'] as String; - final paymentId = responseJSON['xmr_required_payment_id_short'] as String; - final amount = responseJSON['incoming_amount_total'].toString(); - final stateRaw = responseJSON['state'] as String; - final expiredAtRaw = responseJSON['expires_at'] as String; - final expiredAt = DateTime.parse(expiredAtRaw).toLocal(); - final outputTransaction = responseJSON['btc_transaction_id'] as String; - final state = TradeState.deserialize(raw: stateRaw); - - return Trade( - id: id, - provider: description, - from: CryptoCurrency.xmr, - to: CryptoCurrency.btc, - inputAddress: address, - extraId: paymentId, - expiredAt: expiredAt, - amount: amount, - state: state, - outputTransaction: outputTransaction); - } - - @override - Future fetchRate( - {required CryptoCurrency from, - required CryptoCurrency to, - required double amount, - required bool isFixedRateMode, - required bool isReceiveAmount}) async { - if (from != CryptoCurrency.xmr && to != CryptoCurrency.btc) { - return 0; - } - - if (_rate == 0) { - _rate = await _fetchRates(); - } - - final double result = isReceiveAmount - ? _rate == 0 - ? 0 - : amount / _rate - : _rate * amount; - - return double.parse(result.toStringAsFixed(12)); - } - - Future _fetchRates() async { - try { - final url = originalApiUri + _orderParameterUriSuffix; - final uri = Uri.parse(url); - final response = await get(uri, headers: _headers); - final responseJSON = json.decode(response.body) as Map; - final price = double.parse(responseJSON['price'] as String); - - return price; - } catch (e) { - print(e.toString()); - return 0.0; - } - } - - double _limitsFormat(double limit) => double.parse(limit.toStringAsFixed(3)); -} diff --git a/lib/exchange/xmrto/xmrto_trade_request.dart b/lib/exchange/xmrto/xmrto_trade_request.dart deleted file mode 100644 index 4ff8d6645..000000000 --- a/lib/exchange/xmrto/xmrto_trade_request.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:cw_core/crypto_currency.dart'; -import 'package:cake_wallet/exchange/trade_request.dart'; - -class XMRTOTradeRequest extends TradeRequest { - XMRTOTradeRequest( - {required this.from, - required this.to, - required this.amount, - required this.receiveAmount, - required this.address, - required this.refundAddress, - required this.isBTCRequest}); - - final CryptoCurrency from; - final CryptoCurrency to; - final String amount; - final String receiveAmount; - final String address; - final String refundAddress; - final bool isBTCRequest; -} diff --git a/lib/ionia/ionia_service.dart b/lib/ionia/ionia_service.dart index 51e23ad28..0396ed7c1 100644 --- a/lib/ionia/ionia_service.dart +++ b/lib/ionia/ionia_service.dart @@ -6,7 +6,6 @@ import 'package:cake_wallet/.secrets.g.dart' as secrets; import 'package:cake_wallet/ionia/ionia_api.dart'; import 'package:cake_wallet/ionia/ionia_gift_card.dart'; import 'package:cake_wallet/ionia/ionia_category.dart'; -import 'package:platform_device_id/platform_device_id.dart'; class IoniaService { IoniaService(this.secureStorage, this.ioniaApi); @@ -112,9 +111,9 @@ class IoniaService { required String currency}) async { final username = (await secureStorage.read(key: ioniaUsernameStorageKey))!; final password = (await secureStorage.read(key: ioniaPasswordStorageKey))!; - final deviceId = await PlatformDeviceId.getDeviceId; + final deviceId = ''; return ioniaApi.purchaseGiftCard( - requestedUUID: deviceId!, + requestedUUID: deviceId, merchId: merchId, amount: amount, currency: currency, diff --git a/lib/main.dart b/lib/main.dart index 05b97dbca..6807e9185 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -5,6 +5,7 @@ import 'package:cake_wallet/entities/language_service.dart'; import 'package:cake_wallet/buy/order.dart'; import 'package:cake_wallet/locales/locale.dart'; import 'package:cake_wallet/store/yat/yat_store.dart'; +import 'package:cake_wallet/utils/device_info.dart'; import 'package:cake_wallet/utils/exception_handler.dart'; import 'package:cw_core/address_info.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; @@ -39,7 +40,6 @@ import 'package:cake_wallet/src/screens/root/root.dart'; import 'package:uni_links/uni_links.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cake_wallet/monero/monero.dart'; -import 'package:cake_wallet/wallet_type_utils.dart'; import 'package:cw_core/cake_hive.dart'; final navigatorKey = GlobalKey(); @@ -98,10 +98,10 @@ Future initializeAppConfigs() async { CakeHive.registerAdapter(WalletInfoAdapter()); } - if (!Hive.isAdapterRegistered(DERIVATION_TYPE_TYPE_ID)) { + if (!CakeHive.isAdapterRegistered(DERIVATION_TYPE_TYPE_ID)) { CakeHive.registerAdapter(DerivationTypeAdapter()); } - + if (!CakeHive.isAdapterRegistered(WALLET_TYPE_TYPE_ID)) { CakeHive.registerAdapter(WalletTypeAdapter()); } @@ -118,7 +118,7 @@ Future initializeAppConfigs() async { CakeHive.registerAdapter(OrderAdapter()); } - if (!isMoneroOnly && !CakeHive.isAdapterRegistered(UnspentCoinsInfo.typeId)) { + if (!CakeHive.isAdapterRegistered(UnspentCoinsInfo.typeId)) { CakeHive.registerAdapter(UnspentCoinsInfoAdapter()); } @@ -126,14 +126,17 @@ Future initializeAppConfigs() async { CakeHive.registerAdapter(AnonpayInvoiceInfoAdapter()); } - final secureStorage = FlutterSecureStorage(); + final secureStorage = FlutterSecureStorage( + iOptions: IOSOptions(accessibility: KeychainAccessibility.first_unlock), + ); final transactionDescriptionsBoxKey = await getEncryptionKey(secureStorage: secureStorage, forKey: TransactionDescription.boxKey); final tradesBoxKey = await getEncryptionKey(secureStorage: secureStorage, forKey: Trade.boxKey); final ordersBoxKey = await getEncryptionKey(secureStorage: secureStorage, forKey: Order.boxKey); final contacts = await CakeHive.openBox(Contact.boxName); final nodes = await CakeHive.openBox(Node.boxName); - final powNodes = await CakeHive.openBox(Node.boxName + "pow");// must be different from Node.boxName + final powNodes = + await CakeHive.openBox(Node.boxName + "pow"); // must be different from Node.boxName final transactionDescriptions = await CakeHive.openBox( TransactionDescription.boxName, encryptionKey: transactionDescriptionsBoxKey); @@ -160,8 +163,8 @@ Future initializeAppConfigs() async { transactionDescriptions: transactionDescriptions, secureStorage: secureStorage, anonpayInvoiceInfo: anonpayInvoiceInfo, - initialMigrationVersion: 22); - } + initialMigrationVersion: 23); +} Future initialSetup( {required SharedPreferences sharedPreferences, @@ -200,7 +203,8 @@ Future initialSetup( transactionDescriptionBox: transactionDescriptions, ordersSource: ordersSource, anonpayInvoiceInfoSource: anonpayInvoiceInfo, - unspentCoinsInfoSource: unspentCoinsInfoSource); + unspentCoinsInfoSource: unspentCoinsInfoSource, + secureStorage: secureStorage); await bootstrap(navigatorKey); monero?.onStartup(); } @@ -317,26 +321,24 @@ class _Home extends StatefulWidget { } class _HomeState extends State<_Home> { - @override + @override void didChangeDependencies() { - if(!ResponsiveLayoutUtil.instance.isMobile){ _setOrientation(context); - } + super.didChangeDependencies(); } - - void _setOrientation(BuildContext context){ - final orientation = MediaQuery.of(context).orientation; - final width = MediaQuery.of(context).size.width; - final height = MediaQuery.of(context).size.height; - if (orientation == Orientation.portrait && width < height) { - SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]); - } else if (orientation == Orientation.landscape && width > height) { - SystemChrome.setPreferredOrientations([DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight]); + void _setOrientation(BuildContext context) { + if (!DeviceInfo.instance.isDesktop) { + if (responsiveLayoutUtil.shouldRenderMobileUI) { + SystemChrome.setPreferredOrientations( + [DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]); + } else { + SystemChrome.setPreferredOrientations( + [DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight]); + } } - - } + } @override Widget build(BuildContext context) { diff --git a/lib/mastodon/mastodon_api.dart b/lib/mastodon/mastodon_api.dart new file mode 100644 index 000000000..8326ce05d --- /dev/null +++ b/lib/mastodon/mastodon_api.dart @@ -0,0 +1,63 @@ +import 'dart:convert'; +import 'package:http/http.dart' as http; +import 'package:cake_wallet/mastodon/mastodon_user.dart'; + +class MastodonAPI { + static const httpsScheme = 'https'; + static const userPath = '/api/v1/accounts/lookup'; + static const statusesPath = '/api/v1/accounts/:id/statuses'; + + static Future lookupUserByUserName( + {required String userName, required String apiHost}) async { + try { + final queryParams = {'acct': userName}; + + final uri = Uri( + scheme: httpsScheme, + host: apiHost, + path: userPath, + queryParameters: queryParams, + ); + + final response = await http.get(uri); + + if (response.statusCode != 200) return null; + + final Map responseJSON = json.decode(response.body) as Map; + + return MastodonUser.fromJson(responseJSON); + } catch (e) { + print('Error in lookupUserByUserName: $e'); + return null; + } + } + + static Future> getPinnedPosts({ + required String userId, + required String apiHost, + }) async { + try { + final queryParams = {'pinned': 'true'}; + + final uri = Uri( + scheme: httpsScheme, + host: apiHost, + path: statusesPath.replaceAll(':id', userId), + queryParameters: queryParams, + ); + + final response = await http.get(uri); + + if (response.statusCode != 200) { + throw Exception('Unexpected HTTP status: ${response.statusCode}'); + } + + final List responseJSON = json.decode(response.body) as List; + + return responseJSON.map((json) => PinnedPost.fromJson(json as Map)).toList(); + } catch (e) { + print('Error in getPinnedPosts: $e'); + throw e; + } + } +} diff --git a/lib/mastodon/mastodon_user.dart b/lib/mastodon/mastodon_user.dart new file mode 100644 index 000000000..f5a29f298 --- /dev/null +++ b/lib/mastodon/mastodon_user.dart @@ -0,0 +1,36 @@ +class MastodonUser { + String id; + String username; + String acct; + String note; + + MastodonUser({ + required this.id, + required this.username, + required this.acct, + required this.note, + }); + + factory MastodonUser.fromJson(Map json) { + return MastodonUser( + id: json['id'] as String, + username: json['username'] as String, + acct: json['acct'] as String, + note: json['note'] as String, + ); + } +} + +class PinnedPost { + final String id; + final String content; + + PinnedPost({required this.id, required this.content}); + + factory PinnedPost.fromJson(Map json) { + return PinnedPost( + id: json['id'] as String, + content: json['content'] as String, + ); + } +} diff --git a/lib/monero/cw_monero.dart b/lib/monero/cw_monero.dart index 4f45bc974..9ae248ca0 100644 --- a/lib/monero/cw_monero.dart +++ b/lib/monero/cw_monero.dart @@ -120,8 +120,6 @@ class CWMoneroWalletDetails extends MoneroWalletDetails { @computed @override MoneroBalance get balance { - final moneroWallet = _wallet as MoneroWallet; - final balance = moneroWallet.balance; throw Exception('Unimplemented'); // return MoneroBalance(); //return MoneroBalance( @@ -132,14 +130,10 @@ class CWMoneroWalletDetails extends MoneroWalletDetails { class CWMonero extends Monero { @override - MoneroAccountList getAccountList(Object wallet) { - return CWMoneroAccountList(wallet); - } + MoneroAccountList getAccountList(Object wallet) => CWMoneroAccountList(wallet); @override - MoneroSubaddressList getSubaddressList(Object wallet) { - return CWMoneroSubaddressList(wallet); - } + MoneroSubaddressList getSubaddressList(Object wallet) => CWMoneroSubaddressList(wallet); @override TransactionHistoryBase getTransactionHistory(Object wallet) { @@ -148,19 +142,13 @@ class CWMonero extends Monero { } @override - MoneroWalletDetails getMoneroWalletDetails(Object wallet) { - return CWMoneroWalletDetails(wallet); - } + MoneroWalletDetails getMoneroWalletDetails(Object wallet) => CWMoneroWalletDetails(wallet); @override - int getHeigthByDate({required DateTime date}) { - return getMoneroHeigthByDate(date: date); - } + int getHeightByDate({required DateTime date}) => getMoneroHeigthByDate(date: date); @override - TransactionPriority getDefaultTransactionPriority() { - return MoneroTransactionPriority.automatic; - } + TransactionPriority getDefaultTransactionPriority() => MoneroTransactionPriority.automatic; @override TransactionPriority getMoneroTransactionPrioritySlow() => MoneroTransactionPriority.slow; @@ -170,14 +158,11 @@ class CWMonero extends Monero { MoneroTransactionPriority.automatic; @override - TransactionPriority deserializeMoneroTransactionPriority({required int raw}) { - return MoneroTransactionPriority.deserialize(raw: raw); - } + TransactionPriority deserializeMoneroTransactionPriority({required int raw}) => + MoneroTransactionPriority.deserialize(raw: raw); @override - List getTransactionPriorities() { - return MoneroTransactionPriority.all; - } + List getTransactionPriorities() => MoneroTransactionPriority.all; @override List getMoneroWordList(String language) { @@ -209,41 +194,37 @@ class CWMonero extends Monero { @override WalletCredentials createMoneroRestoreWalletFromKeysCredentials( - {required String name, - required String spendKey, - required String viewKey, - required String address, - required String password, - required String language, - required int height}) { - return MoneroRestoreWalletFromKeysCredentials( - name: name, - spendKey: spendKey, - viewKey: viewKey, - address: address, - password: password, - language: language, - height: height); - } + {required String name, + required String spendKey, + required String viewKey, + required String address, + required String password, + required String language, + required int height}) => + MoneroRestoreWalletFromKeysCredentials( + name: name, + spendKey: spendKey, + viewKey: viewKey, + address: address, + password: password, + language: language, + height: height); @override WalletCredentials createMoneroRestoreWalletFromSeedCredentials( - {required String name, - required String password, - required int height, - required String mnemonic}) { - return MoneroRestoreWalletFromSeedCredentials( - name: name, password: password, height: height, mnemonic: mnemonic); - } + {required String name, + required String password, + required int height, + required String mnemonic}) => + MoneroRestoreWalletFromSeedCredentials( + name: name, password: password, height: height, mnemonic: mnemonic); @override WalletCredentials createMoneroNewWalletCredentials({ required String name, required String language, - String? password, - }) { - return MoneroNewWalletCredentials(name: name, password: password, language: language); - } + String? password}) => + MoneroNewWalletCredentials(name: name, password: password, language: language); @override Map getKeys(Object wallet) { @@ -259,43 +240,37 @@ class CWMonero extends Monero { @override Object createMoneroTransactionCreationCredentials( - {required List outputs, required TransactionPriority priority}) { - return MoneroTransactionCreationCredentials( - outputs: outputs - .map((out) => OutputInfo( - fiatAmount: out.fiatAmount, - cryptoAmount: out.cryptoAmount, - address: out.address, - note: out.note, - sendAll: out.sendAll, - extractedAddress: out.extractedAddress, - isParsedAddress: out.isParsedAddress, - formattedCryptoAmount: out.formattedCryptoAmount)) - .toList(), - priority: priority as MoneroTransactionPriority); - } + {required List outputs, required TransactionPriority priority}) => + MoneroTransactionCreationCredentials( + outputs: outputs + .map((out) => OutputInfo( + fiatAmount: out.fiatAmount, + cryptoAmount: out.cryptoAmount, + address: out.address, + note: out.note, + sendAll: out.sendAll, + extractedAddress: out.extractedAddress, + isParsedAddress: out.isParsedAddress, + formattedCryptoAmount: out.formattedCryptoAmount)) + .toList(), + priority: priority as MoneroTransactionPriority); @override Object createMoneroTransactionCreationCredentialsRaw( - {required List outputs, required TransactionPriority priority}) { - return MoneroTransactionCreationCredentials( - outputs: outputs, priority: priority as MoneroTransactionPriority); - } + {required List outputs, required TransactionPriority priority}) => + MoneroTransactionCreationCredentials( + outputs: outputs, priority: priority as MoneroTransactionPriority); @override - String formatterMoneroAmountToString({required int amount}) { - return moneroAmountToString(amount: amount); - } + String formatterMoneroAmountToString({required int amount}) => + moneroAmountToString(amount: amount); @override - double formatterMoneroAmountToDouble({required int amount}) { - return moneroAmountToDouble(amount: amount); - } + double formatterMoneroAmountToDouble({required int amount}) => + moneroAmountToDouble(amount: amount); @override - int formatterMoneroParseAmount({required String amount}) { - return moneroParseAmount(amount: amount); - } + int formatterMoneroParseAmount({required String amount}) => moneroParseAmount(amount: amount); @override Account getCurrentAccount(Object wallet) { @@ -312,9 +287,7 @@ class CWMonero extends Monero { } @override - void onStartup() { - monero_wallet_api.onStartup(); - } + void onStartup() => monero_wallet_api.onStartup(); @override int getTransactionInfoAccountId(TransactionInfo tx) { @@ -324,9 +297,8 @@ class CWMonero extends Monero { @override WalletService createMoneroWalletService( - Box walletInfoSource, Box unspentCoinSource) { - return MoneroWalletService(walletInfoSource, unspentCoinSource); - } + Box walletInfoSource, Box unspentCoinSource) => + MoneroWalletService(walletInfoSource, unspentCoinSource); @override String getTransactionAddress(Object wallet, int accountIndex, int addressIndex) { @@ -349,10 +321,7 @@ class CWMonero extends Monero { @override List getUnspents(Object wallet) { final moneroWallet = wallet as MoneroWallet; - return moneroWallet.unspentCoins - .map((MoneroUnspent moneroUnspent) => Unspent(moneroUnspent.address, moneroUnspent.hash, - moneroUnspent.value, 0, moneroUnspent.keyImage)) - .toList(); + return moneroWallet.unspentCoins; } @override diff --git a/lib/nano/cw_nano.dart b/lib/nano/cw_nano.dart index 5355ebdbd..43ebbe277 100644 --- a/lib/nano/cw_nano.dart +++ b/lib/nano/cw_nano.dart @@ -79,10 +79,6 @@ class CWNano extends Nano { return NanoWalletService(walletInfoSource); } - String getTransactionAddress(Object wallet, int accountIndex, int addressIndex) { - throw UnimplementedError(); - } - @override Map getKeys(Object wallet) { final nanoWallet = wallet as NanoWallet; @@ -152,14 +148,6 @@ class CWNano extends Nano { ); } - @override - TransactionHistoryBase getTransactionHistory(Object wallet) { - throw UnimplementedError(); - } - - @override - void onStartup() {} - @override Object createNanoTransactionCredentials(List outputs) { return NanoTransactionCredentials( @@ -179,10 +167,360 @@ class CWNano extends Nano { } @override - Future setLabelAccount(Object wallet, - {required int accountIndex, required String label}) async { - final nanoWallet = wallet as NanoWallet; - await nanoWallet.walletAddresses.accountList - .setLabelAccount(accountIndex: accountIndex, label: label); + Future changeRep(Object wallet, String address) async { + if ((wallet as NanoWallet).transactionHistory.transactions.isEmpty) { + throw Exception("Can't change representative without an existing transaction history"); + } + return wallet.changeRep(address); + } + + @override + Future updateTransactions(Object wallet) async { + return (wallet as NanoWallet).updateTransactions(); + } + + @override + BigInt getTransactionAmountRaw(TransactionInfo transactionInfo) { + return (transactionInfo as NanoTransactionInfo).amountRaw; + } + + @override + String getRepresentative(Object wallet) { + return (wallet as NanoWallet).representative; + } +} + +class CWNanoUtil extends NanoUtil { + // standard: + @override + String seedToPrivate(String seed, int index) { + return ND.NanoKeys.seedToPrivate(seed, index); + } + + @override + String seedToAddress(String seed, int index) { + return ND.NanoAccounts.createAccount( + ND.NanoAccountType.NANO, privateKeyToPublic(seedToPrivate(seed, index))); + } + + @override + String seedToMnemonic(String seed) { + return NanoMnemomics.seedToMnemonic(seed).join(" "); + } + + @override + Future mnemonicToSeed(String mnemonic) async { + return NanoMnemomics.mnemonicListToSeed(mnemonic.split(' ')); + } + + @override + String privateKeyToPublic(String privateKey) { + // return NanoHelpers.byteToHex(Ed25519Blake2b.getPubkey(NanoHelpers.hexToBytes(privateKey))!); + return ND.NanoKeys.createPublicKey(privateKey); + } + + @override + String addressToPublicKey(String publicAddress) { + return ND.NanoAccounts.extractPublicKey(publicAddress); + } + + // universal: + @override + String privateKeyToAddress(String privateKey) { + return ND.NanoAccounts.createAccount(ND.NanoAccountType.NANO, privateKeyToPublic(privateKey)); + } + + @override + String publicKeyToAddress(String publicKey) { + return ND.NanoAccounts.createAccount(ND.NanoAccountType.NANO, publicKey); + } + + // standard + hd: + @override + bool isValidSeed(String seed) { + // Ensure seed is 64 or 128 characters long + if (seed.length != 64 && seed.length != 128) { + return false; + } + // Ensure seed only contains hex characters, 0-9;A-F + return ND.NanoHelpers.isHexString(seed); + } + + // hd: + @override + Future hdMnemonicListToSeed(List words) async { + // if (words.length != 24) { + // throw Exception('Expected a 24-word list, got a ${words.length} list'); + // } + final Uint8List salt = Uint8List.fromList(utf8.encode('mnemonic')); + final Pbkdf2 hasher = Pbkdf2(iterations: 2048); + final String seed = await hasher.sha512(words.join(' '), salt); + return seed; + } + + @override + Future hdSeedToPrivate(String seed, int index) async { + List seedBytes = hex.decode(seed); + KeyData data = await ED25519_HD_KEY.derivePath("m/44'/165'/$index'", seedBytes); + return hex.encode(data.key); + } + + @override + Future hdSeedToAddress(String seed, int index) async { + return ND.NanoAccounts.createAccount( + ND.NanoAccountType.NANO, privateKeyToPublic(await hdSeedToPrivate(seed, index))); + } + + @override + Future uniSeedToAddress(String seed, int index, String type) { + if (type == "standard") { + return Future.value(seedToAddress(seed, index)); + } else if (type == "hd") { + return hdSeedToAddress(seed, index); + } else { + throw Exception('Unknown seed type'); + } + } + + @override + Future uniSeedToPrivate(String seed, int index, String type) { + if (type == "standard") { + return Future.value(seedToPrivate(seed, index)); + } else if (type == "hd") { + return hdSeedToPrivate(seed, index); + } else { + throw Exception('Unknown seed type'); + } + } + + @override + bool isValidBip39Seed(String seed) { + // Ensure seed is 128 characters long + if (seed.length != 128) { + return false; + } + // Ensure seed only contains hex characters, 0-9;A-F + return ND.NanoHelpers.isHexString(seed); + } + + // number util: + + static const int maxDecimalDigits = 6; // Max digits after decimal + BigInt rawPerNano = BigInt.parse("1000000000000000000000000000000"); + BigInt rawPerNyano = BigInt.parse("1000000000000000000000000"); + BigInt rawPerBanano = BigInt.parse("100000000000000000000000000000"); + BigInt rawPerXMR = BigInt.parse("1000000000000"); + BigInt convertXMRtoNano = BigInt.parse("1000000000000000000"); + // static BigInt convertXMRtoNano = BigInt.parse("1000000000000000000000000000"); + + /// Convert raw to ban and return as BigDecimal + /// + /// @param raw 100000000000000000000000000000 + /// @return Decimal value 1.000000000000000000000000000000 + /// + Decimal _getRawAsDecimal(String? raw, BigInt? rawPerCur) { + rawPerCur ??= rawPerNano; + final Decimal amount = Decimal.parse(raw.toString()); + final Decimal result = (amount / Decimal.parse(rawPerCur.toString())).toDecimal(); + return result; + } + + @override + String getRawAsDecimalString(String? raw, BigInt? rawPerCur) { + final Decimal result = _getRawAsDecimal(raw, rawPerCur); + return result.toString(); + } + + @override + String truncateDecimal(Decimal input, {int digits = maxDecimalDigits}) { + Decimal bigger = input.shift(digits); + bigger = bigger.floor(); // chop off the decimal: 1.059 -> 1.05 + bigger = bigger.shift(-digits); + return bigger.toString(); + } + + /// Return raw as a NANO amount. + /// + /// @param raw 100000000000000000000000000000 + /// @returns 1 + /// + @override + String getRawAsUsableString(String? raw, BigInt rawPerCur) { + final String res = + truncateDecimal(_getRawAsDecimal(raw, rawPerCur), digits: maxDecimalDigits + 9); + + if (raw == null || raw == "0" || raw == "00000000000000000000000000000000") { + return "0"; + } + + if (!res.contains(".")) { + return res; + } + + final String numAmount = res.split(".")[0]; + String decAmount = res.split(".")[1]; + + // truncate: + if (decAmount.length > maxDecimalDigits) { + decAmount = decAmount.substring(0, maxDecimalDigits); + // remove trailing zeros: + decAmount = decAmount.replaceAllMapped(RegExp(r'0+$'), (Match match) => ''); + if (decAmount.isEmpty) { + return numAmount; + } + } + + return "$numAmount.$decAmount"; + } + + @override + String getRawAccuracy(String? raw, BigInt rawPerCur) { + final String rawString = getRawAsUsableString(raw, rawPerCur); + final String rawDecimalString = _getRawAsDecimal(raw, rawPerCur).toString(); + + if (raw == null || raw.isEmpty || raw == "0") { + return ""; + } + + if (rawString != rawDecimalString) { + return "~"; + } + return ""; + } + + /// Return readable string amount as raw string + /// @param amount 1.01 + /// @returns 101000000000000000000000000000 + /// + @override + String getAmountAsRaw(String amount, BigInt rawPerCur) { + final Decimal asDecimal = Decimal.parse(amount); + final Decimal rawDecimal = Decimal.parse(rawPerCur.toString()); + return (asDecimal * rawDecimal).toString(); + } + + @override + Future getInfoFromSeedOrMnemonic( + DerivationType derivationType, { + String? seedKey, + String? mnemonic, + required Node node, + }) async { + NanoClient nanoClient = NanoClient(); + nanoClient.connect(node); + late String publicAddress; + + if (seedKey != null) { + if (seedKey.length == 64) { + try { + mnemonic = nanoUtil!.seedToMnemonic(seedKey); + } catch (e) { + print("not a valid 'nano' seed key"); + } + } + if (derivationType == DerivationType.bip39) { + publicAddress = await hdSeedToAddress(seedKey, 0); + } else if (derivationType == DerivationType.nano) { + publicAddress = await seedToAddress(seedKey, 0); + } + } + + if (derivationType == DerivationType.bip39) { + if (mnemonic != null) { + seedKey = await hdMnemonicListToSeed(mnemonic.split(' ')); + publicAddress = await hdSeedToAddress(seedKey, 0); + } + } + + if (derivationType == DerivationType.nano) { + if (mnemonic != null) { + seedKey = await mnemonicToSeed(mnemonic); + publicAddress = await seedToAddress(seedKey, 0); + } + } + + AccountInfoResponse? accountInfo = await nanoClient.getAccountInfo(publicAddress); + if (accountInfo == null) { + accountInfo = AccountInfoResponse( + frontier: "", balance: "0", representative: "", confirmationHeight: 0); + } + accountInfo.address = publicAddress; + return accountInfo; + } + + @override + Future> compareDerivationMethods({ + String? mnemonic, + String? privateKey, + required Node node, + }) async { + String? seedKey = privateKey; + + if (mnemonic?.split(' ').length == 12) { + return [DerivationType.bip39]; + } + if (seedKey?.length == 128) { + return [DerivationType.bip39]; + } else if (seedKey?.length == 64) { + try { + mnemonic = nanoUtil!.seedToMnemonic(seedKey!); + } catch (e) { + print("not a valid 'nano' seed key"); + } + } + + late String publicAddressStandard; + late String publicAddressBip39; + + try { + NanoClient nanoClient = NanoClient(); + nanoClient.connect(node); + + if (mnemonic != null) { + seedKey = await hdMnemonicListToSeed(mnemonic.split(' ')); + publicAddressBip39 = await hdSeedToAddress(seedKey, 0); + + seedKey = await mnemonicToSeed(mnemonic); + publicAddressStandard = await seedToAddress(seedKey, 0); + } else if (seedKey != null) { + try { + publicAddressBip39 = await hdSeedToAddress(seedKey, 0); + } catch (e) { + return [DerivationType.nano]; + } + try { + publicAddressStandard = await seedToAddress(seedKey, 0); + } catch (e) { + return [DerivationType.bip39]; + } + } + + // check if account has a history: + AccountInfoResponse? bip39Info; + AccountInfoResponse? standardInfo; + + try { + bip39Info = await nanoClient.getAccountInfo(publicAddressBip39); + } catch (e) { + bip39Info = null; + } + try { + standardInfo = await nanoClient.getAccountInfo(publicAddressStandard); + } catch (e) { + standardInfo = null; + } + + // one of these is *probably* null: + if (bip39Info == null && standardInfo != null) { + return [DerivationType.nano]; + } else if (standardInfo == null && bip39Info != null) { + return [DerivationType.bip39]; + } + + // we don't know for sure: + return [DerivationType.nano, DerivationType.bip39]; + } catch (e) { + return [DerivationType.nano, DerivationType.bip39]; + } } } diff --git a/lib/reactions/on_current_wallet_change.dart b/lib/reactions/on_current_wallet_change.dart index cf1cf7b81..5f956dc1a 100644 --- a/lib/reactions/on_current_wallet_change.dart +++ b/lib/reactions/on_current_wallet_change.dart @@ -2,6 +2,7 @@ import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart'; import 'package:cake_wallet/entities/fiat_api_mode.dart'; import 'package:cake_wallet/entities/update_haven_rate.dart'; import 'package:cake_wallet/ethereum/ethereum.dart'; +import 'package:cake_wallet/nano/nano.dart'; import 'package:cw_core/transaction_history.dart'; import 'package:cw_core/balance.dart'; import 'package:cw_core/transaction_info.dart'; diff --git a/lib/reactions/on_wallet_sync_status_change.dart b/lib/reactions/on_wallet_sync_status_change.dart index 767bfd7e8..9a13db597 100644 --- a/lib/reactions/on_wallet_sync_status_change.dart +++ b/lib/reactions/on_wallet_sync_status_change.dart @@ -7,7 +7,7 @@ import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/balance.dart'; import 'package:cw_core/transaction_info.dart'; import 'package:cw_core/sync_status.dart'; -import 'package:wakelock/wakelock.dart'; +import 'package:wakelock_plus/wakelock_plus.dart'; ReactionDisposer? _onWalletSyncStatusChangeReaction; @@ -27,10 +27,10 @@ void startWalletSyncStatusChangeReaction( } } if (status is SyncingSyncStatus) { - await Wakelock.enable(); + await WakelockPlus.enable(); } if (status is SyncedSyncStatus || status is FailedSyncStatus) { - await Wakelock.disable(); + await WakelockPlus.disable(); } } catch(e) { print(e.toString()); diff --git a/lib/router.dart b/lib/router.dart index 799613903..f1c9a372f 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -1,15 +1,16 @@ import 'package:cake_wallet/anonpay/anonpay_info_base.dart'; import 'package:cake_wallet/anonpay/anonpay_invoice_info.dart'; import 'package:cake_wallet/core/totp_request_details.dart'; +import 'package:cake_wallet/core/wallet_connect/web3wallet_service.dart'; import 'package:cake_wallet/entities/contact_record.dart'; import 'package:cake_wallet/buy/order.dart'; import 'package:cake_wallet/entities/qr_view_data.dart'; import 'package:cake_wallet/src/screens/anonpay_details/anonpay_details_page.dart'; import 'package:cake_wallet/src/screens/backup/backup_page.dart'; import 'package:cake_wallet/src/screens/backup/edit_backup_password_page.dart'; +import 'package:cake_wallet/src/screens/buy/buy_options_page.dart'; import 'package:cake_wallet/src/screens/buy/buy_webview_page.dart'; import 'package:cake_wallet/src/screens/buy/webview_page.dart'; -import 'package:cake_wallet/src/screens/buy/pre_order_page.dart'; 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/nano/nano_change_rep_page.dart'; @@ -23,6 +24,7 @@ import 'package:cake_wallet/src/screens/dashboard/widgets/transactions_page.dart import 'package:cake_wallet/src/screens/restore/wallet_restore_choose_derivation.dart'; import 'package:cake_wallet/src/screens/settings/desktop_settings/desktop_settings_page.dart'; 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/manage_pow_nodes_page.dart'; import 'package:cake_wallet/src/screens/settings/other_settings_page.dart'; @@ -41,6 +43,8 @@ import 'package:cake_wallet/src/screens/restore/restore_from_backup_page.dart'; import 'package:cake_wallet/src/screens/restore/wallet_restore_page.dart'; import 'package:cake_wallet/src/screens/seed/pre_seed_page.dart'; import 'package:cake_wallet/src/screens/settings/connection_sync_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_qr_page.dart'; import 'package:cake_wallet/src/screens/setup_2fa/setup_2fa.dart'; @@ -50,6 +54,7 @@ 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/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_connect/wc_connections_listing_view.dart'; import 'package:cake_wallet/utils/payment_request.dart'; import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; import 'package:cake_wallet/view_model/monero_account_list/account_list_item.dart'; @@ -190,6 +195,11 @@ Route createRoute(RouteSettings settings) { param2: false)); } + case Routes.restoreWalletTypeFromQR: + return CupertinoPageRoute( + builder: (_) => getIt.get( + param1: (BuildContext context, WalletType type) => Navigator.of(context).pop(type))); + case Routes.seed: return MaterialPageRoute( fullscreenDialog: true, @@ -201,8 +211,8 @@ Route createRoute(RouteSettings settings) { case Routes.restoreWalletChooseDerivation: return MaterialPageRoute( - builder: (_) => - getIt.get(param1: settings.arguments as List)); + builder: (_) => getIt.get( + param1: settings.arguments as List)); case Routes.sweepingWalletPage: return CupertinoPageRoute(builder: (_) => getIt.get()); @@ -248,7 +258,7 @@ Route createRoute(RouteSettings settings) { return CupertinoPageRoute(builder: (_) => DisclaimerPage(isReadOnly: true)); case Routes.changeRep: - return CupertinoPageRoute(builder: (_) => NanoChangeRepPage()); + return CupertinoPageRoute(builder: (_) => getIt.get()); case Routes.seedLanguage: final args = settings.arguments as List; @@ -316,6 +326,14 @@ Route createRoute(RouteSettings settings) { return CupertinoPageRoute( fullscreenDialog: true, builder: (_) => getIt.get()); + case Routes.trocadorProvidersPage: + return CupertinoPageRoute( + fullscreenDialog: true, builder: (_) => getIt.get()); + + case Routes.domainLookupsPage: + return CupertinoPageRoute( + fullscreenDialog: true, builder: (_) => getIt.get()); + case Routes.displaySettingsPage: return CupertinoPageRoute( fullscreenDialog: true, builder: (_) => getIt.get()); @@ -378,8 +396,8 @@ Route createRoute(RouteSettings settings) { return MaterialPageRoute( builder: (_) => getIt.get(param1: settings.arguments as Order)); - case Routes.preOrder: - return MaterialPageRoute(builder: (_) => getIt.get()); + case Routes.buy: + return MaterialPageRoute(builder: (_) => getIt.get()); case Routes.buyWebView: final args = settings.arguments as List; @@ -402,7 +420,10 @@ Route createRoute(RouteSettings settings) { case Routes.preSeed: return MaterialPageRoute( - builder: (_) => getIt.get(param1: settings.arguments as WalletType)); + builder: (_) => getIt.get( + param1: settings.arguments as WalletType, + param2: getIt.get( + param1: settings.arguments as WalletType))); case Routes.backup: return CupertinoPageRoute( @@ -521,7 +542,7 @@ Route createRoute(RouteSettings settings) { return CupertinoPageRoute( builder: (_) => AdvancedPrivacySettingsPage( getIt.get(param1: type), - getIt.get(param1: type), + getIt.get(param1: type, param2: false), )); case Routes.anonPayInvoicePage: @@ -545,7 +566,7 @@ Route createRoute(RouteSettings settings) { ); case Routes.desktop_settings_page: - return CupertinoPageRoute(builder: (_) => DesktopSettingsPage()); + return CupertinoPageRoute(builder: (_) => getIt.get()); case Routes.empty_no_route: return MaterialPageRoute(builder: (_) => SizedBox.shrink()); @@ -585,7 +606,19 @@ Route createRoute(RouteSettings settings) { ); case Routes.manageNodes: - return MaterialPageRoute(builder: (_) => getIt.get()); + return MaterialPageRoute(builder: (_) => getIt.get(param1: false)); + + case Routes.managePowNodes: + return MaterialPageRoute(builder: (_) => getIt.get(param1: true)); + + case Routes.walletConnectConnectionsListing: + return MaterialPageRoute( + builder: (_) => WalletConnectConnectionsView( + web3walletService: getIt.get(), + launchUri: settings.arguments as Uri?, + )); + case Routes.torPage: + return MaterialPageRoute(builder: (_) => getIt.get()); case Routes.managePowNodes: return MaterialPageRoute(builder: (_) => getIt.get()); diff --git a/lib/routes.dart b/lib/routes.dart index d24a70e1e..4c1a917ab 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -6,6 +6,7 @@ class Routes { static const seed = '/seed'; static const restoreOptions = '/restore_options'; static const restoreWalletFromSeedKeys = '/restore_wallet_from_seeds_keys'; + static const restoreWalletTypeFromQR = '/restore_wallet_from_qr_code'; static const restoreWalletChooseDerivation = '/restore_wallet_choose_derivation'; static const dashboard = '/dashboard'; static const send = '/send'; @@ -54,7 +55,7 @@ class Routes { static const supportLiveChat = '/support/live_chat'; static const supportOtherLinks = '/support/other'; static const orderDetails = '/order_details'; - static const preOrder = '/pre_order'; + static const buy = '/buy'; static const buyWebView = '/buy_web_view'; static const unspentCoinsList = '/unspent_coins_list'; static const unspentCoinsDetails = '/unspent_coins_details'; @@ -80,6 +81,8 @@ class Routes { static const connectionSync = '/connection_sync_page'; static const securityBackupPage = '/security_and_backup_page'; static const privacyPage = '/privacy_page'; + static const trocadorProvidersPage = '/trocador_providers_page'; + static const domainLookupsPage = '/domain_lookups_page'; static const displaySettingsPage = '/display_settings_page'; static const otherSettingsPage = '/other_settings_page'; static const advancedPrivacySettings = '/advanced_privacy_settings'; @@ -97,5 +100,6 @@ 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 torPage = '/tor_page'; } diff --git a/lib/src/screens/backup/backup_page.dart b/lib/src/screens/backup/backup_page.dart index 5995e71c4..d17702724 100644 --- a/lib/src/screens/backup/backup_page.dart +++ b/lib/src/screens/backup/backup_page.dart @@ -17,7 +17,6 @@ import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; -import 'package:permission_handler/permission_handler.dart'; class BackupPage extends BasePage { BackupPage(this.backupViewModelBase); @@ -129,15 +128,8 @@ class BackupPage extends BasePage { alertTitle: S.of(context).export_backup, alertContent: S.of(context).select_destination, rightButtonText: S.of(context).save_to_downloads, - leftButtonText:S.of(context).share, + leftButtonText: S.of(context).share, actionRightButton: () async { - final permission = await Permission.storage.request(); - - if (permission.isDenied) { - Navigator.of(dialogContext).pop(); - return; - } - await backupViewModelBase.saveToDownload(backup.name, backup.content); Navigator.of(dialogContext).pop(); }, diff --git a/lib/src/screens/buy/buy_options_page.dart b/lib/src/screens/buy/buy_options_page.dart new file mode 100644 index 000000000..930878544 --- /dev/null +++ b/lib/src/screens/buy/buy_options_page.dart @@ -0,0 +1,76 @@ +import 'package:cake_wallet/buy/onramper/onramper_buy_provider.dart'; +import 'package:cake_wallet/buy/robinhood/robinhood_buy_provider.dart'; +import 'package:cake_wallet/di.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/src/widgets/option_tile.dart'; +import 'package:cake_wallet/themes/extensions/option_tile_theme.dart'; +import 'package:cake_wallet/themes/extensions/transaction_trade_theme.dart'; +import 'package:flutter/material.dart'; + +class BuyOptionsPage extends BasePage { + final iconDarkRobinhood = 'assets/images/robinhood_dark.png'; + final iconLightRobinhood = 'assets/images/robinhood_light.png'; + final iconDarkOnramper = 'assets/images/onramper_dark.png'; + final iconLightOnramper = 'assets/images/onramper_light.png'; + + @override + String get title => S.current.buy; + + @override + AppBarStyle get appBarStyle => AppBarStyle.regular; + + @override + Widget body(BuildContext context) { + final isLightMode = Theme.of(context).extension()?.useDarkImage ?? false; + final iconRobinhood = + Image.asset(isLightMode ? iconLightRobinhood : iconDarkRobinhood, height: 40, width: 40); + final iconOnramper = + Image.asset(isLightMode ? iconLightOnramper : iconDarkOnramper, height: 40, width: 40); + + return Container( + child: Center( + child: ConstrainedBox( + constraints: BoxConstraints(maxWidth: 330), + child: Column( + children: [ + Padding( + padding: EdgeInsets.only(top: 24), + child: OptionTile( + image: iconOnramper, + title: "Onramper", + description: S.of(context).onramper_option_description, + onPressed: () async => + await getIt.get().launchProvider(context), + ), + ), + Padding( + padding: EdgeInsets.only(top: 24), + child: OptionTile( + image: iconRobinhood, + title: "Robinhood Connect", + description: S.of(context).robinhood_option_description, + onPressed: () async => + await getIt.get().launchProvider(context), + ), + ), + Spacer(), + Padding( + padding: EdgeInsets.fromLTRB(24, 24, 24, 32), + child: Text( + S.of(context).select_buy_provider_notice, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + color: Theme.of(context).extension()!.detailsTitlesColor, + ), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/src/screens/buy/pre_order_page.dart b/lib/src/screens/buy/pre_order_page.dart deleted file mode 100644 index 67b9a18a1..000000000 --- a/lib/src/screens/buy/pre_order_page.dart +++ /dev/null @@ -1,304 +0,0 @@ -import 'package:cake_wallet/buy/buy_amount.dart'; -import 'package:cake_wallet/buy/buy_provider.dart'; -import 'package:cake_wallet/buy/moonpay/moonpay_buy_provider.dart'; -import 'package:cake_wallet/entities/fiat_currency.dart'; -import 'package:cake_wallet/src/widgets/picker.dart'; -import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; -import 'package:cake_wallet/themes/extensions/exchange_page_theme.dart'; -import 'package:cake_wallet/themes/extensions/keyboard_theme.dart'; -import 'package:cake_wallet/themes/extensions/send_page_theme.dart'; -import 'package:cw_core/wallet_type.dart'; -import 'package:cake_wallet/src/screens/buy/widgets/buy_list_item.dart'; -import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; -import 'package:cake_wallet/src/widgets/keyboard_done_button.dart'; -import 'package:cake_wallet/utils/show_pop_up.dart'; -import 'package:cake_wallet/view_model/buy/buy_view_model.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_mobx/flutter_mobx.dart'; -import 'package:keyboard_actions/keyboard_actions.dart'; -import 'package:cake_wallet/routes.dart'; -import 'package:cake_wallet/src/screens/base_page.dart'; -import 'package:cake_wallet/src/widgets/primary_button.dart'; -import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; -import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; -import 'package:cake_wallet/src/widgets/trail_button.dart'; -import 'package:mobx/mobx.dart'; -import 'package:url_launcher/url_launcher.dart'; - -class PreOrderPage extends BasePage { - PreOrderPage({required this.buyViewModel}) - : _amountFocus = FocusNode(), - _amountController = TextEditingController() { - _amountController.addListener(() { - final amount = _amountController.text; - - if (amount != buyViewModel.buyAmountViewModel.amount) { - buyViewModel.buyAmountViewModel.amount = amount; - buyViewModel.selectedProvider = null; - } - }); - - reaction((_) => buyViewModel.buyAmountViewModel.amount, (String amount) { - if (_amountController.text != amount) { - _amountController.text = amount; - } - if (amount.isEmpty) { - buyViewModel.selectedProvider = null; - buyViewModel.isShowProviderButtons = false; - } else { - buyViewModel.isShowProviderButtons = true; - } - }); - } - - static const _amountPattern = '^([0-9]+([.\,][0-9]{0,2})?|[.\,][0-9]{1,2})\$'; - - final BuyViewModel buyViewModel; - final FocusNode _amountFocus; - final TextEditingController _amountController; - - @override - String get title => S.current.buy + ' ' + walletTypeToString(buyViewModel.wallet.type); - - @override - bool get resizeToAvoidBottomInset => false; - - @override - bool get extendBodyBehindAppBar => true; - - @override - AppBarStyle get appBarStyle => AppBarStyle.transparent; - - @override - Widget trailing(context) => TrailButton( - caption: S.of(context).clear, - onPressed: () => buyViewModel.reset()); - - @override - Widget body(BuildContext context) { - return KeyboardActions( - config: KeyboardActionsConfig( - keyboardActionsPlatform: KeyboardActionsPlatform.IOS, - keyboardBarColor: Theme.of(context).extension()!.keyboardBarColor, - nextFocus: false, - actions: [ - KeyboardActionsItem( - focusNode: _amountFocus, - toolbarButtons: [(_) => KeyboardDoneButton()], - ), - ]), - child: Container( - height: 0, - color: Theme.of(context).colorScheme.background, - child: ScrollableWithBottomSection( - contentPadding: EdgeInsets.only(bottom: 24), - content: Observer(builder: (_) => Column( - children: [ - Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.only( - bottomLeft: Radius.circular(24), - bottomRight: Radius.circular(24)), - gradient: LinearGradient(colors: [ - Theme.of(context).extension()!.firstGradientColor, - Theme.of(context).extension()!.secondGradientColor, - ], begin: Alignment.topLeft, end: Alignment.bottomRight), - ), - child: Padding( - padding: EdgeInsets.only(top: 100, bottom: 65), - child: Center( - child: Container( - width: 210, - child: BaseTextFormField( - focusNode: _amountFocus, - controller: _amountController, - keyboardType: TextInputType.numberWithOptions(signed: false, decimal: true), - inputFormatters: [FilteringTextInputFormatter.allow(RegExp(_amountPattern))], - prefixIcon: GestureDetector( - onTap: () { - showPopUp( - context: context, - builder: (_) => Picker( - hintText: S.current.search_currency, - items: FiatCurrency.currenciesAvailableToBuyWith, - selectedAtIndex: - FiatCurrency.currenciesAvailableToBuyWith.indexOf(buyViewModel.fiatCurrency), - onItemSelected: (FiatCurrency selectedCurrency) { - buyViewModel.buyAmountViewModel.fiatCurrency = selectedCurrency; - }, - images: FiatCurrency.currenciesAvailableToBuyWith - .map((e) => Image.asset("assets/images/flags/${e.countryCode}.png")) - .toList(), - isGridView: true, - matchingCriteria: (FiatCurrency currency, String searchText) { - return currency.title.toLowerCase().contains(searchText) || - currency.fullName.toLowerCase().contains(searchText); - }, - ), - ); - }, - child: Padding( - padding: EdgeInsets.only(top: 2), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(Icons.keyboard_arrow_down, color: Colors.white), - Text( - buyViewModel.fiatCurrency.title + ': ', - style: TextStyle( - fontSize: 36, - fontWeight: FontWeight.w600, - color: Colors.white, - ), - ), - ], - ), - ), - ), - hintText: '0.00', - borderColor: Theme.of(context).extension()!.textFieldBorderBottomPanelColor, - borderWidth: 0.5, - textStyle: TextStyle(fontSize: 36, fontWeight: FontWeight.w500, color: Colors.white), - placeholderTextStyle: TextStyle( - color: Theme.of(context).extension()!.textFieldHintColor, - fontWeight: FontWeight.w500, - fontSize: 36, - ), - ), - ), - ), - ), - ), - if (buyViewModel.isShowProviderButtons) Padding( - padding: EdgeInsets.only(top: 38, bottom: 18), - child: Text( - S.of(context).buy_with + ':', - textAlign: TextAlign.center, - style: TextStyle( - color: Theme.of(context).extension()!.titleColor, - fontSize: 18, - fontWeight: FontWeight.bold - ), - ) - ), - if (buyViewModel.isShowProviderButtons) - ...buyViewModel.items.map( - (item) => Observer(builder: (_) => - FutureBuilder( - future: item.buyAmount, - builder: (context, AsyncSnapshot snapshot) { - double sourceAmount; - double destAmount; - double achAmount; - int minAmount; - - if (snapshot.hasData && snapshot.data != null) { - sourceAmount = snapshot.data!.sourceAmount; - destAmount = snapshot.data!.destAmount; - minAmount = snapshot.data!.minAmount; - achAmount = snapshot.data!.achSourceAmount ?? 0; - } else { - sourceAmount = 0.0; - destAmount = 0.0; - minAmount = 0; - achAmount = 0; - } - - return Padding( - padding: - EdgeInsets.only(left: 15, top: 20, right: 15), - child: Observer(builder: (_) { - return BuyListItem( - selectedProvider: - buyViewModel.selectedProvider, - provider: item.provider, - sourceAmount: sourceAmount, - sourceCurrency: buyViewModel.fiatCurrency, - destAmount: destAmount, - destCurrency: buyViewModel.cryptoCurrency, - achSourceAmount: achAmount, - onTap: ((buyViewModel.doubleAmount != 0.0) - && (snapshot.hasData)) ? () => - onSelectBuyProvider( - context: context, - provider: item.provider, - sourceAmount: sourceAmount, - minAmount: minAmount - ) : null - ); - }) - ); - } - )) - ) - ], - )), - bottomSectionPadding: - EdgeInsets.only(left: 24, right: 24, bottom: 24), - bottomSection: Observer(builder: (_) { - return LoadingPrimaryButton( - onPressed: () => onPresentProvider(context: context), - text: buyViewModel.selectedProvider == null - ? S.of(context).buy - : S.of(context).buy_with + - ' ${buyViewModel.selectedProvider!.description.title}', - color: Theme.of(context).primaryColor, - textColor: Colors.white, - isLoading: buyViewModel.isRunning, - isDisabled: (buyViewModel.selectedProvider == null) || - buyViewModel.isDisabled - ); - }) - ) - ) - ); - } - - void onSelectBuyProvider({required BuildContext context, required BuyProvider provider, - required double sourceAmount, required int minAmount}) { - - if ((provider is MoonPayBuyProvider)&& - (buyViewModel.buyAmountViewModel.doubleAmount < minAmount)) { - showPopUp( - context: context, - builder: (BuildContext context) { - return AlertWithOneAction( - alertTitle: 'MoonPay', - alertContent: S.of(context).moonpay_alert_text( - minAmount.toString(), - buyViewModel.fiatCurrency.toString()), - buttonText: S.of(context).ok, - buttonAction: () => Navigator.of(context).pop()); - }); - return; - } - buyViewModel.selectedProvider = provider; - sourceAmount > 0 - ? buyViewModel.isDisabled = false - : buyViewModel.isDisabled = true; - } - - Future onPresentProvider({required BuildContext context}) async { - if (buyViewModel.isRunning) { - return; - } - - buyViewModel.isRunning = true; - final url = await buyViewModel.fetchUrl(); - - if (url.isNotEmpty) { - if (buyViewModel.selectedProvider is MoonPayBuyProvider) { - if (await canLaunch(url)) await launch(url); - } else { - await Navigator.of(context) - .pushNamed(Routes.buyWebView, - arguments: [url, buyViewModel]); - } - } - - buyViewModel.reset(); - buyViewModel.isRunning = false; - } -} diff --git a/lib/src/screens/contact/contact_list_page.dart b/lib/src/screens/contact/contact_list_page.dart index b551875f9..d9e3c0c85 100644 --- a/lib/src/screens/contact/contact_list_page.dart +++ b/lib/src/screens/contact/contact_list_page.dart @@ -35,35 +35,39 @@ class ContactListPage extends BasePage { decoration: BoxDecoration( shape: BoxShape.circle, color: Theme.of(context).extension()!.buttonBackgroundColor), - child: Stack( - alignment: Alignment.center, - children: [ - Icon( - Icons.add, - color: Theme.of(context).appBarTheme.titleTextStyle!.color, - size: 22.0, - ), - ButtonTheme( - minWidth: 32.0, - height: 32.0, - child: TextButton( - // FIX-ME: Style - //shape: CircleBorder(), - onPressed: () async { - if (contactListViewModel.shouldRequireTOTP2FAForAddingContacts) { - authService.authenticateAction( - context, - route: Routes.addressBookAddContact, - conditionToDetermineIfToUse2FA: - contactListViewModel.shouldRequireTOTP2FAForAddingContacts, - ); - } else { - await Navigator.of(context).pushNamed(Routes.addressBookAddContact); - } - }, - child: Offstage()), - ) - ], + child: Semantics( + label: S.of(context).add_contact, + button: true, + child: Stack( + alignment: Alignment.center, + children: [ + Icon( + Icons.add, + color: Theme.of(context).appBarTheme.titleTextStyle!.color, + size: 22.0, + ), + ButtonTheme( + minWidth: 32.0, + height: 32.0, + child: TextButton( + // FIX-ME: Style + //shape: CircleBorder(), + onPressed: () async { + if (contactListViewModel.shouldRequireTOTP2FAForAddingContacts) { + authService.authenticateAction( + context, + route: Routes.addressBookAddContact, + conditionToDetermineIfToUse2FA: + contactListViewModel.shouldRequireTOTP2FAForAddingContacts, + ); + } else { + await Navigator.of(context).pushNamed(Routes.addressBookAddContact); + } + }, + child: Offstage()), + ) + ], + ), ), ), ); diff --git a/lib/src/screens/dashboard/dashboard_page.dart b/lib/src/screens/dashboard/dashboard_page.dart index e918e8c56..d4662c625 100644 --- a/lib/src/screens/dashboard/dashboard_page.dart +++ b/lib/src/screens/dashboard/dashboard_page.dart @@ -1,9 +1,12 @@ import 'dart:async'; +import 'package:cake_wallet/core/wallet_connect/wc_bottom_sheet_service.dart'; +import 'package:cake_wallet/core/wallet_connect/web3wallet_service.dart'; import 'package:cake_wallet/entities/preferences_key.dart'; import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/entities/main_actions.dart'; import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/desktop_sidebar_wrapper.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/market_place_page.dart'; +import 'package:cake_wallet/src/screens/wallet_connect/widgets/modals/bottom_sheet_listener.dart'; import 'package:cake_wallet/src/widgets/gradient_background.dart'; import 'package:cake_wallet/themes/extensions/sync_indicator_theme.dart'; import 'package:cake_wallet/utils/device_info.dart'; @@ -35,36 +38,37 @@ import 'package:cake_wallet/themes/extensions/balance_page_theme.dart'; class DashboardPage extends StatelessWidget { DashboardPage({ + required this.bottomSheetService, required this.balancePage, required this.dashboardViewModel, required this.addressListViewModel, }); final BalancePage balancePage; + final BottomSheetService bottomSheetService; final DashboardViewModel dashboardViewModel; final WalletAddressListViewModel addressListViewModel; @override Widget build(BuildContext context) { return Scaffold( - body: LayoutBuilder( - builder: (context, constraints) { + body: Observer( + builder: (_) { + final dashboardPageView = _DashboardPageView( + balancePage: balancePage, + bottomSheetService: bottomSheetService, + dashboardViewModel: dashboardViewModel, + addressListViewModel: addressListViewModel, + ); + if (DeviceInfo.instance.isDesktop) { - if (constraints.maxWidth > ResponsiveLayoutUtil.kDesktopMaxDashBoardWidthConstraint) { + if (responsiveLayoutUtil.screenWidth > ResponsiveLayoutUtilBase.kDesktopMaxDashBoardWidthConstraint) { return getIt.get(); } else { - return _DashboardPageView( - balancePage: balancePage, - dashboardViewModel: dashboardViewModel, - addressListViewModel: addressListViewModel, - ); + return dashboardPageView; } - } else if (ResponsiveLayoutUtil.instance.shouldRenderMobileUI()) { - return _DashboardPageView( - balancePage: balancePage, - dashboardViewModel: dashboardViewModel, - addressListViewModel: addressListViewModel, - ); + } else if (responsiveLayoutUtil.shouldRenderMobileUI) { + return dashboardPageView; } else { return getIt.get(); } @@ -76,6 +80,7 @@ class DashboardPage extends StatelessWidget { class _DashboardPageView extends BasePage { _DashboardPageView({ + required this.bottomSheetService, required this.balancePage, required this.dashboardViewModel, required this.addressListViewModel, @@ -126,6 +131,7 @@ class _DashboardPageView extends BasePage { } final DashboardViewModel dashboardViewModel; + final BottomSheetService bottomSheetService; final WalletAddressListViewModel addressListViewModel; int get initialPage => dashboardViewModel.shouldShowMarketPlaceInDashboard ? 1 : 0; @@ -158,102 +164,106 @@ class _DashboardPageView extends BasePage { return SafeArea( minimum: EdgeInsets.only(bottom: 24), - child: Column( - mainAxisSize: MainAxisSize.max, - children: [ - Expanded( - child: Observer( - builder: (context) { - return PageView.builder( - controller: controller, - itemCount: pages.length, - itemBuilder: (context, index) => pages[index], - ); - }, - ), - ), - Padding( - padding: EdgeInsets.only(bottom: 24, top: 10), - child: Observer( - builder: (context) { - return ExcludeSemantics( - child: SmoothPageIndicator( + child: BottomSheetListener( + bottomSheetService: bottomSheetService, + child: Column( + mainAxisSize: MainAxisSize.max, + children: [ + Expanded( + child: Observer( + builder: (context) { + return PageView.builder( controller: controller, - count: pages.length, - effect: ColorTransitionEffect( - spacing: 6.0, - radius: 6.0, - dotWidth: 6.0, - dotHeight: 6.0, - dotColor: Theme.of(context).indicatorColor, - activeDotColor: Theme.of(context) - .extension()! - .indicatorDotTheme - .activeIndicatorColor, - ), - ), - ); - }, + itemCount: pages.length, + itemBuilder: (context, index) => pages[index], + ); + }, + ), ), - ), - Observer( - builder: (_) { - return ClipRect( - child: Container( - margin: const EdgeInsets.only(left: 16, right: 16), - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(50.0), - border: Border.all( - color: Theme.of(context).extension()!.cardBorderColor, - width: 1, + Padding( + padding: EdgeInsets.only(bottom: 24, top: 10), + child: Observer( + builder: (context) { + return ExcludeSemantics( + child: SmoothPageIndicator( + controller: controller, + count: pages.length, + effect: ColorTransitionEffect( + spacing: 6.0, + radius: 6.0, + dotWidth: 6.0, + dotHeight: 6.0, + dotColor: Theme.of(context).indicatorColor, + activeDotColor: Theme.of(context) + .extension()! + .indicatorDotTheme + .activeIndicatorColor, ), - color: - Theme.of(context).extension()!.syncedBackgroundColor, ), + ); + }, + ), + ), + Observer( + builder: (_) { + return ClipRect( + child: Container( + margin: const EdgeInsets.only(left: 16, right: 16), child: Container( - padding: EdgeInsets.only(left: 32, right: 32), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: MainActions.all - .where((element) => element.canShow?.call(dashboardViewModel) ?? true) - .map( - (action) => Semantics( - button: true, - enabled: (action.isEnabled?.call(dashboardViewModel) ?? true), - child: ActionButton( - image: Image.asset( - action.image, - height: 24, - width: 24, - color: action.isEnabled?.call(dashboardViewModel) ?? true - ? Theme.of(context) - .extension()! - .mainActionsIconColor + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(50.0), + border: Border.all( + color: Theme.of(context).extension()!.cardBorderColor, + width: 1, + ), + color: Theme.of(context) + .extension()! + .syncedBackgroundColor, + ), + child: Container( + padding: EdgeInsets.only(left: 32, right: 32), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: MainActions.all + .where((element) => element.canShow?.call(dashboardViewModel) ?? true) + .map( + (action) => Semantics( + button: true, + enabled: (action.isEnabled?.call(dashboardViewModel) ?? true), + child: ActionButton( + image: Image.asset( + action.image, + height: 24, + width: 24, + color: action.isEnabled?.call(dashboardViewModel) ?? true + ? Theme.of(context) + .extension()! + .mainActionsIconColor + : Theme.of(context) + .extension()! + .labelTextColor, + ), + title: action.name(context), + onClick: () async => + await action.onTap(context, dashboardViewModel), + textColor: action.isEnabled?.call(dashboardViewModel) ?? true + ? null : Theme.of(context) .extension()! .labelTextColor, ), - title: action.name(context), - onClick: () async => - await action.onTap(context, dashboardViewModel), - textColor: action.isEnabled?.call(dashboardViewModel) ?? true - ? null - : Theme.of(context) - .extension()! - .labelTextColor, ), - ), - ) - .toList(), + ) + .toList(), + ), ), ), ), - ), - ); - }, - ), - ], + ); + }, + ), + ], + ), ), ); } diff --git a/lib/src/screens/dashboard/desktop_widgets/desktop_sidebar_wrapper.dart b/lib/src/screens/dashboard/desktop_widgets/desktop_sidebar_wrapper.dart index c08b80785..f73570048 100644 --- a/lib/src/screens/dashboard/desktop_widgets/desktop_sidebar_wrapper.dart +++ b/lib/src/screens/dashboard/desktop_widgets/desktop_sidebar_wrapper.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/core/wallet_connect/wc_bottom_sheet_service.dart'; import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/auth/auth_page.dart'; @@ -7,6 +8,7 @@ import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/desktop_sideba import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/desktop_sidebar/side_menu_item.dart'; import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/sync_indicator.dart'; +import 'package:cake_wallet/src/screens/wallet_connect/widgets/modals/bottom_sheet_listener.dart'; import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/desktop_sidebar_view_model.dart'; import 'package:flutter/cupertino.dart'; @@ -16,6 +18,7 @@ import 'package:cake_wallet/router.dart' as Router; import 'package:mobx/mobx.dart'; class DesktopSidebarWrapper extends BasePage { + final BottomSheetService bottomSheetService; final Widget child; final DesktopSidebarViewModel desktopSidebarViewModel; final DashboardViewModel dashboardViewModel; @@ -23,6 +26,7 @@ class DesktopSidebarWrapper extends BasePage { DesktopSidebarWrapper({ required this.child, + required this.bottomSheetService, required this.desktopSidebarViewModel, required this.dashboardViewModel, required this.desktopNavigatorKey, @@ -67,63 +71,75 @@ class DesktopSidebarWrapper extends BasePage { Widget body(BuildContext context) { _setEffects(); - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Observer(builder: (_) { - return SideMenu( - width: sideMenuWidth, - topItems: [ - SideMenuItem( - imagePath: 'assets/images/wallet_outline.png', - isSelected: desktopSidebarViewModel.currentPage == SidebarItem.dashboard, - onTap: () { - desktopSidebarViewModel.onPageChange(SidebarItem.dashboard); - desktopNavigatorKey.currentState - ?.pushNamedAndRemoveUntil(Routes.desktop_actions, (route) => false); - }, - ), - SideMenuItem( - onTap: () { - if (desktopSidebarViewModel.currentPage == SidebarItem.transactions) { + return BottomSheetListener( + bottomSheetService: bottomSheetService, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Observer(builder: (_) { + return SideMenu( + width: sideMenuWidth, + topItems: [ + SideMenuItem( + imagePath: 'assets/images/wallet_outline.png', + isSelected: desktopSidebarViewModel.currentPage == SidebarItem.dashboard, + onTap: () { + desktopSidebarViewModel.onPageChange(SidebarItem.dashboard); desktopNavigatorKey.currentState ?.pushNamedAndRemoveUntil(Routes.desktop_actions, (route) => false); - desktopSidebarViewModel.resetSidebar(); - } else { - desktopSidebarViewModel.onPageChange(SidebarItem.transactions); - desktopNavigatorKey.currentState?.pushNamed(Routes.transactionsPage); - } - }, - isSelected: desktopSidebarViewModel.currentPage == SidebarItem.transactions, - imagePath: desktopSidebarViewModel.currentPage == SidebarItem.transactions - ? selectedIconPath - : unselectedIconPath, - ), - ], - bottomItems: [ - SideMenuItem( - imagePath: 'assets/images/support_icon.png', - isSelected: desktopSidebarViewModel.currentPage == SidebarItem.support, - onTap: () => desktopSidebarViewModel.onPageChange(SidebarItem.support)), - SideMenuItem( - imagePath: 'assets/images/settings_outline.png', - isSelected: desktopSidebarViewModel.currentPage == SidebarItem.settings, - onTap: () => desktopSidebarViewModel.onPageChange(SidebarItem.settings), - ), - ], - ); - }), - Expanded( - child: PageView( - controller: pageController, - physics: NeverScrollableScrollPhysics(), - children: [ - child, - Container( - color: Theme.of(context).colorScheme.background, - padding: EdgeInsets.all(20), - child: Navigator( - initialRoute: Routes.support, + }, + ), + SideMenuItem( + onTap: () { + if (desktopSidebarViewModel.currentPage == SidebarItem.transactions) { + desktopNavigatorKey.currentState + ?.pushNamedAndRemoveUntil(Routes.desktop_actions, (route) => false); + desktopSidebarViewModel.resetSidebar(); + } else { + desktopSidebarViewModel.onPageChange(SidebarItem.transactions); + desktopNavigatorKey.currentState?.pushNamed(Routes.transactionsPage); + } + }, + isSelected: desktopSidebarViewModel.currentPage == SidebarItem.transactions, + imagePath: desktopSidebarViewModel.currentPage == SidebarItem.transactions + ? selectedIconPath + : unselectedIconPath, + ), + ], + bottomItems: [ + SideMenuItem( + imagePath: 'assets/images/support_icon.png', + isSelected: desktopSidebarViewModel.currentPage == SidebarItem.support, + onTap: () => desktopSidebarViewModel.onPageChange(SidebarItem.support)), + SideMenuItem( + imagePath: 'assets/images/settings_outline.png', + isSelected: desktopSidebarViewModel.currentPage == SidebarItem.settings, + onTap: () => desktopSidebarViewModel.onPageChange(SidebarItem.settings), + ), + ], + ); + }), + Expanded( + child: PageView( + controller: pageController, + physics: NeverScrollableScrollPhysics(), + children: [ + child, + Container( + color: Theme.of(context).colorScheme.background, + padding: EdgeInsets.all(20), + child: Navigator( + initialRoute: Routes.support, + onGenerateRoute: (settings) => Router.createRoute(settings), + onGenerateInitialRoutes: (NavigatorState navigator, String initialRouteName) { + return [ + navigator.widget.onGenerateRoute!(RouteSettings(name: initialRouteName))! + ]; + }, + ), + ), + Navigator( + initialRoute: Routes.desktop_settings_page, onGenerateRoute: (settings) => Router.createRoute(settings), onGenerateInitialRoutes: (NavigatorState navigator, String initialRouteName) { return [ @@ -131,20 +147,11 @@ class DesktopSidebarWrapper extends BasePage { ]; }, ), - ), - Navigator( - initialRoute: Routes.desktop_settings_page, - onGenerateRoute: (settings) => Router.createRoute(settings), - onGenerateInitialRoutes: (NavigatorState navigator, String initialRouteName) { - return [ - navigator.widget.onGenerateRoute!(RouteSettings(name: initialRouteName))! - ]; - }, - ), - ], + ], + ), ), - ), - ], + ], + ), ); } 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 b22acdc8b..1aa7f6c4a 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 @@ -33,6 +33,7 @@ class _DesktopWalletSelectionDropDownState extends State { contractAddress: _contractAddressController.text, decimal: int.parse(_tokenDecimalController.text), )); - Navigator.pop(context); + if (context.mounted) { + Navigator.pop(context); + } } }, text: S.of(context).save, diff --git a/lib/src/screens/dashboard/home_settings_page.dart b/lib/src/screens/dashboard/home_settings_page.dart index 9303cb053..a08b8a8a7 100644 --- a/lib/src/screens/dashboard/home_settings_page.dart +++ b/lib/src/screens/dashboard/home_settings_page.dart @@ -9,6 +9,7 @@ import 'package:cake_wallet/src/screens/settings/widgets/settings_picker_cell.da import 'package:cake_wallet/src/screens/settings/widgets/settings_switcher_cell.dart'; import 'package:cake_wallet/themes/extensions/address_theme.dart'; import 'package:cake_wallet/themes/extensions/menu_theme.dart'; +import 'package:cake_wallet/themes/extensions/picker_theme.dart'; import 'package:cake_wallet/view_model/dashboard/home_settings_view_model.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; @@ -55,7 +56,8 @@ class HomeSettingsPage extends BasePage { padding: const EdgeInsetsDirectional.only(start: 16), child: TextFormField( controller: _searchController, - style: TextStyle(color: Theme.of(context).dialogTheme.backgroundColor), + style: TextStyle( + color: Theme.of(context).extension()!.searchHintColor), decoration: InputDecoration( hintText: S.of(context).search_add_token, prefixIcon: Image.asset("assets/images/search_icon.png"), diff --git a/lib/src/screens/dashboard/widgets/address_page.dart b/lib/src/screens/dashboard/widgets/address_page.dart index 84100464c..c57613fa5 100644 --- a/lib/src/screens/dashboard/widgets/address_page.dart +++ b/lib/src/screens/dashboard/widgets/address_page.dart @@ -64,7 +64,15 @@ class AddressPage extends BasePage { @override Widget? leading(BuildContext context) { - bool isMobileView = ResponsiveLayoutUtil.instance.isMobile; + final _backButton = Icon( + Icons.arrow_back_ios, + color: titleColor(context), + size: 16, + ); + final _closeButton = + currentTheme.type == ThemeType.dark ? closeButtonImageDarkTheme : closeButtonImage; + + bool isMobileView = responsiveLayoutUtil.shouldRenderMobileUI; return MergeSemantics( child: SizedBox( @@ -79,7 +87,7 @@ class AddressPage extends BasePage { overlayColor: MaterialStateColor.resolveWith((states) => Colors.transparent), ), onPressed: () => onClose(context), - child: !isMobileView ? closeButton(context) : backButton(context), + child: !isMobileView ? _closeButton : _backButton, ), ), ), @@ -99,19 +107,22 @@ class AddressPage extends BasePage { Widget? trailing(BuildContext context) { return Material( color: Colors.transparent, - child: IconButton( - padding: EdgeInsets.zero, - constraints: BoxConstraints(), - highlightColor: Colors.transparent, - splashColor: Colors.transparent, - iconSize: 25, - onPressed: () { - ShareUtil.share( - text: addressListViewModel.uri.toString(), - context: context, - ); - }, - icon: Icon(Icons.share, size: 20, color: pageIconColor(context)), + child: Semantics( + label: S.of(context).share, + child: IconButton( + padding: EdgeInsets.zero, + constraints: BoxConstraints(), + highlightColor: Colors.transparent, + splashColor: Colors.transparent, + iconSize: 25, + onPressed: () { + ShareUtil.share( + text: addressListViewModel.uri.toString(), + context: context, + ); + }, + icon: Icon(Icons.share, size: 20, color: pageIconColor(context)), + ), ), ); } diff --git a/lib/src/screens/dashboard/widgets/balance_page.dart b/lib/src/screens/dashboard/widgets/balance_page.dart index c8d7faf11..41d4e7bb0 100644 --- a/lib/src/screens/dashboard/widgets/balance_page.dart +++ b/lib/src/screens/dashboard/widgets/balance_page.dart @@ -1,17 +1,18 @@ +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/src/screens/dashboard/widgets/home_screen_account_widget.dart'; import 'package:cake_wallet/src/screens/exchange_trade/information_page.dart'; +import 'package:cake_wallet/src/widgets/introducing_card.dart'; import 'package:cake_wallet/store/settings_store.dart'; +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/show_pop_up.dart'; -import 'package:flutter/material.dart'; import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; -import 'package:auto_size_text/auto_size_text.dart'; -import 'package:cake_wallet/src/widgets/introducing_card.dart'; -import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart'; -import 'package:cake_wallet/themes/extensions/balance_page_theme.dart'; class BalancePage extends StatelessWidget { BalancePage({required this.dashboardViewModel, required this.settingsStore}); @@ -28,47 +29,59 @@ class BalancePage extends StatelessWidget { !dashboardViewModel.balanceViewModel.isReversing, child: SingleChildScrollView( child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, children: [ - SizedBox(height: 56), - Container( - margin: const EdgeInsets.only(left: 24, bottom: 16), - child: Observer( - builder: (_) { - return Row( - children: [ - Text( - dashboardViewModel.balanceViewModel.asset, - style: TextStyle( - fontSize: 24, - fontFamily: 'Lato', - fontWeight: FontWeight.w600, - color: - Theme.of(context).extension()!.pageTitleTextColor, - height: 1, - ), - maxLines: 1, - textAlign: TextAlign.center, - ), - if (dashboardViewModel.balanceViewModel.isHomeScreenSettingsEnabled) - InkWell( - onTap: () => Navigator.pushNamed(context, Routes.homeSettings, - arguments: dashboardViewModel.balanceViewModel), - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Image.asset( - 'assets/images/home_screen_settings_icon.png', - color: Theme.of(context) - .extension()! - .pageTitleTextColor, + Observer( + builder: (_) => dashboardViewModel.balanceViewModel.hasAccounts + ? HomeScreenAccountWidget( + walletName: dashboardViewModel.name, + accountName: dashboardViewModel.subname) + : Column( + children: [ + SizedBox(height: 56), + Container( + margin: const EdgeInsets.only(left: 24, bottom: 16), + child: Observer( + builder: (_) { + return Row( + children: [ + Text( + dashboardViewModel.balanceViewModel.asset, + style: TextStyle( + fontSize: 24, + fontFamily: 'Lato', + fontWeight: FontWeight.w600, + color: Theme.of(context) + .extension()! + .pageTitleTextColor, + height: 1, + ), + maxLines: 1, + textAlign: TextAlign.center, + ), + if (dashboardViewModel + .balanceViewModel.isHomeScreenSettingsEnabled) + InkWell( + onTap: () => Navigator.pushNamed( + context, Routes.homeSettings, + arguments: dashboardViewModel.balanceViewModel), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Image.asset( + 'assets/images/home_screen_settings_icon.png', + color: Theme.of(context) + .extension()! + .pageTitleTextColor, + ), + ), + ), + ], + ); + }, ), ), - ), - ], - ); - }, - ), - ), + ], + )), Observer( builder: (_) { if (dashboardViewModel.balanceViewModel.isShowCard && @@ -152,7 +165,9 @@ class BalancePage extends StatelessWidget { children: [ GestureDetector( behavior: HitTestBehavior.opaque, - onTap: hasAdditionalBalance ? () => _showBalanceDescription(context) : null, + onTap: hasAdditionalBalance ? () => + _showBalanceDescription(context, S.current.available_balance_description) + : null, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -212,47 +227,65 @@ class BalancePage extends StatelessWidget { ], ), if (frozenBalance.isNotEmpty) - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox(height: 26), - Text( - S.current.frozen_balance, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 12, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: Theme.of(context).extension()!.labelTextColor, - height: 1, + 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, + 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: 8), - AutoSizeText( - frozenBalance, - style: TextStyle( - fontSize: 20, - 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()!.textColor, + height: 1, + ), + maxLines: 1, + textAlign: TextAlign.center, ), - maxLines: 1, - textAlign: TextAlign.center, - ), - SizedBox(height: 4), - Text( - frozenFiatBalance, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 12, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: Theme.of(context).extension()!.textColor, - height: 1, + SizedBox(height: 4), + Text( + frozenFiatBalance, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: Theme.of(context).extension()!.textColor, + height: 1, + ), ), - ), - ], + ], + ), ), if (hasAdditionalBalance) Column( @@ -303,9 +336,9 @@ class BalancePage extends StatelessWidget { ); } - void _showBalanceDescription(BuildContext context) { + void _showBalanceDescription(BuildContext context, String content) { showPopUp( context: context, - builder: (_) => InformationPage(information: S.current.available_balance_description)); + builder: (_) => InformationPage(information: content)); } } diff --git a/lib/src/screens/dashboard/widgets/header_row.dart b/lib/src/screens/dashboard/widgets/header_row.dart index 79b7b3fe6..2093a238f 100644 --- a/lib/src/screens/dashboard/widgets/header_row.dart +++ b/lib/src/screens/dashboard/widgets/header_row.dart @@ -31,21 +31,29 @@ class HeaderRow extends StatelessWidget { fontWeight: FontWeight.w500, color: Theme.of(context).extension()!.pageTitleTextColor), ), - GestureDetector( - onTap: () { - showPopUp( - context: context, - builder: (context) => - FilterWidget(dashboardViewModel: dashboardViewModel) - ); - }, - child: Container( - height: 36, - width: 36, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Theme.of(context).extension()!.buttonColor), - child: filterIcon, + Semantics( + container: true, + child: GestureDetector( + onTap: () { + showPopUp( + context: context, + builder: (context) => FilterWidget(dashboardViewModel: dashboardViewModel), + ); + }, + child: Semantics( + label: 'Transaction Filter', + button: true, + enabled: true, + child: Container( + height: 36, + width: 36, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Theme.of(context).extension()!.buttonColor, + ), + child: filterIcon, + ), + ), ), ) ], diff --git a/lib/src/screens/dashboard/widgets/home_screen_account_widget.dart b/lib/src/screens/dashboard/widgets/home_screen_account_widget.dart new file mode 100644 index 000000000..f548a8737 --- /dev/null +++ b/lib/src/screens/dashboard/widgets/home_screen_account_widget.dart @@ -0,0 +1,69 @@ +import 'package:cake_wallet/di.dart'; +import 'package:cake_wallet/src/screens/monero_accounts/monero_account_list_page.dart'; +import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; +import 'package:flutter/material.dart'; + +class HomeScreenAccountWidget extends StatelessWidget { + HomeScreenAccountWidget({this.walletName, this.accountName}); + + final String? walletName; + final String? accountName; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () async { + await showPopUp( + context: context, + builder: (_) => getIt.get()); + }, + child: Container( + height: 100.0, + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + child: Text( + walletName ?? '', + style: TextStyle( + fontSize: 22.0, + fontWeight: FontWeight.bold, + fontFamily: 'Lato', + color: Theme.of(context).extension()!.pageTitleTextColor, + ), + ), + ), + SizedBox( + height: 5.0, + ), + Container( + child: Text( + accountName ?? '', + style: TextStyle( + fontSize: 14.0, + fontWeight: FontWeight.bold, + fontFamily: 'Lato', + color: Theme.of(context).extension()!.pageTitleTextColor, + ), + ), + ), + ], + ), + Container( + child: Icon( + Icons.keyboard_arrow_down, + color: Theme.of(context).extension()!.pageTitleTextColor, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/src/screens/dashboard/widgets/menu_widget.dart b/lib/src/screens/dashboard/widgets/menu_widget.dart index d9e4d0f7c..bf001eb32 100644 --- a/lib/src/screens/dashboard/widgets/menu_widget.dart +++ b/lib/src/screens/dashboard/widgets/menu_widget.dart @@ -31,7 +31,8 @@ class MenuWidgetState extends State { this.havenIcon = Image.asset('assets/images/haven_menu.png'), this.ethereumIcon = Image.asset('assets/images/eth_icon.png'), this.nanoIcon = Image.asset('assets/images/nano_icon.png'), - this.bananoIcon = Image.asset('assets/images/nano_icon.png'); + this.bananoIcon = Image.asset('assets/images/nano_icon.png'), + this.bitcoinCashIcon = Image.asset('assets/images/bch_icon.png'); final largeScreen = 731; @@ -50,10 +51,10 @@ class MenuWidgetState extends State { Image litecoinIcon; Image havenIcon; Image ethereumIcon; + Image bitcoinCashIcon; Image nanoIcon; Image bananoIcon; - @override void initState() { menuWidth = 0; @@ -212,6 +213,8 @@ class MenuWidgetState extends State { return havenIcon; case WalletType.ethereum: return ethereumIcon; + case WalletType.bitcoinCash: + return bitcoinCashIcon; case WalletType.nano: return nanoIcon; case WalletType.banano: diff --git a/lib/src/screens/dashboard/widgets/present_receive_option_picker.dart b/lib/src/screens/dashboard/widgets/present_receive_option_picker.dart index aae42049b..33bceeb5c 100644 --- a/lib/src/screens/dashboard/widgets/present_receive_option_picker.dart +++ b/lib/src/screens/dashboard/widgets/present_receive_option_picker.dart @@ -1,5 +1,5 @@ +import 'package:cake_wallet/src/widgets/alert_close_button.dart'; import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; -import 'package:cake_wallet/palette.dart'; import 'package:cake_wallet/src/screens/ionia/widgets/rounded_checkbox.dart'; import 'package:cake_wallet/src/widgets/alert_background.dart'; import 'package:cake_wallet/typography.dart'; @@ -71,77 +71,69 @@ class PresentReceiveOptionPicker extends StatelessWidget { builder: (BuildContext popUpContext) => Scaffold( resizeToAvoidBottomInset: false, backgroundColor: Colors.transparent, - body: AlertBackground( - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Spacer(), - Container( - margin: EdgeInsets.symmetric(horizontal: 24), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(30), - color: Theme.of(context).colorScheme.background, - ), - child: Padding( - padding: const EdgeInsets.only(top: 24, bottom: 24), - child: (ListView.separated( - padding: EdgeInsets.zero, - shrinkWrap: true, - itemCount: receiveOptionViewModel.options.length, - itemBuilder: (_, index) { - final option = receiveOptionViewModel.options[index]; - return InkWell( - onTap: () { - Navigator.pop(popUpContext); + body: Stack( + alignment: AlignmentDirectional.center, + children:[ AlertBackground( + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Spacer(), + Container( + margin: EdgeInsets.symmetric(horizontal: 24), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(30), + color: Theme.of(context).colorScheme.background, + ), + child: Padding( + padding: const EdgeInsets.only(top: 24, bottom: 24), + child: (ListView.separated( + padding: EdgeInsets.zero, + shrinkWrap: true, + itemCount: receiveOptionViewModel.options.length, + itemBuilder: (_, index) { + final option = receiveOptionViewModel.options[index]; + return InkWell( + onTap: () { + Navigator.pop(popUpContext); - receiveOptionViewModel.selectReceiveOption(option); - }, - child: Padding( - padding: const EdgeInsets.only(left: 24, right: 24), - child: Observer(builder: (_) { - final value = receiveOptionViewModel.selectedReceiveOption; + receiveOptionViewModel.selectReceiveOption(option); + }, + child: Padding( + padding: const EdgeInsets.only(left: 24, right: 24), + child: Observer(builder: (_) { + final value = receiveOptionViewModel.selectedReceiveOption; - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(option.toString(), - textAlign: TextAlign.left, - style: textSmall( - color: Theme.of(context).extension()!.titleColor, - ).copyWith( - fontWeight: - value == option ? FontWeight.w800 : FontWeight.w500, - )), - RoundedCheckbox( - value: value == option, - ) - ], - ); - }), - ), - ); - }, - separatorBuilder: (_, index) => SizedBox(height: 30), - )), - ), - ), - Spacer(), - Container( - margin: EdgeInsets.only(bottom: 40), - child: InkWell( - onTap: () => Navigator.pop(popUpContext), - child: CircleAvatar( - child: Icon( - Icons.close, - color: Palette.darkBlueCraiola, - ), - backgroundColor: Colors.white, + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(option.toString(), + textAlign: TextAlign.left, + style: textSmall( + color: Theme.of(context).extension()!.titleColor, + ).copyWith( + fontWeight: + value == option ? FontWeight.w800 : FontWeight.w500, + )), + RoundedCheckbox( + value: value == option, + ) + ], + ); + }), + ), + ); + }, + separatorBuilder: (_, index) => SizedBox(height: 30), + )), ), ), - ) - ], + Spacer() + ], + ), ), + AlertCloseButton(onTap: () => Navigator.of(popUpContext).pop(), bottom: 40) + ], ), ), context: context, diff --git a/lib/src/screens/dashboard/widgets/trade_row.dart b/lib/src/screens/dashboard/widgets/trade_row.dart index a42593f24..7f570b98e 100644 --- a/lib/src/screens/dashboard/widgets/trade_row.dart +++ b/lib/src/screens/dashboard/widgets/trade_row.dart @@ -94,6 +94,9 @@ class TradeRow extends StatelessWidget { borderRadius: BorderRadius.circular(50), child: Image.asset('assets/images/trocador.png', width: 36, height: 36)); break; + case ExchangeProviderDescription.exolix: + image = Image.asset('assets/images/exolix.png', width: 36, height: 36); + break; default: image = null; } diff --git a/lib/src/screens/dashboard/widgets/transactions_page.dart b/lib/src/screens/dashboard/widgets/transactions_page.dart index 3f6b8a3c6..5c7b78f3a 100644 --- a/lib/src/screens/dashboard/widgets/transactions_page.dart +++ b/lib/src/screens/dashboard/widgets/transactions_page.dart @@ -34,7 +34,7 @@ class TransactionsPage extends StatelessWidget { onLongPressUp: () => dashboardViewModel.balanceViewModel.isReversing = !dashboardViewModel.balanceViewModel.isReversing, child: Container( - color: ResponsiveLayoutUtil.instance.isMobile + color: responsiveLayoutUtil.shouldRenderMobileUI ? null : Theme.of(context).colorScheme.background, padding: EdgeInsets.only(top: 24, bottom: 24), diff --git a/lib/src/screens/exchange/exchange_page.dart b/lib/src/screens/exchange/exchange_page.dart index 7ff9e6c30..1f441ea99 100644 --- a/lib/src/screens/exchange/exchange_page.dart +++ b/lib/src/screens/exchange/exchange_page.dart @@ -127,7 +127,7 @@ class ExchangePage extends BasePage { final _closeButton = currentTheme.type == ThemeType.dark ? closeButtonImageDarkTheme : closeButtonImage; - bool isMobileView = ResponsiveLayoutUtil.instance.isMobile; + bool isMobileView = responsiveLayoutUtil.shouldRenderMobileUI; return MergeSemantics( child: SizedBox( @@ -705,7 +705,7 @@ class ExchangePage extends BasePage { }, )); - if (ResponsiveLayoutUtil.instance.isMobile) { + if (responsiveLayoutUtil.shouldRenderMobileUI) { return MobileExchangeCardsSection( firstExchangeCard: firstExchangeCard, secondExchangeCard: secondExchangeCard, diff --git a/lib/src/screens/exchange/exchange_template_page.dart b/lib/src/screens/exchange/exchange_template_page.dart index 62f36a3fd..31f35661d 100644 --- a/lib/src/screens/exchange/exchange_template_page.dart +++ b/lib/src/screens/exchange/exchange_template_page.dart @@ -1,6 +1,6 @@ import 'package:cake_wallet/themes/extensions/exchange_page_theme.dart'; import 'package:cake_wallet/themes/extensions/keyboard_theme.dart'; -import 'package:cake_wallet/exchange/exchange_provider.dart'; +import 'package:cake_wallet/exchange/provider/exchange_provider.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/widgets/keyboard_done_button.dart'; import 'package:flutter/material.dart'; @@ -9,9 +9,6 @@ import 'package:keyboard_actions/keyboard_actions.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cw_core/crypto_currency.dart'; -import 'package:cake_wallet/exchange/xmrto/xmrto_exchange_provider.dart'; -// import 'package:cake_wallet/exchange/exchange_trade_state.dart'; -// import 'package:cake_wallet/exchange/limits_state.dart'; import 'package:cake_wallet/src/screens/exchange/widgets/exchange_card.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; @@ -176,9 +173,7 @@ class ExchangeTemplatePage extends BasePage { exchangeViewModel.wallet.currency ? exchangeViewModel.wallet.walletAddresses.address : exchangeViewModel.receiveAddress, - initialIsAmountEditable: - exchangeViewModel.provider is - XMRTOExchangeProvider ? true : false, + initialIsAmountEditable: false, initialIsAddressEditable: exchangeViewModel.isReceiveAddressEnabled, isAmountEstimated: true, @@ -205,26 +200,25 @@ class ExchangeTemplatePage extends BasePage { bottomSectionPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24), bottomSection: Column(children: [ - Padding( - padding: EdgeInsets.only(bottom: 15), - child: Observer(builder: (_) { - final description = - exchangeViewModel.provider is XMRTOExchangeProvider - ? S.of(context).amount_is_guaranteed - : S.of(context).amount_is_estimate; - return Center( - child: Text( - description, - textAlign: TextAlign.center, - style: TextStyle( - color: Theme.of(context).extension()!.receiveAmountColor, - fontWeight: FontWeight.w500, - fontSize: 12), + Padding( + padding: EdgeInsets.only(bottom: 15), + child: Observer( + builder: (_) => Center( + child: Text( + S.of(context).amount_is_estimate, + textAlign: TextAlign.center, + style: TextStyle( + color: Theme.of(context) + .extension()! + .receiveAmountColor, + fontWeight: FontWeight.w500, + fontSize: 12, + ), + ), + ), ), - ); - }), - ), - PrimaryButton( + ), + PrimaryButton( onPressed: () { if (_formKey.currentState != null && _formKey.currentState!.validate()) { exchangeViewModel.addTemplate( @@ -340,9 +334,7 @@ class ExchangeTemplatePage extends BasePage { }); reaction((_) => exchangeViewModel.provider, (ExchangeProvider? provider) { - provider is XMRTOExchangeProvider - ? receiveKey.currentState!.isAmountEditable(isEditable: true) - : receiveKey.currentState!.isAmountEditable(isEditable: false); + receiveKey.currentState!.isAmountEditable(isEditable: false); }); /*reaction((_) => exchangeViewModel.limitsState, (LimitsState state) { diff --git a/lib/src/screens/exchange/widgets/exchange_card.dart b/lib/src/screens/exchange/widgets/exchange_card.dart index 9c4707529..b55e96e85 100644 --- a/lib/src/screens/exchange/widgets/exchange_card.dart +++ b/lib/src/screens/exchange/widgets/exchange_card.dart @@ -416,37 +416,40 @@ class ExchangeCardState extends State { width: 34, height: 34, padding: EdgeInsets.only(top: 0), - child: InkWell( - onTap: () async { - final contact = - await Navigator.of(context) - .pushNamed( - Routes.pickerAddressBook, - arguments: widget.initialCurrency, - ); + child: Semantics( + label: S.of(context).address_book, + child: InkWell( + onTap: () async { + final contact = + await Navigator.of(context) + .pushNamed( + Routes.pickerAddressBook, + arguments: widget.initialCurrency, + ); - if (contact is ContactBase && - contact.address != null) { - setState(() => - addressController.text = - contact.address); - widget.onPushAddressBookButton - ?.call(context); - } - }, - child: Container( - padding: EdgeInsets.all(8), - decoration: BoxDecoration( - color: widget - .addressButtonsColor, - borderRadius: - BorderRadius.all( - Radius.circular( - 6))), - child: Image.asset( - 'assets/images/open_book.png', - color: Theme.of(context).extension()!.textFieldButtonIconColor, - )), + if (contact is ContactBase && + contact.address != null) { + setState(() => + addressController.text = + contact.address); + widget.onPushAddressBookButton + ?.call(context); + } + }, + child: Container( + padding: EdgeInsets.all(8), + decoration: BoxDecoration( + color: widget + .addressButtonsColor, + borderRadius: + BorderRadius.all( + Radius.circular( + 6))), + child: Image.asset( + 'assets/images/open_book.png', + color: Theme.of(context).extension()!.textFieldButtonIconColor, + )), + ), )), ), Padding( @@ -455,22 +458,25 @@ class ExchangeCardState extends State { width: 34, height: 34, padding: EdgeInsets.only(top: 0), - child: InkWell( - onTap: () { - Clipboard.setData(ClipboardData( - text: addressController - .text)); - showBar( - context, - S - .of(context) - .copied_to_clipboard); - }, - child: Container( - padding: EdgeInsets.fromLTRB( - 8, 8, 0, 8), - color: Colors.transparent, - child: copyImage), + child: Semantics( + label: S.of(context).copy_address, + child: InkWell( + onTap: () { + Clipboard.setData(ClipboardData( + text: addressController + .text)); + showBar( + context, + S + .of(context) + .copied_to_clipboard); + }, + child: Container( + padding: EdgeInsets.fromLTRB( + 8, 8, 0, 8), + color: Colors.transparent, + child: copyImage), + ), ))) ]))) ])), diff --git a/lib/src/screens/exchange_trade/exchange_trade_page.dart b/lib/src/screens/exchange_trade/exchange_trade_page.dart index dbf6676a1..23595efdf 100644 --- a/lib/src/screens/exchange_trade/exchange_trade_page.dart +++ b/lib/src/screens/exchange_trade/exchange_trade_page.dart @@ -5,7 +5,6 @@ import 'package:cake_wallet/utils/request_review_handler.dart'; import 'package:mobx/mobx.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/services.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/core/execution_state.dart'; @@ -26,16 +25,15 @@ import 'package:cake_wallet/themes/extensions/transaction_trade_theme.dart'; void showInformation( ExchangeTradeViewModel exchangeTradeViewModel, BuildContext context) { - final fetchingLabel = S.current.fetching; final trade = exchangeTradeViewModel.trade; final walletName = exchangeTradeViewModel.wallet.name; final information = exchangeTradeViewModel.isSendable ? S.current.exchange_result_confirm( - trade.amount ?? fetchingLabel, trade.from.toString(), walletName) + + trade.amount, trade.from.toString(), walletName) + exchangeTradeViewModel.extraInfo : S.current.exchange_result_description( - trade.amount ?? fetchingLabel, trade.from.toString()) + + trade.amount, trade.from.toString()) + exchangeTradeViewModel.extraInfo; showPopUp( @@ -93,6 +91,8 @@ class ExchangeTradeState extends State { bool _effectsInstalled = false; + ReactionDisposer? _exchangeStateReaction; + @override void initState() { super.initState(); @@ -105,8 +105,9 @@ class ExchangeTradeState extends State { @override void dispose() { - super.dispose(); widget.exchangeTradeViewModel.timer?.cancel(); + _exchangeStateReaction?.reaction.dispose(); + super.dispose(); } @override @@ -177,7 +178,7 @@ class ExchangeTradeState extends State { ), itemBuilder: (context, index) { final item = widget.exchangeTradeViewModel.items[index]; - final value = item.data ?? fetchingLabel; + final value = item.data; final content = ListRow( title: item.title, @@ -231,7 +232,7 @@ class ExchangeTradeState extends State { return; } - reaction((_) => this.widget.exchangeTradeViewModel.sendViewModel.state, + _exchangeStateReaction = reaction((_) => this.widget.exchangeTradeViewModel.sendViewModel.state, (ExecutionState state) { if (state is FailureState) { WidgetsBinding.instance.addPostFrameCallback((_) { diff --git a/lib/src/screens/monero_accounts/monero_account_list_page.dart b/lib/src/screens/monero_accounts/monero_account_list_page.dart index 6b3d3e08b..38055854b 100644 --- a/lib/src/screens/monero_accounts/monero_account_list_page.dart +++ b/lib/src/screens/monero_accounts/monero_account_list_page.dart @@ -15,7 +15,7 @@ class MoneroAccountListPage extends StatelessWidget { @override Widget build(BuildContext context) { - double itemHeight = 80; + double itemHeight = 65; double buttonHeight = 62; return Observer(builder: (_) { @@ -31,7 +31,7 @@ class MoneroAccountListPage extends StatelessWidget { child: ListView.separated( padding: EdgeInsets.zero, controller: controller, - separatorBuilder: (context, index) => const VerticalSectionDivider(), + separatorBuilder: (context, index) => const HorizontalSectionDivider(), itemCount: accounts.length, itemBuilder: (context, index) { final account = accounts[index]; diff --git a/lib/src/screens/monero_accounts/widgets/account_tile.dart b/lib/src/screens/monero_accounts/widgets/account_tile.dart index 3e428f355..97d328214 100644 --- a/lib/src/screens/monero_accounts/widgets/account_tile.dart +++ b/lib/src/screens/monero_accounts/widgets/account_tile.dart @@ -1,5 +1,4 @@ import 'package:cake_wallet/themes/extensions/account_list_theme.dart'; -import 'package:cake_wallet/themes/extensions/receive_page_theme.dart'; import 'package:flutter/material.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:cake_wallet/generated/i18n.dart'; @@ -33,7 +32,7 @@ class AccountTile extends StatelessWidget { final Widget cell = GestureDetector( onTap: onTap, child: Container( - height: 77, + height: 60, width: double.infinity, padding: EdgeInsets.only(left: 24, right: 24), color: color, diff --git a/lib/src/screens/nano/nano_change_rep_page.dart b/lib/src/screens/nano/nano_change_rep_page.dart index 9a5235657..a625f7e29 100644 --- a/lib/src/screens/nano/nano_change_rep_page.dart +++ b/lib/src/screens/nano/nano_change_rep_page.dart @@ -1,11 +1,14 @@ import 'package:cake_wallet/core/address_validator.dart'; -import 'package:cake_wallet/di.dart'; +import 'package:cake_wallet/nano/nano.dart'; +import 'package:cake_wallet/src/widgets/address_text_field.dart'; +import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; -import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; -import 'package:cake_wallet/store/app_store.dart'; +import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cake_wallet/themes/extensions/address_theme.dart'; +import 'package:cake_wallet/utils/payment_request.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cw_core/crypto_currency.dart'; -import 'package:cw_nano/nano_wallet.dart'; +import 'package:cw_core/wallet_base.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:cake_wallet/generated/i18n.dart'; @@ -14,27 +17,28 @@ import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; class NanoChangeRepPage extends BasePage { - - NanoChangeRepPage() - : _formKey = GlobalKey(), - _addressController = TextEditingController() { - var wallet = getIt.get().wallet!; - if (wallet is NanoWallet /*|| wallet is BananoWallet*/) { - _addressController.text = wallet.representative; - } + NanoChangeRepPage({required SettingsStore settingsStore, required WalletBase wallet}) + : _wallet = wallet, + _settingsStore = settingsStore, + _addressController = TextEditingController(), + _formKey = GlobalKey() { + _addressController.text = nano!.getRepresentative(wallet); } - final GlobalKey _formKey; final TextEditingController _addressController; + final WalletBase _wallet; + final SettingsStore _settingsStore; - // final CryptoCurrency type; + final GlobalKey _formKey; @override String get title => S.current.change_rep; @override Widget body(BuildContext context) { - return Container( + return Form( + key: _formKey, + child: Container( padding: EdgeInsets.only(left: 24, right: 24), child: ScrollableWithBottomSection( contentPadding: EdgeInsets.only(bottom: 24.0), @@ -44,9 +48,17 @@ class NanoChangeRepPage extends BasePage { Row( children: [ Expanded( - child: BaseTextFormField( + child: AddressTextField( controller: _addressController, - hintText: S.of(context).node_address, + onURIScanned: (uri) { + final paymentRequest = PaymentRequest.fromUri(uri); + _addressController.text = paymentRequest.address; + }, + options: [ + AddressTextFieldOption.paste, + AddressTextFieldOption.qrCode, + ], + buttonColor: Theme.of(context).extension()!.actionButtonColor, validator: AddressValidator(type: CryptoCurrency.nano), ), ) @@ -65,6 +77,11 @@ class NanoChangeRepPage extends BasePage { padding: EdgeInsets.only(right: 8.0), child: LoadingPrimaryButton( onPressed: () async { + if (_formKey.currentState != null && + !_formKey.currentState!.validate()) { + return; + } + final confirmed = await showPopUp( context: context, builder: (BuildContext context) { @@ -80,14 +97,29 @@ class NanoChangeRepPage extends BasePage { if (confirmed) { try { - final wallet = getIt.get().wallet!; - if (wallet is NanoWallet) { - await wallet.changeRep(_addressController.text); - } - // TODO: show message saying success: + _settingsStore.defaultNanoRep = _addressController.text; - Navigator.of(context).pop(); + await nano!.changeRep(_wallet, _addressController.text); + + await showPopUp( + context: context, + builder: (BuildContext context) { + return AlertWithOneAction( + alertTitle: S.of(context).successful, + alertContent: S.of(context).change_rep_successful, + buttonText: S.of(context).ok, + buttonAction: () => Navigator.pop(context)); + }); } catch (e) { + await showPopUp( + context: context, + builder: (BuildContext context) { + return AlertWithOneAction( + alertTitle: S.of(context).error, + alertContent: e.toString(), + buttonText: S.of(context).ok, + buttonAction: () => Navigator.pop(context)); + }); throw e; } } @@ -99,6 +131,8 @@ class NanoChangeRepPage extends BasePage { )), ], )), - )); + ), + ), + ); } } diff --git a/lib/src/screens/nano_accounts/widgets/account_tile.dart b/lib/src/screens/nano_accounts/widgets/account_tile.dart index d034ca11a..0d4a59f75 100644 --- a/lib/src/screens/nano_accounts/widgets/account_tile.dart +++ b/lib/src/screens/nano_accounts/widgets/account_tile.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/themes/extensions/account_list_theme.dart'; import 'package:flutter/material.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:cake_wallet/generated/i18n.dart'; @@ -21,11 +22,11 @@ class AccountTile extends StatelessWidget { @override Widget build(BuildContext context) { final color = isCurrent - ? Theme.of(context).textTheme.titleSmall!.decorationColor! - : Theme.of(context).textTheme.displayLarge!.decorationColor!; + ? Theme.of(context).extension()!.currentAccountBackgroundColor + : Theme.of(context).extension()!.tilesBackgroundColor; final textColor = isCurrent - ? Theme.of(context).textTheme.titleSmall!.color! - : Theme.of(context).textTheme.displayLarge!.color!; + ? Theme.of(context).extension()!.currentAccountTextColor + : Theme.of(context).extension()!.tilesTextColor; final Widget cell = GestureDetector( onTap: onTap, 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 386f3012b..66a28042d 100644 --- a/lib/src/screens/new_wallet/advanced_privacy_settings_page.dart +++ b/lib/src/screens/new_wallet/advanced_privacy_settings_page.dart @@ -1,7 +1,9 @@ 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/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'; @@ -94,6 +96,17 @@ class _AdvancedPrivacySettingsBodyState extends State( + title: S.current.seed_phrase_length, + items: SeedPhraseLength.values, + selectedItem: widget.privacySettingsViewModel.seedPhraseLength, + onItemSelected: (SeedPhraseLength length) { + widget.privacySettingsViewModel.setSeedPhraseLength(length); + }, + ); + }), ], ), bottomSectionPadding: EdgeInsets.all(24), diff --git a/lib/src/screens/new_wallet/new_wallet_page.dart b/lib/src/screens/new_wallet/new_wallet_page.dart index b9dcc5ae3..5577fcd88 100644 --- a/lib/src/screens/new_wallet/new_wallet_page.dart +++ b/lib/src/screens/new_wallet/new_wallet_page.dart @@ -96,7 +96,7 @@ class _WalletNameFormState extends State { content: Center( child: ConstrainedBox( constraints: - BoxConstraints(maxWidth: ResponsiveLayoutUtil.kDesktopMaxWidthConstraint), + BoxConstraints(maxWidth: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint), child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ 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 6f3bb078b..225e5b82d 100644 --- a/lib/src/screens/new_wallet/new_wallet_type_page.dart +++ b/lib/src/screens/new_wallet/new_wallet_type_page.dart @@ -70,7 +70,7 @@ class WalletTypeFormState extends State { Widget build(BuildContext context) { return Center( child: ConstrainedBox( - constraints: BoxConstraints(maxWidth: ResponsiveLayoutUtil.kDesktopMaxWidthConstraint), + constraints: BoxConstraints(maxWidth: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint), child: Column( children: [ Padding( diff --git a/lib/src/screens/nodes/node_create_or_edit_page.dart b/lib/src/screens/nodes/node_create_or_edit_page.dart index 3525160e8..50c1c3be5 100644 --- a/lib/src/screens/nodes/node_create_or_edit_page.dart +++ b/lib/src/screens/nodes/node_create_or_edit_page.dart @@ -69,7 +69,7 @@ class NodeCreateOrEditPage extends BasePage { @override Widget trailing(BuildContext context) => IconButton( onPressed: () async { - await nodeCreateOrEditViewModel.scanQRCodeForNewNode(); + await nodeCreateOrEditViewModel.scanQRCodeForNewNode(context); }, splashColor: Colors.transparent, highlightColor: Colors.transparent, @@ -78,7 +78,7 @@ class NodeCreateOrEditPage extends BasePage { 'assets/images/qr_code_icon.png', ), ); - + final NodeCreateOrEditViewModel nodeCreateOrEditViewModel; final Node? editingNode; final bool? isSelected; @@ -133,27 +133,20 @@ class NodeCreateOrEditPage extends BasePage { mainAxisAlignment: MainAxisAlignment.center, children: [ Flexible( - child: Container( - padding: EdgeInsets.only(right: 8.0), - child: LoadingPrimaryButton( + child: Container( + padding: EdgeInsets.only(right: 8.0), + child: LoadingPrimaryButton( onPressed: () async { final confirmed = await showPopUp( context: context, builder: (BuildContext context) { return AlertWithTwoActions( - alertTitle: - S.of(context).remove_node, - alertContent: S - .of(context) - .remove_node_message, - rightButtonText: - S.of(context).remove, - leftButtonText: - S.of(context).cancel, - actionRightButton: () => - Navigator.pop(context, true), - actionLeftButton: () => - Navigator.pop(context, false)); + alertTitle: S.of(context).remove_node, + alertContent: S.of(context).remove_node_message, + rightButtonText: S.of(context).remove, + leftButtonText: S.of(context).cancel, + actionRightButton: () => Navigator.pop(context, true), + actionLeftButton: () => Navigator.pop(context, false)); }) ?? false; @@ -163,11 +156,14 @@ class NodeCreateOrEditPage extends BasePage { } }, text: S.of(context).delete, - isDisabled: !nodeCreateOrEditViewModel.isReady || + isDisabled: editingNode == null || + !nodeCreateOrEditViewModel.isReady || (isSelected ?? false), color: Palette.red, - textColor: Colors.white), - )), + textColor: Colors.white, + ), + ), + ), Flexible( child: Container( padding: EdgeInsets.only(left: 8.0), diff --git a/lib/src/screens/nodes/pow_node_create_or_edit_page.dart b/lib/src/screens/nodes/pow_node_create_or_edit_page.dart index 5dc02d66a..0b60881f6 100644 --- a/lib/src/screens/nodes/pow_node_create_or_edit_page.dart +++ b/lib/src/screens/nodes/pow_node_create_or_edit_page.dart @@ -1,11 +1,10 @@ import 'package:cake_wallet/core/execution_state.dart'; import 'package:cake_wallet/palette.dart'; import 'package:cake_wallet/src/screens/nodes/widgets/node_form.dart'; -import 'package:cake_wallet/src/screens/nodes/widgets/pow_node_form.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; -import 'package:cake_wallet/view_model/node_list/pow_node_create_or_edit_view_model.dart'; +import 'package:cake_wallet/view_model/node_list/node_create_or_edit_view_model.dart'; import 'package:cw_core/node.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; @@ -14,7 +13,6 @@ import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; -import 'package:cake_wallet/view_model/node_list/node_create_or_edit_view_model.dart'; class PowNodeCreateOrEditPage extends BasePage { PowNodeCreateOrEditPage({required this.nodeCreateOrEditViewModel,this.editingNode, this.isSelected}) @@ -71,7 +69,7 @@ class PowNodeCreateOrEditPage extends BasePage { @override Widget trailing(BuildContext context) => IconButton( onPressed: () async { - await nodeCreateOrEditViewModel.scanQRCodeForNewNode(); + await nodeCreateOrEditViewModel.scanQRCodeForNewNode(context); }, splashColor: Colors.transparent, highlightColor: Colors.transparent, @@ -81,7 +79,7 @@ class PowNodeCreateOrEditPage extends BasePage { ), ); - final PowNodeCreateOrEditViewModel nodeCreateOrEditViewModel; + final NodeCreateOrEditViewModel nodeCreateOrEditViewModel; final Node? editingNode; final bool? isSelected; @@ -124,7 +122,7 @@ class PowNodeCreateOrEditPage extends BasePage { padding: EdgeInsets.only(left: 24, right: 24), child: ScrollableWithBottomSection( contentPadding: EdgeInsets.only(bottom: 24.0), - content: PowNodeForm( + content: NodeForm( formKey: _formKey, nodeViewModel: nodeCreateOrEditViewModel, editingNode: editingNode, diff --git a/lib/src/screens/nodes/widgets/node_form.dart b/lib/src/screens/nodes/widgets/node_form.dart index 91974fce5..ab8dcafdf 100644 --- a/lib/src/screens/nodes/widgets/node_form.dart +++ b/lib/src/screens/nodes/widgets/node_form.dart @@ -132,6 +132,9 @@ class NodeForm extends StatelessWidget { Observer( builder: (_) => StandardCheckbox( value: nodeViewModel.useSSL, + gradientBackground: true, + borderColor: Theme.of(context).dividerColor, + iconColor: Colors.white, onChanged: (value) => nodeViewModel.useSSL = value, caption: S.of(context).use_ssl, ), @@ -148,6 +151,9 @@ class NodeForm extends StatelessWidget { Observer( builder: (_) => StandardCheckbox( value: nodeViewModel.trusted, + gradientBackground: true, + borderColor: Theme.of(context).dividerColor, + iconColor: Colors.white, onChanged: (value) => nodeViewModel.trusted = value, caption: S.of(context).trusted, ), @@ -166,6 +172,9 @@ class NodeForm extends StatelessWidget { children: [ StandardCheckbox( value: nodeViewModel.useSocksProxy, + gradientBackground: true, + borderColor: Theme.of(context).dividerColor, + iconColor: Colors.white, onChanged: (value) { if (!value) { _socksAddressController.text = ''; diff --git a/lib/src/screens/nodes/widgets/node_list_row.dart b/lib/src/screens/nodes/widgets/node_list_row.dart index 1739848d7..180942f84 100644 --- a/lib/src/screens/nodes/widgets/node_list_row.dart +++ b/lib/src/screens/nodes/widgets/node_list_row.dart @@ -11,10 +11,12 @@ class NodeListRow extends StandardListRow { {required String title, required this.node, required void Function(BuildContext context) onTap, - required bool isSelected}) + required bool isSelected, + required this.isPow}) : super(title: title, onTap: onTap, isSelected: isSelected); final Node node; + final bool isPow; @override Widget buildLeading(BuildContext context) { @@ -33,7 +35,7 @@ class NodeListRow extends StandardListRow { @override Widget buildTrailing(BuildContext context) { return GestureDetector( - onTap: () => Navigator.of(context).pushNamed(Routes.newNode, + onTap: () => Navigator.of(context).pushNamed(isPow ? Routes.newPowNode : Routes.newNode, arguments: {'editingNode': node, 'isSelected': isSelected}), child: Container( padding: EdgeInsets.all(10), diff --git a/lib/src/screens/pin_code/pin_code_widget.dart b/lib/src/screens/pin_code/pin_code_widget.dart index 9b22fa822..36328aee2 100644 --- a/lib/src/screens/pin_code/pin_code_widget.dart +++ b/lib/src/screens/pin_code/pin_code_widget.dart @@ -191,7 +191,7 @@ class PinCodeState extends State { child: Center( child: ConstrainedBox( constraints: BoxConstraints( - maxWidth: ResponsiveLayoutUtil.kDesktopMaxWidthConstraint, + maxWidth: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint, ), child: Container( key: _gridViewKey, diff --git a/lib/src/screens/receive/anonpay_invoice_page.dart b/lib/src/screens/receive/anonpay_invoice_page.dart index deda679c5..fc835c72d 100644 --- a/lib/src/screens/receive/anonpay_invoice_page.dart +++ b/lib/src/screens/receive/anonpay_invoice_page.dart @@ -99,7 +99,7 @@ class AnonPayInvoicePage extends BasePage { child: ScrollableWithBottomSection( contentPadding: EdgeInsets.only(bottom: 24), content: Container( - decoration: ResponsiveLayoutUtil.instance.isMobile ? BoxDecoration( + decoration: responsiveLayoutUtil.shouldRenderMobileUI ? BoxDecoration( borderRadius: BorderRadius.only( bottomLeft: Radius.circular(24), bottomRight: Radius.circular(24)), gradient: LinearGradient( diff --git a/lib/src/screens/receive/anonpay_receive_page.dart b/lib/src/screens/receive/anonpay_receive_page.dart index 1dae8e452..b602abde6 100644 --- a/lib/src/screens/receive/anonpay_receive_page.dart +++ b/lib/src/screens/receive/anonpay_receive_page.dart @@ -10,7 +10,7 @@ import 'package:cake_wallet/src/screens/receive/widgets/anonpay_status_section.d import 'package:cake_wallet/src/screens/receive/widgets/qr_image.dart'; import 'package:cake_wallet/src/screens/receive/widgets/copy_link_item.dart'; import 'package:cake_wallet/themes/extensions/qr_code_theme.dart'; -import 'package:device_display_brightness/device_display_brightness.dart'; +import 'package:cake_wallet/utils/brightness_util.dart'; import 'package:flutter/material.dart'; import 'package:qr_flutter/qr_flutter.dart' as qr; import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart'; @@ -82,8 +82,7 @@ class AnonPayReceivePage extends BasePage { @override Widget Function(BuildContext, Widget) get rootWrapper => - (BuildContext context, Widget scaffold) => - GradientBackground(scaffold: scaffold); + (BuildContext context, Widget scaffold) => GradientBackground(scaffold: scaffold); @override Widget body(BuildContext context) { @@ -101,19 +100,13 @@ class AnonPayReceivePage extends BasePage { ), child: GestureDetector( onTap: () async { - final double brightness = await DeviceDisplayBrightness.getBrightness(); - - // ignore: unawaited_futures - DeviceDisplayBrightness.setBrightness(1.0); - await Navigator.pushNamed( - context, - Routes.fullscreenQR, - arguments: QrViewData(data: invoiceInfo.clearnetUrl, - version: qr.QrVersions.auto, - ) - ); - // ignore: unawaited_futures - DeviceDisplayBrightness.setBrightness(brightness); + BrightnessUtil.changeBrightnessForFunction(() async { + await Navigator.pushNamed(context, Routes.fullscreenQR, + arguments: QrViewData( + data: invoiceInfo.clearnetUrl, + version: qr.QrVersions.auto, + )); + }); }, child: Hero( tag: Key(invoiceInfo.clearnetUrl), diff --git a/lib/src/screens/receive/widgets/currency_input_field.dart b/lib/src/screens/receive/widgets/currency_input_field.dart index 20e7bd660..1241b2ba7 100644 --- a/lib/src/screens/receive/widgets/currency_input_field.dart +++ b/lib/src/screens/receive/widgets/currency_input_field.dart @@ -32,7 +32,7 @@ class CurrencyInputField extends StatelessWidget { ); // This magic number for wider screen sets the text input focus at center of the inputfield final _width = - ResponsiveLayoutUtil.instance.isMobile ? MediaQuery.of(context).size.width : 500; + responsiveLayoutUtil.shouldRenderMobileUI ? MediaQuery.of(context).size.width : 500; return Column( children: [ diff --git a/lib/src/screens/receive/widgets/qr_widget.dart b/lib/src/screens/receive/widgets/qr_widget.dart index 0d2a7c80d..bbfd4d5c1 100644 --- a/lib/src/screens/receive/widgets/qr_widget.dart +++ b/lib/src/screens/receive/widgets/qr_widget.dart @@ -3,10 +3,9 @@ import 'package:cake_wallet/themes/extensions/qr_code_theme.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart'; import 'package:cake_wallet/src/screens/receive/widgets/currency_input_field.dart'; -import 'package:cake_wallet/utils/device_info.dart'; +import 'package:cake_wallet/utils/brightness_util.dart'; import 'package:cake_wallet/utils/show_bar.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; -import 'package:device_display_brightness/device_display_brightness.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; @@ -39,129 +38,134 @@ class QRWidget extends StatelessWidget { final copyImage = Image.asset('assets/images/copy_address.png', color: Theme.of(context).extension()!.qrWidgetCopyButtonColor); - return Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Column( - children: [ - Padding( - padding: const EdgeInsets.only(bottom: 12), - child: Text( - S.of(context).qr_fullscreen, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: Theme.of(context).extension()!.textColor), - ), - ), - Row( - children: [ - Spacer(flex: 3), - Observer( - builder: (_) => Flexible( - flex: 5, - child: GestureDetector( - onTap: () { - changeBrightnessForRoute( - () async { - await Navigator.pushNamed(context, Routes.fullscreenQR, - arguments: QrViewData( - data: addressListViewModel.uri.toString(), - heroTag: heroTag, - )); + return Center( + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Column( + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 12), + child: Text( + S.of(context).qr_fullscreen, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Theme.of(context).extension()!.textColor), + ), + ), + Row( + children: [ + Spacer(flex: 3), + Observer( + builder: (_) => Flexible( + flex: 5, + child: GestureDetector( + onTap: () { + BrightnessUtil.changeBrightnessForFunction( + () async { + await Navigator.pushNamed(context, Routes.fullscreenQR, + arguments: QrViewData( + data: addressListViewModel.uri.toString(), + heroTag: heroTag, + )); + }, + ); }, - ); - }, - child: Hero( - tag: Key(heroTag ?? addressListViewModel.uri.toString()), - child: Center( - child: AspectRatio( - aspectRatio: 1.0, - child: Container( - padding: EdgeInsets.all(5), - decoration: BoxDecoration( - border: Border.all( - width: 3, - color: Theme.of(context).extension()!.textColor, - ), - ), - child: Container( + child: Hero( + tag: Key(heroTag ?? addressListViewModel.uri.toString()), + child: Center( + child: AspectRatio( + aspectRatio: 1.0, + child: Container( + padding: EdgeInsets.all(5), decoration: BoxDecoration( border: Border.all( width: 3, - color:Colors.white, + color: + Theme.of(context).extension()!.textColor, ), ), - child: QrImage(data: addressListViewModel.uri.toString())), + child: Container( + decoration: BoxDecoration( + border: Border.all( + width: 3, + color: Colors.white, + ), + ), + child: QrImage(data: addressListViewModel.uri.toString())), + ), + ), ), ), ), ), ), - ), - ), - Spacer(flex: 3) - ], - ), - ], - ), - Observer(builder: (_) { - return Padding( - padding: EdgeInsets.only(top: 10), - child: Row( - children: [ - Expanded( - child: Form( - key: formKey, - child: CurrencyInputField( - focusNode: amountTextFieldFocusNode, - controller: amountController, - onTapPicker: () => _presentPicker(context), - selectedCurrency: addressListViewModel.selectedCurrency, - isLight: isLight, - ), - ), - ), - ], - ), - ); - }), - Padding( - padding: EdgeInsets.only(top: 20, bottom: 8), - child: Builder( - builder: (context) => Observer( - builder: (context) => GestureDetector( - onTap: () { - Clipboard.setData(ClipboardData(text: addressListViewModel.address.address)); - showBar(context, S.of(context).copied_to_clipboard); - }, - child: Row( - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: Text( - addressListViewModel.address.address, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 15, - fontWeight: FontWeight.w500, - color: Theme.of(context).extension()!.textColor), - ), - ), - Padding( - padding: EdgeInsets.only(left: 12), - child: copyImage, - ) + Spacer(flex: 3) ], ), - ), + ], ), - ), - ) - ], + Observer(builder: (_) { + return Padding( + padding: EdgeInsets.only(top: 10), + child: Row( + children: [ + Expanded( + child: Form( + key: formKey, + child: CurrencyInputField( + focusNode: amountTextFieldFocusNode, + controller: amountController, + onTapPicker: () => _presentPicker(context), + selectedCurrency: addressListViewModel.selectedCurrency, + isLight: isLight, + ), + ), + ), + ], + ), + ); + }), + Padding( + padding: EdgeInsets.only(top: 20, bottom: 8), + child: Builder( + builder: (context) => Observer( + builder: (context) => GestureDetector( + onTap: () { + Clipboard.setData(ClipboardData(text: addressListViewModel.address.address)); + showBar(context, S.of(context).copied_to_clipboard); + }, + child: Row( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Text( + addressListViewModel.address.address, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w500, + color: Theme.of(context).extension()!.textColor), + ), + ), + Padding( + padding: EdgeInsets.only(left: 12), + child: copyImage, + ) + ], + ), + ), + ), + ), + ) + ], + ), + ), ); } @@ -178,23 +182,4 @@ class QRWidget extends StatelessWidget { // update amount if currency changed addressListViewModel.changeAmount(amountController.text); } - - Future changeBrightnessForRoute(Future Function() navigation) async { - // if not mobile, just navigate - if (!DeviceInfo.instance.isMobile) { - navigation(); - return; - } - - // Get the current brightness: - final brightness = await DeviceDisplayBrightness.getBrightness(); - - // ignore: unawaited_futures - DeviceDisplayBrightness.setBrightness(1.0); - - await navigation(); - - // ignore: unawaited_futures - DeviceDisplayBrightness.setBrightness(brightness); - } } diff --git a/lib/src/screens/restore/restore_from_backup_page.dart b/lib/src/screens/restore/restore_from_backup_page.dart index bf944a6e1..f7fddac3f 100644 --- a/lib/src/screens/restore/restore_from_backup_page.dart +++ b/lib/src/screens/restore/restore_from_backup_page.dart @@ -4,7 +4,6 @@ import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/src/widgets/framework.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/view_model/restore_from_backup_view_model.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; @@ -31,10 +30,11 @@ class RestoreFromBackupPage extends BasePage { context: context, builder: (BuildContext context) { return AlertWithOneAction( - alertTitle: S.of(context).error, - alertContent: state.error, - buttonText: S.of(context).ok, - buttonAction: () => Navigator.of(context).pop()); + alertTitle: S.of(context).error, + alertContent: state.error, + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(context).pop(), + ); }); }); } @@ -42,44 +42,99 @@ class RestoreFromBackupPage extends BasePage { return Center( child: ConstrainedBox( - constraints: BoxConstraints(maxWidth: ResponsiveLayoutUtil.kDesktopMaxWidthConstraint), + constraints: BoxConstraints(maxWidth: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint), child: Padding( - padding: EdgeInsets.only(bottom: 24, left: 24, right: 24), - child: Column(children: [ + padding: EdgeInsets.only(bottom: 24, left: 24, right: 24), + child: Column( + children: [ Expanded( child: Container( child: Center( - child: TextFormField( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextFormField( obscureText: true, enableSuggestions: false, autocorrect: false, - decoration: InputDecoration( - hintText: S.of(context).enter_backup_password), + decoration: + InputDecoration(hintText: S.of(context).enter_backup_password), keyboardType: TextInputType.visiblePassword, controller: textEditingController, - style: TextStyle(fontSize: 26, color: Colors.black))), + style: TextStyle(fontSize: 26, color: Colors.black), + ), + Observer( + builder: (_) { + if (restoreFromBackupViewModel.filePath.isNotEmpty) { + return Column( + children: [ + const SizedBox(height: 100), + Row( + children: [ + Text( + "File Name: ", + style: TextStyle( + fontSize: 18.0, + fontWeight: FontWeight.bold, + fontFamily: 'Lato', + color: titleColor(context), + ), + ), + Expanded( + child: Text( + restoreFromBackupViewModel.filePath.split("/").last, + style: TextStyle( + fontSize: 18.0, + fontWeight: FontWeight.bold, + fontFamily: 'Lato', + color: titleColor(context), + ), + ), + ), + ], + ), + ], + ); + } + + return const SizedBox(); + }, + ), + ], + ), + ), ), ), Container( - child: Row(children: [ - Expanded( - child: PrimaryButton( + child: Row( + children: [ + Expanded( + child: PrimaryButton( onPressed: () => presentFilePicker(), text: S.of(context).select_backup_file, color: Colors.grey, - textColor: Colors.white)), - SizedBox(width: 20), - Expanded(child: Observer(builder: (_) { - return LoadingPrimaryButton( - isLoading: - restoreFromBackupViewModel.state is IsExecutingState, - onPressed: () => onImportHandler(context), - text: S.of(context).import, - color: Theme.of(context).primaryColor, - textColor: Colors.white); - })) - ])), - ])), + textColor: Colors.white, + ), + ), + SizedBox(width: 20), + Expanded( + child: Observer( + builder: (_) { + return LoadingPrimaryButton( + isLoading: restoreFromBackupViewModel.state is IsExecutingState, + onPressed: () => onImportHandler(context), + text: S.of(context).import, + color: Theme.of(context).primaryColor, + textColor: Colors.white); + }, + ), + ), + ], + ), + ), + ], + ), + ), ), ); } @@ -87,7 +142,7 @@ class RestoreFromBackupPage extends BasePage { Future presentFilePicker() async { final result = await FilePicker.platform.pickFiles(); - if (result?.files?.isEmpty ?? true) { + if (result?.files.isEmpty ?? true) { return; } @@ -95,8 +150,7 @@ class RestoreFromBackupPage extends BasePage { } Future onImportHandler(BuildContext context) async { - if (textEditingController.text.isEmpty || - (restoreFromBackupViewModel.filePath.isEmpty ?? true)) { + if (textEditingController.text.isEmpty || (restoreFromBackupViewModel.filePath.isEmpty)) { await showPopUp( context: context, builder: (_) { diff --git a/lib/src/screens/restore/restore_options_page.dart b/lib/src/screens/restore/restore_options_page.dart index 3adad4379..91ee9bd0b 100644 --- a/lib/src/screens/restore/restore_options_page.dart +++ b/lib/src/screens/restore/restore_options_page.dart @@ -2,6 +2,7 @@ import 'package:cake_wallet/core/execution_state.dart'; import 'package:cake_wallet/di.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/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/restore/restore_from_qr_vm.dart'; import 'package:cake_wallet/view_model/restore/wallet_restore_from_qr_code.dart'; @@ -9,9 +10,10 @@ import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:flutter/material.dart'; import 'package:cake_wallet/routes.dart'; import 'package:flutter/cupertino.dart'; -import 'package:cake_wallet/src/screens/restore/widgets/restore_button.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/generated/i18n.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:cake_wallet/utils/permission_handler.dart'; class RestoreOptionsPage extends BasePage { RestoreOptionsPage({required this.isNewInstall}); @@ -19,7 +21,6 @@ class RestoreOptionsPage extends BasePage { @override String get title => S.current.restore_restore_wallet; - final bool isNewInstall; final imageSeedKeys = Image.asset('assets/images/restore_wallet_image.png'); final imageBackup = Image.asset('assets/images/backup.png'); @@ -29,15 +30,14 @@ class RestoreOptionsPage extends BasePage { Widget body(BuildContext context) { return Center( child: Container( - width: ResponsiveLayoutUtil.kDesktopMaxWidthConstraint, + width: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint, height: double.infinity, - padding: EdgeInsets.symmetric(vertical: 24), + padding: EdgeInsets.symmetric(vertical: 24, horizontal: 24), child: SingleChildScrollView( child: Column( children: [ - RestoreButton( - onPressed: () => Navigator.pushNamed( - context, Routes.restoreWalletFromSeedKeys, + OptionTile( + onPressed: () => Navigator.pushNamed(context, Routes.restoreWalletFromSeedKeys, arguments: isNewInstall), image: imageSeedKeys, title: S.of(context).restore_title_from_seed_keys, @@ -45,7 +45,7 @@ class RestoreOptionsPage extends BasePage { if (isNewInstall) Padding( padding: EdgeInsets.only(top: 24), - child: RestoreButton( + child: OptionTile( onPressed: () => Navigator.pushNamed(context, Routes.restoreFromBackup), image: imageBackup, title: S.of(context).restore_title_from_backup, @@ -53,8 +53,11 @@ class RestoreOptionsPage extends BasePage { ), Padding( padding: EdgeInsets.only(top: 24), - child: RestoreButton( + child: OptionTile( onPressed: () async { + bool isCameraPermissionGranted = + await PermissionHandler.checkPermission(Permission.camera, context); + if (!isCameraPermissionGranted) return; bool isPinSet = false; if (isNewInstall) { await Navigator.pushNamed(context, Routes.setupPin, @@ -68,7 +71,8 @@ class RestoreOptionsPage extends BasePage { 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 b1b182af1..2c0dfa53f 100644 --- a/lib/src/screens/restore/wallet_restore_from_keys_form.dart +++ b/lib/src/screens/restore/wallet_restore_from_keys_form.dart @@ -66,6 +66,7 @@ class WalletRestoreFromKeysFromState extends State { addressController.dispose(); viewKeyController.dispose(); privateKeyController.dispose(); + spendKeyController.dispose(); super.dispose(); } @@ -123,9 +124,16 @@ class WalletRestoreFromKeysFromState extends State { Widget _restoreFromKeysFormFields() { if (widget.displayPrivateKeyField) { + // the term "private key" isn't actually what we're accepting here, and it's confusing to + // users of the nano community, what this form actually accepts (when importing for nano) is a nano seed in it's hex form, referred to in code as a "seed key" + // so we should change the placeholder text to reflect this + // supporting actual nano private keys is possible, but it's super niche in the nano community / they're not really used + + bool nanoBased = widget.walletRestoreViewModel.type == WalletType.nano || + widget.walletRestoreViewModel.type == WalletType.banano; return AddressTextField( controller: privateKeyController, - placeholder: S.of(context).private_key, + placeholder: nanoBased ? S.of(context).seed_key : S.of(context).private_key, options: [AddressTextFieldOption.paste], buttonColor: Theme.of(context).hintColor, onPushPasteButton: (_) { diff --git a/lib/src/screens/restore/wallet_restore_page.dart b/lib/src/screens/restore/wallet_restore_page.dart index 189e204f8..744dccd2d 100644 --- a/lib/src/screens/restore/wallet_restore_page.dart +++ b/lib/src/screens/restore/wallet_restore_page.dart @@ -4,11 +4,8 @@ import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/themes/extensions/keyboard_theme.dart'; import 'package:cake_wallet/src/widgets/keyboard_done_button.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; -import 'package:cw_bitcoin/bitcoin_wallet_service.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; -import 'package:cw_nano/nano_util.dart'; -import 'package:cw_nano/nano_wallet_service.dart'; import 'package:flutter/material.dart'; import 'package:keyboard_actions/keyboard_actions.dart'; import 'package:mobx/mobx.dart'; @@ -164,7 +161,7 @@ class WalletRestorePage extends BasePage { color: Theme.of(context).colorScheme.background, child: Center( child: ConstrainedBox( - constraints: BoxConstraints(maxWidth: ResponsiveLayoutUtil.kDesktopMaxWidthConstraint), + constraints: BoxConstraints(maxWidth: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -197,23 +194,35 @@ class WalletRestorePage extends BasePage { ), Padding( padding: EdgeInsets.only(top: 20, bottom: 24, left: 24, right: 24), - child: Observer( - builder: (context) { - return LoadingPrimaryButton( - onPressed: () async { - await _confirmForm(context); + child: Column( + children: [ + Observer( + builder: (context) { + return LoadingPrimaryButton( + onPressed: () async { + await _confirmForm(context); + }, + text: S.of(context).restore_recover, + color: Theme.of(context) + .extension()! + .createNewWalletButtonBackgroundColor, + textColor: Theme.of(context) + .extension()! + .restoreWalletButtonTextColor, + isLoading: walletRestoreViewModel.state is IsExecutingState, + isDisabled: !walletRestoreViewModel.isButtonEnabled, + ); }, - text: S.of(context).restore_recover, - color: Theme.of(context) - .extension()! - .createNewWalletButtonBackgroundColor, - textColor: Theme.of(context) - .extension()! - .restoreWalletButtonTextColor, - isLoading: walletRestoreViewModel.state is IsExecutingState, - isDisabled: !walletRestoreViewModel.isButtonEnabled, - ); - }, + ), + const SizedBox(height: 25), + GestureDetector( + onTap: () { + Navigator.of(context) + .pushNamed(Routes.advancedPrivacySettings, arguments: walletRestoreViewModel.type); + }, + child: Text(S.of(context).advanced_privacy_settings), + ), + ], ), ) ], @@ -241,7 +250,7 @@ class WalletRestorePage extends BasePage { } // bip39: - const validSeedLengths = [12, 15, 18, 21, 24]; + const validSeedLengths = [12, 18, 24]; if (walletRestoreViewModel.type == WalletType.bitcoin && !(validSeedLengths.contains(seedWords.length))) { return false; @@ -305,46 +314,38 @@ class WalletRestorePage extends BasePage { var walletType = credentials["walletType"] as WalletType; var appStore = getIt.get(); var node = appStore.settingsStore.getCurrentNode(walletType); + switch (walletType) { - case WalletType.bitcoin: - String? mnemonic = credentials['seed'] as String?; - return await BitcoinWalletService.getDerivationsFromMnemonic( - mnemonic: mnemonic!, node: node); case WalletType.nano: String? mnemonic = credentials['seed'] as String?; String? seedKey = credentials['private_key'] as String?; - dynamic bip39Info = await NanoWalletService.getInfoFromSeedOrMnemonic(DerivationType.bip39, - mnemonic: mnemonic, seedKey: seedKey, node: node); - dynamic standardInfo = await NanoWalletService.getInfoFromSeedOrMnemonic( + AccountInfoResponse? bip39Info = await nanoUtil!.getInfoFromSeedOrMnemonic( + DerivationType.bip39, + mnemonic: mnemonic, + seedKey: seedKey, + node: node); + AccountInfoResponse? standardInfo = await nanoUtil!.getInfoFromSeedOrMnemonic( DerivationType.nano, mnemonic: mnemonic, seedKey: seedKey, node: node, ); - if (standardInfo["balance"] != null) { + if (standardInfo?.balance != null) { list.add(DerivationInfo( derivationType: DerivationType.nano, - balance: NanoUtil.getRawAsUsableString( - standardInfo["balance"] as String, NanoUtil.rawPerNano), - address: standardInfo["address"] as String, - height: int.tryParse( - standardInfo["confirmation_height"] as String, - ) ?? - 0, + balance: nanoUtil!.getRawAsUsableString(standardInfo!.balance, nanoUtil!.rawPerNano), + address: standardInfo.address!, + height: standardInfo.confirmationHeight, )); } - if (bip39Info["balance"] != null) { + if (bip39Info?.balance != null) { list.add(DerivationInfo( derivationType: DerivationType.bip39, - balance: - NanoUtil.getRawAsUsableString(bip39Info["balance"] as String, NanoUtil.rawPerNano), - address: bip39Info["address"] as String, - height: int.tryParse( - bip39Info["confirmation_height"] as String? ?? "", - ) ?? - 0, + balance: nanoUtil!.getRawAsUsableString(bip39Info!.balance, nanoUtil!.rawPerNano), + address: bip39Info.address!, + height: bip39Info.confirmationHeight, )); } break; @@ -398,6 +399,7 @@ class WalletRestorePage extends BasePage { } } + DerivationInfo? derivationInfo; if (derivationsWithHistory > 1) { @@ -412,8 +414,6 @@ class WalletRestorePage extends BasePage { derivationPath: "m/0'/1", height: 0, ); - this.derivationType = derivationTypes[0]; - this.derivationPath = "m/0'/1"; } if (derivationInfo == null) { diff --git a/lib/src/screens/restore/widgets/restore_button.dart b/lib/src/screens/restore/widgets/restore_button.dart deleted file mode 100644 index c196de059..000000000 --- a/lib/src/screens/restore/widgets/restore_button.dart +++ /dev/null @@ -1,74 +0,0 @@ -import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; -import 'package:flutter/material.dart'; -import 'package:cake_wallet/themes/extensions/transaction_trade_theme.dart'; - -class RestoreButton extends StatelessWidget { - const RestoreButton( - {required this.onPressed, - required this.image, - required this.title, - required this.description}); - - final VoidCallback onPressed; - final Image image; - final String title; - final String description; - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: onPressed, - child: Container( - width: double.infinity, - height: 170, - padding: EdgeInsets.all(24), - alignment: Alignment.topLeft, - decoration: BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(12)), - color: Theme.of(context).cardColor, - ), - child: Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - image, - Expanded( - child: Padding( - padding: EdgeInsets.only(left: 16), - child: Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - color: Theme.of(context).extension()!.titleColor, - ), - ), - Padding( - padding: EdgeInsets.only(top: 5), - child: Text( - description, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.normal, - color: Theme.of(context) - .extension()! - .detailsTitlesColor, - ), - ), - ) - ], - ), - ), - ) - ], - ), - ), - ); - } -} diff --git a/lib/src/screens/root/root.dart b/lib/src/screens/root/root.dart index 3298a50c0..ca86cdccc 100644 --- a/lib/src/screens/root/root.dart +++ b/lib/src/screens/root/root.dart @@ -1,17 +1,19 @@ import 'dart:async'; import 'package:cake_wallet/core/auth_service.dart'; import 'package:cake_wallet/core/totp_request_details.dart'; +import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/utils/device_info.dart'; import 'package:cake_wallet/utils/payment_request.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'; import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/store/authentication_store.dart'; import 'package:cake_wallet/entities/qr_scanner.dart'; +import 'package:fluttertoast/fluttertoast.dart'; import 'package:uni_links/uni_links.dart'; - -import '../setup_2fa/setup_2fa_enter_code_page.dart'; +import 'package:cake_wallet/src/screens/setup_2fa/setup_2fa_enter_code_page.dart'; class Root extends StatefulWidget { Root({ @@ -97,8 +99,7 @@ class RootState extends State with WidgetsBindingObserver { return; } - if (!_isInactive && - widget.authenticationStore.state == AuthenticationState.allowed) { + if (!_isInactive && widget.authenticationStore.state == AuthenticationState.allowed) { setState(() => _setInactive(true)); } @@ -125,23 +126,23 @@ class RootState extends State with WidgetsBindingObserver { return; } else { final useTotp = widget.appStore.settingsStore.useTOTP2FA; - final shouldUseTotp2FAToAccessWallets = widget.appStore - .settingsStore.shouldRequireTOTP2FAForAccessingWallet; + final shouldUseTotp2FAToAccessWallets = + widget.appStore.settingsStore.shouldRequireTOTP2FAForAccessingWallet; if (useTotp && shouldUseTotp2FAToAccessWallets) { _reset(); auth.close( route: Routes.totpAuthCodePage, arguments: TotpAuthArgumentsModel( onTotpAuthenticationFinished: - (bool isAuthenticatedSuccessfully, - TotpAuthCodePageState totpAuth) { + (bool isAuthenticatedSuccessfully, TotpAuthCodePageState totpAuth) { if (!isAuthenticatedSuccessfully) { return; } _reset(); totpAuth.close( - route: launchUri != null ? Routes.send : null, - arguments: PaymentRequest.fromUri(launchUri), + route: _getRouteToGo(), + arguments: + isWalletConnectLink ? launchUri : PaymentRequest.fromUri(launchUri), ); launchUri = null; }, @@ -152,8 +153,8 @@ class RootState extends State with WidgetsBindingObserver { } else { _reset(); auth.close( - route: launchUri != null ? Routes.send : null, - arguments: PaymentRequest.fromUri(launchUri), + route: _getRouteToGo(), + arguments: isWalletConnectLink ? launchUri : PaymentRequest.fromUri(launchUri), ); launchUri = null; } @@ -161,15 +162,29 @@ class RootState extends State with WidgetsBindingObserver { }, ); }); - } else if (launchUri != null) { + } else if (_isValidPaymentUri()) { widget.navigatorKey.currentState?.pushNamed( Routes.send, arguments: PaymentRequest.fromUri(launchUri), ); launchUri = null; + } else if (isWalletConnectLink) { + if (widget.appStore.wallet!.type == WalletType.ethereum) { + widget.navigatorKey.currentState?.pushNamed( + Routes.walletConnectConnectionsListing, + arguments: launchUri, + ); + launchUri = null; + } else { + _nonETHWalletErrorToast(S.current.switchToETHWallet); + } } - - return WillPopScope(onWillPop: () async => false, child: widget.child); + + launchUri = null; + return WillPopScope( + onWillPop: () async => false, + child: widget.child, + ); } void _reset() { @@ -183,4 +198,33 @@ class RootState extends State with WidgetsBindingObserver { _isInactive = value; _isInactiveController.add(value); } + + bool _isValidPaymentUri() => launchUri?.path.isNotEmpty ?? false; + + bool get isWalletConnectLink => launchUri?.authority == 'wc'; + + String? _getRouteToGo() { + if (isWalletConnectLink) { + if (widget.appStore.wallet!.type != WalletType.ethereum) { + _nonETHWalletErrorToast(S.current.switchToETHWallet); + return null; + } + return Routes.walletConnectConnectionsListing; + } else if (_isValidPaymentUri()) { + return Routes.send; + } else { + return null; + } + } + + Future _nonETHWalletErrorToast(String message) async { + Fluttertoast.showToast( + msg: message, + toastLength: Toast.LENGTH_LONG, + gravity: ToastGravity.SNACKBAR, + backgroundColor: Colors.black, + textColor: Colors.white, + fontSize: 16.0, + ); + } } diff --git a/lib/src/screens/seed/pre_seed_page.dart b/lib/src/screens/seed/pre_seed_page.dart index a73e7bbff..947099983 100644 --- a/lib/src/screens/seed/pre_seed_page.dart +++ b/lib/src/screens/seed/pre_seed_page.dart @@ -1,5 +1,6 @@ import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; +import 'package:cake_wallet/view_model/advanced_privacy_settings_view_model.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; @@ -9,15 +10,19 @@ import 'package:cake_wallet/themes/theme_base.dart'; import 'package:flutter/material.dart'; class PreSeedPage extends BasePage { - PreSeedPage(this.type) + PreSeedPage(this.type, this.advancedPrivacySettingsViewModel) : imageLight = Image.asset('assets/images/pre_seed_light.png'), imageDark = Image.asset('assets/images/pre_seed_dark.png'), - wordsCount = _wordsCount(type); + seedPhraseLength = advancedPrivacySettingsViewModel.seedPhraseLength.value { + wordsCount = _wordsCount(type, seedPhraseLength); + } final Image imageDark; final Image imageLight; final WalletType type; - final int wordsCount; + final AdvancedPrivacySettingsViewModel advancedPrivacySettingsViewModel; + final int seedPhraseLength; + late final int wordsCount; @override Widget? leading(BuildContext context) => null; @@ -35,7 +40,7 @@ class PreSeedPage extends BasePage { alignment: Alignment.center, padding: EdgeInsets.all(24), child: ConstrainedBox( - constraints: BoxConstraints(maxWidth: ResponsiveLayoutUtil.kDesktopMaxWidthConstraint), + constraints: BoxConstraints(maxWidth: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint), child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -68,12 +73,13 @@ class PreSeedPage extends BasePage { )); } - static int _wordsCount(WalletType type) { + static int _wordsCount(WalletType type, int seedPhraseLength) { switch (type) { case WalletType.monero: return 25; case WalletType.ethereum: - return 12; + case WalletType.bitcoinCash: + return seedPhraseLength; default: return 24; } diff --git a/lib/src/screens/seed/wallet_seed_page.dart b/lib/src/screens/seed/wallet_seed_page.dart index fa17d7ccf..200b87b7d 100644 --- a/lib/src/screens/seed/wallet_seed_page.dart +++ b/lib/src/screens/seed/wallet_seed_page.dart @@ -93,7 +93,7 @@ class WalletSeedPage extends BasePage { padding: EdgeInsets.all(24), alignment: Alignment.center, child: ConstrainedBox( - constraints: BoxConstraints(maxWidth: ResponsiveLayoutUtil.kDesktopMaxWidthConstraint), + constraints: BoxConstraints(maxWidth: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint), child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ diff --git a/lib/src/screens/send/send_page.dart b/lib/src/screens/send/send_page.dart index 9aa0c8617..b20b94cba 100644 --- a/lib/src/screens/send/send_page.dart +++ b/lib/src/screens/send/send_page.dart @@ -69,7 +69,7 @@ class SendPage extends BasePage { final _closeButton = currentTheme.type == ThemeType.dark ? closeButtonImageDarkTheme : closeButtonImage; - bool isMobileView = ResponsiveLayoutUtil.instance.isMobile; + bool isMobileView = responsiveLayoutUtil.shouldRenderMobileUI; return MergeSemantics( child: SizedBox( @@ -96,9 +96,9 @@ class SendPage extends BasePage { AppBarStyle get appBarStyle => AppBarStyle.transparent; double _sendCardHeight(BuildContext context) { - final double initialHeight = sendViewModel.hasCoinControl ? 490 : 465; + final double initialHeight = sendViewModel.hasCoinControl ? 500 : 465; - if (!ResponsiveLayoutUtil.instance.isMobile) { + if (!responsiveLayoutUtil.shouldRenderMobileUI) { return initialHeight - 66; } return initialHeight; @@ -413,39 +413,39 @@ class SendPage extends BasePage { if (context.mounted) { showPopUp( context: context, - builder: (BuildContext context) { + builder: (BuildContext _dialogContext) { return ConfirmSendingAlert( - alertTitle: S.of(context).confirm_sending, - amount: S.of(context).send_amount, + alertTitle: S.of(_dialogContext).confirm_sending, + amount: S.of(_dialogContext).send_amount, amountValue: sendViewModel.pendingTransaction!.amountFormatted, fiatAmountValue: sendViewModel.pendingTransactionFiatAmountFormatted, - fee: S.of(context).send_fee, + fee: S.of(_dialogContext).send_fee, feeValue: sendViewModel.pendingTransaction!.feeFormatted, feeFiatAmount: sendViewModel.pendingTransactionFeeFiatAmountFormatted, outputs: sendViewModel.outputs, - rightButtonText: S.of(context).ok, - leftButtonText: S.of(context).cancel, + rightButtonText: S.of(_dialogContext).ok, + leftButtonText: S.of(_dialogContext).cancel, actionRightButton: () { - Navigator.of(context).pop(); + Navigator.of(_dialogContext).pop(); sendViewModel.commitTransaction(); showPopUp( context: context, - builder: (BuildContext context) { + builder: (BuildContext _dialogContext) { return Observer(builder: (_) { final state = sendViewModel.state; if (state is FailureState) { - Navigator.of(context).pop(); + Navigator.of(_dialogContext).pop(); } if (state is TransactionCommitted) { return AlertWithOneAction( alertTitle: '', - alertContent: S.of(context).send_success( + alertContent: S.of(_dialogContext).send_success( sendViewModel.selectedCryptoCurrency.toString()), - buttonText: S.of(context).ok, + buttonText: S.of(_dialogContext).ok, buttonAction: () { - Navigator.of(context).pop(); + Navigator.of(_dialogContext).pop(); RequestReviewHandler.requestReview(); }); } @@ -454,7 +454,7 @@ class SendPage extends BasePage { }); }); }, - actionLeftButton: () => Navigator.of(context).pop()); + actionLeftButton: () => Navigator.of(_dialogContext).pop()); }); } }); @@ -472,15 +472,15 @@ class SendPage extends BasePage { Future _setInputsFromTemplate(BuildContext context, {required Output output, required Template template}) async { - final fiatFromTemplate = - FiatCurrency.all.singleWhere((element) => element.title == template.fiatCurrency); - output.address = template.address; if (template.isCurrencySelected) { sendViewModel.setSelectedCryptoCurrency(template.cryptoCurrency); output.setCryptoAmount(template.amount); } else { + final fiatFromTemplate = + FiatCurrency.all.singleWhere((element) => element.title == template.fiatCurrency); + sendViewModel.setFiatCurrency(fiatFromTemplate); output.setFiatAmount(template.amountFiat); } diff --git a/lib/src/screens/send/widgets/extract_address_from_parsed.dart b/lib/src/screens/send/widgets/extract_address_from_parsed.dart index d7e0e3d7f..73bff23c1 100644 --- a/lib/src/screens/send/widgets/extract_address_from_parsed.dart +++ b/lib/src/screens/send/widgets/extract_address_from_parsed.dart @@ -18,6 +18,11 @@ Future extractAddressFromParsed( content = S.of(context).address_from_domain(parsedAddress.name); address = parsedAddress.addresses.first; break; + case ParseFrom.ens: + title = S.of(context).address_detected; + content = S.of(context).extracted_address_content('${parsedAddress.name} (ENS)'); + address = parsedAddress.addresses.first; + break; case ParseFrom.openAlias: title = S.of(context).address_detected; content = S.of(context).extracted_address_content('${parsedAddress.name} (OpenAlias)'); @@ -33,6 +38,11 @@ Future extractAddressFromParsed( content = S.of(context).extracted_address_content('${parsedAddress.name} (Twitter)'); address = parsedAddress.addresses.first; break; + case ParseFrom.mastodon: + title = S.of(context).address_detected; + content = S.of(context).extracted_address_content('${parsedAddress.name} (Mastodon)'); + address = parsedAddress.addresses.first; + break; case ParseFrom.yatRecord: if (parsedAddress.name.isEmpty) { title = S.of(context).yat_error; diff --git a/lib/src/screens/send/widgets/send_card.dart b/lib/src/screens/send/widgets/send_card.dart index ec1ee5087..65069e903 100644 --- a/lib/src/screens/send/widgets/send_card.dart +++ b/lib/src/screens/send/widgets/send_card.dart @@ -122,7 +122,7 @@ class SendCardState extends State with AutomaticKeepAliveClientMixin with AutomaticKeepAliveClientMixin S.current.connection_sync; + final Web3WalletService? web3walletService; final DashboardViewModel dashboardViewModel; @override @@ -81,6 +84,16 @@ class ConnectionSyncPage extends BasePage { ); }, ), + if (dashboardViewModel.wallet.type == WalletType.ethereum) ...[ + WalletConnectTile( + onTap: () => Navigator.of(context).pushNamed(Routes.walletConnectConnectionsListing), + ), + const StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)), + ], + SettingsCellWithArrow( + title: S.current.tor_connection, + handler: (context) => Navigator.of(context).pushNamed(Routes.torPage), + ), ], ), ); 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 bcdb89aec..1d6168e4a 100644 --- a/lib/src/screens/settings/desktop_settings/desktop_settings_page.dart +++ b/lib/src/screens/settings/desktop_settings/desktop_settings_page.dart @@ -12,6 +12,7 @@ final _settingsNavigatorKey = GlobalKey(); class DesktopSettingsPage extends StatefulWidget { const DesktopSettingsPage({super.key}); + @override State createState() => _DesktopSettingsPageState(); } diff --git a/lib/src/screens/settings/display_settings_page.dart b/lib/src/screens/settings/display_settings_page.dart index 3e7da522b..505573f80 100644 --- a/lib/src/screens/settings/display_settings_page.dart +++ b/lib/src/screens/settings/display_settings_page.dart @@ -75,7 +75,7 @@ class DisplaySettingsPage extends BasePage { return LanguageService.list[code]?.toLowerCase().contains(searchText) ?? false; }, ), - if (ResponsiveLayoutUtil.instance.isMobile && DeviceInfo.instance.isMobile) + if (responsiveLayoutUtil.shouldRenderMobileUI && DeviceInfo.instance.isMobile) SettingsThemeChoicesCell(_displaySettingsViewModel), ], ), diff --git a/lib/src/screens/settings/domain_lookups_page.dart b/lib/src/screens/settings/domain_lookups_page.dart new file mode 100644 index 000000000..80849b8ea --- /dev/null +++ b/lib/src/screens/settings/domain_lookups_page.dart @@ -0,0 +1,56 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/src/screens/settings/widgets/settings_switcher_cell.dart'; +import 'package:cake_wallet/view_model/settings/privacy_settings_view_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; + +class DomainLookupsPage extends BasePage { + DomainLookupsPage(this._privacySettingsViewModel); + + @override + String get title => S.current.domain_looks_up; + + final PrivacySettingsViewModel _privacySettingsViewModel; + + @override + Widget body(BuildContext context) { + return SingleChildScrollView( + child: Observer(builder: (_) { + return Container( + padding: EdgeInsets.only(top: 10), + child: Column( + children: [ + SettingsSwitcherCell( + title: 'Twitter', + value: _privacySettingsViewModel.lookupTwitter, + onValueChange: (_, bool value) => _privacySettingsViewModel.setLookupsTwitter(value)), + SettingsSwitcherCell( + title: 'Mastodon', + value: _privacySettingsViewModel.looksUpMastodon, + onValueChange: (_, bool value) => _privacySettingsViewModel.setLookupsMastodon(value)), + SettingsSwitcherCell( + title: 'Yat service', + value: _privacySettingsViewModel.looksUpYatService, + onValueChange: (_, bool value) => _privacySettingsViewModel.setLookupsYatService(value)), + SettingsSwitcherCell( + title: 'Unstoppable Domains', + value: _privacySettingsViewModel.looksUpUnstoppableDomains, + onValueChange: (_, bool value) => _privacySettingsViewModel.setLookupsUnstoppableDomains(value)), + SettingsSwitcherCell( + title: 'OpenAlias,', + value: _privacySettingsViewModel.looksUpOpenAlias, + onValueChange: (_, bool value) => _privacySettingsViewModel.setLookupsOpenAlias(value)), + SettingsSwitcherCell( + title: 'Ethereum Name Service', + value: _privacySettingsViewModel.looksUpENS, + onValueChange: (_, bool value) => _privacySettingsViewModel.setLookupsENS(value)), + + //if (!isHaven) it does not work correctly + ], + ), + ); + }), + ); + } +} diff --git a/lib/src/screens/settings/manage_nodes_page.dart b/lib/src/screens/settings/manage_nodes_page.dart index 682fad64e..3cc251019 100644 --- a/lib/src/screens/settings/manage_nodes_page.dart +++ b/lib/src/screens/settings/manage_nodes_page.dart @@ -6,13 +6,17 @@ import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; import 'package:cake_wallet/src/widgets/standard_list.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/node_list/node_list_view_model.dart'; +import 'package:cake_wallet/view_model/node_list/pow_node_list_view_model.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; class ManageNodesPage extends BasePage { - ManageNodesPage(this.nodeListViewModel); + ManageNodesPage(this.isPow, {this.nodeListViewModel, this.powNodeListViewModel}) + : assert((isPow && powNodeListViewModel != null) || (!isPow && nodeListViewModel != null)); - final NodeListViewModel nodeListViewModel; + final NodeListViewModel? nodeListViewModel; + final PowNodeListViewModel? powNodeListViewModel; + final bool isPow; @override String get title => S.current.manage_nodes; @@ -34,23 +38,29 @@ class ManageNodesPage extends BasePage { SizedBox(height: 20), Observer( builder: (BuildContext context) { - int nodesLength = nodeListViewModel.nodes.length; + int itemsCount = + nodeListViewModel?.nodes.length ?? powNodeListViewModel!.nodes.length; return Flexible( child: SectionStandardList( sectionCount: 1, dividerPadding: EdgeInsets.symmetric(horizontal: 24), - itemCounter: (int sectionIndex) { - return nodesLength; - }, + itemCounter: (int sectionIndex) => itemsCount, itemBuilder: (_, index) { return Observer( builder: (context) { - final node = nodeListViewModel.nodes[index]; - final isSelected = node.keyIndex == nodeListViewModel.currentNode.keyIndex; + final node = + nodeListViewModel?.nodes[index] ?? powNodeListViewModel!.nodes[index]; + late bool isSelected; + if (isPow) { + isSelected = node.keyIndex == powNodeListViewModel!.currentNode.keyIndex; + } else { + isSelected = node.keyIndex == nodeListViewModel!.currentNode.keyIndex; + } final nodeListRow = NodeListRow( title: node.uriRaw, node: node, isSelected: isSelected, + isPow: false, onTap: (_) async { if (isSelected) { return; @@ -61,12 +71,17 @@ class ManageNodesPage extends BasePage { builder: (BuildContext context) { return AlertWithTwoActions( alertTitle: S.of(context).change_current_node_title, - alertContent: nodeListViewModel.getAlertContent(node.uriRaw), + alertContent: nodeListViewModel?.getAlertContent(node.uriRaw) ?? + powNodeListViewModel!.getAlertContent(node.uriRaw), leftButtonText: S.of(context).cancel, rightButtonText: S.of(context).change, actionLeftButton: () => Navigator.of(context).pop(), actionRightButton: () async { - await nodeListViewModel.setAsCurrent(node); + if (isPow) { + await powNodeListViewModel!.setAsCurrent(node); + } else { + await nodeListViewModel!.setAsCurrent(node); + } Navigator.of(context).pop(); }, ); diff --git a/lib/src/screens/settings/other_settings_page.dart b/lib/src/screens/settings/other_settings_page.dart index e93357653..ede816893 100644 --- a/lib/src/screens/settings/other_settings_page.dart +++ b/lib/src/screens/settings/other_settings_page.dart @@ -1,3 +1,5 @@ +import 'package:cake_wallet/buy/buy_provider.dart'; +import 'package:cake_wallet/entities/buy_provider_types.dart'; import 'package:cake_wallet/entities/priority_for_wallet_type.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/routes.dart'; @@ -40,6 +42,13 @@ class OtherSettingsPage extends BasePage { handler: (BuildContext context) => Navigator.of(context).pushNamed(Routes.changeRep), ), + SettingsPickerCell( + title: S.current.default_buy_provider, + items: BuyProviderType.values, + displayItem: _otherSettingsViewModel.getBuyProviderType, + selectedItem: _otherSettingsViewModel.buyProviderType, + onItemSelected: _otherSettingsViewModel.onBuyProviderTypeSelected, + ), SettingsCellWithArrow( title: S.current.settings_terms_and_conditions, handler: (BuildContext context) => diff --git a/lib/src/screens/settings/privacy_page.dart b/lib/src/screens/settings/privacy_page.dart index e953fd4ee..7a8671224 100644 --- a/lib/src/screens/settings/privacy_page.dart +++ b/lib/src/screens/settings/privacy_page.dart @@ -1,7 +1,9 @@ import 'package:cake_wallet/entities/exchange_api_mode.dart'; import 'package:cake_wallet/entities/fiat_api_mode.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/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/utils/device_info.dart'; @@ -84,6 +86,14 @@ class PrivacyPage extends BasePage { onValueChange: (BuildContext _, bool value) { _privacySettingsViewModel.setUseEtherscan(value); }), + SettingsCellWithArrow( + title: S.current.domain_looks_up, + handler: (context) => Navigator.of(context).pushNamed(Routes.domainLookupsPage), + ), + SettingsCellWithArrow( + title: 'Trocador providers', + handler: (context) => Navigator.of(context).pushNamed(Routes.trocadorProvidersPage), + ), ], ); }), diff --git a/lib/src/screens/settings/tor_page.dart b/lib/src/screens/settings/tor_page.dart new file mode 100644 index 000000000..d1d5c7e83 --- /dev/null +++ b/lib/src/screens/settings/tor_page.dart @@ -0,0 +1,267 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/store/app_store.dart'; +import 'package:flutter/material.dart'; +import 'package:tor/tor.dart'; + +class TorPage extends BasePage { + final AppStore appStore; + + TorPage(this.appStore); + + @override + Widget body(BuildContext context) { + return TorPageBody(appStore); + } +} + +class TorPageBody extends StatefulWidget { + final AppStore appStore; + + const TorPageBody(this.appStore, {Key? key}) : super(key: key); + + @override + State createState() => _TorPageBodyState(); +} + +class _TorPageBodyState extends State { + bool torEnabled = false; + bool connecting = false; + + // Set the default text for the host input field. + final hostController = TextEditingController(text: 'https://icanhazip.com/'); + + // https://check.torproject.org is another good option. + + Future startTor() async { + setState(() { + connecting = true; // Update flag + }); + + await Tor.init(); + + // Start the proxy + await Tor.instance.start(); + + // Toggle started flag. + setState(() { + torEnabled = Tor.instance.enabled; // Update flag + connecting = false; + }); + + final node = widget.appStore.settingsStore.getCurrentNode(widget.appStore.wallet!.type); + if (node.socksProxyAddress?.isEmpty ?? true) { + node.socksProxyAddress = "${InternetAddress.loopbackIPv4.address}:${Tor.instance.port}"; + } + widget.appStore.wallet!.connectToNode(node: node); + + print('Done awaiting; tor should be running'); + } + + Future endTor() async { + // Start the proxy + Tor.instance.disable(); + + // Toggle started flag. + setState(() { + torEnabled = Tor.instance.enabled; // Update flag + }); + + print('Done awaiting; tor should be stopped'); + } + + @override + void initState() { + super.initState(); + + torEnabled = Tor.instance.enabled; + } + + @override + void dispose() { + // Clean up the controller when the widget is disposed. + hostController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: Container( + padding: const EdgeInsets.all(10), + child: connecting + ? ConnectingScreen() + : torEnabled + ? DisconnectScreen(disconnect: endTor) + : ConnectScreen(connect: startTor), + ), + ); + } +} + +class ConnectScreen extends StatelessWidget { + final Function() connect; + + const ConnectScreen({super.key, required this.connect}); + + @override + Widget build(BuildContext context) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: 200, + height: 200, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.blue, + ), + child: Icon( + Icons.lock, + color: Colors.white, + size: 100, + ), + ), + SizedBox(height: 20), + Text( + 'Connect to Tor', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + SizedBox(height: 10), + Text( + 'Your connection to the Tor network ensures privacy and security.', + style: TextStyle( + fontSize: 16, + color: Colors.grey, + ), + textAlign: TextAlign.center, + ), + SizedBox(height: 30), + ElevatedButton( + onPressed: connect, + style: ElevatedButton.styleFrom( + primary: Colors.blue, + padding: EdgeInsets.symmetric(horizontal: 40, vertical: 15), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30), + ), + ), + child: Text( + 'Connect', + style: TextStyle( + fontSize: 18, + color: Colors.white, + ), + ), + ), + ], + ), + ); + } +} + +class DisconnectScreen extends StatelessWidget { + final Function() disconnect; + + const DisconnectScreen({super.key, required this.disconnect}); + + @override + Widget build(BuildContext context) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: 200, + height: 200, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.green, + ), + child: Icon( + Icons.check, + color: Colors.white, + size: 100, + ), + ), + SizedBox(height: 20), + Text( + 'Connected to Tor', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + SizedBox(height: 10), + Text( + 'You are currently connected to the Tor network.', + style: TextStyle( + fontSize: 16, + color: Colors.grey, + ), + textAlign: TextAlign.center, + ), + SizedBox(height: 30), + ElevatedButton( + onPressed: disconnect, + style: ElevatedButton.styleFrom( + primary: Colors.red, + padding: EdgeInsets.symmetric(horizontal: 40, vertical: 15), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30), + ), + ), + child: Text( + 'Disconnect', + style: TextStyle( + fontSize: 18, + color: Colors.white, + ), + ), + ), + ], + ), + ); + } +} + +class ConnectingScreen extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: 200, + height: 200, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.yellow, + ), + child: Icon( + Icons.hourglass_bottom, + color: Colors.white, + size: 100, + ), + ), + SizedBox(height: 20), + Text( + 'Connecting...', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + SizedBox(height: 10), + ], + ), + ); + } +} diff --git a/lib/src/screens/settings/trocador_providers_page.dart b/lib/src/screens/settings/trocador_providers_page.dart new file mode 100644 index 000000000..b972c22d5 --- /dev/null +++ b/lib/src/screens/settings/trocador_providers_page.dart @@ -0,0 +1,37 @@ +import 'package:cake_wallet/exchange/provider/trocador_exchange_provider.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/src/screens/settings/widgets/settings_switcher_cell.dart'; +import 'package:cake_wallet/view_model/settings/trocador_providers_view_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; + +class TrocadorProvidersPage extends BasePage { + TrocadorProvidersPage(this.trocadorProvidersViewModel); + + @override + String get title => 'Trocador Providers'; + + final TrocadorProvidersViewModel trocadorProvidersViewModel; + + @override + Widget body(BuildContext context) { + final availableProviders = TrocadorExchangeProvider.availableProviders; + final providerStates = trocadorProvidersViewModel.providerStates; + return Container( + padding: EdgeInsets.only(top: 10), + child: ListView.builder( + itemCount: availableProviders.length, + itemBuilder: (_, index) { + String provider = availableProviders[index]; + return Observer( + builder: (_) => SettingsSwitcherCell( + title: provider, + value: providerStates[provider] ?? false, + onValueChange: (BuildContext _, bool value) { + trocadorProvidersViewModel.toggleProviderState(provider); + })); + }, + ), + ); + } +} diff --git a/lib/src/screens/settings/widgets/settings_version_cell.dart b/lib/src/screens/settings/widgets/settings_version_cell.dart index 8ab1a1672..1607012dc 100644 --- a/lib/src/screens/settings/widgets/settings_version_cell.dart +++ b/lib/src/screens/settings/widgets/settings_version_cell.dart @@ -25,4 +25,4 @@ class SettingsVersionCell extends StatelessWidget { ), ); } -} \ No newline at end of file +} diff --git a/lib/src/screens/settings/widgets/wallet_connect_button.dart b/lib/src/screens/settings/widgets/wallet_connect_button.dart new file mode 100644 index 000000000..d02462619 --- /dev/null +++ b/lib/src/screens/settings/widgets/wallet_connect_button.dart @@ -0,0 +1,46 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; +import 'package:cake_wallet/themes/extensions/transaction_trade_theme.dart'; +import 'package:flutter/material.dart'; + +class WalletConnectTile extends StatelessWidget { + const WalletConnectTile({required this.onTap}); + + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Padding( + padding: EdgeInsets.all(24), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Image.asset( + 'assets/images/walletconnect_logo.png', + height: 24, + width: 24, + ), + SizedBox(width: 16), + Expanded( + child: Text( + S.current.walletConnect, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + color: Theme.of(context).extension()!.titleColor, + ), + ), + ), + Image.asset( + 'assets/images/select_arrow.png', + color: Theme.of(context).extension()!.detailsTitlesColor, + ) + ], + ), + ), + ); + } +} diff --git a/lib/src/screens/setup_2fa/modify_2fa_page.dart b/lib/src/screens/setup_2fa/modify_2fa_page.dart index 148e3076e..601d084e7 100644 --- a/lib/src/screens/setup_2fa/modify_2fa_page.dart +++ b/lib/src/screens/setup_2fa/modify_2fa_page.dart @@ -133,6 +133,17 @@ class _2FAControlsWidget extends StatelessWidget { }, ), StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)), + Observer( + builder: (context) { + return SettingsSwitcherCell( + title: S.current.require_for_exchanges_to_external_wallets, + value: setup2FAViewModel.shouldRequireTOTP2FAForExchangesToExternalWallets, + onValueChange: (context, value) async => + setup2FAViewModel.switchShouldRequireTOTP2FAForExchangesToExternalWallets(value), + ); + }, + ), + StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)), Observer( builder: (context) { return SettingsSwitcherCell( diff --git a/lib/src/screens/setup_2fa/setup_2fa.dart b/lib/src/screens/setup_2fa/setup_2fa.dart index a74152e4f..895fbb9c0 100644 --- a/lib/src/screens/setup_2fa/setup_2fa.dart +++ b/lib/src/screens/setup_2fa/setup_2fa.dart @@ -53,8 +53,10 @@ class Setup2FAPage extends BasePage { SizedBox(height: 86), SettingsCellWithArrow( title: S.current.setup_totp_recommended, - handler: (_) => Navigator.of(context) - .pushReplacementNamed(Routes.setup_2faQRPage), + handler: (_) { + setup2FAViewModel.generateSecretKey(); + return Navigator.of(context).pushReplacementNamed(Routes.setup_2faQRPage); + }, ), StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)), ], diff --git a/lib/src/screens/setup_2fa/setup_2fa_qr_page.dart b/lib/src/screens/setup_2fa/setup_2fa_qr_page.dart index 82a079b39..43dbab05f 100644 --- a/lib/src/screens/setup_2fa/setup_2fa_qr_page.dart +++ b/lib/src/screens/setup_2fa/setup_2fa_qr_page.dart @@ -14,7 +14,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:qr_flutter/qr_flutter.dart' as qr; - class Setup2FAQRPage extends BasePage { Setup2FAQRPage({required this.setup2FAViewModel}); @@ -25,7 +24,6 @@ class Setup2FAQRPage extends BasePage { @override Widget body(BuildContext context) { - final copyImage = Image.asset( 'assets/images/copy_content.png', height: 12, @@ -91,7 +89,7 @@ class Setup2FAQRPage extends BasePage { ), SizedBox(height: 8), Text( - '${setup2FAViewModel.secretKey}', + '${setup2FAViewModel.totpSecretKey}', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w700, @@ -110,7 +108,63 @@ class Setup2FAQRPage extends BasePage { child: InkWell( onTap: () { ClipboardUtil.setSensitiveDataToClipboard( - ClipboardData(text: '${setup2FAViewModel.secretKey}')); + ClipboardData(text: '${setup2FAViewModel.totpSecretKey}')); + showBar(context, S.of(context).copied_to_clipboard); + }, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(6), + color: Color(0xFFF2F0FA), + ), + child: copyImage, + ), + ), + ) + ], + ), + SizedBox(height: 8), + StandardListSeparator(), + SizedBox(height: 13), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + flex: 2, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + S.current.totp_auth_url, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + color: Palette.darkGray, + height: 1.8333, + ), + ), + SizedBox(height: 8), + Text( + '${setup2FAViewModel.totpVersionOneLink}', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w700, + height: 1.375, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + SizedBox(width: 8), + Container( + width: 32, + height: 32, + child: InkWell( + onTap: () { + ClipboardUtil.setSensitiveDataToClipboard( + ClipboardData(text: '${setup2FAViewModel.totpVersionOneLink}')); showBar(context, S.of(context).copied_to_clipboard); }, child: Container( @@ -129,13 +183,10 @@ class Setup2FAQRPage extends BasePage { Spacer(), PrimaryButton( onPressed: () { - Navigator.of(context).pushReplacementNamed( - Routes.totpAuthCodePage, + Navigator.of(context).pushReplacementNamed(Routes.totpAuthCodePage, arguments: TotpAuthArgumentsModel( isForSetup: true, - ) - - ); + )); }, text: S.current.continue_text, color: Theme.of(context).primaryColor, diff --git a/lib/src/screens/support/support_page.dart b/lib/src/screens/support/support_page.dart index 883677832..471ff15b0 100644 --- a/lib/src/screens/support/support_page.dart +++ b/lib/src/screens/support/support_page.dart @@ -1,7 +1,7 @@ import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/routes.dart'; -import 'package:cake_wallet/src/screens/support/widgets/support_tiles.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/src/widgets/option_tile.dart'; import 'package:cake_wallet/utils/device_info.dart'; import 'package:cake_wallet/view_model/support_view_model.dart'; import 'package:flutter/material.dart'; @@ -32,7 +32,7 @@ class SupportPage extends BasePage { children: [ Padding( padding: EdgeInsets.only(top: 24), - child: SupportTile( + child: OptionTile( image: imageLiveSupport, title: S.of(context).support_title_live_chat, description: S.of(context).support_description_live_chat, @@ -47,7 +47,7 @@ class SupportPage extends BasePage { ), Padding( padding: EdgeInsets.only(top: 24), - child: SupportTile( + child: OptionTile( image: imageWalletGuides, title: S.of(context).support_title_guides, description: S.of(context).support_description_guides, @@ -56,7 +56,7 @@ class SupportPage extends BasePage { ), Padding( padding: EdgeInsets.only(top: 24), - child: SupportTile( + child: OptionTile( image: imageMoreLinks, title: S.of(context).support_title_other_links, description: S.of(context).support_description_other_links, diff --git a/lib/src/screens/trade_details/trade_details_page.dart b/lib/src/screens/trade_details/trade_details_page.dart index 17683c600..1028c3939 100644 --- a/lib/src/screens/trade_details/trade_details_page.dart +++ b/lib/src/screens/trade_details/trade_details_page.dart @@ -1,18 +1,19 @@ +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/src/screens/trade_details/track_trade_list_item.dart'; +import 'package:cake_wallet/src/screens/trade_details/trade_details_list_card.dart'; +import 'package:cake_wallet/src/screens/trade_details/trade_details_status_item.dart'; +import 'package:cake_wallet/src/screens/trade_details/trade_provider_unsupported_item.dart'; +import 'package:cake_wallet/src/widgets/list_row.dart'; import 'package:cake_wallet/src/widgets/standard_list.dart'; import 'package:cake_wallet/src/widgets/standard_list_card.dart'; import 'package:cake_wallet/src/widgets/standard_list_status_row.dart'; import 'package:cake_wallet/utils/show_bar.dart'; import 'package:cake_wallet/view_model/trade_details_view_model.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/services.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; -import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/src/screens/base_page.dart'; -import 'package:cake_wallet/src/widgets/list_row.dart'; -import 'package:cake_wallet/src/screens/trade_details/track_trade_list_item.dart'; -import 'package:cake_wallet/src/screens/trade_details/trade_details_list_card.dart'; -import 'package:cake_wallet/src/screens/trade_details/trade_details_status_item.dart'; class TradeDetailsPage extends BasePage { TradeDetailsPage(this.tradeDetailsViewModel); @@ -23,8 +24,7 @@ class TradeDetailsPage extends BasePage { final TradeDetailsViewModel tradeDetailsViewModel; @override - Widget body(BuildContext context) => - TradeDetailsPageBody(tradeDetailsViewModel); + Widget body(BuildContext context) => TradeDetailsPageBody(tradeDetailsViewModel); } class TradeDetailsPageBody extends StatefulWidget { @@ -33,8 +33,7 @@ class TradeDetailsPageBody extends StatefulWidget { final TradeDetailsViewModel tradeDetailsViewModel; @override - TradeDetailsPageBodyState createState() => - TradeDetailsPageBodyState(tradeDetailsViewModel); + TradeDetailsPageBodyState createState() => TradeDetailsPageBodyState(tradeDetailsViewModel); } class TradeDetailsPageBodyState extends State { @@ -51,44 +50,47 @@ class TradeDetailsPageBodyState extends State { @override Widget build(BuildContext context) { return Observer(builder: (_) { - // FIX-ME: Added `context` it was not used here before, maby bug ? + final itemsCount = tradeDetailsViewModel.items.length; + return SectionStandardList( sectionCount: 1, - itemCounter: (int _) => tradeDetailsViewModel.items.length, + itemCounter: (int _) => itemsCount, itemBuilder: (__, index) { final item = tradeDetailsViewModel.items[index]; - if (item is TrackTradeListItem) { + if (item is TrackTradeListItem) return GestureDetector( onTap: item.onTap, - child: ListRow( - title: '${item.title}', value: '${item.value}')); - } + child: ListRow(title: '${item.title}', value: '${item.value}')); - if (item is DetailsListStatusItem) { - return StandardListStatusRow( - title: item.title, - value: item.value); - } + if (item is DetailsListStatusItem) + return StandardListStatusRow(title: item.title, value: item.value); - if (item is TradeDetailsListCardItem) { + if (item is TradeDetailsListCardItem) return TradeDetailsStandardListCard( - id: item.id, - create: item.createdAt, - pair: item.pair, - currentTheme: tradeDetailsViewModel.settingsStore.currentTheme.type, - onTap: item.onTap,); - } + id: item.id, + create: item.createdAt, + pair: item.pair, + currentTheme: tradeDetailsViewModel.settingsStore.currentTheme.type, + onTap: item.onTap, + ); - return GestureDetector( - onTap: () { - Clipboard.setData(ClipboardData(text: '${item.value}')); - showBar(context, S.of(context).copied_to_clipboard); - }, - child: ListRow( - title: '${item.title}', value: '${item.value}')); + if (item is TradeProviderUnsupportedItem) + return AutoSizeText(item.value, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Colors.red, + )); + + return GestureDetector( + onTap: () { + Clipboard.setData(ClipboardData(text: '${item.value}')); + showBar(context, S.of(context).copied_to_clipboard); + }, + child: ListRow(title: '${item.title}', value: '${item.value}')); }); }); } - } diff --git a/lib/src/screens/trade_details/trade_provider_unsupported_item.dart b/lib/src/screens/trade_details/trade_provider_unsupported_item.dart new file mode 100644 index 000000000..2c4e96c84 --- /dev/null +++ b/lib/src/screens/trade_details/trade_provider_unsupported_item.dart @@ -0,0 +1,5 @@ +import 'package:cake_wallet/src/screens/transaction_details/standart_list_item.dart'; + +class TradeProviderUnsupportedItem extends StandartListItem { + TradeProviderUnsupportedItem({required String error}) : super(title: '', value: error); +} diff --git a/lib/src/screens/unspent_coins/unspent_coins_list_page.dart b/lib/src/screens/unspent_coins/unspent_coins_list_page.dart index 1c1fbfa5d..36cbda641 100644 --- a/lib/src/screens/unspent_coins/unspent_coins_list_page.dart +++ b/lib/src/screens/unspent_coins/unspent_coins_list_page.dart @@ -1,13 +1,13 @@ -import 'package:cake_wallet/routes.dart'; -import 'package:cake_wallet/src/screens/unspent_coins/widgets/unspent_coins_list_item.dart'; -import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; -import 'package:cake_wallet/utils/show_pop_up.dart'; -import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_list_view_model.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:cake_wallet/src/screens/base_page.dart'; -import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.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/unspent_coins/widgets/unspent_coins_list_item.dart'; +import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_list_view_model.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; class UnspentCoinsListPage extends BasePage { UnspentCoinsListPage({required this.unspentCoinsListViewModel}); @@ -15,31 +15,10 @@ class UnspentCoinsListPage extends BasePage { @override String get title => S.current.unspent_coins_title; - //@override - //Widget trailing(BuildContext context) { - // final questionImage = Image.asset('assets/images/question_mark.png', - // color: Theme.of(context).extension()!.titleColor); - - // return SizedBox( - // height: 20.0, - // width: 20.0, - // child: ButtonTheme( - // minWidth: double.minPositive, - // child: FlatButton( - // highlightColor: Colors.transparent, - // splashColor: Colors.transparent, - // padding: EdgeInsets.all(0), - // onPressed: () => showUnspentCoinsAlert(context), - // child: questionImage), - // ), - // ); - //} - final UnspentCoinsListViewModel unspentCoinsListViewModel; @override - Widget body(BuildContext context) => - UnspentCoinsListForm(unspentCoinsListViewModel); + Widget body(BuildContext context) => UnspentCoinsListForm(unspentCoinsListViewModel); } class UnspentCoinsListForm extends StatefulWidget { @@ -48,8 +27,7 @@ class UnspentCoinsListForm extends StatefulWidget { final UnspentCoinsListViewModel unspentCoinsListViewModel; @override - UnspentCoinsListFormState createState() => - UnspentCoinsListFormState(unspentCoinsListViewModel); + UnspentCoinsListFormState createState() => UnspentCoinsListFormState(unspentCoinsListViewModel); } class UnspentCoinsListFormState extends State { @@ -57,16 +35,6 @@ class UnspentCoinsListFormState extends State { final UnspentCoinsListViewModel unspentCoinsListViewModel; - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addPostFrameCallback(afterLayout); - } - - void afterLayout(dynamic _) { - //showUnspentCoinsAlert(context); - } - @override Widget build(BuildContext context) { return Container( @@ -74,45 +42,31 @@ class UnspentCoinsListFormState extends State { child: Observer( builder: (_) => ListView.separated( itemCount: unspentCoinsListViewModel.items.length, - separatorBuilder: (_, __) => - SizedBox(height: 15), + separatorBuilder: (_, __) => SizedBox(height: 15), itemBuilder: (_, int index) { return Observer(builder: (_) { final item = unspentCoinsListViewModel.items[index]; + final address = unspentCoinsListViewModel.wallet.type == WalletType.bitcoinCash + ? bitcoinCash!.getCashAddrFormat(item.address) + : item.address; return GestureDetector( - onTap: () => - Navigator.of(context) - .pushNamed(Routes.unspentCoinsDetails, - arguments: [item, unspentCoinsListViewModel]), + onTap: () => Navigator.of(context).pushNamed(Routes.unspentCoinsDetails, + arguments: [item, unspentCoinsListViewModel]), child: UnspentCoinsListItem( note: item.note, amount: item.amount, - address: item.address, + address: address, isSending: item.isSending, isFrozen: item.isFrozen, + isChange: item.isChange, onCheckBoxTap: item.isFrozen - ? null - : () async { - item.isSending = !item.isSending; - await unspentCoinsListViewModel - .saveUnspentCoinInfo(item);})); + ? null + : () async { + item.isSending = !item.isSending; + await unspentCoinsListViewModel.saveUnspentCoinInfo(item); + })); }); - } - ) - ) - ); + }))); } } - -void showUnspentCoinsAlert(BuildContext context) { - showPopUp( - context: context, - builder: (BuildContext context) { - return AlertWithOneAction( - alertTitle: '', - alertContent: 'Information about unspent coins', - buttonText: S.of(context).ok, - buttonAction: () => Navigator.of(context).pop()); - }); -} \ No newline at end of file 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 93cf27af1..d629e9454 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 @@ -1,8 +1,8 @@ import 'package:auto_size_text/auto_size_text.dart'; +import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/widgets/standard_checkbox.dart'; import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; import 'package:flutter/material.dart'; -import 'package:cake_wallet/generated/i18n.dart'; class UnspentCoinsListItem extends StatelessWidget { UnspentCoinsListItem({ @@ -11,6 +11,7 @@ class UnspentCoinsListItem extends StatelessWidget { required this.address, required this.isSending, required this.isFrozen, + required this.isChange, this.onCheckBoxTap, }); @@ -19,6 +20,7 @@ class UnspentCoinsListItem extends StatelessWidget { final String address; final bool isSending; final bool isFrozen; + final bool isChange; final Function()? onCheckBoxTap; @override @@ -27,9 +29,8 @@ class UnspentCoinsListItem extends StatelessWidget { final selectedItemColor = Theme.of(context).primaryColor; final itemColor = isSending ? selectedItemColor : unselectedItemColor; - final amountColor = isSending - ? Colors.white - : Theme.of(context).extension()!.buttonTextColor; + final amountColor = + isSending ? Colors.white : Theme.of(context).extension()!.buttonTextColor; final addressColor = isSending ? Colors.white.withOpacity(0.5) : Theme.of(context).extension()!.buttonSecondaryTextColor; @@ -47,7 +48,8 @@ class UnspentCoinsListItem extends StatelessWidget { child: StandardCheckbox( iconColor: amountColor, borderColor: addressColor, - value: isSending, onChanged: (value) => onCheckBoxTap?.call())), + value: isSending, + onChanged: (value) => onCheckBoxTap?.call())), Expanded( child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -57,9 +59,7 @@ class UnspentCoinsListItem extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ + Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ if (note.isNotEmpty) AutoSizeText( note, @@ -69,8 +69,8 @@ class UnspentCoinsListItem extends StatelessWidget { ), AutoSizeText( amount, - style: - TextStyle(color: amountColor, fontSize: 15, fontWeight: FontWeight.w600), + style: TextStyle( + color: amountColor, fontSize: 15, fontWeight: FontWeight.w600), maxLines: 1, ) ]), @@ -84,23 +84,41 @@ class UnspentCoinsListItem extends StatelessWidget { alignment: Alignment.center, child: Text( S.of(context).frozen, - style: - TextStyle(color: amountColor, fontSize: 7, fontWeight: FontWeight.w600), - )) + style: TextStyle( + color: amountColor, fontSize: 7, fontWeight: FontWeight.w600), + )), ], ), Expanded( child: Row( crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ AutoSizeText( - '${address.substring(0, 5)}...${address.substring(address.length-5)}', // ToDo: Maybe use address label + '${address.substring(0, 5)}...${address.substring(address.length - 5)}', // ToDo: Maybe use address label style: TextStyle( color: addressColor, fontSize: 12, ), 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, + ), + ), + ), ], ), ), diff --git a/lib/src/screens/wallet_connect/utils/namespace_model_builder.dart b/lib/src/screens/wallet_connect/utils/namespace_model_builder.dart new file mode 100644 index 000000000..936df93d3 --- /dev/null +++ b/lib/src/screens/wallet_connect/utils/namespace_model_builder.dart @@ -0,0 +1,71 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/screens/wallet_connect/widgets/connection_widget.dart'; +import 'package:walletconnect_flutter_v2/walletconnect_flutter_v2.dart'; + +import '../../../../core/wallet_connect/models/connection_model.dart'; + +class ConnectionWidgetBuilder { + static List buildFromRequiredNamespaces( + Map requiredNamespaces, + ) { + final List views = []; + for (final key in requiredNamespaces.keys) { + RequiredNamespace ns = requiredNamespaces[key]!; + final List models = []; + // If the chains property is present, add the chain data to the models + if (ns.chains != null) { + models.add(ConnectionModel(title: S.current.chains, elements: ns.chains!)); + } + models.add(ConnectionModel(title: S.current.methods, elements: ns.methods)); + models.add(ConnectionModel(title: S.current.events, elements: ns.events)); + + views.add(ConnectionWidget(title: key, info: models)); + } + + return views; + } + + static List buildFromNamespaces( + String topic, + Map namespaces, + Web3Wallet web3wallet, + ) { + final List views = []; + for (final key in namespaces.keys) { + final Namespace ns = namespaces[key]!; + final List models = []; + // If the chains property is present, add the chain data to the models + models.add( + ConnectionModel( + title: S.current.chains, + elements: ns.accounts, + ), + ); + models.add(ConnectionModel( + title: S.current.methods, + elements: ns.methods, + )); + + Map actions = {}; + for (final String event in ns.events) { + actions[event] = () async { + final String chainId = NamespaceUtils.isValidChainId(key) + ? key + : NamespaceUtils.getChainFromAccount(ns.accounts.first); + await web3wallet.emitSessionEvent( + topic: topic, + chainId: chainId, + event: SessionEventParams(name: event, data: '${S.current.event}: $event'), + ); + }; + } + models.add( + ConnectionModel(title: S.current.events, elements: ns.events, elementActions: actions), + ); + + views.add(ConnectionWidget(title: key, info: models)); + } + + return views; + } +} diff --git a/lib/src/screens/wallet_connect/utils/string_parsing.dart b/lib/src/screens/wallet_connect/utils/string_parsing.dart new file mode 100644 index 000000000..b9fdca7b2 --- /dev/null +++ b/lib/src/screens/wallet_connect/utils/string_parsing.dart @@ -0,0 +1,16 @@ +import 'dart:convert'; + +import 'package:convert/convert.dart'; + +extension StringParsing on String { + String get utf8Message { + if (startsWith('0x')) { + final List decoded = hex.decode( + substring(2), + ); + return utf8.decode(decoded); + } + + return this; + } +} diff --git a/lib/src/screens/wallet_connect/wc_connections_listing_view.dart b/lib/src/screens/wallet_connect/wc_connections_listing_view.dart new file mode 100644 index 000000000..359d96b26 --- /dev/null +++ b/lib/src/screens/wallet_connect/wc_connections_listing_view.dart @@ -0,0 +1,176 @@ +import 'dart:developer'; +import 'package:cake_wallet/core/wallet_connect/web3wallet_service.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/src/screens/wallet_connect/widgets/enter_wallet_connect_uri_widget.dart'; +import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; +import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; +import 'package:cake_wallet/utils/device_info.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:walletconnect_flutter_v2/walletconnect_flutter_v2.dart'; +import 'package:cake_wallet/entities/qr_scanner.dart'; +import 'package:cake_wallet/src/widgets/primary_button.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; +import 'package:cake_wallet/utils/permission_handler.dart'; + +import 'widgets/pairing_item_widget.dart'; +import 'wc_pairing_detail_page.dart'; + +class WalletConnectConnectionsView extends StatelessWidget { + final Web3WalletService web3walletService; + + WalletConnectConnectionsView({required this.web3walletService, Uri? launchUri, Key? key}) + : super(key: key) { + _triggerPairingFromDeeplink(launchUri); + } + + void _triggerPairingFromDeeplink(Uri? launchUri) async { + if (launchUri == null) return; + + final actualLinkList = launchUri.query.split("uri="); + + final query = actualLinkList[1]; + + final uri = Uri.decodeComponent(query); + + final uriData = Uri.parse(uri); + + await web3walletService.pairWithUri(uriData); + } + + @override + Widget build(BuildContext context) { + return WCPairingsWidget(web3walletService: web3walletService); + } +} + +class WCPairingsWidget extends BasePage { + WCPairingsWidget({required this.web3walletService, Key? key}) + : web3wallet = web3walletService.getWeb3Wallet(); + + final Web3Wallet web3wallet; + final Web3WalletService web3walletService; + + @override + String get title => S.current.walletConnect; + + Future _onScanQrCode(BuildContext context, Web3Wallet web3Wallet) async { + final String? uri; + + if (DeviceInfo.instance.isMobile) { + bool isCameraPermissionGranted = + await PermissionHandler.checkPermission(Permission.camera, context); + if (!isCameraPermissionGranted) return; + uri = await presentQRScanner(); + } else { + uri = await _showEnterWalletConnectURIPopUp(context); + } + + if (uri == null) return _invalidUriToast(context, S.current.nullURIError); + + log('_onFoundUri: $uri'); + final Uri uriData = Uri.parse(uri); + await web3walletService.pairWithUri(uriData); + } + + Future _showEnterWalletConnectURIPopUp(BuildContext context) async { + final walletConnectURI = await showPopUp( + context: context, + builder: (BuildContext context) { + return EnterWalletConnectURIWrapperWidget(); + }, + ); + return walletConnectURI; + } + + Future _invalidUriToast(BuildContext context, String message) async { + await showPopUp( + context: context, + builder: (BuildContext context) { + return AlertWithOneAction( + alertTitle: S.of(context).error, + alertContent: message, + buttonText: S.of(context).ok, + buttonAction: Navigator.of(context).pop, + alertBarrierDismissible: false, + ); + }, + ); + } + + @override + Widget body(BuildContext context) { + return Observer( + builder: (context) { + return Column( + children: [ + Padding( + padding: EdgeInsets.symmetric(horizontal: 24), + child: Column( + children: [ + SizedBox(height: 24), + Text( + S.current.connectWalletPrompt, + style: TextStyle( + fontSize: 16.0, + fontWeight: FontWeight.normal, + color: Theme.of(context).extension()!.titleColor, + ), + ), + SizedBox(height: 16), + PrimaryButton( + text: S.current.newConnection, + color: Theme.of(context).primaryColor, + textColor: Colors.white, + onPressed: () => _onScanQrCode(context, web3wallet), + ), + ], + ), + ), + SizedBox(height: 48), + Expanded( + child: Visibility( + visible: web3walletService.pairings.isEmpty, + child: Center( + child: Text( + S.current.activeConnectionsPrompt, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 16.0, + fontWeight: FontWeight.normal, + color: Theme.of(context).extension()!.titleColor, + ), + ), + ), + replacement: ListView.builder( + itemCount: web3walletService.pairings.length, + itemBuilder: (BuildContext context, int index) { + final pairing = web3walletService.pairings[index]; + return PairingItemWidget( + key: ValueKey(pairing.topic), + pairing: pairing, + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => WalletConnectPairingDetailsPage( + pairing: pairing, + web3walletService: web3walletService, + ), + ), + ); + }, + ); + }, + ), + ), + ), + SizedBox(height: 48), + ], + ); + }, + ); + } +} diff --git a/lib/src/screens/wallet_connect/wc_pairing_detail_page.dart b/lib/src/screens/wallet_connect/wc_pairing_detail_page.dart new file mode 100644 index 000000000..f99eb9cdb --- /dev/null +++ b/lib/src/screens/wallet_connect/wc_pairing_detail_page.dart @@ -0,0 +1,186 @@ +import 'dart:developer'; + +import 'package:cake_wallet/core/wallet_connect/web3wallet_service.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; +import 'package:cake_wallet/src/widgets/primary_button.dart'; +import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; +import 'package:flutter/material.dart'; +import 'package:walletconnect_flutter_v2/walletconnect_flutter_v2.dart'; + +import 'utils/namespace_model_builder.dart'; + +class WalletConnectPairingDetailsPage extends StatefulWidget { + final PairingInfo pairing; + final Web3WalletService web3walletService; + + const WalletConnectPairingDetailsPage({ + required this.pairing, + required this.web3walletService, + super.key, + }); + + @override + WalletConnectPairingDetailsPageState createState() => WalletConnectPairingDetailsPageState(); +} + +class WalletConnectPairingDetailsPageState extends State { + List sessionWidgets = []; + late String expiryDate; + @override + void initState() { + super.initState(); + initDateTime(); + initSessions(); + } + + void initDateTime() { + DateTime dateTime = DateTime.fromMillisecondsSinceEpoch(widget.pairing.expiry * 1000); + int year = dateTime.year; + int month = dateTime.month; + int day = dateTime.day; + + expiryDate = '$year-${month.toString().padLeft(2, '0')}-${day.toString().padLeft(2, '0')}'; + } + + void initSessions() { + List sessions = widget.web3walletService.getSessionsForPairingInfo(widget.pairing); + + for (final SessionData session in sessions) { + List namespaceWidget = ConnectionWidgetBuilder.buildFromNamespaces( + session.topic, + session.namespaces, + widget.web3walletService.getWeb3Wallet(), + ); + // Loop through and add the namespace widgets, but put 20 pixels between each one + for (int i = 0; i < namespaceWidget.length; i++) { + sessionWidgets.add(namespaceWidget[i]); + if (i != namespaceWidget.length - 1) { + sessionWidgets.add(const SizedBox(height: 20.0)); + } + } + } + } + + @override + Widget build(BuildContext context) { + return WCCDetailsWidget( + widget.pairing, + expiryDate, + sessionWidgets, + widget.web3walletService, + ); + } +} + +class WCCDetailsWidget extends BasePage { + WCCDetailsWidget( + this.pairing, + this.expiryDate, + this.sessionWidgets, + this.web3walletService, + ); + + final PairingInfo pairing; + final String expiryDate; + final List sessionWidgets; + final Web3WalletService web3walletService; + + @override + Widget body(BuildContext context) { + return Scaffold( + body: SingleChildScrollView( + child: Container( + padding: const EdgeInsets.all(8), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: CircleAvatar( + backgroundImage: (pairing.peerMetadata!.icons.isNotEmpty + ? NetworkImage(pairing.peerMetadata!.icons[0]) + : const AssetImage('assets/images/default_icon.png')) + as ImageProvider, + ), + ), + const SizedBox(height: 20.0), + Text( + pairing.peerMetadata!.name, + style: TextStyle( + fontSize: 16.0, + fontWeight: FontWeight.w500, + color: Theme.of(context).extension()!.titleColor, + ), + ), + const SizedBox(height: 16.0), + Text( + pairing.peerMetadata!.url, + style: TextStyle( + fontSize: 14.0, + fontWeight: FontWeight.normal, + color: Theme.of(context).extension()!.titleColor, + ), + ), + const SizedBox(height: 8.0), + Text( + '${S.current.expiresOn}: $expiryDate', + style: TextStyle( + fontSize: 14.0, + fontWeight: FontWeight.normal, + color: Theme.of(context).extension()!.titleColor, + ), + ), + const SizedBox(height: 20.0), + Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: sessionWidgets, + ), + const SizedBox(height: 20.0), + PrimaryButton( + onPressed: () => + _onDeleteButtonPressed(context, pairing.peerMetadata!.name, web3walletService), + text: S.current.delete, + color: Theme.of(context).primaryColor, + textColor: Colors.white, + ), + ], + ), + ), + ), + ); + } + + Future _onDeleteButtonPressed( + BuildContext context, String dAppName, Web3WalletService web3walletService) async { + bool confirmed = false; + + await showPopUp( + context: context, + builder: (BuildContext dialogContext) { + return AlertWithTwoActions( + alertTitle: S.of(context).delete, + alertContent: '${S.current.deleteConnectionConfirmationPrompt} $dAppName?', + leftButtonText: S.of(context).cancel, + rightButtonText: S.of(context).delete, + actionLeftButton: () => Navigator.of(dialogContext).pop(), + actionRightButton: () { + confirmed = true; + Navigator.of(dialogContext).pop(); + }, + ); + }, + ); + if (confirmed) { + try { + await web3walletService.disconnectSession(pairing.topic); + + Navigator.of(context).pop(); + } catch (e) { + log(e.toString()); + } + } + } +} diff --git a/lib/src/screens/wallet_connect/widgets/connection_item_widget.dart b/lib/src/screens/wallet_connect/widgets/connection_item_widget.dart new file mode 100644 index 000000000..77c30417a --- /dev/null +++ b/lib/src/screens/wallet_connect/widgets/connection_item_widget.dart @@ -0,0 +1,102 @@ +import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; +import 'package:flutter/material.dart'; +import '../../../../core/wallet_connect/models/connection_model.dart'; + +class ConnectionItemWidget extends StatelessWidget { + const ConnectionItemWidget({required this.model, Key? key}) : super(key: key); + + final ConnectionModel model; + + @override + Widget build(BuildContext context) { + + return Container( + width: double.infinity, + decoration: BoxDecoration( + color: Theme.of(context).cardColor, + borderRadius: BorderRadius.circular(8), + ), + padding: const EdgeInsets.all(8), + margin: const EdgeInsetsDirectional.only(top: 8), + child: Visibility( + visible: model.elements != null, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + model.title ?? '', + style: TextStyle( + color: Theme.of(context).extension()!.titleColor, + fontSize: 14, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 8), + if (model.elements != null) + Wrap( + spacing: 4, + runSpacing: 4, + direction: Axis.horizontal, + children: model.elements! + .map((e) => _ModelElementWidget(model: model, modelElement: e)) + .toList(), + ), + ], + ), + replacement: _NoModelElementWidget(model: model), + ), + ); + } +} + +class _NoModelElementWidget extends StatelessWidget { + const _NoModelElementWidget({required this.model}); + + final ConnectionModel model; + + @override + Widget build(BuildContext context) { + return Text( + model.text!, + style: TextStyle( + color: Theme.of(context).extension()!.titleColor, + fontSize: 14, + fontWeight: FontWeight.w600, + ), + ); + } +} + +class _ModelElementWidget extends StatelessWidget { + const _ModelElementWidget({ + required this.model, + required this.modelElement, + }); + + final ConnectionModel model; + final String modelElement; + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: model.elementActions != null ? model.elementActions![modelElement] : null, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.background, + borderRadius: BorderRadius.circular(6), + ), + padding: const EdgeInsets.all(8), + child: Text( + modelElement, + style: TextStyle( + color: Theme.of(context).extension()!.titleColor, + fontSize: 14, + fontWeight: FontWeight.w600, + ), + maxLines: 10, + overflow: TextOverflow.ellipsis, + ), + ), + ); + } +} diff --git a/lib/src/screens/wallet_connect/widgets/connection_request_widget.dart b/lib/src/screens/wallet_connect/widgets/connection_request_widget.dart new file mode 100644 index 000000000..c73c4bfa8 --- /dev/null +++ b/lib/src/screens/wallet_connect/widgets/connection_request_widget.dart @@ -0,0 +1,166 @@ +// ignore_for_file: public_member_api_docs, sort_constructors_first +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:flutter/material.dart'; +import 'package:walletconnect_flutter_v2/walletconnect_flutter_v2.dart'; + +import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; + +import '../../../../core/wallet_connect/models/auth_request_model.dart'; +import '../../../../core/wallet_connect/models/connection_model.dart'; +import '../../../../core/wallet_connect/models/session_request_model.dart'; +import '../utils/namespace_model_builder.dart'; +import 'connection_widget.dart'; + +class ConnectionRequestWidget extends StatefulWidget { + const ConnectionRequestWidget({ + required this.wallet, + this.authRequest, + this.sessionProposal, + Key? key, + }) : super(key: key); + + final Web3Wallet wallet; + final AuthRequestModel? authRequest; + final SessionRequestModel? sessionProposal; + + @override + State createState() => _ConnectionRequestWidgetState(); +} + +class _ConnectionRequestWidgetState extends State { + ConnectionMetadata? metadata; + + @override + void initState() { + super.initState(); + // Get the connection metadata + metadata = widget.authRequest?.request.requester ?? widget.sessionProposal?.request.proposer; + } + + @override + Widget build(BuildContext context) { + if (metadata == null) { + return Text( + S.current.error, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + color: Theme.of(context).extension()!.secondaryTextColor, + ), + ); + } + + return _ConnectionMetadataDisplayWidget( + metadata: metadata, + authRequest: widget.authRequest, + sessionProposal: widget.sessionProposal, + wallet: widget.wallet, + ); + } +} + +class _ConnectionMetadataDisplayWidget extends StatelessWidget { + const _ConnectionMetadataDisplayWidget({ + required this.metadata, + required this.wallet, + this.authRequest, + required this.sessionProposal, + }); + + final ConnectionMetadata? metadata; + final Web3Wallet wallet; + final AuthRequestModel? authRequest; + final SessionRequestModel? sessionProposal; + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: Color.fromARGB(255, 18, 18, 19), + borderRadius: BorderRadius.circular(8), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + metadata!.metadata.name, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.normal, + color: Theme.of(context).extension()!.secondaryTextColor, + ), + textAlign: TextAlign.center, + ), + Text( + S.current.wouoldLikeToConnect, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + color: Theme.of(context).extension()!.secondaryTextColor, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + Text( + metadata!.metadata.url, + style: TextStyle( + fontSize: 16.0, + fontWeight: FontWeight.normal, + color: Theme.of(context).extension()!.secondaryTextColor, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + Visibility( + visible: authRequest != null, + child: _AuthRequestWidget(wallet: wallet, authRequest: authRequest), + + //If authRequest is null, sessionProposal is not null. + replacement: _SessionProposalWidget(sessionProposal: sessionProposal!), + ), + ], + ), + ); + } +} + +class _AuthRequestWidget extends StatelessWidget { + const _AuthRequestWidget({required this.wallet, this.authRequest}); + + final Web3Wallet wallet; + final AuthRequestModel? authRequest; + + @override + Widget build(BuildContext context) { + final model = ConnectionModel( + text: wallet.formatAuthMessage( + iss: 'did:pkh:eip155:1:${authRequest!.iss}', + cacaoPayload: CacaoRequestPayload.fromPayloadParams( + authRequest!.request.payloadParams, + ), + ), + ); + return ConnectionWidget( + title: S.current.message, + info: [model], + ); + } +} + +class _SessionProposalWidget extends StatelessWidget { + const _SessionProposalWidget({required this.sessionProposal}); + + final SessionRequestModel sessionProposal; + + @override + Widget build(BuildContext context) { + // Create the connection models using the required and optional namespaces provided by the proposal data + // The key is the title and the list of values is the data + final List views = ConnectionWidgetBuilder.buildFromRequiredNamespaces( + sessionProposal.request.requiredNamespaces, + ); + + return Column(children: views); + } +} diff --git a/lib/src/screens/wallet_connect/widgets/connection_widget.dart b/lib/src/screens/wallet_connect/widgets/connection_widget.dart new file mode 100644 index 000000000..921d8ea5c --- /dev/null +++ b/lib/src/screens/wallet_connect/widgets/connection_widget.dart @@ -0,0 +1,45 @@ +import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; +import 'package:flutter/material.dart'; + +import '../../../../core/wallet_connect/models/connection_model.dart'; +import 'connection_item_widget.dart'; + +class ConnectionWidget extends StatelessWidget { + const ConnectionWidget({required this.title, required this.info, super.key}); + + final String title; + final List info; + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: Theme.of(context).primaryColorLight, + borderRadius: BorderRadius.circular(8), + ), + padding: const EdgeInsets.all(8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.background, + borderRadius: BorderRadius.circular(8), + ), + padding: EdgeInsets.symmetric(vertical: 8, horizontal: 8), + child: Text( + title, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Theme.of(context).extension()!.titleColor, + ), + ), + ), + const SizedBox(height: 8), + ...info.map((e) => ConnectionItemWidget(model: e)), + ], + ), + ); + } +} diff --git a/lib/src/screens/wallet_connect/widgets/enter_wallet_connect_uri_widget.dart b/lib/src/screens/wallet_connect/widgets/enter_wallet_connect_uri_widget.dart new file mode 100644 index 000000000..927e7fb02 --- /dev/null +++ b/lib/src/screens/wallet_connect/widgets/enter_wallet_connect_uri_widget.dart @@ -0,0 +1,140 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/widgets/base_alert_dialog.dart'; +import 'package:cake_wallet/themes/extensions/send_page_theme.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class EnterWalletConnectURIWrapperWidget extends StatefulWidget { + const EnterWalletConnectURIWrapperWidget({super.key}); + + @override + State createState() => + _EnterWallectConnectURIWrapperWidgetState(); +} + +class _EnterWallectConnectURIWrapperWidgetState extends State { + late final TextEditingController controller; + + @override + void initState() { + super.initState(); + controller = TextEditingController(); + } + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return _EnterWalletConnectURIWidget( + controller: controller, + ); + } +} + +class _EnterWalletConnectURIWidget extends BaseAlertDialog { + _EnterWalletConnectURIWidget({ + required this.controller, + }); + + final TextEditingController controller; + + @override + String get titleText => S.current.enterWalletConnectURI; + + Future _pasteWalletConnectURI() async { + final clipboard = await Clipboard.getData('text/plain'); + final totpURI = clipboard?.text ?? ''; + + if (totpURI.isNotEmpty) { + controller.text = totpURI; + } + } + + @override + Widget content(BuildContext context) { + return Card( + margin: EdgeInsets.zero, + child: Column( + children: [ + SizedBox(height: 8), + Text( + S.current.copyWalletConnectLink, + style: Theme.of(context).textTheme.bodySmall, + ), + SizedBox(height: 16), + TextField( + controller: controller, + style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white), + decoration: InputDecoration( + suffixIcon: Container( + width: 24, + height: 24, + padding: EdgeInsets.only(top: 0), + child: Semantics( + label: S.of(context).paste, + child: InkWell( + onTap: () => _pasteWalletConnectURI(), + child: Container( + padding: EdgeInsets.all(8), + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(6)), + ), + child: Image.asset( + 'assets/images/paste_ios.png', + color: + Theme.of(context).extension()!.textFieldButtonIconColor, + ), + ), + ), + ), + ), + hintText: S.current.enterWalletConnectURI, + border: OutlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).extension()!.textFieldBorderColor, + ), + ), + hintStyle: TextStyle( + color: Theme.of(context).extension()!.textFieldHintColor, + fontWeight: FontWeight.w500, + fontSize: 14, + ), + ), + ), + ], + ), + ); + } + + @override + Widget actionButtons(BuildContext context) { + return Container( + width: 300, + height: 52, + padding: EdgeInsets.only(left: 12, right: 12), + color: Theme.of(context).dialogBackgroundColor, + child: ButtonTheme( + minWidth: double.infinity, + child: TextButton( + onPressed: () { + Navigator.pop(context, controller.text); + }, + child: Text( + S.current.confirm, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + color: Theme.of(context).primaryColor, + decoration: TextDecoration.none, + ), + ), + ), + ), + ); + } +} diff --git a/lib/src/screens/wallet_connect/widgets/message_display_widget.dart b/lib/src/screens/wallet_connect/widgets/message_display_widget.dart new file mode 100644 index 000000000..044915511 --- /dev/null +++ b/lib/src/screens/wallet_connect/widgets/message_display_widget.dart @@ -0,0 +1,42 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:flutter/material.dart'; + +class BottomSheetMessageDisplayWidget extends StatelessWidget { + final String message; + final bool isError; + + const BottomSheetMessageDisplayWidget({super.key, required this.message, this.isError = true}); + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + isError ? S.current.error : S.current.successful, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.normal, + color: Colors.white, + ), + ), + SizedBox(height: 8), + Row( + children: [ + Expanded( + child: Text( + message, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + color: Colors.white, + ), + ), + ), + ], + ), + ], + ); + } +} diff --git a/lib/src/screens/wallet_connect/widgets/modals/bottom_sheet_listener.dart b/lib/src/screens/wallet_connect/widgets/modals/bottom_sheet_listener.dart new file mode 100644 index 000000000..30b6af7e0 --- /dev/null +++ b/lib/src/screens/wallet_connect/widgets/modals/bottom_sheet_listener.dart @@ -0,0 +1,62 @@ +import 'package:cake_wallet/core/wallet_connect/wc_bottom_sheet_service.dart'; +import 'package:flutter/material.dart'; + +import '../../../../../core/wallet_connect/models/bottom_sheet_queue_item_model.dart'; + +class BottomSheetListener extends StatefulWidget { + final BottomSheetService bottomSheetService; + final Widget child; + + const BottomSheetListener({ + required this.child, + required this.bottomSheetService, + super.key, + }); + + @override + BottomSheetListenerState createState() => BottomSheetListenerState(); +} + +class BottomSheetListenerState extends State { + + @override + void initState() { + super.initState(); + widget.bottomSheetService.currentSheet.addListener(_showBottomSheet); + } + + @override + void dispose() { + widget.bottomSheetService.currentSheet.removeListener(_showBottomSheet); + super.dispose(); + } + + Future _showBottomSheet() async { + if (widget.bottomSheetService.currentSheet.value != null) { + BottomSheetQueueItemModel item = widget.bottomSheetService.currentSheet.value!; + final value = await showModalBottomSheet( + context: context, + isDismissible: item.isModalDismissible, + backgroundColor: Color.fromARGB(0, 0, 0, 0), + isScrollControlled: true, + constraints: BoxConstraints(maxHeight: MediaQuery.of(context).size.height * 0.9), + builder: (context) { + return Container( + decoration: const BoxDecoration( + color: Color.fromARGB(255, 18, 18, 19), + borderRadius: BorderRadius.all(Radius.circular(16)), + ), + padding: const EdgeInsets.all(16), + margin: const EdgeInsets.all(16), + child: item.widget, + ); + }, + ); + item.completer.complete(value); + widget.bottomSheetService.resetCurrentSheet(); + } + } + + @override + Widget build(BuildContext context) => widget.child; +} diff --git a/lib/src/screens/wallet_connect/widgets/modals/web3_request_modal.dart b/lib/src/screens/wallet_connect/widgets/modals/web3_request_modal.dart new file mode 100644 index 000000000..f16dcc0f8 --- /dev/null +++ b/lib/src/screens/wallet_connect/widgets/modals/web3_request_modal.dart @@ -0,0 +1,48 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/widgets/primary_button.dart'; +import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; +import 'package:flutter/material.dart'; + +class Web3RequestModal extends StatelessWidget { + const Web3RequestModal({required this.child, this.onAccept, this.onReject, super.key}); + + final Widget child; + final VoidCallback? onAccept; + final VoidCallback? onReject; + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + child, + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + + Expanded( + child: PrimaryButton( + onPressed: onReject ?? () => Navigator.of(context).pop(false), + text: S.current.reject, + color: Theme.of(context).colorScheme.error, + textColor: Theme.of(context).colorScheme.onError, + ), + ), + const SizedBox(width: 16), + Expanded( + child: PrimaryButton( + onPressed: onAccept ?? () => Navigator.of(context).pop(true), + text: S.current.approve, + color: Theme.of(context).primaryColor, + textColor: Theme.of(context).extension()!.titleColor, + ), + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/src/screens/wallet_connect/widgets/pairing_item_widget.dart b/lib/src/screens/wallet_connect/widgets/pairing_item_widget.dart new file mode 100644 index 000000000..063de8ec3 --- /dev/null +++ b/lib/src/screens/wallet_connect/widgets/pairing_item_widget.dart @@ -0,0 +1,82 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; +import 'package:cake_wallet/themes/extensions/receive_page_theme.dart'; +import 'package:flutter/material.dart'; +import 'package:walletconnect_flutter_v2/apis/core/pairing/utils/pairing_models.dart'; + +class PairingItemWidget extends StatelessWidget { + const PairingItemWidget({required this.pairing, required this.onTap, super.key}); + + final PairingInfo pairing; + final void Function() onTap; + + @override + Widget build(BuildContext context) { + PairingMetadata? metadata = pairing.peerMetadata; + if (metadata == null) { + return SizedBox.shrink(); + } + + DateTime dateTime = DateTime.fromMillisecondsSinceEpoch(pairing.expiry * 1000); + int year = dateTime.year; + int month = dateTime.month; + int day = dateTime.day; + + String expiryDate = + '$year-${month.toString().padLeft(2, '0')}-${day.toString().padLeft(2, '0')}'; + + return ListTile( + leading: CircleAvatar( + backgroundImage: (metadata.icons.isNotEmpty + ? NetworkImage(metadata.icons[0]) + : const AssetImage( + 'assets/images/default_icon.png', + )) as ImageProvider, + ), + title: Text( + metadata.name, + style: TextStyle( + fontSize: 16.0, + fontWeight: FontWeight.w700, + color: Theme.of(context).extension()!.titleColor, + ), + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + metadata.url, + style: TextStyle( + fontSize: 14.0, + fontWeight: FontWeight.w700, + color: Theme.of(context).extension()!.secondaryTextColor, + ), + ), + Text( + '${S.current.expiresOn}: $expiryDate', + style: TextStyle( + fontSize: 14.0, + fontWeight: FontWeight.w700, + color: Theme.of(context).extension()!.secondaryTextColor, + ), + ), + ], + ), + trailing: Container( + height: 40, + width: 44, + padding: EdgeInsets.all(10), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Theme.of(context).extension()!.iconsBackgroundColor, + ), + child: Icon( + Icons.edit, + size: 14, + color: Theme.of(context).extension()!.iconsColor, + ), + ), + onTap: onTap, + ); + } +} diff --git a/lib/src/screens/wallet_keys/wallet_keys_page.dart b/lib/src/screens/wallet_keys/wallet_keys_page.dart index 37ed3a692..5117f152f 100644 --- a/lib/src/screens/wallet_keys/wallet_keys_page.dart +++ b/lib/src/screens/wallet_keys/wallet_keys_page.dart @@ -5,10 +5,10 @@ import 'package:cake_wallet/src/widgets/list_row.dart'; import 'package:cake_wallet/src/widgets/section_divider.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/utils/brightness_util.dart'; import 'package:cake_wallet/utils/clipboard_util.dart'; import 'package:cake_wallet/utils/show_bar.dart'; import 'package:cake_wallet/view_model/wallet_keys_view_model.dart'; -import 'package:device_display_brightness/device_display_brightness.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; @@ -26,19 +26,15 @@ class WalletKeysPage extends BasePage { @override Widget trailing(BuildContext context) => IconButton( onPressed: () async { - // Get the current brightness: - final double brightness = await DeviceDisplayBrightness.getBrightness(); final url = await walletKeysViewModel.url; - // ignore: unawaited_futures - DeviceDisplayBrightness.setBrightness(1.0); - await Navigator.pushNamed( - context, - Routes.fullscreenQR, - arguments: QrViewData(data: url.toString(), version: QrVersions.auto), - ); - // ignore: unawaited_futures - DeviceDisplayBrightness.setBrightness(brightness); + BrightnessUtil.changeBrightnessForFunction(() async { + await Navigator.pushNamed( + context, + Routes.fullscreenQR, + arguments: QrViewData(data: url.toString(), version: QrVersions.auto), + ); + }); }, splashColor: Colors.transparent, highlightColor: Colors.transparent, diff --git a/lib/src/screens/wallet_list/wallet_list_page.dart b/lib/src/screens/wallet_list/wallet_list_page.dart index a0b44f375..da5df874a 100644 --- a/lib/src/screens/wallet_list/wallet_list_page.dart +++ b/lib/src/screens/wallet_list/wallet_list_page.dart @@ -13,7 +13,6 @@ import 'package:cw_core/wallet_type.dart'; import 'package:cake_wallet/view_model/wallet_list/wallet_list_view_model.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; -import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; import 'package:cake_wallet/wallet_type_utils.dart'; import 'package:cake_wallet/themes/extensions/wallet_list_theme.dart'; @@ -48,6 +47,7 @@ class WalletListBodyState extends State { final nonWalletTypeIcon = Image.asset('assets/images/close.png', height: 24, width: 24); final havenIcon = Image.asset('assets/images/haven_logo.png', height: 24, width: 24); final ethereumIcon = Image.asset('assets/images/eth_icon.png', height: 24, width: 24); + final bitcoinCashIcon = Image.asset('assets/images/bch_icon.png', height: 24, width: 24); final nanoIcon = Image.asset('assets/images/nano_icon.png', height: 24, width: 24); final scrollController = ScrollController(); final double tileHeight = 60; @@ -64,169 +64,178 @@ class WalletListBodyState extends State { return Container( padding: EdgeInsets.only(top: 16), - child: ScrollableWithBottomSection( - contentPadding: EdgeInsets.only(bottom: 20), - content: Container( - child: Observer( - builder: (_) => ListView.separated( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - separatorBuilder: (_, index) => - Divider(color: Theme.of(context).colorScheme.background, height: 32), - itemCount: widget.walletListViewModel.wallets.length, - 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( - 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) : nonWalletTypeIcon, - SizedBox(width: 10), - Flexible( - child: Text( - wallet.name, - maxLines: null, - softWrap: true, - style: TextStyle( - fontSize: 22, - fontWeight: FontWeight.w500, - color: - Theme.of(context).extension()!.titleColor, - ), - ), - ), - ], + child: Column( + children: [ + Expanded( + child: Container( + child: Observer( + builder: (_) => ListView.separated( + physics: const BouncingScrollPhysics(), + separatorBuilder: (_, index) => + Divider(color: Theme.of(context).colorScheme.background, height: 32), + itemCount: widget.walletListViewModel.wallets.length, + 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( + 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), ), - ), - ), - ], - ), - ), - ); - - return wallet.isCurrent - ? row - : Row( - children: [ - Expanded(child: row), - GestureDetector( - onTap: () => Navigator.of(context).pushNamed(Routes.walletEdit, - arguments: [widget.walletListViewModel, wallet]), - child: Container( - padding: EdgeInsets.only(right: 20), - 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, - ), + 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) + : nonWalletTypeIcon, + SizedBox(width: 10), + Flexible( + child: Text( + wallet.name, + maxLines: null, + softWrap: true, + style: TextStyle( + fontSize: 22, + fontWeight: FontWeight.w500, + color: Theme.of(context) + .extension()! + .titleColor, + ), + ), + ), + ], ), ), ), - ), - ], - ); - }, + ], + ), + ), + ); + + return wallet.isCurrent + ? row + : Row( + children: [ + Expanded(child: row), + GestureDetector( + onTap: () => Navigator.of(context).pushNamed(Routes.walletEdit, + arguments: [widget.walletListViewModel, wallet]), + child: Container( + padding: EdgeInsets.only(right: 20), + 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, + ), + ), + ), + ), + ), + ], + ); + }, + ), + ), ), ), - ), - bottomSectionPadding: EdgeInsets.only(bottom: 24, right: 24, left: 24), - bottomSection: Column( - children: [ - PrimaryImageButton( - onPressed: () { - //TODO(David): Find a way to optimize this - if (isSingleCoin) { - if (widget.walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets) { - widget.authService.authenticateAction( - context, - route: Routes.newWallet, - arguments: widget.walletListViewModel.currentWalletType, - conditionToDetermineIfToUse2FA: - widget.walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets, - ); - } else { - Navigator.of(context).pushNamed( - Routes.newWallet, - arguments: widget.walletListViewModel.currentWalletType, - ); - } - } else { - if (widget.walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets) { - widget.authService.authenticateAction( - context, - route: Routes.newWalletType, - conditionToDetermineIfToUse2FA: - widget.walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets, - ); - } else { - Navigator.of(context).pushNamed(Routes.newWalletType); - } - } - }, - image: newWalletImage, - text: S.of(context).wallet_list_create_new_wallet, - color: Theme.of(context).primaryColor, - textColor: Colors.white, + Padding( + padding: const EdgeInsets.all(24), + child: Column( + children: [ + PrimaryImageButton( + onPressed: () { + //TODO(David): Find a way to optimize this + if (isSingleCoin) { + if (widget.walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets) { + widget.authService.authenticateAction( + context, + route: Routes.newWallet, + arguments: widget.walletListViewModel.currentWalletType, + conditionToDetermineIfToUse2FA: + widget.walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets, + ); + } else { + Navigator.of(context).pushNamed( + Routes.newWallet, + arguments: widget.walletListViewModel.currentWalletType, + ); + } + } else { + if (widget.walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets) { + widget.authService.authenticateAction( + context, + route: Routes.newWalletType, + conditionToDetermineIfToUse2FA: + widget.walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets, + ); + } else { + Navigator.of(context).pushNamed(Routes.newWalletType); + } + } + }, + image: newWalletImage, + text: S.of(context).wallet_list_create_new_wallet, + color: Theme.of(context).primaryColor, + textColor: Colors.white, + ), + SizedBox(height: 10.0), + PrimaryImageButton( + onPressed: () { + if (widget.walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets) { + widget.authService.authenticateAction( + context, + route: Routes.restoreOptions, + arguments: false, + conditionToDetermineIfToUse2FA: + widget.walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets, + ); + } else { + Navigator.of(context).pushNamed(Routes.restoreOptions, arguments: false); + } + }, + image: restoreWalletImage, + text: S.of(context).wallet_list_restore_wallet, + color: Theme.of(context).cardColor, + textColor: Theme.of(context).extension()!.buttonTextColor, + ) + ], ), - SizedBox(height: 10.0), - PrimaryImageButton( - onPressed: () { - if (widget.walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets) { - widget.authService.authenticateAction( - context, - route: Routes.restoreOptions, - arguments: false, - conditionToDetermineIfToUse2FA: - widget.walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets, - ); - } else { - Navigator.of(context).pushNamed(Routes.restoreOptions, arguments: false); - } - }, - image: restoreWalletImage, - text: S.of(context).wallet_list_restore_wallet, - color: Theme.of(context).cardColor, - textColor: Theme.of(context).extension()!.buttonTextColor, - ) - ], - ), + ), + ], ), ); } @@ -243,6 +252,8 @@ class WalletListBodyState extends State { return havenIcon; case WalletType.ethereum: return ethereumIcon; + case WalletType.bitcoinCash: + return bitcoinCashIcon; case WalletType.nano: return nanoIcon; default: @@ -264,7 +275,7 @@ class WalletListBodyState extends State { await hideProgressText(); // only pop the wallets route in mobile as it will go back to dashboard page // in desktop platforms the navigation tree is different - if (ResponsiveLayoutUtil.instance.shouldRenderMobileUI()) { + if (responsiveLayoutUtil.shouldRenderMobileUI) { WidgetsBinding.instance.addPostFrameCallback((_) { Navigator.of(context).pop(); }); diff --git a/lib/src/screens/welcome/welcome_page.dart b/lib/src/screens/welcome/welcome_page.dart index 52de38021..2142fdf9b 100644 --- a/lib/src/screens/welcome/welcome_page.dart +++ b/lib/src/screens/welcome/welcome_page.dart @@ -70,7 +70,7 @@ class WelcomePage extends BasePage { padding: EdgeInsets.only(top: 64, bottom: 24, left: 24, right: 24), child: ConstrainedBox( constraints: BoxConstraints( - maxWidth: ResponsiveLayoutUtil.kDesktopMaxWidthConstraint), + maxWidth: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint), child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ diff --git a/lib/src/widgets/add_template_button.dart b/lib/src/widgets/add_template_button.dart index 667a103ab..a5e9f2e2a 100644 --- a/lib/src/widgets/add_template_button.dart +++ b/lib/src/widgets/add_template_button.dart @@ -27,7 +27,7 @@ class AddTemplateButton extends StatelessWidget { child: Container( height: 34, padding: EdgeInsets.symmetric( - horizontal: ResponsiveLayoutUtil.instance.isMobile ? 10 : 30), + horizontal: responsiveLayoutUtil.shouldRenderMobileUI ? 10 : 30), alignment: Alignment.center, decoration: BoxDecoration( borderRadius: BorderRadius.all(Radius.circular(20)), diff --git a/lib/src/widgets/address_text_field.dart b/lib/src/widgets/address_text_field.dart index 000e6325e..092a70422 100644 --- a/lib/src/widgets/address_text_field.dart +++ b/lib/src/widgets/address_text_field.dart @@ -9,6 +9,8 @@ import 'package:cake_wallet/entities/qr_scanner.dart'; import 'package:cake_wallet/entities/contact_base.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cake_wallet/themes/extensions/send_page_theme.dart'; +import 'package:cake_wallet/utils/permission_handler.dart'; +import 'package:permission_handler/permission_handler.dart'; enum AddressTextFieldOption { paste, qrCode, addressBook } @@ -98,7 +100,7 @@ class AddressTextField extends StatelessWidget { child: SizedBox( width: prefixIconWidth * options.length + (spaceBetweenPrefixIcons * options.length), child: Row( - mainAxisAlignment: ResponsiveLayoutUtil.instance.isMobile + mainAxisAlignment: responsiveLayoutUtil.shouldRenderMobileUI ? MainAxisAlignment.spaceBetween : MainAxisAlignment.end, children: [ @@ -188,6 +190,9 @@ class AddressTextField extends StatelessWidget { } Future _presentQRScanner(BuildContext context) async { + bool isCameraPermissionGranted = + await PermissionHandler.checkPermission(Permission.camera, context); + if (!isCameraPermissionGranted) return; final code = await presentQRScanner(); if (code.isEmpty) { return; diff --git a/lib/src/widgets/alert_background.dart b/lib/src/widgets/alert_background.dart index 0ced8ee06..56f10d1f6 100644 --- a/lib/src/widgets/alert_background.dart +++ b/lib/src/widgets/alert_background.dart @@ -25,7 +25,7 @@ class AlertBackground extends StatelessWidget { Theme.of(context).extension()!.backdropColor), child: Center( child: Container( - width: ResponsiveLayoutUtil.kDesktopMaxWidthConstraint, + width: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint, child: child, ), ), diff --git a/lib/src/widgets/alert_close_button.dart b/lib/src/widgets/alert_close_button.dart index 925756933..e3ff037a9 100644 --- a/lib/src/widgets/alert_close_button.dart +++ b/lib/src/widgets/alert_close_button.dart @@ -1,8 +1,10 @@ +import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/palette.dart'; import 'package:flutter/material.dart'; class AlertCloseButton extends StatelessWidget { AlertCloseButton({this.image, this.bottom, this.onTap}); + final VoidCallback? onTap; final Image? image; @@ -19,12 +21,17 @@ class AlertCloseButton extends StatelessWidget { bottom: bottom ?? 60, child: GestureDetector( onTap: onTap ?? () => Navigator.of(context).pop(), - child: Container( - height: 42, - width: 42, - decoration: BoxDecoration(color: Colors.white, shape: BoxShape.circle), - child: Center( - child: image ?? closeButton, + child: Semantics( + label: S.of(context).close, + button: true, + enabled: true, + child: Container( + height: 42, + width: 42, + decoration: BoxDecoration(color: Colors.white, shape: BoxShape.circle), + child: Center( + child: image ?? closeButton, + ), ), ), ), diff --git a/lib/src/widgets/base_alert_dialog.dart b/lib/src/widgets/base_alert_dialog.dart index 757ee1862..02a1f6ad0 100644 --- a/lib/src/widgets/base_alert_dialog.dart +++ b/lib/src/widgets/base_alert_dialog.dart @@ -3,7 +3,6 @@ import 'dart:ui'; import 'package:cake_wallet/src/widgets/section_divider.dart'; import 'package:cake_wallet/themes/extensions/alert_theme.dart'; import 'package:flutter/material.dart'; -import 'package:cake_wallet/palette.dart'; class BaseAlertDialog extends StatelessWidget { String get titleText => ''; @@ -49,7 +48,7 @@ class BaseAlertDialog extends StatelessWidget { Widget actionButtons(BuildContext context) { return Container( - height: 52, + height: 60, child: Row( mainAxisSize: MainAxisSize.max, crossAxisAlignment: CrossAxisAlignment.stretch, diff --git a/lib/src/widgets/blockchain_height_widget.dart b/lib/src/widgets/blockchain_height_widget.dart index 2ba0a3406..221f87446 100644 --- a/lib/src/widgets/blockchain_height_widget.dart +++ b/lib/src/widgets/blockchain_height_widget.dart @@ -36,16 +36,13 @@ class BlockchainHeightState extends State { restoreHeightController.addListener(() { if (restoreHeightController.text.isNotEmpty) { widget.onHeightOrDateEntered?.call(true); - } - else { + } else { widget.onHeightOrDateEntered?.call(false); dateController.text = ''; } try { - _changeHeight(restoreHeightController.text != null && - restoreHeightController.text.isNotEmpty - ? int.parse(restoreHeightController.text) - : 0); + _changeHeight( + restoreHeightController.text.isNotEmpty ? int.parse(restoreHeightController.text) : 0); } catch (_) { _changeHeight(0); } @@ -117,7 +114,7 @@ class BlockchainHeightState extends State { ); } - Future _selectDate(BuildContext context) async { + Future _selectDate(BuildContext context) async { final now = DateTime.now(); final date = await getDate( context: context, @@ -126,7 +123,7 @@ class BlockchainHeightState extends State { lastDate: now); if (date != null) { - final height = monero!.getHeigthByDate(date: date); + final height = monero!.getHeightByDate(date: date); setState(() { dateController.text = DateFormat('yyyy-MM-dd').format(date); restoreHeightController.text = '$height'; diff --git a/lib/src/widgets/check_box_picker.dart b/lib/src/widgets/check_box_picker.dart index 30f81e981..b4db82628 100644 --- a/lib/src/widgets/check_box_picker.dart +++ b/lib/src/widgets/check_box_picker.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/src/widgets/standard_checkbox.dart'; import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; import 'package:cake_wallet/palette.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; @@ -60,7 +61,7 @@ class CheckBoxPickerState extends State { child: ConstrainedBox( constraints: BoxConstraints( maxHeight: MediaQuery.of(context).size.height * 0.65, - maxWidth: ResponsiveLayoutUtil.kPopupWidth, + maxWidth: ResponsiveLayoutUtilBase.kPopupWidth, ), child: Column( mainAxisSize: MainAxisSize.min, @@ -101,7 +102,7 @@ class CheckBoxPickerState extends State { height: 1, ) : const SizedBox(), - itemCount: items == null || items.isEmpty ? 0 : items.length, + itemCount: items.isEmpty ? 0 : items.length, itemBuilder: (context, index) => buildItem(index), ), ); @@ -112,41 +113,51 @@ class CheckBoxPickerState extends State { return GestureDetector( onTap: () { - Navigator.of(context).pop(); + if (item.isDisabled) { + return; + } + + bool newValue = !item.value; + item.value = newValue; + widget.onChanged(index, newValue); + setState(() {}); }, child: Container( height: 55, color: Theme.of(context).dialogTheme.backgroundColor, padding: EdgeInsets.only(left: 24, right: 24), - child: CheckboxListTile( - value: item.value, - activeColor: item.value - ? Palette.blueCraiola - : Theme.of(context).extension()!.checkboxBackgroundColor, - checkColor: Colors.white, - title: widget.displayItem?.call(item) ?? - Text( - item.title, - style: TextStyle( - fontSize: 14, - fontFamily: 'Lato', - fontWeight: FontWeight.w600, - color: item.isDisabled - ? Colors.grey.withOpacity(0.5) - : Theme.of(context).extension()!.titleColor, - decoration: TextDecoration.none, - ), - ), - onChanged: (bool? value) { - if (value == null) { - return; - } + child: Row( + children: [ + StandardCheckbox( + value: item.value, + gradientBackground: true, + borderColor: Theme.of(context).dividerColor, + iconColor: Colors.white, + onChanged: (bool? value) { + if (value == null || item.isDisabled) { + return; + } - item.value = value; - widget.onChanged(index, value); - setState(() {}); - }, - controlAffinity: ListTileControlAffinity.leading, + item.value = value; + widget.onChanged(index, value); + setState(() {}); + }, + ), + SizedBox(width: 16), + widget.displayItem?.call(item) ?? + Text( + item.title, + style: TextStyle( + fontSize: 14, + fontFamily: 'Lato', + fontWeight: FontWeight.w600, + color: item.isDisabled + ? Colors.grey.withOpacity(0.5) + : Theme.of(context).extension()!.titleColor, + decoration: TextDecoration.none, + ), + ) + ], ), ), ); diff --git a/lib/src/screens/support/widgets/support_tiles.dart b/lib/src/widgets/option_tile.dart similarity index 81% rename from lib/src/screens/support/widgets/support_tiles.dart rename to lib/src/widgets/option_tile.dart index 7107efac6..8b46641fb 100644 --- a/lib/src/screens/support/widgets/support_tiles.dart +++ b/lib/src/widgets/option_tile.dart @@ -1,9 +1,8 @@ -import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; -import 'package:cake_wallet/themes/extensions/transaction_trade_theme.dart'; +import 'package:cake_wallet/themes/extensions/option_tile_theme.dart'; import 'package:flutter/material.dart'; -class SupportTile extends StatelessWidget { - const SupportTile( +class OptionTile extends StatelessWidget { + const OptionTile( {required this.onPressed, required this.image, required this.title, @@ -45,7 +44,7 @@ class SupportTile extends StatelessWidget { style: TextStyle( fontSize: 20, fontWeight: FontWeight.w500, - color: Theme.of(context).extension()!.titleColor, + color: Theme.of(context).extension()!.titleColor, ), ), Padding( @@ -53,9 +52,9 @@ class SupportTile extends StatelessWidget { child: Text( description, style: TextStyle( - fontSize: 16, + fontSize: 14, fontWeight: FontWeight.normal, - color: Theme.of(context).extension()!.detailsTitlesColor, + color: Theme.of(context).extension()!.descriptionColor, ), ), ) diff --git a/lib/src/widgets/picker.dart b/lib/src/widgets/picker.dart index 6b6543d2f..01b869b1b 100644 --- a/lib/src/widgets/picker.dart +++ b/lib/src/widgets/picker.dart @@ -151,7 +151,7 @@ class _PickerState extends State> { child: ConstrainedBox( constraints: BoxConstraints( maxHeight: containerHeight, - maxWidth: ResponsiveLayoutUtil.kPopupWidth, + maxWidth: ResponsiveLayoutUtilBase.kPopupWidth, ), child: Column( mainAxisSize: MainAxisSize.min, diff --git a/lib/src/widgets/picker_inner_wrapper_widget.dart b/lib/src/widgets/picker_inner_wrapper_widget.dart index 3d9a289bc..2dd61e948 100644 --- a/lib/src/widgets/picker_inner_wrapper_widget.dart +++ b/lib/src/widgets/picker_inner_wrapper_widget.dart @@ -52,7 +52,7 @@ class PickerInnerWrapperWidget extends StatelessWidget { itemsHeight != null && itemsHeight! <= containerHeight ? itemsHeight! : containerHeight, - maxWidth: ResponsiveLayoutUtil.kPopupWidth, + maxWidth: ResponsiveLayoutUtilBase.kPopupWidth, ), child: Column( children: children, @@ -77,7 +77,7 @@ class PickerInnerWrapperWidget extends StatelessWidget { child: ConstrainedBox( constraints: BoxConstraints( maxHeight: containerHeight, - maxWidth: ResponsiveLayoutUtil.kPopupWidth, + maxWidth: ResponsiveLayoutUtilBase.kPopupWidth, ), child: Column( children: children, diff --git a/lib/src/widgets/picker_wrapper_widget.dart b/lib/src/widgets/picker_wrapper_widget.dart index 244199936..f69bcd514 100644 --- a/lib/src/widgets/picker_wrapper_widget.dart +++ b/lib/src/widgets/picker_wrapper_widget.dart @@ -44,7 +44,7 @@ class PickerWrapperWidget extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: children, ), - SizedBox(height: ResponsiveLayoutUtil.kPopupSpaceHeight), + SizedBox(height: ResponsiveLayoutUtilBase.kPopupSpaceHeight), AlertCloseButton(bottom: closeButtonBottom), ], ), diff --git a/lib/src/widgets/primary_button.dart b/lib/src/widgets/primary_button.dart index c27169894..5f6b50f8b 100644 --- a/lib/src/widgets/primary_button.dart +++ b/lib/src/widgets/primary_button.dart @@ -26,7 +26,7 @@ class PrimaryButton extends StatelessWidget { @override Widget build(BuildContext context) { final content = ConstrainedBox( - constraints: BoxConstraints(maxWidth: ResponsiveLayoutUtil.kDesktopMaxWidthConstraint), + constraints: BoxConstraints(maxWidth: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint), child: SizedBox( width: double.infinity, height: 52.0, @@ -82,7 +82,7 @@ class LoadingPrimaryButton extends StatelessWidget { @override Widget build(BuildContext context) { return ConstrainedBox( - constraints: BoxConstraints(maxWidth: ResponsiveLayoutUtil.kDesktopMaxWidthConstraint), + constraints: BoxConstraints(maxWidth: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint), child: SizedBox( width: double.infinity, height: 52.0, @@ -138,7 +138,7 @@ class PrimaryIconButton extends StatelessWidget { @override Widget build(BuildContext context) { return ConstrainedBox( - constraints: BoxConstraints(maxWidth: ResponsiveLayoutUtil.kDesktopMaxWidthConstraint), + constraints: BoxConstraints(maxWidth: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint), child: SizedBox( width: double.infinity, height: 52.0, @@ -201,7 +201,7 @@ class PrimaryImageButton extends StatelessWidget { @override Widget build(BuildContext context) { return ConstrainedBox( - constraints: BoxConstraints(maxWidth: ResponsiveLayoutUtil.kDesktopMaxWidthConstraint), + constraints: BoxConstraints(maxWidth: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint), child: SizedBox( width: double.infinity, height: 52.0, diff --git a/lib/src/widgets/seed_widget.dart b/lib/src/widgets/seed_widget.dart index 7015e0acf..bf9a85b32 100644 --- a/lib/src/widgets/seed_widget.dart +++ b/lib/src/widgets/seed_widget.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:cake_wallet/src/widgets/validable_annotated_editable_text.dart'; @@ -75,7 +76,7 @@ class SeedWidgetState extends State { Positioned( top: 10, left: 0, - child: Text('Enter your seed', + child: Text(S.of(context).enter_seed_phrase, style: TextStyle( fontSize: 16.0, color: Theme.of(context).hintColor))), Padding( diff --git a/lib/store/app_store.dart b/lib/store/app_store.dart index 639efacb6..e814ff44b 100644 --- a/lib/store/app_store.dart +++ b/lib/store/app_store.dart @@ -1,5 +1,8 @@ +import 'package:cake_wallet/core/wallet_connect/web3wallet_service.dart'; +import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/utils/exception_handler.dart'; import 'package:cw_core/transaction_info.dart'; +import 'package:cw_core/wallet_type.dart'; import 'package:mobx/mobx.dart'; import 'package:cw_core/balance.dart'; import 'package:cw_core/wallet_base.dart'; @@ -40,5 +43,9 @@ abstract class AppStoreBase with Store { this.wallet?.close(); this.wallet = wallet; this.wallet!.setExceptionHandler(ExceptionHandler.onError); + + if (wallet.type == WalletType.ethereum) { + getIt.get().init(); + } } } diff --git a/lib/store/dashboard/trade_filter_store.dart b/lib/store/dashboard/trade_filter_store.dart index c772a35d6..4e901aa5e 100644 --- a/lib/store/dashboard/trade_filter_store.dart +++ b/lib/store/dashboard/trade_filter_store.dart @@ -1,7 +1,7 @@ -import 'package:cw_core/wallet_base.dart'; -import 'package:mobx/mobx.dart'; import 'package:cake_wallet/exchange/exchange_provider_description.dart'; import 'package:cake_wallet/view_model/dashboard/trade_list_item.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:mobx/mobx.dart'; part'trade_filter_store.g.dart'; @@ -13,7 +13,8 @@ abstract class TradeFilterStoreBase with Store { displaySideShift = true, displayMorphToken = true, displaySimpleSwap = true, - displayTrocador = true; + displayTrocador = true, + displayExolix = true; @observable bool displayXMRTO; @@ -33,8 +34,11 @@ abstract class TradeFilterStoreBase with Store { @observable bool displayTrocador; + @observable + bool displayExolix; + @computed - bool get displayAllTrades => displayChangeNow && displaySideShift && displaySimpleSwap && displayTrocador; + bool get displayAllTrades => displayChangeNow && displaySideShift && displaySimpleSwap && displayTrocador && displayExolix; @action void toggleDisplayExchange(ExchangeProviderDescription provider) { @@ -56,7 +60,10 @@ abstract class TradeFilterStoreBase with Store { break; case ExchangeProviderDescription.trocador: displayTrocador = !displayTrocador; - break; + break; + case ExchangeProviderDescription.exolix: + displayExolix = !displayExolix; + break; case ExchangeProviderDescription.all: if (displayAllTrades) { displayChangeNow = false; @@ -65,6 +72,7 @@ abstract class TradeFilterStoreBase with Store { displayMorphToken = false; displaySimpleSwap = false; displayTrocador = false; + displayExolix = false; } else { displayChangeNow = true; displaySideShift = true; @@ -72,34 +80,38 @@ abstract class TradeFilterStoreBase with Store { displayMorphToken = true; displaySimpleSwap = true; displayTrocador = true; + displayExolix = true; } break; } } List filtered({required List trades, required WalletBase wallet}) { - final _trades = - trades.where((item) => item.trade.walletId == wallet.id).toList(); + final _trades = trades + .where((item) => item.trade.walletId == wallet.id && isTradeInAccount(item, wallet)) + .toList(); final needToFilter = !displayAllTrades; return needToFilter ? _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) - ||(displayTrocador && item.trade.provider == ExchangeProviderDescription.trocador)) - .toList() + .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) || + (displayTrocador && item.trade.provider == ExchangeProviderDescription.trocador) || + (displayExolix && item.trade.provider == ExchangeProviderDescription.exolix)) + .toList() : _trades; } -} \ No newline at end of file + + bool isTradeInAccount(TradeListItem item, WalletBase wallet) => + item.trade.fromWalletAddress == null + ? true + : wallet.walletAddresses.containsAddress(item.trade.fromWalletAddress!); +} diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index 9199b8e6a..1522ea354 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -1,13 +1,17 @@ import 'dart:io'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart'; import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart'; +import 'package:cake_wallet/entities/buy_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/exchange_api_mode.dart'; import 'package:cake_wallet/entities/pin_code_required_duration.dart'; import 'package:cake_wallet/entities/preferences_key.dart'; +import 'package:cake_wallet/entities/seed_phrase_length.dart'; import 'package:cake_wallet/entities/sort_balance_types.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'; @@ -47,6 +51,7 @@ abstract class SettingsStoreBase with Store { required bool initialAppSecure, required bool initialDisableBuy, required bool initialDisableSell, + required BuyProviderType initialDefaultBuyProvider, required FiatApiMode initialFiatMode, required bool initialAllowBiometricalAuthentication, required String initialTotpSecretKey, @@ -67,23 +72,34 @@ abstract class SettingsStoreBase with Store { required this.isBitcoinBuyEnabled, required this.actionlistDisplayMode, required this.pinTimeOutDuration, + required this.seedPhraseLength, required Cake2FAPresetsOptions initialCake2FAPresetOptions, required bool initialShouldRequireTOTP2FAForAccessingWallet, required bool initialShouldRequireTOTP2FAForSendsToContact, required bool initialShouldRequireTOTP2FAForSendsToNonContact, required bool initialShouldRequireTOTP2FAForSendsToInternalWallets, required bool initialShouldRequireTOTP2FAForExchangesToInternalWallets, + required bool initialShouldRequireTOTP2FAForExchangesToExternalWallets, required bool initialShouldRequireTOTP2FAForAddingContacts, required bool initialShouldRequireTOTP2FAForCreatingNewWallets, required bool initialShouldRequireTOTP2FAForAllSecurityAndBackupSettings, required this.sortBalanceBy, required this.pinNativeTokenAtTop, required this.useEtherscan, + required this.defaultNanoRep, + required this.defaultBananoRep, + required this.lookupsTwitter, + required this.lookupsMastodon, + required this.lookupsYatService, + required this.lookupsUnstoppableDomains, + required this.lookupsOpenAlias, + required this.lookupsENS, TransactionPriority? initialBitcoinTransactionPriority, TransactionPriority? initialMoneroTransactionPriority, TransactionPriority? initialHavenTransactionPriority, TransactionPriority? initialLitecoinTransactionPriority, - TransactionPriority? initialEthereumTransactionPriority}) + TransactionPriority? initialEthereumTransactionPriority, + TransactionPriority? initialBitcoinCashTransactionPriority}) : nodes = ObservableMap.of(nodes), powNodes = ObservableMap.of(powNodes), _sharedPreferences = sharedPreferences, @@ -101,6 +117,7 @@ abstract class SettingsStoreBase with Store { isAppSecure = initialAppSecure, disableBuy = initialDisableBuy, disableSell = initialDisableSell, + defaultBuyProvider = initialDefaultBuyProvider, shouldShowMarketPlaceInDashboard = initialShouldShowMarketPlaceInDashboard, exchangeStatus = initialExchangeStatus, currentTheme = initialTheme, @@ -113,6 +130,8 @@ abstract class SettingsStoreBase with Store { initialShouldRequireTOTP2FAForSendsToInternalWallets, shouldRequireTOTP2FAForExchangesToInternalWallets = initialShouldRequireTOTP2FAForExchangesToInternalWallets, + shouldRequireTOTP2FAForExchangesToExternalWallets = + initialShouldRequireTOTP2FAForExchangesToExternalWallets, shouldRequireTOTP2FAForAddingContacts = initialShouldRequireTOTP2FAForAddingContacts, shouldRequireTOTP2FAForCreatingNewWallets = initialShouldRequireTOTP2FAForCreatingNewWallets, @@ -143,6 +162,12 @@ abstract class SettingsStoreBase with Store { priority[WalletType.ethereum] = initialEthereumTransactionPriority; } + if (initialBitcoinCashTransactionPriority != null) { + priority[WalletType.bitcoinCash] = initialBitcoinCashTransactionPriority; + } + + initializeTrocadorProviderStates(); + reaction( (_) => fiatCurrency, (FiatCurrency fiatCurrency) => sharedPreferences.setString( @@ -171,6 +196,9 @@ abstract class SettingsStoreBase with Store { case WalletType.ethereum: key = PreferencesKey.ethereumTransactionPriority; break; + case WalletType.bitcoinCash: + key = PreferencesKey.bitcoinCashTransactionPriority; + break; default: key = null; } @@ -202,6 +230,11 @@ abstract class SettingsStoreBase with Store { (bool disableSell) => sharedPreferences.setBool(PreferencesKey.disableSellKey, disableSell)); + reaction( + (_) => defaultBuyProvider, + (BuyProviderType defaultBuyProvider) => + sharedPreferences.setInt(PreferencesKey.defaultBuyProvider, defaultBuyProvider.index)); + reaction( (_) => autoGenerateSubaddressStatus, (AutoGenerateSubaddressStatus autoGenerateSubaddressStatus) => sharedPreferences.setInt( @@ -254,6 +287,12 @@ abstract class SettingsStoreBase with Store { PreferencesKey.shouldRequireTOTP2FAForExchangesToInternalWallets, requireTOTP2FAForExchangesToInternalWallets)); + reaction( + (_) => shouldRequireTOTP2FAForExchangesToExternalWallets, + (bool requireTOTP2FAForExchangesToExternalWallets) => sharedPreferences.setBool( + PreferencesKey.shouldRequireTOTP2FAForExchangesToExternalWallets, + requireTOTP2FAForExchangesToExternalWallets)); + reaction( (_) => shouldRequireTOTP2FAForAddingContacts, (bool requireTOTP2FAForAddingContacts) => sharedPreferences.setBool( @@ -274,14 +313,13 @@ abstract class SettingsStoreBase with Store { reaction( (_) => useTOTP2FA, (bool use) => sharedPreferences.setBool(PreferencesKey.useTOTP2FA, use)); + reaction((_) => totpSecretKey, + (String totpKey) => sharedPreferences.setString(PreferencesKey.totpSecretKey, totpKey)); reaction( (_) => numberOfFailedTokenTrials, (int failedTokenTrail) => sharedPreferences.setInt(PreferencesKey.failedTotpTokenTrials, failedTokenTrail)); - reaction((_) => totpSecretKey, - (String totpKey) => sharedPreferences.setString(PreferencesKey.totpSecretKey, totpKey)); - reaction( (_) => shouldShowMarketPlaceInDashboard, (bool value) => @@ -295,6 +333,11 @@ abstract class SettingsStoreBase with Store { (String languageCode) => sharedPreferences.setString(PreferencesKey.currentLanguageCode, languageCode)); + reaction( + (_) => seedPhraseLength, + (SeedPhraseLength seedPhraseWordCount) => + sharedPreferences.setInt(PreferencesKey.currentSeedPhraseLength, seedPhraseWordCount.value)); + reaction( (_) => pinTimeOutDuration, (PinCodeRequiredDuration pinCodeInterval) => @@ -337,6 +380,43 @@ abstract class SettingsStoreBase with Store { (bool useEtherscan) => _sharedPreferences.setBool(PreferencesKey.useEtherscan, useEtherscan)); + reaction((_) => defaultNanoRep, + (String nanoRep) => _sharedPreferences.setString(PreferencesKey.defaultNanoRep, nanoRep)); + + reaction( + (_) => defaultBananoRep, + (String bananoRep) => + _sharedPreferences.setString(PreferencesKey.defaultBananoRep, bananoRep)); + reaction( + (_) => lookupsTwitter, + (bool looksUpTwitter) => + _sharedPreferences.setBool(PreferencesKey.lookupsTwitter, looksUpTwitter)); + + reaction( + (_) => lookupsMastodon, + (bool looksUpMastodon) => + _sharedPreferences.setBool(PreferencesKey.lookupsMastodon, looksUpMastodon)); + + reaction( + (_) => lookupsYatService, + (bool looksUpYatService) => + _sharedPreferences.setBool(PreferencesKey.lookupsYatService, looksUpYatService)); + + reaction( + (_) => lookupsUnstoppableDomains, + (bool looksUpUnstoppableDomains) => + _sharedPreferences.setBool(PreferencesKey.lookupsUnstoppableDomains, looksUpUnstoppableDomains)); + + reaction( + (_) => lookupsOpenAlias, + (bool looksUpOpenAlias) => + _sharedPreferences.setBool(PreferencesKey.lookupsOpenAlias, looksUpOpenAlias)); + + reaction( + (_) => lookupsENS, + (bool looksUpENS) => + _sharedPreferences.setBool(PreferencesKey.lookupsENS, looksUpENS)); + this.nodes.observe((change) { if (change.newValue != null && change.key != null) { _saveCurrentNode(change.newValue!, change.key!); @@ -354,6 +434,7 @@ abstract class SettingsStoreBase with Store { static const defaultActionsMode = 11; static const defaultPinCodeTimeOutDuration = PinCodeRequiredDuration.tenminutes; static const defaultAutoGenerateSubaddressStatus = AutoGenerateSubaddressStatus.initialized; + static const defaultSeedPhraseLength = SeedPhraseLength.twelveWords; @observable FiatCurrency fiatCurrency; @@ -388,6 +469,9 @@ abstract class SettingsStoreBase with Store { @observable bool disableSell; + @observable + BuyProviderType defaultBuyProvider; + @observable bool allowBiometricalAuthentication; @@ -406,6 +490,9 @@ abstract class SettingsStoreBase with Store { @observable bool shouldRequireTOTP2FAForExchangesToInternalWallets; + @observable + bool shouldRequireTOTP2FAForExchangesToExternalWallets; + @observable Cake2FAPresetsOptions selectedCake2FAPreset; @@ -419,15 +506,10 @@ abstract class SettingsStoreBase with Store { bool shouldRequireTOTP2FAForAllSecurityAndBackupSettings; @observable - String totpSecretKey; - - @computed - String get totpVersionOneLink { - return 'otpauth://totp/Cake%20Wallet:$deviceName?secret=$totpSecretKey&issuer=Cake%20Wallet&algorithm=SHA512&digits=8&period=30'; - } + bool useTOTP2FA; @observable - bool useTOTP2FA; + String totpSecretKey; @observable int numberOfFailedTokenTrials; @@ -444,6 +526,9 @@ abstract class SettingsStoreBase with Store { @observable PinCodeRequiredDuration pinTimeOutDuration; + @observable + SeedPhraseLength seedPhraseLength; + @computed ThemeData get theme => currentTheme.themeData; @@ -453,6 +538,9 @@ abstract class SettingsStoreBase with Store { @observable ObservableMap priority; + @observable + ObservableMap trocadorProviderStates = ObservableMap(); + @observable SortBalanceBy sortBalanceBy; @@ -462,6 +550,30 @@ abstract class SettingsStoreBase with Store { @observable bool useEtherscan; + @observable + String defaultNanoRep; + + @observable + String defaultBananoRep; + + @observable + bool lookupsTwitter; + + @observable + bool lookupsMastodon; + + @observable + bool lookupsYatService; + + @observable + bool lookupsUnstoppableDomains; + + @observable + bool lookupsOpenAlias; + + @observable + bool lookupsENS; + @observable SyncMode currentSyncMode; @@ -527,6 +639,7 @@ abstract class SettingsStoreBase with Store { TransactionPriority? havenTransactionPriority; TransactionPriority? litecoinTransactionPriority; TransactionPriority? ethereumTransactionPriority; + TransactionPriority? bitcoinCashTransactionPriority; if (sharedPreferences.getInt(PreferencesKey.havenTransactionPriority) != null) { havenTransactionPriority = monero?.deserializeMoneroTransactionPriority( @@ -540,12 +653,17 @@ abstract class SettingsStoreBase with Store { ethereumTransactionPriority = bitcoin?.deserializeLitecoinTransactionPriority( sharedPreferences.getInt(PreferencesKey.ethereumTransactionPriority)!); } + if (sharedPreferences.getInt(PreferencesKey.bitcoinCashTransactionPriority) != null) { + bitcoinCashTransactionPriority = bitcoinCash?.deserializeBitcoinCashTransactionPriority( + sharedPreferences.getInt(PreferencesKey.bitcoinCashTransactionPriority)!); + } moneroTransactionPriority ??= monero?.getDefaultTransactionPriority(); bitcoinTransactionPriority ??= bitcoin?.getMediumTransactionPriority(); havenTransactionPriority ??= monero?.getDefaultTransactionPriority(); litecoinTransactionPriority ??= bitcoin?.getLitecoinTransactionPriorityMedium(); ethereumTransactionPriority ??= ethereum?.getDefaultTransactionPriority(); + bitcoinCashTransactionPriority ??= bitcoinCash?.getDefaultTransactionPriority(); final currentBalanceDisplayMode = BalanceDisplayMode.deserialize( raw: sharedPreferences.getInt(PreferencesKey.currentBalanceDisplayModeKey)!); @@ -555,6 +673,8 @@ abstract class SettingsStoreBase with Store { final isAppSecure = sharedPreferences.getBool(PreferencesKey.isAppSecureKey) ?? false; final disableBuy = sharedPreferences.getBool(PreferencesKey.disableBuyKey) ?? false; final disableSell = sharedPreferences.getBool(PreferencesKey.disableSellKey) ?? false; + final defaultBuyProvider = + BuyProviderType.values[sharedPreferences.getInt(PreferencesKey.defaultBuyProvider) ?? 0]; final currentFiatApiMode = FiatApiMode.deserialize( raw: sharedPreferences.getInt(PreferencesKey.currentFiatApiModeKey) ?? FiatApiMode.enabled.raw); @@ -575,6 +695,9 @@ abstract class SettingsStoreBase with Store { final shouldRequireTOTP2FAForExchangesToInternalWallets = sharedPreferences .getBool(PreferencesKey.shouldRequireTOTP2FAForExchangesToInternalWallets) ?? false; + final shouldRequireTOTP2FAForExchangesToExternalWallets = sharedPreferences + .getBool(PreferencesKey.shouldRequireTOTP2FAForExchangesToExternalWallets) ?? + false; final shouldRequireTOTP2FAForAddingContacts = sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForAddingContacts) ?? false; final shouldRequireTOTP2FAForCreatingNewWallets = @@ -583,8 +706,8 @@ abstract class SettingsStoreBase with Store { final shouldRequireTOTP2FAForAllSecurityAndBackupSettings = sharedPreferences .getBool(PreferencesKey.shouldRequireTOTP2FAForAllSecurityAndBackupSettings) ?? false; - final totpSecretKey = sharedPreferences.getString(PreferencesKey.totpSecretKey) ?? ''; final useTOTP2FA = sharedPreferences.getBool(PreferencesKey.useTOTP2FA) ?? false; + final totpSecretKey = sharedPreferences.getString(PreferencesKey.totpSecretKey) ?? ''; final tokenTrialNumber = sharedPreferences.getInt(PreferencesKey.failedTotpTokenTrials) ?? 0; final shouldShowMarketPlaceInDashboard = sharedPreferences.getBool(PreferencesKey.shouldShowMarketPlaceInDashboard) ?? true; @@ -602,14 +725,26 @@ abstract class SettingsStoreBase with Store { sharedPreferences.getInt(PreferencesKey.displayActionListModeKey) ?? defaultActionsMode)); var pinLength = sharedPreferences.getInt(PreferencesKey.currentPinLength); final timeOutDuration = sharedPreferences.getInt(PreferencesKey.pinTimeOutDuration); + final seedPhraseCount = sharedPreferences.getInt(PreferencesKey.currentSeedPhraseLength); final pinCodeTimeOutDuration = timeOutDuration != null ? PinCodeRequiredDuration.deserialize(raw: timeOutDuration) : defaultPinCodeTimeOutDuration; + final seedPhraseWordCount = seedPhraseCount != null + ? SeedPhraseLength.deserialize(raw: seedPhraseCount) + : defaultSeedPhraseLength; final sortBalanceBy = SortBalanceBy.values[sharedPreferences.getInt(PreferencesKey.sortBalanceBy) ?? 0]; final pinNativeTokenAtTop = sharedPreferences.getBool(PreferencesKey.pinNativeTokenAtTop) ?? true; final useEtherscan = sharedPreferences.getBool(PreferencesKey.useEtherscan) ?? true; + final defaultNanoRep = sharedPreferences.getString(PreferencesKey.defaultNanoRep) ?? ""; + final defaultBananoRep = sharedPreferences.getString(PreferencesKey.defaultBananoRep) ?? ""; + final lookupsTwitter = sharedPreferences.getBool(PreferencesKey.lookupsTwitter) ?? true; + final lookupsMastodon = sharedPreferences.getBool(PreferencesKey.lookupsMastodon) ?? true; + final lookupsYatService = sharedPreferences.getBool(PreferencesKey.lookupsYatService) ?? true; + final lookupsUnstoppableDomains = sharedPreferences.getBool(PreferencesKey.lookupsUnstoppableDomains) ?? true; + final lookupsOpenAlias = sharedPreferences.getBool(PreferencesKey.lookupsOpenAlias) ?? true; + final lookupsENS = sharedPreferences.getBool(PreferencesKey.lookupsENS) ?? true; // If no value if (pinLength == null || pinLength == 0) { @@ -623,6 +758,8 @@ abstract class SettingsStoreBase with Store { sharedPreferences.getInt(PreferencesKey.currentBitcoinElectrumSererIdKey); final litecoinElectrumServerId = sharedPreferences.getInt(PreferencesKey.currentLitecoinElectrumSererIdKey); + final bitcoinCashElectrumServerId = + sharedPreferences.getInt(PreferencesKey.currentBitcoinCashNodeIdKey); final havenNodeId = sharedPreferences.getInt(PreferencesKey.currentHavenNodeIdKey); final ethereumNodeId = sharedPreferences.getInt(PreferencesKey.currentEthereumNodeIdKey); final nanoNodeId = sharedPreferences.getInt(PreferencesKey.currentNanoNodeIdKey); @@ -632,6 +769,7 @@ abstract class SettingsStoreBase with Store { final litecoinElectrumServer = nodeSource.get(litecoinElectrumServerId); final havenNode = nodeSource.get(havenNodeId); final ethereumNode = nodeSource.get(ethereumNodeId); + final bitcoinCashElectrumServer = nodeSource.get(bitcoinCashElectrumServerId); final nanoNode = nodeSource.get(nanoNodeId); final nanoPowNode = powNodeSource.get(nanoPowNodeId); final packageInfo = await PackageInfo.fromPlatform(); @@ -666,9 +804,14 @@ abstract class SettingsStoreBase with Store { nodes[WalletType.ethereum] = ethereumNode; } + if (bitcoinCashElectrumServer != null) { + nodes[WalletType.bitcoinCash] = bitcoinCashElectrumServer; + } + if (nanoNode != null) { nodes[WalletType.nano] = nanoNode; } + if (nanoPowNode != null) { powNodes[WalletType.nano] = nanoPowNode; } @@ -678,57 +821,70 @@ abstract class SettingsStoreBase with Store { }); final savedSyncAll = sharedPreferences.getBool(PreferencesKey.syncAllKey) ?? true; - return SettingsStore( - sharedPreferences: sharedPreferences, - initialShouldShowMarketPlaceInDashboard: shouldShowMarketPlaceInDashboard, - nodes: nodes, - powNodes: powNodes, - appVersion: packageInfo.version, - deviceName: deviceName, - isBitcoinBuyEnabled: isBitcoinBuyEnabled, - initialFiatCurrency: currentFiatCurrency, - initialBalanceDisplayMode: currentBalanceDisplayMode, - initialSaveRecipientAddress: shouldSaveRecipientAddress, - initialAutoGenerateSubaddressStatus: autoGenerateSubaddressStatus, - initialAppSecure: isAppSecure, - initialDisableBuy: disableBuy, - initialDisableSell: disableSell, - initialFiatMode: currentFiatApiMode, - initialAllowBiometricalAuthentication: allowBiometricalAuthentication, - initialCake2FAPresetOptions: selectedCake2FAPreset, - initialTotpSecretKey: totpSecretKey, - initialUseTOTP2FA: useTOTP2FA, - initialFailedTokenTrial: tokenTrialNumber, - initialExchangeStatus: exchangeStatus, - initialTheme: savedTheme, - actionlistDisplayMode: actionListDisplayMode, - initialPinLength: pinLength, - pinTimeOutDuration: pinCodeTimeOutDuration, - initialLanguageCode: savedLanguageCode, - sortBalanceBy: sortBalanceBy, - pinNativeTokenAtTop: pinNativeTokenAtTop, - useEtherscan: useEtherscan, - initialMoneroTransactionPriority: moneroTransactionPriority, - initialBitcoinTransactionPriority: bitcoinTransactionPriority, - initialHavenTransactionPriority: havenTransactionPriority, - initialLitecoinTransactionPriority: litecoinTransactionPriority, - initialShouldRequireTOTP2FAForAccessingWallet: shouldRequireTOTP2FAForAccessingWallet, - initialShouldRequireTOTP2FAForSendsToContact: shouldRequireTOTP2FAForSendsToContact, - initialShouldRequireTOTP2FAForSendsToNonContact: shouldRequireTOTP2FAForSendsToNonContact, - initialShouldRequireTOTP2FAForSendsToInternalWallets: - shouldRequireTOTP2FAForSendsToInternalWallets, - initialShouldRequireTOTP2FAForExchangesToInternalWallets: - shouldRequireTOTP2FAForExchangesToInternalWallets, - initialShouldRequireTOTP2FAForAddingContacts: shouldRequireTOTP2FAForAddingContacts, - initialShouldRequireTOTP2FAForCreatingNewWallets: shouldRequireTOTP2FAForCreatingNewWallets, - initialShouldRequireTOTP2FAForAllSecurityAndBackupSettings: - shouldRequireTOTP2FAForAllSecurityAndBackupSettings, - initialEthereumTransactionPriority: ethereumTransactionPriority, - backgroundTasks: backgroundTasks, - initialSyncMode: savedSyncMode, - initialSyncAll: savedSyncAll, - shouldShowYatPopup: shouldShowYatPopup); - } + return SettingsStore( + sharedPreferences: sharedPreferences, + initialShouldShowMarketPlaceInDashboard: shouldShowMarketPlaceInDashboard, + nodes: nodes, + powNodes: powNodes, + appVersion: packageInfo.version, + deviceName: deviceName, + isBitcoinBuyEnabled: isBitcoinBuyEnabled, + initialFiatCurrency: currentFiatCurrency, + initialBalanceDisplayMode: currentBalanceDisplayMode, + initialSaveRecipientAddress: shouldSaveRecipientAddress, + initialAutoGenerateSubaddressStatus: autoGenerateSubaddressStatus, + initialAppSecure: isAppSecure, + initialDisableBuy: disableBuy, + initialDisableSell: disableSell, + initialDefaultBuyProvider: defaultBuyProvider, + initialFiatMode: currentFiatApiMode, + initialAllowBiometricalAuthentication: allowBiometricalAuthentication, + initialCake2FAPresetOptions: selectedCake2FAPreset, + initialUseTOTP2FA: useTOTP2FA, + initialTotpSecretKey: totpSecretKey, + initialFailedTokenTrial: tokenTrialNumber, + initialExchangeStatus: exchangeStatus, + initialTheme: savedTheme, + actionlistDisplayMode: actionListDisplayMode, + initialPinLength: pinLength, + pinTimeOutDuration: pinCodeTimeOutDuration, + seedPhraseLength: seedPhraseWordCount, + initialLanguageCode: savedLanguageCode, + sortBalanceBy: sortBalanceBy, + pinNativeTokenAtTop: pinNativeTokenAtTop, + useEtherscan: useEtherscan, + defaultNanoRep: defaultNanoRep, + defaultBananoRep: defaultBananoRep, + lookupsTwitter: lookupsTwitter, + lookupsMastodon: lookupsMastodon, + lookupsYatService: lookupsYatService, + lookupsUnstoppableDomains: lookupsUnstoppableDomains, + lookupsOpenAlias: lookupsOpenAlias, + lookupsENS: lookupsENS, + initialMoneroTransactionPriority: moneroTransactionPriority, + initialBitcoinTransactionPriority: bitcoinTransactionPriority, + initialHavenTransactionPriority: havenTransactionPriority, + initialLitecoinTransactionPriority: litecoinTransactionPriority, + initialBitcoinCashTransactionPriority: bitcoinCashTransactionPriority, + initialShouldRequireTOTP2FAForAccessingWallet: shouldRequireTOTP2FAForAccessingWallet, + initialShouldRequireTOTP2FAForSendsToContact: shouldRequireTOTP2FAForSendsToContact, + initialShouldRequireTOTP2FAForSendsToNonContact: shouldRequireTOTP2FAForSendsToNonContact, + initialShouldRequireTOTP2FAForSendsToInternalWallets: + shouldRequireTOTP2FAForSendsToInternalWallets, + initialShouldRequireTOTP2FAForExchangesToInternalWallets: + shouldRequireTOTP2FAForExchangesToInternalWallets, + initialShouldRequireTOTP2FAForExchangesToExternalWallets: + shouldRequireTOTP2FAForExchangesToExternalWallets, + initialShouldRequireTOTP2FAForAddingContacts: shouldRequireTOTP2FAForAddingContacts, + initialShouldRequireTOTP2FAForCreatingNewWallets: shouldRequireTOTP2FAForCreatingNewWallets, + initialShouldRequireTOTP2FAForAllSecurityAndBackupSettings: + shouldRequireTOTP2FAForAllSecurityAndBackupSettings, + initialEthereumTransactionPriority: ethereumTransactionPriority, + backgroundTasks: backgroundTasks, + initialSyncMode: savedSyncMode, + initialSyncAll: savedSyncAll, + shouldShowYatPopup: shouldShowYatPopup); + } Future reload({required Box nodeSource}) async { final sharedPreferences = await getIt.getAsync(); @@ -758,6 +914,11 @@ abstract class SettingsStoreBase with Store { sharedPreferences.getInt(PreferencesKey.ethereumTransactionPriority)!) ?? priority[WalletType.ethereum]!; } + if (sharedPreferences.getInt(PreferencesKey.bitcoinCashTransactionPriority) != null) { + priority[WalletType.bitcoinCash] = bitcoinCash?.deserializeBitcoinCashTransactionPriority( + sharedPreferences.getInt(PreferencesKey.bitcoinCashTransactionPriority)!) ?? + priority[WalletType.bitcoinCash]!; + } final generateSubaddresses = sharedPreferences.getInt(PreferencesKey.autoGenerateSubaddressStatusKey); @@ -771,14 +932,15 @@ abstract class SettingsStoreBase with Store { shouldSaveRecipientAddress = sharedPreferences.getBool(PreferencesKey.shouldSaveRecipientAddressKey) ?? shouldSaveRecipientAddress; - totpSecretKey = sharedPreferences.getString(PreferencesKey.totpSecretKey) ?? totpSecretKey; useTOTP2FA = sharedPreferences.getBool(PreferencesKey.useTOTP2FA) ?? useTOTP2FA; - + totpSecretKey = sharedPreferences.getString(PreferencesKey.totpSecretKey) ?? totpSecretKey; numberOfFailedTokenTrials = sharedPreferences.getInt(PreferencesKey.failedTotpTokenTrials) ?? numberOfFailedTokenTrials; isAppSecure = sharedPreferences.getBool(PreferencesKey.isAppSecureKey) ?? isAppSecure; disableBuy = sharedPreferences.getBool(PreferencesKey.disableBuyKey) ?? disableBuy; disableSell = sharedPreferences.getBool(PreferencesKey.disableSellKey) ?? disableSell; + defaultBuyProvider = + BuyProviderType.values[sharedPreferences.getInt(PreferencesKey.defaultBuyProvider) ?? 0]; allowBiometricalAuthentication = sharedPreferences.getBool(PreferencesKey.allowBiometricalAuthenticationKey) ?? allowBiometricalAuthentication; @@ -797,6 +959,9 @@ abstract class SettingsStoreBase with Store { shouldRequireTOTP2FAForExchangesToInternalWallets = sharedPreferences .getBool(PreferencesKey.shouldRequireTOTP2FAForExchangesToInternalWallets) ?? false; + shouldRequireTOTP2FAForExchangesToExternalWallets = sharedPreferences + .getBool(PreferencesKey.shouldRequireTOTP2FAForExchangesToExternalWallets) ?? + false; shouldRequireTOTP2FAForAddingContacts = sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForAddingContacts) ?? false; shouldRequireTOTP2FAForCreatingNewWallets = @@ -836,12 +1001,22 @@ abstract class SettingsStoreBase with Store { .values[sharedPreferences.getInt(PreferencesKey.sortBalanceBy) ?? sortBalanceBy.index]; pinNativeTokenAtTop = sharedPreferences.getBool(PreferencesKey.pinNativeTokenAtTop) ?? true; useEtherscan = sharedPreferences.getBool(PreferencesKey.useEtherscan) ?? true; + defaultNanoRep = sharedPreferences.getString(PreferencesKey.defaultNanoRep) ?? ""; + defaultBananoRep = sharedPreferences.getString(PreferencesKey.defaultBananoRep) ?? ""; + lookupsTwitter = sharedPreferences.getBool(PreferencesKey.lookupsTwitter) ?? true; + lookupsMastodon = sharedPreferences.getBool(PreferencesKey.lookupsMastodon) ?? true; + lookupsYatService = sharedPreferences.getBool(PreferencesKey.lookupsYatService) ?? true; + lookupsUnstoppableDomains = sharedPreferences.getBool(PreferencesKey.lookupsUnstoppableDomains) ?? true; + lookupsOpenAlias = sharedPreferences.getBool(PreferencesKey.lookupsOpenAlias) ?? true; + lookupsENS = sharedPreferences.getBool(PreferencesKey.lookupsENS) ?? true; final nodeId = sharedPreferences.getInt(PreferencesKey.currentNodeIdKey); final bitcoinElectrumServerId = sharedPreferences.getInt(PreferencesKey.currentBitcoinElectrumSererIdKey); final litecoinElectrumServerId = sharedPreferences.getInt(PreferencesKey.currentLitecoinElectrumSererIdKey); + final bitcoinCashElectrumServerId = + sharedPreferences.getInt(PreferencesKey.currentBitcoinCashNodeIdKey); final havenNodeId = sharedPreferences.getInt(PreferencesKey.currentHavenNodeIdKey); final ethereumNodeId = sharedPreferences.getInt(PreferencesKey.currentEthereumNodeIdKey); final nanoNodeId = sharedPreferences.getInt(PreferencesKey.currentNanoNodeIdKey); @@ -851,6 +1026,7 @@ abstract class SettingsStoreBase with Store { final litecoinElectrumServer = nodeSource.get(litecoinElectrumServerId); final havenNode = nodeSource.get(havenNodeId); final ethereumNode = nodeSource.get(ethereumNodeId); + final bitcoinCashNode = nodeSource.get(bitcoinCashElectrumServerId); final nanoNode = nodeSource.get(nanoNodeId); if (moneroNode != null) { @@ -873,6 +1049,10 @@ abstract class SettingsStoreBase with Store { nodes[WalletType.ethereum] = ethereumNode; } + if (bitcoinCashNode != null) { + nodes[WalletType.bitcoinCash] = bitcoinCashNode; + } + if (nanoNode != null) { nodes[WalletType.nano] = nanoNode; } @@ -897,6 +1077,10 @@ abstract class SettingsStoreBase with Store { case WalletType.ethereum: await _sharedPreferences.setInt(PreferencesKey.currentEthereumNodeIdKey, node.key as int); break; + case WalletType.bitcoinCash: + await _sharedPreferences.setInt( + PreferencesKey.currentBitcoinCashNodeIdKey, node.key as int); + break; case WalletType.nano: await _sharedPreferences.setInt(PreferencesKey.currentNanoNodeIdKey, node.key as int); break; @@ -919,6 +1103,19 @@ abstract class SettingsStoreBase with Store { powNodes[walletType] = node; } + void initializeTrocadorProviderStates() { + for (var provider in TrocadorExchangeProvider.availableProviders) { + final savedState = _sharedPreferences.getBool(provider) ?? true; + trocadorProviderStates[provider] = savedState; + } + } + + void saveTrocadorProviderState(String providerName, bool state) { + _sharedPreferences.setBool(providerName, state); + trocadorProviderStates[providerName] = state; + } + + static Future _getDeviceName() async { String? deviceName = ''; final deviceInfoPlugin = DeviceInfoPlugin(); diff --git a/lib/themes/dark_theme.dart b/lib/themes/dark_theme.dart index 747dc2527..4e18628fa 100644 --- a/lib/themes/dark_theme.dart +++ b/lib/themes/dark_theme.dart @@ -12,6 +12,7 @@ import 'package:cake_wallet/themes/extensions/info_theme.dart'; import 'package:cake_wallet/themes/extensions/keyboard_theme.dart'; import 'package:cake_wallet/themes/extensions/menu_theme.dart'; import 'package:cake_wallet/themes/extensions/new_wallet_theme.dart'; +import 'package:cake_wallet/themes/extensions/option_tile_theme.dart'; import 'package:cake_wallet/themes/extensions/order_theme.dart'; import 'package:cake_wallet/themes/extensions/picker_theme.dart'; import 'package:cake_wallet/themes/extensions/pin_code_theme.dart'; @@ -215,6 +216,10 @@ class DarkTheme extends ThemeBase { qrCodeColor: PaletteDark.lightBlueGrey, qrWidgetCopyButtonColor: PaletteDark.lightBlueGrey); + @override + OptionTileTheme get optionTileTheme => OptionTileTheme( + titleColor: primaryTextColor, descriptionColor: primaryTextColor, useDarkImage: false); + @override ThemeData get themeData => super.themeData.copyWith( dividerColor: PaletteDark.dividerColor, diff --git a/lib/themes/extensions/option_tile_theme.dart b/lib/themes/extensions/option_tile_theme.dart new file mode 100644 index 000000000..6c125a4ac --- /dev/null +++ b/lib/themes/extensions/option_tile_theme.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; + +class OptionTileTheme extends ThemeExtension { + final Color titleColor; + final Color descriptionColor; + final bool useDarkImage; + + OptionTileTheme( + {required this.titleColor, required this.descriptionColor, this.useDarkImage = false}); + + @override + OptionTileTheme copyWith({Color? titleColor, Color? descriptionColor, bool? useDarkImage}) => + OptionTileTheme( + titleColor: titleColor ?? this.titleColor, + descriptionColor: descriptionColor ?? this.descriptionColor, + useDarkImage: useDarkImage ?? this.useDarkImage); + + @override + OptionTileTheme lerp(ThemeExtension? other, double t) { + if (other is! OptionTileTheme) { + return this; + } + + return OptionTileTheme( + titleColor: Color.lerp(titleColor, other.titleColor, t) ?? titleColor, + descriptionColor: + Color.lerp(descriptionColor, other.descriptionColor, t) ?? descriptionColor, + useDarkImage: other.useDarkImage); + } +} diff --git a/lib/themes/high_contrast_theme.dart b/lib/themes/high_contrast_theme.dart index 0483adb38..bef959a70 100644 --- a/lib/themes/high_contrast_theme.dart +++ b/lib/themes/high_contrast_theme.dart @@ -9,6 +9,7 @@ import 'package:cake_wallet/themes/extensions/exchange_page_theme.dart'; import 'package:cake_wallet/themes/extensions/filter_theme.dart'; import 'package:cake_wallet/themes/extensions/indicator_dot_theme.dart'; import 'package:cake_wallet/themes/extensions/menu_theme.dart'; +import 'package:cake_wallet/themes/extensions/option_tile_theme.dart'; import 'package:cake_wallet/themes/extensions/picker_theme.dart'; import 'package:cake_wallet/themes/extensions/receive_page_theme.dart'; import 'package:cake_wallet/themes/extensions/send_page_theme.dart'; @@ -103,6 +104,10 @@ class HighContrastTheme extends MoneroLightTheme { ReceivePageTheme get receivePageTheme => super.receivePageTheme.copyWith( tilesTextColor: Colors.white, iconsBackgroundColor: Colors.grey, iconsColor: Colors.black); + @override + OptionTileTheme get optionTileTheme => OptionTileTheme( + titleColor: Colors.white, descriptionColor: Colors.white, useDarkImage: false); + @override ThemeData get themeData => super.themeData.copyWith( disabledColor: Colors.grey, diff --git a/lib/themes/light_theme.dart b/lib/themes/light_theme.dart index f24551e66..492b2c145 100644 --- a/lib/themes/light_theme.dart +++ b/lib/themes/light_theme.dart @@ -12,6 +12,7 @@ import 'package:cake_wallet/themes/extensions/info_theme.dart'; import 'package:cake_wallet/themes/extensions/keyboard_theme.dart'; import 'package:cake_wallet/themes/extensions/menu_theme.dart'; import 'package:cake_wallet/themes/extensions/new_wallet_theme.dart'; +import 'package:cake_wallet/themes/extensions/option_tile_theme.dart'; import 'package:cake_wallet/themes/extensions/order_theme.dart'; import 'package:cake_wallet/themes/extensions/picker_theme.dart'; import 'package:cake_wallet/themes/extensions/pin_code_theme.dart'; @@ -215,6 +216,10 @@ class LightTheme extends ThemeBase { qrCodeColor: Colors.white, qrWidgetCopyButtonColor: PaletteDark.lightBlueGrey); + @override + OptionTileTheme get optionTileTheme => OptionTileTheme( + titleColor: primaryTextColor, descriptionColor: primaryTextColor, useDarkImage: true); + @override ThemeData get themeData => super.themeData.copyWith( dividerColor: Palette.paleBlue, diff --git a/lib/themes/theme_base.dart b/lib/themes/theme_base.dart index b5f42e7de..3bba6f65f 100644 --- a/lib/themes/theme_base.dart +++ b/lib/themes/theme_base.dart @@ -12,6 +12,7 @@ import 'package:cake_wallet/themes/extensions/info_theme.dart'; import 'package:cake_wallet/themes/extensions/keyboard_theme.dart'; import 'package:cake_wallet/themes/extensions/menu_theme.dart'; import 'package:cake_wallet/themes/extensions/new_wallet_theme.dart'; +import 'package:cake_wallet/themes/extensions/option_tile_theme.dart'; import 'package:cake_wallet/themes/extensions/order_theme.dart'; import 'package:cake_wallet/themes/extensions/picker_theme.dart'; import 'package:cake_wallet/themes/extensions/pin_code_theme.dart'; @@ -114,6 +115,8 @@ abstract class ThemeBase { QRCodeTheme get qrCodeTheme; + OptionTileTheme get optionTileTheme; + ThemeData get themeData => generatedThemeData.copyWith( primaryColor: primaryColor, cardColor: containerColor, @@ -144,6 +147,7 @@ abstract class ThemeBase { accountListTheme, receivePageTheme, qrCodeTheme, + optionTileTheme ], scrollbarTheme: generatedThemeData.scrollbarTheme.copyWith( thumbColor: MaterialStateProperty.all(scrollbarTheme.thumbColor), diff --git a/lib/twitter/twitter_api.dart b/lib/twitter/twitter_api.dart index 41f5df61d..24121c9c0 100644 --- a/lib/twitter/twitter_api.dart +++ b/lib/twitter/twitter_api.dart @@ -1,7 +1,8 @@ import 'dart:convert'; + +import 'package:cake_wallet/.secrets.g.dart' as secrets; import 'package:cake_wallet/twitter/twitter_user.dart'; import 'package:http/http.dart' as http; -import 'package:cake_wallet/.secrets.g.dart' as secrets; class TwitterApi { static const twitterBearerToken = secrets.twitterBearerToken; @@ -10,28 +11,49 @@ class TwitterApi { static const userPath = '/2/users/by/username/'; static Future lookupUserByName({required String userName}) async { - final queryParams = {'user.fields': 'description', 'expansions': 'pinned_tweet_id'}; - + final queryParams = { + 'user.fields': 'description', + 'expansions': 'pinned_tweet_id', + 'tweet.fields': 'note_tweet' + }; final headers = {'authorization': 'Bearer $twitterBearerToken'}; - final uri = Uri( - scheme: httpsScheme, - host: apiHost, - path: userPath + userName, - queryParameters: queryParams, - ); + scheme: httpsScheme, + host: apiHost, + path: userPath + userName, + queryParameters: queryParams); - var response = await http.get(uri, headers: headers); + final response = await http.get(uri, headers: headers).catchError((error) { + throw Exception('HTTP request failed: $error'); + }); if (response.statusCode != 200) { throw Exception('Unexpected http status: ${response.statusCode}'); } - final responseJSON = json.decode(response.body) as Map; + final Map responseJSON = jsonDecode(response.body) as Map; if (responseJSON['errors'] != null) { throw Exception(responseJSON['errors'][0]['detail']); } - return TwitterUser.fromJson(responseJSON); + return TwitterUser.fromJson(responseJSON, _getPinnedTweet(responseJSON)); + } + + static Tweet? _getPinnedTweet(Map responseJSON) { + final tweetId = responseJSON['data']['pinned_tweet_id'] as String?; + if (tweetId == null || responseJSON['includes'] == null) return null; + + final tweetIncludes = List.from(responseJSON['includes']['tweets'] as List); + final pinnedTweetData = tweetIncludes.firstWhere( + (tweet) => tweet['id'] == tweetId, + orElse: () => null, + ) as Map?; + + if (pinnedTweetData == null) return null; + + final pinnedTweetText = + (pinnedTweetData['note_tweet']?['text'] ?? pinnedTweetData['text']) as String; + + return Tweet(id: tweetId, text: pinnedTweetText); } } diff --git a/lib/twitter/twitter_user.dart b/lib/twitter/twitter_user.dart index ac373fd62..c0eb5431c 100644 --- a/lib/twitter/twitter_user.dart +++ b/lib/twitter/twitter_user.dart @@ -4,25 +4,21 @@ class TwitterUser { required this.username, required this.name, required this.description, - this.tweets}); + this.pinnedTweet}); final String id; final String username; final String name; final String description; - final List? tweets; + final Tweet? pinnedTweet; - factory TwitterUser.fromJson(Map json) { + factory TwitterUser.fromJson(Map json, [Tweet? pinnedTweet]) { return TwitterUser( id: json['data']['id'] as String, username: json['data']['username'] as String, name: json['data']['name'] as String, description: json['data']['description'] as String? ?? '', - tweets: json['includes'] != null - ? List.from(json['includes']['tweets'] as List) - .map((e) => Tweet.fromJson(e as Map)) - .toList() - : null, + pinnedTweet: pinnedTweet, ); } } diff --git a/lib/utils/brightness_util.dart b/lib/utils/brightness_util.dart new file mode 100644 index 000000000..5afe065e5 --- /dev/null +++ b/lib/utils/brightness_util.dart @@ -0,0 +1,23 @@ +import 'package:cake_wallet/utils/device_info.dart'; +import 'package:device_display_brightness/device_display_brightness.dart'; + +class BrightnessUtil { + static Future changeBrightnessForFunction(Future Function() func) async { + // if not mobile, just navigate + if (!DeviceInfo.instance.isMobile) { + func(); + 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); + } +} \ No newline at end of file diff --git a/lib/utils/exception_handler.dart b/lib/utils/exception_handler.dart index e8e7702fa..bea43a6c6 100644 --- a/lib/utils/exception_handler.dart +++ b/lib/utils/exception_handler.dart @@ -32,6 +32,14 @@ class ExceptionHandler { const String separator = '''\n\n========================================================== ==========================================================\n\n'''; + /// don't save existing errors + if (file.existsSync()) { + final String fileContent = await file.readAsString(); + if (fileContent.contains("${exception.values.first}")) { + return; + } + } + file.writeAsStringSync( "$exception $separator", mode: FileMode.append, @@ -83,6 +91,10 @@ class ExceptionHandler { library: errorDetails.library, ); + if (errorDetails.silent) { + return; + } + final sharedPrefs = await SharedPreferences.getInstance(); final lastPopupDate = @@ -149,6 +161,7 @@ class ExceptionHandler { "Handshake error in client", "Error while launching http", "OS Error: Network is unreachable", + "ClientException: Write failed, uri=http", ]; static Future _addDeviceInfo(File file) async { diff --git a/lib/utils/feature_flag.dart b/lib/utils/feature_flag.dart index 2b0d5a2b9..628023f85 100644 --- a/lib/utils/feature_flag.dart +++ b/lib/utils/feature_flag.dart @@ -1,3 +1,4 @@ class FeatureFlag { static const bool isCakePayEnabled = false; + static const bool isExolixEnabled = false; } \ No newline at end of file diff --git a/lib/utils/payment_request.dart b/lib/utils/payment_request.dart index b454c11c7..00093b413 100644 --- a/lib/utils/payment_request.dart +++ b/lib/utils/payment_request.dart @@ -1,4 +1,4 @@ -import 'package:cw_nano/nano_util.dart'; +import 'package:cake_wallet/nano/nano.dart'; class PaymentRequest { PaymentRequest(this.address, this.amount, this.note, this.scheme); @@ -16,10 +16,14 @@ class PaymentRequest { scheme = uri.scheme; } - if (address.contains("nano")) { - amount = NanoUtil.getRawAsUsableString(amount, NanoUtil.rawPerNano); - } else if (address.contains("ban")) { - amount = NanoUtil.getRawAsUsableString(amount, NanoUtil.rawPerBanano); + if (nano != null) { + if (amount.isNotEmpty) { + if (address.contains("nano")) { + amount = nanoUtil!.getRawAsUsableString(amount, nanoUtil!.rawPerNano); + } else if (address.contains("ban")) { + amount = nanoUtil!.getRawAsUsableString(amount, nanoUtil!.rawPerBanano); + } + } } return PaymentRequest(address, amount, note, scheme); diff --git a/lib/utils/permission_handler.dart b/lib/utils/permission_handler.dart new file mode 100644 index 000000000..6a6126df2 --- /dev/null +++ b/lib/utils/permission_handler.dart @@ -0,0 +1,39 @@ +import 'dart:io'; + +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/utils/show_bar.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:permission_handler/permission_handler.dart'; + +class PermissionHandler { + static Future checkPermission(Permission permission, BuildContext context) async { + if (Platform.isIOS) { + return true; + } + final Map _permissionMessages = { + Permission.camera: S.of(context).camera_permission_is_required, + }; + + var status = await permission.status; + + if (status.isDenied) { + try { + status = await permission.request(); + } catch (_) {} + } + + if (status.isPermanentlyDenied || status.isDenied) { + String? message = _permissionMessages[permission]; + if (message != null) { + showBar(context, message); + } + return false; + } + + if (status.isGranted) { + return true; + } + + return false; + } +} diff --git a/lib/utils/responsive_layout_util.dart b/lib/utils/responsive_layout_util.dart index 4c06b7a3c..428ab61cc 100644 --- a/lib/utils/responsive_layout_util.dart +++ b/lib/utils/responsive_layout_util.dart @@ -1,46 +1,53 @@ import 'package:flutter/material.dart'; +import 'package:mobx/mobx.dart'; -class ResponsiveLayoutUtil { +part 'responsive_layout_util.g.dart'; + +class _ResponsiveLayoutUtil = ResponsiveLayoutUtilBase with _$_ResponsiveLayoutUtil; + +abstract class ResponsiveLayoutUtilBase with Store, WidgetsBindingObserver { static const double _kMobileThreshold = 550; static const double kDesktopMaxWidthConstraint = 400; static const double kDesktopMaxDashBoardWidthConstraint = 900; static const double kPopupWidth = 400; static const double kPopupSpaceHeight = 100; - const ResponsiveLayoutUtil._(); - - static final instance = ResponsiveLayoutUtil._(); - - bool get isMobile => - MediaQueryData.fromWindow(WidgetsBinding.instance.window).size.shortestSide <= - _kMobileThreshold; - - bool shouldRenderMobileUI() { - final mediaQuery = MediaQueryData.fromWindow(WidgetsBinding.instance.window); - final orientation = mediaQuery.orientation; - final width = mediaQuery.size.width; - final height = mediaQuery.size.height; - if (isMobile || - (orientation == Orientation.portrait && width < height) || - (orientation == Orientation.landscape && width < height)) { - return true; - } else { - return false; - } + ResponsiveLayoutUtilBase() { + WidgetsBinding.instance.addObserver(this); + final initialMediaQuery = MediaQueryData.fromView(WidgetsBinding.instance!.window); + updateDeviceInfo(initialMediaQuery); } - /// Returns dynamic size. - /// - /// If screen size is mobile, it returns 66% ([scale]) of the [originalValue]. - double getDynamicSize( - double originalValue, { - double? mobileSize, - double? scale, - }) { - scale ??= 2 / 3; - mobileSize ??= originalValue * scale; - final value = isMobile ? mobileSize : originalValue; + @override + void didChangeMetrics() { + final mediaQuery = MediaQueryData.fromView(WidgetsBinding.instance!.window); + updateDeviceInfo(mediaQuery); + } - return value.roundToDouble(); + @observable + double screenWidth = 0.0; + + @observable + double screenHeight = 0.0; + + @observable + Orientation orientation = Orientation.portrait; + + @action + void updateDeviceInfo(MediaQueryData mediaQuery) { + orientation = mediaQuery.orientation; + screenWidth = mediaQuery.size.width; + screenHeight = mediaQuery.size.height; + } + + @computed + bool get shouldRenderMobileUI { + return (screenWidth <= _kMobileThreshold) || + (orientation == Orientation.portrait && screenWidth < screenHeight) || + (orientation == Orientation.landscape && screenWidth < screenHeight); } } + +_ResponsiveLayoutUtil _singletonResponsiveLayoutUtil = _ResponsiveLayoutUtil(); + +_ResponsiveLayoutUtil get responsiveLayoutUtil => _singletonResponsiveLayoutUtil; diff --git a/lib/view_model/advanced_privacy_settings_view_model.dart b/lib/view_model/advanced_privacy_settings_view_model.dart index 380937212..b67a4378a 100644 --- a/lib/view_model/advanced_privacy_settings_view_model.dart +++ b/lib/view_model/advanced_privacy_settings_view_model.dart @@ -1,5 +1,6 @@ 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/store/settings_store.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:mobx/mobx.dart'; @@ -25,9 +26,15 @@ abstract class AdvancedPrivacySettingsViewModelBase with Store { final SettingsStore _settingsStore; + bool get hasSeedPhraseLengthOption => + type == WalletType.bitcoinCash || type == WalletType.ethereum; + @computed bool get addCustomNode => _addCustomNode; + @computed + SeedPhraseLength get seedPhraseLength => _settingsStore.seedPhraseLength; + @action void setFiatApiMode(FiatApiMode fiatApiMode) => _settingsStore.fiatApiMode = fiatApiMode; @@ -36,4 +43,7 @@ abstract class AdvancedPrivacySettingsViewModelBase with Store { @action void toggleAddCustomNode() => _addCustomNode = !_addCustomNode; + + @action + void setSeedPhraseLength(SeedPhraseLength length) => _settingsStore.seedPhraseLength = length; } diff --git a/lib/view_model/anon_invoice_page_view_model.dart b/lib/view_model/anon_invoice_page_view_model.dart index b9617e6dd..53e8473a0 100644 --- a/lib/view_model/anon_invoice_page_view_model.dart +++ b/lib/view_model/anon_invoice_page_view_model.dart @@ -93,7 +93,11 @@ abstract class AnonInvoicePageViewModelBase with Store { Future createInvoice() async { state = IsExecutingState(); if (amount.isNotEmpty) { - final amountInCrypto = double.parse(amount); + final amountInCrypto = double.tryParse(amount); + if (amountInCrypto == null) { + state = FailureState('Amount is invalid'); + return; + } if (minimum != null && amountInCrypto < minimum!) { state = FailureState('Amount is too small'); return; diff --git a/lib/view_model/dashboard/balance_view_model.dart b/lib/view_model/dashboard/balance_view_model.dart index b0e60963d..9366985b5 100644 --- a/lib/view_model/dashboard/balance_view_model.dart +++ b/lib/view_model/dashboard/balance_view_model.dart @@ -83,6 +83,9 @@ abstract class BalanceViewModelBase with Store { @computed bool get isHomeScreenSettingsEnabled => wallet.type == WalletType.ethereum; + @computed + bool get hasAccounts => wallet.type == WalletType.monero; + @computed SortBalanceBy get sortBalanceBy => settingsStore.sortBalanceBy; @@ -172,10 +175,8 @@ abstract class BalanceViewModelBase with Store { return '---'; } - return _getFiatBalance( - price: price, - cryptoAmount: getFormattedFrozenBalance(walletBalance)) + ' ' + fiatCurrency.toString(); - + return _getFiatBalance(price: price, cryptoAmount: getFormattedFrozenBalance(walletBalance)) + + ' ${fiatCurrency.toString()}'; } @computed @@ -198,10 +199,8 @@ abstract class BalanceViewModelBase with Store { return '---'; } - return _getFiatBalance( - price: price, - cryptoAmount: walletBalance.formattedAvailableBalance) + ' ' + fiatCurrency.toString(); - + return _getFiatBalance(price: price, cryptoAmount: walletBalance.formattedAvailableBalance) + + ' ${fiatCurrency.toString()}'; } @computed @@ -213,10 +212,8 @@ abstract class BalanceViewModelBase with Store { return '---'; } - return _getFiatBalance( - price: price, - cryptoAmount: walletBalance.formattedAdditionalBalance) + ' ' + fiatCurrency.toString(); - + return _getFiatBalance(price: price, cryptoAmount: walletBalance.formattedAdditionalBalance) + + ' ${fiatCurrency.toString()}'; } @computed @@ -395,6 +392,6 @@ abstract class BalanceViewModelBase with Store { } } - String getFormattedFrozenBalance(Balance walletBalance) => walletBalance.formattedFrozenBalance; + String getFormattedFrozenBalance(Balance walletBalance) => walletBalance.formattedUnAvailableBalance; } diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index 7cae4fade..fbb2fc76f 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -1,36 +1,36 @@ import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart'; -import 'package:cake_wallet/entities/exchange_api_mode.dart'; -import 'package:cake_wallet/nano/nano.dart'; -import 'package:cake_wallet/store/anonpay/anonpay_transactions_store.dart'; -import 'package:cake_wallet/view_model/dashboard/anonpay_transaction_list_item.dart'; -import 'package:cake_wallet/view_model/settings/sync_mode.dart'; -import 'package:cake_wallet/wallet_type_utils.dart'; -import 'package:cw_core/transaction_history.dart'; -import 'package:cw_core/balance.dart'; import 'package:cake_wallet/entities/balance_display_mode.dart'; -import 'package:cw_core/transaction_info.dart'; +import 'package:cake_wallet/entities/buy_provider_types.dart'; +import 'package:cake_wallet/entities/exchange_api_mode.dart'; import 'package:cake_wallet/exchange/exchange_provider_description.dart'; -import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/monero/monero.dart'; +import 'package:cake_wallet/store/anonpay/anonpay_transactions_store.dart'; +import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/store/dashboard/orders_store.dart'; +import 'package:cake_wallet/store/dashboard/trade_filter_store.dart'; +import 'package:cake_wallet/store/dashboard/trades_store.dart'; +import 'package:cake_wallet/store/dashboard/transaction_filter_store.dart'; +import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/store/yat/yat_store.dart'; import 'package:cake_wallet/utils/mobx.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:cake_wallet/view_model/dashboard/balance_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/filter_item.dart'; +import 'package:cake_wallet/view_model/dashboard/formatted_item_list.dart'; import 'package:cake_wallet/view_model/dashboard/order_list_item.dart'; import 'package:cake_wallet/view_model/dashboard/trade_list_item.dart'; import 'package:cake_wallet/view_model/dashboard/transaction_list_item.dart'; -import 'package:cake_wallet/view_model/dashboard/action_list_item.dart'; -import 'package:mobx/mobx.dart'; -import 'package:cw_core/wallet_base.dart'; +import 'package:cake_wallet/view_model/settings/sync_mode.dart'; +import 'package:cake_wallet/wallet_type_utils.dart'; +import 'package:cw_core/balance.dart'; import 'package:cw_core/sync_status.dart'; +import 'package:cw_core/transaction_history.dart'; +import 'package:cw_core/transaction_info.dart'; +import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_type.dart'; -import 'package:cake_wallet/store/app_store.dart'; -import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/store/dashboard/trades_store.dart'; -import 'package:cake_wallet/store/dashboard/trade_filter_store.dart'; -import 'package:cake_wallet/store/dashboard/transaction_filter_store.dart'; -import 'package:cake_wallet/view_model/dashboard/formatted_item_list.dart'; -import 'package:cake_wallet/monero/monero.dart'; +import 'package:mobx/mobx.dart'; part 'dashboard_view_model.g.dart'; @@ -98,6 +98,11 @@ abstract class DashboardViewModelBase with Store { caption: ExchangeProviderDescription.trocador.title, onChanged: () => tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.trocador)), + FilterItem( + value: () => tradeFilterStore.displayExolix, + caption: ExchangeProviderDescription.exolix.title, + onChanged: () => + tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.exolix)), ] }, subname = '', @@ -217,9 +222,8 @@ abstract class DashboardViewModelBase with Store { BalanceDisplayMode get balanceDisplayMode => appStore.settingsStore.balanceDisplayMode; @computed - bool get shouldShowMarketPlaceInDashboard { - return appStore.settingsStore.shouldShowMarketPlaceInDashboard; - } + bool get shouldShowMarketPlaceInDashboard => + appStore.settingsStore.shouldShowMarketPlaceInDashboard; @computed List get trades => @@ -278,6 +282,8 @@ abstract class DashboardViewModelBase with Store { Map> filterItems; + BuyProviderType get defaultBuyProvider => settingsStore.defaultBuyProvider; + bool get isBuyEnabled => settingsStore.isBitcoinBuyEnabled; bool get shouldShowYatPopup => settingsStore.shouldShowYatPopup; diff --git a/lib/view_model/dashboard/trade_list_item.dart b/lib/view_model/dashboard/trade_list_item.dart index d83445308..964ba4ffa 100644 --- a/lib/view_model/dashboard/trade_list_item.dart +++ b/lib/view_model/dashboard/trade_list_item.dart @@ -1,25 +1,18 @@ +import 'package:cake_wallet/entities/balance_display_mode.dart'; import 'package:cake_wallet/exchange/trade.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/view_model/dashboard/action_list_item.dart'; -import 'package:cake_wallet/entities/balance_display_mode.dart'; class TradeListItem extends ActionListItem { - TradeListItem({ - required this.trade, - required this.settingsStore}); + TradeListItem({required this.trade, required this.settingsStore}); final Trade trade; final SettingsStore settingsStore; BalanceDisplayMode get displayMode => settingsStore.balanceDisplayMode; - String get tradeFormattedAmount { - return trade.amount != null - ? displayMode == BalanceDisplayMode.hiddenBalance - ? '---' - : trade.amountFormatted() - : trade.amount; - } + String get tradeFormattedAmount => + displayMode == BalanceDisplayMode.hiddenBalance ? '---' : trade.amountFormatted(); @override DateTime get date => trade.createdAt!; diff --git a/lib/view_model/dashboard/transaction_list_item.dart b/lib/view_model/dashboard/transaction_list_item.dart index 522c31dcc..26985567a 100644 --- a/lib/view_model/dashboard/transaction_list_item.dart +++ b/lib/view_model/dashboard/transaction_list_item.dart @@ -74,6 +74,7 @@ class TransactionListItem extends ActionListItem with Keyable { break; case WalletType.bitcoin: case WalletType.litecoin: + case WalletType.bitcoinCash: amount = calculateFiatAmountRaw( cryptoAmount: bitcoin!.formatterBitcoinAmountToDouble(amount: transaction.amount), price: price); @@ -93,18 +94,9 @@ class TransactionListItem extends ActionListItem with Keyable { price: price); break; case WalletType.nano: - final nanoTransaction = transaction as NanoTransactionInfo; amount = calculateFiatAmountRaw( - cryptoAmount: - NanoUtil.getRawAsDecimal(nanoTransaction.amountRaw.toString(), NanoUtil.rawPerNano) - .toDouble(), - price: price); - break; - case WalletType.ethereum: - final asset = ethereum!.assetOfTransaction(balanceViewModel.wallet, transaction); - final price = balanceViewModel.fiatConvertationStore.prices[asset]; - amount = calculateFiatAmountRaw( - cryptoAmount: ethereum!.formatterEthereumAmountToDouble(transaction: transaction), + cryptoAmount: double.parse(nanoUtil!.getRawAsDecimalString( + nano!.getTransactionAmountRaw(transaction).toString(), nanoUtil!.rawPerNano)), price: price); break; default: diff --git a/lib/view_model/exchange/exchange_trade_view_model.dart b/lib/view_model/exchange/exchange_trade_view_model.dart index cfabd994f..bc7f53af0 100644 --- a/lib/view_model/exchange/exchange_trade_view_model.dart +++ b/lib/view_model/exchange/exchange_trade_view_model.dart @@ -1,21 +1,21 @@ import 'dart:async'; -import 'package:cake_wallet/exchange/sideshift/sideshift_exchange_provider.dart'; -import 'package:cake_wallet/exchange/simpleswap/simpleswap_exchange_provider.dart'; -import 'package:cake_wallet/exchange/trocador/trocador_exchange_provider.dart'; -import 'package:cw_core/wallet_base.dart'; -import 'package:cw_core/crypto_currency.dart'; -import 'package:cake_wallet/exchange/changenow/changenow_exchange_provider.dart'; -import 'package:cake_wallet/exchange/exchange_provider.dart'; + import 'package:cake_wallet/exchange/exchange_provider_description.dart'; -import 'package:cake_wallet/exchange/morphtoken/morphtoken_exchange_provider.dart'; +import 'package:cake_wallet/exchange/provider/changenow_exchange_provider.dart'; +import 'package:cake_wallet/exchange/provider/exchange_provider.dart'; +import 'package:cake_wallet/exchange/provider/exolix_exchange_provider.dart'; +import 'package:cake_wallet/exchange/provider/sideshift_exchange_provider.dart'; +import 'package:cake_wallet/exchange/provider/simpleswap_exchange_provider.dart'; +import 'package:cake_wallet/exchange/provider/trocador_exchange_provider.dart'; import 'package:cake_wallet/exchange/trade.dart'; -import 'package:cake_wallet/exchange/xmrto/xmrto_exchange_provider.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/screens/exchange_trade/exchange_trade_item.dart'; import 'package:cake_wallet/store/dashboard/trades_store.dart'; import 'package:cake_wallet/view_model/send/send_view_model.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/wallet_base.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; -import 'package:cake_wallet/src/screens/exchange_trade/exchange_trade_item.dart'; -import 'package:cake_wallet/generated/i18n.dart'; part 'exchange_trade_view_model.g.dart'; @@ -34,16 +34,10 @@ abstract class ExchangeTradeViewModelBase with Store { tradesStore.trade!.from.tag == CryptoCurrency.eth.title), items = ObservableList() { switch (trade.provider) { - case ExchangeProviderDescription.xmrto: - _provider = XMRTOExchangeProvider(); - break; case ExchangeProviderDescription.changeNow: _provider = ChangeNowExchangeProvider(settingsStore: sendViewModel.balanceViewModel.settingsStore); break; - case ExchangeProviderDescription.morphToken: - _provider = MorphTokenExchangeProvider(trades: trades); - break; case ExchangeProviderDescription.sideShift: _provider = SideShiftExchangeProvider(); break; @@ -53,11 +47,17 @@ abstract class ExchangeTradeViewModelBase with Store { case ExchangeProviderDescription.trocador: _provider = TrocadorExchangeProvider(); break; + case ExchangeProviderDescription.exolix: + _provider = ExolixExchangeProvider(); + break; } _updateItems(); - _updateTrade(); - timer = Timer.periodic(Duration(seconds: 20), (_) async => _updateTrade()); + + if (_provider != null) { + _updateTrade(); + timer = Timer.periodic(Duration(seconds: 20), (_) async => _updateTrade()); + } } final WalletBase wallet; @@ -96,10 +96,8 @@ abstract class ExchangeTradeViewModelBase with Store { Timer? timer; @action - Future confirmSending() async { - if (!isSendable) { - return; - } + Future confirmSending() async { + if (!isSendable) return; sendViewModel.clearOutputs(); final output = sendViewModel.outputs.first; @@ -114,13 +112,10 @@ abstract class ExchangeTradeViewModelBase with Store { try { final updatedTrade = await _provider!.findTradeById(id: trade.id); - if (updatedTrade.createdAt == null && trade.createdAt != null) { + if (updatedTrade.createdAt == null && trade.createdAt != null) updatedTrade.createdAt = trade.createdAt; - } - if (updatedTrade.amount.isEmpty) { - updatedTrade.amount = trade.amount; - } + if (updatedTrade.amount.isEmpty) updatedTrade.amount = trade.amount; trade = updatedTrade; diff --git a/lib/view_model/exchange/exchange_view_model.dart b/lib/view_model/exchange/exchange_view_model.dart index 4603b2007..80c331ab2 100644 --- a/lib/view_model/exchange/exchange_view_model.dart +++ b/lib/view_model/exchange/exchange_view_model.dart @@ -2,44 +2,40 @@ import 'dart:async'; import 'dart:collection'; import 'dart:convert'; +import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart'; +import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/core/wallet_change_listener_view_model.dart'; import 'package:cake_wallet/entities/exchange_api_mode.dart'; import 'package:cake_wallet/entities/preferences_key.dart'; import 'package:cake_wallet/entities/wallet_contact.dart'; -import 'package:cake_wallet/exchange/sideshift/sideshift_exchange_provider.dart'; -import 'package:cake_wallet/exchange/sideshift/sideshift_request.dart'; -import 'package:cake_wallet/exchange/simpleswap/simpleswap_exchange_provider.dart'; -import 'package:cake_wallet/exchange/simpleswap/simpleswap_request.dart'; -import 'package:cake_wallet/exchange/trocador/trocador_exchange_provider.dart'; -import 'package:cake_wallet/exchange/trocador/trocador_request.dart'; -import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart'; -import 'package:cw_core/transaction_priority.dart'; -import 'package:cake_wallet/store/app_store.dart'; -import 'package:cw_core/crypto_currency.dart'; -import 'package:cw_core/sync_status.dart'; -import 'package:cw_core/wallet_type.dart'; -import 'package:cake_wallet/bitcoin/bitcoin.dart'; -import 'package:cake_wallet/monero/monero.dart'; -import 'package:cake_wallet/exchange/exchange_provider.dart'; +import 'package:cake_wallet/ethereum/ethereum.dart'; +import 'package:cake_wallet/exchange/exchange_template.dart'; +import 'package:cake_wallet/exchange/exchange_trade_state.dart'; import 'package:cake_wallet/exchange/limits.dart'; -import 'package:cake_wallet/exchange/trade.dart'; import 'package:cake_wallet/exchange/limits_state.dart'; +import 'package:cake_wallet/exchange/provider/changenow_exchange_provider.dart'; +import 'package:cake_wallet/exchange/provider/exchange_provider.dart'; +import 'package:cake_wallet/exchange/provider/exolix_exchange_provider.dart'; +import 'package:cake_wallet/exchange/provider/sideshift_exchange_provider.dart'; +import 'package:cake_wallet/exchange/provider/simpleswap_exchange_provider.dart'; +import 'package:cake_wallet/exchange/provider/trocador_exchange_provider.dart'; +import 'package:cake_wallet/exchange/trade.dart'; +import 'package:cake_wallet/exchange/trade_request.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/monero/monero.dart'; +import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/store/dashboard/trades_store.dart'; import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cake_wallet/store/templates/exchange_template_store.dart'; +import 'package:cake_wallet/utils/feature_flag.dart'; +import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/sync_status.dart'; +import 'package:cw_core/transaction_priority.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:hive/hive.dart'; import 'package:intl/intl.dart'; import 'package:mobx/mobx.dart'; -import 'package:cake_wallet/generated/i18n.dart'; -import 'package:hive/hive.dart'; -import 'package:cake_wallet/exchange/exchange_trade_state.dart'; -import 'package:cake_wallet/exchange/changenow/changenow_exchange_provider.dart'; -import 'package:cake_wallet/exchange/changenow/changenow_request.dart'; -import 'package:cake_wallet/exchange/trade_request.dart'; -import 'package:cake_wallet/exchange/xmrto/xmrto_exchange_provider.dart'; -import 'package:cake_wallet/exchange/xmrto/xmrto_trade_request.dart'; -import 'package:cake_wallet/exchange/morphtoken/morphtoken_exchange_provider.dart'; -import 'package:cake_wallet/exchange/morphtoken/morphtoken_request.dart'; -import 'package:cake_wallet/store/templates/exchange_template_store.dart'; -import 'package:cake_wallet/exchange/exchange_template.dart'; import 'package:shared_preferences/shared_preferences.dart'; part 'exchange_view_model.g.dart'; @@ -84,13 +80,12 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with super(appStore: appStore) { _useTorOnly = _settingsStore.exchangeStatus == ExchangeApiMode.torOnly; _setProviders(); - const excludeDepositCurrencies = [CryptoCurrency.btt, CryptoCurrency.nano]; + const excludeDepositCurrencies = [CryptoCurrency.btt]; const excludeReceiveCurrencies = [ CryptoCurrency.xlm, CryptoCurrency.xrp, CryptoCurrency.bnb, - CryptoCurrency.btt, - CryptoCurrency.nano + CryptoCurrency.btt ]; _initialPairBasedOnWallet(); @@ -140,6 +135,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with _calculateBestRate(); }); } + bool _useTorOnly; final Box trades; final ExchangeTemplateStore _exchangeTemplateStore; @@ -150,7 +146,9 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with ChangeNowExchangeProvider(settingsStore: _settingsStore), SideShiftExchangeProvider(), SimpleSwapExchangeProvider(), - TrocadorExchangeProvider(useTorOnly: _useTorOnly), + TrocadorExchangeProvider(useTorOnly: _useTorOnly, + providerStates: _settingsStore.trocadorProviderStates), + if (FeatureFlag.isExolixEnabled) ExolixExchangeProvider(), ]; @observable @@ -225,7 +223,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with @computed List get walletContactsToShow => contactListViewModel.walletContacts - .where((element) => receiveCurrency == null || element.type == receiveCurrency) + .where((element) => element.type == receiveCurrency) .toList(); @action @@ -240,15 +238,21 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with bool get shouldDisplayTOTP2FAForExchangesToInternalWallet => _settingsStore.shouldRequireTOTP2FAForExchangesToInternalWallets; + @computed + bool get shouldDisplayTOTP2FAForExchangesToExternalWallet => + _settingsStore.shouldRequireTOTP2FAForExchangesToExternalWallets; + //* Still open to further optimize these checks //* It works but can be made better @action bool shouldDisplayTOTP() { final isInternalWallet = checkIfWalletIsAnInternalWallet(receiveAddress); + if (isInternalWallet) { return shouldDisplayTOTP2FAForExchangesToInternalWallet; + } else { + return shouldDisplayTOTP2FAForExchangesToExternalWallet; } - return false; } @computed @@ -263,7 +267,9 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with } bool get hasAllAmount => - (wallet.type == WalletType.bitcoin || wallet.type == WalletType.litecoin) && + (wallet.type == WalletType.bitcoin || + wallet.type == WalletType.litecoin || + wallet.type == WalletType.bitcoinCash) && depositCurrency == wallet.currency; bool get isMoneroWallet => wallet.type == WalletType.monero; @@ -277,6 +283,10 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with return transactionPriority == bitcoin!.getBitcoinTransactionPrioritySlow(); case WalletType.litecoin: return transactionPriority == bitcoin!.getLitecoinTransactionPrioritySlow(); + case WalletType.ethereum: + return transactionPriority == ethereum!.getEthereumTransactionPrioritySlow(); + case WalletType.bitcoinCash: + return transactionPriority == bitcoinCash!.getBitcoinCashTransactionPrioritySlow(); default: return false; } @@ -370,13 +380,9 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with double minLimit = limits.min ?? 0; double? maxLimit = limits.max; - if (_enteredAmount < minLimit) { - return false; - } + if (_enteredAmount < minLimit) return false; - if (maxLimit != null && _enteredAmount > maxLimit) { - return false; - } + if (maxLimit != null && _enteredAmount > maxLimit) return false; return true; } @@ -408,16 +414,12 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with } } } - if (_sortedAvailableProviders.isNotEmpty) { - _bestRate = _sortedAvailableProviders.keys.first; - } + if (_sortedAvailableProviders.isNotEmpty) _bestRate = _sortedAvailableProviders.keys.first; } @action Future loadLimits() async { - if (selectedProviders.isEmpty) { - return; - } + if (selectedProviders.isEmpty) return; limitsState = LimitsIsLoading(); @@ -430,20 +432,16 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with try { for (var provider in selectedProviders) { /// if this provider is not valid for the current pair, skip it - if (!providersForCurrentPair().contains(provider)) { - continue; - } + if (!providersForCurrentPair().contains(provider)) continue; try { final tempLimits = await provider.fetchLimits(from: from, to: to, isFixedRateMode: isFixedRateMode); - if (lowestMin != null && (tempLimits.min ?? -1) < lowestMin) { - lowestMin = tempLimits.min; - } - if (highestMax != null && (tempLimits.max ?? double.maxFinite) > highestMax) { + if (lowestMin != null && (tempLimits.min ?? -1) < lowestMin) lowestMin = tempLimits.min; + + if (highestMax != null && (tempLimits.max ?? double.maxFinite) > highestMax) highestMax = tempLimits.max; - } } catch (e) { continue; } @@ -468,98 +466,36 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with @action Future createTrade() async { - TradeRequest? request; - String amount = ''; - try { for (var provider in _sortedAvailableProviders.values) { - if (!(await provider.checkIsAvailable())) { - continue; - } + if (!(await provider.checkIsAvailable())) continue; - if (provider is SideShiftExchangeProvider) { - request = SideShiftRequest( - depositMethod: depositCurrency, - settleMethod: receiveCurrency, - depositAmount: isFixedRateMode - ? receiveAmount.replaceAll(',', '.') - : depositAmount.replaceAll(',', '.'), - settleAddress: receiveAddress, + final request = TradeRequest( + fromCurrency: depositCurrency, + toCurrency: receiveCurrency, + fromAmount: depositAmount.replaceAll(',', '.'), + toAmount: receiveAmount.replaceAll(',', '.'), refundAddress: depositAddress, - ); - amount = isFixedRateMode ? receiveAmount : depositAmount; - } - - if (provider is SimpleSwapExchangeProvider) { - request = SimpleSwapRequest( - from: depositCurrency, - to: receiveCurrency, - amount: depositAmount.replaceAll(',', '.'), - address: receiveAddress, - refundAddress: depositAddress, - ); - amount = isFixedRateMode ? receiveAmount : depositAmount; - } - - if (provider is XMRTOExchangeProvider) { - request = XMRTOTradeRequest( - from: depositCurrency, - to: receiveCurrency, - amount: depositAmount.replaceAll(',', '.'), - receiveAmount: receiveAmount.replaceAll(',', '.'), - address: receiveAddress, - refundAddress: depositAddress, - isBTCRequest: isReceiveAmountEntered); - amount = isFixedRateMode ? receiveAmount : depositAmount; - } - - if (provider is ChangeNowExchangeProvider) { - request = ChangeNowRequest( - from: depositCurrency, - to: receiveCurrency, - fromAmount: depositAmount.replaceAll(',', '.'), - toAmount: receiveAmount.replaceAll(',', '.'), - refundAddress: depositAddress, - address: receiveAddress, - isReverse: isFixedRateMode); - amount = isFixedRateMode ? receiveAmount : depositAmount; - } - - if (provider is MorphTokenExchangeProvider) { - request = MorphTokenRequest( - from: depositCurrency, - to: receiveCurrency, - amount: depositAmount.replaceAll(',', '.'), - refundAddress: depositAddress, - address: receiveAddress); - amount = isFixedRateMode ? receiveAmount : depositAmount; - } - - if (provider is TrocadorExchangeProvider) { - request = TrocadorRequest( - from: depositCurrency, - to: receiveCurrency, - fromAmount: depositAmount.replaceAll(',', '.'), - toAmount: receiveAmount.replaceAll(',', '.'), - refundAddress: depositAddress, - address: receiveAddress, - isReverse: isFixedRateMode); - amount = isFixedRateMode ? receiveAmount : depositAmount; - } + toAddress: receiveAddress, + isFixedRate: isFixedRateMode); + var amount = isFixedRateMode ? receiveAmount : depositAmount; amount = amount.replaceAll(',', '.'); if (limitsState is LimitsLoadedSuccessfully) { - if (limits.max != null && double.parse(amount) < limits.min!) { + if (double.tryParse(amount) == null) continue; + + if (limits.max != null && double.parse(amount) < limits.min!) continue; - } else if (limits.max != null && double.parse(amount) > limits.max!) { + else if (limits.max != null && double.parse(amount) > limits.max!) continue; - } else { + else { try { tradeState = TradeIsCreating(); final trade = - await provider.createTrade(request: request!, isFixedRateMode: isFixedRateMode); + await provider.createTrade(request: request, isFixedRateMode: isFixedRateMode); trade.walletId = wallet.id; + trade.fromWalletAddress = wallet.walletAddresses.address; tradesStore.setTrade(trade); await trades.add(trade); tradeState = TradeIsCreatedSuccessfully(trade: trade); @@ -603,14 +539,14 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with @action void calculateDepositAllAmount() { - if (wallet.type == WalletType.bitcoin || wallet.type == WalletType.litecoin) { + if (wallet.type == WalletType.bitcoin || + wallet.type == WalletType.litecoin || + wallet.type == WalletType.bitcoinCash) { final availableBalance = wallet.balance[wallet.currency]!.available; final priority = _settingsStore.priority[wallet.type]!; final fee = wallet.calculateEstimatedFee(priority, null); - if (availableBalance < fee || availableBalance == 0) { - return; - } + if (availableBalance < fee || availableBalance == 0) return; final amount = availableBalance - fee; changeDepositAmount(amount: bitcoin!.formatterBitcoinAmountToString(amount: amount)); @@ -641,19 +577,15 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with void removeTemplate({required ExchangeTemplate template}) => _exchangeTemplateStore.remove(template: template); - List providersForCurrentPair() { - return _providersForPair(from: depositCurrency, to: receiveCurrency); - } + List providersForCurrentPair() => + _providersForPair(from: depositCurrency, to: receiveCurrency); List _providersForPair( - {required CryptoCurrency from, required CryptoCurrency to}) { - final providers = providerList - .where((provider) => - provider.pairList.where((pair) => pair.from == from && pair.to == to).isNotEmpty) - .toList(); - - return providers; - } + {required CryptoCurrency from, required CryptoCurrency to}) => + providerList + .where((provider) => + provider.pairList.where((pair) => pair.from == from && pair.to == to).isNotEmpty) + .toList(); void _onPairChange() { depositAmount = ''; @@ -678,6 +610,10 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with depositCurrency = CryptoCurrency.ltc; receiveCurrency = CryptoCurrency.xmr; break; + case WalletType.bitcoinCash: + depositCurrency = CryptoCurrency.bch; + receiveCurrency = CryptoCurrency.xmr; + break; case WalletType.haven: depositCurrency = CryptoCurrency.xhv; receiveCurrency = CryptoCurrency.btc; @@ -712,9 +648,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with @action void addExchangeProvider(ExchangeProvider provider) { selectedProviders.add(provider); - if (providersForCurrentPair().contains(provider)) { - _tradeAvailableProviders.add(provider); - } + if (providersForCurrentPair().contains(provider)) _tradeAvailableProviders.add(provider); } @action @@ -773,20 +707,25 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with case WalletType.litecoin: _settingsStore.priority[wallet.type] = bitcoin!.getLitecoinTransactionPriorityMedium(); break; + case WalletType.ethereum: + _settingsStore.priority[wallet.type] = ethereum!.getDefaultTransactionPriority(); + break; + case WalletType.bitcoinCash: + _settingsStore.priority[wallet.type] = bitcoinCash!.getDefaultTransactionPriority(); + break; default: break; } } void _setProviders() { - if (_settingsStore.exchangeStatus == ExchangeApiMode.torOnly) { + if (_settingsStore.exchangeStatus == ExchangeApiMode.torOnly) providerList = _allProviders.where((provider) => provider.supportsOnionAddress).toList(); - } else { + else providerList = _allProviders; - } } - int get depositMaxDigits => depositCurrency == CryptoCurrency.btc ? 8 : 12; + int get depositMaxDigits => depositCurrency.decimals; - int get receiveMaxDigits => receiveCurrency == CryptoCurrency.btc ? 8 : 12; + int get receiveMaxDigits => receiveCurrency.decimals; } diff --git a/lib/view_model/nano_account_list/nano_account_edit_or_create_view_model.dart b/lib/view_model/nano_account_list/nano_account_edit_or_create_view_model.dart index 14b7b7a56..0f2f5251d 100644 --- a/lib/view_model/nano_account_list/nano_account_edit_or_create_view_model.dart +++ b/lib/view_model/nano_account_list/nano_account_edit_or_create_view_model.dart @@ -2,12 +2,9 @@ import 'package:cake_wallet/nano/nano.dart'; import 'package:cw_core/nano_account.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_type.dart'; -// import 'package:cw_nano/nano_account_list.dart'; -import 'package:flutter/foundation.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/core/execution_state.dart'; -import 'package:cake_wallet/view_model/monero_account_list/account_list_item.dart'; part 'nano_account_edit_or_create_view_model.g.dart'; @@ -16,9 +13,7 @@ class NanoAccountEditOrCreateViewModel = NanoAccountEditOrCreateViewModelBase abstract class NanoAccountEditOrCreateViewModelBase with Store { NanoAccountEditOrCreateViewModelBase(this._nanoAccountList, - /*this._bananoAccountList,*/ - {required WalletBase wallet, - NanoAccount? accountListItem}) + {required WalletBase wallet, NanoAccount? accountListItem}) : state = InitialExecutionState(), isEdit = accountListItem != null, label = accountListItem?.label ?? '', @@ -34,7 +29,6 @@ abstract class NanoAccountEditOrCreateViewModelBase with Store { String label; final NanoAccountList _nanoAccountList; - // final BananoAccountList? _bananoAccountList; final NanoAccount? _accountListItem; final WalletBase _wallet; @@ -42,10 +36,6 @@ abstract class NanoAccountEditOrCreateViewModelBase with Store { if (_wallet.type == WalletType.nano) { await saveNano(); } - - // if (_wallet.type == WalletType.banano) { - // await saveBanano(); - // } } Future saveNano() async { @@ -53,7 +43,8 @@ abstract class NanoAccountEditOrCreateViewModelBase with Store { state = IsExecutingState(); if (_accountListItem != null) { - await _nanoAccountList.setLabelAccount(_wallet, accountIndex: _accountListItem!.id, label: label); + await _nanoAccountList.setLabelAccount(_wallet, + accountIndex: _accountListItem!.id, label: label); } else { await _nanoAccountList.addAccount(_wallet, label: label); } @@ -64,26 +55,4 @@ abstract class NanoAccountEditOrCreateViewModelBase with Store { state = FailureState(e.toString()); } } - -// Future saveBanano() async { -// if (!(_wallet.type == WalletType.banano)) { -// return; -// } - -// try { -// state = IsExecutingState(); - -// if (_accountListItem != null) { -// await _bananoAccountList! -// .setLabelAccount(_wallet, accountIndex: _accountListItem!.id, label: label); -// } else { -// await _bananoAccountList!.addAccount(_wallet, label: label); -// } - -// await _wallet.save(); -// state = ExecutedSuccessfullyState(); -// } catch (e) { -// state = FailureState(e.toString()); -// } -// } } diff --git a/lib/view_model/nano_account_list/nano_account_list_view_model.dart b/lib/view_model/nano_account_list/nano_account_list_view_model.dart index 4a4c35e75..ac92931cc 100644 --- a/lib/view_model/nano_account_list/nano_account_list_view_model.dart +++ b/lib/view_model/nano_account_list/nano_account_list_view_model.dart @@ -36,17 +36,6 @@ abstract class NanoAccountListViewModelBase with Store { .toList(); } - // if (_wallet.type == WalletType.banano) { - // return banano - // !.getAccountList(_wallet) - // .accounts.map((acc) => AccountListItem( - // label: acc.label, - // id: acc.id, - // balance: acc.balance, - // isSelected: acc.id == banano!.getCurrentAccount(_wallet).id)) - // .toList(); - // } - throw Exception('Unexpected wallet type: ${_wallet.type}'); } @@ -61,13 +50,5 @@ abstract class NanoAccountListViewModelBase with Store { item.balance, ); } - - // if (_wallet.type == WalletType.banano) { - // banano!.setCurrentAccount( - // _wallet, - // item.id, - // item.label, - // ); - // } } } diff --git a/lib/view_model/node_list/node_create_or_edit_view_model.dart b/lib/view_model/node_list/node_create_or_edit_view_model.dart index f749ed0d5..0fb9a83c6 100644 --- a/lib/view_model/node_list/node_create_or_edit_view_model.dart +++ b/lib/view_model/node_list/node_create_or_edit_view_model.dart @@ -1,11 +1,14 @@ import 'package:cake_wallet/core/execution_state.dart'; import 'package:cake_wallet/entities/qr_scanner.dart'; import 'package:cake_wallet/store/settings_store.dart'; +import 'package:flutter/cupertino.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:cw_core/node.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:collection/collection.dart'; +import 'package:cake_wallet/utils/permission_handler.dart'; +import 'package:permission_handler/permission_handler.dart'; part 'node_create_or_edit_view_model.g.dart'; @@ -175,8 +178,11 @@ abstract class NodeCreateOrEditViewModelBase with Store { void setAsCurrent(Node node) => _settingsStore.nodes[_walletType] = node; @action - Future scanQRCodeForNewNode() async { + Future scanQRCodeForNewNode(BuildContext context) async { try { + bool isCameraPermissionGranted = + await PermissionHandler.checkPermission(Permission.camera, context); + if (!isCameraPermissionGranted) return; String code = await presentQRScanner(); if (code.isEmpty) { diff --git a/lib/view_model/node_list/node_list_view_model.dart b/lib/view_model/node_list/node_list_view_model.dart index fb1198c41..ae0edba30 100644 --- a/lib/view_model/node_list/node_list_view_model.dart +++ b/lib/view_model/node_list/node_list_view_model.dart @@ -66,6 +66,9 @@ abstract class NodeListViewModelBase with Store { case WalletType.ethereum: node = getEthereumDefaultNode(nodes: _nodeSource)!; break; + case WalletType.bitcoinCash: + node = getBitcoinCashDefaultElectrumServer(nodes: _nodeSource)!; + break; case WalletType.nano: node = getNanoDefaultNode(nodes: _nodeSource)!; break; diff --git a/lib/view_model/restore/restore_from_qr_vm.dart b/lib/view_model/restore/restore_from_qr_vm.dart index 39a7b682f..4ffc81cef 100644 --- a/lib/view_model/restore/restore_from_qr_vm.dart +++ b/lib/view_model/restore/restore_from_qr_vm.dart @@ -1,5 +1,7 @@ import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart'; import 'package:cake_wallet/ethereum/ethereum.dart'; +import 'package:cake_wallet/nano/nano.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'; @@ -84,9 +86,15 @@ abstract class WalletRestorationFromQRVMBase extends WalletCreationVM with Store case WalletType.litecoin: return bitcoin!.createBitcoinRestoreWalletFromSeedCredentials( name: name, mnemonic: restoreWallet.mnemonicSeed ?? '', password: password); + case WalletType.bitcoinCash: + return bitcoinCash!.createBitcoinCashRestoreWalletFromSeedCredentials( + name: name, mnemonic: restoreWallet.mnemonicSeed ?? '', password: password); case WalletType.ethereum: return ethereum!.createEthereumRestoreWalletFromSeedCredentials( name: name, mnemonic: restoreWallet.mnemonicSeed ?? '', password: password); + case WalletType.nano: + return nano!.createNanoRestoreWalletFromSeedCredentials( + name: name, mnemonic: restoreWallet.mnemonicSeed ?? '', password: password); default: throw Exception('Unexpected type: ${type.toString()}'); } diff --git a/lib/view_model/restore/wallet_restore_from_qr_code.dart b/lib/view_model/restore/wallet_restore_from_qr_code.dart index e9aed55c6..077675d1f 100644 --- a/lib/view_model/restore/wallet_restore_from_qr_code.dart +++ b/lib/view_model/restore/wallet_restore_from_qr_code.dart @@ -1,44 +1,106 @@ import 'package:cake_wallet/core/seed_validator.dart'; import 'package:cake_wallet/entities/parse_address_from_domain.dart'; import 'package:cake_wallet/entities/qr_scanner.dart'; +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/restore/restore_mode.dart'; import 'package:cake_wallet/view_model/restore/restore_wallet.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/cupertino.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:collection/collection.dart'; class WalletRestoreFromQRCode { WalletRestoreFromQRCode(); + static const Map _walletTypeMap = { + 'monero': WalletType.monero, + 'monero-wallet': WalletType.monero, + 'monero_wallet': WalletType.monero, + 'bitcoin': WalletType.bitcoin, + 'bitcoin-wallet': WalletType.bitcoin, + 'bitcoin_wallet': WalletType.bitcoin, + 'litecoin': WalletType.litecoin, + 'litecoin-wallet': WalletType.litecoin, + 'litecoin_wallet': WalletType.litecoin, + 'ethereum-wallet': WalletType.ethereum, + 'nano-wallet': WalletType.nano, + 'nano_wallet': WalletType.nano, + 'bitcoincash': WalletType.bitcoinCash, + 'bitcoincash-wallet': WalletType.bitcoinCash, + 'bitcoincash_wallet': WalletType.bitcoinCash, + }; + + static bool _containsAssetSpecifier(String code) => _extractWalletType(code) != null; + + static WalletType? _extractWalletType(String code) { + final sortedKeys = _walletTypeMap.keys.toList()..sort((a, b) => b.length.compareTo(a.length)); + + final extracted = sortedKeys.firstWhereOrNull((key) => code.toLowerCase().contains(key)); + + return _walletTypeMap[extracted]; + } + + static String? _extractAddressFromUrl(String rawString, WalletType type) { + return AddressResolver.extractAddressByType( + raw: rawString, type: walletTypeToCryptoCurrency(type)); + } + + static String? _extractSeedPhraseFromUrl(String rawString, WalletType walletType) { + RegExp _getPattern(int wordCount) => + RegExp(r'(?<=\W|^)((?:\w+\s+){' + (wordCount - 1).toString() + r'}\w+)(?=\W|$)'); + + List patternCounts = walletType == WalletType.monero ? [25, 14, 13] : [24, 18, 12]; + + for (final count in patternCounts) { + final pattern = _getPattern(count); + final match = pattern.firstMatch(rawString); + if (match != null) { + return match.group(1)?.trim(); + } + } + return null; + } + static Future scanQRCodeForRestoring(BuildContext context) async { String code = await presentQRScanner(); - Map credentials = {}; + if (code.isEmpty) throw Exception('Unexpected scan QR code value: value is empty'); - if (code.isEmpty) { - throw Exception('Unexpected scan QR code value: value is empty'); - } - final formattedUri = getFormattedUri(code); - final uri = Uri.parse(formattedUri); - final queryParameters = uri.queryParameters; - credentials['type'] = getWalletTypeFromUrl(uri.scheme); + WalletType? walletType; + String formattedUri = ''; - final address = getAddressFromUrl( - type: credentials['type'] as WalletType, - rawString: queryParameters.toString(), - ); - if (address != null) { - credentials['address'] = address; - } + if (!_containsAssetSpecifier(code)) { + await _specifyWalletAssets(context, "Can't determine wallet type, please pick it manually"); + walletType = + await Navigator.pushNamed(context, Routes.restoreWalletTypeFromQR) as WalletType?; + if (walletType == null) throw Exception("Failed to determine wallet type."); - final seed = - getSeedPhraseFromUrl(queryParameters.toString(), credentials['type'] as WalletType); - if (seed != null) { - credentials['seed'] = seed; + final seedPhrase = _extractSeedPhraseFromUrl(code, walletType); + + formattedUri = seedPhrase != null + ? '$walletType:?seed=$seedPhrase' + : throw Exception('Failed to determine valid seed phrase'); } else { - credentials['private_key'] = queryParameters['private_key']; + walletType = _extractWalletType(code); + final index = code.indexOf(':'); + final query = code.substring(index + 1).replaceAll('?', '&'); + formattedUri = '$walletType:?$query'; } - credentials.addAll(queryParameters); - credentials['mode'] = getWalletRestoreMode(credentials); + final uri = Uri.parse(formattedUri); + Map queryParameters = {...uri.queryParameters}; + + if (queryParameters['seed'] == null) { + queryParameters['seed'] = _extractSeedPhraseFromUrl(code, walletType!); + } + if (queryParameters['address'] == null) { + queryParameters['address'] = _extractAddressFromUrl(code, walletType!); + } + + Map credentials = {'type': walletType, ...queryParameters}; + + credentials['mode'] = _determineWalletRestoreMode(credentials); switch (credentials['mode']) { case WalletRestoreMode.txids: @@ -52,98 +114,21 @@ class WalletRestoreFromQRCode { } } - static String getFormattedUri(String code) { - final index = code.indexOf(':'); - if (index == -1) return throw Exception('Unexpected wallet type: $code, try to scan again'); - final scheme = code.substring(0, index).replaceAll('_', '-'); - final query = code.substring(index + 1).replaceAll('?', '&'); - final formattedUri = '$scheme:?$query'; - return formattedUri; - } - - static WalletType getWalletTypeFromUrl(String scheme) { - switch (scheme) { - case 'monero': - case 'monero-wallet': - return WalletType.monero; - case 'bitcoin': - case 'bitcoin-wallet': - return WalletType.bitcoin; - case 'litecoin': - case 'litecoin-wallet': - return WalletType.litecoin; - case 'ethereum-wallet': - return WalletType.ethereum; - default: - throw Exception('Unexpected wallet type: ${scheme.toString()}'); - } - } - - static String? getAddressFromUrl({required WalletType type, required String rawString}) { - return AddressResolver.extractAddressByType( - raw: rawString, type: walletTypeToCryptoCurrency(type)); - } - - static String? getSeedPhraseFromUrl(String rawString, WalletType walletType) { - switch (walletType) { - case WalletType.monero: - RegExp regex25 = RegExp(r'\b(\S+\b\s+){24}\S+\b'); - RegExp regex14 = RegExp(r'\b(\S+\b\s+){13}\S+\b'); - RegExp regex13 = RegExp(r'\b(\S+\b\s+){12}\S+\b'); - - if (regex25.firstMatch(rawString) == null) { - if (regex14.firstMatch(rawString) == null) { - if (regex13.firstMatch(rawString) == null) { - return null; - } else { - return regex13.firstMatch(rawString)!.group(0)!; - } - } else { - return regex14.firstMatch(rawString)!.group(0)!; - } - } else { - return regex25.firstMatch(rawString)!.group(0)!; - } - case WalletType.bitcoin: - case WalletType.litecoin: - case WalletType.ethereum: - RegExp regex24 = RegExp(r'\b(\S+\b\s+){23}\S+\b'); - RegExp regex18 = RegExp(r'\b(\S+\b\s+){17}\S+\b'); - RegExp regex12 = RegExp(r'\b(\S+\b\s+){11}\S+\b'); - - if (regex24.firstMatch(rawString) == null) { - if (regex18.firstMatch(rawString) == null) { - if (regex12.firstMatch(rawString) == null) { - return null; - } else { - return regex12.firstMatch(rawString)!.group(0)!; - } - } else { - return regex18.firstMatch(rawString)!.group(0)!; - } - } else { - return regex24.firstMatch(rawString)!.group(0)!; - } - default: - return null; - } - } - - static WalletRestoreMode getWalletRestoreMode(Map credentials) { + static WalletRestoreMode _determineWalletRestoreMode(Map credentials) { final type = credentials['type'] as WalletType; if (credentials.containsKey('tx_payment_id')) { final txIdValue = credentials['tx_payment_id'] as String? ?? ''; - return txIdValue.isNotEmpty - ? WalletRestoreMode.txids - : throw Exception('Unexpected restore mode: tx_payment_id is invalid'); + if (txIdValue.isNotEmpty) return WalletRestoreMode.txids; + throw Exception('Unexpected restore mode: tx_payment_id is invalid'); } - if (credentials.containsKey('seed')) { - final seedValue = credentials['seed'] as String; + if (credentials['seed'] != null) { + final seedValue = credentials['seed']; final words = SeedValidator.getWordList(type: type, language: 'english'); seedValue.split(' ').forEach((element) { if (!words.contains(element)) { - throw Exception('Unexpected restore mode: mnemonic_seed is invalid'); + throw Exception( + 'Unexpected restore mode: mnemonic_seed is invalid or does\'t match wallet type'); } }); return WalletRestoreMode.seed; @@ -169,3 +154,15 @@ class WalletRestoreFromQRCode { throw Exception('Unexpected restore mode: restore params are invalid'); } } + +Future _specifyWalletAssets(BuildContext context, String error) async { + await showPopUp( + context: context, + builder: (BuildContext context) { + return AlertWithOneAction( + alertTitle: S.current.error, + alertContent: error, + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(context).pop()); + }); +} diff --git a/lib/view_model/send/output.dart b/lib/view_model/send/output.dart index 8008812ba..2e696e16f 100644 --- a/lib/view_model/send/output.dart +++ b/lib/view_model/send/output.dart @@ -81,10 +81,8 @@ abstract class OutputBase with Store { _amount = monero!.formatterMoneroParseAmount(amount: _cryptoAmount); break; case WalletType.bitcoin: - _amount = - bitcoin!.formatterStringDoubleToBitcoinAmount(_cryptoAmount); - break; case WalletType.litecoin: + case WalletType.bitcoinCash: _amount = bitcoin!.formatterStringDoubleToBitcoinAmount(_cryptoAmount); break; @@ -116,7 +114,8 @@ abstract class OutputBase with Store { _settingsStore.priority[_wallet.type]!, formattedCryptoAmount); if (_wallet.type == WalletType.bitcoin || - _wallet.type == WalletType.litecoin) { + _wallet.type == WalletType.litecoin || + _wallet.type == WalletType.bitcoinCash) { return bitcoin!.formatterBitcoinAmountToDouble(amount: fee); } @@ -234,6 +233,9 @@ abstract class OutputBase with Store { case WalletType.litecoin: maximumFractionDigits = 8; break; + case WalletType.bitcoinCash: + maximumFractionDigits = 8; + break; case WalletType.haven: maximumFractionDigits = 12; break; diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index 2030af1d7..be822aff3 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -1,4 +1,3 @@ -import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/entities/priority_for_wallet_type.dart'; import 'package:cake_wallet/entities/transaction_description.dart'; import 'package:cake_wallet/ethereum/ethereum.dart'; @@ -12,7 +11,6 @@ import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart'; import 'package:cw_core/transaction_priority.dart'; import 'package:cake_wallet/view_model/send/output.dart'; import 'package:cake_wallet/view_model/send/send_template_view_model.dart'; -import 'package:cw_nano/nano_wallet.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/entities/template.dart'; @@ -33,6 +31,7 @@ import 'package:cake_wallet/view_model/send/send_view_model_state.dart'; import 'package:cake_wallet/entities/parsed_address.dart'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/haven/haven.dart'; +import 'package:cake_wallet/generated/i18n.dart'; part 'send_view_model.g.dart'; @@ -187,11 +186,14 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor bool get hasCoinControl => wallet.type == WalletType.bitcoin || wallet.type == WalletType.litecoin || - wallet.type == WalletType.monero; + wallet.type == WalletType.monero || + wallet.type == WalletType.bitcoinCash; @computed bool get isElectrumWallet => - wallet.type == WalletType.bitcoin || wallet.type == WalletType.litecoin; + wallet.type == WalletType.bitcoin || + wallet.type == WalletType.litecoin || + wallet.type == WalletType.bitcoinCash; @computed bool get hasFees => wallet.type != WalletType.nano && wallet.type != WalletType.banano; @@ -279,7 +281,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor List conditionsList = []; for (var output in outputs) { - final show = checkThroughChecksToDisplayTOTP(output.address); + final show = checkThroughChecksToDisplayTOTP(output.extractedAddress); conditionsList.add(show); } @@ -322,8 +324,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor await pendingTransaction!.commit(); if (walletType == WalletType.nano) { - var wallet = getIt.get().wallet as NanoWallet?; - wallet?.updateTransactions(); + nano!.updateTransactions(wallet); } if (pendingTransaction!.id.isNotEmpty) { @@ -336,7 +337,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor state = TransactionCommitted(); } catch (e) { - state = FailureState(e.toString()); + String translatedError = translateErrorMessage(e.toString(), wallet.type, wallet.currency); + state = FailureState(translatedError); } } @@ -345,54 +347,31 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor _settingsStore.priority[wallet.type] = priority; Object _credentials() { + final priority = _settingsStore.priority[wallet.type]; + + if (priority == null && wallet.type != WalletType.nano) { + throw Exception('Priority is null for wallet type: ${wallet.type}'); + } + switch (wallet.type) { case WalletType.bitcoin: - final priority = _settingsStore.priority[wallet.type]; - - if (priority == null) { - throw Exception('Priority is null for wallet type: ${wallet.type}'); - } - - return bitcoin!.createBitcoinTransactionCredentials(outputs, priority: priority); case WalletType.litecoin: - final priority = _settingsStore.priority[wallet.type]; + case WalletType.bitcoinCash: + return bitcoin!.createBitcoinTransactionCredentials(outputs, priority: priority!); - if (priority == null) { - throw Exception('Priority is null for wallet type: ${wallet.type}'); - } - - return bitcoin!.createBitcoinTransactionCredentials(outputs, priority: priority); case WalletType.monero: - final priority = _settingsStore.priority[wallet.type]; - - if (priority == null) { - throw Exception('Priority is null for wallet type: ${wallet.type}'); - } - return monero! - .createMoneroTransactionCreationCredentials(outputs: outputs, priority: priority); + .createMoneroTransactionCreationCredentials(outputs: outputs, priority: priority!); + case WalletType.haven: - final priority = _settingsStore.priority[wallet.type]; - - if (priority == null) { - throw Exception('Priority is null for wallet type: ${wallet.type}'); - } - return haven!.createHavenTransactionCreationCredentials( - outputs: outputs, priority: priority, assetType: selectedCryptoCurrency.title); + outputs: outputs, priority: priority!, assetType: selectedCryptoCurrency.title); + case WalletType.ethereum: - final priority = _settingsStore.priority[wallet.type]; - - if (priority == null) { - throw Exception('Priority is null for wallet type: ${wallet.type}'); - } - return ethereum!.createEthereumTransactionCredentials(outputs, - priority: priority, currency: selectedCryptoCurrency); + priority: priority!, currency: selectedCryptoCurrency); case WalletType.nano: - return nano!.createNanoTransactionCredentials( - outputs, - ); + return nano!.createNanoTransactionCredentials(outputs); default: throw Exception('Unexpected wallet type: ${wallet.type}'); } @@ -427,4 +406,19 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor selectedCryptoCurrency = wallet.currency; } } + + String translateErrorMessage( + String error, + WalletType walletType, + CryptoCurrency currency, + ) { + if (walletType == WalletType.ethereum || walletType == WalletType.haven) { + if (error.contains('gas required exceeds allowance') || + error.contains('insufficient funds for')) { + return S.current.do_not_have_enough_gas_asset(currency.toString()); + } + } + + return error; + } } diff --git a/lib/view_model/set_up_2fa_viewmodel.dart b/lib/view_model/set_up_2fa_viewmodel.dart index 0b4b614ab..9587e3075 100644 --- a/lib/view_model/set_up_2fa_viewmodel.dart +++ b/lib/view_model/set_up_2fa_viewmodel.dart @@ -27,7 +27,6 @@ abstract class Setup2FAViewModelBase with Store { unhighlightTabs = false, selected2FASettings = ObservableList(), state = InitialExecutionState() { - _getRandomBase32SecretKey(); selectCakePreset(selectedCake2FAPreset); reaction((_) => state, _saveLastAuthTime); } @@ -36,9 +35,12 @@ abstract class Setup2FAViewModelBase with Store { static const banTimeout = 180; // 3 minutes final banTimeoutKey = S.current.auth_store_ban_timeout; - String get secretKey => _settingsStore.totpSecretKey; String get deviceName => _settingsStore.deviceName; - String get totpVersionOneLink => _settingsStore.totpVersionOneLink; + + @computed + String get totpSecretKey => _settingsStore.totpSecretKey; + + String totpVersionOneLink = ''; @observable ExecutionState state; @@ -72,6 +74,10 @@ abstract class Setup2FAViewModelBase with Store { bool get shouldRequireTOTP2FAForExchangesToInternalWallets => _settingsStore.shouldRequireTOTP2FAForExchangesToInternalWallets; + @computed + bool get shouldRequireTOTP2FAForExchangesToExternalWallets => + _settingsStore.shouldRequireTOTP2FAForExchangesToExternalWallets; + @computed bool get shouldRequireTOTP2FAForAddingContacts => _settingsStore.shouldRequireTOTP2FAForAddingContacts; @@ -84,9 +90,14 @@ abstract class Setup2FAViewModelBase with Store { bool get shouldRequireTOTP2FAForAllSecurityAndBackupSettings => _settingsStore.shouldRequireTOTP2FAForAllSecurityAndBackupSettings; - void _getRandomBase32SecretKey() { - final randomBase32Key = Utils.generateRandomBase32SecretKey(16); - _setBase32SecretKey(randomBase32Key); + @action + void generateSecretKey() { + final _totpSecretKey = Utils.generateRandomBase32SecretKey(16); + + totpVersionOneLink = + 'otpauth://totp/Cake%20Wallet:$deviceName?secret=$_totpSecretKey&issuer=Cake%20Wallet&algorithm=SHA512&digits=8&period=30'; + + setTOTPSecretKey(_totpSecretKey); } @action @@ -95,15 +106,10 @@ abstract class Setup2FAViewModelBase with Store { } @action - void _setBase32SecretKey(String value) { + void setTOTPSecretKey(String value) { _settingsStore.totpSecretKey = value; } - @action - void clearBase32SecretKey() { - _settingsStore.totpSecretKey = ''; - } - Duration? banDuration() { final unbanTimestamp = _sharedPreferences.getInt(banTimeoutKey); @@ -145,7 +151,7 @@ abstract class Setup2FAViewModelBase with Store { } final result = Utils.verify( - secretKey: secretKey, + secretKey: totpSecretKey, otp: otpText, ); @@ -156,7 +162,6 @@ abstract class Setup2FAViewModelBase with Store { } else { final value = _settingsStore.numberOfFailedTokenTrials + 1; adjustTokenTrialNumber(value); - print(value); if (_failureCounter >= maxFailedTrials) { final banDuration = await ban(); state = AuthenticationBanned( @@ -200,38 +205,15 @@ abstract class Setup2FAViewModelBase with Store { @observable ObservableList selected2FASettings; - //! The code here works, but can be improved - //! Still trying out various ways to improve it - @action - void selectCakePreset(Cake2FAPresetsOptions cake2FAPreset) { - // The tabs are ordered in the format [Narrow || Normal || Verbose] - // Where Narrow = 0, Normal = 1 and Verbose = 2 - switch (cake2FAPreset) { - case Cake2FAPresetsOptions.narrow: - activateCake2FANarrowPreset(); - break; - case Cake2FAPresetsOptions.normal: - activateCake2FANormalPreset(); - break; - case Cake2FAPresetsOptions.aggressive: - activateCake2FAAggressivePreset(); - break; - default: - activateCake2FANormalPreset(); - } - } - @action void checkIfTheCurrentSettingMatchesAnyOfThePresets() { final hasNormalPreset = checkIfTheNormalPresetIsPresent(); final hasNarrowPreset = checkIfTheNarrowPresetIsPresent(); final hasVerbosePreset = checkIfTheVerbosePresetIsPresent(); - if (hasNormalPreset || hasNarrowPreset || hasVerbosePreset) { - unhighlightTabs = false; - } else { - unhighlightTabs = true; - } + if (hasNormalPreset || hasNarrowPreset || hasVerbosePreset) return; + + noCake2FAPresetSelected(); } @action @@ -287,32 +269,8 @@ abstract class Setup2FAViewModelBase with Store { } @action - void activateCake2FANormalPreset() { - _settingsStore.selectedCake2FAPreset = Cake2FAPresetsOptions.normal; - setAllControlsToFalse(); - switchShouldRequireTOTP2FAForSendsToNonContact(true); - switchShouldRequireTOTP2FAForSendsToContact(true); - switchShouldRequireTOTP2FAForSendsToInternalWallets(true); - switchShouldRequireTOTP2FAForExchangesToInternalWallets(true); - switchShouldRequireTOTP2FAForAllSecurityAndBackupSettings(true); - } - - @action - void activateCake2FANarrowPreset() { - _settingsStore.selectedCake2FAPreset = Cake2FAPresetsOptions.narrow; - setAllControlsToFalse(); - switchShouldRequireTOTP2FAForSendsToNonContact(true); - switchShouldRequireTOTP2FAForAddingContacts(true); - switchShouldRequireTOTP2FAForCreatingNewWallet(true); - switchShouldRequireTOTP2FAForAllSecurityAndBackupSettings(true); - } - - @action - void activateCake2FAAggressivePreset() { - _settingsStore.selectedCake2FAPreset = Cake2FAPresetsOptions.aggressive; - setAllControlsToFalse(); - switchShouldRequireTOTP2FAForAccessingWallet(true); - switchShouldRequireTOTP2FAForAllSecurityAndBackupSettings(true); + void noCake2FAPresetSelected() { + _settingsStore.selectedCake2FAPreset = Cake2FAPresetsOptions.none; } @action @@ -323,96 +281,122 @@ abstract class Setup2FAViewModelBase with Store { switchShouldRequireTOTP2FAForAddingContacts(false); switchShouldRequireTOTP2FAForCreatingNewWallet(false); switchShouldRequireTOTP2FAForExchangesToInternalWallets(false); + switchShouldRequireTOTP2FAForExchangesToExternalWallets(false); switchShouldRequireTOTP2FAForSendsToInternalWallets(false); switchShouldRequireTOTP2FAForAllSecurityAndBackupSettings(false); selected2FASettings.clear(); unhighlightTabs = false; } + final Map> presetsMap = { + Cake2FAPresetsOptions.normal: [ + VerboseControlSettings.sendsToContacts, + VerboseControlSettings.sendsToNonContacts, + VerboseControlSettings.sendsToInternalWallets, + VerboseControlSettings.securityAndBackupSettings, + VerboseControlSettings.exchangesToInternalWallets + ], + Cake2FAPresetsOptions.narrow: [ + VerboseControlSettings.addingContacts, + VerboseControlSettings.sendsToNonContacts, + VerboseControlSettings.creatingNewWallets, + VerboseControlSettings.securityAndBackupSettings, + ], + Cake2FAPresetsOptions.aggressive: [ + VerboseControlSettings.accessWallet, + VerboseControlSettings.securityAndBackupSettings, + ], + Cake2FAPresetsOptions.none: [], + }; + @action - void switchShouldRequireTOTP2FAForAccessingWallet(bool value) { - _settingsStore.shouldRequireTOTP2FAForAccessingWallet = value; - if (value) { - selected2FASettings.add(VerboseControlSettings.accessWallet); - } else { - selected2FASettings.remove(VerboseControlSettings.accessWallet); - } - checkIfTheCurrentSettingMatchesAnyOfThePresets(); + void selectCakePreset(Cake2FAPresetsOptions preset) { + setAllControlsToFalse(); + presetsMap[preset]?.forEach(toggleControl); + _settingsStore.selectedCake2FAPreset = preset; + } + + @action + void toggleControl(VerboseControlSettings control, [bool value = true]) { + final methodsMap = { + VerboseControlSettings.sendsToContacts: switchShouldRequireTOTP2FAForSendsToContact, + VerboseControlSettings.accessWallet: switchShouldRequireTOTP2FAForAccessingWallet, + VerboseControlSettings.addingContacts: switchShouldRequireTOTP2FAForAddingContacts, + VerboseControlSettings.creatingNewWallets: switchShouldRequireTOTP2FAForCreatingNewWallet, + VerboseControlSettings.sendsToNonContacts: switchShouldRequireTOTP2FAForSendsToNonContact, + VerboseControlSettings.sendsToInternalWallets: + switchShouldRequireTOTP2FAForSendsToInternalWallets, + VerboseControlSettings.securityAndBackupSettings: + switchShouldRequireTOTP2FAForAllSecurityAndBackupSettings, + VerboseControlSettings.exchangesToInternalWallets: + switchShouldRequireTOTP2FAForExchangesToInternalWallets, + VerboseControlSettings.exchangesToExternalWallets: + switchShouldRequireTOTP2FAForExchangesToExternalWallets, + }; + + methodsMap[control]?.call(value); } @action void switchShouldRequireTOTP2FAForSendsToContact(bool value) { _settingsStore.shouldRequireTOTP2FAForSendsToContact = value; - if (value) { - selected2FASettings.add(VerboseControlSettings.sendsToContacts); - } else { - selected2FASettings.remove(VerboseControlSettings.sendsToContacts); - } - checkIfTheCurrentSettingMatchesAnyOfThePresets(); + updateSelectedSettings(VerboseControlSettings.sendsToContacts, value); + } + + @action + void switchShouldRequireTOTP2FAForAccessingWallet(bool value) { + _settingsStore.shouldRequireTOTP2FAForAccessingWallet = value; + updateSelectedSettings(VerboseControlSettings.accessWallet, value); } @action void switchShouldRequireTOTP2FAForSendsToNonContact(bool value) { _settingsStore.shouldRequireTOTP2FAForSendsToNonContact = value; - if (value) { - selected2FASettings.add(VerboseControlSettings.sendsToNonContacts); - } else { - selected2FASettings.remove(VerboseControlSettings.sendsToNonContacts); - } - checkIfTheCurrentSettingMatchesAnyOfThePresets(); + updateSelectedSettings(VerboseControlSettings.sendsToNonContacts, value); } @action void switchShouldRequireTOTP2FAForSendsToInternalWallets(bool value) { _settingsStore.shouldRequireTOTP2FAForSendsToInternalWallets = value; - if (value) { - selected2FASettings.add(VerboseControlSettings.sendsToInternalWallets); - } else { - selected2FASettings.remove(VerboseControlSettings.sendsToInternalWallets); - } - checkIfTheCurrentSettingMatchesAnyOfThePresets(); + updateSelectedSettings(VerboseControlSettings.sendsToInternalWallets, value); } @action void switchShouldRequireTOTP2FAForExchangesToInternalWallets(bool value) { _settingsStore.shouldRequireTOTP2FAForExchangesToInternalWallets = value; - if (value) { - selected2FASettings.add(VerboseControlSettings.exchangesToInternalWallets); - } else { - selected2FASettings.remove(VerboseControlSettings.exchangesToInternalWallets); - } - checkIfTheCurrentSettingMatchesAnyOfThePresets(); + updateSelectedSettings(VerboseControlSettings.exchangesToInternalWallets, value); + } + + @action + void switchShouldRequireTOTP2FAForExchangesToExternalWallets(bool value) { + _settingsStore.shouldRequireTOTP2FAForExchangesToExternalWallets = value; + updateSelectedSettings(VerboseControlSettings.exchangesToExternalWallets, value); } @action void switchShouldRequireTOTP2FAForAddingContacts(bool value) { _settingsStore.shouldRequireTOTP2FAForAddingContacts = value; - if (value) - selected2FASettings.add(VerboseControlSettings.addingContacts); - else { - selected2FASettings.remove(VerboseControlSettings.addingContacts); - } - checkIfTheCurrentSettingMatchesAnyOfThePresets(); + updateSelectedSettings(VerboseControlSettings.addingContacts, value); } @action void switchShouldRequireTOTP2FAForCreatingNewWallet(bool value) { _settingsStore.shouldRequireTOTP2FAForCreatingNewWallets = value; - if (value) { - selected2FASettings.add(VerboseControlSettings.creatingNewWallets); - } else { - selected2FASettings.remove(VerboseControlSettings.creatingNewWallets); - } - checkIfTheCurrentSettingMatchesAnyOfThePresets(); + updateSelectedSettings(VerboseControlSettings.creatingNewWallets, value); } @action void switchShouldRequireTOTP2FAForAllSecurityAndBackupSettings(bool value) { _settingsStore.shouldRequireTOTP2FAForAllSecurityAndBackupSettings = value; - if (value) - selected2FASettings.add(VerboseControlSettings.securityAndBackupSettings); - else { - selected2FASettings.remove(VerboseControlSettings.securityAndBackupSettings); + updateSelectedSettings(VerboseControlSettings.securityAndBackupSettings, value); + } + + @action + void updateSelectedSettings(VerboseControlSettings control, bool value) { + if (value) { + selected2FASettings.add(control); + } else { + selected2FASettings.remove(control); } checkIfTheCurrentSettingMatchesAnyOfThePresets(); } diff --git a/lib/view_model/settings/other_settings_view_model.dart b/lib/view_model/settings/other_settings_view_model.dart index 1aec2dc31..b4ca46f70 100644 --- a/lib/view_model/settings/other_settings_view_model.dart +++ b/lib/view_model/settings/other_settings_view_model.dart @@ -1,4 +1,5 @@ import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/entities/buy_provider_types.dart'; import 'package:cake_wallet/entities/priority_for_wallet_type.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cw_core/balance.dart'; @@ -24,7 +25,7 @@ abstract class OtherSettingsViewModelBase with Store { final priority = _settingsStore.priority[_wallet.type]; final priorities = priorityForWalletType(_wallet.type); - if (!priorities.contains(priority)) { + if (!priorities.contains(priority) && priorities.isNotEmpty) { _settingsStore.priority[_wallet.type] = priorities.first; } } @@ -56,11 +57,15 @@ abstract class OtherSettingsViewModelBase with Store { return false; } + + BuyProviderType get buyProviderType { return _settingsStore.defaultBuyProvider; } String getDisplayPriority(dynamic priority) { final _priority = priority as TransactionPriority; - if (_wallet.type == WalletType.bitcoin || _wallet.type == WalletType.litecoin) { + if (_wallet.type == WalletType.bitcoin || + _wallet.type == WalletType.litecoin || + _wallet.type == WalletType.bitcoinCash) { final rate = bitcoin!.getFeeRate(_wallet, _priority); return bitcoin!.bitcoinTransactionPriorityWithLabel(_priority, rate); } @@ -68,6 +73,16 @@ abstract class OtherSettingsViewModelBase with Store { return priority.toString(); } + String getBuyProviderType (dynamic buyProviderType) { + final _buyProviderType = buyProviderType as BuyProviderType; + + return _buyProviderType.toString(); + } + void onDisplayPrioritySelected(TransactionPriority priority) => _settingsStore.priority[_wallet.type] = priority; + + void onBuyProviderTypeSelected(BuyProviderType buyProviderType) => + _settingsStore.defaultBuyProvider = buyProviderType; + } diff --git a/lib/view_model/settings/privacy_settings_view_model.dart b/lib/view_model/settings/privacy_settings_view_model.dart index 27ce919df..b3ffeb353 100644 --- a/lib/view_model/settings/privacy_settings_view_model.dart +++ b/lib/view_model/settings/privacy_settings_view_model.dart @@ -12,7 +12,8 @@ import 'package:cake_wallet/entities/fiat_api_mode.dart'; part 'privacy_settings_view_model.g.dart'; -class PrivacySettingsViewModel = PrivacySettingsViewModelBase with _$PrivacySettingsViewModel; +class +PrivacySettingsViewModel = PrivacySettingsViewModelBase with _$PrivacySettingsViewModel; abstract class PrivacySettingsViewModelBase with Store { PrivacySettingsViewModelBase(this._settingsStore, this._wallet); @@ -57,6 +58,24 @@ abstract class PrivacySettingsViewModelBase with Store { @computed bool get useEtherscan => _settingsStore.useEtherscan; + @computed + bool get lookupTwitter => _settingsStore.lookupsTwitter; + + @computed + bool get looksUpMastodon => _settingsStore.lookupsMastodon; + + @computed + bool get looksUpYatService => _settingsStore.lookupsYatService; + + @computed + bool get looksUpUnstoppableDomains => _settingsStore.lookupsUnstoppableDomains; + + @computed + bool get looksUpOpenAlias => _settingsStore.lookupsOpenAlias; + + @computed + bool get looksUpENS => _settingsStore.lookupsENS; + bool get canUseEtherscan => _wallet.type == WalletType.ethereum; @action @@ -78,6 +97,24 @@ abstract class PrivacySettingsViewModelBase with Store { @action void setDisableSell(bool value) => _settingsStore.disableSell = value; + @action + void setLookupsTwitter(bool value) => _settingsStore.lookupsTwitter = value; + + @action + void setLookupsMastodon(bool value) => _settingsStore.lookupsMastodon = value; + + @action + void setLookupsENS(bool value) => _settingsStore.lookupsENS = value; + + @action + void setLookupsYatService(bool value) => _settingsStore.lookupsYatService = value; + + @action + void setLookupsUnstoppableDomains(bool value) => _settingsStore.lookupsUnstoppableDomains = value; + + @action + void setLookupsOpenAlias(bool value) => _settingsStore.lookupsOpenAlias = value; + @action void setUseEtherscan(bool value) { _settingsStore.useEtherscan = value; diff --git a/lib/view_model/settings/trocador_providers_view_model.dart b/lib/view_model/settings/trocador_providers_view_model.dart new file mode 100644 index 000000000..19204d1f9 --- /dev/null +++ b/lib/view_model/settings/trocador_providers_view_model.dart @@ -0,0 +1,21 @@ +import 'package:cake_wallet/store/settings_store.dart'; +import 'package:mobx/mobx.dart'; + +part 'trocador_providers_view_model.g.dart'; + +class TrocadorProvidersViewModel = TrocadorProvidersViewModelBase with _$TrocadorProvidersViewModel; + +abstract class TrocadorProvidersViewModelBase with Store { + TrocadorProvidersViewModelBase(this._settingsStore); + + final SettingsStore _settingsStore; + + @computed + Map get providerStates => _settingsStore.trocadorProviderStates; + + @action + void toggleProviderState(String providerName) { + final currentState = providerStates[providerName] ?? false; + _settingsStore.saveTrocadorProviderState(providerName, !currentState); + } +} \ No newline at end of file diff --git a/lib/view_model/support_view_model.dart b/lib/view_model/support_view_model.dart index d3b14c59b..ccef76154 100644 --- a/lib/view_model/support_view_model.dart +++ b/lib/view_model/support_view_model.dart @@ -53,6 +53,11 @@ abstract class SupportViewModelBase with Store { icon: 'assets/images/simpleSwap.png', linkTitle: 'support@simpleswap.io', link: 'mailto:support@simpleswap.io'), + LinkListItem( + title: 'Exolix', + icon: 'assets/images/exolix.png', + linkTitle: 'support@exolix.com', + link: 'mailto:support@exolix.com'), if (!isMoneroOnly) ... [ LinkListItem( title: 'Wyre', diff --git a/lib/view_model/trade_details_view_model.dart b/lib/view_model/trade_details_view_model.dart index c0b1ac461..45502fd74 100644 --- a/lib/view_model/trade_details_view_model.dart +++ b/lib/view_model/trade_details_view_model.dart @@ -1,27 +1,28 @@ import 'dart:async'; -import 'package:cake_wallet/exchange/changenow/changenow_exchange_provider.dart'; -import 'package:cake_wallet/exchange/exchange_provider.dart'; + import 'package:cake_wallet/exchange/exchange_provider_description.dart'; -import 'package:cake_wallet/exchange/morphtoken/morphtoken_exchange_provider.dart'; -import 'package:cake_wallet/exchange/sideshift/sideshift_exchange_provider.dart'; -import 'package:cake_wallet/exchange/simpleswap/simpleswap_exchange_provider.dart'; +import 'package:cake_wallet/exchange/provider/changenow_exchange_provider.dart'; +import 'package:cake_wallet/exchange/provider/exchange_provider.dart'; +import 'package:cake_wallet/exchange/provider/exolix_exchange_provider.dart'; +import 'package:cake_wallet/exchange/provider/sideshift_exchange_provider.dart'; +import 'package:cake_wallet/exchange/provider/simpleswap_exchange_provider.dart'; +import 'package:cake_wallet/exchange/provider/trocador_exchange_provider.dart'; import 'package:cake_wallet/exchange/trade.dart'; -import 'package:cake_wallet/exchange/trocador/trocador_exchange_provider.dart'; -import 'package:cake_wallet/exchange/xmrto/xmrto_exchange_provider.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/screens/trade_details/track_trade_list_item.dart'; +import 'package:cake_wallet/src/screens/trade_details/trade_details_list_card.dart'; +import 'package:cake_wallet/src/screens/trade_details/trade_details_status_item.dart'; +import 'package:cake_wallet/src/screens/trade_details/trade_provider_unsupported_item.dart'; +import 'package:cake_wallet/src/screens/transaction_details/standart_list_item.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/utils/date_formatter.dart'; import 'package:cake_wallet/utils/show_bar.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/services.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; -import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/src/screens/transaction_details/standart_list_item.dart'; -import 'package:cake_wallet/src/screens/trade_details/track_trade_list_item.dart'; -import 'package:cake_wallet/src/screens/trade_details/trade_details_list_card.dart'; -import 'package:cake_wallet/src/screens/trade_details/trade_details_status_item.dart'; import 'package:url_launcher/url_launcher.dart'; -import 'package:collection/collection.dart'; part 'trade_details_view_model.g.dart'; @@ -36,15 +37,9 @@ abstract class TradeDetailsViewModelBase with Store { trade = trades.values.firstWhereOrNull((element) => element.id == tradeForDetails.id) ?? tradeForDetails { switch (trade.provider) { - case ExchangeProviderDescription.xmrto: - _provider = XMRTOExchangeProvider(); - break; case ExchangeProviderDescription.changeNow: _provider = ChangeNowExchangeProvider(settingsStore: settingsStore); break; - case ExchangeProviderDescription.morphToken: - _provider = MorphTokenExchangeProvider(trades: trades); - break; case ExchangeProviderDescription.sideShift: _provider = SideShiftExchangeProvider(); break; @@ -54,13 +49,17 @@ abstract class TradeDetailsViewModelBase with Store { case ExchangeProviderDescription.trocador: _provider = TrocadorExchangeProvider(); break; + case ExchangeProviderDescription.exolix: + _provider = ExolixExchangeProvider(); + break; } _updateItems(); - _updateTrade(); - - timer = Timer.periodic(Duration(seconds: 20), (_) async => _updateTrade()); + if (_provider != null) { + _updateTrade(); + timer = Timer.periodic(Duration(seconds: 20), (_) async => _updateTrade()); + } } final Box trades; @@ -82,9 +81,9 @@ abstract class TradeDetailsViewModelBase with Store { try { final updatedTrade = await _provider!.findTradeById(id: trade.id); - if (updatedTrade.createdAt == null && trade.createdAt != null) { + if (updatedTrade.createdAt == null && trade.createdAt != null) updatedTrade.createdAt = trade.createdAt; - } + Trade? foundElement = trades.values.firstWhereOrNull((element) => element.id == trade.id); if (foundElement != null) { final editedTrade = trades.get(foundElement.key); @@ -105,6 +104,10 @@ abstract class TradeDetailsViewModelBase with Store { items.clear(); + if (_provider == null) + items.add(TradeProviderUnsupportedItem( + error: S.current.exchange_provider_unsupported(trade.provider.title))); + items.add( DetailsListStatusItem(title: S.current.trade_details_state, value: trade.state.toString())); @@ -157,6 +160,12 @@ abstract class TradeDetailsViewModelBase with Store { items.add(StandartListItem( title: '${trade.providerName} ${S.current.password}', value: trade.password ?? '')); } + + if (trade.provider == ExchangeProviderDescription.exolix) { + final buildURL = 'https://exolix.com/transaction/${trade.id.toString()}'; + items.add( + TrackTradeListItem(title: 'Track', value: buildURL, onTap: () => _launchUrl(buildURL))); + } } void _launchUrl(String url) { diff --git a/lib/view_model/transaction_details_view_model.dart b/lib/view_model/transaction_details_view_model.dart index 2c0b0540d..29e2e8169 100644 --- a/lib/view_model/transaction_details_view_model.dart +++ b/lib/view_model/transaction_details_view_model.dart @@ -39,6 +39,7 @@ abstract class TransactionDetailsViewModelBase with Store { break; case WalletType.bitcoin: case WalletType.litecoin: + case WalletType.bitcoinCash: _addElectrumListItems(tx, dateFormat); break; case WalletType.haven: @@ -115,6 +116,8 @@ abstract class TransactionDetailsViewModelBase with Store { return 'https://mempool.space/tx/${txId}'; case WalletType.litecoin: return 'https://blockchair.com/litecoin/transaction/${txId}'; + case WalletType.bitcoinCash: + return 'https://blockchair.com/bitcoin-cash/transaction/${txId}'; case WalletType.haven: return 'https://explorer.havenprotocol.org/search?value=${txId}'; case WalletType.ethereum: @@ -135,6 +138,7 @@ abstract class TransactionDetailsViewModelBase with Store { case WalletType.bitcoin: return S.current.view_transaction_on + 'mempool.space'; case WalletType.litecoin: + case WalletType.bitcoinCash: return S.current.view_transaction_on + 'Blockchair.com'; case WalletType.haven: return S.current.view_transaction_on + 'explorer.havenprotocol.org'; @@ -226,6 +230,22 @@ abstract class TransactionDetailsViewModelBase with Store { StandartListItem(title: S.current.transaction_details_amount, value: tx.amountFormatted()), if (tx.feeFormatted()?.isNotEmpty ?? false) StandartListItem(title: S.current.transaction_details_fee, value: tx.feeFormatted()!), + if (showRecipientAddress && tx.to != null) + StandartListItem(title: S.current.transaction_details_recipient_address, value: tx.to!), + ]; + + items.addAll(_items); + } + + + void _addNanoListItems(TransactionInfo tx, DateFormat dateFormat) { + final _items = [ + StandartListItem(title: S.current.transaction_details_transaction_id, value: tx.id), + StandartListItem( + title: S.current.transaction_details_date, value: dateFormat.format(tx.date)), + StandartListItem(title: S.current.confirmations, value: (tx.confirmations > 0).toString()), + StandartListItem(title: S.current.transaction_details_height, value: '${tx.height}'), + StandartListItem(title: S.current.transaction_details_amount, value: tx.amountFormatted()), ]; items.addAll(_items); diff --git a/lib/view_model/unspent_coins/unspent_coins_details_view_model.dart b/lib/view_model/unspent_coins/unspent_coins_details_view_model.dart index 992991147..4da43c241 100644 --- a/lib/view_model/unspent_coins/unspent_coins_details_view_model.dart +++ b/lib/view_model/unspent_coins/unspent_coins_details_view_model.dart @@ -1,9 +1,10 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart'; import 'package:cake_wallet/src/screens/transaction_details/blockexplorer_list_item.dart'; import 'package:cake_wallet/src/screens/transaction_details/standart_list_item.dart'; import 'package:cake_wallet/src/screens/transaction_details/textfield_list_item.dart'; import 'package:cake_wallet/src/screens/transaction_details/transaction_details_list_item.dart'; import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_item.dart'; -import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_list_view_model.dart'; import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_switch_item.dart'; import 'package:cw_core/wallet_type.dart'; @@ -19,12 +20,14 @@ abstract class UnspentCoinsDetailsViewModelBase with Store { UnspentCoinsDetailsViewModelBase( {required this.unspentCoinsItem, required this.unspentCoinsListViewModel}) : items = [], + _type = unspentCoinsListViewModel.wallet.type, isFrozen = unspentCoinsItem.isFrozen, note = unspentCoinsItem.note { items = [ StandartListItem(title: S.current.transaction_details_amount, value: unspentCoinsItem.amount), - StandartListItem(title: S.current.transaction_details_transaction_id, value: unspentCoinsItem.hash), - StandartListItem(title: S.current.widgets_address, value: unspentCoinsItem.address), + StandartListItem( + title: S.current.transaction_details_transaction_id, value: unspentCoinsItem.hash), + StandartListItem(title: S.current.widgets_address, value: formattedAddress), TextFieldListItem( title: S.current.note_tap_to_change, value: note, @@ -46,14 +49,13 @@ abstract class UnspentCoinsDetailsViewModelBase with Store { }) ]; - if ([WalletType.bitcoin, WalletType.litecoin].contains(unspentCoinsListViewModel.wallet.type)) { + if ([WalletType.bitcoin, WalletType.litecoin, WalletType.bitcoinCash].contains(_type)) { items.add(BlockExplorerListItem( title: S.current.view_in_block_explorer, - value: _explorerDescription(unspentCoinsListViewModel.wallet.type), + value: _explorerDescription(_type), onTap: () { try { - final url = Uri.parse( - _explorerUrl(unspentCoinsListViewModel.wallet.type, unspentCoinsItem.hash)); + final url = Uri.parse(_explorerUrl(_type, unspentCoinsItem.hash)); return launchUrl(url); } catch (e) {} }, @@ -67,6 +69,8 @@ abstract class UnspentCoinsDetailsViewModelBase with Store { return 'https://ordinals.com/tx/${txId}'; case WalletType.litecoin: return 'https://litecoin.earlyordies.com/tx/${txId}'; + case WalletType.bitcoinCash: + return 'https://blockchair.com/bitcoin-cash/transaction/${txId}'; default: return ''; } @@ -78,6 +82,8 @@ abstract class UnspentCoinsDetailsViewModelBase with Store { return S.current.view_transaction_on + 'Ordinals.com'; case WalletType.litecoin: return S.current.view_transaction_on + 'Earlyordies.com'; + case WalletType.bitcoinCash: + return S.current.view_transaction_on + 'Blockchair.com'; default: return ''; } @@ -91,5 +97,10 @@ abstract class UnspentCoinsDetailsViewModelBase with Store { final UnspentCoinsItem unspentCoinsItem; final UnspentCoinsListViewModel unspentCoinsListViewModel; + final WalletType _type; List items; + + String get formattedAddress => WalletType.bitcoinCash == _type + ? bitcoinCash!.getCashAddrFormat(unspentCoinsItem.address) + : unspentCoinsItem.address; } diff --git a/lib/view_model/unspent_coins/unspent_coins_item.dart b/lib/view_model/unspent_coins/unspent_coins_item.dart index 9d1f6c71c..bb5c4dd7b 100644 --- a/lib/view_model/unspent_coins/unspent_coins_item.dart +++ b/lib/view_model/unspent_coins/unspent_coins_item.dart @@ -12,6 +12,7 @@ abstract class UnspentCoinsItemBase with Store { required this.isFrozen, required this.note, required this.isSending, + required this.isChange, required this.amountRaw, required this.vout, required this.keyImage @@ -35,6 +36,9 @@ abstract class UnspentCoinsItemBase with Store { @observable bool isSending; + @observable + bool isChange; + @observable int amountRaw; 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 657a0cb74..1815b1689 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 @@ -1,9 +1,8 @@ -import 'package:collection/collection.dart'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; -import 'package:cake_wallet/entities/unspent_transaction_output.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_item.dart'; import 'package:cw_core/unspent_coins_info.dart'; +import 'package:cw_core/unspent_transaction_output.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:hive/hive.dart'; @@ -24,89 +23,68 @@ abstract class UnspentCoinsListViewModelBase with Store { final Box _unspentCoinsInfo; @computed - ObservableList get items => - ObservableList.of(_getUnspents().map((elem) { - final amount = formatAmountToString(elem.value) + ' ${wallet.currency.title}'; - - final info = getUnspentCoinInfo(elem.hash, elem.address, elem.value, elem.vout, elem.keyImage); + ObservableList get items => ObservableList.of(_getUnspents().map((elem) { + final info = + getUnspentCoinInfo(elem.hash, elem.address, elem.value, elem.vout, elem.keyImage); return UnspentCoinsItem( address: elem.address, - amount: amount, + amount: '${formatAmountToString(elem.value)} ${wallet.currency.title}', hash: elem.hash, - isFrozen: info?.isFrozen ?? false, - note: info?.note ?? '', - isSending: info?.isSending ?? true, + isFrozen: info.isFrozen, + note: info.note, + isSending: info.isSending, amountRaw: elem.value, vout: elem.vout, - keyImage: elem.keyImage + keyImage: elem.keyImage, + isChange: elem.isChange, ); })); Future saveUnspentCoinInfo(UnspentCoinsItem item) async { try { - final info = getUnspentCoinInfo(item.hash, item.address, item.amountRaw, item.vout, item.keyImage); - if (info == null) { - final newInfo = UnspentCoinsInfo( - walletId: wallet.id, - hash: item.hash, - address: item.address, - value: item.amountRaw, - vout: item.vout, - isFrozen: item.isFrozen, - isSending: item.isSending, - noteRaw: item.note, - keyImage: item.keyImage - ); + final info = + getUnspentCoinInfo(item.hash, item.address, item.amountRaw, item.vout, item.keyImage); - await _unspentCoinsInfo.add(newInfo); - _updateUnspents(); - wallet.updateBalance(); - return; - } info.isFrozen = item.isFrozen; info.isSending = item.isSending; info.note = item.note; await info.save(); - _updateUnspents(); - wallet.updateBalance(); + await _updateUnspents(); + await wallet.updateBalance(); } catch (e) { print(e.toString()); } } - UnspentCoinsInfo? getUnspentCoinInfo(String hash, String address, int value, int vout, String? keyImage) { - return _unspentCoinsInfo.values.firstWhereOrNull((element) => - element.walletId == wallet.id && - element.hash == hash && - element.address == address && - element.value == value && - element.vout == vout && - element.keyImage == keyImage - ); - } + UnspentCoinsInfo getUnspentCoinInfo( + String hash, String address, int value, int vout, String? keyImage) => + _unspentCoinsInfo.values.firstWhere((element) => + element.walletId == wallet.id && + element.hash == hash && + element.address == address && + element.value == value && + element.vout == vout && + element.keyImage == keyImage); String formatAmountToString(int fullBalance) { if (wallet.type == WalletType.monero) return monero!.formatterMoneroAmountToString(amount: fullBalance); - if ([WalletType.bitcoin, WalletType.litecoin].contains(wallet.type)) + if ([WalletType.bitcoin, WalletType.litecoin, WalletType.bitcoinCash].contains(wallet.type)) return bitcoin!.formatterBitcoinAmountToString(amount: fullBalance); return ''; } - - void _updateUnspents() { - if (wallet.type == WalletType.monero) - return monero!.updateUnspents(wallet); - if ([WalletType.bitcoin, WalletType.litecoin].contains(wallet.type)) + Future _updateUnspents() async { + if (wallet.type == WalletType.monero) return monero!.updateUnspents(wallet); + if ([WalletType.bitcoin, WalletType.litecoin, WalletType.bitcoinCash].contains(wallet.type)) return bitcoin!.updateUnspents(wallet); } List _getUnspents() { - if (wallet.type == WalletType.monero) - return monero!.getUnspents(wallet); - if ([WalletType.bitcoin, WalletType.litecoin].contains(wallet.type)) + if (wallet.type == WalletType.monero) return monero!.getUnspents(wallet); + if ([WalletType.bitcoin, WalletType.litecoin, WalletType.bitcoinCash].contains(wallet.type)) return bitcoin!.getUnspents(wallet); return List.empty(); } 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 a4eb3d386..9e2aa7187 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 @@ -66,7 +66,8 @@ abstract class WalletAddressEditOrCreateViewModelBase with Store { final wallet = _wallet; if (wallet.type == WalletType.bitcoin - || wallet.type == WalletType.litecoin) { + || wallet.type == WalletType.litecoin + || wallet.type == WalletType.bitcoinCash) { await bitcoin!.generateNewAddress(wallet); await wallet.save(); } 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 b35c76cdd..4d5eefdb7 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 @@ -107,6 +107,23 @@ class EthereumURI extends PaymentURI { } } +class BitcoinCashURI extends PaymentURI { + BitcoinCashURI({required String amount, required String address}) + : super(amount: amount, address: address); + @override + String toString() { + var base = address; + + if (amount.isNotEmpty) { + base += '?amount=${amount.replaceAll(',', '.')}'; + } + + return base; + } + } + + + class NanoURI extends PaymentURI { NanoURI({required String amount, required String address}) : super(amount: amount, address: address); @@ -114,7 +131,6 @@ class NanoURI extends PaymentURI { @override String toString() { var base = 'nano:' + address; - if (amount.isNotEmpty) { base += '?amount=${amount.replaceAll(',', '.')}'; } @@ -192,6 +208,10 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo 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); } @@ -280,7 +300,9 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo @computed bool get showElectrumAddressDisclaimer => - wallet.type == WalletType.bitcoin || wallet.type == WalletType.litecoin; + wallet.type == WalletType.bitcoin || + wallet.type == WalletType.litecoin || + wallet.type == WalletType.bitcoinCash; List _baseItems; @@ -294,9 +316,9 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo _baseItems = []; if (wallet.type == WalletType.monero || - wallet.type == WalletType.haven || + wallet.type == WalletType.haven /*|| wallet.type == WalletType.nano || - wallet.type == WalletType.banano) { + wallet.type == WalletType.banano*/) { _baseItems.add(WalletAccountListHeader()); } diff --git a/lib/view_model/wallet_keys_view_model.dart b/lib/view_model/wallet_keys_view_model.dart index e2938c74e..faf05eff9 100644 --- a/lib/view_model/wallet_keys_view_model.dart +++ b/lib/view_model/wallet_keys_view_model.dart @@ -19,6 +19,7 @@ abstract class WalletKeysViewModelBase with Store { WalletKeysViewModelBase(this._appStore) : title = _appStore.wallet!.type == WalletType.bitcoin || _appStore.wallet!.type == WalletType.litecoin || + _appStore.wallet!.type == WalletType.bitcoinCash || _appStore.wallet!.type == WalletType.ethereum ? S.current.wallet_seed : S.current.wallet_keys, @@ -91,7 +92,8 @@ abstract class WalletKeysViewModelBase with Store { } if (_appStore.wallet!.type == WalletType.bitcoin || - _appStore.wallet!.type == WalletType.litecoin) { + _appStore.wallet!.type == WalletType.litecoin || + _appStore.wallet!.type == WalletType.bitcoinCash) { items.addAll([ StandartListItem(title: S.current.wallet_seed, value: _appStore.wallet!.seed!), ]); @@ -145,6 +147,8 @@ abstract class WalletKeysViewModelBase with Store { return 'haven-wallet'; case WalletType.ethereum: return 'ethereum-wallet'; + case WalletType.bitcoinCash: + return 'bitcoincash-wallet'; case WalletType.nano: return 'nano-wallet'; case WalletType.banano: @@ -190,7 +194,7 @@ abstract class WalletKeysViewModelBase with Store { int _getRestoreHeightByTransactions(WalletType type, DateTime date) { if (type == WalletType.monero) { - return monero!.getHeigthByDate(date: date); + return monero!.getHeightByDate(date: date); } else if (type == WalletType.haven) { return haven!.getHeightByDate(date: date); } diff --git a/lib/view_model/wallet_new_vm.dart b/lib/view_model/wallet_new_vm.dart index 04da7190e..9b1f0834d 100644 --- a/lib/view_model/wallet_new_vm.dart +++ b/lib/view_model/wallet_new_vm.dart @@ -1,6 +1,7 @@ import 'package:cake_wallet/view_model/restore/restore_wallet.dart'; import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:flutter/foundation.dart'; +import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/monero/monero.dart'; @@ -46,10 +47,12 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store { name: name, language: options as String); case WalletType.ethereum: return ethereum!.createEthereumNewWalletCredentials(name: name); + case WalletType.bitcoinCash: + return bitcoinCash!.createBitcoinCashNewWalletCredentials(name: name); case WalletType.nano: return nano!.createNanoNewWalletCredentials(name: name); default: - throw Exception('Unexpected type: ${type.toString()}');; + throw Exception('Unexpected type: ${type.toString()}'); } } diff --git a/lib/view_model/wallet_restore_choose_derivation_view_model.dart b/lib/view_model/wallet_restore_choose_derivation_view_model.dart index 3d287c06c..0cfcb780a 100644 --- a/lib/view_model/wallet_restore_choose_derivation_view_model.dart +++ b/lib/view_model/wallet_restore_choose_derivation_view_model.dart @@ -1,11 +1,5 @@ -import 'package:cake_wallet/di.dart'; -import 'package:cake_wallet/store/app_store.dart'; -import 'package:cw_bitcoin/bitcoin_wallet_service.dart'; import 'package:cw_core/wallet_info.dart'; -import 'package:cw_nano/nano_util.dart'; -import 'package:cw_nano/nano_wallet_service.dart'; import 'package:mobx/mobx.dart'; -import 'package:cw_core/wallet_type.dart'; import 'package:cake_wallet/view_model/restore/restore_mode.dart'; part 'wallet_restore_choose_derivation_view_model.g.dart'; diff --git a/lib/view_model/wallet_restore_view_model.dart b/lib/view_model/wallet_restore_view_model.dart index cf03a98d5..030801412 100644 --- a/lib/view_model/wallet_restore_view_model.dart +++ b/lib/view_model/wallet_restore_view_model.dart @@ -3,9 +3,7 @@ import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/nano/nano.dart'; import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cw_bitcoin/bitcoin_wallet_service.dart'; -import 'package:cw_core/node.dart'; -import 'package:cw_nano/nano_wallet.dart'; -import 'package:cw_nano/nano_wallet_service.dart'; +import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/store/app_store.dart'; @@ -87,8 +85,8 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { name: name, mnemonic: seed, password: password, - derivationType: derivationType, - derivationPath: derivationPath, + derivationType: derivationType!, + derivationPath: derivationPath!, ); case WalletType.litecoin: return bitcoin!.createBitcoinRestoreWalletFromSeedCredentials( @@ -98,7 +96,14 @@ 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); + case WalletType.bitcoinCash: + return bitcoinCash!.createBitcoinCashRestoreWalletFromSeedCredentials( + name: name, + mnemonic: seed, + password: password); case WalletType.nano: return nano!.createNanoRestoreWalletFromSeedCredentials( name: name, @@ -175,7 +180,7 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { // return bitcoin!.createBitcoinRestoreWalletFromSeedCredentials( // name: name, mnemonic: seed, password: password); case WalletType.nano: - return await NanoWalletService.compareDerivationMethods( + return await nanoUtil.compareDerivationMethods( mnemonic: mnemonic, seedKey: seedKey, node: node, @@ -183,8 +188,7 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { default: break; } - - // throw Exception('Unexpected type: ${type.toString()}'); + return [DerivationType.def]; } diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index c55241797..68d03b5f8 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -12,13 +12,12 @@ import devicelocale import flutter_secure_storage_macos import in_app_review import package_info +import package_info_plus import path_provider_foundation -import platform_device_id -import platform_device_id_macos import share_plus_macos import shared_preferences_foundation import url_launcher_macos -import wakelock_macos +import wakelock_plus func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { ConnectivityPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlugin")) @@ -28,11 +27,10 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) InAppReviewPlugin.register(with: registry.registrar(forPlugin: "InAppReviewPlugin")) FLTPackageInfoPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlugin")) + FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) - PlatformDeviceIdMacosPlugin.register(with: registry.registrar(forPlugin: "PlatformDeviceIdMacosPlugin")) - PlatformDeviceIdMacosPlugin.register(with: registry.registrar(forPlugin: "PlatformDeviceIdMacosPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) - WakelockMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockMacosPlugin")) + WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin")) } diff --git a/model_generator.sh b/model_generator.sh index 0e4345c25..50cb3d353 100755 --- a/model_generator.sh +++ b/model_generator.sh @@ -4,4 +4,5 @@ cd cw_bitcoin && flutter pub get && flutter packages pub run build_runner build cd cw_haven && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. cd cw_ethereum && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. cd cw_nano && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. +cd cw_bitcoin_cash && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. flutter packages pub run build_runner build --delete-conflicting-outputs \ No newline at end of file diff --git a/pubspec_base.yaml b/pubspec_base.yaml index 45868105c..c6101d28c 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -49,7 +49,7 @@ dependencies: lottie: ^1.3.0 animate_do: ^2.1.0 cupertino_icons: ^1.0.5 - encrypt: ^5.0.1 + encrypt: 5.0.2 crypto: ^3.0.2 # password: ^1.0.0 basic_utils: ^5.6.1 @@ -60,16 +60,21 @@ dependencies: another_flushbar: ^1.12.29 archive: ^3.3.0 cryptography: ^2.0.5 - file_picker: ^5.2.5 + file_picker: + git: + url: https://github.com/cake-tech/flutter_file_picker.git + ref: master unorm_dart: ^0.2.0 # check unorm_dart for usage and for replace permission_handler: ^10.0.0 - device_display_brightness: ^0.0.6 + device_display_brightness: + git: + url: https://github.com/cake-tech/device_display_brightness.git + ref: master workmanager: ^0.5.1 - platform_device_id: ^1.0.1 - wakelock: ^0.6.2 + wakelock_plus: ^1.1.3 flutter_mailer: ^2.0.2 - device_info_plus: 8.1.0 + device_info_plus: ^9.1.0 base32: 2.1.3 in_app_review: ^2.0.6 cake_backup: @@ -78,14 +83,26 @@ dependencies: ref: main version: 1.0.0 flutter_plugin_android_lifecycle: 2.0.9 - path_provider_android: 2.0.24 + path_provider_android: ^2.2.1 shared_preferences_android: 2.0.17 url_launcher_android: 6.0.24 sensitive_clipboard: ^1.0.0 + walletconnect_flutter_v2: ^2.1.4 + eth_sig_util: ^0.0.9 + ens_dart: + git: + url: https://github.com/cake-tech/ens_dart.git + ref: main bitcoin_flutter: git: url: https://github.com/cake-tech/bitcoin_flutter.git ref: cake-update-v3 + fluttertoast: 8.1.4 + tor: + git: + url: https://github.com/cake-tech/tor.git + ref: main + socks5_proxy: ^1.0.4 dev_dependencies: flutter_test: @@ -123,6 +140,7 @@ flutter: - assets/bitcoin_electrum_server_list.yml - assets/litecoin_electrum_server_list.yml - assets/ethereum_server_list.yml + - assets/bitcoin_cash_electrum_server_list.yml - assets/nano_node_list.yml - assets/nano_pow_node_list.yml - assets/text/ diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index 1f68afa98..84a0a72f5 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -339,7 +339,6 @@ "template": "قالب", "confirm_delete_template": "سيؤدي هذا الإجراء إلى حذف هذا القالب. هل ترغب في الاستمرار؟", "confirm_delete_wallet": "سيؤدي هذا الإجراء إلى حذف هذه المحفظة. هل ترغب في الاستمرار؟", - "picker_description": "لاختيار ChangeNOW أو MorphToken ، يرجى تغيير زوج التداول الخاص بك أولاً", "change_wallet_alert_title": "تغيير المحفظة الحالية", "change_wallet_alert_content": "هل تريد تغيير المحفظة الحالية إلى ${wallet_name}؟", "creating_new_wallet": "يتم إنشاء محفظة جديدة", @@ -592,7 +591,6 @@ "sweeping_wallet_alert": "لن يستغرق هذا وقتًا طويلاً. لا تترك هذه الشاشة وإلا فقد يتم فقد أموال سويبت", "decimal_places_error": "عدد كبير جدًا من المنازل العشرية", "edit_node": "تحرير العقدة", - "frozen_balance": "الرصيد المجمد", "invoice_details": "تفاصيل الفاتورة", "donation_link_details": "تفاصيل رابط التبرع", "anonpay_description": "توليد ${type}. يمكن للمستلم ${method} بأي عملة مشفرة مدعومة ، وستتلقى أموالاً في هذه", @@ -688,7 +686,50 @@ "choose_derivation": "اختر اشتقاق المحفظة", "new_first_wallet_text": "حافظ بسهولة على أمان العملة المشفرة", "select_destination": ".ﻲﻃﺎﻴﺘﺣﻻﺍ ﺦﺴﻨﻟﺍ ﻒﻠﻣ ﺔﻬﺟﻭ ﺪﻳﺪﺤﺗ ءﺎﺟﺮﻟﺍ", + "auto_generate_subaddresses": "تلقائي توليد subddresses", "save_to_downloads": "ﺕﻼﻳﺰﻨﺘﻟﺍ ﻲﻓ ﻆﻔﺣ", - "support_description_other_links": "انضم إلى مجتمعاتنا أو تصل إلينا شركائنا من خلال أساليب أخرى", - "auto_generate_subaddresses": "تلقائي توليد subddresses" + "select_buy_provider_notice": "حدد مزود شراء أعلاه. يمكنك تخطي هذه الشاشة عن طريق تعيين مزود شراء الافتراضي في إعدادات التطبيق.", + "onramper_option_description": "شراء بسرعة التشفير مع العديد من طرق الدفع. متوفر في معظم البلدان. ينتشر وتختلف الرسوم.", + "default_buy_provider": "مزود شراء الافتراضي", + "ask_each_time": "اسأل في كل مرة", + "buy_provider_unavailable": "مزود حاليا غير متوفر.", + "signTransaction": " ﺔﻠﻣﺎﻌﻤﻟﺍ ﻊﻴﻗﻮﺗ", + "errorGettingCredentials": "ﺩﺎﻤﺘﻋﻻﺍ ﺕﺎﻧﺎﻴﺑ ﻰﻠﻋ ﻝﻮﺼﺤﻟﺍ ءﺎﻨﺛﺃ ﺄﻄﺧ ﺙﺪﺣ :ﻞﺸﻓ", + "errorSigningTransaction": "ﺔﻠﻣﺎﻌﻤﻟﺍ ﻊﻴﻗﻮﺗ ءﺎﻨﺛﺃ ﺄﻄﺧ ﺙﺪﺣ", + "pairingInvalidEvent": "ﺢﻟﺎﺻ ﺮﻴﻏ ﺙﺪﺣ ﻥﺍﺮﻗﺇ", + "chains": "ﻞﺳﻼﺴﻟﺍ", + "methods": " ﻕﺮﻃُ", + "events": "ﺙﺍﺪﺣﻷﺍ", + "reject": "ﺾﻓﺮﻳ", + "approve": "ﺪﻤﺘﻌﻳ", + "expiresOn": "ﻲﻓ ﻪﺘﻴﺣﻼﺻ ﻲﻬﺘﻨﺗ", + "walletConnect": "WalletConnect", + "nullURIError": "ﻍﺭﺎﻓ (URI) ﻢﻈﺘﻨﻤﻟﺍ ﺩﺭﺍﻮﻤﻟﺍ ﻑﺮﻌﻣ", + "connectWalletPrompt": "ﺕﻼﻣﺎﻌﻤﻟﺍ ءﺍﺮﺟﻹ WalletConnect ﻊﻣ ﻚﺘﻈﻔﺤﻣ ﻞﻴﺻﻮﺘﺑ ﻢﻗ", + "newConnection": "ﺪﻳﺪﺟ ﻝﺎﺼﺗﺍ", + "activeConnectionsPrompt": "ﺎﻨﻫ ﺔﻄﺸﻨﻟﺍ ﺕﻻﺎﺼﺗﻻﺍ ﺮﻬﻈﺘﺳ", + "deleteConnectionConfirmationPrompt": "ـﺑ ﻝﺎﺼﺗﻻﺍ ﻑﺬﺣ ﺪﻳﺮﺗ ﻚﻧﺃ ﺪﻛﺄﺘﻣ ﺖﻧﺃ ﻞﻫ", + "event": "ﺙﺪﺣ", + "successful": "ﺢﺟﺎﻧ", + "wouoldLikeToConnect": "ﻝﺎﺼﺗﻻﺍ ﻲﻓ ﺐﻏﺮﺗ", + "message": "ﺔﻟﺎﺳﺭ", + "do_not_have_enough_gas_asset": "ليس لديك ما يكفي من ${currency} لإجراء معاملة وفقًا لشروط شبكة blockchain الحالية. أنت بحاجة إلى المزيد من ${currency} لدفع رسوم شبكة blockchain، حتى لو كنت ترسل أصلًا مختلفًا.", + "totp_auth_url": "TOTP ﺔﻗﺩﺎﺼﻤﻟ URL ﻥﺍﻮﻨﻋ", + "awaitDAppProcessing": ".ﺔﺠﻟﺎﻌﻤﻟﺍ ﻦﻣ dApp ﻲﻬﺘﻨﻳ ﻰﺘﺣ ﺭﺎﻈﺘﻧﻻﺍ ﻰﺟﺮﻳ", + "copyWalletConnectLink": "ﺎﻨﻫ ﻪﻘﺼﻟﺍﻭ dApp ﻦﻣ WalletConnect ﻂﺑﺍﺭ ﺦﺴﻧﺍ", + "enterWalletConnectURI": "WalletConnect ـﻟ URI ﻞﺧﺩﺃ", + "seed_key": "مفتاح البذور", + "enter_seed_phrase": "أدخل عبارة البذور الخاصة بك", + "change_rep_successful": "تم تغيير ممثل بنجاح", + "add_contact": "ﻝﺎﺼﺗﺍ ﺔﻬﺟ ﺔﻓﺎﺿﺇ", + "exchange_provider_unsupported": "${providerName} لم يعد مدعومًا!", + "domain_looks_up": "ﻝﺎﺠﻤﻟﺍ ﺚﺤﺑ ﺕﺎﻴﻠﻤﻋ", + "require_for_exchanges_to_external_wallets": "ﺔﻴﺟﺭﺎﺧ ﻆﻓﺎﺤﻣ ﻰﻟﺇ ﺕﻻﺩﺎﺒﺘﻟﺍ ﺐﻠﻄﺘﺗ", + "camera_permission_is_required": ".ﺍﺮﻴﻣﺎﻜﻟﺍ ﻥﺫﺇ ﺏﻮﻠﻄﻣ", + "switchToETHWallet": "ﻯﺮﺧﺃ ﺓﺮﻣ ﺔﻟﻭﺎﺤﻤﻟﺍﻭ Ethereum ﺔﻈﻔﺤﻣ ﻰﻟﺇ ﻞﻳﺪﺒﺘﻟﺍ ﻰﺟﺮﻳ", + "seed_phrase_length": " ﺭﻭﺬﺒﻟﺍ ﺓﺭﺎﺒﻌﻟﺍ ﻝﻮﻃ", + "unavailable_balance": " ﺮﻓﻮﺘﻣ ﺮﻴﻏ ﺪﻴﺻﺭ", + "unavailable_balance_description": ".ﺎﻫﺪﻴﻤﺠﺗ ءﺎﻐﻟﺇ ﺭﺮﻘﺗ ﻰﺘﺣ ﺕﻼﻣﺎﻌﻤﻠﻟ ﻝﻮﺻﻮﻠﻟ ﺔﻠﺑﺎﻗ ﺮﻴﻏ ﺓﺪﻤﺠﻤﻟﺍ ﺓﺪﺻﺭﻷﺍ ﻞﻈﺗ ﺎﻤﻨﻴﺑ ،ﺎﻬﺑ ﺔﺻﺎﺨﻟﺍ ﺕﻼﻣﺎﻌﻤﻟﺍ ﻝﺎﻤﺘﻛﺍ ﺩﺮﺠﻤﺑ ﺔﺣﺎﺘﻣ ﺔﻠﻔﻘﻤﻟﺍ ﺓﺪﺻﺭﻷﺍ ﺢﺒﺼﺘﺳ .ﻚﺑ ﺔﺻﺎﺨﻟﺍ ﺕﻼﻤﻌﻟﺍ ﻲﻓ ﻢﻜﺤﺘﻟﺍ ﺕﺍﺩﺍﺪﻋﺇ ﻲﻓ ﻂﺸﻧ ﻞﻜﺸﺑ ﺎﻫﺪﻴﻤﺠﺘﺑ ﺖﻤﻗ", + "unspent_change": "يتغير", + "tor_connection": " ﺭﻮﺗ ﻝﺎﺼﺗﺍ" } diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index b35fe257e..b1851156c 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -339,7 +339,6 @@ "template": "Шаблон", "confirm_delete_template": "Този шаблон ще бъде изтрит. Искате ли да продължите?", "confirm_delete_wallet": "Този портфейл ще бъде изтрит. Искате ли да продължите?", - "picker_description": "За да изберете ChangeNOW или MorphToken, моля, първо променете своя trading pair", "change_wallet_alert_title": "Смяна на сегашния портфейл", "change_wallet_alert_content": "Искате ли да смените сегашния портфейл на ${wallet_name}?", "creating_new_wallet": "Създаване на нов портфейл", @@ -589,7 +588,6 @@ "error_dialog_content": "Получихме грешка.\n\nМоля, изпратете доклада до нашия отдел поддръжка, за да подобрим приложението.", "decimal_places_error": "Твърде много знаци след десетичната запетая", "edit_node": "Редактиране на възел", - "frozen_balance": "Замразен баланс", "invoice_details": "IДанни за фактура", "donation_link_details": "Подробности за връзката за дарение", "anonpay_description": "Генерирайте ${type}. Получателят може да ${method} с всяка поддържана криптовалута и вие ще получите средства в този портфейл.", @@ -685,5 +683,49 @@ "choose_derivation": "Изберете производно на портфейла", "new_first_wallet_text": "Лесно пазете криптовалутата си в безопасност", "select_destination": "Моля, изберете дестинация за архивния файл.", - "save_to_downloads": "Запазване в Изтегляния" + "save_to_downloads": "Запазване в Изтегляния", + "select_buy_provider_notice": "Изберете доставчик на покупка по -горе. Можете да пропуснете този екран, като зададете вашия доставчик по подразбиране по подразбиране в настройките на приложението.", + "onramper_option_description": "Бързо купувайте криптовалута с много методи за плащане. Предлага се в повечето страни. Разпространенията и таксите варират.", + "default_buy_provider": "Доставчик по подразбиране купува", + "ask_each_time": "Питайте всеки път", + "buy_provider_unavailable": "Понастоящем доставчик не е наличен.", + "signTransaction": "Подпишете транзакция", + "errorGettingCredentials": "Неуспешно: Грешка при получаване на идентификационни данни", + "errorSigningTransaction": "Възникна грешка при подписване на транзакция", + "pairingInvalidEvent": "Невалидно събитие при сдвояване", + "chains": "Вериги", + "methods": "Методи", + "events": "събития", + "reject": "Отхвърляне", + "approve": "Одобряване", + "expiresOn": "Изтича на", + "walletConnect": "WalletConnect", + "nullURIError": "URI е нула", + "connectWalletPrompt": "Свържете портфейла си с WalletConnect, за да извършвате транзакции", + "newConnection": "Нова връзка", + "activeConnectionsPrompt": "Тук ще се появят активни връзки", + "deleteConnectionConfirmationPrompt": "Сигурни ли сте, че искате да изтриете връзката към", + "event": "Събитие", + "successful": "Успешен", + "wouoldLikeToConnect": "иска да се свърже", + "message": "Съобщение", + "do_not_have_enough_gas_asset": "Нямате достатъчно ${currency}, за да извършите транзакция с текущите условия на блокчейн мрежата. Имате нужда от повече ${currency}, за да платите таксите за блокчейн мрежа, дори ако изпращате различен актив.", + "totp_auth_url": "TOTP AUTH URL", + "awaitDAppProcessing": "Моля, изчакайте dApp да завърши обработката.", + "copyWalletConnectLink": "Копирайте връзката WalletConnect от dApp и я поставете тук", + "enterWalletConnectURI": "Въведете URI на WalletConnect", + "seed_key": "Ключ за семена", + "enter_seed_phrase": "Въведете вашата фраза за семена", + "change_rep_successful": "Успешно промени представител", + "add_contact": "Добави контакт", + "exchange_provider_unsupported": "${providerName} вече не се поддържа!", + "domain_looks_up": "Търсене на домейни", + "require_for_exchanges_to_external_wallets": "Изискване за обмен към външни портфейли", + "camera_permission_is_required": "Изисква се разрешение за камерата.\nМоля, активирайте го от настройките на приложението.", + "switchToETHWallet": "Моля, преминете към портфейл Ethereum и опитайте отново", + "seed_phrase_length": "Дължина на началната фраза", + "unavailable_balance": "Неналично салдо", + "unavailable_balance_description": "Неналично салдо: Тази обща сума включва средства, които са заключени в чакащи транзакции и тези, които сте замразили активно в настройките за контрол на монетите. Заключените баланси ще станат достъпни, след като съответните им транзакции бъдат завършени, докато замразените баланси остават недостъпни за транзакции, докато не решите да ги размразите.", + "unspent_change": "Промяна", + "tor_connection": "Tor връзка" } diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index dcb093a69..2e59f5fc5 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -339,7 +339,6 @@ "template": "Šablona", "confirm_delete_template": "Tato akce smaže tuto šablonu. Přejete si pokračovat?", "confirm_delete_wallet": "Tato akce smaže tuto peněženku. Přejete si pokračovat?", - "picker_description": "Pro volbu ChangeNOW, nebo MorphToken si prosím vyberte nejprve pár pro obchodování", "change_wallet_alert_title": "Přepnout peněženku", "change_wallet_alert_content": "Opravdu chcete změnit aktivní peněženku na ${wallet_name}?", "creating_new_wallet": "Vytvářím novou peněženku", @@ -589,7 +588,6 @@ "error_dialog_content": "Nastala chyba.\n\nProsím odešlete zprávu o chybě naší podpoře, aby mohli zajistit opravu.", "decimal_places_error": "Příliš mnoho desetinných míst", "edit_node": "Upravit uzel", - "frozen_balance": "Zmrazená bilance", "invoice_details": "detaily faktury", "donation_link_details": "Podrobnosti odkazu na darování", "anonpay_description": "Vygenerujte ${type}. Příjemce může ${method} s jakoukoli podporovanou kryptoměnou a vy obdržíte prostředky v této peněžence.", @@ -685,5 +683,49 @@ "choose_derivation": "Vyberte derivaci peněženky", "new_first_wallet_text": "Snadno udržujte svou kryptoměnu v bezpečí", "select_destination": "Vyberte cíl pro záložní soubor.", - "save_to_downloads": "Uložit do Stažených souborů" + "save_to_downloads": "Uložit do Stažených souborů", + "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.", + "onramper_option_description": "Rychle si koupte krypto s mnoha metodami plateb. K dispozici ve většině zemí. Rozpětí a poplatky se liší.", + "default_buy_provider": "Výchozí poskytovatel nákupu", + "ask_each_time": "Zeptejte se pokaždé", + "buy_provider_unavailable": "Poskytovatel aktuálně nedostupný.", + "signTransaction": "Podepsat transakci", + "errorGettingCredentials": "Selhalo: Chyba při získávání přihlašovacích údajů", + "errorSigningTransaction": "Při podepisování transakce došlo k chybě", + "pairingInvalidEvent": "Neplatná událost párování", + "chains": "Řetězy", + "methods": "Metody", + "events": "Události", + "reject": "Odmítnout", + "approve": "Schvalovat", + "expiresOn": "Vyprší dne", + "walletConnect": "WalletConnect", + "nullURIError": "URI je nulové", + "connectWalletPrompt": "Propojte svou peněženku s WalletConnect a provádějte transakce", + "newConnection": "Nové připojení", + "activeConnectionsPrompt": "Zde se zobrazí aktivní připojení", + "deleteConnectionConfirmationPrompt": "Jste si jisti, že chcete smazat připojení k?", + "event": "událost", + "successful": "Úspěšný", + "wouoldLikeToConnect": "by se chtělo připojit", + "message": "Zpráva", + "do_not_have_enough_gas_asset": "Nemáte dostatek ${currency} k provedení transakce s aktuálními podmínkami blockchainové sítě. K placení poplatků za blockchainovou síť potřebujete více ${currency}, i když posíláte jiné aktivum.", + "totp_auth_url": "URL AUTH TOTP", + "awaitDAppProcessing": "Počkejte, až dApp dokončí zpracování.", + "copyWalletConnectLink": "Zkopírujte odkaz WalletConnect z dApp a vložte jej sem", + "enterWalletConnectURI": "Zadejte identifikátor URI WalletConnect", + "seed_key": "Klíč semen", + "enter_seed_phrase": "Zadejte svou frázi semen", + "change_rep_successful": "Úspěšně změnil zástupce", + "add_contact": "Přidat kontakt", + "exchange_provider_unsupported": "${providerName} již není podporováno!", + "domain_looks_up": "Vyhledávání domén", + "require_for_exchanges_to_external_wallets": "Vyžadovat pro výměny do externích peněženek", + "camera_permission_is_required": "Vyžaduje se povolení fotoaparátu.\nPovolte jej v nastavení aplikace.", + "switchToETHWallet": "Přejděte na peněženku Ethereum a zkuste to znovu", + "seed_phrase_length": "Délka fráze semene", + "unavailable_balance": "Nedostupný zůstatek", + "unavailable_balance_description": "Nedostupný zůstatek: Tento součet zahrnuje prostředky, které jsou uzamčeny v nevyřízených transakcích a ty, které jste aktivně zmrazili v nastavení kontroly mincí. Uzamčené zůstatky budou k dispozici po dokončení příslušných transakcí, zatímco zmrazené zůstatky zůstanou pro transakce nepřístupné, dokud se nerozhodnete je uvolnit.", + "unspent_change": "Změna", + "tor_connection": "Připojení Tor" } diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 736261572..7868553be 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -339,7 +339,6 @@ "template": "Vorlage", "confirm_delete_template": "Diese Aktion löscht diese Vorlage. Möchten Sie fortfahren?", "confirm_delete_wallet": "Diese Aktion löscht diese Wallet. Möchten Sie fortfahren?", - "picker_description": "Um ChangeNOW oder MorphToken zu wählen, ändern Sie bitte zuerst Ihr Handelspaar", "change_wallet_alert_title": "Aktuelle Wallet ändern", "change_wallet_alert_content": "Möchten Sie die aktuelle Wallet zu ${wallet_name} ändern?", "creating_new_wallet": "Neue Wallet erstellen", @@ -364,8 +363,8 @@ "enter_your_note": "Geben Sie Ihre Bemerkung ein…", "note_optional": "Bemerkung (optional)", "note_tap_to_change": "Bemerkung (zum Ändern tippen)", - "view_in_block_explorer": "View in Block Explorer", - "view_transaction_on": "View Transaction on ", + "view_in_block_explorer": "In Block Explorer anzeigen", + "view_transaction_on": "Anzeigen der Transaktion auf ", "transaction_key": "Transaktionsschlüssel", "confirmations": "Bestätigungen", "recipient_address": "Empfängeradresse", @@ -405,11 +404,11 @@ "moonpay_alert_text": "Der Wert des Betrags muss größer oder gleich ${minAmount} ${fiatCurrency} sein", "outdated_electrum_wallet_receive_warning": "Wenn diese Wallet einen 12-Wort-Seed hat und in Cake erstellt wurde, zahlen Sie KEINE Bitcoins in diese Wallet ein. Alle auf diese Wallet übertragenen BTC können verloren gehen. Erstellen Sie eine neue 24-Wort-Wallet (tippen Sie auf das Menü oben rechts, wählen Sie Wallets, wählen Sie Neue Wallet erstellen und dann Bitcoin) und verschieben Sie Ihre BTC SOFORT dorthin. Neue (24-Wort-)BTC-Wallets von Cake sind sicher", "do_not_show_me": "Zeig mir das nicht noch einmal", - "unspent_coins_title": "Nicht ausgegebene Münzen", - "unspent_coins_details_title": "Details zu nicht ausgegebenen Münzen", + "unspent_coins_title": "Nicht ausgegebene Coins", + "unspent_coins_details_title": "Details zu nicht ausgegebenen Coins", "freeze": "Einfrieren", "frozen": "Gefroren", - "coin_control": "Münzkontrolle (optional)", + "coin_control": "Coin Control (optional)", "address_detected": "Adresse erkannt", "address_from_domain": "Diese Adresse ist von ${domain} auf Unstoppable Domains", "add_receiver": "Fügen Sie einen weiteren Empfänger hinzu (optional)", @@ -551,7 +550,7 @@ "custom_redeem_amount": "Benutzerdefinierter Einlösungsbetrag", "add_custom_redemption": "Benutzerdefinierte Einlösung hinzufügen", "remaining": "Rest", - "delete_wallet": "Geldbörse löschen", + "delete_wallet": "Wallet löschen", "delete_wallet_confirm_message": "Sind Sie sicher, dass Sie das ${wallet_name} Wallet löschen möchten?", "low_fee": "Niedrige Gebühr", "low_fee_alert": "Sie verwenden derzeit eine niedrige Netzwerkgebührenpriorität. Dies kann zu langen Wartezeiten, unterschiedlichen Kursen oder stornierten Trades führen. Wir empfehlen, für ein besseres Erlebnis eine höhere Gebühr festzulegen.", @@ -560,7 +559,7 @@ "do_not_share_warning_text": "Teilen Sie diese nicht mit anderen, einschließlich Support.\n\nIhr Geld kann und wird gestohlen werden!", "help": "hilfe", "all_transactions": "Alle Transaktionen", - "all_trades": "Alle Gewerke", + "all_trades": "Alle Trades", "connection_sync": "Verbindung und Synchronisierung", "security_and_backup": "Sicherheit und Datensicherung", "create_backup": "Backup erstellen", @@ -583,7 +582,7 @@ "unmatched_currencies": "Die Währung Ihres aktuellen Wallets stimmt nicht mit der des gescannten QR überein", "orbot_running_alert": "Bitte stellen Sie sicher, dass Orbot läuft, bevor Sie sich mit diesem Knoten verbinden.", "contact_list_contacts": "Kontakte", - "contact_list_wallets": "Meine Geldbörsen", + "contact_list_wallets": "Meine Wallets", "bitcoin_payments_require_1_confirmation": "Bitcoin-Zahlungen erfordern 1 Bestätigung, was 20 Minuten oder länger dauern kann. Danke für Ihre Geduld! Sie erhalten eine E-Mail, wenn die Zahlung bestätigt ist.", "send_to_this_address": "Senden Sie ${currency} ${tag}an diese Adresse", "arrive_in_this_address": "${currency} ${tag}wird an dieser Adresse ankommen", @@ -592,11 +591,10 @@ "scan_qr_code": "QR-Code scannen", "cold_or_recover_wallet": "Fügen Sie eine Cold Wallet hinzu oder stellen Sie eine Paper Wallet wieder her", "please_wait": "Warten Sie mal", - "sweeping_wallet": "Kehre Geldbörse", - "sweeping_wallet_alert": "Das sollte nicht lange dauern. VERLASSEN SIE DIESEN BILDSCHIRM NICHT, ANDERNFALLS KÖNNEN DIE SWEPT-GELDER VERLOREN GEHEN", + "sweeping_wallet": "Wallet leeren", + "sweeping_wallet_alert": "Das sollte nicht lange dauern. VERLASSEN SIE DIESEN BILDSCHIRM NICHT, ANDERNFALLS KÖNNEN DIE GELDER VERLOREN GEHEN", "decimal_places_error": "Zu viele Nachkommastellen", "edit_node": "Knoten bearbeiten", - "frozen_balance": "Gefrorenes Guthaben", "invoice_details": "Rechnungs-Details", "donation_link_details": "Details zum Spendenlink", "anonpay_description": "Generieren Sie ${type}. Der Empfänger kann ${method} mit jeder unterstützten Kryptowährung verwenden, und Sie erhalten Geld in dieser Wallet.", @@ -630,7 +628,7 @@ "add_secret_code": "Fügen Sie diesen Geheimcode einem anderen Gerät hinzu", "totp_secret_code": "TOTP-Geheimcode", "important_note": "Wichtiger Hinweis", - "setup_2fa_text": "Cake 2FA ist NICHT so sicher wie eine Kühllagerung. 2FA schützt vor grundlegenden Arten von Angriffen, z. B. wenn Ihr Freund Ihren Fingerabdruck bereitstellt, während Sie schlafen.\n\n Cake 2FA schützt NICHT vor einem kompromittierten Gerät durch einen raffinierten Angreifer.\n\n Wenn Sie den Zugriff auf Ihre 2FA-Codes verlieren , VERLIEREN SIE DEN ZUGANG ZU DIESEM WALLET. Sie müssen Ihre Wallet aus mnemonic Seed wiederherstellen. SIE MÜSSEN DESHALB IHRE MNEMONISCHEN SEEDS SICHERN! Außerdem kann jemand mit Zugriff auf Ihre mnemonischen Seed(s) Ihr Geld stehlen und Cake 2FA umgehen.\n\n Cake-Supportmitarbeiter können Ihnen nicht helfen, wenn Sie den Zugriff auf Ihre mnemonischen Seed(s) verlieren, da Cake Wallet eine Wallet ohne treuhänderische Verwahrung ist.", + "setup_2fa_text": "Cake 2FA ist NICHT so sicher wie eine Cold Wallet. 2FA schützt vor grundlegenden Arten von Angriffen, z.B. wenn Ihr Freund Ihren Fingerabdruck verwendet, während Sie schlafen.\n\n Cake 2FA schützt NICHT vor einem kompromittierten Gerät durch einen raffinierten Angreifer.\n\n Wenn Sie den Zugriff auf Ihre 2FA-Codes verlieren , VERLIEREN SIE DEN ZUGANG ZU DIESEM WALLET. Sie müssen Ihre Wallet aus mnemonic Seed wiederherstellen. SIE MÜSSEN DESHALB IHRE MNEMONISCHEN SEEDS SICHERN! Außerdem kann jemand mit Zugriff auf Ihre mnemonischen Seed(s) Ihr Geld stehlen und Cake 2FA umgehen.\n\n Cake-Supportmitarbeiter können Ihnen nicht helfen, wenn Sie den Zugriff auf Ihre mnemonischen Seed(s) verlieren, da Cake Wallet eine Wallet ohne treuhänderische Verwahrung ist.", "setup_totp_recommended": "TOTP einrichten (empfohlen)", "disable_buy": "Kaufaktion deaktivieren", "disable_sell": "Verkaufsaktion deaktivieren", @@ -675,7 +673,7 @@ "alphabetical": "Alphabetisch", "generate_name": "Namen generieren", "balance_page": "Balance-Seite", - "share": "Aktie", + "share": "Teilen", "slidable": "Verschiebbar", "manage_nodes": "Knoten verwalten", "etherscan_history": "Etherscan-Geschichte", @@ -693,5 +691,49 @@ "choose_derivation": "Wählen Sie Brieftaschenableitung", "new_first_wallet_text": "Bewahren Sie Ihre Kryptowährung einfach sicher auf", "select_destination": "Bitte wählen Sie das Ziel für die Sicherungsdatei aus.", - "save_to_downloads": "Unter „Downloads“ speichern" + "save_to_downloads": "Unter „Downloads“ speichern", + "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.", + "onramper_option_description": "Kaufen Sie schnell Krypto mit vielen Zahlungsmethoden. In den meisten Ländern erhältlich. Spreads und Gebühren variieren.", + "default_buy_provider": "Standard-Kaufanbieter", + "ask_each_time": "Jedes Mal fragen", + "buy_provider_unavailable": "Anbieter derzeit nicht verfügbar.", + "signTransaction": "Transaktion unterzeichnen", + "errorGettingCredentials": "Fehlgeschlagen: Fehler beim Abrufen der Anmeldeinformationen", + "errorSigningTransaction": "Beim Signieren der Transaktion ist ein Fehler aufgetreten", + "pairingInvalidEvent": "Paarung ungültiges Ereignis", + "chains": "Ketten", + "methods": "Methoden", + "events": "Veranstaltungen", + "reject": "Ablehnen", + "approve": "Genehmigen", + "expiresOn": "Läuft aus am", + "walletConnect": "WalletConnect", + "nullURIError": "URI ist null", + "connectWalletPrompt": "Verbinden Sie Ihr Wallet mit WalletConnect, um Transaktionen durchzuführen", + "newConnection": "Neue Verbindung", + "activeConnectionsPrompt": "Hier werden aktive Verbindungen angezeigt", + "deleteConnectionConfirmationPrompt": "Sind Sie sicher, dass Sie die Verbindung zu löschen möchten?", + "event": "Ereignis", + "successful": "Erfolgreich", + "wouoldLikeToConnect": "möchte mich gerne vernetzen", + "message": "Nachricht", + "do_not_have_enough_gas_asset": "Sie verfügen nicht über genügend ${currency}, um eine Transaktion unter den aktuellen Bedingungen des Blockchain-Netzwerks durchzuführen. Sie benötigen mehr ${currency}, um die Gebühren für das Blockchain-Netzwerk zu bezahlen, auch wenn Sie einen anderen Vermögenswert senden.", + "totp_auth_url": "TOTP-Auth-URL", + "awaitDAppProcessing": "Bitte warten Sie, bis die dApp die Verarbeitung abgeschlossen hat.", + "copyWalletConnectLink": "Kopieren Sie den WalletConnect-Link von dApp und fügen Sie ihn hier ein", + "enterWalletConnectURI": "Geben Sie den WalletConnect-URI ein", + "seed_key": "Samenschlüssel", + "enter_seed_phrase": "Geben Sie Ihre Samenphrase ein", + "change_rep_successful": "Erfolgreich veränderte Vertreter", + "add_contact": "Kontakt hinzufügen", + "exchange_provider_unsupported": "${providerName} wird nicht mehr unterstützt!", + "domain_looks_up": "Domain-Suchen", + "require_for_exchanges_to_external_wallets": "Erforderlich für den Umtausch in externe Wallets", + "camera_permission_is_required": "Eine Kameraerlaubnis ist erforderlich.\nBitte aktivieren Sie es in den App-Einstellungen.", + "switchToETHWallet": "Bitte wechseln Sie zu einem Ethereum-Wallet und versuchen Sie es erneut", + "seed_phrase_length": "Länge der Seed-Phrase", + "unavailable_balance": "Nicht verfügbares Guthaben", + "unavailable_balance_description": "Nicht verfügbares Guthaben: Diese Summe umfasst Gelder, die in ausstehenden Transaktionen gesperrt sind, und solche, die Sie in Ihren Münzkontrolleinstellungen aktiv eingefroren haben. Gesperrte Guthaben werden verfügbar, sobald die entsprechenden Transaktionen abgeschlossen sind, während eingefrorene Guthaben für Transaktionen nicht zugänglich bleiben, bis Sie sich dazu entschließen, sie wieder freizugeben.", + "unspent_change": "Wechselgeld", + "tor_connection": "Tor-Verbindung" } diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index ee7cbc6a1..c3cd256d9 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -339,7 +339,6 @@ "template": "Template", "confirm_delete_template": "This action will delete this template. Do you wish to continue?", "confirm_delete_wallet": "This action will delete this wallet. Do you wish to continue?", - "picker_description": "To choose ChangeNOW or MorphToken, please change your trading pair first", "change_wallet_alert_title": "Change current wallet", "change_wallet_alert_content": "Do you want to change current wallet to ${wallet_name}?", "creating_new_wallet": "Creating new wallet", @@ -607,7 +606,6 @@ "onion_link": "Onion link", "decimal_places_error": "Too many decimal places", "edit_node": "Edit Node", - "frozen_balance": "Frozen Balance", "settings": "Settings", "sell_monero_com_alert_content": "Selling Monero is not supported yet", "error_text_input_below_minimum_limit": "Amount is less than the minimum", @@ -689,9 +687,54 @@ "support_title_guides": "Cake Wallet guides", "support_description_guides": "Documentation and support for common issues", "support_title_other_links": "Other support links", - "support_description_other_links": "Join our communities or reach us our our partners through other methods", + "support_description_other_links": "Join our communities or reach us or our partners through other methods", "choose_derivation": "Choose Wallet Derivation", "new_first_wallet_text": "Keep your crypto safe, piece of cake", "select_destination": "Please select destination for the backup file.", - "save_to_downloads": "Save to Downloads" + "save_to_downloads": "Save to Downloads", + "select_buy_provider_notice": "Select a buy provider above. You can skip this screen by setting your default buy provider in app settings.", + "onramper_option_description": "Quickly buy crypto with many payment methods. Available in most countries. Spreads and fees vary.", + "default_buy_provider": "Default Buy Provider", + "ask_each_time": "Ask each time", + "robinhood_option_description": "Buy and transfer instantly using your debit card, bank account, or Robinhood balance. USA only.", + "buy_provider_unavailable": "Provider currently unavailable.", + "signTransaction": "Sign Transaction", + "errorGettingCredentials": "Failed: Error while getting credentials", + "errorSigningTransaction": "An error has occured while signing transaction", + "pairingInvalidEvent": "Pairing Invalid Event", + "chains": "Chains", + "methods": "Methods", + "events": "Events", + "reject": "Reject", + "approve": "Approve", + "expiresOn": "Expires on", + "walletConnect": "WalletConnect", + "nullURIError": "URI is null", + "connectWalletPrompt": "Connect your wallet with WalletConnect to make transactions", + "newConnection": "New Connection", + "activeConnectionsPrompt": "Active connections will appear here", + "deleteConnectionConfirmationPrompt": "Are you sure that you want to delete the connection to", + "event": "Event", + "successful": "Successful", + "wouoldLikeToConnect": "would like to connect", + "message": "Message", + "do_not_have_enough_gas_asset": "You do not have enough ${currency} to make a transaction with the current blockchain network conditions. You need more ${currency} to pay blockchain network fees, even if you are sending a different asset.", + "totp_auth_url": "TOTP AUTH URL", + "awaitDAppProcessing": "Kindly wait for the dApp to finish processing.", + "copyWalletConnectLink": "Copy the WalletConnect link from dApp and paste here", + "enterWalletConnectURI": "Enter WalletConnect URI", + "seed_key": "Seed key", + "enter_seed_phrase": "Enter your seed phrase", + "change_rep_successful": "Successfully changed representative", + "add_contact": "Add contact", + "exchange_provider_unsupported": "${providerName} is no longer supported!", + "domain_looks_up": "Domain lookups", + "require_for_exchanges_to_external_wallets": "Require for exchanges to external wallets", + "camera_permission_is_required": "Camera permission is required. \nPlease enable it from app settings.", + "switchToETHWallet": "Please switch to an Ethereum wallet and try again", + "seed_phrase_length": "Seed phrase length", + "unavailable_balance": "Unavailable balance", + "unavailable_balance_description": "Unavailable Balance: This total includes funds that are locked in pending transactions and those you have actively frozen in your coin control settings. Locked balances will become available once their respective transactions are completed, while frozen balances remain inaccessible for transactions until you decide to unfreeze them.", + "unspent_change": "Change", + "tor_connection": "Tor connection" } diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index 1370ff5dd..4e624a9b4 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -339,7 +339,6 @@ "template": "Plantilla", "confirm_delete_template": "Esta acción eliminará esta plantilla. ¿Desea continuar?", "confirm_delete_wallet": "Esta acción eliminará esta billetera. ¿Desea continuar?", - "picker_description": "Para elegir ChangeNOW o MorphToken, primero cambie su par comercial", "change_wallet_alert_title": "Cambiar billetera actual", "change_wallet_alert_content": "¿Quieres cambiar la billetera actual a ${wallet_name}?", "creating_new_wallet": "Creando nueva billetera", @@ -596,7 +595,6 @@ "sweeping_wallet_alert": "Esto no debería llevar mucho tiempo. NO DEJES ESTA PANTALLA O SE PUEDEN PERDER LOS FONDOS BARRIDOS", "decimal_places_error": "Demasiados lugares decimales", "edit_node": "Editar nodo", - "frozen_balance": "Balance congelado", "invoice_details": "Detalles de la factura", "donation_link_details": "Detalles del enlace de donación", "anonpay_description": "Genera ${type}. El destinatario puede ${method} con cualquier criptomoneda admitida, y recibirá fondos en esta billetera.", @@ -693,5 +691,49 @@ "choose_derivation": "Elija la derivación de la billetera", "new_first_wallet_text": "Mantenga fácilmente su criptomoneda segura", "select_destination": "Seleccione el destino del archivo de copia de seguridad.", - "save_to_downloads": "Guardar en Descargas" + "save_to_downloads": "Guardar en Descargas", + "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.", + "onramper_option_description": "Compre rápidamente cripto con muchos métodos de pago. Disponible en la mayoría de los países. Los diferenciales y las tarifas varían.", + "default_buy_provider": "Proveedor de compra predeterminado", + "ask_each_time": "Pregunta cada vez", + "buy_provider_unavailable": "Proveedor actualmente no disponible.", + "signTransaction": "Firmar transacción", + "errorGettingCredentials": "Error: error al obtener las credenciales", + "errorSigningTransaction": "Se ha producido un error al firmar la transacción.", + "pairingInvalidEvent": "Evento de emparejamiento no válido", + "chains": "Cadenas", + "methods": "Métodos", + "events": "Eventos", + "reject": "Rechazar", + "approve": "Aprobar", + "expiresOn": "Expira el", + "walletConnect": "MonederoConectar", + "nullURIError": "URI es nula", + "connectWalletPrompt": "Conecte su billetera con WalletConnect para realizar transacciones", + "newConnection": "Nueva conexión", + "activeConnectionsPrompt": "Las conexiones activas aparecerán aquí", + "deleteConnectionConfirmationPrompt": "¿Está seguro de que desea eliminar la conexión a", + "event": "Evento", + "successful": "Exitoso", + "wouoldLikeToConnect": "quisiera conectar", + "message": "Mensaje", + "do_not_have_enough_gas_asset": "No tienes suficiente ${currency} para realizar una transacción con las condiciones actuales de la red blockchain. Necesita más ${currency} para pagar las tarifas de la red blockchain, incluso si envía un activo diferente.", + "totp_auth_url": "URL de autenticación TOTP", + "awaitDAppProcessing": "Espere a que la dApp termine de procesarse.", + "copyWalletConnectLink": "Copie el enlace de WalletConnect de dApp y péguelo aquí", + "enterWalletConnectURI": "Ingrese el URI de WalletConnect", + "seed_key": "Llave de semilla", + "enter_seed_phrase": "Ingrese su frase de semillas", + "change_rep_successful": "Representante cambiado con éxito", + "add_contact": "Agregar contacto", + "exchange_provider_unsupported": "¡${providerName} ya no es compatible!", + "domain_looks_up": "Búsquedas de dominio", + "require_for_exchanges_to_external_wallets": "Requerido para intercambios a billeteras externas", + "camera_permission_is_required": "Se requiere permiso de la cámara.\nHabilítelo desde la configuración de la aplicación.", + "switchToETHWallet": "Cambie a una billetera Ethereum e inténtelo nuevamente.", + "seed_phrase_length": "Longitud de la frase inicial", + "unavailable_balance": "Saldo no disponible", + "unavailable_balance_description": "Saldo no disponible: este total incluye fondos que están bloqueados en transacciones pendientes y aquellos que usted ha congelado activamente en su configuración de control de monedas. Los saldos bloqueados estarán disponibles una vez que se completen sus respectivas transacciones, mientras que los saldos congelados permanecerán inaccesibles para las transacciones hasta que usted decida descongelarlos.", + "unspent_change": "Cambiar", + "tor_connection": "conexión tor" } diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index dd9cd1dce..d4d414029 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -21,7 +21,7 @@ "contact_name": "Nom de Contact", "reset": "Réinitialiser", "save": "Sauvegarder", - "address_remove_contact": "Supprimer contact", + "address_remove_contact": "Supprimer le contact", "address_remove_content": "Êtes vous certain de vouloir supprimer le contact sélectionné ?", "authenticated": "Authentifié", "authentication": "Authentification", @@ -67,8 +67,8 @@ "min_value": "Min: ${value} ${currency}", "max_value": "Max: ${value} ${currency}", "change_currency": "Changer de Devise", - "overwrite_amount": "Écraser montant", - "qr_payment_amount": "Ce QR code contient un montant de paiement. Voulez-vous écraser la valeur actuelle ?", + "overwrite_amount": "Remplacer le montant", + "qr_payment_amount": "Ce QR code contient un montant de paiement. Voulez-vous remplacer la valeur actuelle ?", "copy_id": "Copier l'ID", "exchange_result_write_down_trade_id": "Merci de copier ou d'écrire l'ID d'échange pour continuer.", "trade_id": "ID d'échange :", @@ -109,7 +109,7 @@ "nodes": "Nœuds", "node_reset_settings_title": "Réinitialisation des réglages", "nodes_list_reset_to_default_message": "Êtes vous certain de vouloir revenir aux réglages par défaut ?", - "change_current_node": "Êtes vous certain de vouloir changer le nœud actuel vers ${node} ?", + "change_current_node": "Êtes vous certain de vouloir changer le nœud actuel pour ${node} ?", "change": "Changer", "remove_node": "Supprimer le nœud", "remove_node_message": "Êtes vous certain de vouloir supprimer le nœud sélectionné ?", @@ -269,7 +269,7 @@ "error_text_address": "L'adresse du portefeuille (wallet) doit correspondre au type de\ncryptomonnaie", "error_text_node_address": "Merci d'entrer une adresse IPv4", "error_text_node_port": "Le port d'un nœud doit être un nombre compris entre 0 et 65535", - "error_text_node_proxy_address": "Veuillez saisir :, par exemple 127.0.0.1:9050", + "error_text_node_proxy_address": "Veuillez saisir :, par exemple 127.0.0.1:9050", "error_text_payment_id": "Un ID de paiement ne peut être constitué que de 16 à 64 caractères hexadécimaux", "error_text_xmr": "La valeur de XMR dépasse le solde disponible.\nLa partie décimale doit comporter au plus 12 chiffres", "error_text_fiat": "La valeur du montant ne peut dépasser le solde disponible.\nLa partie décimale doit comporter au plus 2 chiffres", @@ -339,7 +339,6 @@ "template": "Modèle", "confirm_delete_template": "Cette action va supprimer ce modèle. Souhaitez-vous continuer ?", "confirm_delete_wallet": "Cette action va supprimer ce portefeuille (wallet). Souhaitez-vous contnuer ?", - "picker_description": "Pour choisir ChangeNOW ou MorphToken, merci de modifier d'abord la paire de votre échange", "change_wallet_alert_title": "Changer le portefeuille (wallet) actuel", "change_wallet_alert_content": "Souhaitez-vous changer le portefeuille (wallet) actuel vers ${wallet_name} ?", "creating_new_wallet": "Création d'un nouveau portefeuille (wallet)", @@ -350,7 +349,7 @@ "seed_alert_yes": "Oui, je suis sûr", "exchange_sync_alert_content": "Merci d'attendre que votre portefeuille (wallet) soit synchronisé", "pre_seed_title": "IMPORTANT", - "pre_seed_description": "Sur la page suivante vous allez voir une série de ${words} mots. Ils constituent votre phrase secrète (seed) unique et privé et sont le SEUL moyen de restaurer votre portefeuille (wallet) en cas de perte ou de dysfonctionnement. Il est de VOTRE responsabilité d'écrire cette série de mots et de les stocker dans un lieu sûr en dehors de l'application Cake Wallet.", + "pre_seed_description": "Sur la page suivante vous allez voir une série de ${words} mots. Ils constituent votre phrase secrète (seed) unique et privée et sont le SEUL moyen de restaurer votre portefeuille (wallet) en cas de perte ou de dysfonctionnement. Il est de VOTRE responsabilité d'écrire cette série de mots et de la stocker dans un lieu sûr en dehors de l'application Cake Wallet.", "pre_seed_button_text": "J'ai compris. Montrez moi ma phrase secrète (seed)", "xmr_to_error": "Erreur XMR.TO", "xmr_to_error_description": "Montant invalide. La partie décimale doit contenir au plus 8 chiffres", @@ -409,15 +408,15 @@ "unspent_coins_details_title": "Détails des pièces (coins) non dépensées", "freeze": "Geler", "frozen": "Gelées", - "coin_control": "Contrôle des pièces (optionnel)", + "coin_control": "Contrôle optionnel des pièces (coins)", "address_detected": "Adresse détectée", "address_from_domain": "Cette adresse est issue de ${domain} sur Unstoppable Domains", "add_receiver": "Ajouter un autre bénéficiaire (optionnel)", "manage_yats": "Gérer les Yats", "yat_alert_title": "Envoyez et recevez des cryptos plus facilement avec Yat", - "yat_alert_content": "Les utilisateurs de Cake Wallet peuvent maintenant envoyer et recevoir leurs monnaies favories avec un utilisateur unique en son genre basé sur les emojis.", - "get_your_yat": "Obtenez votre Yat", - "connect_an_existing_yat": "Connectez un Yat existant", + "yat_alert_content": "Les utilisateurs de Cake Wallet peuvent maintenant envoyer et recevoir leurs monnaies favorites avec un utilisateur unique en son genre basé sur les emojis.", + "get_your_yat": "Obtenir votre Yat", + "connect_an_existing_yat": "Connecter un Yat existant", "connect_yats": "Connecter Yats", "yat_address": "Adresse Yat", "yat": "Yat", @@ -427,13 +426,13 @@ "choose_address": "\n\nMerci de choisir l'adresse :", "yat_popup_title": "L'adresse de votre portefeuille (wallet) peut être emojifiée.", "yat_popup_content": "Vous pouvez à présent envoyer et recevoir des cryptos dans Cake Wallet à l'aide de votre Yat - un nom d'utilisateur court à base d'emoji. Gérér les Yats à tout moment depuis l'écran de paramétrage", - "second_intro_title": "Une adresse emoji pour les gouverner tous", + "second_intro_title": "Une adresse emoji pour les gouverner toutes", "second_intro_content": "Votre Yat est une seule et unique adresse emoji qui remplace toutes vos longues adresses hexadécimales pour toutes vos cryptomonnaies.", - "third_intro_title": "Yat joue bien avec les autres", + "third_intro_title": "Yat est universel", "third_intro_content": "Les Yats existent aussi en dehors de Cake Wallet. Toute adresse sur terre peut être remplacée par un Yat !", "learn_more": "En savoir plus", "search": "Chercher", - "search_language": "Recherche une langue", + "search_language": "Rechercher une langue", "search_currency": "Rechercher une devise", "new_template": "Nouveau Modèle", "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", @@ -514,8 +513,8 @@ "active_cards": "Cartes actives", "delete_account": "Supprimer le compte", "cards": "Cartes", - "active": "Actif", - "redeemed": "racheté", + "active": "Actives", + "redeemed": "Converties", "gift_card_balance_note": "Les cartes-cadeaux avec un solde restant apparaîtront ici", "gift_card_redeemed_note": "Les cartes-cadeaux que vous avez utilisées apparaîtront ici", "logout": "Déconnexion", @@ -523,7 +522,7 @@ "percentageOf": "sur ${amount}", "is_percentage": "est", "search_category": "Catégorie de recherche", - "mark_as_redeemed": "Marquer comme échangé", + "mark_as_redeemed": "Marquer comme convertie", "more_options": "Plus d'options", "awaiting_payment_confirmation": "En attente de confirmation de paiement", "transaction_sent_notice": "Si l'écran ne continue pas après 1 minute, vérifiez un explorateur de blocs et votre e-mail.", @@ -544,7 +543,7 @@ "cake_pay_learn_more": "Achetez et utilisez instantanément des cartes-cadeaux dans l'application !\nBalayer de gauche à droite pour en savoir plus.", "automatic": "Automatique", "fixed_pair_not_supported": "Cette paire fixe n'est pas prise en charge avec les échanges sélectionnés", - "variable_pair_not_supported": "Cette paire de variables n'est pas prise en charge avec les échanges sélectionnés", + "variable_pair_not_supported": "Cette paire variable n'est pas prise en charge avec les échanges sélectionnés", "none_of_selected_providers_can_exchange": "Aucun des prestataires sélectionnés ne peut effectuer cet échange", "choose_one": "Choisissez-en un", "choose_from_available_options": "Choisissez parmi les options disponibles :", @@ -596,7 +595,6 @@ "sweeping_wallet_alert": "Cette opération ne devrait pas prendre longtemps. NE QUITTEZ PAS CET ÉCRAN OU LES FONDS CONSOLIDÉS POURRAIENT ÊTRE PERDUS", "decimal_places_error": "Trop de décimales", "edit_node": "Modifier le nœud", - "frozen_balance": "Solde gelé", "invoice_details": "Détails de la facture", "donation_link_details": "Détails du lien de don", "anonpay_description": "Générez ${type}. Le destinataire peut ${method} avec n'importe quelle crypto-monnaie prise en charge, et vous recevrez des fonds dans ce portefeuille (wallet).", @@ -634,7 +632,7 @@ "setup_totp_recommended": "Configurer TOTP (recommandé)", "disable_buy": "Désactiver l'action d'achat", "disable_sell": "Désactiver l'action de vente", - "cake_2fa_preset": "Gâteau 2FA prédéfini", + "cake_2fa_preset": "Cake 2FA prédéfini", "monero_dark_theme": "Thème sombre Monero", "bitcoin_dark_theme": "Thème sombre Bitcoin", "bitcoin_light_theme": "Thème léger Bitcoin", @@ -644,33 +642,33 @@ "auto_generate_subaddresses": "Générer automatiquement des sous-adresses", "narrow": "Étroit", "normal": "Normal", - "aggressive": "Trop zélé", + "aggressive": "Agressif", "require_for_assessing_wallet": "Nécessaire pour accéder au portefeuille", - "require_for_sends_to_non_contacts": "Exiger pour les envois à des non-contacts", + "require_for_sends_to_non_contacts": "Exiger pour les envois hors contacts", "require_for_sends_to_contacts": "Exiger pour les envois aux contacts", - "require_for_sends_to_internal_wallets": "Exiger pour les envois vers des portefeuilles internes", - "require_for_exchanges_to_internal_wallets": "Exiger pour les échanges vers des portefeuilles internes", + "require_for_sends_to_internal_wallets": "Exiger pour les envois vers des portefeuilles (wallets) internes", + "require_for_exchanges_to_internal_wallets": "Exiger pour les échanges vers des portefeuilles (wallets) internes", "require_for_adding_contacts": "Requis pour ajouter des contacts", - "require_for_creating_new_wallets": "Nécessaire pour créer de nouveaux portefeuilles", + "require_for_creating_new_wallets": "Nécessaire pour créer de nouveaux portefeuilles (wallets)", "require_for_all_security_and_backup_settings": "Exiger pour tous les paramètres de sécurité et de sauvegarde", "available_balance_description": "Le solde disponible est le montant que vous pouvez dépenser immédiatement. Il est calculé en soustrayant le solde gelé du solde total.", - "syncing_wallet_alert_title": "Votre portefeuille est en cours de synchronisation", - "syncing_wallet_alert_content": "Votre solde et votre liste de transactions peuvent ne pas être complets tant qu'il n'y a pas « SYNCHRONISÉ » en haut. Cliquez/appuyez pour en savoir plus.", + "syncing_wallet_alert_title": "Votre portefeuille (wallet) est en cours de synchronisation", + "syncing_wallet_alert_content": "Votre solde et votre liste de transactions peuvent ne pas être à jour tant que la mention « SYNCHRONISÉ » n'apparaît en haut de l'écran. Cliquez/appuyez pour en savoir plus.", "home_screen_settings": "Paramètres de l'écran d'accueil", "sort_by": "Trier par", - "search_add_token": "Rechercher / Ajouter un jeton", - "edit_token": "Modifier le jeton", + "search_add_token": "Rechercher / Ajouter un token", + "edit_token": "Modifier le token", "warning": "Avertissement", - "add_token_warning": "Ne modifiez pas ou n'ajoutez pas de jetons comme indiqué par les escrocs.\nConfirmez toujours les adresses de jeton auprès de sources fiables !", - "add_token_disclaimer_check": "J'ai confirmé l'adresse et les informations du contrat de jeton en utilisant une source fiable. L'ajout d'informations malveillantes ou incorrectes peut entraîner une perte de fonds.", - "token_contract_address": "Adresse du contrat de jeton", - "token_name": "Nom du jeton, par exemple : Tether", - "token_symbol": "Symbole de jeton, par exemple : USDT", - "token_decimal": "Décimal de jeton", + "add_token_warning": "Ne modifiez pas ou n'ajoutez pas de tokens comme pourraient vous le suggérer des escrocs.\nConfirmez toujours les adresses de token auprès de sources fiables !", + "add_token_disclaimer_check": "J'ai confirmé l'adresse et les informations du contrat de token en utilisant une source fiable. L'ajout d'informations malveillantes ou incorrectes peut entraîner une perte de fonds.", + "token_contract_address": "Adresse du contrat de token", + "token_name": "Nom du token, par exemple : Tether", + "token_symbol": "Symbole de token, par exemple : USDT", + "token_decimal": "Décimales de token", "field_required": "Ce champ est obligatoire", "pin_at_top": "épingler ${token} en haut", "invalid_input": "Entrée invalide", - "fiat_balance": "Fiat Balance", + "fiat_balance": "Solde fiat", "gross_balance": "Solde brut", "alphabetical": "Alphabétique", "generate_name": "Générer un nom", @@ -685,13 +683,57 @@ "unsupported_asset": "Nous ne prenons pas en charge cette action pour cet élément. Veuillez créer ou passer à un portefeuille d'un type d'actif pris en charge.", "manage_pow_nodes": "Gérer les nœuds PoW", "support_title_live_chat": "Support en direct", - "support_description_live_chat": "GRATUIT ET RAPIDE! Des représentants de soutien formé sont disponibles pour aider", - "support_title_guides": "Guides de portefeuille à gâteau", - "support_description_guides": "Documentation et soutien aux problèmes communs", + "support_description_live_chat": "GRATUIT ET RAPIDE ! Des représentants de soutien formé sont disponibles pour aider", + "support_title_guides": "Guides de Cake Wallet", + "support_description_guides": "Documentation et support pour les problèmes communs", "support_title_other_links": "Autres liens d'assistance", - "support_description_other_links": "Rejoignez nos communautés ou contactez-nous nos partenaires à travers d'autres méthodes", - "choose_derivation": "Choisissez la dérivation du portefeuille", + "support_description_other_links": "Rejoignez nos communautés ou contactez-nous ou nos partenaires à travers d'autres méthodes", + "choose_derivation": "Choisissez le chemin de dérivation du portefeuille", "new_first_wallet_text": "Gardez facilement votre crypto-monnaie en sécurité", "select_destination": "Veuillez sélectionner la destination du fichier de sauvegarde.", - "save_to_downloads": "Enregistrer dans les téléchargements" + "save_to_downloads": "Enregistrer dans les téléchargements", + "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.", + "onramper_option_description": "Achetez rapidement des cryptomonnaies avec de nombreuses méthodes de paiement. Disponible dans la plupart des pays. Les spreads et les frais peuvent varier.", + "default_buy_provider": "Fournisseur d'achat par défaut", + "ask_each_time": "Demander à chaque fois", + "buy_provider_unavailable": "Fournisseur actuellement indisponible.", + "signTransaction": "Signer une transaction", + "errorGettingCredentials": "Échec : erreur lors de l'obtention des informations d'identification", + "errorSigningTransaction": "Une erreur s'est produite lors de la signature de la transaction", + "pairingInvalidEvent": "Événement de couplage non valide", + "chains": "Chaînes", + "methods": "Méthodes", + "events": "Événements", + "reject": "Rejeter", + "approve": "Approuver", + "expiresOn": "Expire le", + "walletConnect": "WalletConnect", + "nullURIError": "L'URI est nul", + "connectWalletPrompt": "Connectez votre portefeuille (wallet) avec WalletConnect pour effectuer des transactions", + "newConnection": "Nouvelle connexion", + "activeConnectionsPrompt": "Les connexions actives apparaîtront ici", + "deleteConnectionConfirmationPrompt": "Êtes-vous sûr de vouloir supprimer la connexion à", + "event": "Événement", + "successful": "Réussi", + "wouoldLikeToConnect": "je voudrais me connecter", + "message": "Message", + "do_not_have_enough_gas_asset": "Vous n'avez pas assez de ${currency} pour effectuer une transaction avec les conditions actuelles du réseau blockchain. Vous avez besoin de plus de ${currency} pour payer les frais du réseau blockchain, même si vous envoyez un actif différent.", + "totp_auth_url": "URL D'AUTORISATION TOTP", + "awaitDAppProcessing": "Veuillez attendre que l'application décentralisée (dApp) termine le traitement.", + "copyWalletConnectLink": "Copiez le lien WalletConnect depuis l'application décentralisée (dApp) et collez-le ici", + "enterWalletConnectURI": "Saisissez l'URI de WalletConnect.", + "seed_key": "Clé secrète (seed key)", + "enter_seed_phrase": "Entrez votre phrase secrète (seed)", + "change_rep_successful": "Représentant changé avec succès", + "add_contact": "Ajouter le contact", + "exchange_provider_unsupported": "${providerName} n'est plus pris en charge !", + "domain_looks_up": "Résolution de nom", + "require_for_exchanges_to_external_wallets": "Exiger pour les échanges vers des portefeuilles externes", + "seed_phrase_length": "Longueur de la phrase de départ", + "unavailable_balance": "Solde indisponible", + "unavailable_balance_description": "Solde indisponible : ce total comprend les fonds bloqués dans les transactions en attente et ceux que vous avez activement gelés dans vos paramètres de contrôle des pièces. Les soldes bloqués deviendront disponibles une fois leurs transactions respectives terminées, tandis que les soldes gelés resteront inaccessibles aux transactions jusqu'à ce que vous décidiez de les débloquer.", + "camera_permission_is_required": "L'autorisation d'accès à la caméra est requise.\nVeuillez l'activer depuis les paramètres de l'application.", + "switchToETHWallet": "Veuillez passer à un portefeuille (wallet) Ethereum et réessayer", + "unspent_change": "Changement", + "tor_connection": "Connexion Tor" } diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index bb3e495ec..0e2589b6a 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -340,7 +340,6 @@ "template": "Samfura", "confirm_delete_template": "Wannan aikin zai share wannan samfuri. Kuna so ku ci gaba?", "confirm_delete_wallet": "Wannan aikin zai share wannan walat. Kuna so ku ci gaba?", - "picker_description": "Don zaɓar ChangeNOW ko MorphToken, da farko canja kasuwancin pair din ku", "change_wallet_alert_title": "Canja walat yanzu", "change_wallet_alert_content": "Kana so ka canja walat yanzu zuwa ${wallet_name}?", "creating_new_wallet": "Haliccin walat sabuwa", @@ -603,7 +602,6 @@ "onion_link": "Lambar onion", "decimal_places_error": "Wadannan suna da tsawon harsuna", "edit_node": "Shirya Node", - "frozen_balance": "Falin kuma maɓallin", "settings": "Saiti", "sell_monero_com_alert_content": "Selling Monero bai sami ƙarshen mai bukatar samun ba", "error_text_input_below_minimum_limit": "Kudin ba a kamai", @@ -671,5 +669,49 @@ "choose_derivation": "Zaɓi walatawa", "new_first_wallet_text": "A sauƙaƙe kiyaye kuzarin ku", "select_destination": "Da fatan za a zaɓi wurin da za a yi wa madadin fayil ɗin.", - "save_to_downloads": "Ajiye zuwa Zazzagewa" + "save_to_downloads": "Ajiye zuwa Zazzagewa", + "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.", + "onramper_option_description": "Da sauri sayi Crypto tare da hanyoyin biyan kuɗi da yawa. Akwai a yawancin ƙasashe. Yaduwa da kudade sun bambanta.", + "default_buy_provider": "Tsohuwar Siyarwa", + "ask_each_time": "Tambaya kowane lokaci", + "buy_provider_unavailable": "Mai ba da kyauta a halin yanzu babu.", + "signTransaction": "Sa hannu Ma'amala", + "errorGettingCredentials": "Ba a yi nasara ba: Kuskure yayin samun takaddun shaida", + "errorSigningTransaction": "An sami kuskure yayin sanya hannu kan ciniki", + "pairingInvalidEvent": "Haɗa Lamarin mara inganci", + "chains": "Sarkoki", + "methods": "Hanyoyin", + "events": "Abubuwan da suka faru", + "reject": "Ƙi", + "approve": "Amincewa", + "expiresOn": "Yana ƙarewa", + "walletConnect": "WalletConnect", + "nullURIError": "URI banza ne", + "connectWalletPrompt": "Haɗa walat ɗin ku tare da WalletConnect don yin ma'amala", + "newConnection": "Sabuwar Haɗi", + "activeConnectionsPrompt": "Haɗin kai mai aiki zai bayyana a nan", + "deleteConnectionConfirmationPrompt": "Shin kun tabbata cewa kuna son share haɗin zuwa", + "event": "Lamarin", + "successful": "Nasara", + "wouoldLikeToConnect": "ina son haɗi", + "message": "Sako", + "do_not_have_enough_gas_asset": "Ba ku da isassun ${currency} don yin ma'amala tare da yanayin cibiyar sadarwar blockchain na yanzu. Kuna buƙatar ƙarin ${currency} don biyan kuɗaɗen cibiyar sadarwar blockchain, koda kuwa kuna aika wata kadara daban.", + "totp_auth_url": "TOTP AUTH URL", + "awaitDAppProcessing": "Da fatan za a jira dApp ya gama aiki.", + "copyWalletConnectLink": "Kwafi hanyar haɗin WalletConnect daga dApp kuma liƙa a nan", + "enterWalletConnectURI": "Shigar da WalletConnect URI", + "seed_key": "Maɓallin iri", + "enter_seed_phrase": "Shigar da Sert Sentarku", + "change_rep_successful": "An samu nasarar canzawa wakilin", + "add_contact": "Ƙara lamba", + "exchange_provider_unsupported": "${providerName}", + "domain_looks_up": "Binciken yanki", + "require_for_exchanges_to_external_wallets": "Bukatar musanya zuwa wallet na waje", + "camera_permission_is_required": "Ana buƙatar izinin kyamara.\nDa fatan za a kunna shi daga saitunan app.", + "switchToETHWallet": "Da fatan za a canza zuwa walat ɗin Ethereum kuma a sake gwadawa", + "seed_phrase_length": "Tsawon jimlar iri", + "unavailable_balance": "Ma'aunin da ba ya samuwa", + "unavailable_balance_description": "Ma'auni Babu: Wannan jimlar ya haɗa da kuɗi waɗanda ke kulle a cikin ma'amaloli da ke jiran aiki da waɗanda kuka daskare sosai a cikin saitunan sarrafa kuɗin ku. Ma'auni da aka kulle za su kasance da zarar an kammala ma'amalolinsu, yayin da daskararrun ma'auni ba za su iya samun damar yin ciniki ba har sai kun yanke shawarar cire su.", + "unspent_change": "Canza", + "tor_connection": "Tor haɗin gwiwa" } diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index a2a138aa1..19467af09 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -339,7 +339,6 @@ "template": "खाका", "confirm_delete_template": "यह क्रिया इस टेम्पलेट को हटा देगी। क्या आप जारी रखना चाहते हैं?", "confirm_delete_wallet": "यह क्रिया इस वॉलेट को हटा देगी। क्या आप जारी रखना चाहते हैं?", - "picker_description": "ChangeNOW या MorphToken चुनने के लिए, कृपया अपनी ट्रेडिंग जोड़ी को पहले बदलें", "change_wallet_alert_title": "वर्तमान बटुआ बदलें", "change_wallet_alert_content": "क्या आप करंट वॉलेट को बदलना चाहते हैं ${wallet_name}?", "creating_new_wallet": "नया बटुआ बनाना", @@ -596,7 +595,6 @@ "sweeping_wallet_alert": "इसमें अधिक समय नहीं लगना चाहिए। इस स्क्रीन को न छोड़ें या स्वैप्ट फंड खो सकते हैं", "decimal_places_error": "बहुत अधिक दशमलव स्थान", "edit_node": "नोड संपादित करें", - "frozen_balance": "जमे हुए संतुलन", "invoice_details": "चालान विवरण", "donation_link_details": "दान लिंक विवरण", "anonpay_description": "${type} उत्पन्न करें। प्राप्तकर्ता किसी भी समर्थित क्रिप्टोकरेंसी के साथ ${method} कर सकता है, और आपको इस वॉलेट में धन प्राप्त होगा।", @@ -693,5 +691,49 @@ "choose_derivation": "वॉलेट व्युत्पत्ति चुनें", "new_first_wallet_text": "आसानी से अपनी क्रिप्टोक्यूरेंसी को सुरक्षित रखें", "select_destination": "कृपया बैकअप फ़ाइल के लिए गंतव्य का चयन करें।", - "save_to_downloads": "डाउनलोड में सहेजें" + "save_to_downloads": "डाउनलोड में सहेजें", + "select_buy_provider_notice": "ऊपर एक खरीद प्रदाता का चयन करें। आप इस स्क्रीन को ऐप सेटिंग्स में अपना डिफ़ॉल्ट बाय प्रदाता सेट करके छोड़ सकते हैं।", + "onramper_option_description": "जल्दी से कई भुगतान विधियों के साथ क्रिप्टो खरीदें। अधिकांश देशों में उपलब्ध है। फैलता है और फीस अलग -अलग होती है।", + "default_buy_provider": "डिफ़ॉल्ट खरीद प्रदाता", + "ask_each_time": "हर बार पूछें", + "buy_provider_unavailable": "वर्तमान में प्रदाता अनुपलब्ध है।", + "signTransaction": "लेन-देन पर हस्ताक्षर करें", + "errorGettingCredentials": "विफल: क्रेडेंशियल प्राप्त करते समय त्रुटि", + "errorSigningTransaction": "लेन-देन पर हस्ताक्षर करते समय एक त्रुटि उत्पन्न हुई है", + "pairingInvalidEvent": "अमान्य ईवेंट युग्मित करना", + "chains": "चेन", + "methods": "तरीकों", + "events": "आयोजन", + "reject": "अस्वीकार करना", + "approve": "मंज़ूरी देना", + "expiresOn": "पर समय सीमा समाप्त", + "walletConnect": "वॉलेटकनेक्ट", + "nullURIError": "यूआरआई शून्य है", + "connectWalletPrompt": "लेन-देन करने के लिए अपने वॉलेट को वॉलेटकनेक्ट से कनेक्ट करें", + "newConnection": "नया कनेक्शन", + "activeConnectionsPrompt": "सक्रिय कनेक्शन यहां दिखाई देंगे", + "deleteConnectionConfirmationPrompt": "क्या आप वाकई कनेक्शन हटाना चाहते हैं?", + "event": "आयोजन", + "successful": "सफल", + "wouoldLikeToConnect": "जुड़ना चाहेंगे", + "message": "संदेश", + "do_not_have_enough_gas_asset": "वर्तमान ब्लॉकचेन नेटवर्क स्थितियों में लेनदेन करने के लिए आपके पास पर्याप्त ${currency} नहीं है। ब्लॉकचेन नेटवर्क शुल्क का भुगतान करने के लिए आपको अधिक ${currency} की आवश्यकता है, भले ही आप एक अलग संपत्ति भेज रहे हों।", + "totp_auth_url": "TOTP प्रामाणिक यूआरएल", + "awaitDAppProcessing": "कृपया डीएपी की प्रोसेसिंग पूरी होने तक प्रतीक्षा करें।", + "copyWalletConnectLink": "dApp से वॉलेटकनेक्ट लिंक को कॉपी करें और यहां पेस्ट करें", + "enterWalletConnectURI": "वॉलेटकनेक्ट यूआरआई दर्ज करें", + "seed_key": "बीज कुंजी", + "enter_seed_phrase": "अपना बीज वाक्यांश दर्ज करें", + "change_rep_successful": "सफलतापूर्वक बदलकर प्रतिनिधि", + "add_contact": "संपर्क जोड़ें", + "exchange_provider_unsupported": "${providerName} अब समर्थित नहीं है!", + "domain_looks_up": "डोमेन लुकअप", + "require_for_exchanges_to_external_wallets": "बाहरी वॉलेट में एक्सचेंज की आवश्यकता है", + "camera_permission_is_required": "कैमरे की अनुमति आवश्यक है.\nकृपया इसे ऐप सेटिंग से सक्षम करें।", + "switchToETHWallet": "कृपया एथेरियम वॉलेट पर स्विच करें और पुनः प्रयास करें", + "seed_phrase_length": "बीज वाक्यांश की लंबाई", + "unavailable_balance": "अनुपलब्ध शेष", + "unavailable_balance_description": "अनुपलब्ध शेष राशि: इस कुल में वे धनराशि शामिल हैं जो लंबित लेनदेन में बंद हैं और जिन्हें आपने अपनी सिक्का नियंत्रण सेटिंग्स में सक्रिय रूप से जमा कर रखा है। लॉक किए गए शेष उनके संबंधित लेन-देन पूरे होने के बाद उपलब्ध हो जाएंगे, जबकि जमे हुए शेष लेन-देन के लिए अप्राप्य रहेंगे जब तक कि आप उन्हें अनफ्रीज करने का निर्णय नहीं लेते।", + "unspent_change": "परिवर्तन", + "tor_connection": "टोर कनेक्शन" } diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index e6f0c1b14..30eea10dc 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -339,7 +339,6 @@ "template": "Predložak", "confirm_delete_template": "Ovom ćete radnjom izbrisati ovaj predložak. Želite li nastaviti?", "confirm_delete_wallet": "Ovom ćete radnjom izbrisati ovaj novčanik. Želite li nastaviti?", - "picker_description": "Da biste odabrali ChangeNOW ili MorphToken, molimo da prvo odabete dvije valute za trgovanje", "change_wallet_alert_title": "Izmijeni trenutni novčanik", "change_wallet_alert_content": "Želite li promijeniti trenutni novčanik u ${wallet_name}?", "creating_new_wallet": "Stvaranje novog novčanika", @@ -596,7 +595,6 @@ "sweeping_wallet_alert": "Ovo ne bi trebalo dugo trajati. NE NAPUŠTAJTE OVAJ ZASLON INAČE SE POBREŠENA SREDSTVA MOGU IZGUBITI", "decimal_places_error": "Previše decimalnih mjesta", "edit_node": "Uredi čvor", - "frozen_balance": "Zamrznuti saldo", "invoice_details": "Podaci o fakturi", "donation_link_details": "Detalji veza za donacije", "anonpay_description": "Generiraj ${type}. Primatelj može ${method} s bilo kojom podržanom kriptovalutom, a vi ćete primiti sredstva u ovaj novčanik.", @@ -691,5 +689,49 @@ "choose_derivation": "Odaberite izvedbu novčanika", "new_first_wallet_text": "Jednostavno čuvajte svoju kripto valutu", "select_destination": "Odaberite odredište za datoteku sigurnosne kopije.", - "save_to_downloads": "Spremi u Preuzimanja" + "save_to_downloads": "Spremi u Preuzimanja", + "select_buy_provider_notice": "Odaberite gornji davatelj kupnje. Ovaj zaslon možete preskočiti postavljanjem zadanog davatelja usluga kupnje u postavkama aplikacija.", + "onramper_option_description": "Brzo kupite kriptovalute s mnogim načinima plaćanja. Dostupno u većini zemalja. Širenja i naknade variraju.", + "default_buy_provider": "Zadani davatelj kupnje", + "ask_each_time": "Pitajte svaki put", + "buy_provider_unavailable": "Davatelj trenutno nije dostupan.", + "signTransaction": "Potpišite transakciju", + "errorGettingCredentials": "Neuspješno: Pogreška prilikom dobivanja vjerodajnica", + "errorSigningTransaction": "Došlo je do pogreške prilikom potpisivanja transakcije", + "pairingInvalidEvent": "Nevažeći događaj uparivanja", + "chains": "Lanci", + "methods": "Metode", + "events": "Događaji", + "reject": "Odbiti", + "approve": "Odobriti", + "expiresOn": "Istječe", + "walletConnect": "WalletConnect", + "nullURIError": "URI je nula", + "connectWalletPrompt": "Povežite svoj novčanik s WalletConnectom za obavljanje transakcija", + "newConnection": "Nova veza", + "activeConnectionsPrompt": "Ovdje će se pojaviti aktivne veze", + "deleteConnectionConfirmationPrompt": "Jeste li sigurni da želite izbrisati vezu s", + "event": "Događaj", + "successful": "Uspješno", + "wouoldLikeToConnect": "želio bi se povezati", + "message": "Poruka", + "do_not_have_enough_gas_asset": "Nemate dovoljno ${currency} da izvršite transakciju s trenutačnim uvjetima blockchain mreže. Trebate više ${currency} da platite naknade za blockchain mrežu, čak i ako šaljete drugu imovinu.", + "totp_auth_url": "TOTP AUTH URL", + "awaitDAppProcessing": "Molimo pričekajte da dApp završi obradu.", + "copyWalletConnectLink": "Kopirajte vezu WalletConnect iz dApp-a i zalijepite je ovdje", + "enterWalletConnectURI": "Unesite WalletConnect URI", + "seed_key": "Sjemenski ključ", + "enter_seed_phrase": "Unesite svoju sjemensku frazu", + "change_rep_successful": "Uspješno promijenjena reprezentativna", + "add_contact": "Dodaj kontakt", + "exchange_provider_unsupported": "${providerName} više nije podržan!", + "domain_looks_up": "Pretraga domena", + "require_for_exchanges_to_external_wallets": "Zahtijeva razmjene na vanjske novčanike", + "camera_permission_is_required": "Potrebno je dopuštenje kamere.\nOmogućite ga u postavkama aplikacije.", + "switchToETHWallet": "Prijeđite na Ethereum novčanik i pokušajte ponovno", + "seed_phrase_length": "Duljina početne fraze", + "unavailable_balance": "Nedostupno stanje", + "unavailable_balance_description": "Nedostupno stanje: Ovaj ukupni iznos uključuje sredstva koja su zaključana u transakcijama na čekanju i ona koja ste aktivno zamrznuli u postavkama kontrole novčića. Zaključani saldi postat će dostupni kada se dovrše njihove transakcije, dok zamrznuti saldi ostaju nedostupni za transakcije sve dok ih ne odlučite odmrznuti.", + "unspent_change": "Promijeniti", + "tor_connection": "Tor veza" } diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index b41881488..c10feffbc 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -340,7 +340,6 @@ "template": "Template", "confirm_delete_template": "Tindakan ini akan menghapus template ini. Apakah Anda ingin melanjutkan?", "confirm_delete_wallet": "Tindakan ini akan menghapus dompet ini. Apakah Anda ingin melanjutkan?", - "picker_description": "Untuk memilih ChangeNOW atau MorphToken, silakan ubah pasangan perdagangan Anda terlebih dahulu", "change_wallet_alert_title": "Ganti dompet saat ini", "change_wallet_alert_content": "Apakah Anda ingin mengganti dompet saat ini ke ${wallet_name}?", "creating_new_wallet": "Membuat dompet baru", @@ -585,7 +584,6 @@ "contact_list_wallets": "Dompet Saya", "decimal_places_error": "Terlalu banyak tempat desimal", "edit_node": "Sunting Node", - "frozen_balance": "Saldo Beku", "invoice_details": "Detail faktur", "donation_link_details": "Detail tautan donasi", "anonpay_description": "Hasilkan ${type}. Penerima dapat ${method} dengan cryptocurrency apa pun yang didukung, dan Anda akan menerima dana di dompet ini.", @@ -681,5 +679,49 @@ "choose_derivation": "Pilih dompet dompet", "new_first_wallet_text": "Dengan mudah menjaga cryptocurrency Anda aman", "select_destination": "Silakan pilih tujuan untuk file cadangan.", - "save_to_downloads": "Simpan ke Unduhan" -} \ No newline at end of file + "save_to_downloads": "Simpan ke Unduhan", + "select_buy_provider_notice": "Pilih penyedia beli di atas. Anda dapat melewatkan layar ini dengan mengatur penyedia pembelian default Anda di pengaturan aplikasi.", + "onramper_option_description": "Beli crypto dengan cepat dengan banyak metode pembayaran. Tersedia di sebagian besar negara. Spread dan biaya bervariasi.", + "default_buy_provider": "Penyedia beli default", + "ask_each_time": "Tanyakan setiap kali", + "buy_provider_unavailable": "Penyedia saat ini tidak tersedia.", + "signTransaction": "Tandatangani Transaksi", + "errorGettingCredentials": "Gagal: Terjadi kesalahan saat mendapatkan kredensial", + "errorSigningTransaction": "Terjadi kesalahan saat menandatangani transaksi", + "pairingInvalidEvent": "Menyandingkan Acara Tidak Valid", + "chains": "Rantai", + "methods": "Metode", + "events": "Acara", + "reject": "Menolak", + "approve": "Menyetujui", + "expiresOn": "Kadaluarsa pada", + "walletConnect": "DompetConnect", + "nullURIError": "URI adalah nol", + "connectWalletPrompt": "Hubungkan dompet Anda dengan WalletConnect untuk melakukan transaksi", + "newConnection": "Koneksi Baru", + "activeConnectionsPrompt": "Koneksi aktif akan muncul di sini", + "deleteConnectionConfirmationPrompt": "Apakah Anda yakin ingin menghapus koneksi ke", + "event": "Peristiwa", + "successful": "Berhasil", + "wouoldLikeToConnect": "ingin terhubung", + "message": "Pesan", + "do_not_have_enough_gas_asset": "Anda tidak memiliki cukup ${currency} untuk melakukan transaksi dengan kondisi jaringan blockchain saat ini. Anda memerlukan lebih banyak ${currency} untuk membayar biaya jaringan blockchain, meskipun Anda mengirimkan aset yang berbeda.", + "totp_auth_url": "URL Otentikasi TOTP", + "awaitDAppProcessing": "Mohon tunggu hingga dApp menyelesaikan pemrosesan.", + "copyWalletConnectLink": "Salin tautan WalletConnect dari dApp dan tempel di sini", + "enterWalletConnectURI": "Masukkan URI WalletConnect", + "seed_key": "Kunci benih", + "enter_seed_phrase": "Masukkan frasa benih Anda", + "change_rep_successful": "Berhasil mengubah perwakilan", + "add_contact": "Tambah kontak", + "exchange_provider_unsupported": "${providerName} tidak lagi didukung!", + "domain_looks_up": "Pencarian domain", + "require_for_exchanges_to_external_wallets": "Memerlukan pertukaran ke dompet eksternal", + "camera_permission_is_required": "Izin kamera diperlukan.\nSilakan aktifkan dari pengaturan aplikasi.", + "switchToETHWallet": "Silakan beralih ke dompet Ethereum dan coba lagi", + "seed_phrase_length": "Panjang frase benih", + "unavailable_balance": "Saldo tidak tersedia", + "unavailable_balance_description": "Saldo Tidak Tersedia: Total ini termasuk dana yang terkunci dalam transaksi yang tertunda dan dana yang telah Anda bekukan secara aktif di pengaturan kontrol koin Anda. Saldo yang terkunci akan tersedia setelah transaksi masing-masing selesai, sedangkan saldo yang dibekukan tetap tidak dapat diakses untuk transaksi sampai Anda memutuskan untuk mencairkannya.", + "unspent_change": "Mengubah", + "tor_connection": "koneksi Tor" +} diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index f356fe9e1..a13930666 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -339,7 +339,6 @@ "template": "Modello", "confirm_delete_template": "Questa azione cancellerà questo modello. Desideri continuare?", "confirm_delete_wallet": "Questa azione cancellerà questo portafoglio. Desideri continuare?", - "picker_description": "Per scegliere ChangeNOW o MorphToken, gentilmente cambia prima la tua coppia di valute", "change_wallet_alert_title": "Cambia portafoglio attuale", "change_wallet_alert_content": "Sei sicuro di voler cambiare il portafoglio attuale con ${wallet_name}?", "creating_new_wallet": "Creazione nuovo portafoglio", @@ -596,7 +595,6 @@ "sweeping_wallet_alert": "Questo non dovrebbe richiedere molto tempo. NON LASCIARE QUESTA SCHERMATA O I FONDI SPAZZATI POTREBBERO ANDARE PERSI", "decimal_places_error": "Troppe cifre decimali", "edit_node": "Modifica nodo", - "frozen_balance": "Equilibrio congelato", "invoice_details": "Dettagli della fattura", "donation_link_details": "Dettagli del collegamento alla donazione", "anonpay_description": "Genera ${type}. Il destinatario può ${method} con qualsiasi criptovaluta supportata e riceverai fondi in questo portafoglio.", @@ -693,5 +691,49 @@ "choose_derivation": "Scegli la derivazione del portafoglio", "new_first_wallet_text": "Mantieni facilmente la tua criptovaluta al sicuro", "select_destination": "Seleziona la destinazione per il file di backup.", - "save_to_downloads": "Salva in Download" + "save_to_downloads": "Salva in Download", + "select_buy_provider_notice": "Seleziona un fornitore di acquisto sopra. È possibile saltare questa schermata impostando il provider di acquisto predefinito nelle impostazioni dell'app.", + "onramper_option_description": "Acquista rapidamente la criptovaluta con molti metodi di pagamento. Disponibile nella maggior parte dei paesi. Gli spread e le commissioni variano.", + "default_buy_provider": "Provider di acquisto predefinito", + "ask_each_time": "Chiedi ogni volta", + "buy_provider_unavailable": "Provider attualmente non disponibile.", + "signTransaction": "Firma la transazione", + "errorGettingCredentials": "Non riuscito: errore durante il recupero delle credenziali", + "errorSigningTransaction": "Si è verificato un errore durante la firma della transazione", + "pairingInvalidEvent": "Associazione evento non valido", + "chains": "Catene", + "methods": "Metodi", + "events": "Eventi", + "reject": "Rifiutare", + "approve": "Approvare", + "expiresOn": "Scade il", + "walletConnect": "PortafoglioConnetti", + "nullURIError": "L'URI è nullo", + "connectWalletPrompt": "Collega il tuo portafoglio con WalletConnect per effettuare transazioni", + "newConnection": "Nuova connessione", + "activeConnectionsPrompt": "Le connessioni attive verranno visualizzate qui", + "deleteConnectionConfirmationPrompt": "Sei sicuro di voler eliminare la connessione a", + "event": "Evento", + "successful": "Riuscito", + "wouoldLikeToConnect": "vorrei connettermi", + "message": "Messaggio", + "do_not_have_enough_gas_asset": "Non hai abbastanza ${currency} per effettuare una transazione con le attuali condizioni della rete blockchain. Hai bisogno di più ${currency} per pagare le commissioni della rete blockchain, anche se stai inviando una risorsa diversa.", + "totp_auth_url": "URL DI AUT. TOTP", + "awaitDAppProcessing": "Attendi gentilmente che la dApp termini l'elaborazione.", + "copyWalletConnectLink": "Copia il collegamento WalletConnect dalla dApp e incollalo qui", + "enterWalletConnectURI": "Inserisci l'URI di WalletConnect", + "seed_key": "Chiave di semi", + "enter_seed_phrase": "Inserisci la tua frase di semi", + "change_rep_successful": "Rappresentante modificato con successo", + "add_contact": "Aggiungi contatto", + "exchange_provider_unsupported": "${providerName} non è più supportato!", + "domain_looks_up": "Ricerche di domini", + "require_for_exchanges_to_external_wallets": "Richiede scambi con portafogli esterni", + "camera_permission_is_required": "È richiesta l'autorizzazione della fotocamera.\nAbilitalo dalle impostazioni dell'app.", + "switchToETHWallet": "Passa a un portafoglio Ethereum e riprova", + "seed_phrase_length": "Lunghezza della frase seed", + "unavailable_balance": "Saldo non disponibile", + "unavailable_balance_description": "Saldo non disponibile: questo totale include i fondi bloccati nelle transazioni in sospeso e quelli che hai congelato attivamente nelle impostazioni di controllo delle monete. I saldi bloccati diventeranno disponibili una volta completate le rispettive transazioni, mentre i saldi congelati rimarranno inaccessibili per le transazioni finché non deciderai di sbloccarli.", + "unspent_change": "Modifica", + "tor_connection": "Connessione Tor" } diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index e9a610d15..3e61dbd9d 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -339,7 +339,6 @@ "template": "テンプレート", "confirm_delete_template": "この操作により、このテンプレートが削除されます。 続行しますか?", "confirm_delete_wallet": "このアクションにより、このウォレットが削除されます。 続行しますか?", - "picker_description": "ChangeNOWまたはMorphTokenを選択するには、最初にトレーディングペアを変更してください", "change_wallet_alert_title": "現在のウォレットを変更する", "change_wallet_alert_content": "現在のウォレットをに変更しますか ${wallet_name}?", "creating_new_wallet": "新しいウォレットの作成", @@ -596,7 +595,6 @@ "sweeping_wallet_alert": "これには時間がかかりません。この画面から離れないでください。そうしないと、スイープ ファンドが失われる可能性があります", "decimal_places_error": "小数点以下の桁数が多すぎる", "edit_node": "ノードを編集", - "frozen_balance": "冷凍残高", "invoice_details": "請求の詳細", "donation_link_details": "寄付リンクの詳細", "anonpay_description": "${type} を生成します。受取人はサポートされている任意の暗号通貨で ${method} でき、あなたはこのウォレットで資金を受け取ります。", @@ -692,6 +690,50 @@ "choose_derivation": "ウォレット派生を選択します", "new_first_wallet_text": "暗号通貨を簡単に安全に保ちます", "select_destination": "バックアップファイルの保存先を選択してください。", + "auto_generate_subaddresses": "Autoはサブアドレスを生成します", "save_to_downloads": "ダウンロードに保存", - "auto_generate_subaddresses": "Autoはサブアドレスを生成します" + "select_buy_provider_notice": "上記の購入プロバイダーを選択してください。デフォルトの購入プロバイダーをアプリ設定で設定して、この画面をスキップできます。", + "onramper_option_description": "多くの支払い方法で暗号をすばやく購入してください。ほとんどの国で利用可能です。スプレッドと料金は異なります。", + "default_buy_provider": "デフォルトの購入プロバイダー", + "ask_each_time": "毎回尋ねてください", + "buy_provider_unavailable": "現在、プロバイダーは利用できません。", + "signTransaction": "トランザクションに署名する", + "errorGettingCredentials": "失敗: 認証情報の取得中にエラーが発生しました", + "errorSigningTransaction": "トランザクションの署名中にエラーが発生しました", + "pairingInvalidEvent": "ペアリング無効イベント", + "chains": "チェーン", + "methods": "メソッド", + "events": "イベント", + "reject": "拒否する", + "approve": "承認する", + "expiresOn": "有効期限は次のとおりです", + "walletConnect": "ウォレットコネクト", + "nullURIError": "URIがnullです", + "connectWalletPrompt": "ウォレットを WalletConnect に接続して取引を行う", + "newConnection": "新しい接続", + "activeConnectionsPrompt": "アクティブな接続がここに表示されます", + "deleteConnectionConfirmationPrompt": "への接続を削除してもよろしいですか?", + "event": "イベント", + "successful": "成功", + "wouoldLikeToConnect": "接続したいです", + "message": "メッセージ", + "do_not_have_enough_gas_asset": "現在のブロックチェーン ネットワークの状況では、トランザクションを行うのに十分な ${currency} がありません。別のアセットを送信する場合でも、ブロックチェーン ネットワーク料金を支払うにはさらに ${currency} が必要です。", + "totp_auth_url": "TOTP認証URL", + "awaitDAppProcessing": "dAppの処理が完了するまでお待ちください。", + "copyWalletConnectLink": "dApp から WalletConnect リンクをコピーし、ここに貼り付けます", + "enterWalletConnectURI": "WalletConnect URI を入力してください", + "seed_key": "シードキー", + "enter_seed_phrase": "シードフレーズを入力してください", + "change_rep_successful": "代表者の変更に成功しました", + "add_contact": "連絡先を追加", + "exchange_provider_unsupported": "${providerName}はサポートされなくなりました!", + "domain_looks_up": "ドメイン検索", + "require_for_exchanges_to_external_wallets": "外部ウォレットへの交換に必要", + "camera_permission_is_required": "カメラの許可が必要です。\nアプリの設定から有効にしてください。", + "switchToETHWallet": "イーサリアムウォレットに切り替えてもう一度お試しください", + "seed_phrase_length": "シードフレーズの長さ", + "unavailable_balance": "利用できない残高", + "unavailable_balance_description": "利用不可能な残高: この合計には、保留中のトランザクションにロックされている資金と、コイン管理設定でアクティブに凍結した資金が含まれます。ロックされた残高は、それぞれの取引が完了すると利用可能になりますが、凍結された残高は、凍結を解除するまで取引にアクセスできません。", + "unspent_change": "変化", + "tor_connection": "Tor接続" } diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index 8f0b9ee0a..de09c65d1 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -339,7 +339,6 @@ "template": "주형", "confirm_delete_template": "이 작업은이 템플릿을 삭제합니다. 계속 하시겠습니까?", "confirm_delete_wallet": "이 작업은이 지갑을 삭제합니다. 계속 하시겠습니까?", - "picker_description": "ChangeNOW 또는 MorphToken을 선택하려면 먼저 거래 쌍을 변경하십시오.", "change_wallet_alert_title": "현재 지갑 변경", "change_wallet_alert_content": "현재 지갑을 다음으로 변경 하시겠습니까 ${wallet_name}?", "creating_new_wallet": "새 지갑 생성", @@ -596,7 +595,6 @@ "sweeping_wallet_alert": "오래 걸리지 않습니다. 이 화면을 떠나지 마십시오. 그렇지 않으면 스웹트 자금이 손실될 수 있습니다.", "decimal_places_error": "소수점 이하 자릿수가 너무 많습니다.", "edit_node": "노드 편집", - "frozen_balance": "얼어붙은 균형", "invoice_details": "인보이스 세부정보", "donation_link_details": "기부 링크 세부정보", "anonpay_description": "${type} 생성. 수신자는 지원되는 모든 암호화폐로 ${method}할 수 있으며 이 지갑에서 자금을 받게 됩니다.", @@ -690,6 +688,50 @@ "choose_derivation": "지갑 파생을 선택하십시오", "new_first_wallet_text": "cryptocurrency를 쉽게 안전하게 유지하십시오", "select_destination": "백업 파일의 대상을 선택하십시오.", + "auto_generate_subaddresses": "자동 생성 서브 아드 드레스", "save_to_downloads": "다운로드에 저장", - "auto_generate_subaddresses": "자동 생성 서브 아드 드레스" + "select_buy_provider_notice": "위의 구매 제공자를 선택하십시오. 앱 설정에서 기본 구매 제공자를 설정 하여이 화면을 건너 뛸 수 있습니다.", + "onramper_option_description": "많은 결제 방법으로 암호화를 신속하게 구입하십시오. 대부분의 국가에서 사용할 수 있습니다. 스프레드와 수수료는 다양합니다.", + "default_buy_provider": "기본 구매 제공자", + "ask_each_time": "매번 물어보십시오", + "buy_provider_unavailable": "제공자는 현재 사용할 수 없습니다.", + "signTransaction": "거래 서명", + "errorGettingCredentials": "실패: 자격 증명을 가져오는 중 오류가 발생했습니다.", + "errorSigningTransaction": "거래에 서명하는 동안 오류가 발생했습니다.", + "pairingInvalidEvent": "잘못된 이벤트 페어링", + "chains": "쇠사슬", + "methods": "행동 양식", + "events": "이벤트", + "reject": "거부하다", + "approve": "승인하다", + "expiresOn": "만료 날짜", + "walletConnect": "월렛커넥트", + "nullURIError": "URI가 null입니다.", + "connectWalletPrompt": "거래를 하려면 WalletConnect에 지갑을 연결하세요.", + "newConnection": "새로운 연결", + "activeConnectionsPrompt": "활성 연결이 여기에 표시됩니다", + "deleteConnectionConfirmationPrompt": "다음 연결을 삭제하시겠습니까?", + "event": "이벤트", + "successful": "성공적인", + "wouoldLikeToConnect": "연결하고 싶습니다", + "message": "메시지", + "do_not_have_enough_gas_asset": "현재 블록체인 네트워크 조건으로 거래를 하기에는 ${currency}이(가) 충분하지 않습니다. 다른 자산을 보내더라도 블록체인 네트워크 수수료를 지불하려면 ${currency}가 더 필요합니다.", + "totp_auth_url": "TOTP 인증 URL", + "awaitDAppProcessing": "dApp이 처리를 마칠 때까지 기다려주세요.", + "copyWalletConnectLink": "dApp에서 WalletConnect 링크를 복사하여 여기에 붙여넣으세요.", + "enterWalletConnectURI": "WalletConnect URI를 입력하세요.", + "seed_key": "시드 키", + "enter_seed_phrase": "시드 문구를 입력하십시오", + "change_rep_successful": "대리인이 성공적으로 변경되었습니다", + "add_contact": "주소록에 추가", + "exchange_provider_unsupported": "${providerName}은 더 이상 지원되지 않습니다!", + "domain_looks_up": "도메인 조회", + "require_for_exchanges_to_external_wallets": "외부 지갑으로의 교환을 위해 필요", + "camera_permission_is_required": "카메라 권한이 필요합니다.\n앱 설정에서 활성화해 주세요.", + "switchToETHWallet": "이더리움 지갑으로 전환한 후 다시 시도해 주세요.", + "seed_phrase_length": "시드 문구 길이", + "unavailable_balance": "사용할 수 없는 잔액", + "unavailable_balance_description": "사용할 수 없는 잔액: 이 총계에는 보류 중인 거래에 잠겨 있는 자금과 코인 관리 설정에서 적극적으로 동결된 자금이 포함됩니다. 잠긴 잔액은 해당 거래가 완료되면 사용할 수 있게 되며, 동결된 잔액은 동결을 해제하기 전까지 거래에 액세스할 수 없습니다.", + "unspent_change": "변화", + "tor_connection": "토르 연결" } diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index f0a97d740..1853c0eff 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -339,7 +339,6 @@ "template": "ပုံစံခွက်", "confirm_delete_template": "ဤလုပ်ဆောင်ချက်သည် ဤပုံစံပြားကို ဖျက်လိုက်ပါမည်။ ဆက်လုပ်လိုပါသလား။", "confirm_delete_wallet": "ဤလုပ်ဆောင်ချက်သည် ဤပိုက်ဆံအိတ်ကို ဖျက်လိုက်ပါမည်။ ဆက်လုပ်လိုပါသလား။", - "picker_description": "ChangeNOW သို့မဟုတ် MorphToken ကိုရွေးချယ်ရန်၊ ကျေးဇူးပြု၍ သင်၏ကုန်သွယ်မှုအတွဲကို ဦးစွာပြောင်းလဲပါ။", "change_wallet_alert_title": "လက်ရှိပိုက်ဆံအိတ်ကို ပြောင်းပါ။", "change_wallet_alert_content": "လက်ရှိပိုက်ဆံအိတ်ကို ${wallet_name} သို့ ပြောင်းလိုပါသလား။", "creating_new_wallet": "ပိုက်ဆံအိတ်အသစ်ဖန်တီးခြင်း။", @@ -594,7 +593,6 @@ "sweeping_wallet_alert": "ဒါက ကြာကြာမခံသင့်ပါဘူး။ ဤစခရင်ကို ချန်မထားပါနှင့် သို့မဟုတ် ထုတ်ယူထားသော ရန်ပုံငွေများ ဆုံးရှုံးနိုင်သည်", "decimal_places_error": "ဒဿမနေရာများ များလွန်းသည်။", "edit_node": "Node ကို တည်းဖြတ်ပါ။", - "frozen_balance": "ေးခဲမှူ", "invoice_details": "ပြေစာအသေးစိတ်", "donation_link_details": "လှူဒါန်းရန်လင့်ခ်အသေးစိတ်", "anonpay_description": "${type} ကို ဖန်တီးပါ။ လက်ခံသူက ${method} ကို ပံ့ပိုးပေးထားသည့် cryptocurrency တစ်ခုခုဖြင့် လုပ်ဆောင်နိုင်ပြီး၊ သင်သည် ဤပိုက်ဆံအိတ်တွင် ရံပုံငွေများ ရရှိမည်ဖြစ်သည်။", @@ -690,6 +688,50 @@ "choose_derivation": "ပိုက်ဆံအိတ်ကိုရွေးချယ်ပါ", "new_first_wallet_text": "သင့်ရဲ့ cryptocurrencrencres ကိုအလွယ်တကူလုံခြုံစွာထားရှိပါ", "select_destination": "အရန်ဖိုင်အတွက် ဦးတည်ရာကို ရွေးပါ။", + "auto_generate_subaddresses": "အော်တို Generate Subaddresses", "save_to_downloads": "ဒေါင်းလုဒ်များထံ သိမ်းဆည်းပါ။", - "auto_generate_subaddresses": "အော်တို Generate Subaddresses" + "select_buy_provider_notice": "အပေါ်ကဝယ်သူတစ် ဦး ကိုရွေးချယ်ပါ။ သင်၏ default 0 ယ်သူအား app settings တွင် setting လုပ်ခြင်းဖြင့်ဤ screen ကိုကျော်သွားနိုင်သည်။", + "onramper_option_description": "ငွေပေးချေမှုနည်းလမ်းများစွာဖြင့် Crypto ကိုလျင်မြန်စွာ 0 ယ်ပါ။ နိုင်ငံအများစုတွင်ရရှိနိုင်ပါသည်။ ဖြန့်ဖြူးနှင့်အခကြေးငွေကွဲပြားခြားနားသည်။", + "default_buy_provider": "Default Provider ကိုဝယ်ပါ", + "ask_each_time": "တစ်ခုချင်းစီကိုအချိန်မေးပါ", + "buy_provider_unavailable": "လက်ရှိတွင်လက်ရှိမရနိုင်ပါ။", + "signTransaction": "ငွေလွှဲဝင်ပါ။", + "errorGettingCredentials": "မအောင်မြင်ပါ- အထောက်အထားများ ရယူနေစဉ် အမှားအယွင်း", + "errorSigningTransaction": "ငွေပေးငွေယူ လက်မှတ်ထိုးစဉ် အမှားအယွင်းတစ်ခု ဖြစ်ပေါ်ခဲ့သည်။", + "pairingInvalidEvent": "မမှန်ကန်သောဖြစ်ရပ်ကို တွဲချိတ်ခြင်း။", + "chains": "ဆွဲကြိုး", + "methods": "နည်းလမ်းများ", + "events": "အဲ့ဒါနဲ့", + "reject": "ငြင်းပယ်ပါ။", + "approve": "လက်မခံပါ။", + "expiresOn": "သက်တမ်းကုန်သည်။", + "walletConnect": "Wallet ချိတ်ဆက်မှု", + "nullURIError": "URI သည် null ဖြစ်သည်။", + "connectWalletPrompt": "အရောင်းအဝယ်ပြုလုပ်ရန် သင့်ပိုက်ဆံအိတ်ကို WalletConnect နှင့် ချိတ်ဆက်ပါ။", + "newConnection": "ချိတ်ဆက်မှုအသစ်", + "activeConnectionsPrompt": "လက်ရှိချိတ်ဆက်မှုများ ဤနေရာတွင် ပေါ်လာပါမည်။", + "deleteConnectionConfirmationPrompt": "ချိတ်ဆက်မှုကို ဖျက်လိုသည်မှာ သေချာပါသလား။", + "event": "ပွဲ", + "successful": "အောင်မြင်တယ်။", + "wouoldLikeToConnect": "ချိတ်ဆက်ချင်ပါတယ်။", + "message": "မက်ဆေ့ချ်", + "do_not_have_enough_gas_asset": "လက်ရှိ blockchain ကွန်ရက်အခြေအနေများနှင့် အရောင်းအဝယ်ပြုလုပ်ရန် သင့်တွင် ${currency} လုံလောက်မှုမရှိပါ။ သင်သည် မတူညီသော ပိုင်ဆိုင်မှုတစ်ခုကို ပေးပို့နေသော်လည်း blockchain ကွန်ရက်အခကြေးငွေကို ပေးဆောင်ရန် သင်သည် နောက်ထပ် ${currency} လိုအပ်ပါသည်။", + "totp_auth_url": "TOTP AUTH URL", + "awaitDAppProcessing": "ကျေးဇူးပြု၍ dApp ကို စီမံလုပ်ဆောင်ခြင်း အပြီးသတ်ရန် စောင့်ပါ။", + "copyWalletConnectLink": "dApp မှ WalletConnect လင့်ခ်ကို ကူးယူပြီး ဤနေရာတွင် ကူးထည့်ပါ။", + "enterWalletConnectURI": "WalletConnect URI ကိုရိုက်ထည့်ပါ။", + "seed_key": "မျိုးစေ့သော့", + "enter_seed_phrase": "သင့်ရဲ့မျိုးစေ့စကားစုကိုရိုက်ထည့်ပါ", + "change_rep_successful": "အောင်မြင်စွာကိုယ်စားလှယ်ပြောင်းလဲသွားတယ်", + "add_contact": "အဆက်အသွယ်ထည့်ပါ။", + "exchange_provider_unsupported": "${providerName} မရှိတော့ပါ!", + "domain_looks_up": "ဒိုမိန်းရှာဖွေမှုများ", + "require_for_exchanges_to_external_wallets": "ပြင်ပပိုက်ဆံအိတ်များသို့ လဲလှယ်ရန် လိုအပ်သည်။", + "camera_permission_is_required": "ကင်မရာခွင့်ပြုချက် လိုအပ်ပါသည်။\nအက်ပ်ဆက်တင်များမှ ၎င်းကိုဖွင့်ပါ။", + "switchToETHWallet": "ကျေးဇူးပြု၍ Ethereum ပိုက်ဆံအိတ်သို့ ပြောင်းပြီး ထပ်စမ်းကြည့်ပါ။", + "seed_phrase_length": "မျိုးစေ့စာပိုဒ်တိုအရှည်", + "unavailable_balance": "လက်ကျန်ငွေ မရရှိနိုင်ပါ။", + "unavailable_balance_description": "မရရှိနိုင်သော လက်ကျန်ငွေ- ဤစုစုပေါင်းတွင် ဆိုင်းငံ့ထားသော ငွေပေးငွေယူများတွင် သော့ခတ်ထားသော ငွေကြေးများနှင့် သင်၏ coin ထိန်းချုပ်မှုဆက်တင်များတွင် သင် တက်ကြွစွာ အေးခဲထားသော ငွေများ ပါဝင်သည်။ သော့ခတ်ထားသော လက်ကျန်ငွေများကို ၎င်းတို့၏ သက်ဆိုင်ရာ ငွေပေးငွေယူများ ပြီးမြောက်သည်နှင့် တပြိုင်နက် ရရှိနိုင်မည်ဖြစ်ပြီး၊ အေးခဲထားသော လက်ကျန်များကို ၎င်းတို့အား ပြန်ဖြုတ်ရန် သင်ဆုံးဖြတ်သည်အထိ ငွေပေးငွေယူများအတွက် ဆက်လက်၍မရနိုင်ပါ။", + "unspent_change": "ပေြာင်းလဲခြင်း", + "tor_connection": "Tor ချိတ်ဆက်မှု" } diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 72da05311..a497c20ab 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -339,7 +339,6 @@ "template": "Sjabloon", "confirm_delete_template": "Met deze actie wordt deze sjabloon verwijderd. Wilt u doorgaan?", "confirm_delete_wallet": "Met deze actie wordt deze portemonnee verwijderd. Wilt u doorgaan?", - "picker_description": "Om ChangeNOW of MorphToken te kiezen, moet u eerst uw handelspaar wijzigen", "change_wallet_alert_title": "Wijzig huidige portemonnee", "change_wallet_alert_content": "Wilt u de huidige portemonnee wijzigen in ${wallet_name}?", "creating_new_wallet": "Nieuwe portemonnee aanmaken", @@ -596,7 +595,6 @@ "sweeping_wallet_alert": "Dit duurt niet lang. VERLAAT DIT SCHERM NIET, ANDERS KAN HET SWEPT-GELD VERLOREN WORDEN", "decimal_places_error": "Te veel decimalen", "edit_node": "Knooppunt bewerken", - "frozen_balance": "Bevroren saldo", "invoice_details": "Factuurgegevens", "donation_link_details": "Details van de donatielink", "anonpay_description": "Genereer ${type}. De ontvanger kan ${method} gebruiken met elke ondersteunde cryptocurrency en u ontvangt geld in deze portemonnee", @@ -693,5 +691,49 @@ "choose_derivation": "Kies portemonnee -afleiding", "new_first_wallet_text": "Houd uw cryptocurrency gemakkelijk veilig", "select_destination": "Selecteer de bestemming voor het back-upbestand.", - "save_to_downloads": "Opslaan in downloads" + "save_to_downloads": "Opslaan in downloads", + "select_buy_provider_notice": "Selecteer hierboven een koopprovider. U kunt dit scherm overslaan door uw standaard kopenprovider in te stellen in app -instellingen.", + "onramper_option_description": "Koop snel crypto met veel betaalmethoden. Beschikbaar in de meeste landen. Spreads en vergoedingen variëren.", + "default_buy_provider": "Standaard Koopprovider", + "ask_each_time": "Vraag het elke keer", + "buy_provider_unavailable": "Provider momenteel niet beschikbaar.", + "signTransaction": "Transactie ondertekenen", + "errorGettingCredentials": "Mislukt: fout bij het ophalen van inloggegevens", + "errorSigningTransaction": "Er is een fout opgetreden tijdens het ondertekenen van de transactie", + "pairingInvalidEvent": "Koppelen Ongeldige gebeurtenis", + "chains": "Ketens", + "methods": "Methoden", + "events": "Evenementen", + "reject": "Afwijzen", + "approve": "Goedkeuren", + "expiresOn": "Verloopt op", + "walletConnect": "WalletConnect", + "nullURIError": "URI is nul", + "connectWalletPrompt": "Verbind uw portemonnee met WalletConnect om transacties uit te voeren", + "newConnection": "Nieuwe verbinding", + "activeConnectionsPrompt": "Actieve verbindingen worden hier weergegeven", + "deleteConnectionConfirmationPrompt": "Weet u zeker dat u de verbinding met", + "event": "Evenement", + "successful": "Succesvol", + "wouoldLikeToConnect": "wil graag verbinden", + "message": "Bericht", + "do_not_have_enough_gas_asset": "U heeft niet genoeg ${currency} om een transactie uit te voeren met de huidige blockchain-netwerkomstandigheden. U heeft meer ${currency} nodig om blockchain-netwerkkosten te betalen, zelfs als u een ander item verzendt.", + "totp_auth_url": "TOTP AUTH-URL", + "awaitDAppProcessing": "Wacht tot de dApp klaar is met verwerken.", + "copyWalletConnectLink": "Kopieer de WalletConnect-link van dApp en plak deze hier", + "enterWalletConnectURI": "Voer WalletConnect-URI in", + "seed_key": "Zaadsleutel", + "enter_seed_phrase": "Voer uw zaadzin in", + "change_rep_successful": "Met succes veranderde vertegenwoordiger", + "add_contact": "Contactpersoon toevoegen", + "exchange_provider_unsupported": "${providerName} wordt niet langer ondersteund!", + "domain_looks_up": "Domein opzoeken", + "require_for_exchanges_to_external_wallets": "Vereist voor uitwisselingen naar externe portemonnees", + "camera_permission_is_required": "Cameratoestemming is vereist.\nSchakel dit in via de app-instellingen.", + "switchToETHWallet": "Schakel over naar een Ethereum-portemonnee en probeer het opnieuw", + "seed_phrase_length": "Lengte van de zaadzin", + "unavailable_balance": "Onbeschikbaar saldo", + "unavailable_balance_description": "Niet-beschikbaar saldo: Dit totaal omvat het geld dat is vergrendeld in lopende transacties en het geld dat u actief hebt bevroren in uw muntcontrole-instellingen. Vergrendelde saldi komen beschikbaar zodra de betreffende transacties zijn voltooid, terwijl bevroren saldi ontoegankelijk blijven voor transacties totdat u besluit ze weer vrij te geven.", + "unspent_change": "Wijziging", + "tor_connection": "Tor-verbinding" } diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index f5d0826e6..dc69468bd 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -292,7 +292,7 @@ "available_balance": "Dostępne środki", "hidden_balance": "Ukryte saldo", "sync_status_syncronizing": "SYNCHRONIZACJA", - "sync_status_syncronized": "SYNCHRONIZOWANIE", + "sync_status_syncronized": "ZSYNCHRONIZOWANO", "sync_status_not_connected": "NIE POŁĄCZONY", "sync_status_starting_sync": "ROZPOCZĘCIE SYNCHRONIZACJI", "sync_status_failed_connect": "POŁĄCZENIE NIEUDANE", @@ -339,7 +339,6 @@ "template": "Szablon", "confirm_delete_template": "Ta czynność usunie ten szablon. Czy chcesz kontynuować?", "confirm_delete_wallet": "Ta czynność usunie ten portfel. Czy chcesz kontynuować?", - "picker_description": "Aby wybrać ChangeNOW lub MorphToken, najpierw zmień swoją parę handlową", "change_wallet_alert_title": "Zmień obecny portfel", "change_wallet_alert_content": "Czy chcesz zmienić obecny portfel na ${wallet_name}?", "creating_new_wallet": "Tworzenie nowego portfela", @@ -596,7 +595,6 @@ "sweeping_wallet_alert": "To nie powinno zająć dużo czasu. NIE WYCHODŹ Z TEGO EKRANU, W PRZECIWNYM WYPADKU MOŻE ZOSTAĆ UTRACONA ŚRODKI", "decimal_places_error": "Za dużo miejsc dziesiętnych", "edit_node": "Edytuj węzeł", - "frozen_balance": "Zamrożona równowaga", "invoice_details": "Dane do faktury", "donation_link_details": "Szczegóły linku darowizny", "anonpay_description": "Wygeneruj ${type}. Odbiorca może ${method} z dowolną obsługiwaną kryptowalutą, a Ty otrzymasz środki w tym portfelu.", @@ -693,5 +691,49 @@ "choose_derivation": "Wybierz wyprowadzenie portfela", "new_first_wallet_text": "Łatwo zapewnić bezpieczeństwo kryptowalut", "select_destination": "Wybierz miejsce docelowe dla pliku kopii zapasowej.", - "save_to_downloads": "Zapisz w Pobranych" + "save_to_downloads": "Zapisz w Pobranych", + "select_buy_provider_notice": "Wybierz powyższe dostawcę zakupu. Możesz pominąć ten ekran, ustawiając domyślnego dostawcę zakupu w ustawieniach aplikacji.", + "onramper_option_description": "Szybko kup kryptowaluty z wieloma metodami płatności. Dostępne w większości krajów. Spready i opłaty różnią się.", + "default_buy_provider": "Domyślny dostawca zakupu", + "ask_each_time": "Zapytaj za każdym razem", + "buy_provider_unavailable": "Dostawca obecnie niedostępny.", + "signTransaction": "Podpisz transakcję", + "errorGettingCredentials": "Niepowodzenie: Błąd podczas uzyskiwania poświadczeń", + "errorSigningTransaction": "Wystąpił błąd podczas podpisywania transakcji", + "pairingInvalidEvent": "Nieprawidłowe zdarzenie parowania", + "chains": "Więzy", + "methods": "Metody", + "events": "Wydarzenia", + "reject": "Odrzucić", + "approve": "Zatwierdzić", + "expiresOn": "Upływa w dniu", + "walletConnect": "PortfelPołącz", + "nullURIError": "URI ma wartość zerową", + "connectWalletPrompt": "Połącz swój portfel z WalletConnect, aby dokonywać transakcji", + "newConnection": "Nowe połączenie", + "activeConnectionsPrompt": "Tutaj pojawią się aktywne połączenia", + "deleteConnectionConfirmationPrompt": "Czy na pewno chcesz usunąć połączenie z", + "event": "Wydarzenie", + "successful": "Udany", + "wouoldLikeToConnect": "chciałbym się połączyć", + "message": "Wiadomość", + "do_not_have_enough_gas_asset": "Nie masz wystarczającej ilości ${currency}, aby dokonać transakcji przy bieżących warunkach sieci blockchain. Potrzebujesz więcej ${currency}, aby uiścić opłaty za sieć blockchain, nawet jeśli wysyłasz inny zasób.", + "totp_auth_url": "Adres URL TOTP AUTH", + "awaitDAppProcessing": "Poczekaj, aż dApp zakończy przetwarzanie.", + "copyWalletConnectLink": "Skopiuj link do WalletConnect z dApp i wklej tutaj", + "enterWalletConnectURI": "Wprowadź identyfikator URI WalletConnect", + "seed_key": "Klucz nasion", + "enter_seed_phrase": "Wprowadź swoją frazę nasienną", + "change_rep_successful": "Pomyślnie zmienił przedstawiciela", + "add_contact": "Dodaj kontakt", + "exchange_provider_unsupported": "${providerName} nie jest już obsługiwany!", + "domain_looks_up": "Wyszukiwanie domen", + "require_for_exchanges_to_external_wallets": "Wymagaj wymiany na portfele zewnętrzne", + "camera_permission_is_required": "Wymagane jest pozwolenie na korzystanie z aparatu.\nWłącz tę funkcję w ustawieniach aplikacji.", + "switchToETHWallet": "Przejdź na portfel Ethereum i spróbuj ponownie", + "seed_phrase_length": "Długość frazy początkowej", + "unavailable_balance": "Niedostępne saldo", + "unavailable_balance_description": "Niedostępne saldo: Suma ta obejmuje środki zablokowane w transakcjach oczekujących oraz te, które aktywnie zamroziłeś w ustawieniach kontroli monet. Zablokowane salda staną się dostępne po zakończeniu odpowiednich transakcji, natomiast zamrożone salda pozostaną niedostępne dla transakcji, dopóki nie zdecydujesz się ich odblokować.", + "unspent_change": "Zmiana", + "tor_connection": "Połączenie Torem" } diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index 8c741b1c8..c6d22ae23 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -339,7 +339,6 @@ "template": "Modelo", "confirm_delete_template": "Esta ação excluirá este modelo. Você deseja continuar?", "confirm_delete_wallet": "Esta ação excluirá esta carteira. Você deseja continuar?", - "picker_description": "Para escolher ChangeNOW ou MorphToken, altere primeiro o seu par de negociação", "change_wallet_alert_title": "Alterar carteira atual", "change_wallet_alert_content": "Quer mudar a carteira atual para ${wallet_name}?", "creating_new_wallet": "Criando nova carteira", @@ -595,7 +594,6 @@ "sweeping_wallet_alert": "To nie powinno zająć dużo czasu. NIE WYCHODŹ Z TEGO EKRANU, W PRZECIWNYM WYPADKU MOŻE ZOSTAĆ UTRACONA ŚRODKI", "decimal_places_error": "Muitas casas decimais", "edit_node": "Editar nó", - "frozen_balance": "Saldo Congelado", "invoice_details": "Detalhes da fatura", "donation_link_details": "Detalhes do link de doação", "anonpay_description": "Gere ${type}. O destinatário pode ${method} com qualquer criptomoeda suportada e você receberá fundos nesta carteira.", @@ -692,5 +690,49 @@ "choose_derivation": "Escolha a derivação da carteira", "new_first_wallet_text": "Mantenha sua criptomoeda facilmente segura", "select_destination": "Selecione o destino para o arquivo de backup.", - "save_to_downloads": "Salvar em Downloads" + "save_to_downloads": "Salvar em Downloads", + "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.", + "onramper_option_description": "Compre rapidamente criptografia com muitos métodos de pagamento. Disponível na maioria dos países. Os spreads e taxas variam.", + "default_buy_provider": "Provedor de compra padrão", + "ask_each_time": "Pergunte cada vez", + "buy_provider_unavailable": "Provedor atualmente indisponível.", + "signTransaction": "Assinar transação", + "errorGettingCredentials": "Falha: Erro ao obter credenciais", + "errorSigningTransaction": "Ocorreu um erro ao assinar a transação", + "pairingInvalidEvent": "Emparelhamento de evento inválido", + "chains": "Correntes", + "methods": "Métodos", + "events": "Eventos", + "reject": "Rejeitar", + "approve": "Aprovar", + "expiresOn": "Expira em", + "walletConnect": "CarteiraConectada", + "nullURIError": "URI é nulo", + "connectWalletPrompt": "Conecte sua carteira ao WalletConnect para fazer transações", + "newConnection": "Nova conexão", + "activeConnectionsPrompt": "Conexões ativas aparecerão aqui", + "deleteConnectionConfirmationPrompt": "Tem certeza de que deseja excluir a conexão com", + "event": "Evento", + "successful": "Bem-sucedido", + "wouoldLikeToConnect": "gostaria de me conectar", + "message": "Mensagem", + "do_not_have_enough_gas_asset": "Você não tem ${currency} suficiente para fazer uma transação com as condições atuais da rede blockchain. Você precisa de mais ${currency} para pagar as taxas da rede blockchain, mesmo se estiver enviando um ativo diferente.", + "totp_auth_url": "URL de autenticação TOTP", + "awaitDAppProcessing": "Aguarde até que o dApp termine o processamento.", + "copyWalletConnectLink": "Copie o link WalletConnect do dApp e cole aqui", + "enterWalletConnectURI": "Insira o URI do WalletConnect", + "seed_key": "Chave de semente", + "enter_seed_phrase": "Digite sua frase de semente", + "change_rep_successful": "Mudou com sucesso o representante", + "add_contact": "Adicionar contato", + "exchange_provider_unsupported": "${providerName} não é mais suportado!", + "domain_looks_up": "Pesquisas de domínio", + "require_for_exchanges_to_external_wallets": "Exigir trocas para carteiras externas", + "camera_permission_is_required": "É necessária permissão da câmera.\nAtive-o nas configurações do aplicativo.", + "switchToETHWallet": "Mude para uma carteira Ethereum e tente novamente", + "seed_phrase_length": "Comprimento da frase-semente", + "unavailable_balance": "Saldo indisponível", + "unavailable_balance_description": "Saldo Indisponível: Este total inclui fundos bloqueados em transações pendentes e aqueles que você congelou ativamente nas configurações de controle de moedas. Os saldos bloqueados ficarão disponíveis assim que suas respectivas transações forem concluídas, enquanto os saldos congelados permanecerão inacessíveis para transações até que você decida descongelá-los.", + "unspent_change": "Mudar", + "tor_connection": "Conexão Tor" } diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index b57484606..50bef718d 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -339,7 +339,6 @@ "template": "Шаблон", "confirm_delete_template": "Это действие удалит шаблон. Вы хотите продолжить?", "confirm_delete_wallet": "Это действие удалит кошелек. Вы хотите продолжить?", - "picker_description": "Чтобы выбрать ChangeNOW или MorphToken, сначала смените пару для обмена", "change_wallet_alert_title": "Изменить текущий кошелек", "change_wallet_alert_content": "Вы хотите изменить текущий кошелек на ${wallet_name}?", "creating_new_wallet": "Создание нового кошелька", @@ -596,7 +595,6 @@ "sweeping_wallet_alert": "Это не должно занять много времени. НЕ ПОКИДАЙТЕ ЭТОТ ЭКРАН, ИНАЧЕ ВЫЧИСЛЕННЫЕ СРЕДСТВА МОГУТ БЫТЬ ПОТЕРЯНЫ", "decimal_places_error": "Слишком много десятичных знаков", "edit_node": "Редактировать узел", - "frozen_balance": "Замороженный баланс", "invoice_details": "Детали счета", "donation_link_details": "Информация о ссылке для пожертвований", "anonpay_description": "Создайте ${type}. Получатель может использовать ${method} с любой поддерживаемой криптовалютой, и вы получите средства на этот кошелек.", @@ -692,6 +690,50 @@ "choose_derivation": "Выберите вывод кошелька", "new_first_wallet_text": "Легко сохранить свою криптовалюту в безопасности", "select_destination": "Пожалуйста, выберите место для файла резервной копии.", + "auto_generate_subaddresses": "Авто генерируйте Subaddresses", "save_to_downloads": "Сохранить в загрузках", - "auto_generate_subaddresses": "Авто генерируйте Subaddresses" + "select_buy_provider_notice": "Выберите поставщика покупки выше. Вы можете пропустить этот экран, установив поставщика покупки по умолчанию в настройках приложения.", + "onramper_option_description": "Быстро купите крипто со многими способами оплаты. Доступно в большинстве стран. Спреды и сборы различаются.", + "default_buy_provider": "По умолчанию поставщик покупки", + "ask_each_time": "Спросите каждый раз", + "buy_provider_unavailable": "Поставщик в настоящее время недоступен.", + "signTransaction": "Подписать транзакцию", + "errorGettingCredentials": "Не удалось: ошибка при получении учетных данных.", + "errorSigningTransaction": "Произошла ошибка при подписании транзакции", + "pairingInvalidEvent": "Недействительное событие сопряжения", + "chains": "Цепи", + "methods": "Методы", + "events": "События", + "reject": "Отклонять", + "approve": "Утвердить", + "expiresOn": "Годен до", + "walletConnect": "КошелекПодключиться", + "nullURIError": "URI имеет значение null", + "connectWalletPrompt": "Подключите свой кошелек к WalletConnect для совершения транзакций.", + "newConnection": "Новое соединение", + "activeConnectionsPrompt": "Здесь появятся активные подключения", + "deleteConnectionConfirmationPrompt": "Вы уверены, что хотите удалить подключение к", + "event": "Событие", + "successful": "Успешный", + "wouoldLikeToConnect": "хотел бы подключиться", + "message": "Сообщение", + "do_not_have_enough_gas_asset": "У вас недостаточно ${currency} для совершения транзакции при текущих условиях сети блокчейн. Вам нужно больше ${currency} для оплаты комиссий за сеть блокчейна, даже если вы отправляете другой актив.", + "totp_auth_url": "URL-адрес TOTP-АВТОРИЗАЦИИ", + "awaitDAppProcessing": "Пожалуйста, подождите, пока dApp завершит обработку.", + "copyWalletConnectLink": "Скопируйте ссылку WalletConnect из dApp и вставьте сюда.", + "enterWalletConnectURI": "Введите URI WalletConnect", + "seed_key": "Ключ семян", + "enter_seed_phrase": "Введите свою семенную фразу", + "change_rep_successful": "Успешно изменил представитель", + "add_contact": "Добавить контакт", + "exchange_provider_unsupported": "${providerName} больше не поддерживается!", + "domain_looks_up": "Поиск доменов", + "require_for_exchanges_to_external_wallets": "Требовать обмена на внешние кошельки", + "camera_permission_is_required": "Требуется разрешение камеры.\nПожалуйста, включите его в настройках приложения.", + "switchToETHWallet": "Пожалуйста, переключитесь на кошелек Ethereum и повторите попытку.", + "seed_phrase_length": "Длина исходной фразы", + "unavailable_balance": "Недоступный баланс", + "unavailable_balance_description": "Недоступный баланс: в эту сумму входят средства, заблокированные в ожидающих транзакциях, и средства, которые вы активно заморозили в настройках управления монетами. Заблокированные балансы станут доступны после завершения соответствующих транзакций, а замороженные балансы останутся недоступными для транзакций, пока вы не решите их разморозить.", + "unspent_change": "Изменять", + "tor_connection": "Тор соединение" } diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index 0e3405cca..95a1cd406 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -339,7 +339,6 @@ "template": "แบบฟอร์ม", "confirm_delete_template": "การดำเนินการนี้จะลบแบบฟอร์มนี้ คุณต้องการดำเนินการต่อหรือไม่?", "confirm_delete_wallet": "การดำเนินการนี้จะลบกระเป๋านี้ คุณต้องการดำเนินการต่อหรือไม่?", - "picker_description": "ในการเลือก ChangeNOW หรือ MorphToken โปรดเปลี่ยนคู่แลกเปลี่ยนก่อน", "change_wallet_alert_title": "เปลี่ยนกระเป๋าปัจจุบัน", "change_wallet_alert_content": "คุณต้องการเปลี่ยนกระเป๋าปัจจุบันเป็น ${wallet_name} หรือไม่?", "creating_new_wallet": "กำลังสร้างกระเป๋าใหม่", @@ -594,7 +593,6 @@ "sweeping_wallet_alert": "การดำเนินการนี้ใช้เวลาไม่นาน อย่าออกจากหน้าจอนี้ มิฉะนั้นเงินที่กวาดไปอาจสูญหาย", "decimal_places_error": "ทศนิยมมากเกินไป", "edit_node": "แก้ไขโหนด", - "frozen_balance": "ยอดคงเหลือแช่แข็ง", "invoice_details": "รายละเอียดใบแจ้งหนี้", "donation_link_details": "รายละเอียดลิงค์บริจาค", "anonpay_description": "สร้าง ${type} ผู้รับสามารถ ${method} ด้วยสกุลเงินดิจิทัลที่รองรับ และคุณจะได้รับเงินในกระเป๋าสตางค์นี้", @@ -690,6 +688,50 @@ "choose_derivation": "เลือก Wallet Derivation", "new_first_wallet_text": "ทำให้สกุลเงินดิจิตอลของคุณปลอดภัยได้อย่างง่ายดาย", "select_destination": "โปรดเลือกปลายทางสำหรับไฟล์สำรอง", + "auto_generate_subaddresses": "Auto สร้าง subaddresses", "save_to_downloads": "บันทึกลงดาวน์โหลด", - "auto_generate_subaddresses": "Auto สร้าง subaddresses" + "select_buy_provider_notice": "เลือกผู้ให้บริการซื้อด้านบน คุณสามารถข้ามหน้าจอนี้ได้โดยการตั้งค่าผู้ให้บริการซื้อเริ่มต้นในการตั้งค่าแอป", + "onramper_option_description": "ซื้อ crypto อย่างรวดเร็วด้วยวิธีการชำระเงินจำนวนมาก มีให้บริการในประเทศส่วนใหญ่ สเปรดและค่าธรรมเนียมแตกต่างกันไป", + "default_buy_provider": "ผู้ให้บริการซื้อเริ่มต้น", + "ask_each_time": "ถามทุกครั้ง", + "buy_provider_unavailable": "ผู้ให้บริการไม่สามารถใช้งานได้ในปัจจุบัน", + "signTransaction": "ลงนามในการทำธุรกรรม", + "errorGettingCredentials": "ล้มเหลว: เกิดข้อผิดพลาดขณะรับข้อมูลรับรอง", + "errorSigningTransaction": "เกิดข้อผิดพลาดขณะลงนามธุรกรรม", + "pairingInvalidEvent": "การจับคู่เหตุการณ์ที่ไม่ถูกต้อง", + "chains": "ห่วงโซ่", + "methods": "วิธีการ", + "events": "กิจกรรม", + "reject": "ปฏิเสธ", + "approve": "อนุมัติ", + "expiresOn": "หมดอายุวันที่", + "walletConnect": "WalletConnect", + "nullURIError": "URI เป็นโมฆะ", + "connectWalletPrompt": "เชื่อมต่อกระเป๋าเงินของคุณด้วย WalletConnect เพื่อทำธุรกรรม", + "newConnection": "การเชื่อมต่อใหม่", + "activeConnectionsPrompt": "การเชื่อมต่อที่ใช้งานอยู่จะปรากฏที่นี่", + "deleteConnectionConfirmationPrompt": "คุณแน่ใจหรือไม่ว่าต้องการลบการเชื่อมต่อไปยัง", + "event": "เหตุการณ์", + "successful": "ประสบความสำเร็จ", + "wouoldLikeToConnect": "ต้องการเชื่อมต่อ", + "message": "ข้อความ", + "do_not_have_enough_gas_asset": "คุณมี ${currency} ไม่เพียงพอที่จะทำธุรกรรมกับเงื่อนไขเครือข่ายบล็อคเชนในปัจจุบัน คุณต้องมี ${currency} เพิ่มขึ้นเพื่อชำระค่าธรรมเนียมเครือข่ายบล็อคเชน แม้ว่าคุณจะส่งสินทรัพย์อื่นก็ตาม", + "totp_auth_url": "URL การตรวจสอบสิทธิ์ TOTP", + "awaitDAppProcessing": "โปรดรอให้ dApp ประมวลผลเสร็จสิ้น", + "copyWalletConnectLink": "คัดลอกลิงก์ WalletConnect จาก dApp แล้ววางที่นี่", + "enterWalletConnectURI": "เข้าสู่ WalletConnect URI", + "seed_key": "คีย์เมล็ดพันธุ์", + "enter_seed_phrase": "ป้อนวลีเมล็ดพันธุ์ของคุณ", + "change_rep_successful": "เปลี่ยนตัวแทนสำเร็จ", + "add_contact": "เพิ่มผู้ติดต่อ", + "exchange_provider_unsupported": "${providerName} ไม่ได้รับการสนับสนุนอีกต่อไป!", + "domain_looks_up": "การค้นหาโดเมน", + "require_for_exchanges_to_external_wallets": "จำเป็นต้องแลกเปลี่ยนกับกระเป๋าเงินภายนอก", + "camera_permission_is_required": "ต้องได้รับอนุญาตจากกล้อง\nโปรดเปิดใช้งานจากการตั้งค่าแอป", + "switchToETHWallet": "โปรดเปลี่ยนไปใช้กระเป๋าเงิน Ethereum แล้วลองอีกครั้ง", + "seed_phrase_length": "ความยาววลีของเมล็ด", + "unavailable_balance": "ยอดคงเหลือไม่พร้อมใช้งาน", + "unavailable_balance_description": "ยอดคงเหลือที่ไม่พร้อมใช้งาน: ยอดรวมนี้รวมถึงเงินทุนที่ถูกล็อคในการทำธุรกรรมที่รอดำเนินการและที่คุณได้แช่แข็งไว้ในการตั้งค่าการควบคุมเหรียญของคุณ ยอดคงเหลือที่ถูกล็อคจะพร้อมใช้งานเมื่อธุรกรรมที่เกี่ยวข้องเสร็จสมบูรณ์ ในขณะที่ยอดคงเหลือที่แช่แข็งจะไม่สามารถเข้าถึงได้สำหรับธุรกรรมจนกว่าคุณจะตัดสินใจยกเลิกการแช่แข็ง", + "unspent_change": "เปลี่ยน", + "tor_connection": "การเชื่อมต่อทอร์" } diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb new file mode 100644 index 000000000..960432a1b --- /dev/null +++ b/res/values/strings_tl.arb @@ -0,0 +1,734 @@ +{ + "welcome": "Maligayang pagdating sa", + "cake_wallet": "Cake wallet", + "first_wallet_text": "Kahanga -hangang pitaka para sa Monero, Bitcoin, Ethereum, Litecoin, at Haven", + "please_make_selection": "Mangyaring gumawa ng isang pagpipilian sa ibaba upang lumikha o mabawi ang iyong pitaka.", + "create_new": "Lumikha ng bagong pitaka", + "restore_wallet": "Ibalik ang pitaka", + "monero_com": "Monero.com sa pamamagitan ng cake wallet", + "monero_com_wallet_text": "Galing ng pitaka para sa Monero", + "haven_app": "Haven sa pamamagitan ng cake wallet", + "haven_app_wallet_text": "Galing ng pitaka para sa Haven", + "accounts": "Mga Account", + "edit": "I -edit", + "account": "Account", + "add": "Idagdag", + "address_book": "Address Book", + "contact": "Makipag -ugnay", + "please_select": "Pakipili:", + "cancel": "Kanselahin", + "ok": "Ok", + "contact_name": "pangalan ng contact", + "reset": "I -reset", + "save": "I -save", + "address_remove_contact": "Alisin ang contact", + "address_remove_content": "Sigurado ka bang nais mong alisin ang napiling contact?", + "authenticated": "Napatunayan", + "authentication": "Pagpapatunay", + "failed_authentication": "Nabigong pagpapatunay. ${state_error}", + "wallet_menu": "Menu", + "Blocks_remaining": "Ang natitirang ${status} ay natitira", + "please_try_to_connect_to_another_node": "Mangyaring subukang kumonekta sa isa pang node", + "xmr_hidden": "Nakatago", + "xmr_available_balance": "Magagamit na balanse", + "xmr_full_balance": "Buong balanse", + "send": "Ipadala", + "receive": "Tumanggap", + "transactions": "Mga Transaksyon", + "incoming": "Papasok", + "outgoing": "Palabas", + "transactions_by_date": "Mga Transaksyon ayon sa Petsa", + "trades": "Trading", + "filter_by": "Filter ni", + "today": "Ngayon", + "yesterday": "Kahapon", + "received": "Natanggap", + "sent": "Ipinadala", + "pending": "(Pending)", + "rescan": "Rescan", + "reconnect": "Kumonekta muli", + "wallets": "Wallets", + "show_seed": "Magpakita ng binhi", + "show_keys": "Ipakita ang mga binhi/susi", + "address_book_menu": "Address Book", + "reconnection": "Pag -ugnay muli", + "reconnect_alert_text": "Sigurado ka bang nais mong muling kumonekta?", + "exchange": "Palitan", + "clear": "Malinaw", + "refund_address": "Refund address", + "change_exchange_provider": "Baguhin ang tagapagbigay ng palitan", + "you_will_send": "I -convert mula sa", + "you_will_get": "Mag -convert sa", + "amount_is_guaranteed": "Ang natanggap na halaga ay garantisado", + "amount_is_estimate": "Ang natanggap na halaga ay isang pagtatantya", + "powered_by": "Pinapagana ng ${title}", + "error": "Error", + "estimated": "Tinatayang", + "min_value": "Min: ${value} ${currency}", + "max_value": "Max: ${value} ${currency}", + "change_currency": "Baguhin ang pera", + "overwrite_amount": "Overwrite na halaga", + "qr_payment_amount": "Ang QR code na ito ay naglalaman ng isang halaga ng pagbabayad. Nais mo bang i -overwrite ang kasalukuyang halaga?", + "copy_id": "Kopyahin ang id", + "exchange_result_write_down_trade_id": "Mangyaring kopyahin o isulat ang trade ID upang magpatuloy.", + "trade_id": "Trade ID:", + "copied_to_clipboard": "Kinopya sa clipboard", + "saved_the_trade_id": "Nai -save ko ang trade ID", + "fetching": "Pagkuha", + "id": "ID:", + "amount": "Halaga:", + "payment_id": "Payment ID:", + "status": "Katayuan:", + "offer_expires_in": "Mag -expire ang alok sa:", + "trade_is_powered_by": "Ang kalakalan na ito ay pinalakas ng ${provider}", + "copy_address": "Kopyahin ang address", + "exchange_result_confirm": "Sa pamamagitan ng pagpindot ng kumpirmahin, magpapadala ka ng ${fetchingLabel} ${from} mula sa iyong pitaka na tinatawag na ${walletName} sa address na ipinakita sa ibaba. O maaari kang magpadala mula sa iyong panlabas na pitaka sa ibaba address/qr code.\n\nMangyaring pindutin ang kumpirmahin na magpatuloy o bumalik upang baguhin ang mga halaga.", + "exchange_result_description": "Dapat kang magpadala ng isang minimum na ${fetchingLabel} ${from} hanggang sa address na ipinakita sa susunod na pahina. Kung nagpapadala ka ng isang halaga na mas mababa kaysa sa ${fetchingLabel} ${from} maaaring hindi ito ma -convert at maaaring hindi ito ibabalik.", + "exchange_result_write_down_ID": "*Mangyaring kopyahin o isulat ang iyong ID na ipinakita sa itaas.", + "confirm": "Kumpirmahin", + "confirm_sending": "Kumpirmahin ang pagpapadala", + "commit_transaction_amount_fee": "Gumawa ng transaksyon\nHalaga: ${amount}\nBayad: ${fee}", + "sending": "Pagpapadala", + "transaction_sent": "Ipinadala ang transaksyon!", + "expired": "Nag -expire", + "time": "${minutes} m ${seconds} s", + "send_xmr": "Magpadala ng XMR", + "exchange_new_template": "Bagong template", + "faq": "FAQ", + "enter_your_pin": "Ipasok ang iyong pin", + "loading_your_wallet": "Naglo -load ng iyong pitaka", + "new_wallet": "Bagong pitaka", + "wallet_name": "Pangalan ng Wallet", + "continue_text": "Magpatuloy", + "choose_wallet_currency": "Mangyaring piliin ang Pera ng Wallet:", + "node_new": "Bagong node", + "node_address": "Node address", + "node_port": "Node port", + "login": "Mag log in", + "password": "Password", + "nodes": "Node", + "node_reset_settings_title": "I -reset ang Mga Setting", + "nodes_list_reset_to_default_message": "Sigurado ka bang nais mong i -reset ang mga setting upang default?", + "change_current_node": "Sigurado ka bang baguhin ang kasalukuyang node sa ${node}?", + "change": "Palitan", + "remove_node": "Alisin ang node", + "remove_node_message": "Sigurado ka bang nais mong alisin ang napiling node?", + "remove": "Alisin", + "delete": "Tanggalin", + "add_new_node": "Magdagdag ng bagong node", + "change_current_node_title": "Baguhin ang kasalukuyang node", + "node_test": "Pagsusulit", + "node_connection_successful": "Ang koneksyon ay matagumpay", + "node_connection_failed": "Nabigo ang koneksyon", + "new_node_testing": "Bagong pagsubok sa node", + "use": "Lumipat sa", + "digit_pin": "-digit pin", + "share_address": "Ibahagi ang address", + "receive_amount": "Halaga", + "subaddresses": "Mga Subaddresses", + "addresses": "Mga address", + "scan_qr_code_to_get_address": "I -scan ang QR code upang makuha ang address", + "qr_fullscreen": "Tapikin upang buksan ang buong screen QR code", + "rename": "Palitan ang pangalan", + "choose_account": "Pumili ng account", + "create_new_account": "Lumikha ng Bagong Account", + "accounts_subaddresses": "Mga Account at Subaddresses", + "restore_restore_wallet": "Ibalik ang pitaka", + "restore_title_from_seed_keys": "Ibalik mula sa mga binhi/susi", + "restore_description_from_seed_keys": "Ibalik ang iyong pitaka mula sa mga binhi/susi na na -save mo upang ma -secure ang lugar", + "restore_next": "Susunod", + "restore_title_from_backup": "Ibalik mula sa backup", + "restore_description_from_backup": "Maaari mong ibalik ang buong cake wallet app mula sa iyong back-up file", + "restore_seed_keys_restore": "Ibinalik ang mga binhi/susi", + "restore_title_from_seed": "Ibalik mula sa binhi", + "restore_description_from_seed": "Ibalik ang iyong pitaka mula sa alinman sa 25 salita o 13 na code ng kombinasyon ng salita", + "restore_title_from_keys": "Ibalik mula sa mga susi", + "restore_description_from_keys": "Ibalik ang iyong pitaka mula sa nabuong mga keystroke na na -save mula sa iyong mga pribadong susi", + "restore_wallet_name": "Pangalan ng Wallet", + "restore_address": "Address", + "restore_view_key_private": "Tingnan ang Key (Pribado)", + "restore_spend_key_private": "Gumastos ng susi (pribado)", + "restore_recover": "Ibalik", + "restore_wallet_restore_description": "Paglalarawan ng Wallet", + "restore_new_seed": "Bagong binhi", + "restore_active_seed": "Aktibong binhi", + "restore_bitcoin_description_from_seed": "Ibalik ang iyong pitaka mula sa 24 na code ng kombinasyon ng salita", + "restore_bitcoin_description_from_keys": "Ibalik ang iyong pitaka mula sa nabuong wif string mula sa iyong mga pribadong susi", + "restore_bitcoin_title_from_keys": "Ibalik mula sa WIF", + "restore_from_date_or_blockheight": "Mangyaring magpasok ng isang petsa ng ilang araw bago mo nilikha ang pitaka na ito. O kung alam mo ang blockheight, mangyaring ipasok ito sa halip", + "seed_reminder": "Mangyaring isulat ang mga ito kung sakaling mawala ka o punasan ang iyong telepono", + "seed_title": "Binhi", + "seed_share": "Magbahagi ng binhi", + "copy": "Kopya", + "seed_language_choose": "Mangyaring pumili ng wika ng binhi:", + "seed_choose": "Pumili ng wika ng binhi", + "seed_language_next": "Susunod", + "seed_language_english": "Ingles", + "seed_language_chinese": "Tsino", + "seed_language_dutch": "Dutch", + "seed_language_german": "Aleman", + "seed_language_japanese": "Hapon", + "seed_language_portuguese": "Portuges", + "seed_language_russian": "Russian", + "seed_language_spanish": "Espanyol", + "seed_language_french": "Pranses", + "seed_language_italian": "Italyano", + "send_title": "Ipadala", + "send_your_wallet": "Iyong pitaka", + "send_address": "${cryptoCurrency} address", + "send_payment_id": "Payment ID (Opsyonal)", + "all": "Lahat", + "send_error_minimum_value": "Ang pinakamababang halaga ng halaga ay 0.01", + "send_error_currency": "Ang pera ay maaari lamang maglaman ng mga numero", + "send_estimated_fee": "Tinatayang bayad:", + "send_priority": "Sa kasalukuyan ang bayad ay nakatakda sa ${transactionPriority} priority.\nAng priority ng transaksyon ay maaaring maiakma sa mga setting", + "send_creating_transaction": "Paglikha ng transaksyon", + "send_templates": "Mga template", + "send_new": "Bago", + "send_amount": "Halaga:", + "send_fee": "Bayad:", + "send_name": "Pangalan", + "got_it": "Nakuha ko", + "send_sending": "Pagpapadala ...", + "send_success": "Ang iyong ${crypto} ay matagumpay na naipadala", + "settings_title": "Mga setting", + "settings_nodes": "Node", + "settings_current_node": "Kasalukuyang node", + "settings_wallets": "Wallets", + "settings_display_balance": "Ipakita ang balanse", + "settings_currency": "Pera", + "settings_fee_priority": "Priority priority", + "settings_save_recipient_address": "I -save ang address ng tatanggap", + "settings_personal": "Personal", + "settings_change_pin": "Baguhin ang pin", + "settings_change_language": "Baguhin ang wika", + "settings_allow_biometrical_authentication": "Payagan ang pagpapatunay ng biometrical", + "settings_dark_mode": "Madilim na mode", + "settings_transactions": "Mga Transaksyon", + "settings_trades": "Trading", + "settings_display_on_dashboard_list": "Ipakita sa listahan ng dashboard", + "settings_all": "Lahat", + "settings_only_trades": "TRADES LAMANG", + "settings_only_transactions": "Mga transaksyon lamang", + "settings_none": "Wala", + "settings_support": "Suporta", + "settings_terms_and_conditions": "Mga Tuntunin at Kundisyon", + "pin_is_incorrect": "Mali ang pin", + "setup_pin": "Setup pin", + "enter_your_pin_again": "Ipasok muli ang iyong pin", + "setup_successful": "Matagumpay na na -set up ang iyong pin!", + "wallet_keys": "Mga buto/susi ng pitaka", + "wallet_seed": "SEED ng Wallet", + "private_key": "Pribadong susi", + "public_key": "Pampublikong susi", + "view_key_private": "Tingnan ang Key (Pribado)", + "view_key_public": "Tingnan ang Key (Publiko)", + "spend_key_private": "Gumastos ng susi (pribado)", + "spend_key_public": "Gumastos ng susi (publiko)", + "copied_key_to_clipboard": "Kinopya ang ${key} sa clipboard", + "new_subaddress_title": "Bagong tirahan", + "new_subaddress_label_name": "Pangalan ng label", + "new_subaddress_create": "Lumikha", + "address_label": "Address Label", + "subaddress_title": "Listahan ng Subaddress", + "trade_details_title": "Mga detalye sa kalakalan", + "trade_details_id": "ID", + "trade_details_state": "Katayuan", + "trade_details_fetching": "Pagkuha", + "trade_details_provider": "Tagabigay", + "trade_details_created_at": "Nilikha sa", + "trade_details_pair": "Pares", + "trade_details_copied": "${title} kinopya sa clipboard", + "trade_history_title": "Kasaysayan ng Kalakal", + "transaction_details_title": "Mga detalye ng transaksyon", + "transaction_details_transaction_id": "Transaction ID", + "transaction_details_date": "Petsa", + "transaction_details_height": "Taas", + "transaction_details_amount": "Halaga", + "transaction_details_fee": "Bayad", + "transaction_details_copied": "${title} kinopya sa clipboard", + "transaction_details_recipient_address": "Mga address ng tatanggap", + "wallet_list_title": "Monero Wallet", + "wallet_list_create_new_wallet": "Lumikha ng bagong pitaka", + "wallet_list_edit_wallet": "I -edit ang Wallet", + "wallet_list_wallet_name": "Pangalan ng Wallet", + "wallet_list_restore_wallet": "Ibalik ang pitaka", + "wallet_list_load_wallet": "Mag -load ng pitaka", + "wallet_list_loading_wallet": "Naglo -load ng ${wallet_name} Wallet", + "wallet_list_failed_to_load": "Nabigong mag -load ng ${wallet_name} pitaka. ${error}", + "wallet_list_removing_wallet": "Pag -alis ng ${wallet_name} Wallet", + "wallet_list_failed_to_remove": "Nabigong alisin ang ${wallet_name} wallet. ${error}", + "widgets_address": "Address", + "widgets_restore_from_blockheight": "Ibalik mula sa blockheight", + "widgets_restore_from_date": "Ibalik mula sa petsa", + "widgets_or": "o", + "widgets_seed": "Binhi", + "router_no_route": "Walang ruta na tinukoy para sa ${name}", + "error_text_account_name": "Ang pangalan ng account ay maaari lamang maglaman ng mga titik, numero\nat dapat sa pagitan ng 1 at 15 character ang haba", + "error_text_contact_name": "Ang pangalan ng contact ay hindi maaaring maglaman ng mga simbolo ng ',' \"\nat dapat sa pagitan ng 1 at 32 character ang haba", + "error_text_address": "Ang wallet address ay dapat na tumutugma sa uri\nng cryptocurrency", + "error_text_node_address": "Mangyaring magpasok ng isang address ng IPv4", + "error_text_node_port": "Ang Node Port ay maaari lamang maglaman ng mga numero sa pagitan ng 0 at 65535", + "error_text_node_proxy_address": "Mangyaring ipasok ang : , halimbawa 127.0.0.1:9050", + "error_text_payment_id": "Ang Payment ID ay maaari lamang maglaman mula 16 hanggang 64 chars sa hex", + "error_text_xmr": "Ang halaga ng XMR ay hindi maaaring lumampas sa magagamit na balanse.\nAng bilang ng mga numero ng fraction ay dapat na mas mababa o katumbas ng 12", + "error_text_fiat": "Ang halaga ng halaga ay hindi maaaring lumampas sa magagamit na balanse.\nAng bilang ng mga numero ng fraction ay dapat na mas mababa o katumbas ng 2", + "error_text_subaddress_name": "Ang pangalan ng subaddress ay hindi maaaring maglaman ng mga simbolo na `, '\"\nat dapat sa pagitan ng 1 at 20 character ang haba", + "error_text_amount": "Ang halaga ay maaari lamang maglaman ng mga numero", + "error_text_wallet_name": "Ang pangalan ng pitaka ay maaari lamang maglaman ng mga titik, numero, _ - mga simbolo\nat dapat sa pagitan ng 1 at 33 character ang haba", + "error_text_keys": "Ang mga susi ng wallet ay maaari lamang maglaman ng 64 chars sa hex", + "error_text_crypto_currency": "Ang bilang ng mga numero ng fraction\ndapat mas mababa o katumbas ng 12", + "error_text_minimal_limit": "Ang kalakalan para sa ${provider} ay hindi nilikha. Ang halaga ay mas mababa pagkatapos ng minimal: ${min} ${currency}", + "error_text_maximum_limit": "Ang kalakalan para sa ${provider} ay hindi nilikha. Ang halaga ay higit na maximum: ${max} ${currency}", + "error_text_limits_loading_failed": "Ang kalakalan para sa ${provider} ay hindi nilikha. Nabigo ang mga limitasyon sa paglo -load", + "error_text_template": "Ang pangalan ng template at address ay hindi maaaring maglaman ng mga simbolo ng ',' \"\nat dapat sa pagitan ng 1 at 106 na character ang haba", + "auth_store_ban_timeout": "ban_timeout", + "auth_store_banned_for": "Pinagbawalan para sa", + "auth_store_banned_minutes": "minuto", + "auth_store_incorrect_password": "Maling pin", + "wallet_store_monero_wallet": "Monero Wallet", + "wallet_restoration_store_incorrect_seed_length": "Maling haba ng binhi", + "full_balance": "Buong balanse", + "available_balance": "Magagamit na balanse", + "hidden_balance": "Nakatagong balanse", + "sync_status_syncronizing": "Pag -synchronize", + "sync_status_syncronized": "Naka -synchronize", + "sync_status_not_connected": "HINDI KONEKTADO", + "sync_status_starting_sync": "Simula sa pag -sync", + "sync_status_failed_connect": "Naka -disconnect", + "sync_status_connecting": "Pagkonekta", + "sync_status_connected": "Konektado", + "sync_status_attempting_sync": "Pagtatangka ng pag -sync", + "transaction_priority_slow": "Mabagal", + "transaction_priority_regular": "Regular", + "transaction_priority_medium": "Katamtaman", + "transaction_priority_fast": "Mabilis", + "transaction_priority_fastest": "Pinakamabilis", + "trade_for_not_created": "Ang kalakalan para sa ${title} ay hindi nilikha.", + "trade_not_created": "Hindi nilikha ang kalakalan", + "trade_id_not_found": "Trade ${tradeId} ng ${title} Hindi natagpuan.", + "trade_not_found": "Hindi natagpuan ang kalakalan.", + "trade_state_pending": "Nakabinbin", + "trade_state_confirming": "Pagkumpirma", + "trade_state_trading": "Pangangalakal", + "trade_state_traded": "Ipinagpalit", + "trade_state_complete": "Kumpleto", + "trade_state_to_be_created": "Upang malikha", + "trade_state_unpaid": "Walang bayad", + "trade_state_underpaid": "Underpaid", + "trade_state_paid_unconfirmed": "Bayad na hindi nakumpirma", + "trade_state_paid": "Bayad", + "trade_state_btc_sent": "Ipinadala ang BTC", + "trade_state_timeout": "Oras ng oras", + "trade_state_created": "Nilikha", + "trade_state_finished": "Tapos na", + "change_language": "Baguhin ang wika", + "change_language_to": "Baguhin ang wika sa ${language}?", + "paste": "I -paste", + "restore_from_seed_placeholder": "Mangyaring ipasok o i -paste ang iyong binhi dito", + "add_new_word": "Magdagdag ng bagong salita", + "incorrect_seed": "Ang teksto na ipinasok ay hindi wasto.", + "biometric_auth_reason": "I -scan ang iyong fingerprint upang mapatunayan", + "version": "Bersyon ${currentVersion}", + "extracted_address_content": "Magpapadala ka ng pondo sa\n${recipient_name}", + "card_address": "Address:", + "buy": "Bilhin", + "sell": "Ibenta", + "placeholder_transactions": "Ang iyong mga transaksyon ay ipapakita dito", + "placeholder_contacts": "Ang iyong mga contact ay ipapakita dito", + "template": "Template", + "confirm_delete_template": "Ang pagkilos na ito ay tatanggalin ang template na ito. Nais mo bang magpatuloy?", + "confirm_delete_wallet": "Ang pagkilos na ito ay tatanggalin ang pitaka na ito. Nais mo bang magpatuloy?", + "change_wallet_alert_title": "Baguhin ang kasalukuyang pitaka", + "change_wallet_alert_content": "Nais mo bang baguhin ang kasalukuyang pitaka sa ${wallet_name}?", + "creating_new_wallet": "Lumilikha ng bagong pitaka", + "creating_new_wallet_error": "Error: ${description}", + "seed_alert_title": "Pansin", + "seed_alert_content": "Ang binhi ay ang tanging paraan upang mabawi ang iyong pitaka. Nasulat mo na ba ito?", + "seed_alert_back": "Bumalik ka", + "seed_alert_yes": "Oo meron ako", + "exchange_sync_alert_content": "Mangyaring maghintay hanggang ang iyong pitaka ay naka -synchronize", + "pre_seed_title": "Mahalaga", + "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_button_text": "Naiintindihan ko. Ipakita sa akin ang aking binhi", + "xmr_to_error": "Xmr.to error", + "xmr_to_error_description": "Di -wastong halaga. Pinakamataas na limitasyon ng 8 numero pagkatapos ng punto ng desimal", + "provider_error": "${provider} error", + "use_ssl": "Gumamit ng SSL", + "trusted": "Pinagkakatiwalaan", + "color_theme": "Tema ng kulay", + "light_theme": "Ilaw", + "bright_theme": "Maliwanag", + "dark_theme": "Madilim", + "enter_your_note": "Ipasok ang iyong tala ...", + "note_optional": "Tandaan (Opsyonal)", + "note_tap_to_change": "Tandaan (Tapikin upang baguhin)", + "view_in_block_explorer": "Tingnan sa Block Explorer", + "view_transaction_on": "Tingnan ang transaksyon sa", + "transaction_key": "Susi ng transaksyon", + "confirmations": "Mga kumpirmasyon", + "recipient_address": "Address ng tatanggap", + "extra_id": "Dagdag na ID:", + "destination_tag": "Tag ng patutunguhan:", + "memo": "Memo:", + "backup": "Backup", + "change_password": "Palitan ANG password", + "backup_password": "Backup password", + "write_down_backup_password": "Mangyaring isulat ang iyong backup password, na ginagamit para sa pag -import ng iyong mga backup file.", + "export_backup": "I -export ang backup", + "save_backup_password": "Mangyaring tiyaking nai -save mo ang iyong backup password. Hindi mo mai -import ang iyong mga backup na file nang wala ito.", + "backup_file": "Backup file", + "edit_backup_password": "I -edit ang backup password", + "save_backup_password_alert": "I -save ang backup password", + "change_backup_password_alert": "Ang iyong mga nakaraang backup file ay hindi magagamit upang mai -import gamit ang bagong backup password. Ang bagong backup na password ay gagamitin lamang para sa mga bagong backup file. Sigurado ka bang nais mong baguhin ang backup password?", + "enter_backup_password": "Ipasok ang backup password dito", + "select_backup_file": "Piliin ang backup file", + "import": "Angkat", + "please_select_backup_file": "Mangyaring piliin ang backup file at ipasok ang backup password.", + "fixed_rate": "Naayos na rate", + "fixed_rate_alert": "Magagawa mong ipasok ang makatanggap na halaga kapag naka -check ang naayos na mode ng rate. Nais mo bang lumipat sa nakapirming mode ng rate?", + "xlm_extra_info": "Mangyaring huwag kalimutan na tukuyin ang memo ID habang ipinapadala ang transaksyon ng XLM para sa palitan", + "xrp_extra_info": "Mangyaring huwag kalimutan na tukuyin ang patutunguhan na tag habang ipinapadala ang transaksyon ng XRP para sa palitan", + "exchange_incorrect_current_wallet_for_xmr": "Kung nais mong makipagpalitan ng XMR mula sa iyong balanse ng cake wallet Monero, mangyaring lumipat sa iyong Monero Wallet muna.", + "confirmed": "Nakumpirma na balanse", + "unconfirmed": "Hindi nakumpirma na balanse", + "displayable": "Maipapakita", + "submit_request": "magsumite ng isang kahilingan", + "buy_alert_content": "Sa kasalukuyan ay sinusuportahan lamang namin ang pagbili ng Bitcoin, Ethereum, Litecoin, at Monero. Mangyaring lumikha o lumipat sa iyong Bitcoin, Ethereum, Litecoin, o Monero Wallet.", + "sell_alert_content": "Kasalukuyan lamang naming sinusuportahan ang pagbebenta ng Bitcoin, Ethereum at Litecoin. Mangyaring lumikha o lumipat sa iyong Bitcoin, Ethereum o Litecoin Wallet.", + "outdated_electrum_wallet_description": "Ang mga bagong wallets ng Bitcoin na nilikha sa cake ay mayroon na ngayong 24-salitang binhi. Ipinag-uutos na lumikha ka ng isang bagong pitaka ng Bitcoin at ilipat ang lahat ng iyong mga pondo sa bagong 24-salitang pitaka, at itigil ang paggamit ng mga pitaka na may 12-salitang binhi. Mangyaring gawin ito kaagad upang ma -secure ang iyong mga pondo.", + "understand": "naiintindihan ko", + "apk_update": "APK Update", + "buy_bitcoin": "Bumili ng bitcoin", + "buy_with": "Bumili ka", + "moonpay_alert_text": "Ang halaga ng halaga ay dapat na higit pa o katumbas ng ${minAmount} ${fiatCurrency}", + "outdated_electrum_wallet_receive_warning": "Kung ang pitaka na ito ay may 12-salitang binhi at nilikha sa cake, huwag magdeposito sa Bitcoin sa pitaka na ito. Ang anumang BTC na inilipat sa pitaka na ito ay maaaring mawala. Lumikha ng isang bagong 24-word wallet (tapikin ang menu sa kanang tuktok, piliin ang mga pitaka, piliin ang Lumikha ng Bagong Wallet, pagkatapos ay piliin ang Bitcoin) at agad na ilipat ang iyong BTC doon. Ang mga bagong (24-salita) BTC Wallets mula sa cake ay ligtas", + "do_not_show_me": "Huwag mo itong ipakita muli", + "unspent_coins_title": "Unspent barya", + "unspent_coins_details_title": "Mga Detalye ng Unspent Coins", + "freeze": "I -freeze", + "frozen": "Frozen", + "coin_control": "Control ng barya (opsyonal)", + "address_detected": "Nakita ang address", + "address_from_domain": "Ang address na ito ay mula sa ${domain} sa mga hindi mapigilan na mga domain", + "add_receiver": "Magdagdag ng isa pang tatanggap (opsyonal)", + "manage_yats": "Pamahalaan ang mga yats", + "yat_alert_title": "Magpadala at tumanggap ng crypto nang mas madali sa yat", + "yat_alert_content": "Ang mga gumagamit ng cake wallet ay maaari na ngayong magpadala at makatanggap ng lahat ng kanilang mga paboritong pera na may isang one-of-a-kind emoji-based username.", + "get_your_yat": "Kunin ang iyong yat", + "connect_an_existing_yat": "Ikonekta ang isang umiiral na yat", + "connect_yats": "Ikonekta ang mga yats", + "yat_address": "Yat address", + "yat": "Yat", + "address_from_yat": "Ang address na ito ay mula sa ${emoji} sa yat", + "yat_error": "Error sa yat", + "yat_error_content": "Walang mga address na naka -link sa yat na ito. Subukan ang isa pang yat", + "choose_address": "Mangyaring piliin ang address:", + "yat_popup_title": "Ang iyong wallet address ay maaaring ma -emojified.", + "yat_popup_content": "Maaari ka na ngayong magpadala at makatanggap ng crypto sa cake wallet kasama ang iyong yat - isang maikli, emoji na batay sa username. Pamahalaan ang mga yats anumang oras sa screen ng Mga Setting", + "second_intro_title": "Isang address ng emoji upang mamuno sa kanilang lahat", + "second_intro_content": "Ang iyong yat ay isang solong natatanging address ng emoji na pumapalit sa lahat ng iyong mahabang hexadecimal address para sa lahat ng iyong mga pera.", + "third_intro_title": "Si Yat ay mahusay na gumaganap sa iba", + "third_intro_content": "Ang mga yats ay nakatira sa labas ng cake wallet, din. Ang anumang address ng pitaka sa mundo ay maaaring mapalitan ng isang yat!", + "learn_more": "Matuto nang higit pa", + "search": "Maghanap", + "search_language": "Maghanap ng wika", + "search_currency": "Maghanap ng pera", + "new_template": "Bagong template", + "electrum_address_disclaimer": "Bumubuo kami ng mga bagong address sa tuwing gumagamit ka ng isa, ngunit ang mga nakaraang address ay patuloy na gumagana", + "wallet_name_exists": "Ang isang pitaka na may pangalang iyon ay mayroon na. Mangyaring pumili ng ibang pangalan o palitan muna ang iba pang pitaka.", + "market_place": "Marketplace", + "cake_pay_title": "Cake pay card card", + "cake_pay_subtitle": "Bumili ng mga diskwento na gift card (USA lamang)", + "cake_pay_web_cards_title": "Cake pay web card", + "cake_pay_web_cards_subtitle": "Bumili ng mga pandaigdigang prepaid card at gift card", + "about_cake_pay": "Pinapayagan ka ng cake Pay na madaling bumili ng mga kard ng regalo na may mga virtual na pag -aari, gastusin agad sa higit sa 150,000 mga mangangalakal sa Estados Unidos.", + "cake_pay_account_note": "Mag -sign up na may isang email address lamang upang makita at bumili ng mga kard. Ang ilan ay magagamit kahit sa isang diskwento!", + "already_have_account": "Mayroon nang account?", + "create_account": "Lumikha ng account", + "privacy_policy": "Patakaran sa Pagkapribado", + "welcome_to_cakepay": "Maligayang pagdating sa cake pay!", + "sign_up": "Mag -sign up", + "forgot_password": "Nakalimutan ang password", + "reset_password": "I -reset ang password", + "gift_cards": "Mga kard ng regalo", + "setup_your_debit_card": "I -set up ang iyong debit card", + "no_id_required": "Walang kinakailangang ID. I -top up at gumastos kahit saan", + "how_to_use_card": "Paano gamitin ang kard na ito", + "purchase_gift_card": "Bumili ng Gift Card", + "verification": "Pag -verify", + "fill_code": "Mangyaring punan ang verification code na ibinigay sa iyong email", + "didnt_get_code": "Hindi nakuha ang code?", + "resend_code": "Mangyaring ipagpatuloy ito", + "debit_card": "Debit card", + "cakepay_prepaid_card": "Cakepay prepaid debit card", + "no_id_needed": "Hindi kailangan ng ID!", + "frequently_asked_questions": "Madalas na nagtanong", + "debit_card_terms": "Ang pag -iimbak at paggamit ng numero ng iyong card ng pagbabayad (at mga kredensyal na naaayon sa iyong numero ng card ng pagbabayad) sa digital na pitaka na ito ay napapailalim sa mga termino at kundisyon ng naaangkop na kasunduan sa cardholder kasama ang nagbigay ng card ng pagbabayad, tulad ng sa oras -oras.", + "please_reference_document": "Mangyaring sanggunian ang mga dokumento sa ibaba para sa karagdagang impormasyon.", + "cardholder_agreement": "Kasunduan sa Cardholder", + "e_sign_consent": "E-sign na pahintulot", + "agree_and_continue": "Sumang -ayon at magpatuloy", + "email_address": "Email address", + "agree_to": "Sa pamamagitan ng paglikha ng account sumasang -ayon ka sa", + "and": "at", + "enter_code": "Ipasok ang code", + "congratulations": "Binabati kita!", + "you_now_have_debit_card": "Mayroon ka na ngayong debit card", + "min_amount": "Min: ${value}", + "max_amount": "Max: ${value}", + "enter_amount": "Ipasok ang halaga", + "billing_address_info": "Kung tatanungin ang isang address ng pagsingil, ibigay ang iyong address sa pagpapadala", + "order_physical_card": "Mag -order ng pisikal na kard", + "add_value": "Magdagdag ng halaga", + "activate": "Buhayin", + "get_a": "Kumuha ng", + "digital_and_physical_card": "Digital at Physical Prepaid Debit Card", + "get_card_note": "na maaari mong i -reload sa mga digital na pera. Walang karagdagang impormasyon na kailangan!", + "signup_for_card_accept_terms": "Mag -sign up para sa card at tanggapin ang mga termino.", + "add_fund_to_card": "Magdagdag ng prepaid na pondo sa mga kard (hanggang sa ${value})", + "use_card_info_two": "Ang mga pondo ay na -convert sa USD kapag gaganapin sila sa prepaid account, hindi sa mga digital na pera.", + "use_card_info_three": "Gamitin ang digital card online o sa mga pamamaraan ng pagbabayad na walang contact.", + "optionally_order_card": "Opsyonal na mag -order ng isang pisikal na kard.", + "hide_details": "Itago ang mga detalye", + "show_details": "Ipakita ang mga detalye", + "upto": "Hanggang sa ${value}", + "discount": "Makatipid ng ${value}%", + "gift_card_amount": "Halaga ng Gift Card", + "bill_amount": "Halaga ng Bill", + "you_pay": "Magbabayad ka", + "tip": "Tip:", + "custom": "pasadya", + "by_cake_pay": "sa pamamagitan ng cake pay", + "expires": "Mag -expire", + "mm": "Mm", + "yy": "YY", + "online": "Online", + "offline": "Offline", + "gift_card_number": "Numero ng regalo card", + "pin_number": "Numero ng pin", + "total_saving": "Kabuuang pagtitipid", + "last_30_days": "Huling 30 araw", + "avg_savings": "Avg. Matitipid", + "view_all": "Tingnan lahat", + "active_cards": "Mga aktibong kard", + "delete_account": "Tanggalin ang account", + "cards": "Mga Card", + "active": "Aktibo", + "redeemed": "Tinubos", + "gift_card_balance_note": "Ang mga kard ng regalo na may natitirang balanse ay lilitaw dito", + "gift_card_redeemed_note": "Ang mga kard ng regalo na iyong tinubos ay lilitaw dito", + "logout": "Mag -logout", + "add_tip": "Magdagdag ng tip", + "percentageOf": "ng ${amount}", + "is_percentage": "ay", + "search_category": "Kategorya ng paghahanap", + "mark_as_redeemed": "Markahan bilang tinubos", + "more_options": "Higit pang mga pagpipilian", + "awaiting_payment_confirmation": "Naghihintay ng kumpirmasyon sa pagbabayad", + "transaction_sent_notice": "Kung ang screen ay hindi magpatuloy pagkatapos ng 1 minuto, suriin ang isang block explorer at ang iyong email.", + "agree": "Sumang -ayon", + "in_store": "Nakatago", + "generating_gift_card": "Bumubuo ng Gift Card", + "payment_was_received": "Natanggap ang iyong pagbabayad.", + "proceed_after_one_minute": "Kung ang screen ay hindi magpatuloy pagkatapos ng 1 minuto, suriin ang iyong email.", + "order_id": "Order id", + "gift_card_is_generated": "Ang card ng regalo ay nabuo", + "open_gift_card": "Buksan ang Gift Card", + "contact_support": "Makipag -ugnay sa suporta", + "gift_cards_unavailable": "Magagamit ang mga gift card para sa pagbili lamang kasama ang Monero, Bitcoin, at Litecoin sa oras na ito", + "background_sync_mode": "Mode ng pag -sync ng background", + "sync_all_wallets": "I -sync ang lahat ng mga pitaka", + "introducing_cake_pay": "Ipinakikilala ang cake pay!", + "cake_pay_learn_more": "Agad na bumili at tubusin ang mga kard ng regalo sa app!\nMag -swipe pakaliwa sa kanan upang matuto nang higit pa.", + "automatic": "Awtomatiko", + "fixed_pair_not_supported": "Ang nakapirming pares na ito ay hindi suportado sa mga napiling palitan", + "variable_pair_not_supported": "Ang variable na pares na ito ay hindi suportado sa mga napiling palitan", + "none_of_selected_providers_can_exchange": "Wala sa mga napiling tagapagkaloob na maaaring gumawa ng palitan na ito", + "choose_one": "Pumili ng isa", + "choose_from_available_options": "Pumili mula sa magagamit na mga pagpipilian:", + "custom_redeem_amount": "Pasadyang tinubos ang halaga", + "add_custom_redemption": "Magdagdag ng pasadyang pagtubos", + "remaining": "natitira", + "delete_wallet": "Tanggalin ang pitaka", + "delete_wallet_confirm_message": "Sigurado ka bang nais mong tanggalin ang ${wallet_name} wallet?", + "low_fee": "Mababang bayad", + "low_fee_alert": "Kasalukuyan kang gumagamit ng isang mababang priyoridad sa bayad sa network. Maaari itong maging sanhi ng mahabang paghihintay, iba't ibang mga rate, o kanselahin ang mga trading. Inirerekumenda namin ang pagtatakda ng isang mas mataas na bayad para sa isang mas mahusay na karanasan.", + "ignor": "Huwag pansinin", + "use_suggested": "Gumamit ng iminungkahing", + "do_not_share_warning_text": "Huwag ibahagi ang mga ito sa sinumang iba pa, kabilang ang suporta.\n\nAng iyong mga pondo ay maaari at ninakaw!", + "help": "Tulong", + "all_transactions": "Lahat ng mga transaksyon", + "all_trades": "Lahat ng mga kalakal", + "connection_sync": "Koneksyon at pag -sync", + "security_and_backup": "Seguridad at backup", + "create_backup": "Gumawa ng backup", + "privacy_settings": "Settings para sa pagsasa-pribado", + "privacy": "Privacy", + "display_settings": "Mga setting ng pagpapakita", + "other_settings": "Iba pang mga setting", + "auto_generate_subaddresses": "Ang Auto ay bumubuo ng mga subaddresses", + "require_pin_after": "Nangangailangan ng pin pagkatapos", + "always": "Palagi", + "minutes_to_pin_code": "${minute} minuto", + "disable_exchange": "Huwag paganahin ang palitan", + "advanced_privacy_settings": "Mga setting ng advanced na privacy", + "settings_can_be_changed_later": "Ang mga setting na ito ay maaaring mabago mamaya sa mga setting ng app", + "add_custom_node": "Magdagdag ng bagong pasadyang node", + "disable_fiat": "Huwag paganahin ang Fiat", + "fiat_api": "Fiat API", + "disabled": "Hindi pinagana", + "enabled": "Pinagana", + "tor_only": "Tor lang", + "unmatched_currencies": "Ang pera ng iyong kasalukuyang pitaka ay hindi tumutugma sa na -scan na QR", + "orbot_running_alert": "Mangyaring tiyakin na ang Orbot ay tumatakbo bago kumonekta sa node na ito.", + "contact_list_contacts": "Mga contact", + "contact_list_wallets": "Ang mga wallets ko", + "bitcoin_payments_require_1_confirmation": "Ang mga pagbabayad sa Bitcoin ay nangangailangan ng 1 kumpirmasyon, na maaaring tumagal ng 20 minuto o mas mahaba. Salamat sa iyong pasensya! Mag -email ka kapag nakumpirma ang pagbabayad.", + "send_to_this_address": "Magpadala ng ${currency} ${tag} sa address na ito", + "arrive_in_this_address": "Ang ${currency} ${tag} ay darating sa address na ito", + "do_not_send": "Huwag magpadala", + "error_dialog_content": "Oops, nakakuha kami ng ilang error.\n\nMangyaring ipadala ang ulat ng pag -crash sa aming koponan ng suporta upang maging mas mahusay ang application.", + "scan_qr_code": "I -scan ang QR Code", + "cold_or_recover_wallet": "Magdagdag ng isang malamig na pitaka o mabawi ang isang wallet ng papel", + "please_wait": "Mangyaring maghintay", + "sweeping_wallet": "Pagwawalis ng pitaka", + "sweeping_wallet_alert": "Hindi ito dapat magtagal. Huwag iwanan ang screen na ito o maaaring mawala ang mga pondo ng swept.", + "invoice_details": "Mga detalye ng invoice", + "donation_link_details": "Mga Detalye ng Link ng Donasyon", + "anonpay_description": "Bumuo ng ${type}. Ang tatanggap ay maaaring ${method} na may anumang suportadong cryptocurrency, at makakatanggap ka ng mga pondo sa pitaka na ito.", + "create_invoice": "Lumikha ng invoice", + "create_donation_link": "Lumikha ng link ng donasyon", + "optional_email_hint": "Opsyonal na Payee Notification Email", + "optional_description": "Opsyonal na paglalarawan", + "optional_name": "Opsyonal na pangalan ng tatanggap", + "clearnet_link": "Link ng Clearnet", + "onion_link": "Link ng Onion", + "decimal_places_error": "Masyadong maraming mga lugar na desimal", + "edit_node": "I -edit ang node", + "settings": "Mga setting", + "sell_monero_com_alert_content": "Ang pagbebenta ng Monero ay hindi pa suportado", + "error_text_input_below_minimum_limit": "Ang halaga ay mas mababa sa minimum", + "error_text_input_above_maximum_limit": "Ang halaga ay higit pa sa maximum", + "show_market_place": "Ipakita ang Marketplace", + "prevent_screenshots": "Maiwasan ang mga screenshot at pag -record ng screen", + "profile": "Profile", + "close": "Malapit", + "modify_2fa": "Baguhin ang cake 2FA", + "disable_cake_2fa": "Huwag paganahin ang cake 2FA", + "question_to_disable_2fa": "Sigurado ka bang nais mong huwag paganahin ang cake 2fa? Ang isang 2FA code ay hindi na kinakailangan upang ma -access ang pitaka at ilang mga pag -andar.", + "disable": "Huwag paganahin", + "setup_2fa": "Setup cake 2fa", + "verify_with_2fa": "Mag -verify sa cake 2FA", + "totp_code": "TOTP code", + "please_fill_totp": "Mangyaring punan ang 8-digit na code na naroroon sa iyong iba pang aparato", + "totp_2fa_success": "Tagumpay! Pinagana ang cake 2FA para sa pitaka na ito. Tandaan na i -save ang iyong mnemonic seed kung sakaling mawalan ka ng pag -access sa pitaka.", + "totp_verification_success": "Matagumpay ang pagpapatunay!", + "totp_2fa_failure": "Maling code. Mangyaring subukan ang ibang code o makabuo ng isang bagong lihim na susi. Gumamit ng isang katugmang 2FA app na sumusuporta sa 8-digit na mga code at SHA512.", + "enter_totp_code": "Mangyaring ipasok ang TOTP code.", + "add_secret_code": "Idagdag ang lihim na code na ito sa isa pang aparato", + "totp_secret_code": "TOTP Secret Code", + "important_note": "Mahalagang paalaala", + "setup_2fa_text": "Ang cake 2FA ay hindi ligtas tulad ng malamig na imbakan. Pinoprotektahan ng 2FA laban sa mga pangunahing uri ng pag -atake, tulad ng iyong kaibigan na nagbibigay ng iyong fingerprint habang natutulog ka.\n\n Hindi pinoprotektahan ng cake 2FA laban sa isang nakompromiso na aparato ng isang sopistikadong umaatake.\n\n Kung nawalan ka ng pag -access sa iyong 2FA code, mawawalan ka ng access sa pitaka na ito. Kakailanganin mong ibalik ang iyong pitaka mula sa binhi ng mnemonic. Dapat mong i -back up ang iyong mga buto ng mnemonic! Dagdag pa, ang isang tao na may access sa iyong (mga) binhi ng mnemonic ay maaaring magnakaw ng iyong mga pondo, na lumampas sa cake 2FA.\n\n Ang mga kawani ng suporta sa cake ay hindi makakatulong sa iyo kung nawalan ka ng pag -access sa iyong mnemonic seed, dahil ang cake ay isang noncustodial wallet.", + "setup_totp_recommended": "I -set up ang TOTP (inirerekomenda)", + "disable_buy": "Huwag paganahin ang pagkilos ng pagbili", + "disable_sell": "Huwag paganahin ang pagkilos ng pagbebenta", + "monero_dark_theme": "Monero Madilim na Tema", + "bitcoin_dark_theme": "Bitcoin Madilim na Tema", + "bitcoin_light_theme": "Tema ng ilaw ng bitcoin", + "high_contrast_theme": "Mataas na tema ng kaibahan", + "matrix_green_dark_theme": "Matrix Green Madilim na Tema", + "monero_light_theme": "Tema ng Monero Light", + "cake_2fa_preset": "Cake 2fa preset", + "narrow": "Makitid", + "normal": "Normal", + "aggressive": "Agresibo", + "require_for_assessing_wallet": "Nangangailangan para sa pag -access ng pitaka", + "require_for_sends_to_non_contacts": "Nangangailangan para sa pagpapadala sa mga hindi contact", + "require_for_sends_to_contacts": "Nangangailangan para sa pagpapadala sa mga contact", + "require_for_sends_to_internal_wallets": "Nangangailangan para sa pagpapadala sa mga panloob na mga pitaka", + "require_for_exchanges_to_internal_wallets": "Nangangailangan para sa mga palitan sa mga panloob na mga pitaka", + "require_for_adding_contacts": "Nangangailangan para sa pagdaragdag ng mga contact", + "require_for_creating_new_wallets": "Nangangailangan para sa paglikha ng mga bagong pitaka", + "require_for_all_security_and_backup_settings": "Nangangailangan para sa lahat ng mga setting ng seguridad at backup", + "available_balance_description": "Ang \"magagamit na balanse\" o \"nakumpirma na balanse\" ay mga pondo na maaaring gastusin kaagad. Kung lumilitaw ang mga pondo sa mas mababang balanse ngunit hindi ang nangungunang balanse, dapat kang maghintay ng ilang minuto para sa mga papasok na pondo upang makakuha ng mas maraming mga kumpirmasyon sa network. Matapos silang makakuha ng higit pang mga kumpirmasyon, gugugol sila.", + "syncing_wallet_alert_title": "Ang iyong pitaka ay nag -sync", + "syncing_wallet_alert_content": "Ang iyong balanse at listahan ng transaksyon ay maaaring hindi kumpleto hanggang sa sabihin nito na \"naka -synchronize\" sa tuktok. Mag -click/tap upang malaman ang higit pa.", + "home_screen_settings": "Mga setting ng home screen", + "sort_by": "Pag -uri -uriin sa pamamagitan ng", + "search_add_token": "Maghanap / Magdagdag ng Token", + "edit_token": "I -edit ang token", + "warning": "Babala", + "add_token_warning": "Huwag i -edit o magdagdag ng mga token tulad ng itinuro ng mga scammers.\nLaging kumpirmahin ang mga token address na may mga kagalang -galang na mapagkukunan!", + "add_token_disclaimer_check": "Kinumpirma ko ang address ng kontrata ng token at impormasyon gamit ang isang kagalang -galang na mapagkukunan. Ang pagdaragdag ng nakakahamak o hindi tamang impormasyon ay maaaring magresulta sa pagkawala ng mga pondo.", + "token_contract_address": "Token Address ng Kontrata", + "token_name": "Pangalan ng Token hal: Tether", + "token_symbol": "Simbolo ng token hal: USDT", + "token_decimal": "Token Decimal", + "field_required": "Kinakailangan ang patlang na ito", + "pin_at_top": "Pin ${token} sa tuktok", + "invalid_input": "Di -wastong input", + "fiat_balance": "Balanse ng fiat", + "gross_balance": "Balanse ng gross", + "alphabetical": "Alpabeto", + "generate_name": "Bumuo ng pangalan", + "balance_page": "Pahina ng Balanse", + "share": "Ibahagi", + "slidable": "Slidable", + "manage_nodes": "Pamahalaan ang mga node", + "etherscan_history": "Kasaysayan ng Etherscan", + "template_name": "Pangalan ng Template", + "support_title_live_chat": "Live na suporta", + "support_description_live_chat": "Libre at mabilis! Ang mga bihasang kinatawan ng suporta ay magagamit upang tulungan", + "support_title_guides": "Mga Gabay sa Wallet ng cake", + "support_description_guides": "Dokumentasyon at suporta para sa mga karaniwang isyu", + "support_title_other_links": "Iba pang mga link sa suporta", + "support_description_other_links": "Sumali sa aming mga komunidad o maabot sa amin ang aming mga kasosyo sa pamamagitan ng iba pang mga pamamaraan", + "select_destination": "Mangyaring piliin ang patutunguhan para sa backup file.", + "save_to_downloads": "I -save sa mga pag -download", + "select_buy_provider_notice": "Pumili ng provider ng pagbili sa itaas. Maaari mong laktawan ang screen na ito sa pamamagitan ng pagtatakda ng iyong default na provider ng pagbili sa mga setting ng app.", + "onramper_option_description": "Mabilis na bumili ng crypto na may maraming paraan ng pagbabayad. Available sa karamihan ng mga bansa. Iba-iba ang mga spread at bayarin.", + "default_buy_provider": "Default na Provider ng Pagbili", + "ask_each_time": "Magtanong sa bawat oras", + "robinhood_option_description": "Bumili at ilipat kaagad gamit ang iyong debit card, bank account, o balanse ng Robinhood. USA lang.", + "buy_provider_unavailable": "Kasalukuyang hindi available ang provider.", + "signTransaction": "Mag-sign Transaksyon", + "errorGettingCredentials": "Nabigo: Error habang kumukuha ng mga kredensyal", + "errorSigningTransaction": "May naganap na error habang pinipirmahan ang transaksyon", + "pairingInvalidEvent": "Pagpares ng Di-wastong Kaganapan", + "chains": "Mga tanikala", + "methods": "Paraan", + "events": "Mga kaganapan", + "reject": "Tanggihan", + "approve": "Aprubahan", + "expiresOn": "Mag-e-expire sa", + "walletConnect": "WalletConnect", + "nullURIError": "Ang URI ay null", + "connectWalletPrompt": "Ikonekta ang iyong wallet sa WalletConnect upang gumawa ng mga transaksyon", + "newConnection": "Bagong Koneksyon", + "activeConnectionsPrompt": "Lalabas dito ang mga aktibong koneksyon", + "deleteConnectionConfirmationPrompt": "Sigurado ka bang gusto mong tanggalin ang koneksyon sa", + "event": "Kaganapan", + "successful": "Matagumpay", + "wouoldLikeToConnect": "gustong kumonekta", + "message": "Mensahe", + "do_not_have_enough_gas_asset": "Wala kang sapat na ${currency} para gumawa ng transaksyon sa kasalukuyang kundisyon ng network ng blockchain. Kailangan mo ng higit pang ${currency} upang magbayad ng mga bayarin sa network ng blockchain, kahit na nagpapadala ka ng ibang asset.", + "totp_auth_url": "TOTP AUTH URL", + "awaitDAppProcessing": "Pakihintay na matapos ang pagproseso ng dApp.", + "copyWalletConnectLink": "Kopyahin ang link ng WalletConnect mula sa dApp at i-paste dito", + "enterWalletConnectURI": "Ilagay ang WalletConnect URI", + "seed_key": "Seed Key", + "enter_seed_phrase": "Ipasok ang iyong pariralang binhi", + "change_rep_successful": "Matagumpay na nagbago ng kinatawan", + "add_contact": "Magdagdag ng contact", + "exchange_provider_unsupported": "Ang ${providerName} ay hindi na suportado!", + "domain_looks_up": "Mga paghahanap ng domain", + "require_for_exchanges_to_external_wallets": "Kinakailangan para sa mga palitan sa mga panlabas na wallet", + "camera_permission_is_required": "Kinakailangan ang pahintulot sa camera.\nMangyaring paganahin ito mula sa mga setting ng app.", + "switchToETHWallet": "Mangyaring lumipat sa isang Ethereum wallet at subukang muli", + "seed_phrase_length": "Haba ng parirala ng binhi", + "unavailable_balance": "Hindi available na balanse", + "unavailable_balance_description": "Hindi Available na Balanse: Kasama sa kabuuang ito ang mga pondong naka-lock sa mga nakabinbing transaksyon at ang mga aktibong na-freeze mo sa iyong mga setting ng kontrol ng coin. Magiging available ang mga naka-lock na balanse kapag nakumpleto na ang kani-kanilang mga transaksyon, habang ang mga nakapirming balanse ay nananatiling hindi naa-access para sa mga transaksyon hanggang sa magpasya kang i-unfreeze ang mga ito.", + "unspent_change": "Baguhin", + "tor_connection": "Koneksyon ng Tor" +} diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index 9185183e5..1b7982504 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -339,7 +339,6 @@ "template": "Şablon", "confirm_delete_template": "Bu eylem, bu şablonu silecek. Devam etmek istiyor musun?", "confirm_delete_wallet": "Bu eylem, bu cüzdanı silecek. Devam etmek istiyor musun?", - "picker_description": "ChangeNOW veya MorphToken'ı seçmek için lütfen önce işlem paritenizi değiştirin", "change_wallet_alert_title": "Şimdiki cüzdanı değiştir", "change_wallet_alert_content": "Şimdiki cüzdanı ${wallet_name} cüzdanı ile değiştirmek istediğinden emin misin?", "creating_new_wallet": "Cüzdan oluşturuluyor", @@ -594,7 +593,6 @@ "sweeping_wallet_alert": "Bu uzun sürmemeli. BU EKRANDAN BIRAKMAYIN YOKSA SÜPÜRÜLEN FONLAR KAYBOLABİLİR", "decimal_places_error": "Çok fazla ondalık basamak", "edit_node": "Düğümü Düzenle", - "frozen_balance": "Dondurulmuş Bakiye", "invoice_details": "fatura detayları", "donation_link_details": "Bağış bağlantısı ayrıntıları", "anonpay_description": "${type} oluşturun. Alıcı, desteklenen herhangi bir kripto para birimi ile ${method} yapabilir ve bu cüzdanda para alırsınız.", @@ -691,5 +689,49 @@ "choose_derivation": "Cüzdan türevini seçin", "new_first_wallet_text": "Kripto para biriminizi kolayca güvende tutun", "select_destination": "Lütfen yedekleme dosyası için hedef seçin.", - "save_to_downloads": "İndirilenlere Kaydet" + "save_to_downloads": "İndirilenlere Kaydet", + "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.", + "onramper_option_description": "Birçok ödeme yöntemi ile hızlı bir şekilde kripto satın alın. Çoğu ülkede mevcuttur. Forma ve ücretler değişir.", + "default_buy_provider": "Varsayılan Satın Alma Sağlayıcısı", + "ask_each_time": "Her seferinde sor", + "buy_provider_unavailable": "Sağlayıcı şu anda kullanılamıyor.", + "signTransaction": "İşlem İmzala", + "errorGettingCredentials": "Başarısız: Kimlik bilgileri alınırken hata oluştu", + "errorSigningTransaction": "İşlem imzalanırken bir hata oluştu", + "pairingInvalidEvent": "Geçersiz Etkinliği Eşleştirme", + "chains": "Zincirler", + "methods": "Yöntemler", + "events": "Olaylar", + "reject": "Reddetmek", + "approve": "Onaylamak", + "expiresOn": "Tarihinde sona eriyor", + "walletConnect": "WalletConnect", + "nullURIError": "URI boş", + "connectWalletPrompt": "İşlem yapmak için cüzdanınızı WalletConnect'e bağlayın", + "newConnection": "Yeni bağlantı", + "activeConnectionsPrompt": "Aktif bağlantılar burada görünecek", + "deleteConnectionConfirmationPrompt": "Bağlantıyı silmek istediğinizden emin misiniz?", + "event": "Etkinlik", + "successful": "Başarılı", + "wouoldLikeToConnect": "bağlanmak istiyorum", + "message": "İleti", + "do_not_have_enough_gas_asset": "Mevcut blockchain ağ koşullarıyla işlem yapmak için yeterli ${currency} paranız yok. Farklı bir varlık gönderiyor olsanız bile blockchain ağ ücretlerini ödemek için daha fazla ${currency} miktarına ihtiyacınız var.", + "totp_auth_url": "TOTP YETKİ URL'si", + "awaitDAppProcessing": "Lütfen dApp'in işlemeyi bitirmesini bekleyin.", + "copyWalletConnectLink": "WalletConnect bağlantısını dApp'ten kopyalayıp buraya yapıştırın", + "enterWalletConnectURI": "WalletConnect URI'sini girin", + "seed_key": "Tohum", + "enter_seed_phrase": "Tohum ifadenizi girin", + "change_rep_successful": "Temsilciyi başarıyla değiştirdi", + "add_contact": "Kişi ekle", + "exchange_provider_unsupported": "${providerName} artık desteklenmiyor!", + "domain_looks_up": "Etki alanı aramaları", + "require_for_exchanges_to_external_wallets": "Harici cüzdanlara geçiş yapılmasını zorunlu kılın", + "camera_permission_is_required": "Kamera izni gereklidir.\nLütfen uygulama ayarlarından etkinleştirin.", + "switchToETHWallet": "Lütfen bir Ethereum cüzdanına geçin ve tekrar deneyin", + "seed_phrase_length": "Çekirdek cümle uzunluğu", + "unavailable_balance": "Kullanılamayan bakiye", + "unavailable_balance_description": "Kullanılamayan Bakiye: Bu toplam, bekleyen işlemlerde kilitlenen fonları ve jeton kontrol ayarlarınızda aktif olarak dondurduğunuz fonları içerir. Kilitli bakiyeler, ilgili işlemleri tamamlandıktan sonra kullanılabilir hale gelir; dondurulmuş bakiyeler ise siz onları dondurmaya karar verene kadar işlemler için erişilemez durumda kalır.", + "unspent_change": "Değiştirmek", + "tor_connection": "Tor bağlantısı" } diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index 694942833..849542e6f 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -339,7 +339,6 @@ "template": "Шаблон", "confirm_delete_template": "Ця дія видалить шаблон. Ви хочете продовжити?", "confirm_delete_wallet": "Ця дія видалить гаманець. Ви хочете продовжити?", - "picker_description": "Щоб вибрати ChangeNOW або MorphToken, спочатку змініть пару для обміну", "change_wallet_alert_title": "Змінити поточний гаманець", "change_wallet_alert_content": "Ви хочете змінити поточний гаманець на ${wallet_name}?", "creating_new_wallet": "Створення нового гаманця", @@ -596,7 +595,6 @@ "sweeping_wallet_alert": "Це не повинно зайняти багато часу. НЕ ЗАЛИШАЙТЕ ЦЬОГО ЕКРАНУ, АБО КОШТИ МОЖУТЬ БУТИ ВТРАЧЕНІ", "decimal_places_error": "Забагато знаків після коми", "edit_node": "Редагувати вузол", - "frozen_balance": "Заморожений баланс", "invoice_details": "Реквізити рахунку-фактури", "donation_link_details": "Деталі посилання для пожертв", "anonpay_description": "Згенерувати ${type}. Одержувач може ${method} будь-якою підтримуваною криптовалютою, і ви отримаєте кошти на цей гаманець.", @@ -693,5 +691,49 @@ "choose_derivation": "Виберіть деривацію гаманця", "new_first_wallet_text": "Легко зберігайте свою криптовалюту в безпеці", "select_destination": "Виберіть місце призначення для файлу резервної копії.", - "save_to_downloads": "Зберегти до завантажень" + "save_to_downloads": "Зберегти до завантажень", + "select_buy_provider_notice": "Виберіть постачальника купівлі вище. Ви можете пропустити цей екран, встановивши свого постачальника купівлі за замовчуванням у налаштуваннях додатків.", + "onramper_option_description": "Швидко купуйте криптовалюту з багатьма методами оплати. Доступний у більшості країн. Поширення та збори різняться.", + "default_buy_provider": "Постачальник покупки за замовчуванням", + "ask_each_time": "Запитайте кожен раз", + "buy_provider_unavailable": "В даний час постачальник недоступний.", + "signTransaction": "Підписати транзакцію", + "errorGettingCredentials": "Помилка: помилка під час отримання облікових даних", + "errorSigningTransaction": "Під час підписання транзакції сталася помилка", + "pairingInvalidEvent": "Недійсна подія сполучення", + "chains": "Ланцюги", + "methods": "методи", + "events": "Події", + "reject": "Відхиляти", + "approve": "Затвердити", + "expiresOn": "Термін дії закінчується", + "walletConnect": "WalletConnect", + "nullURIError": "URI нульовий", + "connectWalletPrompt": "Підключіть свій гаманець до WalletConnect, щоб здійснювати транзакції", + "newConnection": "Нове підключення", + "activeConnectionsPrompt": "Тут з’являться активні підключення", + "deleteConnectionConfirmationPrompt": "Ви впевнені, що хочете видалити з’єднання з", + "event": "Подія", + "successful": "Успішний", + "wouoldLikeToConnect": "хотів би підключитися", + "message": "повідомлення", + "do_not_have_enough_gas_asset": "У вас недостатньо ${currency}, щоб здійснити трансакцію з поточними умовами мережі блокчейн. Вам потрібно більше ${currency}, щоб сплатити комісію мережі блокчейн, навіть якщо ви надсилаєте інший актив.", + "totp_auth_url": "TOTP AUTH URL", + "awaitDAppProcessing": "Зачекайте, доки dApp завершить обробку.", + "copyWalletConnectLink": "Скопіюйте посилання WalletConnect із dApp і вставте сюди", + "enterWalletConnectURI": "Введіть URI WalletConnect", + "seed_key": "Насіннєвий ключ", + "enter_seed_phrase": "Введіть свою насіннєву фразу", + "change_rep_successful": "Успішно змінив представник", + "add_contact": "Додати контакт", + "exchange_provider_unsupported": "${providerName} більше не підтримується!", + "domain_looks_up": "Пошук доменів", + "require_for_exchanges_to_external_wallets": "Потрібен для обміну на зовнішні гаманці", + "camera_permission_is_required": "Потрібен дозвіл камери.\nУвімкніть його в налаштуваннях програми.", + "switchToETHWallet": "Перейдіть на гаманець Ethereum і повторіть спробу", + "seed_phrase_length": "Довжина початкової фрази", + "unavailable_balance": "Недоступний баланс", + "unavailable_balance_description": "Недоступний баланс: ця сума включає кошти, заблоковані в незавершених транзакціях, і ті, які ви активно заморозили в налаштуваннях контролю монет. Заблоковані баланси стануть доступними після завершення відповідних транзакцій, тоді як заморожені баланси залишаються недоступними для транзакцій, доки ви не вирішите їх розморозити.", + "unspent_change": "Зміна", + "tor_connection": "Підключення Tor" } diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index 23586d159..0b7e4c0a7 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -340,7 +340,6 @@ "template": "سانچے", "confirm_delete_template": "یہ عمل اس ٹیمپلیٹ کو حذف کر دے گا۔ کیا آپ جاری رکھنا چاہتے ہیں؟", "confirm_delete_wallet": "اس کارروائی سے یہ پرس حذف ہو جائے گا۔ کیا آپ جاری رکھنا چاہتے ہیں؟", - "picker_description": "ChangeNOW یا MorphToken کو منتخب کرنے کے لیے، براہ کرم پہلے اپنا تجارتی جوڑا تبدیل کریں۔", "change_wallet_alert_title": "موجودہ پرس تبدیل کریں۔", "change_wallet_alert_content": "کیا آپ موجودہ والیٹ کو ${wallet_name} میں تبدیل کرنا چاہتے ہیں؟", "creating_new_wallet": "نیا پرس بنانا", @@ -589,7 +588,6 @@ "error_dialog_content": "افوہ، ہمیں کچھ خرابی ملی۔\n\nایپلی کیشن کو بہتر بنانے کے لیے براہ کرم کریش رپورٹ ہماری سپورٹ ٹیم کو بھیجیں۔", "decimal_places_error": "بہت زیادہ اعشاریہ جگہیں۔", "edit_node": "نوڈ میں ترمیم کریں۔", - "frozen_balance": "منجمد بیلنس", "invoice_details": "رسید کی تفصیلات", "donation_link_details": "عطیہ کے لنک کی تفصیلات", "anonpay_description": "${type} بنائیں۔ وصول کنندہ کسی بھی تعاون یافتہ کرپٹو کرنسی کے ساتھ ${method} کرسکتا ہے، اور آپ کو اس بٹوے میں فنڈز موصول ہوں گے۔", @@ -684,6 +682,50 @@ "choose_derivation": "پرس سے ماخوذ منتخب کریں", "new_first_wallet_text": "آسانی سے اپنے cryptocurrency محفوظ رکھیں", "select_destination": "۔ﮟﯾﺮﮐ ﺏﺎﺨﺘﻧﺍ ﺎﮐ ﻝﺰﻨﻣ ﮯﯿﻟ ﮯﮐ ﻞﺋﺎﻓ ﭖﺍ ﮏﯿﺑ ﻡﺮﮐ ﮦﺍﺮﺑ", + "auto_generate_subaddresses": "آٹو سب ایڈریس تیار کرتا ہے", "save_to_downloads": "۔ﮟﯾﺮﮐ ﻅﻮﻔﺤﻣ ﮟﯿﻣ ﺯﮈﻮﻟ ﻥﺅﺍﮈ", - "auto_generate_subaddresses": "آٹو سب ایڈریس تیار کرتا ہے" + "select_buy_provider_notice": "اوپر خریدنے والا خریدنے والا منتخب کریں۔ آپ ایپ کی ترتیبات میں اپنے پہلے سے طے شدہ خریدنے والے کو ترتیب دے کر اس اسکرین کو چھوڑ سکتے ہیں۔", + "onramper_option_description": "ادائیگی کے بہت سے طریقوں سے جلدی سے کرپٹو خریدیں۔ زیادہ تر ممالک میں دستیاب ہے۔ پھیلاؤ اور فیس مختلف ہوتی ہے۔", + "default_buy_provider": "پہلے سے طے شدہ خریدنے والا", + "ask_each_time": "ہر بار پوچھیں", + "buy_provider_unavailable": "فراہم کنندہ فی الحال دستیاب نہیں ہے۔", + "signTransaction": "۔ﮟﯾﺮﮐ ﻂﺨﺘﺳﺩ ﺮﭘ ﻦﯾﺩ ﻦﯿﻟ", + "errorGettingCredentials": "۔ﯽﺑﺍﺮﺧ ﮟﯿﻣ ﮯﻧﺮﮐ ﻞﺻﺎﺣ ﺩﺎﻨﺳﺍ :ﻡﺎﮐﺎﻧ", + "errorSigningTransaction": "۔ﮯﮨ ﯽﺌﮔﺁ ﺶﯿﭘ ﯽﺑﺍﺮﺧ ﮏﯾﺍ ﺖﻗﻭ ﮯﺗﺮﮐ ﻂﺨﺘﺳﺩ ﺮﭘ ﻦﯾﺩ ﻦﯿﻟ", + "pairingInvalidEvent": "ﭧﻧﻮﯾﺍ ﻂﻠﻏ ﺎﻧﺎﻨﺑ ﺍﮌﻮﺟ", + "chains": "ﮟﯾﺮﯿﺠﻧﺯ", + "methods": "ﮯﻘﯾﺮﻃ", + "events": "ﺕﺎﺒﯾﺮﻘﺗ", + "reject": "ﺎﻧﺮﮐ ﺩﺭ", + "approve": "ﻭﺮﮐ ﺭﻮﻈﻨﻣ", + "expiresOn": "ﺩﺎﻌﯿﻣ ﯽﻣﺎﺘﺘﺧﺍ", + "walletConnect": "WalletConnect", + "nullURIError": "URI ۔ﮯﮨ ﻡﺪﻌﻟﺎﮐ", + "connectWalletPrompt": "۔ﮟﯾﮌﻮﺟ ﮫﺗﺎﺳ ﮯﮐ WalletConnect ﻮﮐ ﮮﻮﭩﺑ ﮯﻨﭘﺍ ﮯﯿﻟ ﮯﮐ ﮯﻧﺮﮐ ﻦﯾﺩ ﻦﯿﻟ", + "newConnection": "ﻦﺸﮑﻨﮐ ﺎﯿﻧ", + "activeConnectionsPrompt": "۔ﮯﮔ ﮞﻮﮨ ﺮﮨﺎﻇ ﮞﺎﮩﯾ ﺰﻨﺸﮑﻨﮐ ﻝﺎﻌﻓ", + "deleteConnectionConfirmationPrompt": "۔ﮟﯿﮨ ﮯﺘﮨﺎﭼ ﺎﻧﺮﮐ ﻑﺬﺣ ﻮﮐ ﻦﺸﮑﻨﮐ ﭖﺁ ﮧﮐ ﮯﮨ ﻦﯿﻘﯾ ﻮﮐ ﭖﺁ ﺎﯿﮐ", + "event": "ﺐﯾﺮﻘﺗ", + "successful": "ﺏﺎﯿﻣﺎﮐ", + "wouoldLikeToConnect": "؟ﮯﮔ ﮟﯿﮨﺎﭼ ﺎﻧﮍﺟ", + "message": "ﻡﺎﻐﯿﭘ", + "do_not_have_enough_gas_asset": "آپ کے پاس موجودہ بلاکچین نیٹ ورک کی شرائط کے ساتھ لین دین کرنے کے لیے کافی ${currency} نہیں ہے۔ آپ کو بلاکچین نیٹ ورک کی فیس ادا کرنے کے لیے مزید ${currency} کی ضرورت ہے، چاہے آپ کوئی مختلف اثاثہ بھیج رہے ہوں۔", + "totp_auth_url": "TOTP AUTH URL", + "awaitDAppProcessing": "۔ﮟﯾﺮﮐ ﺭﺎﻈﺘﻧﺍ ﺎﮐ ﮯﻧﻮﮨ ﻞﻤﮑﻣ ﮓﻨﺴﯿﺳﻭﺮﭘ ﮯﮐ dApp ﻡﺮﮐ ﮦﺍﺮﺑ", + "copyWalletConnectLink": "dApp ﮯﺳ WalletConnect ۔ﮟﯾﺮﮐ ﭧﺴﯿﭘ ﮞﺎﮩﯾ ﺭﻭﺍ ﮟﯾﺮﮐ ﯽﭘﺎﮐ ﻮﮐ ﮏﻨﻟ", + "enterWalletConnectURI": "WalletConnect URI ۔ﮟﯾﺮﮐ ﺝﺭﺩ", + "seed_key": "بیج کی کلید", + "enter_seed_phrase": "اپنے بیج کا جملہ درج کریں", + "change_rep_successful": "نمائندہ کو کامیابی کے ساتھ تبدیل کیا", + "add_contact": "۔ﮟﯾﺮﮐ ﻞﻣﺎﺷ ﮧﻄﺑﺍﺭ", + "exchange_provider_unsupported": "${providerName} اب تعاون نہیں کیا جاتا ہے!", + "domain_looks_up": "ڈومین تلاش کرنا", + "require_for_exchanges_to_external_wallets": "۔ﮯﮨ ﺕﺭﻭﺮﺿ ﯽﮐ ﮯﻟﺩﺎﺒﺗ ﮟﯿﻣ ﮮﻮﭩﺑ ﯽﻧﻭﺮﯿﺑ", + "camera_permission_is_required": "۔ﮯﮨ ﺭﺎﮐﺭﺩ ﺕﺯﺎﺟﺍ ﯽﮐ ﮮﺮﻤﯿﮐ", + "switchToETHWallet": "۔ﮟﯾﺮﮐ ﺶﺷﻮﮐ ﮦﺭﺎﺑﻭﺩ ﺭﻭﺍ ﮟﯾﺮﮐ ﭻﺋﻮﺳ ﺮﭘ ﭧﯿﻟﺍﻭ Ethereum ﻡﺮﮐ ﮦﺍﺮﺑ", + "seed_phrase_length": " ﯽﺋﺎﺒﻤﻟ ﯽﮐ ﮯﻠﻤﺟ ﮯﮐ ﺞﯿﺑ", + "unavailable_balance": " ﺲﻨﻠﯿﺑ ﺏﺎﯿﺘﺳﺩ ﺮﯿﻏ", + "unavailable_balance_description": "۔ﮯﺗﺮﮐ ﮟﯿﮩﻧ ﮧﻠﺼﯿﻓ ﺎﮐ ﮯﻧﺮﮐ ﺪﻤﺠﻨﻣ ﻥﺍ ﮟﯿﮩﻧﺍ ﭖﺁ ﮧﮐ ﮏﺗ ﺐﺟ ﮟﯿﮨ ﮯﺘﮨﺭ ﯽﺋﺎﺳﺭ ﻞﺑﺎﻗﺎﻧ ﮏﺗ ﺖﻗﻭ ﺱﺍ ﮯﯿﻟ ﮯﮐ ﻦﯾﺩ ﻦﯿﻟ ﺲﻨﻠﯿﺑ ﺪﻤﺠﻨﻣ ﮧﮐ ﺐﺟ ،ﮯﮔ ﮟﯿﺋﺎﺟ ﻮﮨ ﺏﺎﯿﺘﺳﺩ ﺲﻨﻠﯿﺑ ﻞﻔﻘﻣ ﺪﻌﺑ ﮯﮐ ﮯﻧﻮﮨ ﻞﻤﮑﻣ ﻦﯾﺩ ﻦﯿﻟ ﮧﻘﻠﻌﺘﻣ ﮯﮐ ﻥﺍ ۔ﮯﮨ ﺎﮭﮐﺭ ﺮ", + "unspent_change": "تبدیل کریں", + "tor_connection": " ﻦﺸﮑﻨﮐ ﺭﻮﭨ" } diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index ff43c68b8..57962b38e 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -339,7 +339,6 @@ "template": "Àwòṣe", "confirm_delete_template": "Ìṣe yìí máa yọ àwòṣe yìí kúrò. Ṣé ẹ fẹ́ tẹ̀síwájú?", "confirm_delete_wallet": "Ìṣe yìí máa yọ àpamọ́wọ́ yìí kúrò. Ṣé ẹ fẹ́ tẹ̀síwájú?", - "picker_description": "Ẹ jọ̀wọ́ pààrọ̀ owó tí ẹ pàṣípààrọ̀ jọ yín lákọ̀ọ́kọ́ kí ẹ yán ChangeNOW tàbí MorphToken", "change_wallet_alert_title": "Ẹ pààrọ̀ àpamọ́wọ́ yìí", "change_wallet_alert_content": "Ṣe ẹ fẹ́ pààrọ̀ àpamọ́wọ́ yìí sí ${wallet_name}?", "creating_new_wallet": "Ń dá àpamọ́wọ́ títun", @@ -600,7 +599,6 @@ "onion_link": "Kọja ilọ alubosa", "decimal_places_error": "Oọ̀rọ̀ ayipada ti o wa ni o dara julọ", "edit_node": "Tun awọn ọwọnrin ṣiṣe", - "frozen_balance": "Aferugbo Iye", "settings": "Awọn aseṣe", "sell_monero_com_alert_content": "Kọ ju lọwọ Monero ko ṣe ni ibamu", "error_text_input_below_minimum_limit": "Iye jọwọ ni o kere ti o wọle diẹ", @@ -686,6 +684,50 @@ "matrix_green_dark_theme": "Matrix Green Dark Akori", "monero_light_theme": "Monero Light Akori", "select_destination": "Jọwọ yan ibi ti o nlo fun faili afẹyinti.", + "auto_generate_subaddresses": "Aṣiṣe Ibi-Afọwọkọ", "save_to_downloads": "Fipamọ si Awọn igbasilẹ", - "auto_generate_subaddresses": "Aṣiṣe Ibi-Afọwọkọ" + "select_buy_provider_notice": "Yan olupese Ra loke. O le skii iboju yii nipa ṣiṣeto olupese rẹ ni awọn eto App.", + "onramper_option_description": "Ni kiakia Ra Crypto pẹlu ọpọlọpọ awọn ọna isanwo. Wa ni ọpọlọpọ awọn orilẹ-ede. Itankale ati awọn idiyele yatọ.", + "default_buy_provider": "Aiyipada Ra Olupese", + "ask_each_time": "Beere lọwọ kọọkan", + "buy_provider_unavailable": "Olupese lọwọlọwọ ko si.", + "signTransaction": "Wole Idunadura", + "errorGettingCredentials": "Kuna: Aṣiṣe lakoko gbigba awọn iwe-ẹri", + "errorSigningTransaction": "Aṣiṣe kan ti waye lakoko ti o fowo si iṣowo", + "pairingInvalidEvent": "Pipọpọ Iṣẹlẹ Ti ko tọ", + "chains": "Awọn ẹwọn", + "methods": "Awọn ọna", + "events": "Awọn iṣẹlẹ", + "reject": "Kọ", + "approve": "Fi ọwọ si", + "expiresOn": "Ipari lori", + "walletConnect": "Asopọmọra apamọwọ", + "nullURIError": "URI jẹ asan", + "connectWalletPrompt": "So apamọwọ rẹ pọ pẹlu WalletConnect lati ṣe awọn iṣowo", + "newConnection": "Tuntun Asopọ", + "activeConnectionsPrompt": "Awọn asopọ ti nṣiṣe lọwọ yoo han nibi", + "deleteConnectionConfirmationPrompt": "Ṣe o da ọ loju pe o fẹ paarẹ asopọ si", + "event": "Iṣẹlẹ", + "successful": "Aseyori", + "wouoldLikeToConnect": "yoo fẹ lati sopọ", + "message": "Ifiranṣẹ", + "do_not_have_enough_gas_asset": "O ko ni to ${currency} lati ṣe idunadura kan pẹlu awọn ipo nẹtiwọki blockchain lọwọlọwọ. O nilo diẹ sii ${currency} lati san awọn owo nẹtiwọọki blockchain, paapaa ti o ba nfi dukia miiran ranṣẹ.", + "totp_auth_url": "TOTP AUTH URL", + "awaitDAppProcessing": "Fi inurere duro fun dApp lati pari sisẹ.", + "copyWalletConnectLink": "Daakọ ọna asopọ WalletConnect lati dApp ki o si lẹẹmọ nibi", + "enterWalletConnectURI": "Tẹ WalletConnect URI sii", + "seed_key": "Bọtini Ose", + "enter_seed_phrase": "Tẹ ọrọ-iru irugbin rẹ", + "change_rep_successful": "Ni ifijišẹ yipada aṣoju", + "add_contact": "Fi olubasọrọ kun", + "exchange_provider_unsupported": "${providerName} ko ni atilẹyin mọ!", + "domain_looks_up": "Awọn wiwa agbegbe", + "require_for_exchanges_to_external_wallets": "Beere fun awọn paṣipaarọ si awọn apamọwọ ita", + "camera_permission_is_required": "A nilo igbanilaaye kamẹra.\nJọwọ jeki o lati app eto.", + "switchToETHWallet": "Jọwọ yipada si apamọwọ Ethereum ki o tun gbiyanju lẹẹkansi", + "seed_phrase_length": "Gigun gbolohun irugbin", + "unavailable_balance": "Iwontunwonsi ti ko si", + "unavailable_balance_description": "Iwontunws.funfun ti ko si: Lapapọ yii pẹlu awọn owo ti o wa ni titiipa ni awọn iṣowo isunmọ ati awọn ti o ti didi ni itara ninu awọn eto iṣakoso owo rẹ. Awọn iwọntunwọnsi titiipa yoo wa ni kete ti awọn iṣowo oniwun wọn ba ti pari, lakoko ti awọn iwọntunwọnsi tio tutunini ko ni iraye si fun awọn iṣowo titi iwọ o fi pinnu lati mu wọn kuro.", + "unspent_change": "Yipada", + "tor_connection": "Tor asopọ" } diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index 16dcd0a66..0862f20e2 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -64,8 +64,8 @@ "powered_by": "Powered by ${title}", "error": "错误", "estimated": "估计值", - "min_value": "最低: ${value} ${currency}", - "max_value": "最高: ${value} ${currency}", + "min_value": "最小: ${value} ${currency}", + "max_value": "最大: ${value} ${currency}", "change_currency": "更改币种", "overwrite_amount": "Overwrite amount", "qr_payment_amount": "This QR code contains a payment amount. Do you want to overwrite the current value?", @@ -338,7 +338,6 @@ "template": "模板", "confirm_delete_template": "此操作将刪除此模板。确定吗?", "confirm_delete_wallet": "此操作将刪除此钱包。确定吗?", - "picker_description": "要选择ChangeNOW或MorphToken,请先更改您的交易币", "change_wallet_alert_title": "更换当前钱包", "change_wallet_alert_content": "您是否想将当前钱包改为 ${wallet_name}?", "creating_new_wallet": "创建新钱包", @@ -595,7 +594,6 @@ "sweeping_wallet_alert": "\n这应该不会花很长时间。请勿离开此屏幕,否则可能会丢失所掠取的资金", "decimal_places_error": "小数位太多", "edit_node": "编辑节点", - "frozen_balance": "冻结余额", "invoice_details": "发票明细", "donation_link_details": "捐赠链接详情", "anonpay_description": "生成 ${type}。收款人可以使用任何受支持的加密货币 ${method},您将在此钱包中收到资金。", @@ -691,6 +689,50 @@ "matrix_green_dark_theme": "矩阵绿暗主题", "monero_light_theme": "门罗币浅色主题", "select_destination": "请选择备份文件的目的地。", + "auto_generate_subaddresses": "自动生成子辅助", "save_to_downloads": "保存到下载", - "auto_generate_subaddresses": "自动生成子辅助" + "select_buy_provider_notice": "在上面选择买入提供商。您可以通过在应用程序设置中设置默认的购买提供商来跳过此屏幕。", + "onramper_option_description": "快速使用许多付款方式购买加密货币。在大多数国家 /地区可用。利差和费用各不相同。", + "default_buy_provider": "默认购买提供商", + "ask_each_time": "每次问", + "buy_provider_unavailable": "提供者目前不可用。", + "signTransaction": "签署交易", + "errorGettingCredentials": "失败:获取凭据时出错", + "errorSigningTransaction": "签署交易时发生错误", + "pairingInvalidEvent": "配对无效事件", + "chains": "链条", + "methods": "方法", + "events": "活动", + "reject": "拒绝", + "approve": "批准", + "expiresOn": "到期", + "walletConnect": "钱包连接", + "nullURIError": "URI 为空", + "connectWalletPrompt": "将您的钱包与 WalletConnect 连接以进行交易", + "newConnection": "新连接", + "activeConnectionsPrompt": "活动连接将出现在这里", + "deleteConnectionConfirmationPrompt": "您确定要删除与", + "event": "事件", + "successful": "成功的", + "wouoldLikeToConnect": "想要连接", + "message": "信息", + "do_not_have_enough_gas_asset": "您没有足够的 ${currency} 来在当前的区块链网络条件下进行交易。即使您发送的是不同的资产,您也需要更多的 ${currency} 来支付区块链网络费用。", + "totp_auth_url": "TOTP 授权 URL", + "awaitDAppProcessing": "请等待 dApp 处理完成。", + "copyWalletConnectLink": "从 dApp 复制 WalletConnect 链接并粘贴到此处", + "enterWalletConnectURI": "输入 WalletConnect URI", + "seed_key": "种子钥匙", + "enter_seed_phrase": "输入您的种子短语", + "change_rep_successful": "成功改变了代表", + "add_contact": "增加联系人", + "exchange_provider_unsupported": "${providerName}不再支持!", + "domain_looks_up": "域名查找", + "require_for_exchanges_to_external_wallets": "需要兑换到外部钱包", + "camera_permission_is_required": "需要相机许可。\n请从应用程序设置中启用它。", + "switchToETHWallet": "请切换到以太坊钱包并重试", + "seed_phrase_length": "种子短语长度", + "unavailable_balance": "不可用余额", + "unavailable_balance_description": "不可用余额:此总额包括锁定在待处理交易中的资金以及您在硬币控制设置中主动冻结的资金。一旦各自的交易完成,锁定的余额将变得可用,而冻结的余额在您决定解冻之前仍然无法进行交易。", + "unspent_change": "改变", + "tor_connection": "Tor连接" } diff --git a/scripts/android/app_config.sh b/scripts/android/app_config.sh index 01edb14a4..e2cbd72da 100755 --- a/scripts/android/app_config.sh +++ b/scripts/android/app_config.sh @@ -9,4 +9,4 @@ fi ./app_icon.sh ./pubspec_gen.sh ./manifest.sh -./inject_app_details.sh \ No newline at end of file +./inject_app_details.sh diff --git a/scripts/android/app_env.sh b/scripts/android/app_env.sh index 627b8d318..ba7575d3a 100644 --- a/scripts/android/app_env.sh +++ b/scripts/android/app_env.sh @@ -5,6 +5,7 @@ APP_ANDROID_VERSION="" APP_ANDROID_BUILD_VERSION="" APP_ANDROID_ID="" APP_ANDROID_PACKAGE="" +APP_ANDROID_SCHEME="" MONERO_COM="monero.com" CAKEWALLET="cakewallet" @@ -14,16 +15,18 @@ TYPES=($MONERO_COM $CAKEWALLET $HAVEN) APP_ANDROID_TYPE=$1 MONERO_COM_NAME="Monero.com" -MONERO_COM_VERSION="1.6.0" -MONERO_COM_BUILD_NUMBER=56 +MONERO_COM_VERSION="1.7.3" +MONERO_COM_BUILD_NUMBER=64 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.9.0" -CAKEWALLET_BUILD_NUMBER=169 +CAKEWALLET_VERSION="4.10.3" +CAKEWALLET_BUILD_NUMBER=178 CAKEWALLET_BUNDLE_ID="com.cakewallet.cake_wallet" CAKEWALLET_PACKAGE="com.cakewallet.cake_wallet" +CAKEWALLET_SCHEME="cakewallet" HAVEN_NAME="Haven" HAVEN_VERSION="1.0.0" @@ -44,6 +47,7 @@ case $APP_ANDROID_TYPE in APP_ANDROID_BUILD_NUMBER=$MONERO_COM_BUILD_NUMBER APP_ANDROID_BUNDLE_ID=$MONERO_COM_BUNDLE_ID APP_ANDROID_PACKAGE=$MONERO_COM_PACKAGE + APP_ANDROID_SCHEME=$MONERO_COM_SCHEME ;; $CAKEWALLET) APP_ANDROID_NAME=$CAKEWALLET_NAME @@ -51,6 +55,7 @@ case $APP_ANDROID_TYPE in APP_ANDROID_BUILD_NUMBER=$CAKEWALLET_BUILD_NUMBER APP_ANDROID_BUNDLE_ID=$CAKEWALLET_BUNDLE_ID APP_ANDROID_PACKAGE=$CAKEWALLET_PACKAGE + APP_ANDROID_SCHEME=$CAKEWALLET_SCHEME ;; $HAVEN) APP_ANDROID_NAME=$HAVEN_NAME @@ -66,4 +71,5 @@ export APP_ANDROID_NAME export APP_ANDROID_VERSION export APP_ANDROID_BUILD_NUMBER export APP_ANDROID_BUNDLE_ID -export APP_ANDROID_PACKAGE \ No newline at end of file +export APP_ANDROID_PACKAGE +export APP_ANDROID_SCHEME \ No newline at end of file diff --git a/scripts/android/inject_app_details.sh b/scripts/android/inject_app_details.sh index 340966044..27b7efa39 100755 --- a/scripts/android/inject_app_details.sh +++ b/scripts/android/inject_app_details.sh @@ -8,6 +8,7 @@ fi cd ../.. sed -i "0,/version:/{s/version:.*/version: ${APP_ANDROID_VERSION}+${APP_ANDROID_BUILD_NUMBER}/}" ./pubspec.yaml sed -i "0,/version:/{s/__APP_PACKAGE__/${APP_ANDROID_PACKAGE}/}" ./android/app/src/main/AndroidManifest.xml +sed -i "0,/__APP_SCHEME__/s/__APP_SCHEME__/${APP_ANDROID_SCHEME}/" ./android/app/src/main/AndroidManifest.xml sed -i "0,/version:/{s/__versionCode__/${APP_ANDROID_BUILD_NUMBER}/}" ./android/app/src/main/AndroidManifest.xml sed -i "0,/version:/{s/__versionName__/${APP_ANDROID_VERSION}/}" ./android/app/src/main/AndroidManifest.xml cd scripts/android diff --git a/scripts/android/pubspec_gen.sh b/scripts/android/pubspec_gen.sh index c74108bf1..dd9852072 100755 --- a/scripts/android/pubspec_gen.sh +++ b/scripts/android/pubspec_gen.sh @@ -10,7 +10,7 @@ case $APP_ANDROID_TYPE in CONFIG_ARGS="--monero" ;; $CAKEWALLET) - CONFIG_ARGS="--monero --bitcoin --haven --ethereum --nano" + CONFIG_ARGS="--monero --bitcoin --haven --ethereum --nano --bitcoinCash" ;; $HAVEN) CONFIG_ARGS="--haven" diff --git a/scripts/append_translation.sh b/scripts/append_translation.sh index 2dc373e0e..0cc33fc0f 100755 --- a/scripts/append_translation.sh +++ b/scripts/append_translation.sh @@ -7,7 +7,7 @@ # if you get an error `command not found` # give the correct permissions to this file using `chmod 777 append_translation.sh` -langs=("ar" "bg" "cs" "de" "en" "es" "fr" "ha" "hi" "hr" "id" "it" "ja" "ko" "my" "nl" "pl" "pt" "ru" "th" "tr" "uk" "ur" "yo" "zh") +langs=("ar" "bg" "cs" "de" "en" "es" "fr" "ha" "hi" "hr" "id" "it" "ja" "ko" "my" "nl" "pl" "pt" "ru" "th" "tl" "tr" "uk" "ur" "yo" "zh") name=$1 text=$2 diff --git a/scripts/ios/app_config.sh b/scripts/ios/app_config.sh index 79365ab0c..8d999f594 100755 --- a/scripts/ios/app_config.sh +++ b/scripts/ios/app_config.sh @@ -16,6 +16,11 @@ cp -rf ./ios/Runner/InfoBase.plist ./ios/Runner/Info.plist /usr/libexec/PlistBuddy -c "Set :CFBundleIdentifier ${APP_IOS_BUNDLE_ID}" ./ios/Runner/Info.plist /usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString ${APP_IOS_VERSION}" ./ios/Runner/Info.plist /usr/libexec/PlistBuddy -c "Set :CFBundleVersion ${APP_IOS_BUILD_NUMBER}" ./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 + CONFIG_ARGS="" case $APP_IOS_TYPE in @@ -23,9 +28,11 @@ case $APP_IOS_TYPE in CONFIG_ARGS="--monero" ;; $CAKEWALLET) - CONFIG_ARGS="--monero --bitcoin --haven --ethereum --nano" + CONFIG_ARGS="--monero --bitcoin --haven --ethereum --nano --bitcoinCash" ;; $HAVEN) + + CONFIG_ARGS="--haven" ;; esac diff --git a/scripts/ios/app_env.sh b/scripts/ios/app_env.sh index 08de936e8..328b3825b 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.6.0" -MONERO_COM_BUILD_NUMBER=54 +MONERO_COM_VERSION="1.7.4" +MONERO_COM_BUILD_NUMBER=63 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="4.9.0" -CAKEWALLET_BUILD_NUMBER=178 +CAKEWALLET_VERSION="4.10.4" +CAKEWALLET_BUILD_NUMBER=196 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" HAVEN_NAME="Haven" diff --git a/scripts/macos/app_config.sh b/scripts/macos/app_config.sh index 2af101485..48b680330 100755 --- a/scripts/macos/app_config.sh +++ b/scripts/macos/app_config.sh @@ -23,7 +23,7 @@ CONFIG_ARGS="" case $APP_MACOS_TYPE in $CAKEWALLET) - CONFIG_ARGS="--monero --bitcoin --ethereum --nano";; #--haven + CONFIG_ARGS="--monero --bitcoin --ethereum --nano --bitcoinCash";; #--haven esac cp -rf pubspec_description.yaml pubspec.yaml diff --git a/scripts/macos/app_env.sh b/scripts/macos/app_env.sh index 28e7d5f4f..57434db8e 100755 --- a/scripts/macos/app_env.sh +++ b/scripts/macos/app_env.sh @@ -15,8 +15,8 @@ if [ -n "$1" ]; then fi CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="1.2.0" -CAKEWALLET_BUILD_NUMBER=31 +CAKEWALLET_VERSION="1.3.3" +CAKEWALLET_BUILD_NUMBER=40 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" if ! [[ " ${TYPES[*]} " =~ " ${APP_MACOS_TYPE} " ]]; then diff --git a/scripts/macos/build_sodium.sh b/scripts/macos/build_sodium.sh index b50d3c2ee..19aad3c97 100755 --- a/scripts/macos/build_sodium.sh +++ b/scripts/macos/build_sodium.sh @@ -10,7 +10,7 @@ echo "============================ SODIUM ============================" echo "Cloning SODIUM from - $SODIUM_URL" git clone $SODIUM_URL $SODIUM_PATH --branch stable cd $SODIUM_PATH -./dist-build/osx.sh +./dist-build/macos.sh mv ${SODIUM_PATH}/libsodium-osx/include/* $EXTERNAL_MACOS_INCLUDE_DIR mv ${SODIUM_PATH}/libsodium-osx/lib/* $EXTERNAL_MACOS_LIB_DIR \ No newline at end of file diff --git a/tool/configure.dart b/tool/configure.dart index 35ad5f3b4..531536968 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -4,6 +4,7 @@ const bitcoinOutputPath = 'lib/bitcoin/bitcoin.dart'; const moneroOutputPath = 'lib/monero/monero.dart'; const havenOutputPath = 'lib/haven/haven.dart'; const ethereumOutputPath = 'lib/ethereum/ethereum.dart'; +const bitcoinCashOutputPath = 'lib/bitcoin_cash/bitcoin_cash.dart'; const nanoOutputPath = 'lib/nano/nano.dart'; const walletTypesPath = 'lib/wallet_types.g.dart'; const pubspecDefaultPath = 'pubspec_default.yaml'; @@ -15,6 +16,7 @@ Future main(List args) async { final hasMonero = args.contains('${prefix}monero'); final hasHaven = args.contains('${prefix}haven'); final hasEthereum = args.contains('${prefix}ethereum'); + final hasBitcoinCash = args.contains('${prefix}bitcoinCash'); final hasNano = args.contains('${prefix}nano'); final hasBanano = args.contains('${prefix}banano'); @@ -22,6 +24,7 @@ Future main(List args) async { await generateMonero(hasMonero); await generateHaven(hasHaven); await generateEthereum(hasEthereum); + await generateBitcoinCash(hasBitcoinCash); await generateNano(hasNano); // await generateBanano(hasEthereum); @@ -32,6 +35,7 @@ Future main(List args) async { hasEthereum: hasEthereum, hasNano: hasNano, hasBanano: hasBanano, + hasBitcoinCash: hasBitcoinCash, ); await generateWalletTypes( hasMonero: hasMonero, @@ -40,13 +44,14 @@ Future main(List args) async { hasEthereum: hasEthereum, hasNano: hasNano, hasBanano: hasBanano, + hasBitcoinCash: hasBitcoinCash, ); } Future generateBitcoin(bool hasImplementation) async { final outputFile = File(bitcoinOutputPath); const bitcoinCommonHeaders = """ -import 'package:cake_wallet/entities/unspent_transaction_output.dart'; +import 'package:cw_core/unspent_transaction_output.dart'; import 'package:cw_core/wallet_credentials.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/transaction_priority.dart'; @@ -60,7 +65,6 @@ import 'package:cw_bitcoin/electrum_wallet.dart'; import 'package:cw_bitcoin/bitcoin_unspent.dart'; import 'package:cw_bitcoin/bitcoin_mnemonic.dart'; import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; -import 'package:cw_bitcoin/bitcoin_wallet.dart'; import 'package:cw_bitcoin/bitcoin_wallet_service.dart'; import 'package:cw_bitcoin/bitcoin_wallet_creation_credentials.dart'; import 'package:cw_bitcoin/bitcoin_amount_format.dart'; @@ -80,8 +84,8 @@ abstract class Bitcoin { Map getWalletKeys(Object wallet); List getTransactionPriorities(); List getLitecoinTransactionPriorities(); - TransactionPriority deserializeBitcoinTransactionPriority(int raw); - TransactionPriority deserializeLitecoinTransactionPriority(int raw); + TransactionPriority deserializeBitcoinTransactionPriority(int raw); + TransactionPriority deserializeLitecoinTransactionPriority(int raw); int getFeeRate(Object wallet, TransactionPriority priority); Future generateNewAddress(Object wallet); Object createBitcoinTransactionCredentials(List outputs, {required TransactionPriority priority, int? feeRate}); @@ -126,7 +130,7 @@ abstract class Bitcoin { Future generateMonero(bool hasImplementation) async { final outputFile = File(moneroOutputPath); const moneroCommonHeaders = """ -import 'package:cake_wallet/entities/unspent_transaction_output.dart'; +import 'package:cw_core/unspent_transaction_output.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_monero/monero_unspent.dart'; import 'package:mobx/mobx.dart'; @@ -231,7 +235,7 @@ abstract class Monero { String getSubaddressLabel(Object wallet, int accountIndex, int addressIndex); - int getHeigthByDate({required DateTime date}); + int getHeightByDate({required DateTime date}); TransactionPriority getDefaultTransactionPriority(); TransactionPriority getMoneroTransactionPrioritySlow(); TransactionPriority getMoneroTransactionPriorityAutomatic(); @@ -486,7 +490,6 @@ Future generateEthereum(bool hasImplementation) async { final outputFile = File(ethereumOutputPath); const ethereumCommonHeaders = """ import 'package:cake_wallet/view_model/send/output.dart'; -import 'package:cw_core/crypto_amount_format.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/erc20_token.dart'; import 'package:cw_core/output_info.dart'; @@ -496,7 +499,9 @@ import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_credentials.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_service.dart'; +import 'package:eth_sig_util/util/utils.dart'; import 'package:hive/hive.dart'; +import 'package:web3dart/web3dart.dart'; """; const ethereumCWHeaders = """ import 'package:cw_ethereum/ethereum_formatter.dart'; @@ -517,7 +522,10 @@ abstract class Ethereum { WalletCredentials createEthereumRestoreWalletFromSeedCredentials({required String name, required String mnemonic, required String password}); WalletCredentials createEthereumRestoreWalletFromPrivateKey({required String name, required String privateKey, required String password}); String getAddress(WalletBase wallet); + String getPrivateKey(WalletBase wallet); + String getPublicKey(WalletBase wallet); TransactionPriority getDefaultTransactionPriority(); + TransactionPriority getEthereumTransactionPrioritySlow(); List getTransactionPriorities(); TransactionPriority deserializeEthereumTransactionPriority(int raw); @@ -544,6 +552,7 @@ abstract class Ethereum { CryptoCurrency assetOfTransaction(WalletBase wallet, TransactionInfo transaction); void updateEtherscanUsageState(WalletBase wallet, bool isEnabled); + Web3Client? getWeb3Client(WalletBase wallet); } """; @@ -564,26 +573,102 @@ abstract class Ethereum { await outputFile.writeAsString(output); } +Future generateBitcoinCash(bool hasImplementation) async { + final outputFile = File(bitcoinCashOutputPath); + const bitcoinCashCommonHeaders = """ +import 'dart:typed_data'; + +import 'package:cw_core/unspent_transaction_output.dart'; +import 'package:cw_core/transaction_priority.dart'; +import 'package:cw_core/unspent_coins_info.dart'; +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'; +"""; + const bitcoinCashCWHeaders = """ +import 'package:cw_bitcoin_cash/cw_bitcoin_cash.dart'; +import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; +"""; + const bitcoinCashCwPart = "part 'cw_bitcoin_cash.dart';"; + const bitcoinCashContent = """ +abstract class BitcoinCash { + String getMnemonic(int? strength); + + Uint8List getSeedFromMnemonic(String seed); + + String getCashAddrFormat(String address); + + WalletService createBitcoinCashWalletService( + Box walletInfoSource, Box unspentCoinSource); + + WalletCredentials createBitcoinCashNewWalletCredentials( + {required String name, WalletInfo? walletInfo}); + + WalletCredentials createBitcoinCashRestoreWalletFromSeedCredentials( + {required String name, required String mnemonic, required String password}); + + TransactionPriority deserializeBitcoinCashTransactionPriority(int raw); + + TransactionPriority getDefaultTransactionPriority(); + + List getTransactionPriorities(); + + TransactionPriority getBitcoinCashTransactionPrioritySlow(); +} + """; + + const bitcoinCashEmptyDefinition = 'BitcoinCash? bitcoinCash;\n'; + const bitcoinCashCWDefinition = 'BitcoinCash? bitcoinCash = CWBitcoinCash();\n'; + + final output = '$bitcoinCashCommonHeaders\n' + + (hasImplementation ? '$bitcoinCashCWHeaders\n' : '\n') + + (hasImplementation ? '$bitcoinCashCwPart\n\n' : '\n') + + (hasImplementation ? bitcoinCashCWDefinition : bitcoinCashEmptyDefinition) + + '\n' + + bitcoinCashContent; + + if (outputFile.existsSync()) { + await outputFile.delete(); + } + + await outputFile.writeAsString(output); +} + Future generateNano(bool hasImplementation) async { final outputFile = File(nanoOutputPath); const nanoCommonHeaders = """ -"""; - const nanoCWHeaders = """ +import 'package:cw_core/cake_hive.dart'; import 'package:cw_core/nano_account.dart'; -import 'package:cw_nano/nano_mnemonic.dart'; -import 'package:cw_nano/nano_wallet.dart'; -import 'package:cw_nano/nano_wallet_service.dart'; -import 'package:cake_wallet/view_model/send/output.dart'; import 'package:cw_core/account.dart'; -import 'package:mobx/mobx.dart'; +import 'package:cw_core/node.dart'; import 'package:cw_core/wallet_credentials.dart'; import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/transaction_info.dart'; import 'package:cw_core/transaction_history.dart'; import 'package:cw_core/wallet_service.dart'; import 'package:cw_core/output_info.dart'; +import 'package:cw_core/nano_account_info_response.dart'; +import 'package:mobx/mobx.dart'; import 'package:hive/hive.dart'; +import 'package:cake_wallet/view_model/send/output.dart'; +"""; + const nanoCWHeaders = """ +import 'package:cw_nano/nano_client.dart'; +import 'package:cw_nano/nano_mnemonic.dart'; +import 'package:cw_nano/nano_wallet.dart'; +import 'package:cw_nano/nano_wallet_service.dart'; +import 'package:cw_nano/nano_transaction_info.dart'; import 'package:cw_nano/nano_transaction_credentials.dart'; import 'package:cw_nano/nano_wallet_creation_credentials.dart'; +// needed for nano_util: +import 'dart:convert'; +import 'dart:typed_data'; +import 'package:convert/convert.dart'; +import "package:ed25519_hd_key/ed25519_hd_key.dart"; +import 'package:libcrypto/libcrypto.dart'; +import 'package:nanodart/nanodart.dart' as ND; +import 'package:decimal/decimal.dart'; """; const nanoCwPart = "part 'cw_nano.dart';"; const nanoContent = """ @@ -596,8 +681,6 @@ abstract class Nano { WalletService createNanoWalletService(Box walletInfoSource); - TransactionHistoryBase getTransactionHistory(Object wallet); - WalletCredentials createNanoNewWalletCredentials({ required String name, String password, @@ -617,13 +700,13 @@ abstract class Nano { DerivationType? derivationType, }); - String getTransactionAddress(Object wallet, int accountIndex, int addressIndex); - - void onStartup(); - List getNanoWordList(String language); Map getKeys(Object wallet); Object createNanoTransactionCredentials(List outputs); + Future changeRep(Object wallet, String address); + Future updateTransactions(Object wallet); + BigInt getTransactionAmountRaw(TransactionInfo transactionInfo); + String getRepresentative(Object wallet); } abstract class NanoAccountList { @@ -634,10 +717,51 @@ abstract class NanoAccountList { Future addAccount(Object wallet, {required String label}); Future setLabelAccount(Object wallet, {required int accountIndex, required String label}); } + +abstract class NanoUtil { + String seedToPrivate(String seed, int index); + String seedToAddress(String seed, int index); + String seedToMnemonic(String seed); + Future mnemonicToSeed(String mnemonic); + String privateKeyToPublic(String privateKey); + String addressToPublicKey(String publicAddress); + String privateKeyToAddress(String privateKey); + String publicKeyToAddress(String publicKey); + bool isValidSeed(String seed); + Future hdMnemonicListToSeed(List words); + Future hdSeedToPrivate(String seed, int index); + Future hdSeedToAddress(String seed, int index); + Future uniSeedToAddress(String seed, int index, String type); + Future uniSeedToPrivate(String seed, int index, String type); + bool isValidBip39Seed(String seed); + static const int maxDecimalDigits = 6; // Max digits after decimal + BigInt rawPerNano = BigInt.parse("1000000000000000000000000000000"); + BigInt rawPerNyano = BigInt.parse("1000000000000000000000000"); + BigInt rawPerBanano = BigInt.parse("100000000000000000000000000000"); + BigInt rawPerXMR = BigInt.parse("1000000000000"); + BigInt convertXMRtoNano = BigInt.parse("1000000000000000000"); + String getRawAsDecimalString(String? raw, BigInt? rawPerCur); + String getRawAsUsableString(String? raw, BigInt rawPerCur); + String getRawAccuracy(String? raw, BigInt rawPerCur); + String getAmountAsRaw(String amount, BigInt rawPerCur); + + // derivationInfo: + Future getInfoFromSeedOrMnemonic( + DerivationType derivationType, { + String? seedKey, + String? mnemonic, + required Node node, + }); + Future> compareDerivationMethods({ + String? mnemonic, + String? privateKey, + required Node node, + }); +} """; - const nanoEmptyDefinition = 'Nano? nano;\n'; - const nanoCWDefinition = 'Nano? nano = CWNano();\n'; + const nanoEmptyDefinition = 'Nano? nano;\nNanoUtil? nanoUtil;\n'; + const nanoCWDefinition = 'Nano? nano = CWNano();\nNanoUtil? nanoUtil = CWNanoUtil();\n'; final output = '$nanoCommonHeaders\n' + (hasImplementation ? '$nanoCWHeaders\n' : '\n') + @@ -653,14 +777,14 @@ abstract class NanoAccountList { await outputFile.writeAsString(output); } -Future generatePubspec({ - required bool hasMonero, - required bool hasBitcoin, - required bool hasHaven, - required bool hasEthereum, - required bool hasNano, - required bool hasBanano, -}) async { +Future generatePubspec( + {required bool hasMonero, + required bool hasBitcoin, + required bool hasHaven, + required bool hasEthereum, + required bool hasNano, + required bool hasBanano, + required bool hasBitcoinCash}) async { const cwCore = """ cw_core: path: ./cw_core @@ -685,6 +809,10 @@ Future generatePubspec({ cw_ethereum: path: ./cw_ethereum """; + const cwBitcoinCash = """ + cw_bitcoin_cash: + path: ./cw_bitcoin_cash + """; const cwNano = """ cw_nano: path: ./cw_nano @@ -719,6 +847,10 @@ Future generatePubspec({ output += '\n$cwBanano'; } + if (hasBitcoinCash) { + output += '\n$cwBitcoinCash'; + } + if (hasHaven && !hasMonero) { output += '\n$cwSharedExternal\n$cwHaven'; } else if (hasHaven) { @@ -737,14 +869,14 @@ Future generatePubspec({ await outputFile.writeAsString(outputContent); } -Future generateWalletTypes({ - required bool hasMonero, - required bool hasBitcoin, - required bool hasHaven, - required bool hasEthereum, - required bool hasNano, - required bool hasBanano, -}) async { +Future generateWalletTypes( + {required bool hasMonero, + required bool hasBitcoin, + required bool hasHaven, + required bool hasEthereum, + required bool hasNano, + required bool hasBanano, + required bool hasBitcoinCash}) async { final walletTypesFile = File(walletTypesPath); if (walletTypesFile.existsSync()) { @@ -771,6 +903,10 @@ Future generateWalletTypes({ outputContent += '\tWalletType.litecoin,\n'; } + if (hasBitcoinCash) { + outputContent += '\tWalletType.bitcoinCash,\n'; + } + if (hasNano) { outputContent += '\tWalletType.nano,\n'; } diff --git a/tool/translation_add_lang.dart b/tool/translation_add_lang.dart new file mode 100644 index 000000000..8b392df6e --- /dev/null +++ b/tool/translation_add_lang.dart @@ -0,0 +1,36 @@ +import 'dart:io'; + +import 'utils/translation/arb_file_utils.dart'; +import 'utils/translation/translation_constants.dart'; +import 'utils/translation/translation_utils.dart'; + +void main(List args) async { + if (args.length != 1) { + throw Exception( + 'Insufficient arguments!\n\nTry to run `./translation_add_lang.dart langCode`'); + } + + final targetLang = args.first; + + final fileName = getArbFileName(defaultLang); + final file = File(fileName); + final arbObj = readArbFile(file); + + final targetFileName = getArbFileName(targetLang); + final targetKeys = arbObj.keys; + + final targetFile = File(targetFileName); + targetFile.createSync(exclusive: true); + targetFile.writeAsStringSync("{}"); + + final translations = Map(); + for (var targetKey in targetKeys) { + final srcString = arbObj[targetKey] as String; + final translation = await getTranslation(srcString, targetLang); + + translations[targetKey] = translation; + } + + appendStringsToArbFile(targetFileName, translations); + print("Success! Please add your Language Code to lib/entities/language_service.dart"); +} diff --git a/tool/utils/secret_key.dart b/tool/utils/secret_key.dart index 7819a582c..a8c6a6166 100644 --- a/tool/utils/secret_key.dart +++ b/tool/utils/secret_key.dart @@ -32,6 +32,10 @@ class SecretKey { SecretKey('fiatApiKey', () => ''), SecretKey('payfuraApiKey', () => ''), SecretKey('chatwootWebsiteToken', () => ''), + SecretKey('exolixApiKey', () => ''), + SecretKey('robinhoodApplicationId', () => ''), + SecretKey('robinhoodCIdApiSecret', () => ''), + SecretKey('walletConnectProjectId', () => ''), ]; static final ethereumSecrets = [ diff --git a/tool/utils/translation/arb_file_utils.dart b/tool/utils/translation/arb_file_utils.dart index 693c5b93e..c43de091a 100644 --- a/tool/utils/translation/arb_file_utils.dart +++ b/tool/utils/translation/arb_file_utils.dart @@ -17,7 +17,8 @@ void appendStringToArbFile(String fileName, String name, String text) { .replaceAll('","', '",\n "') .replaceAll('{"', '{\n "') .replaceAll('"}', '"\n}') - .replaceAll('":"', '": "'); + .replaceAll('":"', '": "') + .replaceAll('\$ {', '\${'); file.writeAsStringSync(outputContent); } @@ -33,7 +34,8 @@ void appendStringsToArbFile(String fileName, Map strings) { .replaceAll('","', '",\n "') .replaceAll('{"', '{\n "') .replaceAll('"}', '"\n}') - .replaceAll('":"', '": "'); + .replaceAll('":"', '": "') + .replaceAll('\$ {', '\${'); file.writeAsStringSync(outputContent); } diff --git a/tool/utils/translation/translation_constants.dart b/tool/utils/translation/translation_constants.dart index 6563feb32..3a472d8c4 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", - "ja", "ko", "my", "nl", "pl", "pt", "ru", "th", "tr", "uk", "ur", "yo", + "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) ]; diff --git a/tool/utils/translation/translation_utils.dart b/tool/utils/translation/translation_utils.dart index a37838b91..173b66c39 100644 --- a/tool/utils/translation/translation_utils.dart +++ b/tool/utils/translation/translation_utils.dart @@ -18,20 +18,28 @@ Future appendTranslations(String lang, Map defaults) async for (var key in defaults.keys) { final value = defaults[key]!; - - if (value.contains("{")) continue; final translation = await getTranslation(value, lang); translations[key] = translation; } - print(translations); - appendStringsToArbFile(fileName, translations); } Future getTranslation(String text, String lang) async { if (lang == defaultLang) return text; - return (await translator.translate(text, from: defaultLang, to: lang)).text; -} + final regExp = RegExp(r'{(.*?)}'); + final placeholder = + regExp.allMatches(text).map((e) => text.substring(e.start, e.end)).toList().asMap(); + + var translation = (await translator.translate(text, from: defaultLang, to: lang)).text; + + placeholder.forEach((index, value) { + final translatedPlaceholder = regExp.allMatches(translation).toList()[index]; + translation = + translation.replaceRange(translatedPlaceholder.start, translatedPlaceholder.end, value); + }); + + return translation; +} diff --git a/tool/utils/utils.dart b/tool/utils/utils.dart index 2fd474de7..e43063165 100644 --- a/tool/utils/utils.dart +++ b/tool/utils/utils.dart @@ -10,4 +10,4 @@ String normalizeKeyName(String key) { } String generateConst(String name, Map config) => - 'const $name = \'${config["$name"]}\';\n'; \ No newline at end of file + 'const $name = \'${config["$name"]}\';\n';