From 0ba54fa60283b34f03ddac061911c84bd342ef89 Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Fri, 21 Mar 2025 04:18:47 +0200 Subject: [PATCH] Add decred (#1938) * decred: Add decred. (#1322) * multi: Add initial decred screens. (#1165) Use a mock libwallet for now. * cw_decred: add libdcrwallet dependency and link library for android, ios and macos (#1240) * change cw_decred from package to plugin * add libdcrwallet dependency and link library for android, ios and macos * remove spvwallet, make some libdcrwallet fns async, light refactor * libdcrwallet: use json payload returns * use specific libwallet commit hash * decred: fix Rename wallet. --------- Co-authored-by: JoeGruff * decred: Add sync. * decred: Add send transaction. * decred: Fix fee estimation. * decred: List transactions. * decred: Add rescan. * decred: Sign message. * decred: Add new addr and addrs. * decred: Add change wallet pass. * decred: Add restore from seed. * decred: Add watching only wallets. * decred: Enable mainnet. * decred: Allow using blank node address. This allows a persistent peer to be unset, falling back to decred seeders. * decred: Rescan from wallet birthday. * add and update macos build scripts, update build readme, gitignore macos project.pbxproj Signed-off-by: Philemon Ukane * multi: hide decred rescan page if it's not ready - move hasRescan method to WalletBase and implement for decred Signed-off-by: Philemon Ukane * cw_decred: fix bug where decred wallets are not loaded after app restart Signed-off-by: Philemon Ukane * add buy and sell for decred via onramp Signed-off-by: Philemon Ukane * bug-fix: account for other send outputs that are part of the same tx Signed-off-by: Philemon Ukane * decred: Return address with no peers. * decred: Update pubspec. * decred: Add verify message. * upgrade hive_generator dep in cw_decred * decred: Clean up code. --------- Signed-off-by: Philemon Ukane Co-authored-by: Wisdom Arerosuoghene Co-authored-by: Philemon Ukane * fix extracted addresses not used fix conflicts with main * remove print [skip ci] * minor formatting * fix initial migration version * add build decred script to workflow * install go before build decred fix switch cases * trial 2 to fix decred build * re-install go * revert build script change * refactor/clean nodes functions * Fix address book issue Fix send ALL (to be continued with the fees point) * Fix transactions display issues Add missing file * Fix unconfirmed balance not displayed Change Wallet order Minor cleanup * Fix workflow * Fix workflow * Fix workflow * test * hardcode path for now * fix + cleanup decred build script to work on mac and linux * Update decred build script * Run actions on pull requests, extract commit message * run after checkout * add safe directory * Get commit message from base.sha instead of last commit * base -> head * Do not merge main branch into pr * [skip slack] [run tests] clone by sha * Proper name for decred library in the build script * Throw an error when ANDROID_HOME or ANDROID_NDK_VERSION is missing * Fix conflicts with main * minor code enhancement * decred: Add used address history. (#1941) * decred: Update pubspec. * decred testnet * decred: Add used address history. * decred: Remove default node list. * populate transaction history before sync begins * decred: Add some awaits. * decred: Fix send all. * decred: Add clang export to build script. * decred: Update logo colors. * cleanup cw_decred.dart * make decred wallet addresses selectable in receive page * decred: Always set default addr when used. * decred: Add back default node list. * decred: Allow creating addresses manually. --------- Co-authored-by: Wisdom Arerosuoghene Co-authored-by: Omar Hatem * minor fixes and cleanup * minor fix, feel free to test now * - Fix transaction details - Fix Nodes - Add processing sync status * Add decred info card * push missing file * Add missing text for decred info card * minor: change docs link [skip ci] * decred: Update derivation info. (#2013) * decred: Update derivation info. * decred: Allow unsynced unused addresses. * decred: Update dcrwallet dep to 4.3.0. * Merge main and fix conflicts * Merge main and fix conflicts * decred: Fix background sync panic. (#2080) * decred: Run libwallet in isolate. (#2077) * decred: Fix contact save inquiry. (#2083) Also fix tx time and the fee shown on pending transactions. * Disable send button in view only decred wallets * - Fix frozen coins - Add URI support - Fix fees in tx details - Handle empty coins send - Handle wallets in address book * Merge main * remove print [skip ci] * Fix restore from QR * minor improvement for QR restore * minor fixes [skip ci] * decred: Get slip44 addrs before sync completes. (#2092) * - Fix loading wallet more than one time - Fix minor UI issue --------- Signed-off-by: Philemon Ukane Co-authored-by: JoeGruffins <34998433+JoeGruffins@users.noreply.github.com> Co-authored-by: Wisdom Arerosuoghene Co-authored-by: Philemon Ukane Co-authored-by: Czarek Nakamoto --- .github/workflows/no_print_in_dart.yaml | 4 +- .github/workflows/pr_test_build_android.yml | 12 +- .github/workflows/pr_test_build_linux.yml | 1 + .gitignore | 3 + android/app/src/main/AndroidManifestBase.xml | 3 + assets/decred_node_list.yml | 6 + assets/images/2.0x/decred.png | Bin 0 -> 2112 bytes assets/images/2.0x/decred_menu.png | Bin 0 -> 1871 bytes assets/images/3.0x/decred.png | Bin 0 -> 2918 bytes assets/images/3.0x/decred_menu.png | Bin 0 -> 1423 bytes assets/images/dcr_icon.png | Bin 117789 -> 91254 bytes assets/images/decred.png | Bin 0 -> 1237 bytes assets/images/decred_icon.png | Bin 0 -> 1542 bytes assets/images/decred_menu.png | Bin 0 -> 1482 bytes cw_bitcoin/lib/bitcoin_wallet.dart | 3 + cw_bitcoin/lib/litecoin_wallet.dart | 4 +- cw_core/lib/amount_converter.dart | 1 + cw_core/lib/currency_for_wallet_type.dart | 7 +- cw_core/lib/node.dart | 18 + cw_core/lib/receive_page_option.dart | 1 + cw_core/lib/sync_status.dart | 10 + cw_core/lib/wallet_base.dart | 4 + cw_core/lib/wallet_type.dart | 15 +- cw_core/pubspec.lock | 18 + cw_decred/.gitignore | 39 + cw_decred/.metadata | 36 + cw_decred/CHANGELOG.md | 3 + cw_decred/LICENSE | 1 + cw_decred/README.md | 3 + cw_decred/analysis_options.yaml | 4 + cw_decred/android/.gitignore | 9 + cw_decred/android/build.gradle | 59 + cw_decred/android/settings.gradle | 1 + .../android/src/main/AndroidManifest.xml | 4 + .../cakewallet/cw_decred/CwDecredPlugin.kt | 35 + cw_decred/ios/.gitignore | 38 + cw_decred/ios/Assets/.gitkeep | 0 cw_decred/ios/Classes/CwDecredPlugin.swift | 19 + cw_decred/ios/cw_decred.podspec | 22 + cw_decred/lib/amount_format.dart | 26 + cw_decred/lib/api/libdcrwallet.dart | 693 ++++++ cw_decred/lib/api/util.dart | 64 + cw_decred/lib/balance.dart | 25 + cw_decred/lib/mnemonic.dart | 2050 +++++++++++++++++ cw_decred/lib/pending_transaction.dart | 39 + cw_decred/lib/transaction_credentials.dart | 10 + cw_decred/lib/transaction_history.dart | 31 + cw_decred/lib/transaction_info.dart | 45 + cw_decred/lib/transaction_priority.dart | 69 + cw_decred/lib/wallet.dart | 729 ++++++ cw_decred/lib/wallet_addresses.dart | 137 ++ .../lib/wallet_creation_credentials.dart | 40 + cw_decred/lib/wallet_service.dart | 186 ++ cw_decred/macos/Classes/CwDecredPlugin.swift | 19 + cw_decred/macos/cw_decred.podspec | 22 + cw_decred/pubspec.lock | 852 +++++++ cw_decred/pubspec.yaml | 84 + cw_monero/lib/monero_wallet.dart | 3 + cw_wownero/lib/wownero_wallet.dart | 3 + ios/Podfile.lock | 43 + ios/Runner/InfoBase.plist | 40 + lib/core/address_validator.dart | 2 +- lib/core/node_address_validator.dart | 11 + lib/core/seed_validator.dart | 3 + lib/core/sync_status_title.dart | 4 + lib/core/wallet_creation_service.dart | 1 + lib/decred/cw_decred.dart | 114 + lib/di.dart | 6 +- lib/entities/default_settings_migration.dart | 851 +++---- lib/entities/main_actions.dart | 1 + lib/entities/node_list.dart | 262 +-- lib/entities/preferences_key.dart | 3 + lib/entities/priority_for_wallet_type.dart | 3 + lib/entities/provider_types.dart | 2 + lib/main.dart | 2 +- lib/reactions/bip39_wallet_utils.dart | 1 + lib/reactions/check_connection.dart | 3 + lib/reactions/fiat_rate_update.dart | 1 - lib/reactions/on_current_wallet_change.dart | 3 +- .../on_wallet_sync_status_change.dart | 2 +- .../desktop_wallet_selection_dropdown.dart | 3 + .../pages/balance/crypto_balance_widget.dart | 106 +- .../dashboard/pages/navigation_dock.dart | 6 +- .../screens/dashboard/widgets/info_card.dart | 88 + .../dashboard/widgets/menu_widget.dart | 6 +- .../dashboard/widgets/sync_indicator.dart | 4 +- .../advanced_privacy_settings_page.dart | 7 +- .../nodes/node_create_or_edit_page.dart | 5 +- lib/src/screens/nodes/widgets/node_form.dart | 5 +- lib/src/screens/rescan/rescan_page.dart | 71 +- .../wallet_restore_from_keys_form.dart | 19 + .../screens/restore/wallet_restore_page.dart | 27 +- .../screens/wallet_keys/wallet_keys_page.dart | 30 +- .../screens/wallet_list/wallet_list_page.dart | 15 +- lib/src/widgets/blockchain_height_widget.dart | 7 +- lib/src/widgets/seed_widget.dart | 3 + lib/store/settings_store.dart | 38 +- .../advanced_privacy_settings_view_model.dart | 1 + .../contact_list/contact_list_view_model.dart | 12 +- .../contact_list/contact_view_model.dart | 12 +- .../dashboard/balance_view_model.dart | 1 + .../dashboard/dashboard_view_model.dart | 66 +- .../dashboard/home_settings_view_model.dart | 1 + .../dashboard/receive_option_view_model.dart | 15 +- .../dashboard/transaction_list_item.dart | 13 +- .../exchange/exchange_view_model.dart | 11 +- .../node_create_or_edit_view_model.dart | 7 +- .../node_list/node_list_view_model.dart | 48 +- .../restore/wallet_restore_from_qr_code.dart | 12 +- lib/view_model/send/fees_view_model.dart | 9 +- lib/view_model/send/output.dart | 8 + lib/view_model/send/send_view_model.dart | 95 +- .../settings/privacy_settings_view_model.dart | 3 +- .../transaction_details_view_model.dart | 56 +- .../unspent_coins_list_view_model.dart | 9 +- ...let_address_edit_or_create_view_model.dart | 68 +- .../wallet_address_list_view_model.dart | 32 +- lib/view_model/wallet_keys_view_model.dart | 9 + lib/view_model/wallet_new_vm.dart | 5 + lib/view_model/wallet_restore_view_model.dart | 24 +- macos/Flutter/GeneratedPluginRegistrant.swift | 2 + macos/Podfile.lock | 8 + model_generator.sh | 8 +- pubspec_base.yaml | 1 + res/values/strings_ar.arb | 3 + res/values/strings_bg.arb | 3 + res/values/strings_cs.arb | 3 + res/values/strings_de.arb | 3 + res/values/strings_en.arb | 3 + res/values/strings_es.arb | 3 + res/values/strings_fr.arb | 3 + res/values/strings_ha.arb | 3 + res/values/strings_hi.arb | 3 + res/values/strings_hr.arb | 3 + res/values/strings_hy.arb | 3 + res/values/strings_id.arb | 3 + res/values/strings_it.arb | 3 + res/values/strings_ja.arb | 3 + res/values/strings_ko.arb | 3 + res/values/strings_my.arb | 3 + res/values/strings_nl.arb | 3 + res/values/strings_pl.arb | 3 + res/values/strings_pt.arb | 3 + res/values/strings_ru.arb | 3 + res/values/strings_th.arb | 3 + res/values/strings_tl.arb | 3 + res/values/strings_tr.arb | 3 + res/values/strings_uk.arb | 3 + res/values/strings_ur.arb | 3 + res/values/strings_vi.arb | 3 + res/values/strings_yo.arb | 3 + res/values/strings_zh.arb | 3 + scripts/android/.gitignore | 3 +- scripts/android/build_all.sh | 3 +- scripts/android/build_decred.sh | 84 + scripts/android/pubspec_gen.sh | 4 +- scripts/ios/app_config.sh | 8 +- scripts/ios/build_all.sh | 2 +- scripts/ios/build_decred.sh | 35 + scripts/ios/build_zmq.sh | 2 +- scripts/macos/app_config.sh | 2 +- scripts/macos/build_all.sh | 2 +- scripts/macos/build_boost_arm64.sh | 1 + scripts/macos/build_boost_common.sh | 8 +- scripts/macos/build_decred.sh | 34 + scripts/macos/build_expat.sh | 4 + scripts/macos/build_monero.sh | 4 + scripts/macos/build_openssl_arm64.sh | 1 + scripts/macos/build_openssl_common.sh | 4 +- scripts/macos/build_sodium.sh | 4 +- scripts/macos/build_unbound.sh | 5 +- scripts/macos/build_zmq.sh | 4 +- scripts/macos/gen.sh | 6 + scripts/macos/gen_common.sh | 4 +- tool/configure.dart | 108 +- 175 files changed, 7145 insertions(+), 1115 deletions(-) create mode 100644 assets/decred_node_list.yml create mode 100644 assets/images/2.0x/decred.png create mode 100644 assets/images/2.0x/decred_menu.png create mode 100644 assets/images/3.0x/decred.png create mode 100644 assets/images/3.0x/decred_menu.png create mode 100644 assets/images/decred.png create mode 100644 assets/images/decred_icon.png create mode 100644 assets/images/decred_menu.png create mode 100644 cw_decred/.gitignore create mode 100644 cw_decred/.metadata create mode 100644 cw_decred/CHANGELOG.md create mode 100644 cw_decred/LICENSE create mode 100644 cw_decred/README.md create mode 100644 cw_decred/analysis_options.yaml create mode 100644 cw_decred/android/.gitignore create mode 100644 cw_decred/android/build.gradle create mode 100644 cw_decred/android/settings.gradle create mode 100644 cw_decred/android/src/main/AndroidManifest.xml create mode 100644 cw_decred/android/src/main/kotlin/com/cakewallet/cw_decred/CwDecredPlugin.kt create mode 100644 cw_decred/ios/.gitignore create mode 100644 cw_decred/ios/Assets/.gitkeep create mode 100644 cw_decred/ios/Classes/CwDecredPlugin.swift create mode 100644 cw_decred/ios/cw_decred.podspec create mode 100644 cw_decred/lib/amount_format.dart create mode 100644 cw_decred/lib/api/libdcrwallet.dart create mode 100644 cw_decred/lib/api/util.dart create mode 100644 cw_decred/lib/balance.dart create mode 100644 cw_decred/lib/mnemonic.dart create mode 100644 cw_decred/lib/pending_transaction.dart create mode 100644 cw_decred/lib/transaction_credentials.dart create mode 100644 cw_decred/lib/transaction_history.dart create mode 100644 cw_decred/lib/transaction_info.dart create mode 100644 cw_decred/lib/transaction_priority.dart create mode 100644 cw_decred/lib/wallet.dart create mode 100644 cw_decred/lib/wallet_addresses.dart create mode 100644 cw_decred/lib/wallet_creation_credentials.dart create mode 100644 cw_decred/lib/wallet_service.dart create mode 100644 cw_decred/macos/Classes/CwDecredPlugin.swift create mode 100644 cw_decred/macos/cw_decred.podspec create mode 100644 cw_decred/pubspec.lock create mode 100644 cw_decred/pubspec.yaml create mode 100644 lib/decred/cw_decred.dart create mode 100644 lib/src/screens/dashboard/widgets/info_card.dart create mode 100755 scripts/android/build_decred.sh create mode 100755 scripts/ios/build_decred.sh create mode 100755 scripts/macos/build_decred.sh create mode 100755 scripts/macos/gen.sh diff --git a/.github/workflows/no_print_in_dart.yaml b/.github/workflows/no_print_in_dart.yaml index 8cd24edfe..b321a9cc9 100644 --- a/.github/workflows/no_print_in_dart.yaml +++ b/.github/workflows/no_print_in_dart.yaml @@ -1,8 +1,6 @@ name: No print statements in dart files -on: - pull_request: - branches: [main] +on: [pull_request] jobs: PR_test_build: diff --git a/.github/workflows/pr_test_build_android.yml b/.github/workflows/pr_test_build_android.yml index 762144ac1..61fb67ee2 100644 --- a/.github/workflows/pr_test_build_android.yml +++ b/.github/workflows/pr_test_build_android.yml @@ -47,6 +47,7 @@ jobs: echo "message<> $GITHUB_ENV echo "$FULL_MESSAGE" >> $GITHUB_ENV echo "EOF" >> $GITHUB_ENV + - name: Add secrets run: | touch lib/.secrets.g.dart @@ -243,6 +244,13 @@ jobs: ./build_mwebd.sh --dont-install popd + - name: Build Decred + run: | + set -x -e + pushd scripts/android + ./build_decred.sh + popd + - name: Build generated code run: | ./model_generator.sh async @@ -281,7 +289,7 @@ jobs: set -x apk_file=$(ls build/app/outputs/flutter-apk/test-apk/${BRANCH_NAME}.apk || exit 1) echo "APK_FILE=$apk_file" >> $GITHUB_ENV - + - name: Upload artifact to slack if: ${{ !contains(env.message, 'skip slack') }} continue-on-error: true @@ -294,7 +302,7 @@ jobs: - name: cleanup run: rm -rf build/app/outputs/flutter-apk/test-apk/ - + - name: Upload Artifact to github uses: actions/upload-artifact@v4 with: diff --git a/.github/workflows/pr_test_build_linux.yml b/.github/workflows/pr_test_build_linux.yml index c5cb26dd9..53904149f 100644 --- a/.github/workflows/pr_test_build_linux.yml +++ b/.github/workflows/pr_test_build_linux.yml @@ -32,6 +32,7 @@ jobs: - uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.sha }} + repository: ${{ github.event.pull_request.head.repo.full_name }} - name: configure git run: | git config --global --add safe.directory '*' diff --git a/.gitignore b/.gitignore index c431a7f60..e78b1c4e6 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ .history .svn/ .fvm/ +.fvmrc # IntelliJ related *.iml @@ -138,6 +139,7 @@ lib/solana/solana.dart lib/tron/tron.dart lib/wownero/wownero.dart lib/zano/zano.dart +lib/decred/decred.dart ios/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_180.png ios/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_120.png @@ -171,6 +173,7 @@ macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png macos/Runner/Configs/AppInfo.xcconfig +macos/Runner.xcodeproj/project.pbxproj integration_test/playground.dart diff --git a/android/app/src/main/AndroidManifestBase.xml b/android/app/src/main/AndroidManifestBase.xml index 5a1824a17..9a324edf3 100644 --- a/android/app/src/main/AndroidManifestBase.xml +++ b/android/app/src/main/AndroidManifestBase.xml @@ -92,6 +92,9 @@ + + + diff --git a/assets/decred_node_list.yml b/assets/decred_node_list.yml new file mode 100644 index 000000000..cb171e701 --- /dev/null +++ b/assets/decred_node_list.yml @@ -0,0 +1,6 @@ +- + uri: default-spv-nodes + is_default: true +- + uri: dcrd.sethforprivacy.com:9108 + useSSL: true \ No newline at end of file diff --git a/assets/images/2.0x/decred.png b/assets/images/2.0x/decred.png new file mode 100644 index 0000000000000000000000000000000000000000..2f4919cecda0ccdff5eed70d5b53938be2e5ea27 GIT binary patch literal 2112 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 QBLDyZ07*qoM6N<$f<`ZExc~qF literal 0 HcmV?d00001 diff --git a/assets/images/3.0x/decred_menu.png b/assets/images/3.0x/decred_menu.png new file mode 100644 index 0000000000000000000000000000000000000000..e55b3fb5c843a0d48b496d8a90a4de1868af8e2b GIT binary patch literal 1423 zcmV;A1#tR_P)EX>4Tx04R}tkv&MmKpe$iTeYQBL@X%ckfA!+MMWHI6^c+H)C#RSm|XfHG-*gu zTpR`0f`cE6RRU7TlmpZjz4Dg~1PK7lyFbi*QEC!XH4 zbk6(4QC5}|;&bA0gDyz?$aUG}H_j!8{X8>jWHWQbQDULg!Ey()vY`@B5yuo&qkMnP zWrgz=XSGset$XqpMhe>U64z;tAcX}iLWBSrHEf^)3vpUCQcPrMKk4BgcKjl_WO8kQ zkz*cJsE`~#_#gc4)+|m=xJi*D(EVcDAEQ8U7iiXP`}^3onpV2qvfq`2fyz2JW+Q;bwkfpBDH^9Lm zFjk`MHIH|PI(z%~Osl^iI~sD3>NA5u00006VoOIv00000008+zyMF)x010qNS#tmY z3ljhU3ljkVnw%H_000McNliru=LiZ0J0zTBU1R_N02y>eSad^gZEa<4bO1wgWnpw> zWFU8GbZ8()Nlj2!fese{00U=9L_t(|+U?!hQ6xbO1kjaY?86g$7$5Ki2jPo|{Q!dL z(UF;zZ2*+&wd&}pBmN0IulM(lj=%eMe+l-w34>~bhe5T$!>HQWVOVYGFswFo7*-oP z466+thSi1+<7y*^VYQ*dxZ22JSZ(Mqt~PQQR~tEutBoAS)kY5EY9ohnwUNWP+Q?yC zZR9YnHgXtO8#!HN+D}wvZ{_6tnO36n<2_32^+=3S6armW%;0XY%xmmQxjBvsW{7Tjs&eS_2z)xD~q@!J}wD#AA?jr8$F*Ok76!CK?%eqHGYrOj|SA z%9JOqFg&d+qdOZArZou2(`87-3_&dC>;3&><9|a9vQ)QwZod4bJN&DFRCYUV!nW-2 zuL_7gled~8$Esyjl)n>4IU}D2H;WP0KM#tVaZ@ApajnvSk`mbB*w#%YKW# z+J~zlXIEjE2QSRXqo>U>pXREP$#F_wbi%;vyRv%8j(8@AYG9{j^6ITCUjcdGHUGlL zWKc-ijB?LI_m4ahcQ$yQd@+M3Fj#L0Ww|*fP_m5?8Uguf;Eg7A3mXCXk#dw=5WU@R zPGU1P*QHmyqYRAvGy)r$5BFhUBXi-d&4c~SJ-+WxID0Px8<}S|0dob6pPp%$j~^7q zEde$%E^ZmHkuh;gfsKrZTMlewEZi1gBjez<0UH?uw-wmXyRc)xM&83612*(L>eMOa8fVjQNkxI!ao{# d3S8kV?N6GS<`Mcb7Cry~002ovPDHLkV1kl0ii`jN literal 0 HcmV?d00001 diff --git a/assets/images/dcr_icon.png b/assets/images/dcr_icon.png index 609873611f9ed85a3d6df68753e4a6afbe628b5d..757cd03882756113d81edee26b6349fa38e144d5 100644 GIT binary patch literal 91254 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 z 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 literal 117789 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 diff --git a/assets/images/decred.png b/assets/images/decred.png new file mode 100644 index 0000000000000000000000000000000000000000..0b12f2ef012e54d7d0e2f9c88843720bc29fcc83 GIT binary patch literal 1237 zcmV;`1SEX>4Tx04R}tkv&MmP!xqvQ>CI62aAe01guVWQ4z;lg(6f4wL+^7CNKSiCJjl8 zi=*ILaPVib>fqw6tAnc`2>yV$3r>nIQsR9{p+$@r9`ED4dk*j22MF~N)2xmtpy?Ge z8IOtS%&Hi8g%1Ph!5~IuW*Kvmlz?Y_-BUN!U4&kZ*5YKE@ z49@$+Ay$x8;&b8&lP*a7$aTf#H_my71)do)Q^|Sa5V4qVW2KE*!PJPSiNmU?RNSu0gp^Pc>L!K}Wr%yn8Lh+`2;kRU=q6-AU#L44YK10|AXJNwQ^IFZc;D?biUa3#~2XU1sXNm{yw(t#tGnm2Cnp$zg!1qKS{5( zwD1wow+&oew=`uBxZDATo($QP9m!8i$mM|dGy0|s(0>bbueoz;p5ycZNYShkH^9Lm zFrKIEb&q#y{D4^000SaNLh0L z01m?d01m?e$8V@)00007bV*G`2k8M83nwU6s5EB)000?uMObu0Z*6U5Zgc=ca%Ew3 zWn>_CX>@2HM@dakSAh-}00084Nkl&DbOuj*a7>6N-W{ALK%4 znszoK6qQK}t4)J_xlAG`sKo>s1TCf@+B8%aWFH74xoF|wt}+`JE~c~y8nH%3Q%TH} zY;o^6Z$2fl4m{rF-1Gkbd(Qozd!Ircvv;hN0Y?E3P%9sRG5Lgm?ucKR%!Q%k0JZ>~ zz;U2}{I&#i0hc0vCAKDkyrcmNprJDUN#5x>%_b*gFLXMo^3z>(hsB}Qfja^*ob z#ttPiQ8YDwzs{BCDwi$dS7LhU=uChsz{Ar}?||iHxY-SXcOvLoGqmhUqQqz5%?MoY zgZN@bxjKPUij?Abx>_D-NuEGpNcgWFfWwWc%Z+}6wp%cfWoqopx8hxBfZY`t z1hx8X1T78XJv|GT127QHEf>?4g&=+_b0+SIe zr2>3m5xQQ$jT{6zv)fk&?9+Pd%Anc`u+s?@n?xTBYe=jX(JwvWtjG8XUHtWxd9&MB1=J^I zek?=pAe0-xTp}b0V6O(N0s22klIgr8_alDg@jpN8Q~-GsdD%GgCT_RYK9J{k`v!ow zYj3sxdu3`eu>k?8gtcr*#@f=sbN*Fqlh00000NkvXXu0mjfp@b+! literal 0 HcmV?d00001 diff --git a/assets/images/decred_icon.png b/assets/images/decred_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..9391abc3d8247f4c60d3d226bc80723f00629855 GIT binary patch literal 1542 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 true; + static Future create({ required String mnemonic, required String password, diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index 5e71bfd6d..d54303075 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -1,6 +1,5 @@ import 'dart:async'; import 'dart:convert'; -import 'dart:typed_data'; import 'package:convert/convert.dart' as convert; import 'dart:math'; @@ -156,6 +155,9 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { @observable SyncStatus mwebSyncStatus = NotConnectedSyncStatus(); + @override + bool get hasRescan => true; + List get scanSecret => mwebHd!.childKey(Bip32KeyIndex(0x80000000)).privateKey.privKey.raw; List get spendSecret => mwebHd!.childKey(Bip32KeyIndex(0x80000001)).privateKey.privKey.raw; diff --git a/cw_core/lib/amount_converter.dart b/cw_core/lib/amount_converter.dart index 71d0cef42..e01c3834a 100644 --- a/cw_core/lib/amount_converter.dart +++ b/cw_core/lib/amount_converter.dart @@ -29,6 +29,7 @@ class AmountConverter { case CryptoCurrency.btc: case CryptoCurrency.bch: case CryptoCurrency.ltc: + case CryptoCurrency.dcr: return _bitcoinAmountToString(amount); case CryptoCurrency.xhv: case CryptoCurrency.xag: diff --git a/cw_core/lib/currency_for_wallet_type.dart b/cw_core/lib/currency_for_wallet_type.dart index bd1c224a3..0f913cb79 100644 --- a/cw_core/lib/currency_for_wallet_type.dart +++ b/cw_core/lib/currency_for_wallet_type.dart @@ -32,9 +32,10 @@ CryptoCurrency currencyForWalletType(WalletType type, {bool? isTestnet}) { return CryptoCurrency.wow; case WalletType.zano: return CryptoCurrency.zano; + case WalletType.decred: + return CryptoCurrency.dcr; case WalletType.none: throw Exception( - 'Unexpected wallet type: ${type.toString()} for CryptoCurrency currencyForWalletType'); } } @@ -65,6 +66,10 @@ WalletType? walletTypeForCurrency(CryptoCurrency currency) { return WalletType.tron; case CryptoCurrency.wow: return WalletType.wownero; + case CryptoCurrency.zano: + return WalletType.zano; + case CryptoCurrency.dcr: + return WalletType.decred; default: return null; } diff --git a/cw_core/lib/node.dart b/cw_core/lib/node.dart index c984cd03b..38fcde9e1 100644 --- a/cw_core/lib/node.dart +++ b/cw_core/lib/node.dart @@ -103,6 +103,7 @@ class Node extends HiveObject with Keyable { case WalletType.solana: case WalletType.tron: case WalletType.zano: + case WalletType.decred: return Uri.parse( "http${isSSL ? "s" : ""}://$uriRaw${path!.startsWith("/") || path!.isEmpty ? path : "/$path"}"); case WalletType.none: @@ -167,6 +168,8 @@ class Node extends HiveObject with Keyable { return requestElectrumServer(); case WalletType.zano: return requestZanoNode(); + case WalletType.decred: + return requestDecredNode(); case WalletType.none: return false; } @@ -355,6 +358,21 @@ class Node extends HiveObject with Keyable { return false; } } + + Future requestDecredNode() async { + if (uri.host == "default-spv-nodes") { + // Just show default port as ok. The wallet will connect to a list of known + // nodes automatically. + return true; + } + try { + final socket = await Socket.connect(uri.host, uri.port, timeout: Duration(seconds: 5)); + socket.destroy(); + return true; + } catch (_) { + return false; + } + } } /// https://github.com/ManyMath/digest_auth/ diff --git a/cw_core/lib/receive_page_option.dart b/cw_core/lib/receive_page_option.dart index 786d07bc5..f7d69bf0a 100644 --- a/cw_core/lib/receive_page_option.dart +++ b/cw_core/lib/receive_page_option.dart @@ -2,6 +2,7 @@ import 'package:cw_core/enumerate.dart'; class ReceivePageOption implements Enumerate { static const mainnet = ReceivePageOption._('mainnet'); + static const testnet = ReceivePageOption._('testnet'); static const anonPayInvoice = ReceivePageOption._('anonPayInvoice'); static const anonPayDonationLink = ReceivePageOption._('anonPayDonationLink'); diff --git a/cw_core/lib/sync_status.dart b/cw_core/lib/sync_status.dart index 7d6b0a285..3183d9d27 100644 --- a/cw_core/lib/sync_status.dart +++ b/cw_core/lib/sync_status.dart @@ -34,6 +34,16 @@ class SyncingSyncStatus extends SyncStatus { } } +class ProcessingSyncStatus extends SyncStatus { + final String? message; + + ProcessingSyncStatus({this.message}); + + @override + double progress() => 0.99; + +} + class SyncedSyncStatus extends SyncStatus { @override double progress() => 1.0; diff --git a/cw_core/lib/wallet_base.dart b/cw_core/lib/wallet_base.dart index 16c794a25..42bd66da0 100644 --- a/cw_core/lib/wallet_base.dart +++ b/cw_core/lib/wallet_base.dart @@ -60,6 +60,8 @@ abstract class WalletBase walletInfo.isHardwareWallet; + bool get hasRescan => false; + Future connectToNode({required Node node}); // there is a default definition here because only coins with a pow node (nano based) need to override this @@ -100,4 +102,6 @@ abstract class WalletBase verifyMessage(String message, String signature, {String? address = null}); bool isTestnet = false; + + bool canSend() => true; } diff --git a/cw_core/lib/wallet_type.dart b/cw_core/lib/wallet_type.dart index 79d2b002d..5ae1c1290 100644 --- a/cw_core/lib/wallet_type.dart +++ b/cw_core/lib/wallet_type.dart @@ -17,6 +17,7 @@ const walletTypes = [ WalletType.solana, WalletType.tron, WalletType.zano, + WalletType.decred, ]; @HiveType(typeId: WALLET_TYPE_TYPE_ID) @@ -60,9 +61,11 @@ enum WalletType { @HiveField(12) wownero, - @HiveField(13) + @HiveField(13) zano, + @HiveField(14) + decred } int serializeToInt(WalletType type) { @@ -93,6 +96,8 @@ int serializeToInt(WalletType type) { return 11; case WalletType.zano: return 12; + case WalletType.decred: + return 13; case WalletType.none: return -1; } @@ -126,6 +131,8 @@ WalletType deserializeFromInt(int raw) { return WalletType.wownero; case 12: return WalletType.zano; + case 13: + return WalletType.decred; default: throw Exception( 'Unexpected token: $raw for WalletType deserializeFromInt'); @@ -160,6 +167,8 @@ String walletTypeToString(WalletType type) { return 'Wownero'; case WalletType.zano: return 'Zano'; + case WalletType.decred: + return 'Decred'; case WalletType.none: return ''; } @@ -193,6 +202,8 @@ String walletTypeToDisplayName(WalletType type) { return 'Wownero (WOW)'; case WalletType.zano: return 'Zano (ZANO)'; + case WalletType.decred: + return 'Decred (DCR)'; case WalletType.none: return ''; } @@ -229,6 +240,8 @@ CryptoCurrency walletTypeToCryptoCurrency(WalletType type, {bool isTestnet = fal return CryptoCurrency.wow; case WalletType.zano: return CryptoCurrency.zano; + case WalletType.decred: + return CryptoCurrency.dcr; case WalletType.none: throw Exception( 'Unexpected wallet type: ${type.toString()} for CryptoCurrency walletTypeToCryptoCurrency'); diff --git a/cw_core/pubspec.lock b/cw_core/pubspec.lock index f023dc153..da7768ee0 100644 --- a/cw_core/pubspec.lock +++ b/cw_core/pubspec.lock @@ -46,6 +46,15 @@ packages: url: "https://pub.dev" source: hosted version: "2.11.0" + blockchain_utils: + dependency: transitive + description: + path: "." + ref: cake-update-v2 + resolved-ref: "59fdf29d72068e0522a96a8953ed7272833a9f57" + url: "https://github.com/cake-tech/blockchain_utils" + source: git + version: "3.3.0" boolean_selector: dependency: transitive description: @@ -465,6 +474,15 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + on_chain: + dependency: "direct main" + description: + path: "." + ref: cake-update-v2 + resolved-ref: "93440dc5126369b873ca1fccc13c3c1240b1c5c2" + url: "https://github.com/cake-tech/on_chain.git" + source: git + version: "3.7.0" package_config: dependency: transitive description: diff --git a/cw_decred/.gitignore b/cw_decred/.gitignore new file mode 100644 index 000000000..d8452de53 --- /dev/null +++ b/cw_decred/.gitignore @@ -0,0 +1,39 @@ +# 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/ + +android/.externalNativeBuild/ +android/.cxx/ +android/libs +ios/External/ +macos/External/ + +*libdcrwallet.h +libdcrwallet_bindings.dart diff --git a/cw_decred/.metadata b/cw_decred/.metadata new file mode 100644 index 000000000..fa060de4b --- /dev/null +++ b/cw_decred/.metadata @@ -0,0 +1,36 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled. + +version: + revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + channel: unknown + +project_type: plugin + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + - platform: android + create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + - platform: ios + create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + - platform: macos + create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/cw_decred/CHANGELOG.md b/cw_decred/CHANGELOG.md new file mode 100644 index 000000000..ac071598e --- /dev/null +++ b/cw_decred/CHANGELOG.md @@ -0,0 +1,3 @@ +## [0.0.1] - TODO: Add release date. + +* TODO: Describe initial release. diff --git a/cw_decred/LICENSE b/cw_decred/LICENSE new file mode 100644 index 000000000..ba75c69f7 --- /dev/null +++ b/cw_decred/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/cw_decred/README.md b/cw_decred/README.md new file mode 100644 index 000000000..d24bc80a4 --- /dev/null +++ b/cw_decred/README.md @@ -0,0 +1,3 @@ +# cw_decred + +TODO: Fill this out. diff --git a/cw_decred/analysis_options.yaml b/cw_decred/analysis_options.yaml new file mode 100644 index 000000000..a5744c1cf --- /dev/null +++ b/cw_decred/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_decred/android/.gitignore b/cw_decred/android/.gitignore new file mode 100644 index 000000000..161bdcdaf --- /dev/null +++ b/cw_decred/android/.gitignore @@ -0,0 +1,9 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures +.cxx diff --git a/cw_decred/android/build.gradle b/cw_decred/android/build.gradle new file mode 100644 index 000000000..0fb2f3cb8 --- /dev/null +++ b/cw_decred/android/build.gradle @@ -0,0 +1,59 @@ +group 'com.cakewallet.cw_decred' +version '1.0-SNAPSHOT' + +buildscript { + ext.kotlin_version = '2.0.21' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:8.7.1' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +rootProject.allprojects { + repositories { + google() + mavenCentral() + } +} + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion 33 + + if (project.android.hasProperty("namespace")) { + namespace 'com.cakewallet.cw_decred' + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = '17' + } + sourceSets { + main { + java.srcDirs += 'src/main/kotlin' + jniLibs.srcDirs 'libs' // contains libdcrwallet.so shared libraries + } + } + defaultConfig { + minSdkVersion 21 + } + externalNativeBuild { + cmake { + } + } +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" +} diff --git a/cw_decred/android/settings.gradle b/cw_decred/android/settings.gradle new file mode 100644 index 000000000..1c81706ad --- /dev/null +++ b/cw_decred/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'cw_decred' diff --git a/cw_decred/android/src/main/AndroidManifest.xml b/cw_decred/android/src/main/AndroidManifest.xml new file mode 100644 index 000000000..ea58d3c72 --- /dev/null +++ b/cw_decred/android/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + diff --git a/cw_decred/android/src/main/kotlin/com/cakewallet/cw_decred/CwDecredPlugin.kt b/cw_decred/android/src/main/kotlin/com/cakewallet/cw_decred/CwDecredPlugin.kt new file mode 100644 index 000000000..4bd6f3395 --- /dev/null +++ b/cw_decred/android/src/main/kotlin/com/cakewallet/cw_decred/CwDecredPlugin.kt @@ -0,0 +1,35 @@ +package com.cakewallet.cw_decred + +import androidx.annotation.NonNull + +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.MethodCallHandler +import io.flutter.plugin.common.MethodChannel.Result + +/** CwDecredPlugin */ +class CwDecredPlugin: FlutterPlugin, MethodCallHandler { + /// The MethodChannel that will the communication between Flutter and native Android + /// + /// This local reference serves to register the plugin with the Flutter Engine and unregister it + /// when the Flutter Engine is detached from the Activity + private lateinit var channel : MethodChannel + + override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + channel = MethodChannel(flutterPluginBinding.binaryMessenger, "cw_decred") + channel.setMethodCallHandler(this) + } + + override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { + if (call.method == "getPlatformVersion") { + result.success("Android ${android.os.Build.VERSION.RELEASE}") + } else { + result.notImplemented() + } + } + + override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { + channel.setMethodCallHandler(null) + } +} diff --git a/cw_decred/ios/.gitignore b/cw_decred/ios/.gitignore new file mode 100644 index 000000000..0c885071e --- /dev/null +++ b/cw_decred/ios/.gitignore @@ -0,0 +1,38 @@ +.idea/ +.vagrant/ +.sconsign.dblite +.svn/ + +.DS_Store +*.swp +profile + +DerivedData/ +build/ +GeneratedPluginRegistrant.h +GeneratedPluginRegistrant.m + +.generated/ + +*.pbxuser +*.mode1v3 +*.mode2v3 +*.perspectivev3 + +!default.pbxuser +!default.mode1v3 +!default.mode2v3 +!default.perspectivev3 + +xcuserdata + +*.moved-aside + +*.pyc +*sync/ +Icon? +.tags* + +/Flutter/Generated.xcconfig +/Flutter/ephemeral/ +/Flutter/flutter_export_environment.sh \ No newline at end of file diff --git a/cw_decred/ios/Assets/.gitkeep b/cw_decred/ios/Assets/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/cw_decred/ios/Classes/CwDecredPlugin.swift b/cw_decred/ios/Classes/CwDecredPlugin.swift new file mode 100644 index 000000000..c38d15516 --- /dev/null +++ b/cw_decred/ios/Classes/CwDecredPlugin.swift @@ -0,0 +1,19 @@ +import Flutter +import UIKit + +public class CwDecredPlugin: NSObject, FlutterPlugin { + public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel(name: "cw_decred", binaryMessenger: registrar.messenger()) + let instance = CwDecredPlugin() + registrar.addMethodCallDelegate(instance, channel: channel) + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + switch call.method { + case "getPlatformVersion": + result("iOS " + UIDevice.current.systemVersion) + default: + result(FlutterMethodNotImplemented) + } + } +} diff --git a/cw_decred/ios/cw_decred.podspec b/cw_decred/ios/cw_decred.podspec new file mode 100644 index 000000000..b36789e08 --- /dev/null +++ b/cw_decred/ios/cw_decred.podspec @@ -0,0 +1,22 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint cw_decred.podspec` to validate before publishing. +# +Pod::Spec.new do |s| + s.name = 'cw_decred' + s.version = '0.0.1' + s.summary = 'Cake Wallet Decred' + s.description = 'Cake Wallet wrapper over Decred project' + s.homepage = 'http://cakewallet.com' + s.license = { :file => '../LICENSE' } + s.author = { 'Cake Wallet' => 'support@cakewallet.com' } + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.dependency 'Flutter' + s.platform = :ios, '11.0' + + s.vendored_libraries = 'External/lib/libdcrwallet.a' + # Flutter.framework does not contain a i386 slice. + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386', "OTHER_LDFLAGS" => "-force_load $(PODS_TARGET_SRCROOT)/External/lib/libdcrwallet.a -lstdc++" } + s.swift_version = '5.0' +end diff --git a/cw_decred/lib/amount_format.dart b/cw_decred/lib/amount_format.dart new file mode 100644 index 000000000..c09f76b3b --- /dev/null +++ b/cw_decred/lib/amount_format.dart @@ -0,0 +1,26 @@ +import 'package:intl/intl.dart'; +import 'package:cw_core/crypto_amount_format.dart'; + +const decredAmountLength = 8; +const decredAmountDivider = 100000000; +final decredAmountFormat = NumberFormat() + ..maximumFractionDigits = decredAmountLength + ..minimumFractionDigits = 1; + +String decredAmountToString({required int amount}) => + decredAmountFormat.format(cryptoAmountToDouble(amount: amount, divider: decredAmountDivider)); + +double decredAmountToDouble({required int amount}) => + cryptoAmountToDouble(amount: amount, divider: decredAmountDivider); + +int stringDoubleToDecredAmount(String amount) { + int result = 0; + + try { + result = (double.parse(amount) * decredAmountDivider).round(); + } catch (e) { + result = 0; + } + + return result; +} diff --git a/cw_decred/lib/api/libdcrwallet.dart b/cw_decred/lib/api/libdcrwallet.dart new file mode 100644 index 000000000..6a26e64c6 --- /dev/null +++ b/cw_decred/lib/api/libdcrwallet.dart @@ -0,0 +1,693 @@ +import 'dart:convert'; +import 'dart:ffi'; +import 'dart:io'; +import 'dart:async'; +import 'dart:isolate'; +import 'package:flutter/foundation.dart'; +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_decred/api/libdcrwallet_bindings.dart'; +import 'package:cw_decred/api/util.dart'; + +final int ErrCodeNotSynced = 1; + +final String libraryName = Platform.isAndroid || Platform.isLinux // TODO: Linux. + ? 'libdcrwallet.so' + : 'cw_decred.framework/cw_decred'; + +class Libwallet { + final SendPort _commands; + final ReceivePort _responses; + final Map> _activeRequests = {}; + int _idCounter = 0; + bool _closed = false; + + static Future spawn() async { + // Create a receive port and add its initial message handler. + final initPort = RawReceivePort(); + final connection = Completer<(ReceivePort, SendPort)>.sync(); + initPort.handler = (initialMessage) { + final commandPort = initialMessage as SendPort; + connection.complete(( + ReceivePort.fromRawReceivePort(initPort), + commandPort, + )); + }; + // Spawn the isolate. + try { + await Isolate.spawn(_startRemoteIsolate, (initPort.sendPort)); + } on Object { + initPort.close(); + rethrow; + } + + final (ReceivePort receivePort, SendPort sendPort) = await connection.future; + + return Libwallet._(receivePort, sendPort); + } + + Libwallet._(this._responses, this._commands) { + _responses.listen(_handleResponsesFromIsolate); + } + + void _handleResponsesFromIsolate(dynamic message) { + final (int id, Object? response) = message as (int, Object?); + final completer = _activeRequests.remove(id)!; + + if (response is RemoteError) { + completer.completeError(response); + } else { + completer.complete(response); + } + + if (_closed && _activeRequests.isEmpty) _responses.close(); + } + + static void _handleCommandsToIsolate( + ReceivePort receivePort, + SendPort sendPort, + ) { + final dcrwalletApi = libdcrwallet(DynamicLibrary.open(libraryName)); + receivePort.listen((message) { + if (message == 'shutdown') { + receivePort.close(); + return; + } + final (int id, Map args) = message as (int, Map); + var res = PayloadResult("", "", 0); + final method = args["method"] ?? ""; + try { + switch (method) { + case "initlibdcrwallet": + final logDir = args["logdir"] ?? ""; + final cLogDir = logDir.toCString(); + executePayloadFn( + fn: () => dcrwalletApi.initialize(cLogDir), + ptrsToFree: [cLogDir], + ); + break; + case "createwallet": + final config = args["config"] ?? ""; + final cConfig = config.toCString(); + executePayloadFn( + fn: () => dcrwalletApi.createWallet(cConfig), + ptrsToFree: [cConfig], + ); + break; + case "createwatchonlywallet": + final config = args["config"] ?? ""; + final cConfig = config.toCString(); + executePayloadFn( + fn: () => dcrwalletApi.createWatchOnlyWallet(cConfig), + ptrsToFree: [cConfig], + ); + break; + case "loadwallet": + final config = args["config"] ?? ""; + final cConfig = config.toCString(); + executePayloadFn( + fn: () => dcrwalletApi.loadWallet(cConfig), + ptrsToFree: [cConfig], + ); + break; + case "startsync": + final name = args["name"] ?? ""; + final peers = args["peers"] ?? ""; + final cName = name.toCString(); + final cPeers = peers.toCString(); + executePayloadFn( + fn: () => dcrwalletApi.syncWallet(cName, cPeers), + ptrsToFree: [cName, cPeers], + ); + break; + case "closewallet": + final name = args["name"] ?? ""; + final cName = name.toCString(); + executePayloadFn( + fn: () => dcrwalletApi.closeWallet(cName), + ptrsToFree: [cName], + ); + break; + case "changewalletpassword": + final name = args["name"] ?? ""; + final oldPass = args["oldpass"] ?? ""; + final newPass = args["newpass"] ?? ""; + final cName = name.toCString(); + final cOldPass = oldPass.toCString(); + final cNewPass = newPass.toCString(); + res = executePayloadFn( + fn: () => dcrwalletApi.changePassphrase(cName, cOldPass, cNewPass), + ptrsToFree: [cName, cOldPass, cNewPass], + ); + break; + case "walletseed": + final name = args["name"] ?? ""; + final pass = args["pass"] ?? ""; + final cName = name.toCString(); + final cPass = pass.toCString(); + res = executePayloadFn( + fn: () => dcrwalletApi.walletSeed(cName, cPass), + ptrsToFree: [cName, cPass], + ); + break; + case "syncstatus": + final name = args["name"] ?? ""; + final cName = name.toCString(); + res = executePayloadFn( + fn: () => dcrwalletApi.syncWalletStatus(cName), + ptrsToFree: [cName], + ); + break; + case "balance": + final name = args["name"] ?? ""; + final cName = name.toCString(); + res = executePayloadFn( + fn: () => dcrwalletApi.walletBalance(cName), + ptrsToFree: [cName], + ); + break; + case "estimatefee": + final name = args["name"] ?? ""; + final numBlocks = args["numblocks"] ?? ""; + final cName = name.toCString(); + final cNumBlocks = numBlocks.toCString(); + res = executePayloadFn( + fn: () => dcrwalletApi.estimateFee(cName, cNumBlocks), + ptrsToFree: [cName, cNumBlocks], + ); + break; + case "createsignedtransaction": + final name = args["name"] ?? ""; + final signReq = args["signreq"] ?? ""; + final cName = name.toCString(); + final cSignReq = signReq.toCString(); + res = executePayloadFn( + fn: () => dcrwalletApi.createSignedTransaction(cName, cSignReq), + ptrsToFree: [cName, cSignReq], + ); + break; + case "sendrawtransaction": + final name = args["name"] ?? ""; + final txHex = args["txhex"] ?? ""; + final cName = name.toCString(); + final cTxHex = txHex.toCString(); + res = executePayloadFn( + fn: () => dcrwalletApi.sendRawTransaction(cName, cTxHex), + ptrsToFree: [cName, cTxHex], + ); + break; + case "listtransactions": + final name = args["name"] ?? ""; + final from = args["from"] ?? ""; + final count = args["count"] ?? ""; + final cName = name.toCString(); + final cFrom = from.toCString(); + final cCount = count.toCString(); + res = executePayloadFn( + fn: () => dcrwalletApi.listTransactions(cName, cFrom, cCount), + ptrsToFree: [cName, cFrom, cCount], + ); + break; + case "bestblock": + final name = args["name"] ?? ""; + final cName = name.toCString(); + res = executePayloadFn( + fn: () => dcrwalletApi.bestBlock(cName), + ptrsToFree: [cName], + ); + break; + case "listunspents": + final name = args["name"] ?? ""; + final cName = name.toCString(); + res = executePayloadFn( + fn: () => dcrwalletApi.listUnspents(cName), + ptrsToFree: [cName], + ); + break; + case "rescanfromheight": + final name = args["name"] ?? ""; + final height = args["height"] ?? ""; + final cName = name.toCString(); + final cHeight = height.toCString(); + res = executePayloadFn( + fn: () => dcrwalletApi.rescanFromHeight(cName, cHeight), + ptrsToFree: [cName, cHeight], + ); + break; + case "signmessage": + final name = args["name"] ?? ""; + final message = args["message"] ?? ""; + final address = args["address"] ?? ""; + final pass = args["pass"] ?? ""; + final cName = name.toCString(); + final cMessage = message.toCString(); + final cAddress = address.toCString(); + final cPass = pass.toCString(); + res = executePayloadFn( + fn: () => dcrwalletApi.signMessage(cName, cMessage, cAddress, cPass), + ptrsToFree: [cName, cMessage, cAddress, cPass], + ); + break; + case "verifymessage": + final name = args["name"] ?? ""; + final message = args["message"] ?? ""; + final address = args["address"] ?? ""; + final sig = args["sig"] ?? ""; + final cName = name.toCString(); + final cMessage = message.toCString(); + final cAddress = address.toCString(); + final cSig = sig.toCString(); + res = executePayloadFn( + fn: () => dcrwalletApi.verifyMessage(cName, cMessage, cAddress, cSig), + ptrsToFree: [cName, cMessage, cAddress, cSig], + ); + break; + case "newexternaladdress": + final name = args["name"] ?? ""; + final cName = name.toCString(); + res = executePayloadFn( + fn: () => dcrwalletApi.newExternalAddress(cName), + ptrsToFree: [cName], + skipErrorCheck: true, + ); + break; + case "defaultpubkey": + final name = args["name"] ?? ""; + final cName = name.toCString(); + res = executePayloadFn( + fn: () => dcrwalletApi.defaultPubkey(cName), + ptrsToFree: [cName], + ); + break; + case "addresses": + final name = args["name"] ?? ""; + final nUsed = args["nused"] ?? ""; + final nUnused = args["nunused"] ?? ""; + final cName = name.toCString(); + final cNUsed = nUsed.toCString(); + final cNUnused = nUnused.toCString(); + res = executePayloadFn( + fn: () => dcrwalletApi.addresses(cName, cNUsed, cNUnused), + ptrsToFree: [cName, cNUsed, cNUnused], + ); + break; + case "birthstate": + final name = args["name"] ?? ""; + final cName = name.toCString(); + res = executePayloadFn( + fn: () => dcrwalletApi.birthState(cName), + ptrsToFree: [cName], + ); + break; + case "shutdown": + final name = args["name"] ?? ""; + final cName = name.toCString(); + executePayloadFn( + fn: () => dcrwalletApi.shutdown(), + ptrsToFree: [], + ); + break; + default: + res = PayloadResult("", "unknown libwallet method ${method}", 0); + } + sendPort.send((id, res)); + } catch (e) { + final errMsg = e.toString(); + printV("decred libwallet returned an error for method ${method}: ${errMsg}"); + sendPort.send((id, PayloadResult("", errMsg, 0))); + } + }); + } + + static void _startRemoteIsolate(SendPort sendPort) { + final receivePort = ReceivePort(); + sendPort.send(receivePort.sendPort); + _handleCommandsToIsolate(receivePort, sendPort); + } + + // initLibdcrwallet initializes libdcrwallet using the provided logDir and gets + // it ready for use. This must be done before attempting to create, load or use + // a wallet. + Future initLibdcrwallet(String logDir) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "initlibdcrwallet", + "logdir": logDir, + }; + _commands.send((id, req)); + await completer.future; + } + + Future createWallet(String config) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "createwallet", + "config": config, + }; + _commands.send((id, req)); + await completer.future; + } + + Future createWatchOnlyWallet(String config) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "createwatchonlywallet", + "config": config, + }; + _commands.send((id, req)); + await completer.future; + } + + Future loadWallet(String config) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "loadwallet", + "config": config, + }; + _commands.send((id, req)); + await completer.future; + } + + Future startSync(String walletName, String peers) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "startsync", + "name": walletName, + "peers": peers, + }; + _commands.send((id, req)); + await completer.future; + } + + Future closeWallet(String walletName) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "closewallet", + "name": walletName, + }; + _commands.send((id, req)); + await completer.future; + } + + Future changeWalletPassword( + String walletName, String currentPassword, String newPassword) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "changewalletpassword", + "name": walletName, + "oldpass": currentPassword, + "newpass": newPassword + }; + _commands.send((id, req)); + final res = await completer.future as PayloadResult; + return res.payload; + } + + Future walletSeed(String walletName, String walletPassword) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "walletseed", + "name": walletName, + "pass": walletPassword, + }; + _commands.send((id, req)); + final res = await completer.future as PayloadResult; + return res.payload; + } + + Future syncStatus(String walletName) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "syncstatus", + "name": walletName, + }; + _commands.send((id, req)); + final res = await completer.future as PayloadResult; + return res.payload; + } + + Future balance(String walletName) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "balance", + "name": walletName, + }; + _commands.send((id, req)); + final res = await completer.future as PayloadResult; + return jsonDecode(res.payload); + } + + Future estimateFee(String walletName, int numBlocks) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "estimatefee", + "name": walletName, + "numblocks": numBlocks.toString(), + }; + _commands.send((id, req)); + final res = await completer.future as PayloadResult; + return res.payload; + } + + Future createSignedTransaction( + String walletName, String createSignedTransactionReq) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "createsignedtransaction", + "name": walletName, + "signreq": createSignedTransactionReq, + }; + _commands.send((id, req)); + final res = await completer.future as PayloadResult; + return res.payload; + } + + Future sendRawTransaction(String walletName, String txHex) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "sendrawtransaction", + "name": walletName, + "txhex": txHex, + }; + _commands.send((id, req)); + final res = await completer.future as PayloadResult; + return res.payload; + } + + Future listTransactions(String walletName, String from, String count) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "listtransactions", + "name": walletName, + "from": from, + "count": count, + }; + _commands.send((id, req)); + final res = await completer.future as PayloadResult; + return res.payload; + } + + Future bestBlock(String walletName) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "bestblock", + "name": walletName, + }; + _commands.send((id, req)); + final res = await completer.future as PayloadResult; + return res.payload; + } + + Future listUnspents(String walletName) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "listunspents", + "name": walletName, + }; + _commands.send((id, req)); + final res = await completer.future as PayloadResult; + return res.payload; + } + + Future rescanFromHeight(String walletName, String height) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "rescanfromheight", + "name": walletName, + "height": height, + }; + _commands.send((id, req)); + final res = await completer.future as PayloadResult; + return res.payload; + } + + Future signMessage( + String walletName, String message, String address, String walletPass) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "signmessage", + "name": walletName, + "message": message, + "address": address, + "pass": walletPass, + }; + _commands.send((id, req)); + final res = await completer.future as PayloadResult; + return res.payload; + } + + Future verifyMessage( + String walletName, String message, String address, String sig) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "verifymessage", + "name": walletName, + "message": message, + "address": address, + "sig": sig, + }; + _commands.send((id, req)); + final res = await completer.future as PayloadResult; + return res.payload; + } + + Future newExternalAddress(String walletName) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "newexternaladdress", + "name": walletName, + }; + _commands.send((id, req)); + final res = await completer.future as PayloadResult; + if (res.errCode == ErrCodeNotSynced) { + // Wallet is not synced. We do not want to give out a used address so give + // nothing. + return null; + } + checkErr(res.err); + return res.payload; + } + + Future defaultPubkey(String walletName) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "defaultpubkey", + "name": walletName, + }; + _commands.send((id, req)); + final res = await completer.future as PayloadResult; + return res.payload; + } + + Future addresses(String walletName, String nUsed, String nUnused) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "addresses", + "name": walletName, + "nused": nUsed, + "nunused": nUnused, + }; + _commands.send((id, req)); + final res = await completer.future as PayloadResult; + return res.payload; + } + + Future birthState(String walletName) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "birthstate", + "name": walletName, + }; + _commands.send((id, req)); + final res = await completer.future as PayloadResult; + return res.payload; + } + + Future shutdown() async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "shutdown", + }; + _commands.send((id, req)); + await completer.future as PayloadResult; + } + + void close() { + if (!_closed) { + _closed = true; + _commands.send('shutdown'); + if (_activeRequests.isEmpty) _responses.close(); + } + } +} diff --git a/cw_decred/lib/api/util.dart b/cw_decred/lib/api/util.dart new file mode 100644 index 000000000..42c3def70 --- /dev/null +++ b/cw_decred/lib/api/util.dart @@ -0,0 +1,64 @@ +import 'dart:ffi'; +import 'package:ffi/ffi.dart'; +import 'dart:convert'; + +class PayloadResult { + final String payload; + final String err; + final int errCode; + + const PayloadResult(this.payload, this.err, this.errCode); +} + +// Executes the provided fn and converts the string response to a PayloadResult. +// Returns payload, error code, and error. +PayloadResult executePayloadFn({ + required Pointer fn(), + required List ptrsToFree, + bool skipErrorCheck = false, +}) { + final jsonStr = fn().toDartString(); + freePointers(ptrsToFree); + if (jsonStr == null) throw Exception("no json return from wallet library"); + final decoded = json.decode(jsonStr); + + final err = decoded["error"] ?? ""; + if (!skipErrorCheck) { + checkErr(err); + } + + final payload = decoded["payload"] ?? ""; + final errCode = decoded["errorcode"] ?? -1; + return new PayloadResult(payload, err, errCode); +} + +void freePointers(List ptrsToFree) { + for (final ptr in ptrsToFree) { + malloc.free(ptr); + } +} + +void checkErr(String err) { + if (err == "") return; + throw Exception(err); +} + +extension StringUtil on String { + Pointer toCString() => toNativeUtf8().cast(); +} + +extension CStringUtil on Pointer { + bool get isNull => address == nullptr.address; + + free() { + malloc.free(this); + } + + String? toDartString() { + if (isNull) return null; + + final str = cast().toDartString(); + free(); + return str; + } +} diff --git a/cw_decred/lib/balance.dart b/cw_decred/lib/balance.dart new file mode 100644 index 000000000..a88098a9f --- /dev/null +++ b/cw_decred/lib/balance.dart @@ -0,0 +1,25 @@ +import 'package:cw_decred/amount_format.dart'; +import 'package:cw_core/balance.dart'; + +class DecredBalance extends Balance { + const DecredBalance({required this.confirmed, required this.unconfirmed, required this.frozen}) + : super(confirmed, unconfirmed); + + factory DecredBalance.zero() => DecredBalance(confirmed: 0, unconfirmed: 0, frozen: 0); + + final int confirmed; + final int unconfirmed; + final int frozen; + + @override + String get formattedAvailableBalance => decredAmountToString(amount: confirmed - frozen); + + @override + String get formattedAdditionalBalance => decredAmountToString(amount: unconfirmed); + + @override + String get formattedUnAvailableBalance { + final frozenFormatted = decredAmountToString(amount: frozen); + return frozenFormatted == '0.0' ? '' : frozenFormatted; + } +} diff --git a/cw_decred/lib/mnemonic.dart b/cw_decred/lib/mnemonic.dart new file mode 100644 index 000000000..bd39e2a18 --- /dev/null +++ b/cw_decred/lib/mnemonic.dart @@ -0,0 +1,2050 @@ +final wordlist = [ + "abandon", + "ability", + "able", + "about", + "above", + "absent", + "absorb", + "abstract", + "absurd", + "abuse", + "access", + "accident", + "account", + "accuse", + "achieve", + "acid", + "acoustic", + "acquire", + "across", + "act", + "action", + "actor", + "actress", + "actual", + "adapt", + "add", + "addict", + "address", + "adjust", + "admit", + "adult", + "advance", + "advice", + "aerobic", + "affair", + "afford", + "afraid", + "again", + "age", + "agent", + "agree", + "ahead", + "aim", + "air", + "airport", + "aisle", + "alarm", + "album", + "alcohol", + "alert", + "alien", + "all", + "alley", + "allow", + "almost", + "alone", + "alpha", + "already", + "also", + "alter", + "always", + "amateur", + "amazing", + "among", + "amount", + "amused", + "analyst", + "anchor", + "ancient", + "anger", + "angle", + "angry", + "animal", + "ankle", + "announce", + "annual", + "another", + "answer", + "antenna", + "antique", + "anxiety", + "any", + "apart", + "apology", + "appear", + "apple", + "approve", + "april", + "arch", + "arctic", + "area", + "arena", + "argue", + "arm", + "armed", + "armor", + "army", + "around", + "arrange", + "arrest", + "arrive", + "arrow", + "art", + "artefact", + "artist", + "artwork", + "ask", + "aspect", + "assault", + "asset", + "assist", + "assume", + "asthma", + "athlete", + "atom", + "attack", + "attend", + "attitude", + "attract", + "auction", + "audit", + "august", + "aunt", + "author", + "auto", + "autumn", + "average", + "avocado", + "avoid", + "awake", + "aware", + "away", + "awesome", + "awful", + "awkward", + "axis", + "baby", + "bachelor", + "bacon", + "badge", + "bag", + "balance", + "balcony", + "ball", + "bamboo", + "banana", + "banner", + "bar", + "barely", + "bargain", + "barrel", + "base", + "basic", + "basket", + "battle", + "beach", + "bean", + "beauty", + "because", + "become", + "beef", + "before", + "begin", + "behave", + "behind", + "believe", + "below", + "belt", + "bench", + "benefit", + "best", + "betray", + "better", + "between", + "beyond", + "bicycle", + "bid", + "bike", + "bind", + "biology", + "bird", + "birth", + "bitter", + "black", + "blade", + "blame", + "blanket", + "blast", + "bleak", + "bless", + "blind", + "blood", + "blossom", + "blouse", + "blue", + "blur", + "blush", + "board", + "boat", + "body", + "boil", + "bomb", + "bone", + "bonus", + "book", + "boost", + "border", + "boring", + "borrow", + "boss", + "bottom", + "bounce", + "box", + "boy", + "bracket", + "brain", + "brand", + "brass", + "brave", + "bread", + "breeze", + "brick", + "bridge", + "brief", + "bright", + "bring", + "brisk", + "broccoli", + "broken", + "bronze", + "broom", + "brother", + "brown", + "brush", + "bubble", + "buddy", + "budget", + "buffalo", + "build", + "bulb", + "bulk", + "bullet", + "bundle", + "bunker", + "burden", + "burger", + "burst", + "bus", + "business", + "busy", + "butter", + "buyer", + "buzz", + "cabbage", + "cabin", + "cable", + "cactus", + "cage", + "cake", + "call", + "calm", + "camera", + "camp", + "can", + "canal", + "cancel", + "candy", + "cannon", + "canoe", + "canvas", + "canyon", + "capable", + "capital", + "captain", + "car", + "carbon", + "card", + "cargo", + "carpet", + "carry", + "cart", + "case", + "cash", + "casino", + "castle", + "casual", + "cat", + "catalog", + "catch", + "category", + "cattle", + "caught", + "cause", + "caution", + "cave", + "ceiling", + "celery", + "cement", + "census", + "century", + "cereal", + "certain", + "chair", + "chalk", + "champion", + "change", + "chaos", + "chapter", + "charge", + "chase", + "chat", + "cheap", + "check", + "cheese", + "chef", + "cherry", + "chest", + "chicken", + "chief", + "child", + "chimney", + "choice", + "choose", + "chronic", + "chuckle", + "chunk", + "churn", + "cigar", + "cinnamon", + "circle", + "citizen", + "city", + "civil", + "claim", + "clap", + "clarify", + "claw", + "clay", + "clean", + "clerk", + "clever", + "click", + "client", + "cliff", + "climb", + "clinic", + "clip", + "clock", + "clog", + "close", + "cloth", + "cloud", + "clown", + "club", + "clump", + "cluster", + "clutch", + "coach", + "coast", + "coconut", + "code", + "coffee", + "coil", + "coin", + "collect", + "color", + "column", + "combine", + "come", + "comfort", + "comic", + "common", + "company", + "concert", + "conduct", + "confirm", + "congress", + "connect", + "consider", + "control", + "convince", + "cook", + "cool", + "copper", + "copy", + "coral", + "core", + "corn", + "correct", + "cost", + "cotton", + "couch", + "country", + "couple", + "course", + "cousin", + "cover", + "coyote", + "crack", + "cradle", + "craft", + "cram", + "crane", + "crash", + "crater", + "crawl", + "crazy", + "cream", + "credit", + "creek", + "crew", + "cricket", + "crime", + "crisp", + "critic", + "crop", + "cross", + "crouch", + "crowd", + "crucial", + "cruel", + "cruise", + "crumble", + "crunch", + "crush", + "cry", + "crystal", + "cube", + "culture", + "cup", + "cupboard", + "curious", + "current", + "curtain", + "curve", + "cushion", + "custom", + "cute", + "cycle", + "dad", + "damage", + "damp", + "dance", + "danger", + "daring", + "dash", + "daughter", + "dawn", + "day", + "deal", + "debate", + "debris", + "decade", + "december", + "decide", + "decline", + "decorate", + "decrease", + "deer", + "defense", + "define", + "defy", + "degree", + "delay", + "deliver", + "demand", + "demise", + "denial", + "dentist", + "deny", + "depart", + "depend", + "deposit", + "depth", + "deputy", + "derive", + "describe", + "desert", + "design", + "desk", + "despair", + "destroy", + "detail", + "detect", + "develop", + "device", + "devote", + "diagram", + "dial", + "diamond", + "diary", + "dice", + "diesel", + "diet", + "differ", + "digital", + "dignity", + "dilemma", + "dinner", + "dinosaur", + "direct", + "dirt", + "disagree", + "discover", + "disease", + "dish", + "dismiss", + "disorder", + "display", + "distance", + "divert", + "divide", + "divorce", + "dizzy", + "doctor", + "document", + "dog", + "doll", + "dolphin", + "domain", + "donate", + "donkey", + "donor", + "door", + "dose", + "double", + "dove", + "draft", + "dragon", + "drama", + "drastic", + "draw", + "dream", + "dress", + "drift", + "drill", + "drink", + "drip", + "drive", + "drop", + "drum", + "dry", + "duck", + "dumb", + "dune", + "during", + "dust", + "dutch", + "duty", + "dwarf", + "dynamic", + "eager", + "eagle", + "early", + "earn", + "earth", + "easily", + "east", + "easy", + "echo", + "ecology", + "economy", + "edge", + "edit", + "educate", + "effort", + "egg", + "eight", + "either", + "elbow", + "elder", + "electric", + "elegant", + "element", + "elephant", + "elevator", + "elite", + "else", + "embark", + "embody", + "embrace", + "emerge", + "emotion", + "employ", + "empower", + "empty", + "enable", + "enact", + "end", + "endless", + "endorse", + "enemy", + "energy", + "enforce", + "engage", + "engine", + "enhance", + "enjoy", + "enlist", + "enough", + "enrich", + "enroll", + "ensure", + "enter", + "entire", + "entry", + "envelope", + "episode", + "equal", + "equip", + "era", + "erase", + "erode", + "erosion", + "error", + "erupt", + "escape", + "essay", + "essence", + "estate", + "eternal", + "ethics", + "evidence", + "evil", + "evoke", + "evolve", + "exact", + "example", + "excess", + "exchange", + "excite", + "exclude", + "excuse", + "execute", + "exercise", + "exhaust", + "exhibit", + "exile", + "exist", + "exit", + "exotic", + "expand", + "expect", + "expire", + "explain", + "expose", + "express", + "extend", + "extra", + "eye", + "eyebrow", + "fabric", + "face", + "faculty", + "fade", + "faint", + "faith", + "fall", + "false", + "fame", + "family", + "famous", + "fan", + "fancy", + "fantasy", + "farm", + "fashion", + "fat", + "fatal", + "father", + "fatigue", + "fault", + "favorite", + "feature", + "february", + "federal", + "fee", + "feed", + "feel", + "female", + "fence", + "festival", + "fetch", + "fever", + "few", + "fiber", + "fiction", + "field", + "figure", + "file", + "film", + "filter", + "final", + "find", + "fine", + "finger", + "finish", + "fire", + "firm", + "first", + "fiscal", + "fish", + "fit", + "fitness", + "fix", + "flag", + "flame", + "flash", + "flat", + "flavor", + "flee", + "flight", + "flip", + "float", + "flock", + "floor", + "flower", + "fluid", + "flush", + "fly", + "foam", + "focus", + "fog", + "foil", + "fold", + "follow", + "food", + "foot", + "force", + "forest", + "forget", + "fork", + "fortune", + "forum", + "forward", + "fossil", + "foster", + "found", + "fox", + "fragile", + "frame", + "frequent", + "fresh", + "friend", + "fringe", + "frog", + "front", + "frost", + "frown", + "frozen", + "fruit", + "fuel", + "fun", + "funny", + "furnace", + "fury", + "future", + "gadget", + "gain", + "galaxy", + "gallery", + "game", + "gap", + "garage", + "garbage", + "garden", + "garlic", + "garment", + "gas", + "gasp", + "gate", + "gather", + "gauge", + "gaze", + "general", + "genius", + "genre", + "gentle", + "genuine", + "gesture", + "ghost", + "giant", + "gift", + "giggle", + "ginger", + "giraffe", + "girl", + "give", + "glad", + "glance", + "glare", + "glass", + "glide", + "glimpse", + "globe", + "gloom", + "glory", + "glove", + "glow", + "glue", + "goat", + "goddess", + "gold", + "good", + "goose", + "gorilla", + "gospel", + "gossip", + "govern", + "gown", + "grab", + "grace", + "grain", + "grant", + "grape", + "grass", + "gravity", + "great", + "green", + "grid", + "grief", + "grit", + "grocery", + "group", + "grow", + "grunt", + "guard", + "guess", + "guide", + "guilt", + "guitar", + "gun", + "gym", + "habit", + "hair", + "half", + "hammer", + "hamster", + "hand", + "happy", + "harbor", + "hard", + "harsh", + "harvest", + "hat", + "have", + "hawk", + "hazard", + "head", + "health", + "heart", + "heavy", + "hedgehog", + "height", + "hello", + "helmet", + "help", + "hen", + "hero", + "hidden", + "high", + "hill", + "hint", + "hip", + "hire", + "history", + "hobby", + "hockey", + "hold", + "hole", + "holiday", + "hollow", + "home", + "honey", + "hood", + "hope", + "horn", + "horror", + "horse", + "hospital", + "host", + "hotel", + "hour", + "hover", + "hub", + "huge", + "human", + "humble", + "humor", + "hundred", + "hungry", + "hunt", + "hurdle", + "hurry", + "hurt", + "husband", + "hybrid", + "ice", + "icon", + "idea", + "identify", + "idle", + "ignore", + "ill", + "illegal", + "illness", + "image", + "imitate", + "immense", + "immune", + "impact", + "impose", + "improve", + "impulse", + "inch", + "include", + "income", + "increase", + "index", + "indicate", + "indoor", + "industry", + "infant", + "inflict", + "inform", + "inhale", + "inherit", + "initial", + "inject", + "injury", + "inmate", + "inner", + "innocent", + "input", + "inquiry", + "insane", + "insect", + "inside", + "inspire", + "install", + "intact", + "interest", + "into", + "invest", + "invite", + "involve", + "iron", + "island", + "isolate", + "issue", + "item", + "ivory", + "jacket", + "jaguar", + "jar", + "jazz", + "jealous", + "jeans", + "jelly", + "jewel", + "job", + "join", + "joke", + "journey", + "joy", + "judge", + "juice", + "jump", + "jungle", + "junior", + "junk", + "just", + "kangaroo", + "keen", + "keep", + "ketchup", + "key", + "kick", + "kid", + "kidney", + "kind", + "kingdom", + "kiss", + "kit", + "kitchen", + "kite", + "kitten", + "kiwi", + "knee", + "knife", + "knock", + "know", + "lab", + "label", + "labor", + "ladder", + "lady", + "lake", + "lamp", + "language", + "laptop", + "large", + "later", + "latin", + "laugh", + "laundry", + "lava", + "law", + "lawn", + "lawsuit", + "layer", + "lazy", + "leader", + "leaf", + "learn", + "leave", + "lecture", + "left", + "leg", + "legal", + "legend", + "leisure", + "lemon", + "lend", + "length", + "lens", + "leopard", + "lesson", + "letter", + "level", + "liar", + "liberty", + "library", + "license", + "life", + "lift", + "light", + "like", + "limb", + "limit", + "link", + "lion", + "liquid", + "list", + "little", + "live", + "lizard", + "load", + "loan", + "lobster", + "local", + "lock", + "logic", + "lonely", + "long", + "loop", + "lottery", + "loud", + "lounge", + "love", + "loyal", + "lucky", + "luggage", + "lumber", + "lunar", + "lunch", + "luxury", + "lyrics", + "machine", + "mad", + "magic", + "magnet", + "maid", + "mail", + "main", + "major", + "make", + "mammal", + "man", + "manage", + "mandate", + "mango", + "mansion", + "manual", + "maple", + "marble", + "march", + "margin", + "marine", + "market", + "marriage", + "mask", + "mass", + "master", + "match", + "material", + "math", + "matrix", + "matter", + "maximum", + "maze", + "meadow", + "mean", + "measure", + "meat", + "mechanic", + "medal", + "media", + "melody", + "melt", + "member", + "memory", + "mention", + "menu", + "mercy", + "merge", + "merit", + "merry", + "mesh", + "message", + "metal", + "method", + "middle", + "midnight", + "milk", + "million", + "mimic", + "mind", + "minimum", + "minor", + "minute", + "miracle", + "mirror", + "misery", + "miss", + "mistake", + "mix", + "mixed", + "mixture", + "mobile", + "model", + "modify", + "mom", + "moment", + "monitor", + "monkey", + "monster", + "month", + "moon", + "moral", + "more", + "morning", + "mosquito", + "mother", + "motion", + "motor", + "mountain", + "mouse", + "move", + "movie", + "much", + "muffin", + "mule", + "multiply", + "muscle", + "museum", + "mushroom", + "music", + "must", + "mutual", + "myself", + "mystery", + "myth", + "naive", + "name", + "napkin", + "narrow", + "nasty", + "nation", + "nature", + "near", + "neck", + "need", + "negative", + "neglect", + "neither", + "nephew", + "nerve", + "nest", + "net", + "network", + "neutral", + "never", + "news", + "next", + "nice", + "night", + "noble", + "noise", + "nominee", + "noodle", + "normal", + "north", + "nose", + "notable", + "note", + "nothing", + "notice", + "novel", + "now", + "nuclear", + "number", + "nurse", + "nut", + "oak", + "obey", + "object", + "oblige", + "obscure", + "observe", + "obtain", + "obvious", + "occur", + "ocean", + "october", + "odor", + "off", + "offer", + "office", + "often", + "oil", + "okay", + "old", + "olive", + "olympic", + "omit", + "once", + "one", + "onion", + "online", + "only", + "open", + "opera", + "opinion", + "oppose", + "option", + "orange", + "orbit", + "orchard", + "order", + "ordinary", + "organ", + "orient", + "original", + "orphan", + "ostrich", + "other", + "outdoor", + "outer", + "output", + "outside", + "oval", + "oven", + "over", + "own", + "owner", + "oxygen", + "oyster", + "ozone", + "pact", + "paddle", + "page", + "pair", + "palace", + "palm", + "panda", + "panel", + "panic", + "panther", + "paper", + "parade", + "parent", + "park", + "parrot", + "party", + "pass", + "patch", + "path", + "patient", + "patrol", + "pattern", + "pause", + "pave", + "payment", + "peace", + "peanut", + "pear", + "peasant", + "pelican", + "pen", + "penalty", + "pencil", + "people", + "pepper", + "perfect", + "permit", + "person", + "pet", + "phone", + "photo", + "phrase", + "physical", + "piano", + "picnic", + "picture", + "piece", + "pig", + "pigeon", + "pill", + "pilot", + "pink", + "pioneer", + "pipe", + "pistol", + "pitch", + "pizza", + "place", + "planet", + "plastic", + "plate", + "play", + "please", + "pledge", + "pluck", + "plug", + "plunge", + "poem", + "poet", + "point", + "polar", + "pole", + "police", + "pond", + "pony", + "pool", + "popular", + "portion", + "position", + "possible", + "post", + "potato", + "pottery", + "poverty", + "powder", + "power", + "practice", + "praise", + "predict", + "prefer", + "prepare", + "present", + "pretty", + "prevent", + "price", + "pride", + "primary", + "print", + "priority", + "prison", + "private", + "prize", + "problem", + "process", + "produce", + "profit", + "program", + "project", + "promote", + "proof", + "property", + "prosper", + "protect", + "proud", + "provide", + "public", + "pudding", + "pull", + "pulp", + "pulse", + "pumpkin", + "punch", + "pupil", + "puppy", + "purchase", + "purity", + "purpose", + "purse", + "push", + "put", + "puzzle", + "pyramid", + "quality", + "quantum", + "quarter", + "question", + "quick", + "quit", + "quiz", + "quote", + "rabbit", + "raccoon", + "race", + "rack", + "radar", + "radio", + "rail", + "rain", + "raise", + "rally", + "ramp", + "ranch", + "random", + "range", + "rapid", + "rare", + "rate", + "rather", + "raven", + "raw", + "razor", + "ready", + "real", + "reason", + "rebel", + "rebuild", + "recall", + "receive", + "recipe", + "record", + "recycle", + "reduce", + "reflect", + "reform", + "refuse", + "region", + "regret", + "regular", + "reject", + "relax", + "release", + "relief", + "rely", + "remain", + "remember", + "remind", + "remove", + "render", + "renew", + "rent", + "reopen", + "repair", + "repeat", + "replace", + "report", + "require", + "rescue", + "resemble", + "resist", + "resource", + "response", + "result", + "retire", + "retreat", + "return", + "reunion", + "reveal", + "review", + "reward", + "rhythm", + "rib", + "ribbon", + "rice", + "rich", + "ride", + "ridge", + "rifle", + "right", + "rigid", + "ring", + "riot", + "ripple", + "risk", + "ritual", + "rival", + "river", + "road", + "roast", + "robot", + "robust", + "rocket", + "romance", + "roof", + "rookie", + "room", + "rose", + "rotate", + "rough", + "round", + "route", + "royal", + "rubber", + "rude", + "rug", + "rule", + "run", + "runway", + "rural", + "sad", + "saddle", + "sadness", + "safe", + "sail", + "salad", + "salmon", + "salon", + "salt", + "salute", + "same", + "sample", + "sand", + "satisfy", + "satoshi", + "sauce", + "sausage", + "save", + "say", + "scale", + "scan", + "scare", + "scatter", + "scene", + "scheme", + "school", + "science", + "scissors", + "scorpion", + "scout", + "scrap", + "screen", + "script", + "scrub", + "sea", + "search", + "season", + "seat", + "second", + "secret", + "section", + "security", + "seed", + "seek", + "segment", + "select", + "sell", + "seminar", + "senior", + "sense", + "sentence", + "series", + "service", + "session", + "settle", + "setup", + "seven", + "shadow", + "shaft", + "shallow", + "share", + "shed", + "shell", + "sheriff", + "shield", + "shift", + "shine", + "ship", + "shiver", + "shock", + "shoe", + "shoot", + "shop", + "short", + "shoulder", + "shove", + "shrimp", + "shrug", + "shuffle", + "shy", + "sibling", + "sick", + "side", + "siege", + "sight", + "sign", + "silent", + "silk", + "silly", + "silver", + "similar", + "simple", + "since", + "sing", + "siren", + "sister", + "situate", + "six", + "size", + "skate", + "sketch", + "ski", + "skill", + "skin", + "skirt", + "skull", + "slab", + "slam", + "sleep", + "slender", + "slice", + "slide", + "slight", + "slim", + "slogan", + "slot", + "slow", + "slush", + "small", + "smart", + "smile", + "smoke", + "smooth", + "snack", + "snake", + "snap", + "sniff", + "snow", + "soap", + "soccer", + "social", + "sock", + "soda", + "soft", + "solar", + "soldier", + "solid", + "solution", + "solve", + "someone", + "song", + "soon", + "sorry", + "sort", + "soul", + "sound", + "soup", + "source", + "south", + "space", + "spare", + "spatial", + "spawn", + "speak", + "special", + "speed", + "spell", + "spend", + "sphere", + "spice", + "spider", + "spike", + "spin", + "spirit", + "split", + "spoil", + "sponsor", + "spoon", + "sport", + "spot", + "spray", + "spread", + "spring", + "spy", + "square", + "squeeze", + "squirrel", + "stable", + "stadium", + "staff", + "stage", + "stairs", + "stamp", + "stand", + "start", + "state", + "stay", + "steak", + "steel", + "stem", + "step", + "stereo", + "stick", + "still", + "sting", + "stock", + "stomach", + "stone", + "stool", + "story", + "stove", + "strategy", + "street", + "strike", + "strong", + "struggle", + "student", + "stuff", + "stumble", + "style", + "subject", + "submit", + "subway", + "success", + "such", + "sudden", + "suffer", + "sugar", + "suggest", + "suit", + "summer", + "sun", + "sunny", + "sunset", + "super", + "supply", + "supreme", + "sure", + "surface", + "surge", + "surprise", + "surround", + "survey", + "suspect", + "sustain", + "swallow", + "swamp", + "swap", + "swarm", + "swear", + "sweet", + "swift", + "swim", + "swing", + "switch", + "sword", + "symbol", + "symptom", + "syrup", + "system", + "table", + "tackle", + "tag", + "tail", + "talent", + "talk", + "tank", + "tape", + "target", + "task", + "taste", + "tattoo", + "taxi", + "teach", + "team", + "tell", + "ten", + "tenant", + "tennis", + "tent", + "term", + "test", + "text", + "thank", + "that", + "theme", + "then", + "theory", + "there", + "they", + "thing", + "this", + "thought", + "three", + "thrive", + "throw", + "thumb", + "thunder", + "ticket", + "tide", + "tiger", + "tilt", + "timber", + "time", + "tiny", + "tip", + "tired", + "tissue", + "title", + "toast", + "tobacco", + "today", + "toddler", + "toe", + "together", + "toilet", + "token", + "tomato", + "tomorrow", + "tone", + "tongue", + "tonight", + "tool", + "tooth", + "top", + "topic", + "topple", + "torch", + "tornado", + "tortoise", + "toss", + "total", + "tourist", + "toward", + "tower", + "town", + "toy", + "track", + "trade", + "traffic", + "tragic", + "train", + "transfer", + "trap", + "trash", + "travel", + "tray", + "treat", + "tree", + "trend", + "trial", + "tribe", + "trick", + "trigger", + "trim", + "trip", + "trophy", + "trouble", + "truck", + "true", + "truly", + "trumpet", + "trust", + "truth", + "try", + "tube", + "tuition", + "tumble", + "tuna", + "tunnel", + "turkey", + "turn", + "turtle", + "twelve", + "twenty", + "twice", + "twin", + "twist", + "two", + "type", + "typical", + "ugly", + "umbrella", + "unable", + "unaware", + "uncle", + "uncover", + "under", + "undo", + "unfair", + "unfold", + "unhappy", + "uniform", + "unique", + "unit", + "universe", + "unknown", + "unlock", + "until", + "unusual", + "unveil", + "update", + "upgrade", + "uphold", + "upon", + "upper", + "upset", + "urban", + "urge", + "usage", + "use", + "used", + "useful", + "useless", + "usual", + "utility", + "vacant", + "vacuum", + "vague", + "valid", + "valley", + "valve", + "van", + "vanish", + "vapor", + "various", + "vast", + "vault", + "vehicle", + "velvet", + "vendor", + "venture", + "venue", + "verb", + "verify", + "version", + "very", + "vessel", + "veteran", + "viable", + "vibrant", + "vicious", + "victory", + "video", + "view", + "village", + "vintage", + "violin", + "virtual", + "virus", + "visa", + "visit", + "visual", + "vital", + "vivid", + "vocal", + "voice", + "void", + "volcano", + "volume", + "vote", + "voyage", + "wage", + "wagon", + "wait", + "walk", + "wall", + "walnut", + "want", + "warfare", + "warm", + "warrior", + "wash", + "wasp", + "waste", + "water", + "wave", + "way", + "wealth", + "weapon", + "wear", + "weasel", + "weather", + "web", + "wedding", + "weekend", + "weird", + "welcome", + "west", + "wet", + "whale", + "what", + "wheat", + "wheel", + "when", + "where", + "whip", + "whisper", + "wide", + "width", + "wife", + "wild", + "will", + "win", + "window", + "wine", + "wing", + "wink", + "winner", + "winter", + "wire", + "wisdom", + "wise", + "wish", + "witness", + "wolf", + "woman", + "wonder", + "wood", + "wool", + "word", + "work", + "world", + "worry", + "worth", + "wrap", + "wreck", + "wrestle", + "wrist", + "write", + "wrong", + "yard", + "year", + "yellow", + "you", + "young", + "youth", + "zebra", + "zero", + "zone", + "zoo", +]; diff --git a/cw_decred/lib/pending_transaction.dart b/cw_decred/lib/pending_transaction.dart new file mode 100644 index 000000000..63162bf40 --- /dev/null +++ b/cw_decred/lib/pending_transaction.dart @@ -0,0 +1,39 @@ +import 'package:cw_core/pending_transaction.dart'; +import 'package:cw_decred/amount_format.dart'; + +class DecredPendingTransaction with PendingTransaction { + DecredPendingTransaction( + {required this.txid, + required this.amount, + required this.fee, + required this.rawHex, + required this.send}); + + final int amount; + final int fee; + final String txid; + final String rawHex; + final Future Function() send; + + @override + String get id => txid; + + @override + String get amountFormatted => decredAmountToString(amount: amount); + + @override + String get feeFormatted => decredAmountToString(amount: fee); + + @override + String get hex => rawHex; + + @override + Future commit() async { + return send(); + } + + @override + Future commitUR() { + throw UnimplementedError(); + } +} diff --git a/cw_decred/lib/transaction_credentials.dart b/cw_decred/lib/transaction_credentials.dart new file mode 100644 index 000000000..5ace384f4 --- /dev/null +++ b/cw_decred/lib/transaction_credentials.dart @@ -0,0 +1,10 @@ +import 'package:cw_decred/transaction_priority.dart'; +import 'package:cw_core/output_info.dart'; + +class DecredTransactionCredentials { + DecredTransactionCredentials(this.outputs, {required this.priority, this.feeRate}); + + final List outputs; + final DecredTransactionPriority? priority; + final int? feeRate; +} diff --git a/cw_decred/lib/transaction_history.dart b/cw_decred/lib/transaction_history.dart new file mode 100644 index 000000000..02227aa9c --- /dev/null +++ b/cw_decred/lib/transaction_history.dart @@ -0,0 +1,31 @@ +import 'package:mobx/mobx.dart'; +import 'package:cw_core/transaction_info.dart'; +import 'package:cw_core/transaction_history.dart'; + +class DecredTransactionHistory extends TransactionHistoryBase { + DecredTransactionHistory() { + transactions = ObservableMap(); + } + + @override + void addOne(TransactionInfo transaction) => transactions[transaction.id] = transaction; + + @override + void addMany(Map transactions) => this.transactions.addAll(transactions); + + @override + Future save() async {} + + // update returns true if a known transaction that is not pending was found. + bool update(Map txs) { + var foundOldTx = false; + txs.forEach((_, tx) { + if (!this.transactions.containsKey(tx.id) || this.transactions[tx.id]!.isPending) { + this.transactions[tx.id] = tx; + } else { + foundOldTx = true; + } + }); + return foundOldTx; + } +} diff --git a/cw_decred/lib/transaction_info.dart b/cw_decred/lib/transaction_info.dart new file mode 100644 index 000000000..4afe09e1b --- /dev/null +++ b/cw_decred/lib/transaction_info.dart @@ -0,0 +1,45 @@ +import 'package:cw_core/transaction_direction.dart'; +import 'package:cw_core/transaction_info.dart'; +import 'package:cw_core/format_amount.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:cw_decred/amount_format.dart'; + +class DecredTransactionInfo extends TransactionInfo { + DecredTransactionInfo({ + required String id, + required int amount, + required int fee, + required TransactionDirection direction, + required bool isPending, + required DateTime date, + required int height, + required int confirmations, + required String to, + }) { + this.id = id; + this.amount = amount; + this.fee = fee; + this.height = height; + this.direction = direction; + this.date = date; + this.isPending = isPending; + this.confirmations = confirmations; + this.to = to; + } + + String? _fiatAmount; + + @override + String amountFormatted() => + '${formatAmount(decredAmountToString(amount: amount))} ${walletTypeToCryptoCurrency(WalletType.decred).title}'; + + @override + String? feeFormatted() => + '${formatAmount(decredAmountToString(amount: fee ?? 0))} ${walletTypeToCryptoCurrency(WalletType.decred).title}'; + + @override + String fiatAmount() => _fiatAmount ?? ''; + + @override + void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount); +} diff --git a/cw_decred/lib/transaction_priority.dart b/cw_decred/lib/transaction_priority.dart new file mode 100644 index 000000000..80a9c7e3a --- /dev/null +++ b/cw_decred/lib/transaction_priority.dart @@ -0,0 +1,69 @@ +import 'package:cw_core/transaction_priority.dart'; + +class DecredTransactionPriority extends TransactionPriority { + const DecredTransactionPriority({required String title, required int raw}) + : super(title: title, raw: raw); + + static const List all = [fast, medium, slow]; + static const DecredTransactionPriority slow = DecredTransactionPriority(title: 'Slow', raw: 0); + static const DecredTransactionPriority medium = + DecredTransactionPriority(title: 'Medium', raw: 1); + static const DecredTransactionPriority fast = DecredTransactionPriority(title: 'Fast', raw: 2); + + static DecredTransactionPriority 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 DecredTransactionPriority deserialize'); + } + } + + String get units => 'atom'; + + @override + String toString() { + var label = ''; + + switch (this) { + case DecredTransactionPriority.slow: + label = 'Slow ~24hrs'; // '${S.current.transaction_priority_slow} ~24hrs'; + break; + case DecredTransactionPriority.medium: + label = 'Medium'; // S.current.transaction_priority_medium; + break; + case DecredTransactionPriority.fast: + label = 'Fast'; // S.current.transaction_priority_fast; + break; + default: + break; + } + + return label; + } + + String labelWithRate(int rate) => '${toString()} ($rate ${units}/byte)'; +} + +class FeeCache { + int _feeRate; + DateTime stamp; + FeeCache(this._feeRate) : this.stamp = DateTime(0, 0, 0, 0, 0, 0, 0, 0); + + bool isOld() { + return this.stamp.add(const Duration(minutes: 30)).isBefore(DateTime.now()); + } + + void update(int feeRate) { + this._feeRate = feeRate; + this.stamp = DateTime.now(); + } + + int feeRate() { + return this._feeRate; + } +} diff --git a/cw_decred/lib/wallet.dart b/cw_decred/lib/wallet.dart new file mode 100644 index 000000000..028ba6cda --- /dev/null +++ b/cw_decred/lib/wallet.dart @@ -0,0 +1,729 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'package:cw_core/exceptions.dart'; +import 'package:cw_core/transaction_direction.dart'; +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_decred/pending_transaction.dart'; +import 'package:cw_decred/transaction_credentials.dart'; +import 'package:flutter/foundation.dart'; +import 'package:mobx/mobx.dart'; +import 'package:hive/hive.dart'; + +import 'package:cw_decred/api/libdcrwallet.dart'; +import 'package:cw_decred/transaction_history.dart'; +import 'package:cw_decred/wallet_addresses.dart'; +import 'package:cw_decred/transaction_priority.dart'; +import 'package:cw_decred/wallet_service.dart'; +import 'package:cw_decred/balance.dart'; +import 'package:cw_decred/transaction_info.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/transaction_priority.dart'; +import 'package:cw_core/pending_transaction.dart'; +import 'package:cw_core/pathForWallet.dart'; +import 'package:cw_core/sync_status.dart'; +import 'package:cw_core/node.dart'; +import 'package:cw_core/unspent_coins_info.dart'; +import 'package:cw_core/unspent_transaction_output.dart'; + +part 'wallet.g.dart'; + +class DecredWallet = DecredWalletBase with _$DecredWallet; + +abstract class DecredWalletBase + extends WalletBase with Store { + DecredWalletBase(WalletInfo walletInfo, String password, Box unspentCoinsInfo, + Libwallet libwallet, Function() closeLibwallet) + : _password = password, + _libwallet = libwallet, + _closeLibwallet = closeLibwallet, + this.syncStatus = NotConnectedSyncStatus(), + this.unspentCoinsInfo = unspentCoinsInfo, + this.watchingOnly = + walletInfo.derivationInfo?.derivationPath == DecredWalletService.pubkeyRestorePath || + walletInfo.derivationInfo?.derivationPath == + DecredWalletService.pubkeyRestorePathTestnet, + this.balance = ObservableMap.of({CryptoCurrency.dcr: DecredBalance.zero()}), + this.isTestnet = walletInfo.derivationInfo?.derivationPath == + DecredWalletService.seedRestorePathTestnet || + walletInfo.derivationInfo?.derivationPath == + DecredWalletService.pubkeyRestorePathTestnet, + super(walletInfo) { + walletAddresses = DecredWalletAddresses(walletInfo, libwallet); + transactionHistory = DecredTransactionHistory(); + + reaction((_) => isEnabledAutoGenerateSubaddress, (bool enabled) { + this.walletAddresses.isEnabledAutoGenerateSubaddress = enabled; + }); + } + + // NOTE: Hitting this max fee would be unexpected with current on chain use + // but this may need to be updated in the future. + final maxFeeRate = 100000; + + // syncIntervalSyncing is used up until synced, then transactions are checked + // every syncIntervalSynced. + final syncIntervalSyncing = 5; // seconds + final syncIntervalSynced = 30; // seconds + static final defaultFeeRate = 10000; + final String _password; + final Libwallet _libwallet; + final Function() _closeLibwallet; + final idPrefix = "decred_"; + + // TODO: Encrypt this. + var _seed = ""; + var _pubkey = ""; + var _unspents = []; + + // synced is used to set the syncTimer interval. + bool synced = false; + bool watchingOnly; + bool connecting = false; + String persistantPeer = "default-spv-nodes"; + FeeCache feeRateFast = FeeCache(defaultFeeRate); + FeeCache feeRateMedium = FeeCache(defaultFeeRate); + FeeCache feeRateSlow = FeeCache(defaultFeeRate); + Timer? syncTimer; + Box unspentCoinsInfo; + + @override + @observable + bool isEnabledAutoGenerateSubaddress = true; + + @override + @observable + SyncStatus syncStatus; + + @override + @observable + late ObservableMap balance; + + @override + late DecredWalletAddresses walletAddresses; + + @override + String? get seed { + if (watchingOnly) { + return null; + } + return _seed; + } + + @override + Object get keys => {}; + + @override + bool isTestnet; + + String get pubkey { + return _pubkey; + } + + Future init() async { + final getSeed = () async { + if (!watchingOnly) { + _seed = await _libwallet.walletSeed(walletInfo.name, _password) ?? ""; + } + _pubkey = await _libwallet.defaultPubkey(walletInfo.name); + }; + await Future.wait([ + updateBalance(), + updateTransactionHistory(), + walletAddresses.init(), + fetchTransactions(), + updateFees(), + fetchUnspents(), + getSeed(), + ]); + } + + Future performBackgroundTasks() async { + if (!await checkSync()) { + if (synced == true) { + synced = false; + if (syncTimer != null) { + syncTimer!.cancel(); + } + syncTimer = Timer.periodic( + Duration(seconds: syncIntervalSyncing), (Timer t) => performBackgroundTasks()); + } + return; + } + // Set sync check interval lower since we are synced. + if (synced == false) { + synced = true; + if (syncTimer != null) { + syncTimer!.cancel(); + } + syncTimer = Timer.periodic( + Duration(seconds: syncIntervalSynced), (Timer t) => performBackgroundTasks()); + } + await Future.wait([ + updateTransactionHistory(), + updateFees(), + fetchUnspents(), + updateBalance(), + walletAddresses.updateAddressesInBox(), + ]); + } + + Future updateFees() async { + final feeForNb = (int nb) async { + try { + final feeStr = await _libwallet.estimateFee(walletInfo.name, nb); + var fee = int.parse(feeStr); + if (fee > maxFeeRate) { + throw "dcr fee returned from estimate fee was over max"; + } else if (fee <= 0) { + throw "dcr fee returned from estimate fee was zero"; + } + return fee; + } catch (e) { + printV(e); + return defaultFeeRate; + } + }; + if (feeRateSlow.isOld()) { + feeRateSlow.update(await feeForNb(4)); + } + if (feeRateMedium.isOld()) { + feeRateMedium.update(await feeForNb(2)); + } + if (feeRateFast.isOld()) { + feeRateFast.update(await feeForNb(1)); + } + } + + Future updateTransactionHistory() async { + // from is the number of transactions skipped from most recent, not block + // height. + var from = 0; + while (true) { + // Transactions are returned from newest to oldest. Loop fetching 5 txn + // at a time until we find a batch with txn that no longer need to be + // updated. + final txs = await this.fetchFiveTransactions(from); + if (txs.length == 0) { + return; + } + if (this.transactionHistory.update(txs)) { + return; + } + from += 5; + } + } + + Future checkSync() async { + final syncStatusJSON = await _libwallet.syncStatus(walletInfo.name); + final decoded = json.decode(syncStatusJSON); + + final syncStatusCode = decoded["syncstatuscode"] ?? 0; + // final syncStatusStr = decoded["syncstatus"] ?? ""; + final targetHeight = decoded["targetheight"] ?? 1; + final numPeers = decoded["numpeers"] ?? 0; + // final cFiltersHeight = decoded["cfiltersheight"] ?? 0; + final headersHeight = decoded["headersheight"] ?? 0; + final rescanHeight = decoded["rescanheight"] ?? 0; + + if (numPeers == 0) { + syncStatus = NotConnectedSyncStatus(); + return false; + } + + // Sync codes: + // NotStarted = 0 + // FetchingCFilters = 1 + // FetchingHeaders = 2 + // DiscoveringAddrs = 3 + // Rescanning = 4 + // Complete = 5 + + if (syncStatusCode > 4) { + syncStatus = SyncedSyncStatus(); + return true; + } + + if (syncStatusCode == 0) { + syncStatus = ConnectedSyncStatus(); + return false; + } + + if (syncStatusCode == 1) { + syncStatus = SyncingSyncStatus(targetHeight, 0.0); + return false; + } + + if (syncStatusCode == 2) { + final headersProg = headersHeight / targetHeight; + // Only allow headers progress to go up half way. + syncStatus = SyncingSyncStatus(targetHeight - headersHeight, headersProg); + return false; + } + + // TODO: This step takes a while so should really get more info to the UI + // that we are discovering addresses. + if (syncStatusCode == 3) { + // Hover at half. + syncStatus = ProcessingSyncStatus(); + return false; + } + + if (syncStatusCode == 4) { + // Start at 75%. + final rescanProg = rescanHeight / targetHeight / 4; + syncStatus = SyncingSyncStatus(targetHeight - rescanHeight, .75 + rescanProg); + return false; + } + return false; + } + + @action + @override + Future connectToNode({required Node node}) async { + if (connecting) { + return; + } + connecting = true; + String addr = "default-spv-nodes"; + if (node.uri.host != addr) { + addr = node.uri.host; + if (node.uri.port != "") { + addr += ":" + node.uri.port.toString(); + } + } + if (addr != persistantPeer) { + if (syncTimer != null) { + syncTimer!.cancel(); + syncTimer = null; + } + persistantPeer = addr; + await _libwallet.closeWallet(walletInfo.name); + final network = isTestnet ? "testnet" : "mainnet"; + final config = { + "name": walletInfo.name, + "datadir": walletInfo.dirPath, + "net": network, + "unsyncedaddrs": true, + }; + await _libwallet.loadWallet(jsonEncode(config)); + } + await this._startSync(); + connecting = false; + } + + @action + @override + Future startSync() async { + if (connecting) { + return; + } + connecting = true; + await this._startSync(); + connecting = false; + } + + Future _startSync() async { + if (syncTimer != null) { + return; + } + try { + syncStatus = ConnectingSyncStatus(); + await _libwallet.startSync( + walletInfo.name, + persistantPeer == "default-spv-nodes" ? "" : persistantPeer, + ); + syncTimer = Timer.periodic( + Duration(seconds: syncIntervalSyncing), (Timer t) => performBackgroundTasks()); + } catch (e) { + printV(e.toString()); + syncStatus = FailedSyncStatus(); + } + } + + @override + Future createTransaction(Object credentials) async { + if (watchingOnly) { + return DecredPendingTransaction( + txid: "", + amount: 0, + fee: 0, + rawHex: "", + send: () async { + throw "unable to send with watching only wallet"; + }); + } + var totalIn = 0; + final ignoreInputs = []; + this.unspentCoinsInfo.values.forEach((unspent) { + if (unspent.isFrozen || !unspent.isSending) { + final input = {"txid": unspent.hash, "vout": unspent.vout}; + ignoreInputs.add(input); + return; + } + totalIn += unspent.value; + }); + + final creds = credentials as DecredTransactionCredentials; + var totalAmt = 0; + var sendAll = false; + final outputs = []; + for (final out in creds.outputs) { + var amt = 0; + if (out.sendAll) { + if (creds.outputs.length != 1) { + throw "can only send all to one output"; + } + sendAll = true; + totalAmt = totalIn; + } else if (out.cryptoAmount != null) { + final coins = double.parse(out.cryptoAmount!); + amt = (coins * 1e8).toInt(); + } + totalAmt += amt; + final o = { + "address": out.isParsedAddress ? out.extractedAddress! : out.address, + "amount": amt + }; + outputs.add(o); + } + + // throw exception if no selected coins under coin control + // or if the total coins selected, is less than the amount the user wants to spend + if (ignoreInputs.length == unspentCoinsInfo.values.length || totalIn < totalAmt) { + throw TransactionNoInputsException(); + } + + // The inputs are always used. Currently we don't have use for this + // argument. sendall ingores output value and sends everything. + final signReq = { + // "inputs": inputs, + "ignoreInputs": ignoreInputs, + "outputs": outputs, + "feerate": creds.feeRate ?? defaultFeeRate, + "password": _password, + "sendall": sendAll, + }; + final res = await _libwallet.createSignedTransaction(walletInfo.name, jsonEncode(signReq)); + final decoded = json.decode(res); + final signedHex = decoded["signedhex"]; + final send = () async { + await _libwallet.sendRawTransaction(walletInfo.name, signedHex); + await updateBalance(); + }; + final fee = decoded["fee"] ?? 0; + if (sendAll) { + totalAmt = (totalAmt - fee).toInt(); + } + return DecredPendingTransaction( + txid: decoded["txid"] ?? "", amount: totalAmt, fee: fee, rawHex: signedHex, send: send); + } + + int feeRate(TransactionPriority priority) { + if (!(priority is DecredTransactionPriority)) { + return defaultFeeRate; + } + final p = priority; + switch (p) { + case DecredTransactionPriority.slow: + return feeRateSlow.feeRate(); + case DecredTransactionPriority.medium: + return feeRateMedium.feeRate(); + case DecredTransactionPriority.fast: + return feeRateFast.feeRate(); + } + return defaultFeeRate; + } + + @override + int calculateEstimatedFee(TransactionPriority priority, int? amount) { + if (priority is DecredTransactionPriority) { + final P2PKHOutputSize = + 36; // 8 bytes value + 2 bytes version + at least 1 byte varint script size + P2PKHPkScriptSize + // MsgTxOverhead is 4 bytes version (lower 2 bytes for the real transaction + // version and upper 2 bytes for the serialization type) + 4 bytes locktime + // + 4 bytes expiry + 3 bytes of varints for the number of transaction + // inputs (x2 for witness and prefix) and outputs + final MsgTxOverhead = 15; + // TxInOverhead is the overhead for a wire.TxIn with a scriptSig length < + // 254. prefix (41 bytes) + ValueIn (8 bytes) + BlockHeight (4 bytes) + + // BlockIndex (4 bytes) + sig script var int (at least 1 byte) + final TxInOverhead = 57; + final P2PKHInputSize = + TxInOverhead + 109; // TxInOverhead (57) + var int (1) + P2PKHSigScriptSize (108) + + int inputsCount = 1; + if (amount != null) { + inputsCount += _unspents.where((e) { + amount = (amount!) - e.value; + return (amount!) > 0; + }).length; + } + + // Estimate using a transaction consuming inoutsCount and paying to one address with change. + return (this.feeRate(priority) / 1000).round() * + (MsgTxOverhead + P2PKHInputSize * inputsCount + P2PKHOutputSize * 2); + } + return 0; + } + + @override + Future> fetchTransactions() async { + return this.fetchFiveTransactions(0); + } + + Future> fetchFiveTransactions(int from) async { + final res = await _libwallet.listTransactions(walletInfo.name, from.toString(), "5"); + final decoded = json.decode(res); + var txs = {}; + for (final d in decoded) { + final txid = uniqueTxID(d["txid"] ?? "", d["vout"] ?? 0); + var direction = TransactionDirection.outgoing; + if (d["category"] == "receive") { + direction = TransactionDirection.incoming; + } + final amountDouble = d["amount"] ?? 0.0; + final amount = (amountDouble * 1e8).toInt().abs(); + final feeDouble = d["fee"] ?? 0.0; + final fee = (feeDouble * 1e8).toInt().abs(); + final confs = d["confirmations"] ?? 0; + final sendTime = d["time"] ?? 0; + final height = d["height"] ?? 0; + final txInfo = DecredTransactionInfo( + id: txid, + amount: amount, + fee: fee, + direction: direction, + isPending: confs == 0, + date: DateTime.fromMillisecondsSinceEpoch(sendTime * 1000, isUtc: false), + height: height, + confirmations: confs, + to: d["address"] ?? "", + ); + txs[txid] = txInfo; + } + return txs; + } + + // uniqueTxID combines the tx id and vout to create a unique id. + String uniqueTxID(String id, int vout) { + return id + ":" + vout.toString(); + } + + @override + Future save() async {} + + @override + bool get hasRescan => walletBirthdayBlockHeight() != -1; + + @override + Future rescan({required int height}) async { + // The required height is not used. A birthday time is recorded in the + // mnemonic. As long as not private data is imported into the wallet, we + // can always rescan from there. + var rescanHeight = 0; + if (!watchingOnly) { + rescanHeight = await walletBirthdayBlockHeight(); + // Sync has not yet reached the birthday block. + if (rescanHeight == -1) { + return; + } + } + await _libwallet.rescanFromHeight(walletInfo.name, rescanHeight.toString()); + } + + @override + Future close({bool shouldCleanup = false}) async { + if (syncTimer != null) { + syncTimer!.cancel(); + syncTimer = null; + } + await _libwallet.closeWallet(walletInfo.name); + if (shouldCleanup) { + await _libwallet.shutdown(); + _closeLibwallet(); + } + } + + @override + Future changePassword(String password) async { + if (watchingOnly) { + return; + } + return () async { + await _libwallet.changeWalletPassword(walletInfo.name, _password, password); + }(); + } + + @override + Future updateBalance() async { + final balanceMap = await _libwallet.balance(walletInfo.name); + + var totalFrozen = 0; + + unspentCoinsInfo.values.forEach((info) { + _unspents.forEach((element) { + if (element.hash == info.hash && + element.vout == info.vout && + info.isFrozen && + element.value == info.value) { + totalFrozen += element.value; + } + }); + }); + + balance[CryptoCurrency.dcr] = DecredBalance( + confirmed: balanceMap["confirmed"] ?? 0, + unconfirmed: balanceMap["unconfirmed"] ?? 0, + frozen: totalFrozen, + ); + } + + @override + void setExceptionHandler(void Function(FlutterErrorDetails) onError) => onError; + + Future renameWalletFiles(String newWalletName) async { + final currentDirPath = await pathForWalletDir(name: walletInfo.name, type: type); + + final newDirPath = await pathForWalletDir(name: newWalletName, type: type); + + if (File(newDirPath).existsSync()) { + throw "wallet already exists at $newDirPath"; + } + + await Directory(currentDirPath).rename(newDirPath); + } + + @override + Future signMessage(String message, {String? address = null}) async { + if (watchingOnly) { + throw "a watching only wallet cannot sign"; + } + var addr = address; + if (addr == null) { + addr = walletAddresses.address; + } + if (addr == "") { + throw "unable to get an address from unsynced wallet"; + } + return await _libwallet.signMessage(walletInfo.name, message, addr, _password); + } + + Future fetchUnspents() async { + final res = await _libwallet.listUnspents(walletInfo.name); + final decoded = json.decode(res); + var unspents = []; + for (final d in decoded) { + final spendable = d["spendable"] ?? false; + if (!spendable) { + continue; + } + final amountDouble = d["amount"] ?? 0.0; + final amount = (amountDouble * 1e8).toInt().abs(); + final utxo = Unspent(d["address"] ?? "", d["txid"] ?? "", amount, d["vout"] ?? 0, null); + utxo.isChange = d["ischange"] ?? false; + unspents.add(utxo); + } + _unspents = unspents; + } + + List unspents() { + this.updateUnspents(_unspents); + return _unspents; + } + + void updateUnspents(List unspentCoins) { + if (this.unspentCoinsInfo.isEmpty) { + unspentCoins.forEach((coin) => this.addCoinInfo(coin)); + return; + } + + if (unspentCoins.isEmpty) { + this.unspentCoinsInfo.clear(); + return; + } + + final walletID = idPrefix + walletInfo.name; + if (unspentCoins.isNotEmpty) { + unspentCoins.forEach((coin) { + final coinInfoList = this.unspentCoinsInfo.values.where((element) => + element.walletId == walletID && element.hash == coin.hash && element.vout == coin.vout); + + if (coinInfoList.isEmpty) { + this.addCoinInfo(coin); + } else { + final coinInfo = coinInfoList.first; + + coin.isFrozen = coinInfo.isFrozen; + coin.isSending = coinInfo.isSending; + coin.note = coinInfo.note; + } + }); + } + + final List keys = []; + this.unspentCoinsInfo.values.forEach((element) { + final existUnspentCoins = unspentCoins.where((coin) => element.hash.contains(coin.hash)); + + if (existUnspentCoins.isEmpty) { + keys.add(element.key); + } + }); + + if (keys.isNotEmpty) { + unspentCoinsInfo.deleteAll(keys); + } + } + + void addCoinInfo(Unspent coin) { + final newInfo = UnspentCoinsInfo( + walletId: idPrefix + walletInfo.name, + hash: coin.hash, + isFrozen: false, + isSending: coin.isSending, + noteRaw: "", + address: coin.address, + value: coin.value, + vout: coin.vout, + isChange: coin.isChange, + keyImage: coin.keyImage, + ); + + unspentCoinsInfo.add(newInfo); + } + + // walletBirthdayBlockHeight checks if the wallet birthday is set and returns + // it. Returns -1 if not. + Future walletBirthdayBlockHeight() async { + final res = await _libwallet.birthState(walletInfo.name); + final decoded = json.decode(res); + // Having these values set indicates that sync has not reached the birthday + // yet, so no birthday is set. + if (decoded["setfromheight"] == true || decoded["setfromtime"] == true) { + return -1; + } + return decoded["height"] ?? 0; + } + + Future verifyMessage(String message, String signature, {String? address = null}) async { + var addr = address; + if (addr == null) { + throw "an address is required to verify message"; + } + return () async { + final verified = await _libwallet.verifyMessage(walletInfo.name, message, addr, signature); + if (verified == "true") { + return true; + } + return false; + }(); + } + + @override + String get password => _password; + + @override + bool canSend() => seed != null; +} diff --git a/cw_decred/lib/wallet_addresses.dart b/cw_decred/lib/wallet_addresses.dart new file mode 100644 index 000000000..10970b2d6 --- /dev/null +++ b/cw_decred/lib/wallet_addresses.dart @@ -0,0 +1,137 @@ +import 'dart:convert'; +import 'package:mobx/mobx.dart'; + +import 'package:cw_core/address_info.dart'; +import 'package:cw_core/wallet_addresses.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_decred/api/libdcrwallet.dart'; + +part 'wallet_addresses.g.dart'; + +class DecredWalletAddresses = DecredWalletAddressesBase with _$DecredWalletAddresses; + +abstract class DecredWalletAddressesBase extends WalletAddresses with Store { + DecredWalletAddressesBase(WalletInfo walletInfo, Libwallet libwallet) + : _libwallet = libwallet, + super(walletInfo); + final Libwallet _libwallet; + String currentAddr = ''; + + @observable + bool isEnabledAutoGenerateSubaddress = true; + + @observable + String selectedAddr = ''; + + @override + @computed + String get address { + return selectedAddr; + } + + @override + set address(value) { + selectedAddr = value; + } + + @override + Future init() async { + if (walletInfo.addresses != null) { + addressesMap = walletInfo.addresses!; + } + if (walletInfo.addressInfos != null) { + addressInfos = walletInfo.addressInfos!; + } + if (walletInfo.usedAddresses != null) { + usedAddresses = {...walletInfo.usedAddresses!}; + } + await updateAddressesInBox(); + } + + @override + Future updateAddressesInBox() async { + final addrs = await libAddresses(); + final allAddrs = new List.from(addrs.usedAddrs)..addAll(addrs.unusedAddrs); + + // Add all addresses. + allAddrs.forEach((addr) { + if (addressesMap.containsKey(addr)) { + return; + } + addressesMap[addr] = ""; + addressInfos[0] ??= []; + addressInfos[0]?.add(AddressInfo(address: addr, label: "", accountIndex: 0)); + }); + + // Add used addresses. + addrs.usedAddrs.forEach((addr) { + if (!usedAddresses.contains(addr)) { + usedAddresses.add(addr); + } + }); + + if (addrs.unusedAddrs.length > 0 && addrs.unusedAddrs[0] != currentAddr) { + currentAddr = addrs.unusedAddrs[0]; + selectedAddr = currentAddr; + } + + await saveAddressesInBox(); + } + + List getAddressInfos() { + if (addressInfos.containsKey(0)) { + return addressInfos[0]!; + } + return []; + } + + Future updateAddress(String address, String label) async { + if (!addressInfos.containsKey(0)) { + return; + } + addressInfos[0]!.forEach((info) { + if (info.address == address) { + info.label = label; + } + }); + await saveAddressesInBox(); + } + + Future libAddresses() async { + final nUsed = "10"; + var nUnused = "1"; + if (this.isEnabledAutoGenerateSubaddress) { + nUnused = "3"; + } + final res = await _libwallet.addresses(walletInfo.name, nUsed, nUnused); + final decoded = json.decode(res); + final usedAddrs = List.from(decoded["used"] ?? []); + final unusedAddrs = List.from(decoded["unused"] ?? []); + // index is the index of the first unused address. + final index = decoded["index"] ?? 0; + return new LibAddresses(usedAddrs, unusedAddrs, index); + } + + Future generateNewAddress(String label) async { + // NOTE: This will ignore the gap limit and may cause problems when restoring from seed if too + // many addresses are taken and not used. + final addr = await _libwallet.newExternalAddress(walletInfo.name) ?? ''; + if (addr == "") { + return; + } + if (!addressesMap.containsKey(addr)) { + addressesMap[addr] = ""; + addressInfos[0] ??= []; + addressInfos[0]?.add(AddressInfo(address: addr, label: label, accountIndex: 0)); + } + selectedAddr = addr; + await saveAddressesInBox(); + } +} + +class LibAddresses { + final List usedAddrs, unusedAddrs; + final int firstUnusedAddrIndex; + + LibAddresses(this.usedAddrs, this.unusedAddrs, this.firstUnusedAddrIndex); +} diff --git a/cw_decred/lib/wallet_creation_credentials.dart b/cw_decred/lib/wallet_creation_credentials.dart new file mode 100644 index 000000000..ca0451447 --- /dev/null +++ b/cw_decred/lib/wallet_creation_credentials.dart @@ -0,0 +1,40 @@ +import 'package:cw_core/wallet_credentials.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/hardware/hardware_account_data.dart'; + +class DecredNewWalletCredentials extends WalletCredentials { + DecredNewWalletCredentials({required String name, WalletInfo? walletInfo}) + : super(name: name, walletInfo: walletInfo); +} + +class DecredRestoreWalletFromSeedCredentials extends WalletCredentials { + DecredRestoreWalletFromSeedCredentials( + {required String name, + required String password, + required this.mnemonic, + WalletInfo? walletInfo}) + : super(name: name, password: password, walletInfo: walletInfo); + + final String mnemonic; +} + +class DecredRestoreWalletFromPubkeyCredentials extends WalletCredentials { + DecredRestoreWalletFromPubkeyCredentials( + {required String name, + required String password, + required String this.pubkey, + WalletInfo? walletInfo}) + : super(name: name, password: password, walletInfo: walletInfo); + + final String pubkey; +} + +class DecredRestoreWalletFromHardwareCredentials extends WalletCredentials { + DecredRestoreWalletFromHardwareCredentials( + {required String name, required this.hwAccountData, WalletInfo? walletInfo}) + : t = throw UnimplementedError(), + super(name: name, walletInfo: walletInfo); + + final HardwareAccountData hwAccountData; + final void t; +} diff --git a/cw_decred/lib/wallet_service.dart b/cw_decred/lib/wallet_service.dart new file mode 100644 index 000000000..a54833321 --- /dev/null +++ b/cw_decred/lib/wallet_service.dart @@ -0,0 +1,186 @@ +import 'dart:convert'; +import 'dart:io'; +import 'package:cw_decred/api/libdcrwallet.dart'; +import 'package:cw_decred/wallet_creation_credentials.dart'; +import 'package:cw_decred/wallet.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_service.dart'; +import 'package:cw_core/pathForWallet.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:hive/hive.dart'; +import 'package:collection/collection.dart'; +import 'package:cw_core/unspent_coins_info.dart'; + +class DecredWalletService extends WalletService< + DecredNewWalletCredentials, + DecredRestoreWalletFromSeedCredentials, + DecredRestoreWalletFromPubkeyCredentials, + DecredRestoreWalletFromHardwareCredentials> { + DecredWalletService(this.walletInfoSource, this.unspentCoinsInfoSource); + + final Box walletInfoSource; + final Box unspentCoinsInfoSource; + final seedRestorePath = "m/44'/42'"; + static final seedRestorePathTestnet = "m/44'/1'"; + static final pubkeyRestorePath = "m/44'/42'/0'"; + static final pubkeyRestorePathTestnet = "m/44'/1'/0'"; + final mainnet = "mainnet"; + final testnet = "testnet"; + Libwallet? libwallet; + + Future init() async { + if (libwallet != null) { + return; + } + libwallet = await Libwallet.spawn(); + // Use the general path for all dcr wallets as the general log directory. + // Individual wallet paths may be removed if the wallet is deleted. + final dcrLogDir = await pathForWalletDir(name: '', type: WalletType.decred); + libwallet!.initLibdcrwallet(dcrLogDir); + } + + void closeLibwallet() { + if (libwallet == null) { + return; + } + libwallet!.close(); + libwallet = null; + } + + @override + WalletType getType() => WalletType.decred; + + @override + Future isWalletExit(String name) async => + File(await pathForWallet(name: name, type: getType())).existsSync(); + + @override + Future create(DecredNewWalletCredentials credentials, {bool? isTestnet}) async { + await this.init(); + final config = { + "name": credentials.walletInfo!.name, + "datadir": credentials.walletInfo!.dirPath, + "pass": credentials.password!, + "net": isTestnet == true ? testnet : mainnet, + "unsyncedaddrs": true, + }; + await libwallet!.createWallet(jsonEncode(config)); + final di = DerivationInfo( + derivationPath: isTestnet == true ? seedRestorePathTestnet : seedRestorePath); + credentials.walletInfo!.derivationInfo = di; + final wallet = DecredWallet(credentials.walletInfo!, credentials.password!, + this.unspentCoinsInfoSource, libwallet!, closeLibwallet); + 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 network = walletInfo.derivationInfo?.derivationPath == seedRestorePathTestnet || + walletInfo.derivationInfo?.derivationPath == pubkeyRestorePathTestnet + ? testnet + : mainnet; + + await this.init(); + final walletDirExists = Directory(walletInfo.dirPath).existsSync(); + if (!walletDirExists) { + walletInfo.dirPath = await pathForWalletDir(name: name, type: getType()); + } + + final config = { + "name": walletInfo.name, + "datadir": walletInfo.dirPath, + "net": network, + "unsyncedaddrs": true, + }; + await libwallet!.loadWallet(jsonEncode(config)); + final wallet = + DecredWallet(walletInfo, password, this.unspentCoinsInfoSource, libwallet!, closeLibwallet); + 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 network = currentWalletInfo.derivationInfo?.derivationPath == seedRestorePathTestnet || + currentWalletInfo.derivationInfo?.derivationPath == pubkeyRestorePathTestnet + ? testnet + : mainnet; + final currentWallet = DecredWallet( + currentWalletInfo, password, this.unspentCoinsInfoSource, libwallet!, closeLibwallet); + + await currentWallet.renameWalletFiles(newName); + + final newDirPath = await pathForWalletDir(name: newName, type: getType()); + final newWalletInfo = currentWalletInfo; + newWalletInfo.id = WalletBase.idFor(newName, getType()); + newWalletInfo.name = newName; + newWalletInfo.dirPath = newDirPath; + newWalletInfo.network = network; + + await walletInfoSource.put(currentWalletInfo.key, newWalletInfo); + } + + @override + Future restoreFromSeed(DecredRestoreWalletFromSeedCredentials credentials, + {bool? isTestnet}) async { + await this.init(); + final config = { + "name": credentials.walletInfo!.name, + "datadir": credentials.walletInfo!.dirPath, + "pass": credentials.password!, + "mnemonic": credentials.mnemonic, + "net": isTestnet == true ? testnet : mainnet, + "unsyncedaddrs": true, + }; + await libwallet!.createWallet(jsonEncode(config)); + final di = DerivationInfo( + derivationPath: isTestnet == true ? seedRestorePathTestnet : seedRestorePath); + credentials.walletInfo!.derivationInfo = di; + final wallet = DecredWallet(credentials.walletInfo!, credentials.password!, + this.unspentCoinsInfoSource, libwallet!, closeLibwallet); + await wallet.init(); + return wallet; + } + + // restoreFromKeys only supports restoring a watch only wallet from an account + // pubkey. + @override + Future restoreFromKeys(DecredRestoreWalletFromPubkeyCredentials credentials, + {bool? isTestnet}) async { + await this.init(); + final config = { + "name": credentials.walletInfo!.name, + "datadir": credentials.walletInfo!.dirPath, + "pubkey": credentials.pubkey, + "net": isTestnet == true ? testnet : mainnet, + "unsyncedaddrs": true, + }; + await libwallet!.createWatchOnlyWallet(jsonEncode(config)); + final di = DerivationInfo( + derivationPath: isTestnet == true ? pubkeyRestorePathTestnet : pubkeyRestorePath); + credentials.walletInfo!.derivationInfo = di; + final wallet = DecredWallet(credentials.walletInfo!, credentials.password!, + this.unspentCoinsInfoSource, libwallet!, closeLibwallet); + await wallet.init(); + return wallet; + } + + @override + Future restoreFromHardwareWallet( + DecredRestoreWalletFromHardwareCredentials credentials) async => + throw UnimplementedError(); +} diff --git a/cw_decred/macos/Classes/CwDecredPlugin.swift b/cw_decred/macos/Classes/CwDecredPlugin.swift new file mode 100644 index 000000000..72dae36f4 --- /dev/null +++ b/cw_decred/macos/Classes/CwDecredPlugin.swift @@ -0,0 +1,19 @@ +import Cocoa +import FlutterMacOS + +public class CwDecredPlugin: NSObject, FlutterPlugin { + public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel(name: "cw_decred", binaryMessenger: registrar.messenger) + let instance = CwDecredPlugin() + registrar.addMethodCallDelegate(instance, channel: channel) + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + switch call.method { + case "getPlatformVersion": + result("macOS " + ProcessInfo.processInfo.operatingSystemVersionString) + default: + result(FlutterMethodNotImplemented) + } + } +} diff --git a/cw_decred/macos/cw_decred.podspec b/cw_decred/macos/cw_decred.podspec new file mode 100644 index 000000000..87d82f238 --- /dev/null +++ b/cw_decred/macos/cw_decred.podspec @@ -0,0 +1,22 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint cw_decred.podspec` to validate before publishing. +# +Pod::Spec.new do |s| + s.name = 'cw_decred' + s.version = '0.0.1' + s.summary = 'Cake Wallet Decred' + s.description = 'Cake Wallet wrapper over Decred project' + s.homepage = 'http://cakewallet.com' + s.license = { :file => '../LICENSE' } + s.author = { 'Cake Wallet' => 'support@cakewallet.com' } + + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.dependency 'FlutterMacOS' + + s.platform = :osx, '10.11' + s.vendored_libraries = 'External/lib/libdcrwallet.a' + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', "OTHER_LDFLAGS" => "-force_load $(PODS_TARGET_SRCROOT)/External/lib/libdcrwallet.a -lstdc++" } + s.swift_version = '5.0' +end diff --git a/cw_decred/pubspec.lock b/cw_decred/pubspec.lock new file mode 100644 index 000000000..e3d39372b --- /dev/null +++ b/cw_decred/pubspec.lock @@ -0,0 +1,852 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834 + url: "https://pub.dev" + source: hosted + version: "72.0.0" + _macros: + dependency: transitive + description: dart + source: sdk + version: "0.3.2" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139 + url: "https://pub.dev" + source: hosted + version: "6.7.0" + args: + dependency: transitive + description: + name: args + sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6 + url: "https://pub.dev" + source: hosted + version: "2.6.0" + asn1lib: + dependency: transitive + description: + name: asn1lib + sha256: "4bae5ae63e6d6dd17c4aac8086f3dec26c0236f6a0f03416c6c19d830c367cf5" + url: "https://pub.dev" + source: hosted + version: "1.5.8" + async: + dependency: transitive + description: + name: async + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" + source: hosted + version: "2.11.0" + blockchain_utils: + dependency: transitive + description: + path: "." + ref: cake-update-v2 + resolved-ref: "59fdf29d72068e0522a96a8953ed7272833a9f57" + url: "https://github.com/cake-tech/blockchain_utils" + source: git + version: "3.3.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + build: + dependency: transitive + description: + name: build + sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + build_config: + dependency: transitive + description: + name: build_config + sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 + url: "https://pub.dev" + source: hosted + version: "1.1.1" + build_daemon: + dependency: transitive + description: + name: build_daemon + sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" + url: "https://pub.dev" + source: hosted + version: "4.0.2" + build_resolvers: + dependency: "direct dev" + description: + name: build_resolvers + sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + build_runner: + dependency: "direct dev" + description: + name: build_runner + sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d" + url: "https://pub.dev" + source: hosted + version: "2.4.13" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0 + url: "https://pub.dev" + source: hosted + version: "7.3.2" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: ea90e81dc4a25a043d9bee692d20ed6d1c4a1662a28c03a96417446c093ed6b4 + url: "https://pub.dev" + source: hosted + version: "8.9.5" + cake_backup: + dependency: transitive + description: + path: "." + ref: main + resolved-ref: "3aba867dcab6737f6707782f5db15d71f303db38" + url: "https://github.com/cake-tech/cake_backup.git" + source: git + version: "1.0.0+1" + characters: + dependency: transitive + description: + name: characters + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.dev" + source: hosted + version: "1.3.0" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff + url: "https://pub.dev" + source: hosted + version: "2.0.3" + cli_util: + dependency: transitive + description: + name: cli_util + sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c + url: "https://pub.dev" + source: hosted + version: "0.4.2" + clock: + dependency: transitive + description: + name: clock + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" + source: hosted + version: "1.1.1" + code_builder: + dependency: transitive + description: + name: code_builder + sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e" + url: "https://pub.dev" + source: hosted + version: "4.10.1" + collection: + dependency: transitive + description: + name: collection + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + url: "https://pub.dev" + source: hosted + version: "1.18.0" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + crypto: + dependency: transitive + description: + name: crypto + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" + url: "https://pub.dev" + source: hosted + version: "3.0.6" + cryptography: + dependency: transitive + description: + name: cryptography + sha256: d146b76d33d94548cf035233fbc2f4338c1242fa119013bead807d033fc4ae05 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + cupertino_icons: + dependency: transitive + description: + name: cupertino_icons + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 + url: "https://pub.dev" + source: hosted + version: "1.0.8" + cw_core: + dependency: "direct main" + description: + path: "../cw_core" + relative: true + source: path + version: "0.0.1" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: "7856d364b589d1f08986e140938578ed36ed948581fbc3bc9aef1805039ac5ab" + url: "https://pub.dev" + source: hosted + version: "2.3.7" + decimal: + dependency: transitive + description: + name: decimal + sha256: "24a261d5d5c87e86c7651c417a5dbdf8bcd7080dd592533910e8d0505a279f21" + url: "https://pub.dev" + source: hosted + version: "2.3.3" + encrypt: + dependency: transitive + description: + name: encrypt + sha256: "62d9aa4670cc2a8798bab89b39fc71b6dfbacf615de6cf5001fb39f7e4a996a2" + url: "https://pub.dev" + source: hosted + version: "5.0.3" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" + source: hosted + version: "1.3.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" + url: "https://pub.dev" + source: hosted + version: "2.1.3" + ffigen: + dependency: "direct dev" + description: + name: ffigen + sha256: "2119b4fe3aad0db94dc9531b90283c4640a6231070e613c400b426a4da08c704" + url: "https://pub.dev" + source: hosted + version: "16.1.0" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_mobx: + dependency: transitive + description: + name: flutter_mobx + sha256: ba5e93467866a2991259dc51cffd41ef45f695c667c2b8e7b087bf24118b50fe + url: "https://pub.dev" + source: hosted + version: "2.3.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.dev" + source: hosted + version: "4.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev" + source: hosted + version: "2.1.3" + graphs: + dependency: transitive + description: + name: graphs + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + hive: + dependency: transitive + description: + name: hive + sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941" + url: "https://pub.dev" + source: hosted + version: "2.2.3" + hive_generator: + dependency: "direct dev" + description: + name: hive_generator + sha256: "06cb8f58ace74de61f63500564931f9505368f45f98958bd7a6c35ba24159db4" + url: "https://pub.dev" + source: hosted + version: "2.0.1" + http: + dependency: transitive + description: + name: http + sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f + url: "https://pub.dev" + source: hosted + version: "1.3.0" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 + url: "https://pub.dev" + source: hosted + version: "3.2.2" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" + source: hosted + version: "4.0.2" + intl: + dependency: transitive + description: + name: intl + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + url: "https://pub.dev" + source: hosted + version: "0.19.0" + io: + dependency: transitive + description: + name: io + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b + url: "https://pub.dev" + source: hosted + version: "1.0.5" + js: + dependency: transitive + description: + name: js + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" + source: hosted + version: "0.6.7" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + url: "https://pub.dev" + source: hosted + version: "4.9.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" + url: "https://pub.dev" + source: hosted + version: "10.0.5" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" + url: "https://pub.dev" + source: hosted + version: "3.0.5" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + macros: + dependency: transitive + description: + name: macros + sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536" + url: "https://pub.dev" + source: hosted + version: "0.1.2-main.4" + matcher: + dependency: transitive + description: + name: matcher + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + url: "https://pub.dev" + source: hosted + version: "0.12.16+1" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + url: "https://pub.dev" + source: hosted + version: "1.15.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + mobx: + dependency: transitive + description: + name: mobx + sha256: bf1a90e5bcfd2851fc6984e20eef69557c65d9e4d0a88f5be4cf72c9819ce6b0 + url: "https://pub.dev" + source: hosted + version: "2.5.0" + mobx_codegen: + dependency: "direct dev" + description: + name: mobx_codegen + sha256: "990da80722f7d7c0017dec92040b31545d625b15d40204c36a1e63d167c73cdc" + url: "https://pub.dev" + source: hosted + version: "2.7.0" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + on_chain: + dependency: transitive + description: + path: "." + ref: cake-update-v2 + resolved-ref: "93440dc5126369b873ca1fccc13c3c1240b1c5c2" + url: "https://github.com/cake-tech/on_chain.git" + source: git + version: "3.7.0" + package_config: + dependency: transitive + description: + name: package_config + sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + path: + dependency: transitive + description: + name: path + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + url: "https://pub.dev" + source: hosted + version: "1.9.0" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2" + url: "https://pub.dev" + source: hosted + version: "2.2.15" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + pointycastle: + dependency: transitive + description: + name: pointycastle + sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe" + url: "https://pub.dev" + source: hosted + version: "3.9.1" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" + source: hosted + version: "1.5.1" + provider: + dependency: transitive + description: + name: provider + sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c + url: "https://pub.dev" + source: hosted + version: "6.1.2" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: "81876843eb50dc2e1e5b151792c9a985c5ed2536914115ed04e9c8528f6647b0" + url: "https://pub.dev" + source: hosted + version: "1.4.0" + quiver: + dependency: transitive + description: + name: quiver + sha256: ea0b925899e64ecdfbf9c7becb60d5b50e706ade44a85b2363be2a22d88117d2 + url: "https://pub.dev" + source: hosted + version: "3.2.2" + rational: + dependency: transitive + description: + name: rational + sha256: cb808fb6f1a839e6fc5f7d8cb3b0a10e1db48b3be102de73938c627f0b636336 + url: "https://pub.dev" + source: hosted + version: "2.2.3" + shelf: + dependency: transitive + description: + name: shelf + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + url: "https://pub.dev" + source: hosted + version: "1.4.1" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + socks5_proxy: + dependency: transitive + description: + name: socks5_proxy + sha256: "616818a0ea1064a4823b53c9f7eaf8da64ed82dcd51ed71371c7e54751ed5053" + url: "https://pub.dev" + source: hosted + version: "1.0.6" + source_gen: + dependency: transitive + description: + name: source_gen + sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" + url: "https://pub.dev" + source: hosted + version: "1.5.0" + source_helper: + dependency: transitive + description: + name: source_helper + sha256: "86d247119aedce8e63f4751bd9626fc9613255935558447569ad42f9f5b48b3c" + url: "https://pub.dev" + source: hosted + version: "1.3.5" + source_span: + dependency: transitive + description: + name: source_span + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.dev" + source: hosted + version: "1.10.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + url: "https://pub.dev" + source: hosted + version: "1.11.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + url: "https://pub.dev" + source: hosted + version: "2.1.2" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 + url: "https://pub.dev" + source: hosted + version: "2.1.1" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + test_api: + dependency: transitive + description: + name: test_api + sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" + url: "https://pub.dev" + source: hosted + version: "0.7.2" + timing: + dependency: transitive + description: + name: timing + sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" + url: "https://pub.dev" + source: hosted + version: "1.0.2" + tuple: + dependency: transitive + description: + name: tuple + sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151 + url: "https://pub.dev" + source: hosted + version: "2.0.2" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + unorm_dart: + dependency: transitive + description: + name: unorm_dart + sha256: "23d8bf65605401a6a32cff99435fed66ef3dab3ddcad3454059165df46496a3b" + url: "https://pub.dev" + source: hosted + version: "0.3.0" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc + url: "https://pub.dev" + source: hosted + version: "14.2.4" + watcher: + dependency: transitive + description: + name: watcher + sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" + url: "https://pub.dev" + source: hosted + version: "0.1.6" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: "0b8e2457400d8a859b7b2030786835a28a8e80836ef64402abef392ff4f1d0e5" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" + yaml_edit: + dependency: transitive + description: + name: yaml_edit + sha256: fb38626579fb345ad00e674e2af3a5c9b0cc4b9bfb8fd7f7ff322c7c9e62aef5 + url: "https://pub.dev" + source: hosted + version: "2.2.2" +sdks: + dart: ">=3.5.0 <4.0.0" + flutter: ">=3.24.0" diff --git a/cw_decred/pubspec.yaml b/cw_decred/pubspec.yaml new file mode 100644 index 000000000..fcb2ac5ec --- /dev/null +++ b/cw_decred/pubspec.yaml @@ -0,0 +1,84 @@ +name: cw_decred +description: A new Flutter plugin project. +version: 0.0.1 +publish_to: none +author: Cake Wallet +homepage: https://cakewallet.com + +environment: + sdk: '>=3.2.0-0 <4.0.0' + flutter: ">=3.19.0" + + +dependencies: + flutter: + sdk: flutter + cw_core: + path: ../cw_core + +dev_dependencies: + flutter_test: + sdk: flutter + build_runner: ^2.1.11 + build_resolvers: ^2.0.9 + mobx_codegen: ^2.0.7 + hive_generator: ^2.0.1 + ffigen: ^16.1.0 + +ffigen: + name: libdcrwallet + description: Bindings for dcrwallet go library. + output: "lib/api/libdcrwallet_bindings.dart" + headers: + entry-points: + - "lib/api/libdcrwallet.h" + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + # This section identifies this Flutter project as a plugin project. + # The androidPackage and pluginClass identifiers should not ordinarily + # be modified. They are used by the tooling to maintain consistency when + # adding or updating assets for this project. + plugin: + platforms: + android: + package: com.cakewallet.cw_decred + pluginClass: CwDecredPlugin + ios: + pluginClass: CwDecredPlugin + macos: + pluginClass: CwDecredPlugin + + # To add assets to your plugin package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/assets-and-images/#from-packages + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware + + # To add custom fonts to your plugin package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/custom-fonts/#from-packages diff --git a/cw_monero/lib/monero_wallet.dart b/cw_monero/lib/monero_wallet.dart index 15e8f684e..ebad95f8e 100644 --- a/cw_monero/lib/monero_wallet.dart +++ b/cw_monero/lib/monero_wallet.dart @@ -115,6 +115,9 @@ abstract class MoneroWalletBase extends WalletBase balance; + @override + bool get hasRescan => true; + @override String get seed => monero_wallet.getSeed(); String seedLegacy(String? language) => monero_wallet.getSeedLegacy(language); diff --git a/cw_wownero/lib/wownero_wallet.dart b/cw_wownero/lib/wownero_wallet.dart index 67a9bbb45..6f4630b60 100644 --- a/cw_wownero/lib/wownero_wallet.dart +++ b/cw_wownero/lib/wownero_wallet.dart @@ -123,6 +123,9 @@ abstract class WowneroWalletBase String _password; + @override + bool get hasRescan => true; + @override MoneroWalletKeys get keys => MoneroWalletKeys( primaryAddress: wownero_wallet.getAddress(accountIndex: 0, addressIndex: 0), diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 57d4589c9..f621ab9b2 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -3,8 +3,39 @@ PODS: - Flutter - ReachabilitySwift - CryptoSwift (1.8.3) + - 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_mweb (0.0.1): - Flutter + - cw_decred (0.0.1): + - cw_shared_external (0.0.1): + - cw_shared_external/Boost (= 0.0.1) + - cw_shared_external/OpenSSL (= 0.0.1) + - cw_shared_external/Sodium (= 0.0.1) + - Flutter + - cw_shared_external/Boost (0.0.1): + - Flutter + - cw_shared_external/OpenSSL (0.0.1): + - Flutter + - cw_shared_external/Sodium (0.0.1): + - Flutter - device_display_brightness (0.0.1): - Flutter - device_info_plus (0.0.1): @@ -106,7 +137,10 @@ PODS: DEPENDENCIES: - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) - CryptoSwift + - cw_haven (from `.symlinks/plugins/cw_haven/ios`) - cw_mweb (from `.symlinks/plugins/cw_mweb/ios`) + - cw_shared_external (from `.symlinks/plugins/cw_shared_external/ios`) + - cw_decred (from `.symlinks/plugins/cw_decred/ios`) - device_display_brightness (from `.symlinks/plugins/device_display_brightness/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - devicelocale (from `.symlinks/plugins/devicelocale/ios`) @@ -147,8 +181,14 @@ SPEC REPOS: EXTERNAL SOURCES: connectivity_plus: :path: ".symlinks/plugins/connectivity_plus/ios" + cw_haven: + :path: ".symlinks/plugins/cw_haven/ios" cw_mweb: :path: ".symlinks/plugins/cw_mweb/ios" + cw_shared_external: + :path: ".symlinks/plugins/cw_shared_external/ios" + cw_decred: + :path: ".symlinks/plugins/cw_decred/ios" device_display_brightness: :path: ".symlinks/plugins/device_display_brightness/ios" device_info_plus: @@ -203,7 +243,10 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: connectivity_plus: bf0076dd84a130856aa636df1c71ccaff908fa1d CryptoSwift: 967f37cea5a3294d9cce358f78861652155be483 + cw_haven: b3e54e1fbe7b8e6fda57a93206bc38f8e89b898a cw_mweb: 22cd01dfb8ad2d39b15332006f22046aaa8352a3 + cw_shared_external: 2972d872b8917603478117c9957dfca611845a92 + cw_decred: 9c0e1df74745b51a1289ec5e91fb9e24b68fa14a device_display_brightness: 1510e72c567a1f6ce6ffe393dcd9afd1426034f7 device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6 devicelocale: 35ba84dc7f45f527c3001535d8c8d104edd5d926 diff --git a/ios/Runner/InfoBase.plist b/ios/Runner/InfoBase.plist index 69ab926b8..f27ef8d4f 100644 --- a/ios/Runner/InfoBase.plist +++ b/ios/Runner/InfoBase.plist @@ -242,6 +242,46 @@ wownero-wallet + + CFBundleTypeRole + Viewer + CFBundleURLName + zano + CFBundleURLSchemes + + zano + + + + CFBundleTypeRole + Viewer + CFBundleURLName + zano-wallet + CFBundleURLSchemes + + zano-wallet + + + + CFBundleTypeRole + Viewer + CFBundleURLName + decred + CFBundleURLSchemes + + decred + + + + CFBundleTypeRole + Viewer + CFBundleURLName + decred-wallet + CFBundleURLSchemes + + decred-wallet + + CFBundleVersion $(CURRENT_PROJECT_VERSION) diff --git a/lib/core/address_validator.dart b/lib/core/address_validator.dart index 888be1768..911e939d1 100644 --- a/lib/core/address_validator.dart +++ b/lib/core/address_validator.dart @@ -117,7 +117,7 @@ class AddressValidator extends TextValidator { case CryptoCurrency.zec: pattern = 't1[0-9a-zA-Z]{33}|t3[0-9a-zA-Z]{33}'; case CryptoCurrency.dcr: - pattern = 'D[ksecS]([0-9a-zA-Z])+'; + pattern = '(D|T|S)[ksecS]([0-9a-zA-Z])+'; case CryptoCurrency.rvn: pattern = '[Rr]([1-9a-km-zA-HJ-NP-Z]){33}'; case CryptoCurrency.near: diff --git a/lib/core/node_address_validator.dart b/lib/core/node_address_validator.dart index 0c8a0c37c..68478d501 100644 --- a/lib/core/node_address_validator.dart +++ b/lib/core/node_address_validator.dart @@ -17,3 +17,14 @@ class NodePathValidator extends TextValidator { isAutovalidate: true, ); } + +// NodeAddressValidatorDecredBlankException allows decred to send a blank ip +// address which effectively clears the current set persistant peer. +class NodeAddressValidatorDecredBlankException extends TextValidator { + NodeAddressValidatorDecredBlankException() + : super( + errorMessage: S.current.error_text_node_address, + isAutovalidate: true, + pattern: + '^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\$|^[0-9a-zA-Z.\-]+\$'); +} diff --git a/lib/core/seed_validator.dart b/lib/core/seed_validator.dart index d963fb523..5356ed875 100644 --- a/lib/core/seed_validator.dart +++ b/lib/core/seed_validator.dart @@ -10,6 +10,7 @@ import 'package:cake_wallet/solana/solana.dart'; import 'package:cake_wallet/tron/tron.dart'; import 'package:cake_wallet/wownero/wownero.dart'; import 'package:cake_wallet/zano/zano.dart'; +import 'package:cake_wallet/decred/decred.dart'; import 'package:cake_wallet/utils/language_list.dart'; import 'package:cw_core/wallet_type.dart'; @@ -50,6 +51,8 @@ class SeedValidator extends Validator { return wownero!.getWowneroWordList(language); case WalletType.zano: return zano!.getWordList(language); + case WalletType.decred: + return decred!.getDecredWordList(); case WalletType.none: return []; } diff --git a/lib/core/sync_status_title.dart b/lib/core/sync_status_title.dart index 46dd62c3a..cf4cb8e88 100644 --- a/lib/core/sync_status_title.dart +++ b/lib/core/sync_status_title.dart @@ -63,5 +63,9 @@ String syncStatusTitle(SyncStatus syncStatus) { return S.current.sync_status_attempting_scan; } + if (syncStatus is ProcessingSyncStatus) { + return syncStatus.message ?? S.current.processing; + } + return ''; } diff --git a/lib/core/wallet_creation_service.dart b/lib/core/wallet_creation_service.dart index 3fb4b5b1d..b44e56a98 100644 --- a/lib/core/wallet_creation_service.dart +++ b/lib/core/wallet_creation_service.dart @@ -90,6 +90,7 @@ class WalletCreationService { case WalletType.nano: case WalletType.banano: case WalletType.zano: + case WalletType.decred: return false; } } diff --git a/lib/decred/cw_decred.dart b/lib/decred/cw_decred.dart new file mode 100644 index 000000000..283895936 --- /dev/null +++ b/lib/decred/cw_decred.dart @@ -0,0 +1,114 @@ +part of 'decred.dart'; + +class CWDecred extends Decred { + CWDecred() {} + + @override + WalletCredentials createDecredNewWalletCredentials( + {required String name, WalletInfo? walletInfo}) => + DecredNewWalletCredentials(name: name, walletInfo: walletInfo); + + @override + WalletCredentials createDecredRestoreWalletFromSeedCredentials( + {required String name, required String mnemonic, required String password}) => + DecredRestoreWalletFromSeedCredentials(name: name, mnemonic: mnemonic, password: password); + + @override + WalletCredentials createDecredRestoreWalletFromPubkeyCredentials( + {required String name, required String pubkey, required String password}) => + DecredRestoreWalletFromPubkeyCredentials(name: name, pubkey: pubkey, password: password); + + @override + WalletService createDecredWalletService( + Box walletInfoSource, Box unspentCoinSource) { + return DecredWalletService(walletInfoSource, unspentCoinSource); + } + + @override + List getTransactionPriorities() => DecredTransactionPriority.all; + + @override + TransactionPriority getDecredTransactionPriorityMedium() => DecredTransactionPriority.medium; + + @override + TransactionPriority getDecredTransactionPrioritySlow() => DecredTransactionPriority.slow; + + @override + TransactionPriority deserializeDecredTransactionPriority(int raw) => + DecredTransactionPriority.deserialize(raw: raw); + + @override + Object createDecredTransactionCredentials(List outputs, TransactionPriority priority) => + DecredTransactionCredentials( + 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 DecredTransactionPriority); + + List getAddressInfos(Object wallet) { + final decredWallet = wallet as DecredWallet; + return decredWallet.walletAddresses.getAddressInfos(); + } + + @override + Future updateAddress(Object wallet, String address, String label) async { + final decredWallet = wallet as DecredWallet; + await decredWallet.walletAddresses.updateAddress(address, label); + } + + @override + Future generateNewAddress(Object wallet, String label) async { + final decredWallet = wallet as DecredWallet; + await decredWallet.walletAddresses.generateNewAddress(label); + } + + @override + String formatterDecredAmountToString({required int amount}) => + decredAmountToString(amount: amount); + + @override + double formatterDecredAmountToDouble({required int amount}) => + decredAmountToDouble(amount: amount); + + @override + int formatterStringDoubleToDecredAmount(String amount) => stringDoubleToDecredAmount(amount); + + @override + List getUnspents(Object wallet) { + final decredWallet = wallet as DecredWallet; + return decredWallet.unspents(); + } + + @override + void updateUnspents(Object wallet) { + final decredWallet = wallet as DecredWallet; + decredWallet.unspents(); + } + + @override + int heightByDate(DateTime date) { + final genesisBlocktime = DateTime.fromMillisecondsSinceEpoch(1454954400 * 1000); + final minutesDiff = date.difference(genesisBlocktime).inMinutes; + // Decred has five minute blocks on mainnet. + // NOTE: This is off by about a day but is currently unused by decred as we + // rescan from the wallet birthday. + return minutesDiff ~/ 5; + } + + @override + List getDecredWordList() => wordlist; + + @override + String pubkey(Object wallet) { + final decredWallet = wallet as DecredWallet; + return decredWallet.pubkey; + } +} diff --git a/lib/di.dart b/lib/di.dart index c8d664b6e..5c6d11e4b 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -69,6 +69,7 @@ import 'package:cake_wallet/haven/haven.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/nano/nano.dart'; import 'package:cake_wallet/polygon/polygon.dart'; +import 'package:cake_wallet/decred/decred.dart'; import 'package:cake_wallet/reactions/on_authentication_state_change.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/solana/solana.dart'; @@ -990,7 +991,8 @@ Future setup({ (Node? editingNode, bool? isSelected) => NodeCreateOrEditPage( nodeCreateOrEditViewModel: getIt.get(param2: false), editingNode: editingNode, - isSelected: isSelected)); + isSelected: isSelected, + type: getIt.get().wallet!.type)); getIt.registerFactoryParam( (Node? editingNode, bool? isSelected) => PowNodeCreateOrEditPage( @@ -1115,6 +1117,8 @@ Future setup({ return wownero!.createWowneroWalletService(_walletInfoSource, _unspentCoinsInfoSource); case WalletType.zano: return zano!.createZanoWalletService(_walletInfoSource); + case WalletType.decred: + return decred!.createDecredWalletService(_walletInfoSource, _unspentCoinsInfoSource); case WalletType.none: throw Exception('Unexpected token: ${param1.toString()} for generating of WalletService'); } diff --git a/lib/entities/default_settings_migration.dart b/lib/entities/default_settings_migration.dart index e57d71174..5ed35b5a1 100644 --- a/lib/entities/default_settings_migration.dart +++ b/lib/entities/default_settings_migration.dart @@ -46,6 +46,7 @@ const newCakeWalletBitcoinUri = 'btc-electrum.cakewallet.com:50002'; const wowneroDefaultNodeUri = 'node3.monerodevs.org:34568'; const zanoDefaultNodeUri = 'zano.cakewallet.com:11211'; const moneroWorldNodeUri = '.moneroworld.com'; +const decredDefaultUri = "default-spv-nodes"; Future defaultSettingsMigration( {required int version, @@ -96,30 +97,67 @@ Future defaultSettingsMigration( PreferencesKey.currentBalanceDisplayModeKey, BalanceDisplayMode.availableBalance.raw); await sharedPreferences.setBool('save_recipient_address', true); await resetToDefault(nodes); - await changeMoneroCurrentNodeToDefault( - sharedPreferences: sharedPreferences, nodes: nodes); - await changeBitcoinCurrentElectrumServerToDefault( - sharedPreferences: sharedPreferences, nodes: nodes); - await changeLitecoinCurrentElectrumServerToDefault( - sharedPreferences: sharedPreferences, nodes: nodes); - await changeHavenCurrentNodeToDefault(sharedPreferences: sharedPreferences, nodes: nodes); - await changeBitcoinCashCurrentNodeToDefault( - sharedPreferences: sharedPreferences, nodes: nodes); + await _changeDefaultNode( + nodes: nodes, + sharedPreferences: sharedPreferences, + type: WalletType.monero, + currentNodePreferenceKey: PreferencesKey.currentNodeIdKey, + useSSL: true, + trusted: true, + ); + await _changeDefaultNode( + nodes: nodes, + sharedPreferences: sharedPreferences, + type: WalletType.bitcoin, + currentNodePreferenceKey: PreferencesKey.currentBitcoinElectrumSererIdKey, + useSSL: true, + ); + await _changeDefaultNode( + nodes: nodes, + sharedPreferences: sharedPreferences, + type: WalletType.litecoin, + currentNodePreferenceKey: PreferencesKey.currentLitecoinElectrumSererIdKey, + useSSL: true, + ); + await _changeDefaultNode( + nodes: nodes, + sharedPreferences: sharedPreferences, + type: WalletType.haven, + currentNodePreferenceKey: PreferencesKey.currentHavenNodeIdKey, + ); break; case 2: await replaceNodesMigration(nodes: nodes); - await replaceDefaultNode(sharedPreferences: sharedPreferences, nodes: nodes); - + await _changeDefaultNode( + nodes: nodes, + sharedPreferences: sharedPreferences, + type: WalletType.monero, + newDefaultUri: newCakeWalletMoneroUri, + currentNodePreferenceKey: PreferencesKey.currentNodeIdKey, + useSSL: true, + trusted: true, + oldUri: [ + 'xmr-node-uk.cakewallet.com:18081', + 'eu-node.cakewallet.io:18081', + 'node.cakewallet.io:18081' + ], + ); break; case 3: await updateNodeTypes(nodes: nodes); - await addBitcoinElectrumServerList(nodes: nodes); + await addWalletNodeList(nodes: nodes, type: WalletType.bitcoin); break; case 4: - await changeBitcoinCurrentElectrumServerToDefault( - sharedPreferences: sharedPreferences, nodes: nodes); + await _changeDefaultNode( + nodes: nodes, + sharedPreferences: sharedPreferences, + type: WalletType.bitcoin, + newDefaultUri: newCakeWalletBitcoinUri, + currentNodePreferenceKey: PreferencesKey.currentBitcoinElectrumSererIdKey, + useSSL: true, + ); break; case 5: @@ -139,7 +177,15 @@ Future defaultSettingsMigration( break; case 11: - await changeDefaultMoneroNode(nodes, sharedPreferences); + await _changeDefaultNode( + nodes: nodes, + sharedPreferences: sharedPreferences, + type: WalletType.monero, + newDefaultUri: newCakeWalletMoneroUri, + currentNodePreferenceKey: PreferencesKey.currentNodeIdKey, + trusted: true, + oldUri: ['.cakewallet.com'], + ); break; case 12: @@ -151,28 +197,38 @@ Future defaultSettingsMigration( break; case 15: - await addLitecoinElectrumServerList(nodes: nodes); - await changeLitecoinCurrentElectrumServerToDefault( - sharedPreferences: sharedPreferences, nodes: nodes); + await addWalletNodeList(nodes: nodes, type: WalletType.litecoin); + await _changeDefaultNode( + nodes: nodes, + sharedPreferences: sharedPreferences, + type: WalletType.litecoin, + currentNodePreferenceKey: PreferencesKey.currentLitecoinElectrumSererIdKey, + ); await checkCurrentNodes(nodes, powNodes, sharedPreferences); break; case 16: - await addHavenNodeList(nodes: nodes); - await changeHavenCurrentNodeToDefault(sharedPreferences: sharedPreferences, nodes: nodes); + await addWalletNodeList(nodes: nodes, type: WalletType.haven); + await _changeDefaultNode( + nodes: nodes, + sharedPreferences: sharedPreferences, + type: WalletType.haven, + currentNodePreferenceKey: PreferencesKey.currentHavenNodeIdKey, + ); await checkCurrentNodes(nodes, powNodes, sharedPreferences); break; case 17: - await changeDefaultHavenNode(nodes); + await _changeDefaultNode( + nodes: nodes, + sharedPreferences: sharedPreferences, + type: WalletType.haven, + currentNodePreferenceKey: PreferencesKey.currentHavenNodeIdKey, + ); break; case 18: - await updateWalletTypeNodesWithNewNode( - nodes: nodes, - newNodeUri: "cakexmrl7bonq7ovjka5kuwuyd3f7qnkz6z6s6dmsy3uckwra7bvggyd.onion:18081", - type: WalletType.monero, - ); + addWalletNodeList(nodes: nodes, type: WalletType.monero); break; case 19: @@ -182,47 +238,82 @@ Future defaultSettingsMigration( await migrateExchangeStatus(sharedPreferences); break; case 21: - await addEthereumNodeList(nodes: nodes); - await changeEthereumCurrentNodeToDefault( - sharedPreferences: sharedPreferences, nodes: nodes); + await addWalletNodeList(nodes: nodes, type: WalletType.ethereum); + await _changeDefaultNode( + nodes: nodes, + sharedPreferences: sharedPreferences, + type: WalletType.ethereum, + currentNodePreferenceKey: PreferencesKey.currentEthereumNodeIdKey, + ); break; case 22: - await addNanoNodeList(nodes: nodes); + await addWalletNodeList(nodes: nodes, type: WalletType.nano); await addNanoPowNodeList(nodes: powNodes); - await changeNanoCurrentNodeToDefault(sharedPreferences: sharedPreferences, nodes: nodes); - await changeNanoCurrentPowNodeToDefault( - sharedPreferences: sharedPreferences, nodes: powNodes); + await _changeDefaultNode( + nodes: nodes, + sharedPreferences: sharedPreferences, + type: WalletType.nano, + currentNodePreferenceKey: PreferencesKey.currentNanoNodeIdKey, + ); + await _changeDefaultNode( + nodes: powNodes, + sharedPreferences: sharedPreferences, + type: WalletType.nano, + currentNodePreferenceKey: PreferencesKey.currentNanoPowNodeIdKey, + newDefaultUri: nanoDefaultPowNodeUri, + ); break; case 23: - await addBitcoinCashElectrumServerList(nodes: nodes); - await changeBitcoinCurrentElectrumServerToDefault( - sharedPreferences: sharedPreferences, nodes: nodes); + await addWalletNodeList(nodes: nodes, type: WalletType.bitcoinCash); + await _changeDefaultNode( + nodes: nodes, + sharedPreferences: sharedPreferences, + type: WalletType.bitcoinCash, + currentNodePreferenceKey: PreferencesKey.currentBitcoinCashNodeIdKey, + ); break; case 24: - await addPolygonNodeList(nodes: nodes); - await changePolygonCurrentNodeToDefault( - sharedPreferences: sharedPreferences, nodes: nodes); + await addWalletNodeList(nodes: nodes, type: WalletType.polygon); + await _changeDefaultNode( + nodes: nodes, + sharedPreferences: sharedPreferences, + type: WalletType.polygon, + currentNodePreferenceKey: PreferencesKey.currentPolygonNodeIdKey, + ); break; case 25: await rewriteSecureStoragePin(secureStorage: secureStorage); break; case 26: - /// commented out as it was a probable cause for some users to have white screen issues - /// maybe due to multiple access on Secure Storage at once - /// or long await time on start of the app - // await insecureStorageMigration(secureStorage: secureStorage, sharedPreferences: sharedPreferences); + /// commented out as it was a probable cause for some users to have white screen issues + /// maybe due to multiple access on Secure Storage at once + /// or long await time on start of the app + // await insecureStorageMigration(secureStorage: secureStorage, sharedPreferences: sharedPreferences); + break; case 27: - await addSolanaNodeList(nodes: nodes); - await changeSolanaCurrentNodeToDefault( - sharedPreferences: sharedPreferences, nodes: nodes); + await addWalletNodeList(nodes: nodes, type: WalletType.solana); + await _changeDefaultNode( + nodes: nodes, + sharedPreferences: sharedPreferences, + type: WalletType.solana, + currentNodePreferenceKey: PreferencesKey.currentSolanaNodeIdKey, + ); break; case 28: await _updateMoneroPriority(sharedPreferences); break; case 29: - await changeDefaultBitcoinNode(nodes, sharedPreferences); + await _changeDefaultNode( + nodes: nodes, + sharedPreferences: sharedPreferences, + type: WalletType.bitcoin, + newDefaultUri: newCakeWalletBitcoinUri, + currentNodePreferenceKey: PreferencesKey.currentBitcoinElectrumSererIdKey, + useSSL: true, + oldUri: ['.cakewallet.com'], + ); break; case 30: await disableServiceStatusFiatDisabled(sharedPreferences); @@ -234,28 +325,50 @@ Future defaultSettingsMigration( await updateBtcNanoWalletInfos(walletInfoSource); break; case 33: - await addTronNodeList(nodes: nodes); - await changeTronCurrentNodeToDefault(sharedPreferences: sharedPreferences, nodes: nodes); + await addWalletNodeList(nodes: nodes, type: WalletType.tron); + await _changeDefaultNode( + nodes: nodes, + sharedPreferences: sharedPreferences, + type: WalletType.tron, + currentNodePreferenceKey: PreferencesKey.currentTronNodeIdKey, + ); break; case 34: - await _addElectRsNode(nodes, sharedPreferences); + addWalletNodeList(nodes: nodes, type: WalletType.bitcoin); case 35: - await _switchElectRsNode(nodes, sharedPreferences); + await _changeDefaultNode( + nodes: nodes, + sharedPreferences: sharedPreferences, + type: WalletType.bitcoin, + newDefaultUri: newCakeWalletBitcoinUri, + currentNodePreferenceKey: PreferencesKey.currentBitcoinElectrumSererIdKey, + useSSL: true, + oldUri: ['electrs.cakewallet.com'], + ); break; case 36: - await addWowneroNodeList(nodes: nodes); + await addWalletNodeList(nodes: nodes, type: WalletType.wownero); await changeWowneroCurrentNodeToDefault( sharedPreferences: sharedPreferences, nodes: nodes); break; case 37: - await replaceTronDefaultNode(sharedPreferences: sharedPreferences, nodes: nodes); + // removed as it would be replaced again anyway + // await replaceTronDefaultNode(sharedPreferences: sharedPreferences, nodes: nodes); break; case 38: await fixBtcDerivationPaths(walletInfoSource); break; case 39: _fixNodesUseSSLFlag(nodes); - await changeDefaultNanoNode(nodes, sharedPreferences); + await _changeDefaultNode( + nodes: nodes, + sharedPreferences: sharedPreferences, + type: WalletType.nano, + newDefaultUri: nanoDefaultNodeUri, + currentNodePreferenceKey: PreferencesKey.currentNanoNodeIdKey, + useSSL: true, + oldUri: ['rpc.nano.to'], + ); break; case 40: await removeMoneroWorld(sharedPreferences: sharedPreferences, nodes: nodes); @@ -266,8 +379,8 @@ Future defaultSettingsMigration( providerName: "SwapTrade", enabled: false, ); - await _addSethNode(nodes, sharedPreferences); - await updateTronNodesWithNowNodes(sharedPreferences: sharedPreferences, nodes: nodes); + addWalletNodeList(nodes: nodes, type: WalletType.bitcoin); + addWalletNodeList(nodes: nodes, type: WalletType.tron); break; case 42: _fixNodesUseSSLFlag(nodes); @@ -312,19 +425,8 @@ Future defaultSettingsMigration( case 45: await _backupHavenSeeds(havenSeedStore); - updateWalletTypeNodesWithNewNode( - newNodeUri: 'matic.nownodes.io', - nodes: nodes, - type: WalletType.polygon, - useSSL: true, - ); - updateWalletTypeNodesWithNewNode( - newNodeUri: 'eth.nownodes.io', - nodes: nodes, - type: WalletType.ethereum, - useSSL: true, - ); - + addWalletNodeList(nodes: nodes, type: WalletType.polygon); + addWalletNodeList(nodes: nodes, type: WalletType.ethereum); _changeDefaultNode( nodes: nodes, sharedPreferences: sharedPreferences, @@ -349,18 +451,7 @@ Future defaultSettingsMigration( break; case 46: await _fixNodesUseSSLFlag(nodes); - await updateWalletTypeNodesWithNewNode( - newNodeUri: 'litecoin.stackwallet.com:20063', - nodes: nodes, - type: WalletType.litecoin, - useSSL: true, - ); - await updateWalletTypeNodesWithNewNode( - newNodeUri: 'electrum-ltc.bysh.me:50002', - nodes: nodes, - type: WalletType.litecoin, - useSSL: true, - ); + await addWalletNodeList(nodes: nodes, type: WalletType.litecoin); await _changeDefaultNode( nodes: nodes, sharedPreferences: sharedPreferences, @@ -386,10 +477,14 @@ Future defaultSettingsMigration( newUri: "polygon-bor-rpc.publicnode.com", useSSL: true, ); - break; case 47: - await addZanoNodeList(nodes: nodes); - await changeZanoCurrentNodeToDefault(sharedPreferences: sharedPreferences, nodes: nodes); + await addWalletNodeList(nodes: nodes, type: WalletType.zano); + await _changeDefaultNode( + nodes: nodes, + sharedPreferences: sharedPreferences, + type: WalletType.zano, + currentNodePreferenceKey: PreferencesKey.currentZanoNodeIdKey, + ); _changeExchangeProviderAvailability( sharedPreferences, providerName: "SimpleSwap", @@ -400,8 +495,17 @@ Future defaultSettingsMigration( providerName: "SwapTrade", enabled: false, ); - break; + break; case 48: + await addWalletNodeList(nodes: nodes, type: WalletType.decred); + await _changeDefaultNode( + nodes: nodes, + sharedPreferences: sharedPreferences, + type: WalletType.decred, + currentNodePreferenceKey: PreferencesKey.currentDecredNodeIdKey, + ); + break; + case 49: _changeExchangeProviderAvailability( sharedPreferences, providerName: "SwapTrade", @@ -414,8 +518,9 @@ Future defaultSettingsMigration( await sharedPreferences.setInt( PreferencesKey.currentDefaultSettingsMigrationVersion, version); - } catch (e) { + } catch (e, s) { printV('Migration error: ${e.toString()}'); + printV('Migration error: ${s}'); } }); @@ -455,17 +560,24 @@ Future _changeDefaultNode({ required Box nodes, required SharedPreferences sharedPreferences, required WalletType type, - required String newDefaultUri, required String currentNodePreferenceKey, - required bool useSSL, - required List - oldUri, // leave empty if you want to force replace the node regardless of the user's current node + bool useSSL = true, + bool trusted = false, + String? newDefaultUri, // ignore, if you want to use the default node uri + List? + oldUri, // ignore, if you want to force replace the node regardless of the user's current node }) async { final currentNodeId = sharedPreferences.getInt(currentNodePreferenceKey); - final currentNode = nodes.values.firstWhere((node) => node.key == currentNodeId); - final shouldReplace = oldUri.any((e) => currentNode.uriRaw.contains(e)); + final bool shouldReplace; + if (currentNodeId == null) { + shouldReplace = true; + } else { + final currentNode = nodes.values.firstWhere((node) => node.key == currentNodeId); + shouldReplace = oldUri?.any((e) => currentNode.uriRaw.contains(e)) ?? true; + } if (shouldReplace) { + newDefaultUri ??= _getDefaultNodeUri(type); var newNodeId = nodes.values.firstWhereOrNull((element) => element.uriRaw == newDefaultUri)?.key; @@ -475,6 +587,7 @@ Future _changeDefaultNode({ uri: newDefaultUri, type: type, useSSL: useSSL, + trusted: trusted, ); await nodes.add(newNode); @@ -485,23 +598,38 @@ Future _changeDefaultNode({ } } -/// Generic function for adding a new Node for a Wallet Type. -Future updateWalletTypeNodesWithNewNode({ - required Box nodes, - required WalletType type, - required String newNodeUri, - bool? useSSL, -}) async { - // If it already exists in the box of nodes, no need to add it annymore. - if (nodes.values.any((node) => node.uriRaw == newNodeUri)) return; - - await nodes.add( - Node( - uri: newNodeUri, - type: type, - useSSL: useSSL, - ), - ); +String _getDefaultNodeUri(WalletType type) { + switch (type) { + case WalletType.monero: + return newCakeWalletMoneroUri; + case WalletType.bitcoin: + return newCakeWalletBitcoinUri; + case WalletType.litecoin: + return cakeWalletLitecoinElectrumUri; + case WalletType.haven: + return havenDefaultNodeUri; + case WalletType.ethereum: + return ethereumDefaultNodeUri; + case WalletType.nano: + return nanoDefaultNodeUri; + case WalletType.bitcoinCash: + return cakeWalletBitcoinCashDefaultNodeUri; + case WalletType.polygon: + return polygonDefaultNodeUri; + case WalletType.solana: + return solanaDefaultNodeUri; + case WalletType.tron: + return tronDefaultNodeUri; + case WalletType.wownero: + return wowneroDefaultNodeUri; + case WalletType.zano: + return zanoDefaultNodeUri; + case WalletType.decred: + return decredDefaultUri; + case WalletType.banano: + case WalletType.none: + return ''; + } } void _changeExchangeProviderAvailability(SharedPreferences sharedPreferences, @@ -533,7 +661,7 @@ Future _fixNodesUseSSLFlag(Box nodes) async { } Future updateNanoNodeList({required Box nodes}) async { - final nodeList = await loadDefaultNanoNodes(); + final nodeList = await loadDefaultNodes(WalletType.nano); var listOfNewEndpoints = [ "app.natrium.io", "rainstorm.city", @@ -682,87 +810,16 @@ Future replaceNodesMigration({required Box nodes}) async { }); } -Future changeMoneroCurrentNodeToDefault( - {required SharedPreferences sharedPreferences, required Box nodes}) async { - final node = getMoneroDefaultNode(nodes: nodes); - final nodeId = node.key as int? ?? 0; // 0 - England - - await sharedPreferences.setInt(PreferencesKey.currentNodeIdKey, nodeId); -} - -Node? getBitcoinDefaultElectrumServer({required Box nodes}) { - return nodes.values - .firstWhereOrNull((Node node) => node.uriRaw == cakeWalletBitcoinElectrumUri) ?? - nodes.values.firstWhereOrNull((node) => node.type == WalletType.bitcoin); -} - Node? getBitcoinTestnetDefaultElectrumServer({required Box nodes}) { return nodes.values .firstWhereOrNull((Node node) => node.uriRaw == publicBitcoinTestnetElectrumUri) ?? nodes.values.firstWhereOrNull((node) => node.type == WalletType.bitcoin); } -Node? getLitecoinDefaultElectrumServer({required Box nodes}) { - return nodes.values - .firstWhereOrNull((Node node) => node.uriRaw == cakeWalletLitecoinElectrumUri) ?? - nodes.values.firstWhereOrNull((node) => node.type == WalletType.litecoin); -} - -Node? getHavenDefaultNode({required Box nodes}) { - return nodes.values.firstWhereOrNull((Node node) => node.uriRaw == havenDefaultNodeUri) ?? - nodes.values.firstWhereOrNull((node) => node.type == WalletType.haven); -} - -Node? getEthereumDefaultNode({required Box nodes}) { - return nodes.values.firstWhereOrNull((Node node) => node.uriRaw == ethereumDefaultNodeUri) ?? - nodes.values.firstWhereOrNull((node) => node.type == WalletType.ethereum); -} - -Node? getPolygonDefaultNode({required Box nodes}) { - return nodes.values.firstWhereOrNull((Node node) => node.uriRaw == polygonDefaultNodeUri) ?? - nodes.values.firstWhereOrNull((node) => node.type == WalletType.polygon); -} - -Node? getNanoDefaultNode({required Box nodes}) { - return nodes.values.firstWhereOrNull((Node node) => node.uriRaw == nanoDefaultNodeUri) ?? - nodes.values.firstWhereOrNull((node) => node.type == WalletType.nano); -} - -Node? getNanoDefaultPowNode({required Box nodes}) { - return nodes.values.firstWhereOrNull((Node node) => node.uriRaw == nanoDefaultPowNodeUri) ?? - 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? getZanoDefaultNode({required Box nodes}) { - return nodes.values.firstWhereOrNull( - (Node node) => node.uriRaw == zanoDefaultNodeUri) - ?? nodes.values.firstWhereOrNull((node) => node.type == WalletType.zano); -} - -Node getMoneroDefaultNode({required Box nodes}) { - var nodeUri = newCakeWalletMoneroUri; - - try { - return nodes.values.firstWhere((Node node) => node.uriRaw == nodeUri); - } catch (_) { - return nodes.values.first; - } -} - -Node? getSolanaDefaultNode({required Box nodes}) { - return nodes.values.firstWhereOrNull((Node node) => node.uriRaw == solanaDefaultNodeUri) ?? - nodes.values.firstWhereOrNull((node) => node.type == WalletType.solana); -} - -Node? getTronDefaultNode({required Box nodes}) { - return nodes.values.firstWhereOrNull((Node node) => node.uriRaw == tronDefaultNodeUri) ?? - nodes.values.firstWhereOrNull((node) => node.type == WalletType.tron); +Node? getDefaultNode({required Box nodes, required WalletType type}) { + final defaultUri = _getDefaultNodeUri(type); + return nodes.values.firstWhereOrNull((Node node) => node.uriRaw == defaultUri) ?? + nodes.values.firstWhereOrNull((node) => node.type == type); } Node getWowneroDefaultNode({required Box nodes}) { @@ -899,63 +956,6 @@ Future rewriteSecureStoragePin({required SecureStorage secureStorage}) asy ); } -Future changeBitcoinCurrentElectrumServerToDefault( - {required SharedPreferences sharedPreferences, - required Box nodes, - bool? isTestnet}) async { - Node? server; - if (isTestnet == true) { - server = getBitcoinTestnetDefaultElectrumServer(nodes: nodes); - } else { - server = getBitcoinDefaultElectrumServer(nodes: nodes); - } - final serverId = server?.key as int? ?? 0; - - await sharedPreferences.setInt(PreferencesKey.currentBitcoinElectrumSererIdKey, serverId); -} - -Future changeLitecoinCurrentElectrumServerToDefault( - {required SharedPreferences sharedPreferences, required Box nodes}) async { - final server = getLitecoinDefaultElectrumServer(nodes: nodes); - final serverId = server?.key as int? ?? 0; - - 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); - final nodeId = node?.key as int? ?? 0; - - await sharedPreferences.setInt(PreferencesKey.currentHavenNodeIdKey, nodeId); -} - -Future replaceDefaultNode( - {required SharedPreferences sharedPreferences, required Box nodes}) async { - const nodesForReplace = [ - 'xmr-node-uk.cakewallet.com:18081', - 'eu-node.cakewallet.io:18081', - 'node.cakewallet.io:18081' - ]; - final currentNodeId = sharedPreferences.getInt(PreferencesKey.currentNodeIdKey); - final currentNode = nodes.values.firstWhereOrNull((Node node) => node.key == currentNodeId); - final needToReplace = currentNode == null ? true : nodesForReplace.contains(currentNode.uriRaw); - - if (!needToReplace) { - return; - } - - await changeMoneroCurrentNodeToDefault(sharedPreferences: sharedPreferences, nodes: nodes); -} - Future updateNodeTypes({required Box nodes}) async { nodes.values.forEach((node) async { if (node.type == null) { @@ -965,42 +965,6 @@ Future updateNodeTypes({required Box nodes}) async { }); } -Future addBitcoinElectrumServerList({required Box nodes}) async { - final serverList = await loadBitcoinElectrumServerList(); - for (var node in serverList) { - if (nodes.values.firstWhereOrNull((element) => element.uriRaw == node.uriRaw) == null) { - await nodes.add(node); - } - } -} - -Future addLitecoinElectrumServerList({required Box nodes}) async { - final serverList = await loadLitecoinElectrumServerList(); - for (var node in serverList) { - if (nodes.values.firstWhereOrNull((element) => element.uriRaw == node.uriRaw) == null) { - await nodes.add(node); - } - } -} - -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) { - if (nodes.values.firstWhereOrNull((element) => element.uriRaw == node.uriRaw) == null) { - await nodes.add(node); - } - } -} - Future addAddressesForMoneroWallets(Box walletInfoSource) async { final moneroWalletsInfo = walletInfoSource.values.where((info) => info.type == WalletType.monero); moneroWalletsInfo.forEach((info) async { @@ -1050,31 +1014,6 @@ Future changeTransactionPriorityAndFeeRateKeys(SharedPreferences sharedPre bitcoin!.getMediumTransactionPriority().serialize()); } -Future changeDefaultMoneroNode( - Box nodeSource, SharedPreferences sharedPreferences) async { - const cakeWalletMoneroNodeUriPattern = '.cakewallet.com'; - final currentMoneroNodeId = sharedPreferences.getInt(PreferencesKey.currentNodeIdKey); - final currentMoneroNode = nodeSource.values.firstWhere((node) => node.key == currentMoneroNodeId); - final needToReplaceCurrentMoneroNode = - currentMoneroNode.uri.toString().contains(cakeWalletMoneroNodeUriPattern); - - nodeSource.values.forEach((node) async { - if (node.type == WalletType.monero && - node.uri.toString().contains(cakeWalletMoneroNodeUriPattern)) { - await node.delete(); - } - }); - - final newCakeWalletNode = - Node(uri: newCakeWalletMoneroUri, type: WalletType.monero, trusted: true); - - await nodeSource.add(newCakeWalletNode); - - if (needToReplaceCurrentMoneroNode) { - await sharedPreferences.setInt(PreferencesKey.currentNodeIdKey, newCakeWalletNode.key as int); - } -} - Future fixBtcDerivationPaths(Box walletsInfoSource) async { for (WalletInfo walletInfo in walletsInfoSource.values) { if (walletInfo.type == WalletType.bitcoin || @@ -1102,128 +1041,6 @@ Future updateBtcNanoWalletInfos(Box walletsInfoSource) async { } } -Future changeDefaultNanoNode( - Box nodeSource, SharedPreferences sharedPreferences) async { - const oldNanoNodeUriPattern = 'rpc.nano.to'; - final currentNanoNodeId = sharedPreferences.getInt(PreferencesKey.currentNanoNodeIdKey); - final currentNanoNode = nodeSource.values.firstWhere((node) => node.key == currentNanoNodeId); - - final newCakeWalletNode = Node( - uri: nanoDefaultNodeUri, - type: WalletType.nano, - useSSL: true, - ); - - await nodeSource.add(newCakeWalletNode); - - if (currentNanoNode.uri.toString().contains(oldNanoNodeUriPattern)) { - await sharedPreferences.setInt( - PreferencesKey.currentNanoNodeIdKey, newCakeWalletNode.key as int); - } -} - -Future changeDefaultBitcoinNode( - Box nodeSource, SharedPreferences sharedPreferences) async { - const cakeWalletBitcoinNodeUriPattern = '.cakewallet.com'; - final currentBitcoinNodeId = - sharedPreferences.getInt(PreferencesKey.currentBitcoinElectrumSererIdKey); - final currentBitcoinNode = - nodeSource.values.firstWhere((node) => node.key == currentBitcoinNodeId); - final needToReplaceCurrentBitcoinNode = - currentBitcoinNode.uri.toString().contains(cakeWalletBitcoinNodeUriPattern); - - final newCakeWalletBitcoinNode = - Node(uri: newCakeWalletBitcoinUri, type: WalletType.bitcoin, useSSL: true); - - if (!nodeSource.values.any((element) => element.uriRaw == newCakeWalletBitcoinUri)) { - await nodeSource.add(newCakeWalletBitcoinNode); - } - - if (needToReplaceCurrentBitcoinNode) { - await sharedPreferences.setInt( - PreferencesKey.currentBitcoinElectrumSererIdKey, newCakeWalletBitcoinNode.key as int); - } -} - -Future _addSethNode(Box nodeSource, SharedPreferences sharedPreferences) async { - _addBitcoinNode( - nodeSource: nodeSource, - sharedPreferences: sharedPreferences, - nodeUri: "fulcrum.sethforprivacy.com:50002", - useSSL: false, - ); -} - -Future _addElectRsNode(Box nodeSource, SharedPreferences sharedPreferences) async { - _addBitcoinNode( - nodeSource: nodeSource, - sharedPreferences: sharedPreferences, - nodeUri: cakeWalletSilentPaymentsElectrsUri, - ); -} - -Future _addBitcoinNode({ - required Box nodeSource, - required SharedPreferences sharedPreferences, - required String nodeUri, - bool replaceExisting = false, - bool useSSL = false, -}) async { - bool isNodeExists = nodeSource.values.any((element) => element.uriRaw == nodeUri); - if (isNodeExists) { - return; - } - const cakeWalletBitcoinNodeUriPattern = '.cakewallet.com'; - final currentBitcoinNodeId = - sharedPreferences.getInt(PreferencesKey.currentBitcoinElectrumSererIdKey); - final currentBitcoinNode = - nodeSource.values.firstWhere((node) => node.key == currentBitcoinNodeId); - final needToReplaceCurrentBitcoinNode = - currentBitcoinNode.uri.toString().contains(cakeWalletBitcoinNodeUriPattern); - - final newElectRsBitcoinNode = Node(uri: nodeUri, type: WalletType.bitcoin, useSSL: useSSL); - - await nodeSource.add(newElectRsBitcoinNode); - - if (needToReplaceCurrentBitcoinNode && replaceExisting) { - await sharedPreferences.setInt( - PreferencesKey.currentBitcoinElectrumSererIdKey, newElectRsBitcoinNode.key as int); - } -} - -Future _switchElectRsNode(Box nodeSource, SharedPreferences sharedPreferences) async { - final currentBitcoinNodeId = - sharedPreferences.getInt(PreferencesKey.currentBitcoinElectrumSererIdKey); - final currentBitcoinNode = - nodeSource.values.firstWhere((node) => node.key == currentBitcoinNodeId); - final needToReplaceCurrentBitcoinNode = - currentBitcoinNode.uri.toString().contains('electrs.cakewallet.com'); - - if (!needToReplaceCurrentBitcoinNode) return; - - final btcElectrumNode = nodeSource.values.firstWhereOrNull( - (node) => node.uri.toString().contains('btc-electrum.cakewallet.com'), - ); - - if (btcElectrumNode == null) { - final newBtcElectrumBitcoinNode = Node( - uri: newCakeWalletBitcoinUri, - type: WalletType.bitcoin, - useSSL: false, - ); - await nodeSource.add(newBtcElectrumBitcoinNode); - await sharedPreferences.setInt( - PreferencesKey.currentBitcoinElectrumSererIdKey, - newBtcElectrumBitcoinNode.key as int, - ); - } else { - await sharedPreferences.setInt( - PreferencesKey.currentBitcoinElectrumSererIdKey, - btcElectrumNode.key as int, - ); - } -} - Future checkCurrentNodes( Box nodeSource, Box powNodeSource, SharedPreferences sharedPreferences) async { final currentMoneroNodeId = sharedPreferences.getInt(PreferencesKey.currentNodeIdKey); @@ -1236,6 +1053,7 @@ Future checkCurrentNodes( final currentPolygonNodeId = sharedPreferences.getInt(PreferencesKey.currentPolygonNodeIdKey); final currentNanoNodeId = sharedPreferences.getInt(PreferencesKey.currentNanoNodeIdKey); final currentNanoPowNodeId = sharedPreferences.getInt(PreferencesKey.currentNanoPowNodeIdKey); + final currentDecredNodeId = sharedPreferences.getInt(PreferencesKey.currentDecredNodeIdKey); final currentBitcoinCashNodeId = sharedPreferences.getInt(PreferencesKey.currentBitcoinCashNodeIdKey); final currentSolanaNodeId = sharedPreferences.getInt(PreferencesKey.currentSolanaNodeIdKey); @@ -1256,6 +1074,8 @@ Future checkCurrentNodes( nodeSource.values.firstWhereOrNull((node) => node.key == currentPolygonNodeId); final currentNanoNodeServer = nodeSource.values.firstWhereOrNull((node) => node.key == currentNanoNodeId); + final currentDecredNodeServer = + nodeSource.values.firstWhereOrNull((node) => node.key == currentDecredNodeId); final currentNanoPowNodeServer = powNodeSource.values.firstWhereOrNull((node) => node.key == currentNanoPowNodeId); final currentBitcoinCashNodeServer = @@ -1266,7 +1086,8 @@ Future checkCurrentNodes( nodeSource.values.firstWhereOrNull((node) => node.key == currentTronNodeId); final currentWowneroNodeServer = nodeSource.values.firstWhereOrNull((node) => node.key == currentWowneroNodeId); - final currentZanoNode = nodeSource.values.firstWhereOrNull((node) => node.key == currentZanoNodeId); + final currentZanoNode = + nodeSource.values.firstWhereOrNull((node) => node.key == currentZanoNodeId); if (currentMoneroNode == null) { final newCakeWalletNode = Node(uri: newCakeWalletMoneroUri, type: WalletType.monero); @@ -1357,6 +1178,12 @@ Future checkCurrentNodes( await nodeSource.add(node); await sharedPreferences.setInt(PreferencesKey.currentZanoNodeIdKey, node.key as int); } + + if (currentDecredNodeServer == null) { + final node = Node(uri: decredDefaultUri, type: WalletType.decred); + await nodeSource.add(node); + await sharedPreferences.setInt(PreferencesKey.currentDecredNodeIdKey, node.key as int); + } } Future resetBitcoinElectrumServer( @@ -1385,15 +1212,6 @@ Future resetBitcoinElectrumServer( await oldElectrumServer?.delete(); } -Future changeDefaultHavenNode(Box nodeSource) async { - const previousHavenDefaultNodeUri = 'vault.havenprotocol.org:443'; - final havenNodes = nodeSource.values.where((node) => node.uriRaw == previousHavenDefaultNodeUri); - havenNodes.forEach((node) async { - node.uriRaw = havenDefaultNodeUri; - await node.save(); - }); -} - Future migrateExchangeStatus(SharedPreferences sharedPreferences) async { final isExchangeDisabled = sharedPreferences.getBool(PreferencesKey.disableExchangeKey); if (isExchangeDisabled == null) { @@ -1406,65 +1224,14 @@ Future migrateExchangeStatus(SharedPreferences sharedPreferences) async { await sharedPreferences.remove(PreferencesKey.disableExchangeKey); } -Future addEthereumNodeList({required Box nodes}) async { - final nodeList = await loadDefaultEthereumNodes(); - for (var node in nodeList) { - if (nodes.values.firstWhereOrNull((element) => element.uriRaw == node.uriRaw) == null) { - await nodes.add(node); - } - } -} - -Future changeEthereumCurrentNodeToDefault( - {required SharedPreferences sharedPreferences, required Box nodes}) async { - final node = getEthereumDefaultNode(nodes: nodes); - final nodeId = node?.key as int? ?? 0; - - await sharedPreferences.setInt(PreferencesKey.currentEthereumNodeIdKey, nodeId); -} - -Future addWowneroNodeList({required Box nodes}) async { - final nodeList = await loadDefaultWowneroNodes(); - for (var node in nodeList) { - if (nodes.values.firstWhereOrNull((element) => element.uriRaw == node.uriRaw) == null) { - await nodes.add(node); - } - } -} - -Future addZanoNodeList({required Box nodes}) async { - final nodeList = await loadDefaultZanoNodes(); - for (var node in nodeList) { - if (nodes.values.firstWhereOrNull((element) => element.uriRaw == node.uriRaw) == null) { - await nodes.add(node); - } - } -} - Future changeWowneroCurrentNodeToDefault( {required SharedPreferences sharedPreferences, required Box nodes}) async { final node = getWowneroDefaultNode(nodes: nodes); - final nodeId = node?.key as int? ?? 0; + final nodeId = node.key as int? ?? 0; await sharedPreferences.setInt(PreferencesKey.currentWowneroNodeIdKey, nodeId); } -Future changeZanoCurrentNodeToDefault( - {required SharedPreferences sharedPreferences, required Box nodes}) async { - final node = getZanoDefaultNode(nodes: nodes); - final nodeId = node?.key as int? ?? 0; - await sharedPreferences.setInt(PreferencesKey.currentZanoNodeIdKey, nodeId); -} - -Future addNanoNodeList({required Box nodes}) async { - final nodeList = await loadDefaultNanoNodes(); - for (var node in nodeList) { - if (nodes.values.firstWhereOrNull((element) => element.uriRaw == node.uriRaw) == null) { - await nodes.add(node); - } - } -} - Future addNanoPowNodeList({required Box nodes}) async { final nodeList = await loadDefaultNanoPowNodes(); for (var node in nodeList) { @@ -1474,23 +1241,13 @@ Future addNanoPowNodeList({required Box nodes}) async { } } -Future changeNanoCurrentNodeToDefault( - {required SharedPreferences sharedPreferences, required Box nodes}) async { - final node = getNanoDefaultNode(nodes: nodes); - final nodeId = node?.key as int? ?? 0; - - await sharedPreferences.setInt(PreferencesKey.currentNanoNodeIdKey, nodeId); +Node? getNanoDefaultPowNode({required Box nodes}) { + return nodes.values.firstWhereOrNull((Node node) => node.uriRaw == nanoDefaultPowNodeUri) ?? + nodes.values.firstWhereOrNull((node) => (node.type == WalletType.nano)); } -Future changeNanoCurrentPowNodeToDefault( - {required SharedPreferences sharedPreferences, required Box nodes}) async { - final node = getNanoDefaultPowNode(nodes: nodes); - final nodeId = node?.key as int? ?? 0; - await sharedPreferences.setInt(PreferencesKey.currentNanoPowNodeIdKey, nodeId); -} - -Future addPolygonNodeList({required Box nodes}) async { - final nodeList = await loadDefaultPolygonNodes(); +Future addWalletNodeList({required Box nodes, required WalletType type}) async { + final List nodeList = await loadDefaultNodes(type); for (var node in nodeList) { if (nodes.values.firstWhereOrNull((element) => element.uriRaw == node.uriRaw) == null) { await nodes.add(node); @@ -1498,74 +1255,6 @@ Future addPolygonNodeList({required Box nodes}) async { } } -Future changePolygonCurrentNodeToDefault( - {required SharedPreferences sharedPreferences, required Box nodes}) async { - final node = getPolygonDefaultNode(nodes: nodes); - final nodeId = node?.key as int? ?? 0; - - await sharedPreferences.setInt(PreferencesKey.currentPolygonNodeIdKey, nodeId); -} - -Future addSolanaNodeList({required Box nodes}) async { - final nodeList = await loadDefaultSolanaNodes(); - for (var node in nodeList) { - if (nodes.values.firstWhereOrNull((element) => element.uriRaw == node.uriRaw) == null) { - await nodes.add(node); - } - } -} - -Future changeSolanaCurrentNodeToDefault( - {required SharedPreferences sharedPreferences, required Box nodes}) async { - final node = getSolanaDefaultNode(nodes: nodes); - final nodeId = node?.key as int? ?? 0; - - await sharedPreferences.setInt(PreferencesKey.currentSolanaNodeIdKey, nodeId); -} - -Future addTronNodeList({required Box nodes}) async { - final nodeList = await loadDefaultTronNodes(); - for (var node in nodeList) { - if (nodes.values.firstWhereOrNull((element) => element.uriRaw == node.uriRaw) == null) { - await nodes.add(node); - } - } -} - -Future changeTronCurrentNodeToDefault( - {required SharedPreferences sharedPreferences, required Box nodes}) async { - final node = getTronDefaultNode(nodes: nodes); - final nodeId = node?.key as int? ?? 0; - - await sharedPreferences.setInt(PreferencesKey.currentTronNodeIdKey, nodeId); -} - -Future replaceTronDefaultNode({ - required SharedPreferences sharedPreferences, - required Box nodes, -}) async { - // Get the currently active node - final currentTronNodeId = sharedPreferences.getInt(PreferencesKey.currentTronNodeIdKey); - final currentTronNode = - nodes.values.firstWhereOrNull((Node node) => node.key == currentTronNodeId); - - //Confirm if this node is part of the default nodes from CakeWallet - final tronDefaultNodeList = [ - 'tron-rpc.publicnode.com:443', - 'api.trongrid.io', - ]; - bool needsToBeReplaced = - currentTronNode == null ? true : tronDefaultNodeList.contains(currentTronNode.uriRaw); - - // If it's a custom node, return. We don't want to switch users from their custom nodes - if (!needsToBeReplaced) { - return; - } - - // If it's not, we switch user to the new default node: NowNodes - await changeTronCurrentNodeToDefault(sharedPreferences: sharedPreferences, nodes: nodes); -} - Future removeMoneroWorld( {required SharedPreferences sharedPreferences, required Box nodes}) async { const cakeWalletMoneroNodeUriPattern = '.moneroworld.com'; @@ -1582,19 +1271,13 @@ Future removeMoneroWorld( }); if (needToReplaceCurrentMoneroNode) { - await changeMoneroCurrentNodeToDefault(sharedPreferences: sharedPreferences, nodes: nodes); + await _changeDefaultNode( + nodes: nodes, + sharedPreferences: sharedPreferences, + type: WalletType.monero, + newDefaultUri: newCakeWalletMoneroUri, + currentNodePreferenceKey: PreferencesKey.currentNodeIdKey, + trusted: true, + ); } } - -Future updateTronNodesWithNowNodes({ - required SharedPreferences sharedPreferences, - required Box nodes, -}) async { - final tronNowNodesUri = 'trx.nownodes.io'; - - if (nodes.values.any((node) => node.uriRaw == tronNowNodesUri)) return; - - await nodes.add(Node(uri: tronNowNodesUri, type: WalletType.tron)); - - await replaceTronDefaultNode(sharedPreferences: sharedPreferences, nodes: nodes); -} diff --git a/lib/entities/main_actions.dart b/lib/entities/main_actions.dart index 68f599718..2e633bce8 100644 --- a/lib/entities/main_actions.dart +++ b/lib/entities/main_actions.dart @@ -59,6 +59,7 @@ class MainActions { static MainActions sendAction = MainActions._( name: (context) => S.of(context).send, image: 'assets/images/upload.png', + isEnabled: (viewModel) => viewModel.canSend, onTap: (BuildContext context, DashboardViewModel viewModel) async { Navigator.pushNamed(context, Routes.send); }, diff --git a/lib/entities/node_list.dart b/lib/entities/node_list.dart index 5147aa614..bb489e715 100644 --- a/lib/entities/node_list.dart +++ b/lib/entities/node_list.dart @@ -4,111 +4,62 @@ import "package:yaml/yaml.dart"; import 'package:cw_core/node.dart'; import 'package:cw_core/wallet_type.dart'; -Future> loadDefaultNodes() async { - final nodesRaw = await rootBundle.loadString('assets/node_list.yml'); +Future> loadDefaultNodes(WalletType type) async { + String path; + switch (type) { + case WalletType.monero: + path = 'assets/node_list.yml'; + break; + case WalletType.bitcoin: + path = 'assets/bitcoin_electrum_server_list.yml'; + break; + case WalletType.litecoin: + path = 'assets/litecoin_electrum_server_list.yml'; + break; + case WalletType.haven: + path = 'assets/haven_node_list.yml'; + break; + case WalletType.ethereum: + path = 'assets/ethereum_server_list.yml'; + break; + case WalletType.nano: + path = 'assets/nano_node_list.yml'; + break; + case WalletType.bitcoinCash: + path = 'assets/bitcoin_cash_electrum_server_list.yml'; + break; + case WalletType.polygon: + path = 'assets/polygon_node_list.yml'; + break; + case WalletType.solana: + path = 'assets/solana_node_list.yml'; + break; + case WalletType.tron: + path = 'assets/tron_node_list.yml'; + break; + case WalletType.wownero: + path = 'assets/wownero_node_list.yml'; + break; + case WalletType.zano: + path = 'assets/zano_node_list.yml'; + break; + case WalletType.decred: + path = 'assets/decred_node_list.yml'; + break; + case WalletType.banano: + case WalletType.none: + path = ''; + break; + } + + final nodesRaw = await rootBundle.loadString(path); final loadedNodes = loadYaml(nodesRaw) as YamlList; final nodes = []; for (final raw in loadedNodes) { if (raw is Map) { final node = Node.fromMap(Map.from(raw)); - node.type = WalletType.monero; - nodes.add(node); - } - } - - return nodes; -} - -Future> loadBitcoinElectrumServerList() async { - final serverListRaw = await rootBundle.loadString('assets/bitcoin_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.bitcoin; - serverList.add(node); - } - } - - return serverList; -} - -Future> loadLitecoinElectrumServerList() async { - final serverListRaw = await rootBundle.loadString('assets/litecoin_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.litecoin; - serverList.add(node); - } - } - - return serverList; -} - -Future> loadDefaultHavenNodes() async { - final nodesRaw = await rootBundle.loadString('assets/haven_node_list.yml'); - final loadedNodes = loadYaml(nodesRaw) as YamlList; - final nodes = []; - - for (final raw in loadedNodes) { - if (raw is Map) { - final node = Node.fromMap(Map.from(raw)); - node.type = WalletType.haven; - nodes.add(node); - } - } - - return nodes; -} - -Future> loadDefaultEthereumNodes() async { - final nodesRaw = await rootBundle.loadString('assets/ethereum_server_list.yml'); - final loadedNodes = loadYaml(nodesRaw) as YamlList; - final nodes = []; - - for (final raw in loadedNodes) { - if (raw is Map) { - final node = Node.fromMap(Map.from(raw)); - node.type = WalletType.ethereum; - nodes.add(node); - } - } - - 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; - final nodes = []; - - for (final raw in loadedNodes) { - if (raw is Map) { - final node = Node.fromMap(Map.from(raw)); - node.type = WalletType.nano; + node.type = type; nodes.add(node); } } @@ -132,103 +83,19 @@ Future> loadDefaultNanoPowNodes() async { return nodes; } -Future> loadDefaultPolygonNodes() async { - final nodesRaw = await rootBundle.loadString('assets/polygon_node_list.yml'); - final loadedNodes = loadYaml(nodesRaw) as YamlList; - final nodes = []; - - for (final raw in loadedNodes) { - if (raw is Map) { - final node = Node.fromMap(Map.from(raw)); - - node.type = WalletType.polygon; - nodes.add(node); - } - } - - return nodes; -} - -Future> loadDefaultSolanaNodes() async { - final nodesRaw = await rootBundle.loadString('assets/solana_node_list.yml'); - final loadedNodes = loadYaml(nodesRaw) as YamlList; - final nodes = []; - - for (final raw in loadedNodes) { - if (raw is Map) { - final node = Node.fromMap(Map.from(raw)); - - node.type = WalletType.solana; - nodes.add(node); - } - } - - return nodes; -} - -Future> loadDefaultTronNodes() async { - final nodesRaw = await rootBundle.loadString('assets/tron_node_list.yml'); - final loadedNodes = loadYaml(nodesRaw) as YamlList; - final nodes = []; - - for (final raw in loadedNodes) { - if (raw is Map) { - final node = Node.fromMap(Map.from(raw)); - - node.type = WalletType.tron; - nodes.add(node); - } - } - - return nodes; -} - -Future> loadDefaultWowneroNodes() async { - final nodesRaw = await rootBundle.loadString('assets/wownero_node_list.yml'); - final loadedNodes = loadYaml(nodesRaw) as YamlList; - final nodes = []; - - for (final raw in loadedNodes) { - if (raw is Map) { - final node = Node.fromMap(Map.from(raw)); - - node.type = WalletType.wownero; - nodes.add(node); - } - } - - return nodes; -} - -Future> loadDefaultZanoNodes() async { - final nodesRaw = await rootBundle.loadString('assets/zano_node_list.yml'); - final loadedNodes = loadYaml(nodesRaw) as YamlList; - final nodes = []; - - for (final raw in loadedNodes) { - if (raw is Map) { - final node = Node.fromMap(Map.from(raw)); - - node.type = WalletType.zano; - nodes.add(node); - } - } - - return nodes; -} - Future resetToDefault(Box nodeSource) async { - final moneroNodes = await loadDefaultNodes(); - final bitcoinElectrumServerList = await loadBitcoinElectrumServerList(); - final litecoinElectrumServerList = await loadLitecoinElectrumServerList(); - final bitcoinCashElectrumServerList = await loadBitcoinCashElectrumServerList(); - final havenNodes = await loadDefaultHavenNodes(); - final ethereumNodes = await loadDefaultEthereumNodes(); - final nanoNodes = await loadDefaultNanoNodes(); - final polygonNodes = await loadDefaultPolygonNodes(); - final solanaNodes = await loadDefaultSolanaNodes(); - final tronNodes = await loadDefaultTronNodes(); - final zanoNodes = await loadDefaultZanoNodes(); + final moneroNodes = await loadDefaultNodes(WalletType.monero); + final bitcoinElectrumServerList = await loadDefaultNodes(WalletType.bitcoin); + final litecoinElectrumServerList = await loadDefaultNodes(WalletType.litecoin); + final bitcoinCashElectrumServerList = await loadDefaultNodes(WalletType.bitcoinCash); + final havenNodes = await loadDefaultNodes(WalletType.haven); + final ethereumNodes = await loadDefaultNodes(WalletType.ethereum); + final nanoNodes = await loadDefaultNodes(WalletType.nano); + final polygonNodes = await loadDefaultNodes(WalletType.polygon); + final solanaNodes = await loadDefaultNodes(WalletType.solana); + final tronNodes = await loadDefaultNodes(WalletType.tron); + final decredNodes = await loadDefaultNodes(WalletType.decred); + final zanoNodes = await loadDefaultNodes(WalletType.zano); final nodes = moneroNodes + bitcoinElectrumServerList + @@ -238,7 +105,10 @@ Future resetToDefault(Box nodeSource) async { bitcoinCashElectrumServerList + nanoNodes + polygonNodes + - solanaNodes + tronNodes + zanoNodes; + solanaNodes + + tronNodes + + zanoNodes + + decredNodes; await nodeSource.clear(); await nodeSource.addAll(nodes); diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index 7fbdb645a..9e889ff46 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -10,6 +10,7 @@ class PreferencesKey { static const currentPolygonNodeIdKey = 'current_node_id_matic'; static const currentNanoNodeIdKey = 'current_node_id_nano'; static const currentNanoPowNodeIdKey = 'current_node_id_nano_pow'; + static const currentDecredNodeIdKey = 'current_node_id_decred'; static const currentBananoNodeIdKey = 'current_node_id_banano'; static const currentBananoPowNodeIdKey = 'current_node_id_banano_pow'; static const currentFiatCurrencyKey = 'current_fiat_currency'; @@ -48,6 +49,7 @@ class PreferencesKey { static const bitcoinCashTransactionPriority = 'current_fee_priority_bitcoin_cash'; static const zanoTransactionPriority = 'current_fee_priority_zano'; static const wowneroTransactionPriority = 'current_fee_priority_wownero'; + static const decredTransactionPriority = 'current_fee_priority_decred'; static const customBitcoinFeeRate = 'custom_electrum_fee_rate'; static const silentPaymentsCardDisplay = 'silentPaymentsCardDisplay'; static const silentPaymentsAlwaysScan = 'silentPaymentsAlwaysScan'; @@ -81,6 +83,7 @@ class PreferencesKey { static const lookupsENS = 'looks_up_ens'; static const lookupsWellKnown = 'looks_up_well_known'; static const showCameraConsent = 'show_camera_consent'; + static const showDecredInfoCard = 'show_decred_info_card'; static String moneroWalletUpdateV1Key(String name) => '${PreferencesKey.moneroWalletPasswordUpdateV1Base}_${name}'; diff --git a/lib/entities/priority_for_wallet_type.dart b/lib/entities/priority_for_wallet_type.dart index bbd98d17d..f7165500f 100644 --- a/lib/entities/priority_for_wallet_type.dart +++ b/lib/entities/priority_for_wallet_type.dart @@ -6,6 +6,7 @@ import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/wownero/wownero.dart'; import 'package:cake_wallet/zano/zano.dart'; +import 'package:cake_wallet/decred/decred.dart'; import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/wallet_type.dart'; @@ -35,6 +36,8 @@ List priorityForWalletType(WalletType type) { return []; case WalletType.zano: return zano!.getTransactionPriorities(); + case WalletType.decred: + return decred!.getTransactionPriorities(); default: return []; } diff --git a/lib/entities/provider_types.dart b/lib/entities/provider_types.dart index 5888970b0..2d2a98379 100644 --- a/lib/entities/provider_types.dart +++ b/lib/entities/provider_types.dart @@ -82,6 +82,7 @@ class ProvidersHelper { ProviderType.moonpay, ProviderType.kriptonim ]; + case WalletType.decred: case WalletType.none: case WalletType.haven: case WalletType.zano: @@ -113,6 +114,7 @@ class ProvidersHelper { ]; case WalletType.monero: return [ProviderType.dfx]; + case WalletType.decred: case WalletType.nano: case WalletType.banano: case WalletType.none: diff --git a/lib/main.dart b/lib/main.dart index 1eedfa6a9..2bf0f269d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -215,7 +215,7 @@ Future initializeAppConfigs() async { secureStorage: secureStorage, anonpayInvoiceInfo: anonpayInvoiceInfo, havenSeedStore: havenSeedStore, - initialMigrationVersion: 48, + initialMigrationVersion: 49, ); } diff --git a/lib/reactions/bip39_wallet_utils.dart b/lib/reactions/bip39_wallet_utils.dart index 8b99331ce..0c58bc76f 100644 --- a/lib/reactions/bip39_wallet_utils.dart +++ b/lib/reactions/bip39_wallet_utils.dart @@ -16,6 +16,7 @@ bool isBIP39Wallet(WalletType walletType) { case WalletType.wownero: case WalletType.haven: case WalletType.zano: + case WalletType.decred: case WalletType.none: return false; } diff --git a/lib/reactions/check_connection.dart b/lib/reactions/check_connection.dart index d60037543..1e8fa88fa 100644 --- a/lib/reactions/check_connection.dart +++ b/lib/reactions/check_connection.dart @@ -18,6 +18,9 @@ void startCheckConnectionReaction(WalletBase wallet, SettingsStore settingsStore if (wallet.type == WalletType.bitcoin && wallet.syncStatus is SyncingSyncStatus) { return; } + if (wallet.type == WalletType.decred && wallet.syncStatus is ProcessingSyncStatus) { + return; + } try { final connectivityResult = await (Connectivity().checkConnectivity()); diff --git a/lib/reactions/fiat_rate_update.dart b/lib/reactions/fiat_rate_update.dart index de3dea4a2..2ddb1c6f2 100644 --- a/lib/reactions/fiat_rate_update.dart +++ b/lib/reactions/fiat_rate_update.dart @@ -10,7 +10,6 @@ import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/tron/tron.dart'; import 'package:cw_core/crypto_currency.dart'; -import 'package:cw_core/erc20_token.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:mobx/mobx.dart'; diff --git a/lib/reactions/on_current_wallet_change.dart b/lib/reactions/on_current_wallet_change.dart index a6475571d..513c97c4e 100644 --- a/lib/reactions/on_current_wallet_change.dart +++ b/lib/reactions/on_current_wallet_change.dart @@ -74,7 +74,8 @@ void startCurrentWalletChangeReaction( wallet.type == WalletType.wownero || wallet.type == WalletType.bitcoin || wallet.type == WalletType.litecoin || - wallet.type == WalletType.bitcoinCash) { + wallet.type == WalletType.bitcoinCash || + wallet.type == WalletType.decred) { _setAutoGenerateSubaddressStatus(wallet, settingsStore); } diff --git a/lib/reactions/on_wallet_sync_status_change.dart b/lib/reactions/on_wallet_sync_status_change.dart index 072602e5f..fc618dabd 100644 --- a/lib/reactions/on_wallet_sync_status_change.dart +++ b/lib/reactions/on_wallet_sync_status_change.dart @@ -25,7 +25,7 @@ void startWalletSyncStatusChangeReaction( await updateHavenRate(fiatConversionStore); } } - if (status is SyncingSyncStatus) { + if (status is SyncingSyncStatus || status is ProcessingSyncStatus) { await WakelockPlus.enable(); } if (status is SyncedSyncStatus || status is FailedSyncStatus) { 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 6804467f7..fed9cb6fe 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 @@ -47,6 +47,7 @@ class _DesktopWalletSelectionDropDownState extends State Image.asset( @@ -181,6 +182,8 @@ class _DesktopWalletSelectionDropDownState extends State launchUrl( - Uri.parse("https://docs.cakewallet.com/cryptos/litecoin/#mweb"), - mode: LaunchMode.externalApplication, - ), - child: Text( - S.of(context).learn_more, - style: TextStyle( - fontSize: 12, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: - Theme.of(context).extension()!.labelTextColor, - height: 1, - ), - softWrap: true, - ), - ), - SizedBox(height: 8), - Row( - children: [ - Expanded( - child: ElevatedButton( - onPressed: () => _dismissMweb(context), - style: ElevatedButton.styleFrom( - backgroundColor: Theme.of(context).primaryColor, - ), - child: Text( - S.of(context).litecoin_mweb_dismiss, - style: TextStyle(color: Colors.white), - ), - ), - ), - const SizedBox(width: 8), - Expanded( - child: ElevatedButton( - onPressed: () => _enableMweb(context), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.white, - foregroundColor: Colors.black, - ), - child: Text( - S.of(context).enable, - maxLines: 1, - ), - ), - ), - ], - ), - ], - ), - icon: Container( - decoration: BoxDecoration( - color: Colors.white, - shape: BoxShape.circle, + description: S.of(context).litecoin_mweb_description, + leftButtonTitle: S.of(context).litecoin_mweb_dismiss, + rightButtonTitle: S.of(context).enable, + image: 'assets/images/mweb_logo.png', + leftButtonAction: () => _dismissMweb(context), + rightButtonAction: () => _enableMweb(context), + hintWidget: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => launchUrl( + Uri.parse("https://docs.cakewallet.com/cryptos/litecoin/#mweb"), + mode: LaunchMode.externalApplication, ), - child: ImageIcon( - AssetImage('assets/images/mweb_logo.png'), - color: Color.fromARGB(255, 11, 70, 129), - size: 40, + child: Text( + S.of(context).learn_more, + style: TextStyle( + fontSize: 12, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: Theme.of(context).extension()!.labelTextColor, + height: 1, + ), + softWrap: true, ), ), ), ), ], + if (dashboardViewModel.showDecredInfoCard) ...[ + SizedBox(height: 10), + Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), + child: InfoCard( + title: S.of(context).decred_info_title, + description: S.of(context).decred_info_card_details, + image: 'assets/images/dcr_icon.png', + leftButtonTitle: S.of(context).litecoin_mweb_dismiss, + rightButtonTitle: S.of(context).learn_more, + leftButtonAction: () => dashboardViewModel.dismissDecredInfoCard(), + rightButtonAction: () => launchUrl(Uri.parse("https://docs.cakewallet.com/cryptos/decred/#spv-sync")), + ), + ), + ], ], ); }), diff --git a/lib/src/screens/dashboard/pages/navigation_dock.dart b/lib/src/screens/dashboard/pages/navigation_dock.dart index 52b39cdf7..4eda169d3 100644 --- a/lib/src/screens/dashboard/pages/navigation_dock.dart +++ b/lib/src/screens/dashboard/pages/navigation_dock.dart @@ -75,8 +75,10 @@ class NavigationDock extends StatelessWidget { .labelTextColor, ), title: action.name(context), - onClick: () async => - await action.onTap(context, dashboardViewModel), + onClick: (action.isEnabled?.call(dashboardViewModel) ?? true) + ? () async => + await action.onTap(context, dashboardViewModel) + : null, textColor: action.isEnabled?.call(dashboardViewModel) ?? true ? null : Theme.of(context) diff --git a/lib/src/screens/dashboard/widgets/info_card.dart b/lib/src/screens/dashboard/widgets/info_card.dart new file mode 100644 index 000000000..1bf8a11bf --- /dev/null +++ b/lib/src/screens/dashboard/widgets/info_card.dart @@ -0,0 +1,88 @@ +import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; +import 'package:cake_wallet/src/widgets/dashboard_card_widget.dart'; +import 'package:flutter/material.dart'; + +class InfoCard extends StatelessWidget { + final String leftButtonTitle; + final String rightButtonTitle; + final String title; + final String description; + final String image; + + final Function() leftButtonAction; + final Function() rightButtonAction; + + final Widget? hintWidget; + + const InfoCard({ + Key? key, + required this.title, + required this.description, + required this.leftButtonTitle, + required this.rightButtonTitle, + required this.leftButtonAction, + required this.rightButtonAction, + required this.image, + this.hintWidget, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return DashBoardRoundedCardWidget( + marginH: 0, + marginV: 0, + customBorder: 30, + title: title, + subTitle: description, + hint: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (hintWidget != null) hintWidget!, + if (hintWidget != null) SizedBox(height: 8), + Row( + children: [ + Expanded( + child: ElevatedButton( + onPressed: leftButtonAction, + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).primaryColor, + ), + child: Text( + leftButtonTitle, + style: TextStyle(color: Colors.white), + ), + ), + ), + const SizedBox(width: 8), + Expanded( + child: ElevatedButton( + onPressed: rightButtonAction, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + foregroundColor: Colors.black, + ), + child: Text( + rightButtonTitle, + maxLines: 1, + ), + ), + ), + ], + ), + ], + ), + onTap: () => {}, + icon: Container( + decoration: BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + child: CakeImageWidget( + imageUrl: image, + height: 40, + width: 40, + ), + ), + ); + } +} diff --git a/lib/src/screens/dashboard/widgets/menu_widget.dart b/lib/src/screens/dashboard/widgets/menu_widget.dart index 16e0f88aa..dbf55d046 100644 --- a/lib/src/screens/dashboard/widgets/menu_widget.dart +++ b/lib/src/screens/dashboard/widgets/menu_widget.dart @@ -38,7 +38,8 @@ class MenuWidgetState extends State { this.solanaIcon = Image.asset('assets/images/sol_icon.png'), this.tronIcon = Image.asset('assets/images/trx_icon.png'), this.wowneroIcon = Image.asset('assets/images/wownero_icon.png'), - this.zanoIcon = Image.asset('assets/images/zano_icon.png'); + this.zanoIcon = Image.asset('assets/images/zano_icon.png'), + this.decredIcon = Image.asset('assets/images/decred_menu.png'); final largeScreen = 731; @@ -64,6 +65,7 @@ class MenuWidgetState extends State { Image tronIcon; Image wowneroIcon; Image zanoIcon; + Image decredIcon; @override void initState() { @@ -250,6 +252,8 @@ class MenuWidgetState extends State { return wowneroIcon; case WalletType.zano: return zanoIcon; + case WalletType.decred: + return decredIcon; default: throw Exception('No icon for ${type.toString()}'); } diff --git a/lib/src/screens/dashboard/widgets/sync_indicator.dart b/lib/src/screens/dashboard/widgets/sync_indicator.dart index 27b3d0109..860747d1e 100644 --- a/lib/src/screens/dashboard/widgets/sync_indicator.dart +++ b/lib/src/screens/dashboard/widgets/sync_indicator.dart @@ -22,8 +22,8 @@ class SyncIndicator extends StatelessWidget { builder: (_) { final syncIndicatorWidth = 237.0; final status = dashboardViewModel.status; - final statusText = status != null ? syncStatusTitle(status) : ''; - final progress = status != null ? status.progress() : 0.0; + final statusText = syncStatusTitle(status); + final progress = status.progress(); final indicatorOffset = progress * syncIndicatorWidth; final indicatorWidth = progress < 1 ? indicatorOffset > 0 ? indicatorOffset : 0.0 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 e5853570e..f8901918f 100644 --- a/lib/src/screens/new_wallet/advanced_privacy_settings_page.dart +++ b/lib/src/screens/new_wallet/advanced_privacy_settings_page.dart @@ -274,7 +274,8 @@ class _AdvancedPrivacySettingsBodyState extends State<_AdvancedPrivacySettingsBo ], ); }), - if (widget.privacySettingsViewModel.type == WalletType.bitcoin) + if (widget.privacySettingsViewModel.type == WalletType.bitcoin || + widget.privacySettingsViewModel.type == WalletType.decred) Builder(builder: (_) { final val = testnetValue ?? false; return SettingsSwitcherCell( @@ -301,7 +302,9 @@ class _AdvancedPrivacySettingsBodyState extends State<_AdvancedPrivacySettingsBo widget.nodeViewModel.save(); } - if (testnetValue == true) { + if (testnetValue == true && + widget.privacySettingsViewModel.type == + WalletType.bitcoin) { // TODO: add type (mainnet/testnet) to Node class so when switching wallets the node can be switched to a matching type // Currently this is so you can create a working testnet wallet but you need to keep switching back the node if you use multiple wallets at once widget.nodeViewModel.address = publicBitcoinTestnetElectrumAddress; 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 53c34f302..fc6ac07e0 100644 --- a/lib/src/screens/nodes/node_create_or_edit_page.dart +++ b/lib/src/screens/nodes/node_create_or_edit_page.dart @@ -5,6 +5,7 @@ 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:cw_core/node.dart'; +import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:mobx/mobx.dart'; @@ -15,7 +16,7 @@ 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 NodeCreateOrEditPage extends BasePage { - NodeCreateOrEditPage({required this.nodeCreateOrEditViewModel,this.editingNode, this.isSelected}) + NodeCreateOrEditPage({required this.nodeCreateOrEditViewModel,this.editingNode, this.isSelected, this.type}) : _formKey = GlobalKey(), _addressController = TextEditingController(), _pathController = TextEditingController(), @@ -86,6 +87,7 @@ class NodeCreateOrEditPage extends BasePage { final NodeCreateOrEditViewModel nodeCreateOrEditViewModel; final Node? editingNode; final bool? isSelected; + final WalletType? type; @override Widget body(BuildContext context) { @@ -130,6 +132,7 @@ class NodeCreateOrEditPage extends BasePage { formKey: _formKey, nodeViewModel: nodeCreateOrEditViewModel, editingNode: editingNode, + type: type, ), bottomSectionPadding: EdgeInsets.only(bottom: 24), bottomSection: Observer( diff --git a/lib/src/screens/nodes/widgets/node_form.dart b/lib/src/screens/nodes/widgets/node_form.dart index 22a38f423..eeda073af 100644 --- a/lib/src/screens/nodes/widgets/node_form.dart +++ b/lib/src/screens/nodes/widgets/node_form.dart @@ -5,6 +5,7 @@ import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; import 'package:cake_wallet/src/widgets/standard_checkbox.dart'; import 'package:cake_wallet/view_model/node_list/node_create_or_edit_view_model.dart'; import 'package:cw_core/node.dart'; +import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:cake_wallet/generated/i18n.dart'; @@ -15,6 +16,7 @@ class NodeForm extends StatelessWidget { required this.nodeViewModel, required this.formKey, this.editingNode, + this.type, }) : _addressController = TextEditingController(text: editingNode?.uri.host.toString()), _pathController = TextEditingController(text: editingNode?.path.toString()), _portController = TextEditingController(text: editingNode?.uri.port.toString()), @@ -76,6 +78,7 @@ class NodeForm extends StatelessWidget { final NodeCreateOrEditViewModel nodeViewModel; final GlobalKey formKey; final Node? editingNode; + final WalletType? type; final TextEditingController _addressController; final TextEditingController _pathController; @@ -96,7 +99,7 @@ class NodeForm extends StatelessWidget { child: BaseTextFormField( controller: _addressController, hintText: S.of(context).node_address, - validator: NodeAddressValidator(), + validator: type == WalletType.decred ? NodeAddressValidatorDecredBlankException() : NodeAddressValidator(), ), ) ], diff --git a/lib/src/screens/rescan/rescan_page.dart b/lib/src/screens/rescan/rescan_page.dart index 2c1c213c1..63f13c5df 100644 --- a/lib/src/screens/rescan/rescan_page.dart +++ b/lib/src/screens/rescan/rescan_page.dart @@ -9,41 +9,47 @@ import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/widgets/blockchain_height_widget.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cw_core/wallet_type.dart'; class RescanPage extends BasePage { RescanPage(this._rescanViewModel) : _blockchainHeightWidgetKey = GlobalKey(); @override - String get title => - _rescanViewModel.isSilentPaymentsScan ? S.current.silent_payments_scanning : S.current.rescan; + String get title => _rescanViewModel.isSilentPaymentsScan + ? S.current.silent_payments_scanning + : S.current.rescan; final GlobalKey _blockchainHeightWidgetKey; final RescanViewModel _rescanViewModel; @override Widget body(BuildContext context) { - return GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () => FocusScope.of(context).unfocus(), - child: Padding( + Widget child; + if (_rescanViewModel.wallet.type != WalletType.decred) { + child = Padding( padding: EdgeInsets.only(left: 24, right: 24, bottom: 24), - child: Column(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + child: + Column(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Observer( builder: (_) => BlockchainHeightWidget( key: _blockchainHeightWidgetKey, - onHeightOrDateEntered: (value) => _rescanViewModel.isButtonEnabled = value, + onHeightOrDateEntered: (value) => + _rescanViewModel.isButtonEnabled = value, isSilentPaymentsScan: _rescanViewModel.isSilentPaymentsScan, isMwebScan: _rescanViewModel.isMwebScan, doSingleScan: _rescanViewModel.doSingleScan, - hasDatePicker: !_rescanViewModel.isMwebScan,// disable date picker for mweb for now - toggleSingleScan: () => - _rescanViewModel.doSingleScan = !_rescanViewModel.doSingleScan, + hasDatePicker: !_rescanViewModel + .isMwebScan, // disable date picker for mweb for now + toggleSingleScan: () => _rescanViewModel.doSingleScan = + !_rescanViewModel.doSingleScan, walletType: _rescanViewModel.wallet.type, - bitcoinMempoolAPIEnabled: _rescanViewModel.isBitcoinMempoolAPIEnabled, + bitcoinMempoolAPIEnabled: + _rescanViewModel.isBitcoinMempoolAPIEnabled, )), Observer( builder: (_) => LoadingPrimaryButton( - isLoading: _rescanViewModel.state == RescanWalletState.rescaning, + isLoading: + _rescanViewModel.state == RescanWalletState.rescaning, text: S.of(context).rescan, onPressed: () async { if (_rescanViewModel.isSilentPaymentsScan) { @@ -51,7 +57,8 @@ class RescanPage extends BasePage { } _rescanViewModel.rescanCurrentWallet( - restoreHeight: _blockchainHeightWidgetKey.currentState!.height); + restoreHeight: + _blockchainHeightWidgetKey.currentState!.height); Navigator.of(context).pop(); }, @@ -60,7 +67,35 @@ class RescanPage extends BasePage { isDisabled: !_rescanViewModel.isButtonEnabled, )) ]), - ), + ); + } else { + child = Center( + child: Padding( + padding: EdgeInsets.only(left: 24, right: 24, bottom: 24), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Spacer(), + Observer( + builder: (_) => LoadingPrimaryButton( + isLoading: _rescanViewModel.state == + RescanWalletState.rescaning, + text: S.of(context).rescan, + onPressed: () async { + await _rescanViewModel.rescanCurrentWallet( + restoreHeight: 0); + Navigator.of(context).pop(); + }, + color: Theme.of(context).primaryColor, + textColor: Colors.white, + )) + ]), + )); + } + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => FocusScope.of(context).unfocus(), + child: child, ); } @@ -70,14 +105,16 @@ class RescanPage extends BasePage { Navigator.of(context).pop(); final needsToSwitch = - await bitcoin!.getNodeIsElectrsSPEnabled(_rescanViewModel.wallet) == false; + await bitcoin!.getNodeIsElectrsSPEnabled(_rescanViewModel.wallet) == + false; if (needsToSwitch) { return showPopUp( context: navigatorKey.currentState!.context, builder: (BuildContext _dialogContext) => AlertWithTwoActions( alertTitle: S.of(_dialogContext).change_current_node_title, - alertContent: S.of(_dialogContext).confirm_silent_payments_switch_node, + alertContent: + S.of(_dialogContext).confirm_silent_payments_switch_node, rightButtonText: S.of(_dialogContext).confirm, leftButtonText: S.of(_dialogContext).cancel, actionRightButton: () async { 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 3dc312702..b95fcb4c6 100644 --- a/lib/src/screens/restore/wallet_restore_from_keys_form.dart +++ b/lib/src/screens/restore/wallet_restore_from_keys_form.dart @@ -15,6 +15,7 @@ class WalletRestoreFromKeysForm extends StatefulWidget { WalletRestoreFromKeysForm({ required this.walletRestoreViewModel, required this.onPrivateKeyChange, + required this.onViewKeyEntered, required this.displayPrivateKeyField, required this.onHeightOrDateEntered, required this.displayWalletPassword, @@ -27,6 +28,7 @@ class WalletRestoreFromKeysForm extends StatefulWidget { final Function(bool) onHeightOrDateEntered; final WalletRestoreViewModel walletRestoreViewModel; final void Function(String)? onPrivateKeyChange; + final void Function(bool)? onViewKeyEntered; final bool displayPrivateKeyField; final bool displayWalletPassword; final RestoredWallet? restoredWallet; @@ -97,6 +99,10 @@ class WalletRestoreFromKeysFormState extends State { blockchainHeightKey.currentState?.restoreHeightController.text = widget.restoredWallet!.height.toString(); } }); + + viewKeyController.addListener(() { + widget.onViewKeyEntered?.call(viewKeyController.text.isNotEmpty); + }); } @override @@ -187,6 +193,19 @@ class WalletRestoreFromKeysFormState extends State { } Widget _restoreFromKeysFormFields() { + // Decred can only restore a view only wallet with an account pubkey. Other + // fields are not used. + if (widget.walletRestoreViewModel.type == WalletType.decred) { + return Column( + children: [ + BaseTextFormField( + controller: viewKeyController, + hintText: S.of(context).view_key_public, + maxLines: null, + )], + ); + } + 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" diff --git a/lib/src/screens/restore/wallet_restore_page.dart b/lib/src/screens/restore/wallet_restore_page.dart index 31f02617d..ce8595ba4 100644 --- a/lib/src/screens/restore/wallet_restore_page.dart +++ b/lib/src/screens/restore/wallet_restore_page.dart @@ -168,14 +168,16 @@ class WalletRestorePage extends BasePage { credentials['name'] = walletRestoreFromKeysFormKey.currentState!.nameTextEditingController.text; } else { - credentials['address'] = walletRestoreFromKeysFormKey.currentState!.addressController.text; - credentials['viewKey'] = walletRestoreFromKeysFormKey.currentState!.viewKeyController.text; - credentials['spendKey'] = - walletRestoreFromKeysFormKey.currentState!.spendKeyController.text; - credentials['height'] = - walletRestoreFromKeysFormKey.currentState!.blockchainHeightKey.currentState!.height; credentials['name'] = walletRestoreFromKeysFormKey.currentState!.nameTextEditingController.text; + credentials['viewKey'] = walletRestoreFromKeysFormKey.currentState!.viewKeyController.text; + if (walletRestoreViewModel.type != WalletType.decred) { + credentials['address'] = walletRestoreFromKeysFormKey.currentState!.addressController.text; + credentials['spendKey'] = + walletRestoreFromKeysFormKey.currentState!.spendKeyController.text; + credentials['height'] = + walletRestoreFromKeysFormKey.currentState!.blockchainHeightKey.currentState!.height; + } } } @@ -467,6 +469,11 @@ class _WalletRestorePageBodyState extends State<_WalletRestorePageBody> widget.walletRestoreViewModel.isButtonEnabled = _isValidSeedKey(); } }, + onViewKeyEntered: (bool entered) { + if (walletRestoreViewModel.type == WalletType.decred) { + walletRestoreViewModel.isButtonEnabled = entered; + } + }, onPasswordChange: (String password) => widget.walletRestoreViewModel.walletPassword = password, onRepeatedPasswordChange: (String repeatedPassword) => @@ -538,13 +545,19 @@ class _WalletRestorePageBodyState extends State<_WalletRestorePageBody> // bip39: final validBip39SeedLengths = [12, 18, 24]; - final nonBip39WalletTypes = [WalletType.monero, WalletType.wownero, WalletType.haven]; + final nonBip39WalletTypes = [WalletType.monero, WalletType.wownero, WalletType.haven, WalletType.decred]; // if it's a bip39 wallet and the length is not valid return false if (!nonBip39WalletTypes.contains(walletRestoreViewModel.type) && !(validBip39SeedLengths.contains(seedWords.length))) { return false; } + if ((walletRestoreViewModel.type == WalletType.decred) && + seedWords.length != + WalletRestoreViewModelBase.decredSeedMnemonicLength) { + return false; + } + final words = walletRestoreFromSeedFormKey.currentState!.seedWidgetStateKey.currentState!.words.toSet(); return seedWords.toSet().difference(words).toSet().isEmpty; diff --git a/lib/src/screens/wallet_keys/wallet_keys_page.dart b/lib/src/screens/wallet_keys/wallet_keys_page.dart index be1972106..29a1bfb6f 100644 --- a/lib/src/screens/wallet_keys/wallet_keys_page.dart +++ b/lib/src/screens/wallet_keys/wallet_keys_page.dart @@ -72,6 +72,10 @@ class _WalletKeysPageBodyState extends State late bool showLegacySeedTab; late bool isLegacySeedOnly; + bool get _hasSeeds => + widget.walletKeysViewModel.legacySeedSplit.length > 10 || + widget.walletKeysViewModel.seedSplit.length > 10; + @override void initState() { super.initState(); @@ -160,11 +164,10 @@ class _WalletKeysPageBodyState extends State Widget _buildSeedTab(BuildContext context, bool isLegacySeed) { return Column( children: [ - if (isLegacySeedOnly || isLegacySeed) - ...[ - _buildHeightBox(), - const SizedBox(height: 20), - ], + if (isLegacySeedOnly || isLegacySeed) ...[ + _buildHeightBox(), + const SizedBox(height: 20), + ], (_buildPassphraseBox() ?? Container()), if (widget.walletKeysViewModel.passphrase.isNotEmpty) const SizedBox(height: 20), Expanded( @@ -175,13 +178,14 @@ class _WalletKeysPageBodyState extends State ), ), const SizedBox(height: 10), - _buildBottomActionPanel( - titleForClipboard: S.of(context).wallet_seed.toLowerCase(), - dataToCopy: isLegacySeed - ? widget.walletKeysViewModel.legacySeed - : widget.walletKeysViewModel.seed, - onShowQR: () async => _showQR(context), - ), + if (_hasSeeds) + _buildBottomActionPanel( + titleForClipboard: S.of(context).wallet_seed.toLowerCase(), + dataToCopy: isLegacySeed + ? widget.walletKeysViewModel.legacySeed + : widget.walletKeysViewModel.seed, + onShowQR: () async => _showQR(context), + ), ], ); } @@ -326,7 +330,7 @@ class _WalletKeysPageBodyState extends State ), ); } - + Widget _buildBottomActionPanel({ required String titleForClipboard, required String dataToCopy, diff --git a/lib/src/screens/wallet_list/wallet_list_page.dart b/lib/src/screens/wallet_list/wallet_list_page.dart index 569dce958..62f79bdc4 100644 --- a/lib/src/screens/wallet_list/wallet_list_page.dart +++ b/lib/src/screens/wallet_list/wallet_list_page.dart @@ -122,6 +122,7 @@ class WalletListBodyState extends State { final bitcoinIcon = Image.asset('assets/images/bitcoin.png', height: 24, width: 24); final tBitcoinIcon = Image.asset('assets/images/tbtc.png', height: 24, width: 24); final litecoinIcon = Image.asset('assets/images/litecoin_icon.png', height: 24, width: 24); + final decredIcon = Image.asset('assets/images/decred_icon.png', height: 24, width: 24); 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); @@ -136,6 +137,8 @@ class WalletListBodyState extends State { final double tileHeight = 60; Flushbar? _progressBar; + bool _loadingWallet = false; + @override Widget build(BuildContext context) { final newWalletImage = Image.asset('assets/images/new_wallet.png', @@ -480,6 +483,10 @@ class WalletListBodyState extends State { } Future _loadWallet(WalletListItem wallet) async { + if (_loadingWallet) return; + + _loadingWallet = true; + if (SettingsStoreBase.walletPasswordDirectInput) { Navigator.of(context).pushNamed(Routes.walletUnlockLoadable, arguments: WalletUnlockArguments( @@ -492,13 +499,17 @@ class WalletListBodyState extends State { }, walletName: wallet.name, walletType: wallet.type)); + _loadingWallet = false; return; } await widget.authService.authenticateAction( context, onAuthSuccess: (isAuthenticatedSuccessfully) async { - if (!isAuthenticatedSuccessfully) return; + if (!isAuthenticatedSuccessfully) { + _loadingWallet = false; + return; + } try { final requireHardwareWalletConnection = widget.walletListViewModel @@ -555,6 +566,8 @@ class WalletListBodyState extends State { .of(context) .wallet_list_failed_to_load(wallet.name, e.toString())); } + } finally { + _loadingWallet = false; } }, conditionToDetermineIfToUse2FA: diff --git a/lib/src/widgets/blockchain_height_widget.dart b/lib/src/widgets/blockchain_height_widget.dart index 650ee684d..605ae4484 100644 --- a/lib/src/widgets/blockchain_height_widget.dart +++ b/lib/src/widgets/blockchain_height_widget.dart @@ -9,6 +9,9 @@ import 'package:intl/intl.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; +import 'package:cake_wallet/decred/decred.dart'; +import 'package:cw_core/wallet_type.dart'; + class BlockchainHeightWidget extends StatefulWidget { BlockchainHeightWidget({ @@ -183,7 +186,9 @@ class BlockchainHeightState extends State { bitcoinMempoolAPIEnabled: await widget.bitcoinMempoolAPIEnabled, ); } else { - if (widget.walletType == WalletType.monero) { + if (widget.walletType == WalletType.decred) { + height = decred!.heightByDate(date); + } else if (widget.walletType == WalletType.monero) { height = monero!.getHeightByDate(date: date); } else { assert(widget.walletType == WalletType.wownero, diff --git a/lib/src/widgets/seed_widget.dart b/lib/src/widgets/seed_widget.dart index 7747c5fb6..9b3726afb 100644 --- a/lib/src/widgets/seed_widget.dart +++ b/lib/src/widgets/seed_widget.dart @@ -65,6 +65,9 @@ class SeedWidgetState extends State { }); widget.onSeedChange?.call(text); }); + Future.delayed(Duration.zero, () { + widget.onSeedChange?.call(text); + }); } void changeSeedLanguage(String language) { diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index ef9ab94a4..d43550806 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/decred/decred.dart'; import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart'; import 'package:cake_wallet/core/secure_storage.dart'; import 'package:cake_wallet/di.dart'; @@ -139,6 +140,7 @@ abstract class SettingsStoreBase with Store { TransactionPriority? initialPolygonTransactionPriority, TransactionPriority? initialBitcoinCashTransactionPriority, TransactionPriority? initialZanoTransactionPriority, + TransactionPriority? initialDecredTransactionPriority, Country? initialCakePayCountry}) : nodes = ObservableMap.of(nodes), powNodes = ObservableMap.of(powNodes), @@ -225,6 +227,9 @@ abstract class SettingsStoreBase with Store { if (initialZanoTransactionPriority != null) { priority[WalletType.zano] = initialZanoTransactionPriority; } + if (initialDecredTransactionPriority != null) { + priority[WalletType.decred] = initialDecredTransactionPriority; + } if (initialCakePayCountry != null) { selectedCakePayCountry = initialCakePayCountry; @@ -280,6 +285,9 @@ abstract class SettingsStoreBase with Store { case WalletType.zano: key = PreferencesKey.zanoTransactionPriority; break; + case WalletType.decred: + key = PreferencesKey.decredTransactionPriority; + break; default: key = null; } @@ -898,6 +906,7 @@ abstract class SettingsStoreBase with Store { TransactionPriority? bitcoinCashTransactionPriority; TransactionPriority? wowneroTransactionPriority; TransactionPriority? zanoTransactionPriority; + TransactionPriority? decredTransactionPriority; if (sharedPreferences.getInt(PreferencesKey.havenTransactionPriority) != null) { havenTransactionPriority = monero?.deserializeMoneroTransactionPriority( @@ -927,6 +936,10 @@ abstract class SettingsStoreBase with Store { zanoTransactionPriority = monero?.deserializeMoneroTransactionPriority( raw: sharedPreferences.getInt(PreferencesKey.zanoTransactionPriority)!); } + if (sharedPreferences.getInt(PreferencesKey.decredTransactionPriority) != null) { + decredTransactionPriority = decred?.deserializeDecredTransactionPriority( + sharedPreferences.getInt(PreferencesKey.decredTransactionPriority)!); + } moneroTransactionPriority ??= monero?.getDefaultTransactionPriority(); bitcoinTransactionPriority ??= bitcoin?.getMediumTransactionPriority(); @@ -935,6 +948,7 @@ abstract class SettingsStoreBase with Store { ethereumTransactionPriority ??= ethereum?.getDefaultTransactionPriority(); bitcoinCashTransactionPriority ??= bitcoinCash?.getDefaultTransactionPriority(); wowneroTransactionPriority ??= wownero?.getDefaultTransactionPriority(); + decredTransactionPriority ??= decred?.getDecredTransactionPriorityMedium(); polygonTransactionPriority ??= polygon?.getDefaultTransactionPriority(); zanoTransactionPriority ??= zano?.getDefaultTransactionPriority(); @@ -1038,7 +1052,7 @@ abstract class SettingsStoreBase with Store { final tronNodeId = sharedPreferences.getInt(PreferencesKey.currentTronNodeIdKey); final wowneroNodeId = sharedPreferences.getInt(PreferencesKey.currentWowneroNodeIdKey); final zanoNodeId = sharedPreferences.getInt(PreferencesKey.currentZanoNodeIdKey); - + final decredNodeId = sharedPreferences.getInt(PreferencesKey.currentDecredNodeIdKey); final moneroNode = nodeSource.get(nodeId); final bitcoinElectrumServer = nodeSource.get(bitcoinElectrumServerId); final litecoinElectrumServer = nodeSource.get(litecoinElectrumServerId); @@ -1047,6 +1061,7 @@ abstract class SettingsStoreBase with Store { final polygonNode = nodeSource.get(polygonNodeId); final bitcoinCashElectrumServer = nodeSource.get(bitcoinCashElectrumServerId); final nanoNode = nodeSource.get(nanoNodeId); + final decredNode = nodeSource.get(decredNodeId); final nanoPowNode = powNodeSource.get(nanoPowNodeId); final solanaNode = nodeSource.get(solanaNodeId); final tronNode = nodeSource.get(tronNodeId); @@ -1137,6 +1152,10 @@ abstract class SettingsStoreBase with Store { nodes[WalletType.zano] = zanoNode; } + if (decredNode != null) { + nodes[WalletType.decred] = decredNode; + } + final savedSyncMode = SyncMode.all.firstWhere((element) { return element.type.index == (sharedPreferences.getInt(PreferencesKey.syncModeKey) ?? 0); }); @@ -1304,6 +1323,7 @@ abstract class SettingsStoreBase with Store { initialHavenTransactionPriority: havenTransactionPriority, initialLitecoinTransactionPriority: litecoinTransactionPriority, initialBitcoinCashTransactionPriority: bitcoinCashTransactionPriority, + initialDecredTransactionPriority: decredTransactionPriority, initialShouldRequireTOTP2FAForAccessingWallet: shouldRequireTOTP2FAForAccessingWallet, initialShouldRequireTOTP2FAForSendsToContact: shouldRequireTOTP2FAForSendsToContact, initialShouldRequireTOTP2FAForSendsToNonContact: shouldRequireTOTP2FAForSendsToNonContact, @@ -1378,6 +1398,11 @@ abstract class SettingsStoreBase with Store { priority[WalletType.zano] = zano!.deserializeMoneroTransactionPriority( raw: sharedPreferences.getInt(PreferencesKey.zanoTransactionPriority)!); } + if (decred != null && + sharedPreferences.getInt(PreferencesKey.decredTransactionPriority) != null) { + priority[WalletType.decred] = decred!.deserializeDecredTransactionPriority( + sharedPreferences.getInt(PreferencesKey.decredTransactionPriority)!); + } final generateSubaddresses = sharedPreferences.getInt(PreferencesKey.autoGenerateSubaddressStatusKey); @@ -1489,6 +1514,7 @@ abstract class SettingsStoreBase with Store { final tronNodeId = sharedPreferences.getInt(PreferencesKey.currentTronNodeIdKey); final wowneroNodeId = sharedPreferences.getInt(PreferencesKey.currentWowneroNodeIdKey); final zanoNodeId = sharedPreferences.getInt(PreferencesKey.currentZanoNodeIdKey); + final decredNodeId = sharedPreferences.getInt(PreferencesKey.currentDecredNodeIdKey); final moneroNode = nodeSource.get(nodeId); final bitcoinElectrumServer = nodeSource.get(bitcoinElectrumServerId); final litecoinElectrumServer = nodeSource.get(litecoinElectrumServerId); @@ -1496,11 +1522,12 @@ abstract class SettingsStoreBase with Store { final ethereumNode = nodeSource.get(ethereumNodeId); final polygonNode = nodeSource.get(polygonNodeId); final bitcoinCashNode = nodeSource.get(bitcoinCashElectrumServerId); - final nanoNode = nodeSource.get(nanoNodeId); + final nanoNode = nodeSource.get(nanoNodeId); final solanaNode = nodeSource.get(solanaNodeId); final tronNode = nodeSource.get(tronNodeId); final wowneroNode = nodeSource.get(wowneroNodeId); final zanoNode = nodeSource.get(zanoNodeId); + final decredNode = nodeSource.get(decredNodeId); if (moneroNode != null) { nodes[WalletType.monero] = moneroNode; @@ -1551,6 +1578,10 @@ abstract class SettingsStoreBase with Store { nodes[WalletType.zano] = zanoNode; } + if (decredNode != null) { + nodes[WalletType.decred] = decredNode; + } + // MIGRATED: useTOTP2FA = await SecureKey.getBool( @@ -1687,6 +1718,9 @@ abstract class SettingsStoreBase with Store { case WalletType.wownero: await _sharedPreferences.setInt(PreferencesKey.currentWowneroNodeIdKey, node.key as int); break; + case WalletType.decred: + await _sharedPreferences.setInt(PreferencesKey.currentDecredNodeIdKey, node.key as int); + break; case WalletType.zano: await _sharedPreferences.setInt(PreferencesKey.currentZanoNodeIdKey, node.key as int); default: diff --git a/lib/view_model/advanced_privacy_settings_view_model.dart b/lib/view_model/advanced_privacy_settings_view_model.dart index 3959afe01..803744590 100644 --- a/lib/view_model/advanced_privacy_settings_view_model.dart +++ b/lib/view_model/advanced_privacy_settings_view_model.dart @@ -55,6 +55,7 @@ abstract class AdvancedPrivacySettingsViewModelBase with Store { case WalletType.none: case WalletType.haven: case WalletType.zano: + case WalletType.decred: return false; } } diff --git a/lib/view_model/contact_list/contact_list_view_model.dart b/lib/view_model/contact_list/contact_list_view_model.dart index eb3fb837e..730d0735c 100644 --- a/lib/view_model/contact_list/contact_list_view_model.dart +++ b/lib/view_model/contact_list/contact_list_view_model.dart @@ -9,7 +9,6 @@ import 'package:cake_wallet/entities/wallet_list_order_types.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/utils/mobx.dart'; -import 'package:collection/collection.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; @@ -43,7 +42,8 @@ abstract class ContactListViewModelBase with Store { } } } else if (info.addresses?.isNotEmpty == true && info.addresses!.length > 1) { - if ([WalletType.monero, WalletType.wownero, WalletType.haven].contains(info.type)) { + if ([WalletType.monero, WalletType.wownero, WalletType.haven, WalletType.decred] + .contains(info.type)) { final address = info.address; final name = _createName(info.name, "", key: 0); walletContacts.add(WalletContact( @@ -129,11 +129,9 @@ abstract class ContactListViewModelBase with Store { (element.type == CryptoCurrency.btc || element.type == CryptoCurrency.ltc)) return false; return element.type == _currency || - (element.type.tag != null && - _currency?.tag != null && - element.type.tag == _currency?.tag) || - _currency?.toString() == element.type.tag || - _currency?.tag == element.type.toString(); + (element.type.tag != null && _currency.tag != null && element.type.tag == _currency.tag) || + _currency.toString() == element.type.tag || + _currency.tag == element.type.toString(); } void dispose() => _subscription?.cancel(); diff --git a/lib/view_model/contact_list/contact_view_model.dart b/lib/view_model/contact_list/contact_view_model.dart index 93abfb11c..efa1f09f4 100644 --- a/lib/view_model/contact_list/contact_view_model.dart +++ b/lib/view_model/contact_list/contact_view_model.dart @@ -62,12 +62,12 @@ abstract class ContactViewModelBase with Store { return; } - if (_contact != null && _contact!.original.isInBox) { - _contact?.name = name; - _contact?.address = address; - _contact?.type = currency!; - _contact?.lastChange = now; - await _contact?.save(); + if (_contact != null && _contact.original.isInBox) { + _contact.name = name; + _contact.address = address; + _contact.type = currency!; + _contact.lastChange = now; + await _contact.save(); } else { await _contacts .add(Contact(name: name, address: address, type: currency!, lastChange: now)); diff --git a/lib/view_model/dashboard/balance_view_model.dart b/lib/view_model/dashboard/balance_view_model.dart index 730f07a93..ef5676138 100644 --- a/lib/view_model/dashboard/balance_view_model.dart +++ b/lib/view_model/dashboard/balance_view_model.dart @@ -306,6 +306,7 @@ abstract class BalanceViewModelBase with Store { case WalletType.monero: case WalletType.wownero: case WalletType.zano: + case WalletType.decred: return true; default: return false; diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index ee9c57b65..49c0dbc59 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -23,7 +23,6 @@ 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'; @@ -169,6 +168,9 @@ abstract class DashboardViewModelBase with Store { type = appStore.wallet!.type, transactions = ObservableList(), wallet = appStore.wallet! { + showDecredInfoCard = wallet.type == WalletType.decred && + (sharedPreferences.getBool(PreferencesKey.showDecredInfoCard) ?? true); + name = wallet.name; type = wallet.type; isShowFirstYatIntroduction = false; @@ -261,6 +263,7 @@ abstract class DashboardViewModelBase with Store { reaction((_) => appStore.wallet, (wallet) { _onWalletChange(wallet); _checkMweb(); + showDecredInfoCard = wallet?.type == WalletType.decred; }); _transactionDisposer?.reaction.dispose(); @@ -350,6 +353,10 @@ abstract class DashboardViewModelBase with Store { statusText = S.current.please_try_to_connect_to_another_node; } + if (status is ProcessingSyncStatus) { + statusText = (status as ProcessingSyncStatus).message ?? S.current.processing; + } + return statusText; } @@ -399,13 +406,7 @@ abstract class DashboardViewModelBase with Store { bool get isTestnet => wallet.type == WalletType.bitcoin && bitcoin!.isTestnet(wallet); @computed - bool get hasRescan => [ - WalletType.bitcoin, - WalletType.monero, - WalletType.litecoin, - WalletType.wownero, - WalletType.haven - ].contains(wallet.type); + bool get hasRescan => wallet.hasRescan; @computed bool get isMoneroViewOnly { @@ -490,6 +491,9 @@ abstract class DashboardViewModelBase with Store { @observable bool mwebEnabled = false; + @observable + late bool showDecredInfoCard; + @computed bool get hasEnabledMwebBefore => settingsStore.hasEnabledMwebBefore; @@ -543,6 +547,12 @@ abstract class DashboardViewModelBase with Store { bitcoin!.setMwebEnabled(wallet, false); } + @action + void dismissDecredInfoCard() { + showDecredInfoCard = false; + sharedPreferences.setBool(PreferencesKey.showDecredInfoCard, false); + } + BalanceViewModel balanceViewModel; AppStore appStore; @@ -573,6 +583,9 @@ abstract class DashboardViewModelBase with Store { @computed bool get isEnabledSwapAction => settingsStore.exchangeStatus != ExchangeApiMode.disabled; + @computed + bool get canSend => wallet.canSend(); + @observable bool hasSwapAction; @@ -596,21 +609,28 @@ abstract class DashboardViewModelBase with Store { @computed bool get hasSignMessages { - if (wallet.isHardwareWallet) return false; - - return [ - WalletType.monero, - WalletType.litecoin, - WalletType.bitcoin, - WalletType.bitcoinCash, - WalletType.ethereum, - WalletType.polygon, - WalletType.solana, - WalletType.nano, - WalletType.banano, - WalletType.tron, - WalletType.wownero - ].contains(wallet.type); + if (wallet.isHardwareWallet) { + return false; + } + switch (wallet.type) { + case WalletType.monero: + case WalletType.litecoin: + case WalletType.bitcoin: + case WalletType.bitcoinCash: + case WalletType.ethereum: + case WalletType.polygon: + case WalletType.solana: + case WalletType.nano: + case WalletType.banano: + case WalletType.tron: + case WalletType.wownero: + case WalletType.decred: + return true; + case WalletType.zano: + case WalletType.haven: + case WalletType.none: + return false; + } } bool get showRepWarning { diff --git a/lib/view_model/dashboard/home_settings_view_model.dart b/lib/view_model/dashboard/home_settings_view_model.dart index e3e02a045..4a85ec89a 100644 --- a/lib/view_model/dashboard/home_settings_view_model.dart +++ b/lib/view_model/dashboard/home_settings_view_model.dart @@ -214,6 +214,7 @@ abstract class HomeSettingsViewModelBase with Store { case WalletType.nano: case WalletType.wownero: case WalletType.bitcoinCash: + case WalletType.decred: return false; } diff --git a/lib/view_model/dashboard/receive_option_view_model.dart b/lib/view_model/dashboard/receive_option_view_model.dart index 744e4c58d..f15d7dad6 100644 --- a/lib/view_model/dashboard/receive_option_view_model.dart +++ b/lib/view_model/dashboard/receive_option_view_model.dart @@ -14,7 +14,9 @@ abstract class ReceiveOptionViewModelBase with Store { (_wallet.type == WalletType.bitcoin || _wallet.type == WalletType.litecoin ? bitcoin!.getSelectedAddressType(_wallet) - : ReceivePageOption.mainnet), + : (_wallet.type == WalletType.decred && _wallet.isTestnet) + ? ReceivePageOption.testnet + : ReceivePageOption.mainnet), _options = [] { final walletType = _wallet.type; switch (walletType) { @@ -33,6 +35,17 @@ abstract class ReceiveOptionViewModelBase with Store { case WalletType.haven: _options = [ReceivePageOption.mainnet]; break; + case WalletType.decred: + if (_wallet.isTestnet) { + _options = [ + ReceivePageOption.testnet, + ...ReceivePageOptions.where( + (element) => element != ReceivePageOption.mainnet) + ]; + } else { + _options = ReceivePageOptions; + } + break; default: _options = ReceivePageOptions; } diff --git a/lib/view_model/dashboard/transaction_list_item.dart b/lib/view_model/dashboard/transaction_list_item.dart index 864448293..83953f9f1 100644 --- a/lib/view_model/dashboard/transaction_list_item.dart +++ b/lib/view_model/dashboard/transaction_list_item.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/decred/decred.dart'; import 'package:cake_wallet/entities/balance_display_mode.dart'; import 'package:cake_wallet/entities/fiat_currency.dart'; import 'package:cake_wallet/ethereum/ethereum.dart'; @@ -82,7 +83,9 @@ class TransactionListItem extends ActionListItem with Keyable { if (transaction.confirmations <= 0) { str = S.current.pending; } - if ((isPegOut || fromPegOut) && transaction.confirmations >= 0 && transaction.confirmations < 6) { + if ((isPegOut || fromPegOut) && + transaction.confirmations >= 0 && + transaction.confirmations < 6) { str = " (${transaction.confirmations}/6)"; } if (isPegIn) { @@ -224,7 +227,13 @@ class TransactionListItem extends ActionListItem with Keyable { cryptoAmount: zano!.formatterIntAmountToDouble(amount: transaction.amount, currency: asset, forFee: false), price: price); break; - default: + case WalletType.decred: + amount = calculateFiatAmountRaw( + cryptoAmount: decred!.formatterDecredAmountToDouble(amount: transaction.amount), + price: price); + break; + case WalletType.none: + case WalletType.banano: break; } diff --git a/lib/view_model/exchange/exchange_view_model.dart b/lib/view_model/exchange/exchange_view_model.dart index 6ea9c811f..8b7349e9a 100644 --- a/lib/view_model/exchange/exchange_view_model.dart +++ b/lib/view_model/exchange/exchange_view_model.dart @@ -23,6 +23,8 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'package:cake_wallet/.secrets.g.dart' as secrets; import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart'; +import 'package:cake_wallet/decred/decred.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'; @@ -167,7 +169,8 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with wallet.type == WalletType.bitcoinCash; bool get hideAddressAfterExchange => - wallet.type == WalletType.monero || wallet.type == WalletType.wownero; + wallet.type == WalletType.monero || + wallet.type == WalletType.wownero; bool _useTorOnly; final Box trades; @@ -316,8 +319,6 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with bool get isMoneroWallet => wallet.type == WalletType.monero; - - List receiveCurrencies; List depositCurrencies; @@ -778,6 +779,10 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with depositCurrency = CryptoCurrency.zano; receiveCurrency = CryptoCurrency.xmr; break; + case WalletType.decred: + depositCurrency = CryptoCurrency.dcr; + receiveCurrency = CryptoCurrency.xmr; + break; case WalletType.none: break; } 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 71f996aff..8fbe174e3 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 @@ -63,13 +63,13 @@ abstract class NodeCreateOrEditViewModelBase with Store { String socksProxyAddress; @computed - bool get isReady => address.isNotEmpty && port.isNotEmpty; + bool get isReady => + (address.isNotEmpty && port.isNotEmpty) || + _walletType == WalletType.decred; // Allow an empty address. bool get hasAuthCredentials => _walletType == WalletType.monero || _walletType == WalletType.wownero || _walletType == WalletType.haven; - bool get hasTestnetSupport => _walletType == WalletType.bitcoin; - bool get hasPathSupport { switch (_walletType) { case WalletType.ethereum: @@ -87,6 +87,7 @@ abstract class NodeCreateOrEditViewModelBase with Store { case WalletType.bitcoinCash: case WalletType.bitcoin: case WalletType.zano: + case WalletType.decred: return false; } } 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 71e77eb12..9df5f2980 100644 --- a/lib/view_model/node_list/node_list_view_model.dart +++ b/lib/view_model/node_list/node_list_view_model.dart @@ -49,50 +49,10 @@ abstract class NodeListViewModelBase with Store { await resetToDefault(_nodeSource); Node node; - - switch (_appStore.wallet!.type) { - case WalletType.bitcoin: - if (_appStore.wallet!.isTestnet) { - node = getBitcoinTestnetDefaultElectrumServer(nodes: _nodeSource)!; - } else { - node = getBitcoinDefaultElectrumServer(nodes: _nodeSource)!; - } - break; - case WalletType.monero: - node = getMoneroDefaultNode(nodes: _nodeSource); - break; - case WalletType.litecoin: - node = getLitecoinDefaultElectrumServer(nodes: _nodeSource)!; - break; - case WalletType.haven: - node = getHavenDefaultNode(nodes: _nodeSource)!; - break; - case WalletType.ethereum: - node = getEthereumDefaultNode(nodes: _nodeSource)!; - break; - case WalletType.bitcoinCash: - node = getBitcoinCashDefaultElectrumServer(nodes: _nodeSource)!; - break; - case WalletType.nano: - node = getNanoDefaultNode(nodes: _nodeSource)!; - break; - case WalletType.polygon: - node = getPolygonDefaultNode(nodes: _nodeSource)!; - break; - case WalletType.solana: - node = getSolanaDefaultNode(nodes: _nodeSource)!; - break; - case WalletType.tron: - node = getTronDefaultNode(nodes: _nodeSource)!; - break; - case WalletType.wownero: - node = getWowneroDefaultNode(nodes: _nodeSource); - break; - case WalletType.zano: - node = getZanoDefaultNode(nodes: _nodeSource)!; - break; - default: - throw Exception('Unexpected wallet type: ${_appStore.wallet!.type}'); + if (_appStore.wallet!.type == WalletType.bitcoin && _appStore.wallet!.isTestnet) { + node = getBitcoinTestnetDefaultElectrumServer(nodes: _nodeSource)!; + } else { + node = getDefaultNode(nodes: _nodeSource, type: _appStore.wallet!.type)!; } await setAsCurrent(node); 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 2e4e2da83..c8ff81acc 100644 --- a/lib/view_model/restore/wallet_restore_from_qr_code.dart +++ b/lib/view_model/restore/wallet_restore_from_qr_code.dart @@ -44,6 +44,9 @@ class WalletRestoreFromQRCode { 'zano': WalletType.zano, 'zano-wallet': WalletType.zano, 'zano_wallet': WalletType.zano, + 'decred': WalletType.decred, + 'decred-wallet': WalletType.decred, + 'decred_wallet': WalletType.decred, }; static WalletType? _extractWalletType(String code) { @@ -69,8 +72,12 @@ class WalletRestoreFromQRCode { } static String? _extractAddressFromUrl(String rawString, WalletType type) { - return AddressResolver.extractAddressByType( - raw: rawString, type: walletTypeToCryptoCurrency(type)); + try { + return AddressResolver.extractAddressByType( + raw: rawString, type: walletTypeToCryptoCurrency(type)); + } catch (_) { + return null; + } } static String? _extractSeedPhraseFromUrl(String rawString, WalletType walletType) { @@ -122,7 +129,6 @@ class WalletRestoreFromQRCode { } if (queryParameters['address'] == null) { queryParameters['address'] = _extractAddressFromUrl(code, walletType); - } Map credentials = {'type': walletType, ...queryParameters, 'raw_qr': code}; diff --git a/lib/view_model/send/fees_view_model.dart b/lib/view_model/send/fees_view_model.dart index 088c66039..f6dd0f201 100644 --- a/lib/view_model/send/fees_view_model.dart +++ b/lib/view_model/send/fees_view_model.dart @@ -1,4 +1,5 @@ import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart'; +import 'package:cake_wallet/decred/decred.dart'; import 'package:cake_wallet/entities/priority_for_wallet_type.dart'; import 'package:cake_wallet/core/wallet_change_listener_view_model.dart'; import 'package:cake_wallet/ethereum/ethereum.dart'; @@ -88,7 +89,13 @@ abstract class FeesViewModelBase extends WalletChangeListenerViewModel with Stor return transactionPriority == bitcoinCash!.getBitcoinCashTransactionPrioritySlow(); case WalletType.polygon: return transactionPriority == polygon!.getPolygonTransactionPrioritySlow(); - default: + case WalletType.decred: + return transactionPriority == decred!.getDecredTransactionPrioritySlow(); + case WalletType.none: + case WalletType.nano: + case WalletType.banano: + case WalletType.solana: + case WalletType.tron: return false; } } diff --git a/lib/view_model/send/output.dart b/lib/view_model/send/output.dart index 121ffa693..fdaaeba0d 100644 --- a/lib/view_model/send/output.dart +++ b/lib/view_model/send/output.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/decred/decred.dart'; import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/entities/calculate_fiat_amount_raw.dart'; import 'package:cake_wallet/entities/parse_address_from_domain.dart'; @@ -101,6 +102,9 @@ abstract class OutputBase with Store { case WalletType.bitcoinCash: _amount = bitcoin!.formatterStringDoubleToBitcoinAmount(_cryptoAmount); break; + case WalletType.decred: + _amount = decred!.formatterStringDoubleToDecredAmount(_cryptoAmount); + break; case WalletType.haven: _amount = haven!.formatterMoneroParseAmount(amount: _cryptoAmount); break; @@ -188,6 +192,10 @@ abstract class OutputBase with Store { if (_wallet.type == WalletType.zano) { return zano!.formatterIntAmountToDouble(amount: fee, currency: cryptoCurrencyHandler(), forFee: true); } + + if (_wallet.type == WalletType.decred) { + return decred!.formatterDecredAmountToDouble(amount: fee); + } } catch (e) { printV(e.toString()); } diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index 1cd6b8966..3bf85ef2f 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -6,6 +6,7 @@ import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/exchange/provider/exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/thorchain_exchange.provider.dart'; import 'package:cake_wallet/nano/nano.dart'; +import 'package:cake_wallet/decred/decred.dart'; import 'package:cake_wallet/core/wallet_change_listener_view_model.dart'; import 'package:cake_wallet/entities/contact_record.dart'; import 'package:cake_wallet/entities/wallet_contact.dart'; @@ -251,6 +252,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor wallet.type == WalletType.litecoin || wallet.type == WalletType.monero || wallet.type == WalletType.wownero || + wallet.type == WalletType.decred || wallet.type == WalletType.bitcoinCash; @computed @@ -539,6 +541,9 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor case WalletType.zano: return zano!.createZanoTransactionCredentials( outputs: outputs, priority: priority!, currency: selectedCryptoCurrency); + case WalletType.decred: + this.coinTypeToSpendFrom = UnspentCoinType.any; + return decred!.createDecredTransactionCredentials(outputs, priority!); default: throw Exception('Unexpected wallet type: ${wallet.type}'); } @@ -681,55 +686,51 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor } } - if (walletType == WalletType.bitcoin || - walletType == WalletType.litecoin || - walletType == WalletType.bitcoinCash) { - if (error is TransactionWrongBalanceException) { - if (error.amount != null) - return S.current - .tx_wrong_balance_with_amount_exception(currency.toString(), error.amount.toString()); + if (error is TransactionWrongBalanceException) { + if (error.amount != null) + return S.current + .tx_wrong_balance_with_amount_exception(currency.toString(), error.amount.toString()); - return S.current.tx_wrong_balance_exception(currency.toString()); - } - if (error is TransactionNoInputsException) { - return S.current.tx_not_enough_inputs_exception; - } - if (error is TransactionNoFeeException) { - return S.current.tx_zero_fee_exception; - } - if (error is TransactionNoDustException) { - return S.current.tx_no_dust_exception; - } - if (error is TransactionCommitFailed) { - if (error.errorMessage != null && error.errorMessage!.contains("no peers replied")) { - return S.current.tx_commit_failed_no_peers; - } - return "${S.current.tx_commit_failed}${error.errorMessage != null ? "\n\n${error.errorMessage}" : ""}"; - } - if (error is TransactionCommitFailedDustChange) { - return S.current.tx_rejected_dust_change; - } - if (error is TransactionCommitFailedDustOutput) { - return S.current.tx_rejected_dust_output; - } - if (error is TransactionCommitFailedDustOutputSendAll) { - return S.current.tx_rejected_dust_output_send_all; - } - if (error is TransactionCommitFailedVoutNegative) { - return S.current.tx_rejected_vout_negative; - } - if (error is TransactionCommitFailedBIP68Final) { - return S.current.tx_rejected_bip68_final; - } - if (error is TransactionCommitFailedLessThanMin) { - return S.current.fee_less_than_min; - } - if (error is TransactionNoDustOnChangeException) { - return S.current.tx_commit_exception_no_dust_on_change(error.min, error.max); - } - if (error is TransactionInputNotSupported) { - return S.current.tx_invalid_input; + return S.current.tx_wrong_balance_exception(currency.toString()); + } + if (error is TransactionNoInputsException) { + return S.current.tx_not_enough_inputs_exception; + } + if (error is TransactionNoFeeException) { + return S.current.tx_zero_fee_exception; + } + if (error is TransactionNoDustException) { + return S.current.tx_no_dust_exception; + } + if (error is TransactionCommitFailed) { + if (error.errorMessage != null && error.errorMessage!.contains("no peers replied")) { + return S.current.tx_commit_failed_no_peers; } + return "${S.current.tx_commit_failed}${error.errorMessage != null ? "\n\n${error.errorMessage}" : ""}"; + } + if (error is TransactionCommitFailedDustChange) { + return S.current.tx_rejected_dust_change; + } + if (error is TransactionCommitFailedDustOutput) { + return S.current.tx_rejected_dust_output; + } + if (error is TransactionCommitFailedDustOutputSendAll) { + return S.current.tx_rejected_dust_output_send_all; + } + if (error is TransactionCommitFailedVoutNegative) { + return S.current.tx_rejected_vout_negative; + } + if (error is TransactionCommitFailedBIP68Final) { + return S.current.tx_rejected_bip68_final; + } + if (error is TransactionCommitFailedLessThanMin) { + return S.current.fee_less_than_min; + } + if (error is TransactionNoDustOnChangeException) { + return S.current.tx_commit_exception_no_dust_on_change(error.min, error.max); + } + if (error is TransactionInputNotSupported) { + return S.current.tx_invalid_input; } return errorMessage; diff --git a/lib/view_model/settings/privacy_settings_view_model.dart b/lib/view_model/settings/privacy_settings_view_model.dart index e2c977590..62b656dca 100644 --- a/lib/view_model/settings/privacy_settings_view_model.dart +++ b/lib/view_model/settings/privacy_settings_view_model.dart @@ -44,7 +44,8 @@ abstract class PrivacySettingsViewModelBase with Store { _wallet.type == WalletType.wownero || _wallet.type == WalletType.bitcoin || _wallet.type == WalletType.litecoin || - _wallet.type == WalletType.bitcoinCash; + _wallet.type == WalletType.bitcoinCash || + _wallet.type == WalletType.decred; bool get isMoneroWallet => _wallet.type == WalletType.monero; diff --git a/lib/view_model/transaction_details_view_model.dart b/lib/view_model/transaction_details_view_model.dart index 2dc6478f9..067ca73f9 100644 --- a/lib/view_model/transaction_details_view_model.dart +++ b/lib/view_model/transaction_details_view_model.dart @@ -48,6 +48,7 @@ abstract class TransactionDetailsViewModelBase with Store { final dateFormat = DateFormatter.withCurrentLocal(); final tx = transactionInfo; + // TODO: can be cleaned further switch (wallet.type) { case WalletType.monero: _addMoneroListItems(tx, dateFormat); @@ -84,8 +85,12 @@ abstract class TransactionDetailsViewModelBase with Store { case WalletType.zano: _addZanoListItems(tx, dateFormat); break; - default: + case WalletType.decred: + _addDecredListItems(tx, dateFormat); break; + case WalletType.none: + case WalletType.banano: + break; } final descriptionKey = '${transactionInfo.txHash}_${wallet.walletAddresses.primaryAddress}'; @@ -186,6 +191,8 @@ abstract class TransactionDetailsViewModelBase with Store { return 'https://explore.wownero.com/tx/${txId}'; case WalletType.zano: return 'https://explorer.zano.org/transaction/${txId}'; + case WalletType.decred: + return 'https://${wallet.isTestnet ? "testnet" : "dcrdata"}.decred.org/tx/${txId.split(':')[0]}'; case WalletType.none: return ''; } @@ -218,6 +225,8 @@ abstract class TransactionDetailsViewModelBase with Store { return S.current.view_transaction_on + 'Wownero.com'; case WalletType.zano: return S.current.view_transaction_on + 'explorer.zano.org'; + case WalletType.decred: + return S.current.view_transaction_on + 'dcrdata.decred.org'; case WalletType.none: return ''; } @@ -672,6 +681,51 @@ abstract class TransactionDetailsViewModelBase with Store { items.addAll(_items); } + void _addDecredListItems(TransactionInfo tx, DateFormat dateFormat) { + final _items = [ + StandartListItem( + title: S.current.transaction_details_transaction_id, + value: tx.txHash, + key: ValueKey('standard_list_item_transaction_details_id_key'), + ), + StandartListItem( + title: S.current.transaction_details_date, + value: dateFormat.format(tx.date), + key: ValueKey('standard_list_item_transaction_details_date_key'), + ), + StandartListItem( + title: S.current.transaction_details_height, + value: '${tx.height}', + key: ValueKey('standard_list_item_transaction_details_height_key'), + ), + StandartListItem( + title: S.current.transaction_details_amount, + value: tx.amountFormatted(), + key: ValueKey('standard_list_item_transaction_details_amount_key'), + ), + if (tx.feeFormatted()?.isNotEmpty ?? false) + StandartListItem( + title: S.current.transaction_details_fee, + value: tx.feeFormatted()!, + key: ValueKey('standard_list_item_transaction_details_fee_key'), + ), + if (showRecipientAddress && tx.to != null) + StandartListItem( + title: S.current.transaction_details_recipient_address, + value: tx.to!, + key: ValueKey('standard_list_item_transaction_details_recipient_address_key'), + ), + if (tx.from != null) + StandartListItem( + title: S.current.transaction_details_source_address, + value: tx.from!, + key: ValueKey('standard_list_item_transaction_details_source_address_key'), + ), + ]; + + items.addAll(_items); + } + @action Future _checkForRBF(TransactionInfo tx) async { if (wallet.type == WalletType.bitcoin && 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 9a8a4a8f2..d4fadb2f1 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,6 +1,7 @@ import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/utils/exception_handler.dart'; +import 'package:cake_wallet/decred/decred.dart'; import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_item.dart'; import 'package:cake_wallet/wownero/wownero.dart'; import 'package:cw_core/unspent_coin_type.dart'; @@ -92,6 +93,8 @@ abstract class UnspentCoinsListViewModelBase with Store { return wownero!.formatterWowneroAmountToString(amount: fullBalance); if ([WalletType.bitcoin, WalletType.litecoin, WalletType.bitcoinCash].contains(wallet.type)) return bitcoin!.formatterBitcoinAmountToString(amount: fullBalance); + if (wallet.type == WalletType.decred) + return decred!.formatterDecredAmountToString(amount: fullBalance); return ''; } @@ -105,7 +108,9 @@ abstract class UnspentCoinsListViewModelBase with Store { if ([WalletType.bitcoin, WalletType.litecoin, WalletType.bitcoinCash].contains(wallet.type)) { await bitcoin!.updateUnspents(wallet); } - + if (wallet.type == WalletType.decred) { + decred!.updateUnspents(wallet); + } _updateUnspentCoinsInfo(); } @@ -119,6 +124,8 @@ abstract class UnspentCoinsListViewModelBase with Store { case WalletType.litecoin: case WalletType.bitcoinCash: return bitcoin!.getUnspents(wallet, coinTypeToSpendFrom: coinTypeToSpendFrom); + case WalletType.decred: + return decred!.getUnspents(wallet); default: 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 d365c8e00..3a678ff0f 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 @@ -5,6 +5,7 @@ import 'package:cw_core/wallet_base.dart'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/haven/haven.dart'; +import 'package:cake_wallet/decred/decred.dart'; import 'package:cw_core/wallet_type.dart'; part 'wallet_address_edit_or_create_view_model.g.dart'; @@ -27,13 +28,12 @@ class AddressEditOrCreateStateFailure extends AddressEditOrCreateState { } abstract class WalletAddressEditOrCreateViewModelBase with Store { - WalletAddressEditOrCreateViewModelBase( - {required WalletBase wallet, WalletAddressListItem? item}) + WalletAddressEditOrCreateViewModelBase({required WalletBase wallet, WalletAddressListItem? item}) : isEdit = item != null, state = AddressEditOrCreateStateInitial(), label = item?.name ?? '', _item = item, - _wallet = wallet; + _wallet = wallet; @observable AddressEditOrCreateState state; @@ -46,7 +46,8 @@ abstract class WalletAddressEditOrCreateViewModelBase with Store { final WalletAddressListItem? _item; final WalletBase _wallet; - bool get isElectrum => _wallet.type == WalletType.bitcoin || + bool get isElectrum => + _wallet.type == WalletType.bitcoin || _wallet.type == WalletType.bitcoinCash || _wallet.type == WalletType.litecoin; @@ -69,39 +70,46 @@ abstract class WalletAddressEditOrCreateViewModelBase with Store { Future _createNew() async { final wallet = _wallet; - if (isElectrum) await bitcoin!.generateNewAddress(wallet, label); + if (isElectrum) { + await bitcoin!.generateNewAddress(wallet, label); + await wallet.save(); + } + + if (wallet.type == WalletType.decred) { + await decred!.generateNewAddress(wallet, label); + await wallet.save(); + } if (wallet.type == WalletType.monero) { - await monero - !.getSubaddressList(wallet) - .addSubaddress( - wallet, - accountIndex: monero!.getCurrentAccount(wallet).id, - label: label); - final addr = await monero!.getSubaddressList(wallet).subaddresses.first.address; // first because the order is reversed + await monero! + .getSubaddressList(wallet) + .addSubaddress(wallet, accountIndex: monero!.getCurrentAccount(wallet).id, label: label); + final addr = await monero! + .getSubaddressList(wallet) + .subaddresses + .first + .address; // first because the order is reversed wallet.walletAddresses.manualAddresses.add(addr); await wallet.save(); } if (wallet.type == WalletType.wownero) { - await wownero - !.getSubaddressList(wallet) - .addSubaddress( - wallet, - accountIndex: wownero!.getCurrentAccount(wallet).id, - label: label); - final addr = await wownero!.getSubaddressList(wallet).subaddresses.first.address; // first because the order is reversed + await wownero! + .getSubaddressList(wallet) + .addSubaddress(wallet, accountIndex: wownero!.getCurrentAccount(wallet).id, label: label); + final addr = await wownero! + .getSubaddressList(wallet) + .subaddresses + .first + .address; // first because the order is reversed wallet.walletAddresses.manualAddresses.add(addr); await wallet.save(); } if (wallet.type == WalletType.haven) { - await haven - !.getSubaddressList(wallet) - .addSubaddress( - wallet, - accountIndex: haven!.getCurrentAccount(wallet).id, - label: label); + await haven! + .getSubaddressList(wallet) + .addSubaddress(wallet, accountIndex: haven!.getCurrentAccount(wallet).id, label: label); await wallet.save(); } } @@ -111,6 +119,12 @@ abstract class WalletAddressEditOrCreateViewModelBase with Store { if (isElectrum) await bitcoin!.updateAddress(wallet, _item!.address, label); + if (wallet.type == WalletType.decred) { + await decred!.updateAddress(wallet, _item!.address, label); + await wallet.save(); + return; + } + final index = _item?.id; if (index != null) { if (wallet.type == WalletType.monero) { @@ -125,9 +139,7 @@ abstract class WalletAddressEditOrCreateViewModelBase with Store { } if (wallet.type == WalletType.haven) { await haven!.getSubaddressList(wallet).setLabelSubaddress(wallet, - accountIndex: haven!.getCurrentAccount(wallet).id, - addressIndex: index, - label: label); + accountIndex: haven!.getCurrentAccount(wallet).id, addressIndex: index, label: label); 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 ef1c52f31..0b0873a60 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 @@ -12,6 +12,7 @@ import 'package:cake_wallet/haven/haven.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/solana/solana.dart'; +import 'package:cake_wallet/decred/decred.dart'; import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; import 'package:cake_wallet/store/settings_store.dart'; @@ -222,6 +223,22 @@ class ZanoURI extends PaymentURI { } } +class DecredURI extends PaymentURI { + DecredURI({required String amount, required String address}) + : super(amount: amount, address: address); + + @override + String toString() { + var base = 'decred:' + address; + + if (amount.isNotEmpty) { + base += '?amount=${amount.replaceAll(',', '.')}'; + } + + return base; + } +} + abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewModel with Store { WalletAddressListViewModelBase({ required AppStore appStore, @@ -313,7 +330,9 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo case WalletType.wownero: return WowneroURI(amount: amount, address: address.address); case WalletType.zano: - return ZanoURI(amount: amount, address: address.address); + return ZanoURI(amount: amount, address: address.address); + case WalletType.decred: + return DecredURI(amount: amount, address: address.address); case WalletType.none: throw Exception('Unexpected type: ${type.toString()}'); } @@ -468,6 +487,14 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo addressList.add(WalletAddressListItem(isPrimary: true, name: null, address: primaryAddress)); } + if (wallet.type == WalletType.decred) { + final addrInfos = decred!.getAddressInfos(wallet); + addrInfos.forEach((info) { + addressList.add(new WalletAddressListItem(isPrimary: false, address: info.address, + name: info.label)); + }); + } + for (var i = 0; i < addressList.length; i++) { if (!(addressList[i] is WalletAddressListItem)) continue; (addressList[i] as WalletAddressListItem).isHidden = wallet.walletAddresses.hiddenAddresses @@ -561,7 +588,8 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo WalletType.haven, WalletType.bitcoinCash, WalletType.bitcoin, - WalletType.litecoin + WalletType.litecoin, + WalletType.decred ].contains(wallet.type); @computed diff --git a/lib/view_model/wallet_keys_view_model.dart b/lib/view_model/wallet_keys_view_model.dart index 402764c40..467b7c3a5 100644 --- a/lib/view_model/wallet_keys_view_model.dart +++ b/lib/view_model/wallet_keys_view_model.dart @@ -12,6 +12,7 @@ import 'package:cw_core/wallet_type.dart'; import 'package:cw_monero/monero_wallet.dart'; import 'package:flutter/foundation.dart'; import 'package:mobx/mobx.dart'; +import 'package:cake_wallet/decred/decred.dart'; import 'package:polyseed/polyseed.dart'; part 'wallet_keys_view_model.g.dart'; @@ -155,6 +156,12 @@ abstract class WalletKeysViewModelBase with Store { ), ]); break; + case WalletType.decred: + final pubkey = decred!.pubkey(_appStore.wallet!); + items.addAll([ + StandartListItem(title: S.current.view_key_public, value: pubkey), + ]); + break; case WalletType.bitcoin: case WalletType.litecoin: case WalletType.bitcoinCash: @@ -248,6 +255,8 @@ abstract class WalletKeysViewModelBase with Store { return 'wownero-wallet'; case WalletType.zano: return 'zano-wallet'; + case WalletType.decred: + return 'decred-wallet'; default: throw Exception('Unexpected wallet type: ${_wallet.type.toString()}'); } diff --git a/lib/view_model/wallet_new_vm.dart b/lib/view_model/wallet_new_vm.dart index 0cd730028..e82ae8773 100644 --- a/lib/view_model/wallet_new_vm.dart +++ b/lib/view_model/wallet_new_vm.dart @@ -21,6 +21,7 @@ import 'package:cake_wallet/tron/tron.dart'; import 'package:cake_wallet/view_model/seed_settings_view_model.dart'; import 'package:cake_wallet/view_model/wallet_creation_vm.dart'; import 'package:cake_wallet/wownero/wownero.dart'; +import 'package:cake_wallet/decred/decred.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_credentials.dart'; import 'package:cw_core/wallet_info.dart'; @@ -83,6 +84,8 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store { return 25; case WalletType.zano: return 26; + case WalletType.decred: + return 15; } } @@ -170,6 +173,8 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store { password: walletPassword, passphrase: passphrase, ); + case WalletType.decred: + return decred!.createDecredNewWalletCredentials(name: name); case WalletType.none: throw Exception('Unexpected type: ${type.toString()}'); } diff --git a/lib/view_model/wallet_restore_view_model.dart b/lib/view_model/wallet_restore_view_model.dart index e7df1c221..3e005e9bc 100644 --- a/lib/view_model/wallet_restore_view_model.dart +++ b/lib/view_model/wallet_restore_view_model.dart @@ -11,6 +11,7 @@ import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/solana/solana.dart'; import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/tron/tron.dart'; +import 'package:cake_wallet/decred/decred.dart'; import 'package:cake_wallet/view_model/restore/restore_mode.dart'; import 'package:cake_wallet/view_model/restore/restore_wallet.dart'; import 'package:cake_wallet/view_model/seed_settings_view_model.dart'; @@ -58,6 +59,7 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { case WalletType.haven: case WalletType.ethereum: case WalletType.polygon: + case WalletType.decred: availableModes = [WalletRestoreMode.seed, WalletRestoreMode.keys]; break; case WalletType.bitcoin: @@ -77,6 +79,7 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { } static const moneroSeedMnemonicLength = 25; + static const decredSeedMnemonicLength = 15; late List availableModes; final bool hasSeedLanguageSelector; @@ -171,11 +174,18 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { ); case WalletType.zano: return zano!.createZanoRestoreWalletFromSeedCredentials( - name: name, - password: password, - height: height, + name: name, + password: password, + height: height, passphrase: passphrase??'', - mnemonic: seed); + mnemonic: seed, + ); + case WalletType.decred: + return decred!.createDecredRestoreWalletFromSeedCredentials( + name: name, + mnemonic: seed, + password: password, + ); case WalletType.none: break; } @@ -251,6 +261,12 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { password: password, language: 'English', ); + case WalletType.decred: + return decred!.createDecredRestoreWalletFromPubkeyCredentials( + name: name, + password: password, + pubkey: viewKey!, + ); default: break; } diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 42b9fa84c..528de8c42 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,6 +6,7 @@ import FlutterMacOS import Foundation import connectivity_plus +import cw_decred import cw_mweb import device_info_plus import devicelocale @@ -24,6 +25,7 @@ import wakelock_plus func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { ConnectivityPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlugin")) + CwDecredPlugin.register(with: registry.registrar(forPlugin: "CwDecredPlugin")) CwMwebPlugin.register(with: registry.registrar(forPlugin: "CwMwebPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) DevicelocalePlugin.register(with: registry.registrar(forPlugin: "DevicelocalePlugin")) diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 689f0ea03..cc6ae6e3b 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -3,6 +3,7 @@ PODS: - FlutterMacOS - ReachabilitySwift - cw_mweb (0.0.1): + - cw_decred (0.0.1): - FlutterMacOS - device_info_plus (0.0.1): - FlutterMacOS @@ -45,6 +46,7 @@ PODS: DEPENDENCIES: - connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos`) - cw_mweb (from `Flutter/ephemeral/.symlinks/plugins/cw_mweb/macos`) + - cw_decred (from `Flutter/ephemeral/.symlinks/plugins/cw_decred/macos`) - device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`) - devicelocale (from `Flutter/ephemeral/.symlinks/plugins/devicelocale/macos`) - fast_scanner (from `Flutter/ephemeral/.symlinks/plugins/fast_scanner/macos`) @@ -72,6 +74,10 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos cw_mweb: :path: Flutter/ephemeral/.symlinks/plugins/cw_mweb/macos + cw_decred: + :path: Flutter/ephemeral/.symlinks/plugins/cw_decred/macos + cw_monero: + :path: Flutter/ephemeral/.symlinks/plugins/cw_monero/macos device_info_plus: :path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos devicelocale: @@ -108,6 +114,8 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: connectivity_plus: 18d3c32514c886e046de60e9c13895109866c747 cw_mweb: 7440b12ead811dda972a9918442ea2a458e8742c + cw_monero: ec03de55a19c4a2b174ea687e0f4202edc716fa4 + cw_decred: 0c93fbeb31bd97a6ad4ec5680960af0943bfca78 device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f devicelocale: 9f0f36ac651cabae2c33f32dcff4f32b61c38225 fast_scanner: d31bae07e2653403a69dac99fb710c1722b16a97 diff --git a/model_generator.sh b/model_generator.sh index f3950e2b1..56b891903 100755 --- a/model_generator.sh +++ b/model_generator.sh @@ -1,22 +1,22 @@ #!/bin/bash set -x -e -for cwcoin in cw_{core,evm,monero,bitcoin,haven,nano,bitcoin_cash,solana,tron,wownero,zano} +for cwcoin in cw_{core,evm,monero,bitcoin,haven,nano,bitcoin_cash,solana,tron,wownero,zano,decred} do if [[ "x$1" == "xasync" ]]; then bash -c "cd $cwcoin; flutter pub get; dart run build_runner build --delete-conflicting-outputs; cd .." & else - bash -c "cd $cwcoin; flutter pub get; dart run build_runner build --delete-conflicting-outputs; cd .." + cd $cwcoin; flutter pub get; dart run build_runner build --delete-conflicting-outputs; cd .. fi done for cwcoin in cw_{polygon,ethereum,mweb}; -do +do if [[ "x$1" == "xasync" ]]; then bash -c "cd $cwcoin; flutter pub get; cd .." & else - bash -c "cd $cwcoin; flutter pub get; cd .." + cd $cwcoin; flutter pub get; cd .. fi done diff --git a/pubspec_base.yaml b/pubspec_base.yaml index 767f0b1f3..7e700e588 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -189,6 +189,7 @@ flutter: - assets/tron_node_list.yml - assets/wownero_node_list.yml - assets/zano_node_list.yml + - assets/decred_node_list.yml - assets/text/ - assets/faq/ - assets/animation/ diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index 9f1235919..6072b046c 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -208,6 +208,8 @@ "debit_card_terms": "يخضع تخزين واستخدام رقم بطاقة الدفع الخاصة بك (وبيانات الاعتماد المقابلة لرقم بطاقة الدفع الخاصة بك) في هذه المحفظة الرقمية لشروط وأحكام اتفاقية حامل البطاقة المعمول بها مع جهة إصدار بطاقة الدفع ، كما هو معمول به من وقت لآخر.", "decimal_places_error": "عدد كبير جدًا من المنازل العشرية", "decimals_cannot_be_zero": "الرمز العشري لا يمكن أن يكون الصفر.", + "decred_info_card_details": "يستخدم Decred طريقة متزامنة لا مركزية وحفاظ على الخصوصية المعروفة باسم \"SPV\" ، والتي تستغرق وقتًا أطول من محفظة Bitcoin العادية. لمعرفة المزيد ، انقر أدناه.", + "decred_info_title": "التزامن في Decred", "default_buy_provider": "مزود شراء الافتراضي", "default_sell_provider": "ﻲﺿﺍﺮﺘﻓﻻﺍ ﻊﻴﺒﻟﺍ ﺩﻭﺰﻣ", "delete": "حذف", @@ -546,6 +548,7 @@ "proceed_after_one_minute": "إذا لم تستمر الشاشة بعد دقيقة واحدة ، فتحقق من بريدك الإلكتروني.", "proceed_on_device": "تابع جهازك", "proceed_on_device_description": "يرجى اتباع الإرشادات المطلوبة على محفظة الأجهزة الخاصة بك", + "processing": "يعالج", "profile": "حساب تعريفي", "provider_error": "خطأ ${provider}", "public_key": "مفتاح عمومي", diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index 190dd07d8..62f1bbf18 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -208,6 +208,8 @@ "debit_card_terms": "Съхранението и използването на данните от вашата платежна карта в този дигитален портфейл подлежат на условията на съответното съгласие за картодържец от издателя на картата.", "decimal_places_error": "Твърде много знаци след десетичната запетая", "decimals_cannot_be_zero": "Десетичната точка не може да бъде нула.", + "decred_info_card_details": "DeCred използва децентрализиран и консервиращ метод за синхронизиране, известен като „SPV“, който отнема повече време от нормалния портфейл с биткойн. За да научите повече, докоснете по -долу.", + "decred_info_title": "Синхронизация в Decred", "default_buy_provider": "Доставчик по подразбиране купува", "default_sell_provider": "Доставчик за продажба по подразбиране", "delete": "Изтрий", @@ -546,6 +548,7 @@ "proceed_after_one_minute": "Ако процесът продължи повече от 1 минута, проверете своя имейл.", "proceed_on_device": "Продължете на вашето устройство", "proceed_on_device_description": "Моля, следвайте инструкциите, подканени на вашия хардуер", + "processing": "Обработка", "profile": "Профил", "provider_error": "Грешка на ${provider} ", "public_key": "Публичен ключ", diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index 923db16fe..92eed7a3d 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -208,6 +208,8 @@ "debit_card_terms": "Uložení a použití vašeho čísla platební karty (a přihlašovací údaje k vašemu číslu karty) v této digitální peněžence se řídí Obchodními podmínkami smlouvy příslušného držitele karty s vydavatelem karty (v jejich nejaktuálnější verzi).", "decimal_places_error": "Příliš mnoho desetinných míst", "decimals_cannot_be_zero": "Desetinná desetinná škola nemůže být nulová.", + "decred_info_card_details": "Decred používá decentralizovanou a synchronizační metodu zachovávající soukromí známou jako „SPV“, která trvá déle než normální bitcoinová peněženka. Chcete -li se dozvědět více, klepněte na níže.", + "decred_info_title": "Synchronizace v Decred", "default_buy_provider": "Výchozí poskytovatel nákupu", "default_sell_provider": "Výchozí poskytovatel prodeje", "delete": "Smazat", @@ -546,6 +548,7 @@ "proceed_after_one_minute": "Pokud proces nepokročí během 1 minuty, zkontrolujte svůj e-mail.", "proceed_on_device": "Pokračujte ve svém zařízení", "proceed_on_device_description": "Postupujte podle pokynů na výzvu na vaší hardwarové peněžence", + "processing": "Zpracování", "profile": "Profil", "provider_error": "${provider} chyba", "public_key": "Veřejný klíč", diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 39e3113c1..43db11f18 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -208,6 +208,8 @@ "debit_card_terms": "Die Speicherung und Nutzung Ihrer Zahlungskartennummer (und Ihrer Zahlungskartennummer entsprechenden Anmeldeinformationen) in dieser digitalen Wallet unterliegt den Allgemeinen Geschäftsbedingungen des geltenden Karteninhabervertrags mit dem Zahlungskartenaussteller, gültig ab von Zeit zu Zeit.", "decimal_places_error": "Zu viele Nachkommastellen", "decimals_cannot_be_zero": "Token-Dezimalzahl kann nicht Null sein.", + "decred_info_card_details": "Decred verwendet eine dezentrale und für Privatsphäre erziehende Synchronisation, die als „SPV“ bezeichnet wird und länger als eine normale Bitcoin-Brieftasche dauert. Um mehr zu erfahren, tippen Sie unten.", + "decred_info_title": "Synchronisation in Decred", "default_buy_provider": "Standard-Kaufanbieter", "default_sell_provider": "Standard-Verkaufsanbieter", "delete": "Löschen", @@ -547,6 +549,7 @@ "proceed_after_one_minute": "Wenn der Bildschirm nach 1 Minute nicht weitergeht, überprüfen Sie bitte Ihre E-Mail.", "proceed_on_device": "Fahren Sie auf Ihrem Gerät fort", "proceed_on_device_description": "Bitte befolgen Sie die Anweisungen, die auf Ihrer Hardware-Wallet angezeigt werden", + "processing": "Verarbeitung", "profile": "Profil", "provider_error": "${provider}-Fehler", "public_key": "Öffentlicher Schlüssel", diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 921375c3c..b6023d86e 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -208,6 +208,8 @@ "debit_card_terms": "The storage and usage of your payment card number (and credentials corresponding to your payment card number) in this digital wallet are subject to the Terms and Conditions of the applicable cardholder agreement with the payment card issuer, as in effect from time to time.", "decimal_places_error": "Too many decimal places", "decimals_cannot_be_zero": "Token decimal cannot be zero.", + "decred_info_card_details": "Decred uses a decentralized and privacy-preserving sync method known as “SPV”, which takes longer than a normal Bitcoin wallet. To learn more, tap below.", + "decred_info_title": "Synchronization in Decred", "default_buy_provider": "Default Buy Provider", "default_sell_provider": "Default Sell Provider", "delete": "Delete", @@ -547,6 +549,7 @@ "proceed_after_one_minute": "If the screen doesn’t proceed after 1 minute, check your email.", "proceed_on_device": "Proceed on your device", "proceed_on_device_description": "Please follow the instructions prompted on your hardware wallet", + "processing": "Processing", "profile": "Profile", "provider_error": "${provider} error", "public_key": "Public key", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index ad96c2a93..c0474d672 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -208,6 +208,8 @@ "debit_card_terms": "El almacenamiento y el uso de su número de tarjeta de pago (y las credenciales correspondientes a su número de tarjeta de pago) en esta billetera digital están sujetos a los Términos y condiciones del acuerdo del titular de la tarjeta aplicable con el emisor de la tarjeta de pago, en vigor desde tiempo al tiempo.", "decimal_places_error": "Demasiados lugares decimales", "decimals_cannot_be_zero": "Token Decimal no puede ser cero.", + "decred_info_card_details": "Decred utiliza un método de sincronización descentralizado y que presenta la privacidad conocido como \"SPV\", que lleva más tiempo que una billetera Bitcoin normal. Para obtener más información, toque a continuación.", + "decred_info_title": "Sincronización en Decred", "default_buy_provider": "Proveedor de compra predeterminado", "default_sell_provider": "Proveedor de venta predeterminado", "delete": "Borrar", @@ -547,6 +549,7 @@ "proceed_after_one_minute": "Si la pantalla no continúa después de 1 minuto, revisa tu correo electrónico.", "proceed_on_device": "Continúa con tu dispositivo", "proceed_on_device_description": "Sigue las instrucciones solicitadas en su billetera de hardware", + "processing": "Tratamiento", "profile": "Perfil", "provider_error": "${provider} error", "public_key": "Clave pública", diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index ccd12b1c6..d779a22f5 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -208,6 +208,8 @@ "debit_card_terms": "Le stockage et l'utilisation de votre numéro de carte de paiement (et des informations d'identification correspondant à votre numéro de carte de paiement) dans ce portefeuille (wallet) numérique peuvent être soumis aux conditions générales de l'accord du titulaire de carte parfois en vigueur avec l'émetteur de la carte de paiement.", "decimal_places_error": "Trop de décimales", "decimals_cannot_be_zero": "La décimale du jeton ne peut pas être nulle.", + "decred_info_card_details": "Decred utilise une méthode de synchronisation décentralisée et préservée de confidentialité connue sous le nom de «SPV», qui prend plus de temps qu'un portefeuille Bitcoin normal. Pour en savoir plus, appuyez sur ci-dessous.", + "decred_info_title": "Synchronisation dans Decred", "default_buy_provider": "Fournisseur d'achat par défaut", "default_sell_provider": "Fournisseur de vente par défaut", "delete": "Effacer", @@ -546,6 +548,7 @@ "proceed_after_one_minute": "Si l'écran ne s'affiche pas après 1 minute, vérifiez vos e-mails.", "proceed_on_device": "Continuez sur votre appareil", "proceed_on_device_description": "Veuillez suivre les instructions affichées sur votre portefeuille physique.", + "processing": "Traitement", "profile": "Profil", "provider_error": "Erreur de ${provider}", "public_key": "Clef publique", diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index af931e4df..13cbe1828 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -208,6 +208,8 @@ "debit_card_terms": "Adana da amfani da lambar katin kuɗin ku (da takaddun shaida masu dacewa da lambar katin kuɗin ku) a cikin wannan walat ɗin dijital suna ƙarƙashin Sharuɗɗa da Sharuɗɗa na yarjejeniya mai amfani da katin tare da mai fitar da katin biyan kuɗi, kamar yadda yake aiki daga lokaci zuwa lokaci.", "decimal_places_error": "Wadannan suna da tsawon harsuna", "decimals_cannot_be_zero": "Alamar alama ba zata iya zama sifili ba.", + "decred_info_card_details": "An yanke amfani da shi da ingantaccen tsari da kuma tsarin adana Siscyc na sirri da aka sani da \"SPV\", wanda ke ɗaukar tsayi fiye da walatic na al'ada. Don ƙarin koyo, matsa ƙasa.", + "decred_info_title": "Aiki tare a Decred", "default_buy_provider": "Tsohuwar Siyarwa", "default_sell_provider": "Tsohuwar Mai Bayar Siyarwa", "delete": "Share", @@ -548,6 +550,7 @@ "proceed_after_one_minute": "Idan allon bai ci gaba ba bayan minti 1, duba imel ɗin ku.", "proceed_on_device": "Ci gaba akan na'urarka", "proceed_on_device_description": "Da fatan za a bi umarnin akan walatware", + "processing": "Aiki", "profile": "Rabin fuska", "provider_error": "${provider} kuskure", "public_key": "Maɓallin jama'a", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index 449a3f278..409e0fd07 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -208,6 +208,8 @@ "debit_card_terms": "इस डिजिटल वॉलेट में आपके भुगतान कार्ड नंबर (और आपके भुगतान कार्ड नंबर से संबंधित क्रेडेंशियल) का भंडारण और उपयोग भुगतान कार्ड जारीकर्ता के साथ लागू कार्डधारक समझौते के नियमों और शर्तों के अधीन है, जैसा कि प्रभावी है समय - समय पर।", "decimal_places_error": "बहुत अधिक दशमलव स्थान", "decimals_cannot_be_zero": "टोकन दशमलव शून्य नहीं हो सकता।", + "decred_info_card_details": "डिक्रेड एक विकेन्द्रीकृत और गोपनीयता-संरक्षण सिंक विधि का उपयोग करता है जिसे \"एसपीवी\" के रूप में जाना जाता है, जो एक सामान्य बिटकॉइन वॉलेट से अधिक समय लेता है। अधिक जानने के लिए, नीचे टैप करें।", + "decred_info_title": "डिक्रेड में सिंक्रनाइज़ेशन", "default_buy_provider": "डिफ़ॉल्ट खरीद प्रदाता", "default_sell_provider": "डिफ़ॉल्ट विक्रय प्रदाता", "delete": "हटाएं", @@ -547,6 +549,7 @@ "proceed_after_one_minute": "यदि 1 मिनट के बाद भी स्क्रीन आगे नहीं बढ़ती है, तो अपना ईमेल देखें।", "proceed_on_device": "अपने डिवाइस पर आगे बढ़ें", "proceed_on_device_description": "कृपया अपने हार्डवेयर वॉलेट पर दिए गए निर्देशों का पालन करें", + "processing": "प्रसंस्करण", "profile": "प्रोफ़ाइल", "provider_error": "${provider} त्रुटि", "public_key": "सार्वजनिक कुंजी", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index 25d90e711..f93f86d1d 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -208,6 +208,8 @@ "debit_card_terms": "Pohranjivanje i korištenje broja vaše platne kartice (i vjerodajnica koje odgovaraju broju vaše platne kartice) u ovom digitalnom novčaniku podliježu Uvjetima i odredbama važećeg ugovora vlasnika kartice s izdavateljem platne kartice, koji su na snazi ​​od S vremena na vrijeme.", "decimal_places_error": "Previše decimalnih mjesta", "decimals_cannot_be_zero": "Token Decimal ne može biti nula.", + "decred_info_card_details": "DECRED koristi decentraliziranu metodu sinkronizacije za očuvanje privatnosti poznatu kao \"SPV\", koja traje duže od normalnog Bitcoin novčanika. Da biste saznali više, dodirnite u nastavku.", + "decred_info_title": "Sinkronizacija u DECRED", "default_buy_provider": "Zadani davatelj kupnje", "default_sell_provider": "Zadani dobavljač prodaje", "delete": "Izbriši", @@ -546,6 +548,7 @@ "proceed_after_one_minute": "Ako se zaslon ne nastavi nakon 1 minute, provjerite svoju e-poštu.", "proceed_on_device": "Nastavite na svom uređaju", "proceed_on_device_description": "Slijedite upute zatražene na vašem hardverskom novčaniku", + "processing": "Obrada", "profile": "Profil", "provider_error": "${provider} greška", "public_key": "Javni ključ", diff --git a/res/values/strings_hy.arb b/res/values/strings_hy.arb index 6c35b2be1..3378f1a0d 100644 --- a/res/values/strings_hy.arb +++ b/res/values/strings_hy.arb @@ -208,6 +208,8 @@ "debit_card_terms": "Ձեր վճարային քարտի համարի (և ձեր վճարային քարտի համարի համապատասխան վկայականներ) պահպանումն ու օգտագործումը այս թվային դրամապանակում ենթակա են վճարային քարտ թողարկող կողմի գործող պայմանների և պայմանագրի", "decimal_places_error": "Խմբակային տեղերի սխալ", "decimals_cannot_be_zero": "Խմբակային տեղերը չեն կարող լինել զրո", + "decred_info_card_details": "DECREDE- ն օգտագործում է ապակենտրոնացված եւ գաղտնիության պահպանման համաժամացման համաժամացման մեթոդը, որը հայտնի է որպես «SPV», որը տեւում է ավելի երկար, քան նորմալ Bitcoin դրամապանակը: Ավելին իմանալու համար հպեք ստորեւ:", + "decred_info_title": "Համաժամացումը DEPRED- ում", "default_buy_provider": "Լռելյայն գնման մատակարար", "default_sell_provider": "Լռելյայն վաճառքի մատակարար", "delete": "Ջնջել", @@ -545,6 +547,7 @@ "proceed_after_one_minute": "Եթե էկրանը 1 րոպեից ավել չի անցնում, ստուգեք ձեր էլեկտրոնային փոստը", "proceed_on_device": "Շարունակեք ձեր սարքի վրա", "proceed_on_device_description": "Խնդրում ենք հետևել ձեր սարքի վրա ցուցադրվող հրահանգներին", + "processing": "Վերամշակում", "profile": "Պրոֆիլ", "provider_error": "${provider} սխալ", "public_key": "Հանրային բանալի", diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index 87e45da1c..22558854f 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -208,6 +208,8 @@ "debit_card_terms": "Penyimpanan dan penggunaan nomor kartu pembayaran Anda (dan kredensial yang sesuai dengan nomor kartu pembayaran Anda) dalam dompet digital ini tertakluk pada Syarat dan Ketentuan persetujuan pemegang kartu yang berlaku dengan penerbit kartu pembayaran, seperti yang berlaku dari waktu ke waktu.", "decimal_places_error": "Terlalu banyak tempat desimal", "decimals_cannot_be_zero": "Token desimal tidak bisa nol.", + "decred_info_card_details": "Decred menggunakan metode sinkronisasi yang terdesentralisasi dan mempertahankan privasi yang dikenal sebagai \"SPV\", yang membutuhkan waktu lebih lama dari dompet Bitcoin normal. Untuk mempelajari lebih lanjut, ketuk di bawah ini.", + "decred_info_title": "Sinkronisasi dalam dekred", "default_buy_provider": "Penyedia beli default", "default_sell_provider": "Penyedia Penjualan Default", "delete": "Hapus", @@ -548,6 +550,7 @@ "proceed_after_one_minute": "Jika layar tidak bergerak setelah 1 menit, periksa email Anda.", "proceed_on_device": "Lanjutkan di perangkat Anda", "proceed_on_device_description": "Harap ikuti instruksi yang diminta di dompet perangkat keras Anda", + "processing": "Pengolahan", "profile": "Profil", "provider_error": "${provider} error", "public_key": "Kunci publik", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index a476bbc0c..a30944901 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -208,6 +208,8 @@ "debit_card_terms": "L'archiviazione e l'utilizzo del numero della carta di pagamento (e delle credenziali corrispondenti al numero della carta di pagamento) in questo portafoglio digitale sono soggetti ai Termini e condizioni del contratto applicabile con il titolare della carta con l'emittente della carta di pagamento, come in vigore di tanto in tanto.", "decimal_places_error": "Troppe cifre decimali", "decimals_cannot_be_zero": "Il decimale token non può essere zero.", + "decred_info_card_details": "Decred utilizza un metodo di sincronizzazione decentralizzato e di conservazione della privacy noto come \"SPV\", che richiede più tempo di un normale portafoglio Bitcoin. Per saperne di più, tocca sotto.", + "decred_info_title": "Sincronizzazione in decred", "default_buy_provider": "Provider di acquisto predefinito", "default_sell_provider": "Provider di vendita predefinito", "delete": "Elimina", @@ -547,6 +549,7 @@ "proceed_after_one_minute": "Se la schermata non procede dopo 1 minuto, controlla la tua email.", "proceed_on_device": "Procedi sul tuo dispositivo", "proceed_on_device_description": "Segui le istruzioni richieste sul portafoglio hardware", + "processing": "Elaborazione", "profile": "Profilo", "provider_error": "${provider} errore", "public_key": "Chiave pubblica", diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index 10350d7be..25b16a92a 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -208,6 +208,8 @@ "debit_card_terms": "このデジタルウォレットでの支払いカード番号(および支払いカード番号に対応する資格情報)の保存と使用には、支払いカード発行者との該当するカード所有者契約の利用規約が適用されます。時々。", "decimal_places_error": "小数点以下の桁数が多すぎる", "decimals_cannot_be_zero": "トークン小数はゼロにすることはできません。", + "decred_info_card_details": "Decredは、「SPV」と呼ばれる分散型およびプライバシーを摂取する同期メソッドを使用します。これには、通常のビットコインウォレットよりも時間がかかります。詳細については、以下をタップしてください。", + "decred_info_title": "デコロードの同期", "default_buy_provider": "デフォルトの購入プロバイダー", "default_sell_provider": "デフォルトの販売プロバイダー", "delete": "削除する", @@ -547,6 +549,7 @@ "proceed_after_one_minute": "1分経っても画面が進まない場合は、メールを確認してください。", "proceed_on_device": "デバイスに進みます", "proceed_on_device_description": "ハードウェアウォレットにプロンプ​​トされた指示に従ってください", + "processing": "処理", "profile": "プロフィール", "provider_error": "${provider} エラー", "public_key": "公開鍵", diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index a76318ad0..9849ccd09 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -208,6 +208,8 @@ "debit_card_terms": "이 디지털 지갑에 있는 귀하의 지불 카드 번호(및 귀하의 지불 카드 번호에 해당하는 자격 증명)의 저장 및 사용은 부터 발효되는 지불 카드 발행자와의 해당 카드 소지자 계약의 이용 약관을 따릅니다. 수시로.", "decimal_places_error": "소수점 이하 자릿수가 너무 많습니다.", "decimals_cannot_be_zero": "토큰 소수점은 0이 될 수 없습니다.", + "decred_info_card_details": "Decred는 정상적인 비트 코인 지갑보다 더 오래 걸리는 \"SPV\"로 알려진 분산 및 개인 정보 보호 동기화 방법을 사용합니다. 자세한 내용은 아래를 누릅니다.", + "decred_info_title": "Decred의 동기화", "default_buy_provider": "기본 구매 제공자", "default_sell_provider": "기본 판매 공급자", "delete": "지우다", @@ -546,6 +548,7 @@ "proceed_after_one_minute": "1분 후에도 화면이 진행되지 않으면 이메일을 확인하세요.", "proceed_on_device": "장치를 진행하십시오", "proceed_on_device_description": "하드웨어 지갑에 표시된 지침을 따르십시오", + "processing": "처리", "profile": "프로필", "provider_error": "${provider} 오류", "public_key": "공개 키", diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index ac6749a7f..a8f57a824 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -208,6 +208,8 @@ "debit_card_terms": "ဤဒစ်ဂျစ်တယ်ပိုက်ဆံအိတ်ရှိ သင့်ငွေပေးချေမှုကတ်နံပါတ် (နှင့် သင့်ငွေပေးချေကတ်နံပါတ်နှင့် သက်ဆိုင်သောအထောက်အထားများ) ၏ သိုလှောင်မှုနှင့် အသုံးပြုမှုသည် အချိန်အခါနှင့်အမျှ သက်ရောက်မှုရှိသကဲ့သို့ ကတ်ကိုင်ဆောင်ထားသူ၏ သဘောတူညီချက်၏ စည်းကမ်းသတ်မှတ်ချက်များနှင့် ကိုက်ညီပါသည်။", "decimal_places_error": "ဒဿမနေရာများ များလွန်းသည်။", "decimals_cannot_be_zero": "တိုကင်ဒ decimal မသုညမဖြစ်နိုင်ပါ။", + "decred_info_card_details": "ပုံမှန် bitcoin ပိုက်ဆံအိတ်ထက်ပိုရှည်သော \"SPV\" ဟုလူသိများသောဗဟိုချုပ်ကိုင်မှုလျှော့ချခြင်းနှင့်လုံခြုံမှုထိန်းသိမ်းခြင်းကိုထိန်းသိမ်းထားသည့်ထပ်တူပြုခြင်းနည်းလမ်းကိုအသုံးပြုသည်။ ပိုမိုလေ့လာရန်အောက်ပါကိုအသာပုတ်ပါ။", + "decred_info_title": "ဒီဇင်ဘာလ၌ထပ်တူပြုခြင်း", "default_buy_provider": "Default Provider ကိုဝယ်ပါ", "default_sell_provider": "ပုံသေရောင်းချပေးသူ", "delete": "ဖျက်ပါ။", @@ -546,6 +548,7 @@ "proceed_after_one_minute": "မျက်နှာပြင်သည် ၁ မိနစ်အကြာတွင် ဆက်လက်မလုပ်ဆောင်ပါက သင့်အီးမေးလ်ကို စစ်ဆေးပါ။", "proceed_on_device": "သင့်စက်ပေါ်တွင်ဆက်လက်ဆောင်ရွက်ပါ", "proceed_on_device_description": "သင်၏ hardware ပိုက်ဆံအိတ်ပေါ်ရှိညွှန်ကြားချက်များကိုလိုက်နာပါ", + "processing": "လုပ်ကိုင်ခြင်း", "profile": "ကိုယ်ရေးအကျဉ်း", "provider_error": "${provider} အမှား", "public_key": "အများသူငှာသော့", diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index e38e1ddfd..a4f2bb9a1 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -208,6 +208,8 @@ "debit_card_terms": "De opslag en het gebruik van uw betaalkaartnummer (en inloggegevens die overeenkomen met uw betaalkaartnummer) in deze digitale portemonnee zijn onderworpen aan de Algemene voorwaarden van de toepasselijke kaarthouderovereenkomst met de uitgever van de betaalkaart, zoals van kracht vanaf tijd tot tijd.", "decimal_places_error": "Te veel decimalen", "decimals_cannot_be_zero": "Token decimaal kan niet nul zijn.", + "decred_info_card_details": "Decred maakt gebruik van een gedecentraliseerde en privacy-behouds-synchronisatiemethode bekend als \"SPV\", die langer duurt dan een normale Bitcoin-portemonnee. Voor meer informatie, tik hieronder.", + "decred_info_title": "Synchronisatie in Decred", "default_buy_provider": "Standaard Koopprovider", "default_sell_provider": "Standaard verkoopaanbieder", "delete": "Delete", @@ -546,6 +548,7 @@ "proceed_after_one_minute": "Als het scherm na 1 minuut niet verder gaat, controleer dan uw e-mail.", "proceed_on_device": "Ga verder met uw apparaat", "proceed_on_device_description": "Volg de instructies die zijn aangevraagd op uw hardware -portemonnee", + "processing": "Verwerking", "profile": "Profiel", "provider_error": "${provider} fout", "public_key": "Publieke sleutel", diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index 9ec7db278..12e867518 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -208,6 +208,8 @@ "debit_card_terms": "Przechowywanie i używanie numeru karty płatniczej (oraz danych uwierzytelniających odpowiadających numerowi karty płatniczej) w tym portfelu cyfrowym podlega Warunkom odpowiedniej umowy posiadacza karty z wydawcą karty płatniczej, zgodnie z obowiązującym od od czasu do czasu.", "decimal_places_error": "Za dużo miejsc dziesiętnych", "decimals_cannot_be_zero": "Token dziesiętny nie może być zerowy.", + "decred_info_card_details": "Decred używa zdecentralizowanej i zachowującej prywatność metodę synchronizacji znanej jako „SPV”, która trwa dłużej niż normalny portfel bitcoin. Aby dowiedzieć się więcej, dotknij poniżej.", + "decred_info_title": "Synchronizacja w dekred", "default_buy_provider": "Domyślny dostawca zakupu", "default_sell_provider": "Domyślny dostawca sprzedaży", "delete": "Skasuj", @@ -546,6 +548,7 @@ "proceed_after_one_minute": "Jeśli ekran nie przejdzie dalej po 1 minucie, sprawdź pocztę.", "proceed_on_device": "Kontynuuj swoje urządzenie", "proceed_on_device_description": "Postępuj zgodnie z instrukcjami wyświetlonymi w portfelu sprzętowym", + "processing": "Przetwarzanie", "profile": "Profil", "provider_error": "${provider} pomyłka", "public_key": "Klucz publiczny", diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index 0c0ed2515..7d60a3d79 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -208,6 +208,8 @@ "debit_card_terms": "O armazenamento e uso do número do cartão de pagamento (e credenciais correspondentes ao número do cartão de pagamento) nesta carteira digital estão sujeitos aos Termos e Condições do contrato do titular do cartão aplicável com o emissor do cartão de pagamento, em vigor a partir de tempo ao tempo.", "decimal_places_error": "Muitas casas decimais", "decimals_cannot_be_zero": "Decimal de token não pode ser zero.", + "decred_info_card_details": "O Decred usa um método de sincronização descentralizado e de preservação de privacidade, conhecido como \"SPV\", que leva mais tempo do que uma carteira normal de Bitcoin. Para saber mais, toque abaixo.", + "decred_info_title": "Sincronização em Decred", "default_buy_provider": "Provedor de compra padrão", "default_sell_provider": "Provedor de venda padrão", "delete": "Excluir", @@ -548,6 +550,7 @@ "proceed_after_one_minute": "Se a tela não prosseguir após 1 minuto, verifique seu e-mail.", "proceed_on_device": "Prossiga no seu dispositivo", "proceed_on_device_description": "Siga as instruções solicitadas em sua carteira de hardware", + "processing": "Processamento", "profile": "Perfil", "provider_error": "${provider} erro", "public_key": "Chave pública", diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index 0b511b016..9c80e483f 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -208,6 +208,8 @@ "debit_card_terms": "Хранение и использование номера вашей платежной карты (и учетных данных, соответствующих номеру вашей платежной карты) в этом цифровом кошельке регулируются положениями и условиями применимого соглашения держателя карты с эмитентом платежной карты, действующим с время от времени.", "decimal_places_error": "Слишком много десятичных знаков", "decimals_cannot_be_zero": "Десятичный токен не может быть нулевым.", + "decred_info_card_details": "DepRed использует децентрализованный метод синхронизации и сохраняющего конфиденциальность, известный как «SPV», который занимает больше времени, чем обычный биткойн-кошелек. Чтобы узнать больше, нажмите ниже.", + "decred_info_title": "Синхронизация в декорации", "default_buy_provider": "По умолчанию поставщик покупки", "default_sell_provider": "Поставщик продаж по умолчанию", "delete": "Удалить", @@ -547,6 +549,7 @@ "proceed_after_one_minute": "Если через 1 минуту экран не отображается, проверьте свою электронную почту.", "proceed_on_device": "Пройдите на свое устройство", "proceed_on_device_description": "Пожалуйста, следуйте инструкциям, представленным на вашем аппаратном кошельке", + "processing": "Обработка", "profile": "Профиль", "provider_error": "${provider} ошибка", "public_key": "Публичный ключ", diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index bbbcf7227..874e9f390 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -208,6 +208,8 @@ "debit_card_terms": "การเก็บรักษาและใช้หมายเลขบัตรจ่ายเงิน (และข้อมูลประจำตัวที่เกี่ยวข้องกับหมายเลขบัตรจ่ายเงิน) ในกระเป๋าดิจิทัลนี้ จะต้องยึดถือข้อกำหนดและเงื่อนไขของข้อตกลงผู้ใช้บัตรของผู้ถือบัตรที่เกี่ยวข้องกับบัตรผู้ถือบัตร ซึ่งจะมีผลตั้งแต่เวลานั้น", "decimal_places_error": "ทศนิยมมากเกินไป", "decimals_cannot_be_zero": "ทศนิยมโทเค็นไม่สามารถเป็นศูนย์ได้", + "decred_info_card_details": "Decred ใช้วิธีการซิงค์การกระจายอำนาจและความเป็นส่วนตัวที่เรียกว่า \"SPV\" ซึ่งใช้เวลานานกว่ากระเป๋าเงิน bitcoin ปกติ หากต้องการเรียนรู้เพิ่มเติมแตะด้านล่าง", + "decred_info_title": "การซิงโครไนซ์ใน decred", "default_buy_provider": "ผู้ให้บริการซื้อเริ่มต้น", "default_sell_provider": "ผู้ให้บริการการขายเริ่มต้น", "delete": "ลบ", @@ -546,6 +548,7 @@ "proceed_after_one_minute": "หากหน้าจอไม่ดำเนินการหลังจาก 1 นาทีโปรดตรวจสอบอีเมลของคุณ", "proceed_on_device": "ดำเนินการบนอุปกรณ์ของคุณ", "proceed_on_device_description": "โปรดทำตามคำแนะนำที่ได้รับแจ้งไว้ในกระเป๋าเงินฮาร์ดแวร์ของคุณ", + "processing": "กำลังประมวลผล", "profile": "ประวัติโดยย่อ", "provider_error": "ข้อผิดพลาด ${provider}", "public_key": "คีย์สาธารณะ", diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index 7cdbd0d8b..31683c4fc 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -208,6 +208,8 @@ "debit_card_terms": "Ang pag-iimbak at paggamit ng iyong numero sa card (at mga kredensyal na nauugnay sa numero ng iyong card sa pagbabayad) sa pagbabayad sa digital wallet na ito ay napapailalim sa mga tuntunin at kundisyon ng naaangkop na kasunduan sa may-ari ng card kasama ang nagbigay ng card ng pagbabayad, na may bisa sa pana-panahon.", "decimal_places_error": "Masyadong maraming mga lugar na desimal", "decimals_cannot_be_zero": "Ang token decimal ay hindi maaaring maging zero.", + "decred_info_card_details": "Ang DECRED ay gumagamit ng isang desentralisado at privacy-pagpapanatili ng pamamaraan ng pag-sync na kilala bilang \"SPV\", na mas matagal kaysa sa isang normal na pitaka ng bitcoin. Upang malaman ang higit pa, mag -tap sa ibaba.", + "decred_info_title": "Pag -synchronise sa Decred", "default_buy_provider": "Default na Buy Provider", "default_sell_provider": "Default na Sell Provider", "delete": "Tanggalin", @@ -546,6 +548,7 @@ "proceed_after_one_minute": "Kung ang screen ay hindi magpapatuloy pagkatapos ng 1 minuto, suriin ang iyong email.", "proceed_on_device": "Magpatuloy sa iyong hardware wallet", "proceed_on_device_description": "Mangyaring sundin ang mga tagubilin na sinenyasan sa iyong hardware wallet", + "processing": "Pagproseso", "profile": "Profile", "provider_error": "${provider} error", "public_key": "Public key", diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index bfc87a7e7..f7553d5c1 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -208,6 +208,8 @@ "debit_card_terms": "Ödeme kartı numaranızın (ve kart numaranıza karşılık gelen kimlik bilgilerinin) bu dijital cüzdanda saklanması ve kullanılması, zaman zaman yürürlükte olan ödeme kartı veren kuruluşla yapılan ilgili kart sahibi sözleşmesinin Hüküm ve Koşullarına tabidir.", "decimal_places_error": "Çok fazla ondalık basamak", "decimals_cannot_be_zero": "Token oncial sıfır olamaz.", + "decred_info_card_details": "Decred, normal bir bitcoin cüzdanından daha uzun süren “SPV” olarak bilinen merkezi olmayan ve gizliliği koruyan bir senkronizasyon yöntemi kullanır. Daha fazla bilgi edinmek için aşağıya dokunun.", + "decred_info_title": "Senkronizasyon Dekred", "default_buy_provider": "Varsayılan Satın Alma Sağlayıcısı", "default_sell_provider": "Varsayılan Satış Sağlayıcısı", "delete": "Sil", @@ -546,6 +548,7 @@ "proceed_after_one_minute": "Ekran 1 dakika sonra ilerlemezse, e-postanızı kontrol edin.", "proceed_on_device": "Cihazınıza devam edin", "proceed_on_device_description": "Lütfen donanım cüzdanınızda istenen talimatları izleyin", + "processing": "İşleme", "profile": "Profil", "provider_error": "${provider} hatası", "public_key": "Genel Anahtar", diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index 4b7818347..e3a150bca 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -208,6 +208,8 @@ "debit_card_terms": "Зберігання та використання номера вашої платіжної картки (та облікових даних, які відповідають номеру вашої платіжної картки) у цьому цифровому гаманці регулюються Умовами відповідної угоди власника картки з емітентом платіжної картки, що діє з час від часу.", "decimal_places_error": "Забагато знаків після коми", "decimals_cannot_be_zero": "Десятковий знак не може бути нульовим.", + "decred_info_card_details": "Decred використовує децентралізований метод синхронізації, що зберігає конфіденційність, відомий як \"SPV\", який займає більше часу, ніж звичайний гаманець Bitcoin. Щоб дізнатися більше, торкніться нижче.", + "decred_info_title": "Синхронізація в Decred", "default_buy_provider": "Постачальник покупки за замовчуванням", "default_sell_provider": "Постачальник продажу за замовчуванням", "delete": "Видалити", @@ -546,6 +548,7 @@ "proceed_after_one_minute": "Якщо екран не продовжується через 1 хвилину, перевірте свою електронну пошту.", "proceed_on_device": "Продовжуйте свій пристрій", "proceed_on_device_description": "Будь ласка, дотримуйтесь інструкцій, підказаних на вашому апаратному гаманці", + "processing": "Обробка", "profile": "Профіль", "provider_error": "${provider} помилка", "public_key": "Публічний ключ", diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index f6b92ca36..d95bb9237 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -208,6 +208,8 @@ "debit_card_terms": "اس ڈیجیٹل والیٹ میں آپ کے ادائیگی کارڈ نمبر (اور آپ کے ادائیگی کارڈ نمبر سے متعلقہ اسناد) کا ذخیرہ اور استعمال ادائیگی کارڈ جاری کنندہ کے ساتھ قابل اطلاق کارڈ ہولڈر کے معاہدے کی شرائط و ضوابط کے ساتھ مشروط ہے، جیسا کہ وقتاً فوقتاً نافذ ہوتا ہے۔", "decimal_places_error": "بہت زیادہ اعشاریہ جگہیں۔", "decimals_cannot_be_zero": "ٹوکن اعشاریہ صفر نہیں ہوسکتا۔", + "decred_info_card_details": "ڈیکریڈ ایک विकेंद्रीकृत اور رازداری سے محفوظ رکھنے والا مطابقت پذیری کا طریقہ استعمال کرتا ہے جسے \"ایس پی وی\" کہا جاتا ہے ، جو عام بٹ کوائن پرس سے زیادہ وقت لگتا ہے۔ مزید جاننے کے لئے ، نیچے ٹیپ کریں۔", + "decred_info_title": "فیصلہ شدہ میں ہم آہنگی", "default_buy_provider": "پہلے سے طے شدہ خریدنے والا", "default_sell_provider": " ﮦﺪﻨﻨﮐ ﻢﮨﺍﺮﻓ ﻞﯿﺳ ﭧﻟﺎﻔﯾﮈ", "delete": "حذف کریں۔", @@ -548,6 +550,7 @@ "proceed_after_one_minute": "اگر اسکرین 1 منٹ کے بعد آگے نہیں بڑھتی ہے تو اپنا ای میل چیک کریں۔", "proceed_on_device": "اپنے آلے پر آگے بڑھیں", "proceed_on_device_description": "براہ کرم اپنے ہارڈ ویئر پرس پر آنے والی ہدایات پر عمل کریں", + "processing": "پروسیسنگ", "profile": "پروفائل", "provider_error": "${provider} خرابی۔", "public_key": "عوامی کلید", diff --git a/res/values/strings_vi.arb b/res/values/strings_vi.arb index a759e87c9..3ae00e89f 100644 --- a/res/values/strings_vi.arb +++ b/res/values/strings_vi.arb @@ -207,6 +207,8 @@ "debit_card_terms": "Việc lưu trữ và sử dụng số thẻ thanh toán của bạn (và thông tin xác thực tương ứng với số thẻ thanh toán của bạn) trong ví điện tử này phải tuân theo Điều khoản và Điều kiện của thỏa thuận chủ thẻ hiện hành với tổ chức phát hành thẻ thanh toán, theo thời gian.", "decimal_places_error": "Quá nhiều chữ số thập phân", "decimals_cannot_be_zero": "Chữ số thập phân không thể là số không.", + "decred_info_card_details": "Decred sử dụng một phương pháp đồng bộ hóa bảo tồn và bảo tồn quyền riêng tư được gọi là SPV SPV, mất nhiều thời gian hơn ví Bitcoin bình thường. Để tìm hiểu thêm, nhấn bên dưới.", + "decred_info_title": "Đồng bộ hóa trong Decred", "default_buy_provider": "Nhà cung cấp Mua mặc định", "default_sell_provider": "Nhà cung cấp Bán mặc định", "delete": "Xóa", @@ -544,6 +546,7 @@ "proceed_after_one_minute": "Nếu màn hình không tiếp tục sau 1 phút, hãy kiểm tra email của bạn.", "proceed_on_device": "Tiếp tục trên thiết bị của bạn", "proceed_on_device_description": "Vui lòng làm theo các hướng dẫn được nhắc trên ví phần cứng của bạn", + "processing": "Xử lý", "profile": "Hồ sơ", "provider_error": "Lỗi ${provider}", "public_key": "Khóa công khai", diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index 7d9017d0c..3502629fc 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -208,6 +208,8 @@ "debit_card_terms": "Òfin ti olùṣe àjọrò káàdì ìrajà bójú irú ọ̀nà t'á pamọ́ àti a lo òǹkà ti káàdì ìrajà yín (àti ọ̀rọ̀ ìdánimọ̀ tí káàdì náà) nínú àpamọ́wọ́ yìí.", "decimal_places_error": "Oọ̀rọ̀ ayipada ti o wa ni o dara julọ", "decimals_cannot_be_zero": "Token eleemel ko le jẹ odo.", + "decred_info_card_details": "Devend nlo ọna ti o ni itọju ati ti itọju-itọju-itọju ti o mọ bi \"SPV\", eyiti o gba to gun ju apamọwọ Bitcoin deede. Lati kọ ẹkọ diẹ sii, tẹ ni isalẹ.", + "decred_info_title": "Imuṣiṣẹpọ ni devitered", "default_buy_provider": "Aiyipada Ra Olupese", "default_sell_provider": "Aiyipada Olupese Tita", "delete": "Pa á", @@ -547,6 +549,7 @@ "proceed_after_one_minute": "Tí aṣàfihàn kò bá tẹ̀síwájú l'áàárín ìṣẹ́jú kan, ẹ tọ́ ímeèlì yín wò.", "proceed_on_device": "Tẹsiwaju lori ẹrọ rẹ", "proceed_on_device_description": "Jọwọ tẹle awọn ilana ti a ṣe lori apamọwọ ohun elo rẹ", + "processing": "Iṣaayan", "profile": "profaili", "provider_error": "Àṣìṣe ${provider}", "public_key": "Kọ́kọ́rọ́ tó kò àdáni", diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index 5e009f841..741266f90 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -208,6 +208,8 @@ "debit_card_terms": "您的支付卡号(以及与您的支付卡号对应的凭证)在此数字钱包中的存储和使用受适用的持卡人与支付卡发卡机构签订的协议的条款和条件的约束,自时不时。", "decimal_places_error": "小数位太多", "decimals_cannot_be_zero": "代币十进制不能为零。", + "decred_info_card_details": "DECRED使用称为“ SPV”的分散和隐私的同步方法,该方法比普通的比特币钱包更长。要了解更多信息,请点击下面。", + "decred_info_title": "在Decred中同步", "default_buy_provider": "默认购买提供商", "default_sell_provider": "默认销售提供商", "delete": "删除", @@ -546,6 +548,7 @@ "proceed_after_one_minute": "如果屏幕在 1 分钟后没有继续,请检查您的电子邮件。", "proceed_on_device": "在设备上继续", "proceed_on_device_description": "请按照您的硬件钱包上提示的说明进行操作", + "processing": "加工", "profile": "轮廓", "provider_error": "${provider} 错误", "public_key": "公钥", diff --git a/scripts/android/.gitignore b/scripts/android/.gitignore index f7e94b7c0..3a2b4c98b 100644 --- a/scripts/android/.gitignore +++ b/scripts/android/.gitignore @@ -1 +1,2 @@ -mwebd \ No newline at end of file +mwebd +decred \ No newline at end of file diff --git a/scripts/android/build_all.sh b/scripts/android/build_all.sh index ad4ec984b..dc730af55 100755 --- a/scripts/android/build_all.sh +++ b/scripts/android/build_all.sh @@ -11,6 +11,7 @@ case $APP_ANDROID_TYPE in "monero.com") $DIR/build_monero_all.sh ;; "cakewallet") $DIR/build_monero_all.sh $DIR/build_haven_all.sh - $DIR/build_mwebd.sh ;; + $DIR/build_mwebd.sh + $DIR/build_decred.sh ;; "haven") $DIR/build_haven_all.sh ;; esac diff --git a/scripts/android/build_decred.sh b/scripts/android/build_decred.sh new file mode 100755 index 000000000..d7b2fdb6b --- /dev/null +++ b/scripts/android/build_decred.sh @@ -0,0 +1,84 @@ +#!/bin/sh + +set -e +cd "$(dirname "$0")" +# . ./config.sh + +CW_DECRED_DIR=$(realpath ../..)/cw_decred +LIBWALLET_PATH="${PWD}/decred/libwallet" +LIBWALLET_URL="https://github.com/decred/libwallet.git" +LIBWALLET_VERSION="87b2769538db3065b334d247b25774593fc6443d" + +if [ -e $LIBWALLET_PATH ]; then + rm -fr $LIBWALLET_PATH/{*,.*} || true +fi +mkdir -p $LIBWALLET_PATH || true + +git clone $LIBWALLET_URL $LIBWALLET_PATH +cd $LIBWALLET_PATH +git checkout $LIBWALLET_VERSION + +if [[ "x$ANDROID_HOME" == "x" ]]; +then + echo "ANDROID_HOME is missing, please declare it before building (on macos it is usually $HOME/Library/Android/sdk)" + echo "echo > ~/.zprofile" + echo "echo 'export ANDROID_HOME=\"\$HOME/Library/Android/sdk\" > ~/.zprofile" + exit 1 +fi + +if [[ "x$ANDROID_NDK_VERSION" == "x" ]]; +then + echo "ANDROID_NDK_VERSION is missing, please declare it before building" + echo "You have these versions installed on your system currently:" + ls ${ANDROID_HOME}/ndk/ | cat | awk '{ print "- " $1 }' + echo "echo > ~/.zprofile" + echo "echo 'export ANDROID_NDK_CERSION=..... > ~/.zprofile" + exit 1 +fi + +export NDK_BIN_PATH="${ANDROID_HOME}/ndk/${ANDROID_NDK_VERSION}/toolchains/llvm/prebuilt/$(uname | tr '[:upper:]' '[:lower:]')-x86_64/bin" +export ANDROID_API_VERSION=21 +# export CPATH="$(clang -v 2>&1 | grep "Selected GCC installation" | rev | cut -d' ' -f1 | rev)/include" + +for arch in "aarch" "aarch64" "x86_64" +do + TRIPLET="" + TARGET="" + ARCH_ABI="" + + case $arch in + "aarch") + TRIPLET="armv7a-linux-androideabi" + TARGET="arm" + ARCH_ABI="armeabi-v7a";; + "aarch64") + TRIPLET="aarch64-linux-android" + TARGET="arm64" + ARCH_ABI="arm64-v8a";; + "x86_64") + TRIPLET="x86_64-linux-android" + TARGET="amd64" + ARCH_ABI="x86_64";; + *) + echo "Unknown arch: $arch" + exit 1;; + esac + + # PATH="${TOOLCHAIN_BASE_DIR}_${arch}/bin:${ORIGINAL_PATH}" + if [ -e ./build ]; then + rm -fr ./build + fi + + CLANG_PATH="${NDK_BIN_PATH}/${TRIPLET}${ANDROID_API_VERSION}-clang" + CGO_ENABLED=1 GOOS=android GOARCH=${TARGET} CC=${CLANG_PATH} CXX=${CLANG_PATH}++ \ + go build -v -buildmode=c-shared -o ./build/${TRIPLET}-libdcrwallet.so ./cgo + + DEST_LIB_DIR=${CW_DECRED_DIR}/android/libs/${ARCH_ABI} + mkdir -p $DEST_LIB_DIR + cp ${LIBWALLET_PATH}/build/${TRIPLET}-libdcrwallet.so $DEST_LIB_DIR/libdcrwallet.so +done + +HEADER_DIR=$CW_DECRED_DIR/lib/api +cp ${LIBWALLET_PATH}/build/${TRIPLET}-libdcrwallet.h $HEADER_DIR/libdcrwallet.h +cd $CW_DECRED_DIR +dart run ffigen diff --git a/scripts/android/pubspec_gen.sh b/scripts/android/pubspec_gen.sh index 5d6a24722..d3341f63d 100755 --- a/scripts/android/pubspec_gen.sh +++ b/scripts/android/pubspec_gen.sh @@ -10,7 +10,7 @@ case $APP_ANDROID_TYPE in CONFIG_ARGS="--monero" ;; $CAKEWALLET) - CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana --tron --wownero --zano" + CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana --tron --wownero --zano --decred" if [ "$CW_WITH_HAVEN" = true ];then CONFIG_ARGS="$CONFIG_ARGS --haven" fi @@ -26,4 +26,4 @@ flutter pub get dart run tool/generate_pubspec.dart flutter pub get dart run tool/configure.dart $CONFIG_ARGS -cd scripts/android \ No newline at end of file +cd scripts/android diff --git a/scripts/ios/app_config.sh b/scripts/ios/app_config.sh index d118370b5..32d6d5edd 100755 --- a/scripts/ios/app_config.sh +++ b/scripts/ios/app_config.sh @@ -27,18 +27,18 @@ universal_sed "s/PRODUCT_BUNDLE_IDENTIFIER = .*;/PRODUCT_BUNDLE_IDENTIFIER = $AP CONFIG_ARGS="" case $APP_IOS_TYPE in - $MONERO_COM) + $MONERO_COM) CONFIG_ARGS="--monero" ;; + $CAKEWALLET) - CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana --tron --wownero --zano" + CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana --tron --wownero --zano --decred" if [ "$CW_WITH_HAVEN" = true ];then CONFIG_ARGS="$CONFIG_ARGS --haven" fi ;; + $HAVEN) - - CONFIG_ARGS="--haven" ;; esac diff --git a/scripts/ios/build_all.sh b/scripts/ios/build_all.sh index ba5c55a1f..f5b58d6fa 100755 --- a/scripts/ios/build_all.sh +++ b/scripts/ios/build_all.sh @@ -9,6 +9,6 @@ DIR=$(dirname "$0") case $APP_IOS_TYPE in "monero.com") $DIR/build_monero_all.sh ;; - "cakewallet") $DIR/build_monero_all.sh && $DIR/build_haven.sh && $DIR/build_mwebd.sh ;; + "cakewallet") $DIR/build_monero_all.sh && $DIR/build_haven.sh && $DIR/build_mwebd.sh && $DIR/build_decred.sh ;; "haven") $DIR/build_haven_all.sh ;; esac diff --git a/scripts/ios/build_decred.sh b/scripts/ios/build_decred.sh new file mode 100755 index 000000000..e6b13d0da --- /dev/null +++ b/scripts/ios/build_decred.sh @@ -0,0 +1,35 @@ +#!/bin/sh +set -e +. ./config.sh +LIBWALLET_PATH="${EXTERNAL_IOS_SOURCE_DIR}/libwallet" +LIBWALLET_URL="https://github.com/decred/libwallet.git" +LIBWALLET_VERSION="87b2769538db3065b334d247b25774593fc6443d" + +if [ -e $LIBWALLET_PATH ]; then + rm -fr $LIBWALLET_PATH +fi +mkdir -p $LIBWALLET_PATH +git clone $LIBWALLET_URL $LIBWALLET_PATH +cd $LIBWALLET_PATH +git checkout $LIBWALLET_VERSION + +SYSROOT=`xcrun --sdk iphoneos --show-sdk-path` +CLANG="clang -target arm64-apple-ios -isysroot ${SYSROOT}" +CLANGXX="clang++ -target arm64-apple-ios -isysroot ${SYSROOT}" + +if [ -e ./build ]; then + rm -fr ./build +fi +CGO_ENABLED=1 GOOS=ios GOARCH=arm64 CC=$CLANG CXX=$CLANGXX \ +go build -v -buildmode=c-archive -o ./build/libdcrwallet.a ./cgo || exit 1 + +CW_DECRED_DIR=${CW_ROOT}/cw_decred +HEADER_DIR=$CW_DECRED_DIR/lib/api +mv ${LIBWALLET_PATH}/build/libdcrwallet.h $HEADER_DIR + +DEST_LIB_DIR=${CW_DECRED_DIR}/ios/External/lib +mkdir -p $DEST_LIB_DIR +mv ${LIBWALLET_PATH}/build/libdcrwallet.a $DEST_LIB_DIR + +cd $CW_DECRED_DIR +dart run ffigen diff --git a/scripts/ios/build_zmq.sh b/scripts/ios/build_zmq.sh index e2fd7caae..d72f322a3 100755 --- a/scripts/ios/build_zmq.sh +++ b/scripts/ios/build_zmq.sh @@ -1,4 +1,4 @@ -g#!/bin/sh +#!/bin/sh . ./config.sh diff --git a/scripts/macos/app_config.sh b/scripts/macos/app_config.sh index bb4750803..ad1f04b3e 100755 --- a/scripts/macos/app_config.sh +++ b/scripts/macos/app_config.sh @@ -36,7 +36,7 @@ case $APP_MACOS_TYPE in $MONERO_COM) CONFIG_ARGS="--monero";; $CAKEWALLET) - CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana --tron --wownero";; #--haven + CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana --tron --wownero --decred";; #--haven esac cp -rf pubspec_description.yaml pubspec.yaml diff --git a/scripts/macos/build_all.sh b/scripts/macos/build_all.sh index 030617f7d..7929bb719 100755 --- a/scripts/macos/build_all.sh +++ b/scripts/macos/build_all.sh @@ -1,3 +1,3 @@ #!/bin/sh -./build_monero_all.sh universal \ No newline at end of file +./build_monero_all.sh universal && $DIR/build_decred.sh diff --git a/scripts/macos/build_boost_arm64.sh b/scripts/macos/build_boost_arm64.sh index 11f26040f..b5761b7a2 100755 --- a/scripts/macos/build_boost_arm64.sh +++ b/scripts/macos/build_boost_arm64.sh @@ -1,4 +1,5 @@ #!/bin/sh +. ./config.sh . ./build_boost_common.sh build_boost_arm64 \ No newline at end of file diff --git a/scripts/macos/build_boost_common.sh b/scripts/macos/build_boost_common.sh index 0c75be2bd..5aa17bf7c 100755 --- a/scripts/macos/build_boost_common.sh +++ b/scripts/macos/build_boost_common.sh @@ -35,6 +35,10 @@ BOOST_B2_LINKFLAGS_X86_64="-arch x86_64" BOOST_B2_BUILD_DIR_X86_64=macos-x86_64 build_boost_init_common() { + echo " + ============================ BOOST ============================ + " + CXXFLAGS=$1 CFLAGS=$2 LINKFLAGS=$3 @@ -157,8 +161,8 @@ build_boost_compile_universal() { build_boost_install_common() { ARCH=$1 LIB_DIR="" - mkdir $EXTERNAL_MACOS_LIB_DIR - mkdir $EXTERNAL_MACOS_INCLUDE_DIR + mkdir -p $EXTERNAL_MACOS_LIB_DIR + mkdir -p $EXTERNAL_MACOS_INCLUDE_DIR case $ARCH in arm64) LIB_DIR="${BOOST_B2_BUILD_DIR_ARM_64}/stage/lib";; diff --git a/scripts/macos/build_decred.sh b/scripts/macos/build_decred.sh new file mode 100755 index 000000000..d4b7d4d65 --- /dev/null +++ b/scripts/macos/build_decred.sh @@ -0,0 +1,34 @@ +#!/bin/sh + +. ./config.sh + +LIBWALLET_PATH="${EXTERNAL_MACOS_SOURCE_DIR}/libwallet" +LIBWALLET_URL="https://github.com/decred/libwallet.git" +LIBWALLET_VERSION="87b2769538db3065b334d247b25774593fc6443d" + +echo "======================= DECRED LIBWALLET =========================" + +echo "Cloning DECRED LIBWALLET from - $LIBWALLET_URL" +if [ -e $LIBWALLET_PATH ]; then + rm -fr $LIBWALLET_PATH +fi +mkdir -p $LIBWALLET_PATH +git clone $LIBWALLET_URL $LIBWALLET_PATH +cd $LIBWALLET_PATH +git checkout $LIBWALLET_VERSION + +if [ -e ./build ]; then + rm -fr ./build +fi +go build -buildmode=c-archive -o ./build/libdcrwallet.a ./cgo + +CW_DECRED_DIR=${CW_ROOT}/cw_decred +HEADER_DIR=$CW_DECRED_DIR/lib/api +mv ${LIBWALLET_PATH}/build/libdcrwallet.h $HEADER_DIR + +DEST_LIB_DIR=${CW_DECRED_DIR}/macos/External/lib +mkdir -p $DEST_LIB_DIR +mv ${LIBWALLET_PATH}/build/libdcrwallet.a $DEST_LIB_DIR + +cd $CW_DECRED_DIR +dart run ffigen diff --git a/scripts/macos/build_expat.sh b/scripts/macos/build_expat.sh index 0c5857907..8730e6992 100755 --- a/scripts/macos/build_expat.sh +++ b/scripts/macos/build_expat.sh @@ -6,6 +6,10 @@ EXPAT_VERSION=R_2_4_8 EXPAT_HASH="3bab6c09bbe8bf42d84b81563ddbcf4cca4be838" EXPAT_SRC_DIR=${EXTERNAL_MACOS_SOURCE_DIR}/libexpat +echo " +============================ EXPAT ============================ +" + git clone https://github.com/libexpat/libexpat.git -b ${EXPAT_VERSION} ${EXPAT_SRC_DIR} cd $EXPAT_SRC_DIR test `git rev-parse HEAD` = ${EXPAT_HASH} || exit 1 diff --git a/scripts/macos/build_monero.sh b/scripts/macos/build_monero.sh index 1af7ae0e3..a3b0381e3 100755 --- a/scripts/macos/build_monero.sh +++ b/scripts/macos/build_monero.sh @@ -11,6 +11,10 @@ DEST_LIB_DIR=${EXTERNAL_MACOS_LIB_DIR}/monero DEST_INCLUDE_DIR=${EXTERNAL_MACOS_INCLUDE_DIR}/monero ARCH=`uname -m` +echo " +============================ MONERO ============================ +" + echo "Cloning monero from - $MONERO_URL to - $MONERO_DIR_PATH" git clone $MONERO_URL $MONERO_DIR_PATH cd $MONERO_DIR_PATH diff --git a/scripts/macos/build_openssl_arm64.sh b/scripts/macos/build_openssl_arm64.sh index fd8d7b2f5..d320ef7fa 100755 --- a/scripts/macos/build_openssl_arm64.sh +++ b/scripts/macos/build_openssl_arm64.sh @@ -1,4 +1,5 @@ #!/bin/sh +. ./config.sh . ./build_openssl_common.sh build_openssl_arm64 \ No newline at end of file diff --git a/scripts/macos/build_openssl_common.sh b/scripts/macos/build_openssl_common.sh index 27cb1ef8c..1e4ac6f54 100755 --- a/scripts/macos/build_openssl_common.sh +++ b/scripts/macos/build_openssl_common.sh @@ -13,7 +13,9 @@ build_openssl_init_common() { # Use 1.1.1s because of https://github.com/openssl/openssl/issues/18720 OPENSSL_VERSION="1.1.1s" - echo "============================ OpenSSL ============================" + echo " + ============================ OPENSSL ============================ + " cd $EXTERNAL_MACOS_SOURCE_DIR curl -O https://www.openssl.org/source/openssl-$OPENSSL_VERSION.tar.gz diff --git a/scripts/macos/build_sodium.sh b/scripts/macos/build_sodium.sh index 19aad3c97..d754ce8e4 100755 --- a/scripts/macos/build_sodium.sh +++ b/scripts/macos/build_sodium.sh @@ -5,7 +5,9 @@ SODIUM_PATH="${EXTERNAL_MACOS_SOURCE_DIR}/libsodium" SODIUM_URL="https://github.com/jedisct1/libsodium.git" -echo "============================ SODIUM ============================" +echo " +============================ SODIUM ============================ +" echo "Cloning SODIUM from - $SODIUM_URL" git clone $SODIUM_URL $SODIUM_PATH --branch stable diff --git a/scripts/macos/build_unbound.sh b/scripts/macos/build_unbound.sh index ed115d464..6580ebc7c 100755 --- a/scripts/macos/build_unbound.sh +++ b/scripts/macos/build_unbound.sh @@ -7,7 +7,10 @@ UNBOUND_HASH="cbed768b8ff9bfcf11089a5f1699b7e5707f1ea5" UNBOUND_URL="https://www.nlnetlabs.nl/downloads/unbound/unbound-${UNBOUND_VERSION}.tar.gz" UNBOUND_DIR_PATH="${EXTERNAL_MACOS_SOURCE_DIR}/unbound-1.16.2" -echo "============================ Unbound ============================" +echo " +============================ Unbound ============================ +" + rm -rf ${UNBOUND_DIR_PATH} git clone https://github.com/NLnetLabs/unbound.git -b ${UNBOUND_VERSION} ${UNBOUND_DIR_PATH} cd $UNBOUND_DIR_PATH diff --git a/scripts/macos/build_zmq.sh b/scripts/macos/build_zmq.sh index dd5623f06..15c31f248 100755 --- a/scripts/macos/build_zmq.sh +++ b/scripts/macos/build_zmq.sh @@ -5,7 +5,9 @@ ZMQ_PATH="${EXTERNAL_MACOS_SOURCE_DIR}/libzmq" ZMQ_URL="https://github.com/zeromq/libzmq.git" -echo "============================ ZMQ ============================" +echo " +============================ ZMQ ============================ +" echo "Cloning ZMQ from - $ZMQ_URL" git clone $ZMQ_URL $ZMQ_PATH diff --git a/scripts/macos/gen.sh b/scripts/macos/gen.sh new file mode 100755 index 000000000..3d602c130 --- /dev/null +++ b/scripts/macos/gen.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +. ./gen_common.sh + +ARCH=`uname -m` +gen $ARCH \ No newline at end of file diff --git a/scripts/macos/gen_common.sh b/scripts/macos/gen_common.sh index 72ff638b6..95f74e748 100755 --- a/scripts/macos/gen_common.sh +++ b/scripts/macos/gen_common.sh @@ -15,6 +15,8 @@ gen_podspec() { gen_project() { ARCH=$1 CW_DIR="`pwd`/../../macos/Runner.xcodeproj" + BASE_FILENAME="project_base.pbxproj" + BASE_FILE_PATH="${CW_DIR}/${BASE_FILENAME}" DEFAULT_FILENAME="project.pbxproj" DEFAULT_FILE_PATH="${CW_DIR}/${DEFAULT_FILENAME}" universal_sed "s/ARCHS =.*/ARCHS = \"${ARCH}\";/g" $DEFAULT_FILE_PATH @@ -24,4 +26,4 @@ gen() { ARCH=$1 gen_podspec "${ARCH}" gen_project "${ARCH}" -} \ No newline at end of file +} diff --git a/tool/configure.dart b/tool/configure.dart index 259a5d9a0..d1c9326c8 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -11,6 +11,7 @@ const solanaOutputPath = 'lib/solana/solana.dart'; const tronOutputPath = 'lib/tron/tron.dart'; const wowneroOutputPath = 'lib/wownero/wownero.dart'; const zanoOutputPath = 'lib/zano/zano.dart'; +const decredOutputPath = 'lib/decred/decred.dart'; const walletTypesPath = 'lib/wallet_types.g.dart'; const secureStoragePath = 'lib/core/secure_storage.dart'; const pubspecDefaultPath = 'pubspec_default.yaml'; @@ -30,6 +31,7 @@ Future main(List args) async { final hasTron = args.contains('${prefix}tron'); final hasWownero = args.contains('${prefix}wownero'); final hasZano = args.contains('${prefix}zano'); + final hasDecred = args.contains('${prefix}decred'); final excludeFlutterSecureStorage = args.contains('${prefix}excludeFlutterSecureStorage'); await generateBitcoin(hasBitcoin); @@ -44,6 +46,7 @@ Future main(List args) async { await generateWownero(hasWownero); await generateZano(hasZano); // await generateBanano(hasEthereum); + await generateDecred(hasDecred); await generatePubspec( hasMonero: hasMonero, @@ -59,6 +62,7 @@ Future main(List args) async { hasTron: hasTron, hasWownero: hasWownero, hasZano: hasZano, + hasDecred: hasDecred, ); await generateWalletTypes( hasMonero: hasMonero, @@ -73,6 +77,7 @@ Future main(List args) async { hasTron: hasTron, hasWownero: hasWownero, hasZano: hasZano, + hasDecred: hasDecred, ); await injectSecureStorage(!excludeFlutterSecureStorage); } @@ -1093,12 +1098,15 @@ abstract class BitcoinCash { """; const bitcoinCashEmptyDefinition = 'BitcoinCash? bitcoinCash;\n'; - const bitcoinCashCWDefinition = 'BitcoinCash? bitcoinCash = CWBitcoinCash();\n'; + const bitcoinCashCWDefinition = + 'BitcoinCash? bitcoinCash = CWBitcoinCash();\n'; final output = '$bitcoinCashCommonHeaders\n' + (hasImplementation ? '$bitcoinCashCWHeaders\n' : '\n') + (hasImplementation ? '$bitcoinCashCwPart\n\n' : '\n') + - (hasImplementation ? bitcoinCashCWDefinition : bitcoinCashEmptyDefinition) + + (hasImplementation + ? bitcoinCashCWDefinition + : bitcoinCashEmptyDefinition) + '\n' + bitcoinCashContent; @@ -1233,7 +1241,8 @@ abstract class NanoUtil { """; const nanoEmptyDefinition = 'Nano? nano;\nNanoUtil? nanoUtil;\n'; - const nanoCWDefinition = 'Nano? nano = CWNano();\nNanoUtil? nanoUtil = CWNanoUtil();\n'; + const nanoCWDefinition = + 'Nano? nano = CWNano();\nNanoUtil? nanoUtil = CWNanoUtil();\n'; final output = '$nanoCommonHeaders\n' + (hasImplementation ? '$nanoCWHeaders\n' : '\n') + @@ -1482,6 +1491,85 @@ abstract class Zano { await outputFile.writeAsString(output); } +Future generateDecred(bool hasImplementation) async { + final outputFile = File(decredOutputPath); + const decredCommonHeaders = """ +import 'package:cw_core/wallet_credentials.dart'; +import 'package:cw_core/address_info.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/transaction_priority.dart'; +import 'package:cw_core/output_info.dart'; +import 'package:cw_core/wallet_service.dart'; +import 'package:cw_core/unspent_transaction_output.dart'; +import 'package:cw_core/unspent_coins_info.dart'; +import 'package:cake_wallet/view_model/send/output.dart'; +import 'package:hive/hive.dart'; +"""; + const decredCWHeaders = """ +import 'package:cw_decred/transaction_priority.dart'; +import 'package:cw_decred/wallet.dart'; +import 'package:cw_decred/wallet_service.dart'; +import 'package:cw_decred/wallet_creation_credentials.dart'; +import 'package:cw_decred/amount_format.dart'; +import 'package:cw_decred/transaction_credentials.dart'; +import 'package:cw_decred/mnemonic.dart'; +"""; + const decredCwPart = "part 'cw_decred.dart';"; + const decredContent = """ + +abstract class Decred { + WalletCredentials createDecredNewWalletCredentials( + {required String name, WalletInfo? walletInfo}); + WalletCredentials createDecredRestoreWalletFromSeedCredentials( + {required String name, required String mnemonic, required String password}); + WalletCredentials createDecredRestoreWalletFromPubkeyCredentials( + {required String name, required String pubkey, required String password}); + WalletService createDecredWalletService( + Box walletInfoSource, Box unspentCoinSource); + + List getTransactionPriorities(); + TransactionPriority getDecredTransactionPriorityMedium(); + TransactionPriority getDecredTransactionPrioritySlow(); + TransactionPriority deserializeDecredTransactionPriority(int raw); + + Object createDecredTransactionCredentials(List outputs, TransactionPriority priority); + + List getAddressInfos(Object wallet); + Future updateAddress(Object wallet, String address, String label); + Future generateNewAddress(Object wallet, String label); + + String formatterDecredAmountToString({required int amount}); + double formatterDecredAmountToDouble({required int amount}); + int formatterStringDoubleToDecredAmount(String amount); + + List getUnspents(Object wallet); + void updateUnspents(Object wallet); + + int heightByDate(DateTime date); + + List getDecredWordList(); + + String pubkey(Object wallet); +} +"""; + + const decredEmptyDefinition = 'Decred? decred;\n'; + const decredCWDefinition = 'Decred? decred = CWDecred();\n'; + + final output = '$decredCommonHeaders\n' + + (hasImplementation ? '$decredCWHeaders\n' : '\n') + + (hasImplementation ? '$decredCwPart\n\n' : '\n') + + (hasImplementation ? decredCWDefinition : decredEmptyDefinition) + + '\n' + + decredContent; + + if (outputFile.existsSync()) { + await outputFile.delete(); + } + + await outputFile.writeAsString(output); +} + Future generatePubspec({ required bool hasMonero, required bool hasBitcoin, @@ -1496,6 +1584,7 @@ Future generatePubspec({ required bool hasTron, required bool hasWownero, required bool hasZano, + required bool hasDecred, }) async { const cwCore = """ cw_core: @@ -1564,6 +1653,10 @@ Future generatePubspec({ cw_zano: path: ./cw_zano """; + const cwDecred = """ + cw_decred: + path: ./cw_decred + """; final inputFile = File(pubspecOutputPath); final inputText = await inputFile.readAsString(); final inputLines = inputText.split('\n'); @@ -1613,6 +1706,10 @@ Future generatePubspec({ output += '\n$cwSharedExternal\n$cwHaven'; } + if (hasDecred) { + output += '\n$cwDecred'; + } + if (hasFlutterSecureStorage) { output += '\n$flutterSecureStorage\n'; } @@ -1654,6 +1751,7 @@ Future generateWalletTypes({ required bool hasTron, required bool hasWownero, required bool hasZano, + required bool hasDecred, }) async { final walletTypesFile = File(walletTypesPath); @@ -1709,6 +1807,10 @@ Future generateWalletTypes({ outputContent += '\tWalletType.banano,\n'; } + if (hasDecred) { + outputContent += '\tWalletType.decred,\n'; + } + if (hasWownero) { outputContent += '\tWalletType.wownero,\n'; }